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<boolean> {
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<void> {
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") {