From 7cfb4447c4e2c6b067b85bbec8b53ce6bdd14f76 Mon Sep 17 00:00:00 2001 From: Tom Scanlan Date: Thu, 9 Jul 2026 10:39:17 -0400 Subject: [PATCH] docs(cron,search): correct comments the arming-in-race move made false MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moving sink arming inside the cron ingest race invalidated three comments that now asserted the opposite of the code (this ships to a public upstream): - cron/+server.ts: dropped the claim that racing arming+ingest keeps "the whole tick" under the interval. The race bounds only the raced section (arming + ingest); the tick also spans the un-raced bot/notify/drip stages. State that the bound guards the search-endpoint hazard, not the tick's total duration. - meili-sink.ts (SETTINGS_TIMEOUT_MS doc): the arming fetch no longer runs "before the ingest race" — it runs inside it. Describe the current topology: the bound makes arming self-terminate within the shared budget, and also backs the xrpc arming path (outside any race). Also correct the false "upsert/remove only run inside contrail.ingest under the cron race": upsert is also reached from the geocode drip (after the race) and xrpc notify re-ingests (no race), so those write paths stay unbounded — a pre-existing exposure. - meili-sink.ts (applyMeiliSettings doc): "the arming path runs outside the cron ingest race" is now false for cron; distinguish cron (inside the race) from xrpc (outside any race). Comments only; no logic change. check: 0 errors / 7 pre-existing warnings; test: 283 passed / 4 skipped (unchanged). --- apps/web/src/lib/search/server/meili-sink.ts | 26 +++++++++++++------- apps/web/src/routes/api/cron/+server.ts | 5 +++- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/apps/web/src/lib/search/server/meili-sink.ts b/apps/web/src/lib/search/server/meili-sink.ts index 2e39434..38318aa 100644 --- a/apps/web/src/lib/search/server/meili-sink.ts +++ b/apps/web/src/lib/search/server/meili-sink.ts @@ -28,13 +28,20 @@ type RecordEvent = Parameters[0][number]; export const EVENT_COLLECTION = 'community.lexicon.calendar.event'; /** How long the settings PATCH (which also arms/creates the index) may run - * before we abort it. This request fires on the ensureInit arming path — and - * for the cron handler that runs BEFORE the ingest hard-timeout race, so an - * unbounded fetch to a *stalling* (not erroring) Meili endpoint would hang the - * whole tick past the 55s backstop and starve notify/drip. A short bound - * degrades that to "sink disabled this cycle" (caught by ensureInit's - * try/catch), retried next tick. Upsert/remove stay unbounded here: they only - * run inside contrail.ingest, already under the cron race. */ + * before we abort it. This request fires on the ensureInit arming path. On the + * cron handler that path now runs INSIDE the ingest hard-timeout race, ahead of + * ingest, so the bound is what makes arming self-terminate within the shared + * budget instead of consuming it: without it, a *stalling* (not erroring) Meili + * endpoint would eat the whole race budget and starve the ingest it runs ahead + * of. The same bound also covers the user-facing xrpc arming path, which is NOT + * inside any race and would otherwise hang the request unboundedly. A short + * bound degrades either case to "sink disabled this cycle" (caught by + * ensureInit's try/catch), retried next tick. + * Upsert/remove are NOT bounded here — they pass no AbortSignal. They run under + * the cron race (via contrail.ingest) but ALSO outside it: MeiliEventIndex.upsert + * is reached from the geocode drip (runGeocodeDrip, after the race) and from + * xrpc notify re-ingests (no race at all), so a stalling Meili can still hang + * those write paths. Pre-existing exposure, tracked separately. */ const SETTINGS_TIMEOUT_MS = 8_000; export interface MeiliSinkBackend { @@ -157,8 +164,9 @@ export class MeiliEventIndex { /** PATCH the index settings (and auto-create the index) for a backend. Call * once per worker before the sink starts upserting so the read path's filters * resolve. Bounded by SETTINGS_TIMEOUT_MS (override for tests) so a stalling - * Meili endpoint can't hang the caller — the arming path runs outside the cron - * ingest race. */ + * Meili endpoint can't hang the caller — on cron the arming path runs inside the + * ingest race and must self-terminate within that shared budget; on xrpc it runs + * outside any race, where this bound is the only backstop. */ export async function applyMeiliSettings( backend: MeiliSinkBackend, fetchFn?: typeof fetch, diff --git a/apps/web/src/routes/api/cron/+server.ts b/apps/web/src/routes/api/cron/+server.ts index d1bf03a..9576e6b 100644 --- a/apps/web/src/routes/api/cron/+server.ts +++ b/apps/web/src/routes/api/cron/+server.ts @@ -45,7 +45,10 @@ export const POST: RequestHandler = async ({ request, platform }) => { // 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. + // ingest together bounds the RACED SECTION by HARD, so a stalling search + // endpoint can no longer push that section past the interval. (The whole + // tick also spans the un-raced stages below — bot/notify/drip — so this + // bounds the search-endpoint hazard, not the tick's total duration.) const DRAIN_TIMEOUT_MS = 20_000; const HARD_TIMEOUT_MS = 55_000; await Promise.race([ -- 2.51.2