From 0198233e8df33e716789a997e3a09bf5ddcb2620 Mon Sep 17 00:00:00 2001 From: Tom Scanlan Date: Thu, 9 Jul 2026 10:24:48 -0400 Subject: [PATCH] fix(cron): race sink arming inside the ingest backstop Arming (ensureInit) was awaited before the Promise.race hard backstop, so its bounded settings fetch was additive to the 55s ingest budget: a stalling search endpoint could stretch a tick to ~63s, past the 60s cron interval, overlapping the next tick and breaking the documented "ticks don't overlap" invariant. Move arming inside the race, ahead of ingest, so arming and ingest share ONE budget bounded by HARD_TIMEOUT_MS. Ordering is preserved (the sink must be armed before ingest upserts). Widen the failure log to cover the arming step now that it runs inside the guarded block. --- apps/web/src/routes/api/cron/+server.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/web/src/routes/api/cron/+server.ts b/apps/web/src/routes/api/cron/+server.ts index 6308d67..d1bf03a 100644 --- a/apps/web/src/routes/api/cron/+server.ts +++ b/apps/web/src/routes/api/cron/+server.ts @@ -25,7 +25,6 @@ export const POST: RequestHandler = async ({ request, platform }) => { // Firehose ingest — guarded so an over-budget cycle can't 500 the whole tick // (which previously also took the bot down with it). try { - await ensureInit(db, platform!.env); // Two deliberately-split budgets. contrail saves the jetstream cursor LAST in // runIngestCycle — after the drain, applyEvents, and the per-DID // refreshStaleIdentities network tail. If this handler aborts before that @@ -40,16 +39,26 @@ export const POST: RequestHandler = async ({ request, platform }) => { // normal cycle, and under the cron interval so ticks don't overlap. // Scheduled invocations get ~15min wall-clock and this is I/O-bound, // so the tail has ample room. + // The HARD budget covers ARMING too: ensureInit runs INSIDE the race, ahead + // of ingest (the sink must be armed before ingest upserts — see the ordering + // rationale in $lib/contrail/index.ts). ensureInit's own settings fetch is + // separately time-bounded, but that bound would be ADDITIVE to HARD if arming + // were awaited before the race — a stalling search endpoint could then push a + // tick past the cron interval and overlap the next one. Racing arming + + // ingest together keeps the whole tick under HARD, and thus under the interval. const DRAIN_TIMEOUT_MS = 20_000; const HARD_TIMEOUT_MS = 55_000; await Promise.race([ - contrail.ingest({ timeoutMs: DRAIN_TIMEOUT_MS }, db), + (async () => { + await ensureInit(db, platform!.env); + await contrail.ingest({ timeoutMs: DRAIN_TIMEOUT_MS }, db); + })(), new Promise((_, reject) => setTimeout(() => reject(new Error('ingest hard timeout')), HARD_TIMEOUT_MS) ) ]); } catch (e) { - console.error('[cron] contrail.ingest failed:', e); + console.error('[cron] ingest cycle failed:', e); } // atmo.pub notifications: event reminders + host RSVP alerts. Runs after -- 2.51.2