184 lines · 8.6 KB
import { env } from "cloudflare:workers";
import { all, batch, chunks, placeholders, run, stmt } from "../db";
import { repoDb } from "../scoped-db";
import { siteScopesFrom } from "./store";
import { manifestKey, objectKey, SITE_LIMITS } from "./config";
import type { SiteManifest } from "./config";
import { notifyUsers } from "../notify/deliver";
import { publicUrl } from "../events/emit";

/**
 * Mark-and-sweep for the content-addressed Sites objects under `pages/objects/`. A run marks every
 * hash a retained manifest references (active, superseded within the history limit, previews and commit previews),
 * then deletes unmarked objects uploaded more than GRACE_MS before the run started. Deploys refresh any object they
 * reuse that is older than REFRESH_MS (collect.ts), so an object a deploy in flight relies on is never old enough
 * to be swept. State lives in KV, so each cron tick does a bounded step and the next one resumes.
 */
export const GC_GRACE_MS = 7 * 24 * 60 * 60 * 1000;
const RUN_INTERVAL_MS = 24 * 60 * 60 * 1000;
const RETRY_MS = 60 * 60 * 1000;
const LOCK = "pages-gc";
const LOCK_MS = 4 * 60 * 1000;
/** A retained deployment whose manifest has been gone this long is an orphan, skipped instead of blocking GC. */
const ORPHAN_MS = 7 * 24 * 60 * 60 * 1000;
const STATE_KEY = "pages:gc:state";
const PREFIX = "pages/objects/";
const MARK_PAGE = 50;
/** Bounds R2 work per step (one list, a head per candidate, one delete) to stay far below the subrequest limit. */
const LIST_PAGE = 100;
const HEAD_BATCH = 50;
const STEPS_PER_TICK = 5;
const INSERT_ROWS = 80;

type GcState =
  | { phase: "idle"; nextAt: number }
  | { phase: "clear" | "mark"; startedAt: number; cursor: string }
  | { phase: "sweep"; startedAt: number; cursor: string | null; deleted: number };

const RETAINED_SQL = `SELECT id, status, created_at FROM (
    SELECT id, status, created_at FROM (
      SELECT id, status, created_at, ROW_NUMBER() OVER (PARTITION BY site_id ORDER BY created_at DESC) AS n