90 lines · 3.5 KB
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:<name>`): 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<string>();

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<boolean> {
  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<void>): Promise<boolean> {
  if (await backfillDone(name)) return true;
  const started = Date.now();
  let visits = 0;