45 lines · 2.0 KB
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") {