perf(enrichment): poll long-stuck custody deferrals hourly
Summary
- perf(enrichment): poll long-stuck custody deferrals hourly
The enrichment outbox (kind=sync_reprocess on /v2/sync-jobs/run) re-arms a callback custody deferral every
30ā120s with no bound. Four reasons can outlive the obligation: redirect_unresolved, conversation_missing,
open_live_target and pending_live_bridge. Once one of those reasons has deferred a row continuously for 6h,
the row polls hourly (custody_defer_until). The outbox records deferred_since for the current reason. A
different reason or a completed callback starts a new episode, and an episode never predates the obligation's
first_pending_at, so a reopened dormant row doesn't inherit an old one. Fresh deferrals and every other reason
keep their cadence. Custody, sweep repair and the eventual resume or redirect are unchanged. No row is retired or
discarded.
Scope: this covers the sync-handler outbox (outbox.run ā defer), which is the serving route
(ENRICHMENT_ROUTE is unset on backend-sync). The C3 consumer (/v2/enrichment/run) is unchanged: its store
records the consumer's own not_due reason, so it needs its own episode field before the route flips. Existing
stuck rows have no deferred_since, so they back off 6h after deploy.
Why (prod measurements 2026-11-01, ro-prod, aggregates only)
- About 80% of backend-sync
/v2/sync-jobs/run200s carry the 21-byte{"status":"deferred"}body: ~53k
deliveries/h at a mean of 2.39s, roughly a third of the service's request wall. Measured from the request-log
census by status and responseSize, 16:10ā16:20Z. - Open outbox rows by
deferred_reason(Firestore COUNT):redirect_unresolved1,625,pending_live_bridge
123,open_live_target83,conversation_missing12. - Ages from a projection histogram of
first_pending_at:redirect_unresolved: 98% are older than 6h and 67% are 7ā30 days old; 95% are due within 150s.pending_live_bridge: 69% are older than 6h.
- At the old cadence that is ~49k + ~15k + ~2.5k polls/h. Each poll costs a cohort warm, two outbox CAS writes,
resolver reads and a new Cloud Task.
Delivery reduction (hermetic)
test_stuck_redirect_deliveries_per_day_drop_from_719_to_198 drives a stuck redirect through the real
outbox.run for 24h of fake time. It makes 719 deliveries before the change and 198 after. The first 181
deliveries (the 6h grace) are identical. After the grace, the largest gap between deliveries is one poll interval
(3600s).
Worst-case added delay: one hour, and only for a row that one reason has deferred continuously for more than 6h.
That covers a redirect that resolves late, or a live target or bridge that settles after 6h.
Review round 1 (Luna, FIX-FIRST)
- P2 "an old obligation delays a new bridge by up to an hour": fixed. The grace is now per-reason
deferred_since, notfirst_pending_at. Seetest_new_deferral_reason_on_an_old_obligation_restarts_the_grace
andtest_completed_callback_ends_the_deferral_episode. - P2 "the
sync_holdloop is unchanged": not a live loop. That was an early hypothesis in the context file, and
measurement then disproved it. Open outbox rows witheffects_only=true: 0. Open rows with
deferred_reason=sync_hold: 0 (Firestore COUNT, 16:45Z). The measured loops are the four reasons covered here.
Changingsync_holdwould add risk with no measured saving.
Lane / contract
Lane syncdiet (two-lane rollout, margin50). Edits fswrites' outbox area: coordinated on the bus (msgs 25ebd3,
1d75c9). The change touches only the defer horizon, not the CAS mechanics.
Product invariants affected
none
Verification
backend/test.shwithBACKEND_UNIT_TEST_FILE_LIST: test_sync_enrichment_custody, test_enrichment_route,
test_outbox_firestore, test_sync_pending_live_bridge, test_sync_reprocess_coalesce,
test_enrichment_callback_parity and test_sync_processing_hold all pass (unit layer).backend/testing/sync_cloud_tasks_stack/run.sh --only syncpasses: the hermetic emulator gauntlet.- Not measured: live savings. These need a deploy to backend-sync. Expect the size-121 share of sync-jobs/run to
fall from ~80% toward ~5%.
Rollback
Revert this commit. Or raise STUCK_DEFER_GRACE_SECONDS in a hotfix. No flag; no data or config changes.
Failure-Class: none
š¤ Generated with Claude Code