import { env } from "cloudflare:workers"; import { all } from "./db"; import { forEachScope } from "./account-scope"; /** * One-shot data backfills that walk every repository once, a page per cron run, so new indexes can cover rows * written before they existed. Progress lives in KV (`backfill:`): the last repository id visited, or * DONE. Readers that need the index consult `backfillDone` and keep their old (slower) path until it is. */ const DONE = "done"; const PAGE = 100; /** Keep paging within one cron run while there is time left (the run is shared with other tasks). */ const BUDGET_MS = 20_000; const CONCURRENCY = 8; const MAX_VISITS_PER_RUN = 500; const finished = new Set(); function key(name: string): string { return `backfill:${name}`; } /** False until the backfill has finished, and whenever its state can't be read (callers keep their safe path). */ export async function backfillDone(name: string): Promise { if (finished.has(name)) return true; const state = await Promise.resolve() .then(() => env.KV.get(key(name))) .catch(() => null); if (state === DONE) finished.add(name); return state === DONE; } /** * Visits pages until done, out of time, or out of visits for this run (DO calls count toward the invocation's * subrequest limit). Visits must be idempotent: a page with a failed visit saves the cursor just before the first * failure and stops, so the next run retries from there, and DONE is only written once every id was visited. */ async function walk(name: string, table: "repos" | "accounts", visit: (id: string) => Promise): Promise { if (await backfillDone(name)) return true; const started = Date.now(); let visits = 0; let cursor = (await env.KV.get(key(name))) ?? ""; while (Date.now() - started < BUDGET_MS && visits < MAX_VISITS_PER_RUN) { const rows = await all<{ id: string }>(`SELECT id FROM ${table} WHERE id > ? ORDER BY id LIMIT ?`, cursor, PAGE); const ids = rows.map((row) => row.id); visits += ids.length; const failed = new Set(); await forEachScope( ids, (id) => visit(id).catch((error: unknown) => { failed.add(id); console.error("Backfill visit failed", name, id, error); }), CONCURRENCY, ); const firstFailure = ids.findIndex((id) => failed.has(id)); if (firstFailure !== -1) { if (firstFailure > 0) await env.KV.put(key(name), ids[firstFailure - 1] ?? cursor); return false; } if (ids.length < PAGE) { await env.KV.put(key(name), DONE); finished.add(name); return true; } cursor = ids.at(-1) ?? cursor; await env.KV.put(key(name), cursor); } return false; } /** Visits the next page of repositories in id order; call from the cron until it reports done. */ export function backfillRepos(name: string, visit: (repoId: string) => Promise): Promise { return walk(name, "repos", visit); } /** Visits the next page of accounts (users and organizations) in id order. */ export function backfillAccounts(name: string, visit: (accountId: string) => Promise): Promise { return walk(name, "accounts", visit); } /** A backfill that is one bounded step (a short list): runs until it succeeds once. */ export async function backfillOnce(name: string, work: () => Promise): Promise { if (await backfillDone(name)) return true; await work(); await env.KV.put(key(name), DONE); finished.add(name); return true; }