import { env } from "cloudflare:workers"; import { all } from "./db"; import { loadRepoById } from "./repos"; import { refreshRepoStatus } from "./repo-ops"; import { markProvisioningFailed } from "./repo-provisioning"; /** Longer than a provisioning Workflow's own polling budget, so live instances always finish first. */ const STALE_AFTER_MS = 40 * 60 * 1000; /** Past this age a Workflow that still reports itself live is assumed lost (local dev restarts report stale state). */ const DEAD_AFTER_MS = 90 * 60 * 1000; const SWEEP_LIMIT = 20; const LIVE_STATES = new Set(["queued", "running", "waiting", "paused", "waitingForPause"]); async function workflowIsLive(repoId: string): Promise { try { const instance = await env.REPO_PROVISION.get(`provision-${repoId}`); const { status } = await instance.status(); return LIVE_STATES.has(status); } catch { return false; } } /** * Cron: imports and forks whose provisioning Workflow was lost (deploy, eviction, dev restart) would sit in * "importing"/"forking" until a page view happened to refresh them. Refresh each stale one once; if it is * still not ready and no Workflow instance is alive, mark it failed so its owner can delete and retry. */ export async function sweepStuckProvisioning(): Promise { const stuck = await all<{ id: string; created_at: number }>( `SELECT id, created_at FROM repos WHERE status IN ('importing', 'forking') AND created_at < ? ORDER BY created_at LIMIT ?`, Date.now() - STALE_AFTER_MS, SWEEP_LIMIT, ); for (const { id, created_at: createdAt } of stuck) { if (Date.now() - createdAt < DEAD_AFTER_MS && (await workflowIsLive(id))) continue; const repo = await loadRepoById(id); if (!repo) continue; const refreshed = await refreshRepoStatus(repo).catch(() => repo); if (refreshed.status === "importing" || refreshed.status === "forking") { await markProvisioningFailed(id, "provisioning was interrupted; delete the repository and try again"); } } }