|
| 1 | +# Durable endpoint reconciliation |
| 2 | + |
| 3 | +`delivery_queue` owns endpoint retry state. Queue messages are at-least-once |
| 4 | +wakeups; their optional `reconcileAttempt` cannot grant or reset a retry budget. |
| 5 | +New endpoint materialization starts at generation zero. A delivery series has |
| 6 | +the existing eight-attempt budget. After retry exhaustion, one conditional |
| 7 | +update records `dead_letter`, its error, a six-hour `next_attempt_at` due time, |
| 8 | +and `dlq_pending = 1` when its generation is below five. Generation five is |
| 9 | +terminal: no additional reconciliation intent is created. |
| 10 | + |
| 11 | +The structured DLQ notification remains useful for diagnostics and a delayed |
| 12 | +reconcile wakeup. Failed publication can be retried by an incoming endpoint |
| 13 | +wakeup without another HTTP POST. Its successful send does not consume the |
| 14 | +repair intent. In particular, the local in-memory Queue can accept a message |
| 15 | +and lose it on process restart. Independently, cron/local-startup endpoint |
| 16 | +sweeping scans up to 50 due repair intents and republishes their exact successor |
| 17 | +to MAIN. This scan is separate from the up-to-500 active endpoint scan. Sweeping |
| 18 | +retains the intent even when the send succeeds or throws; if MAIN automatically |
| 19 | +dead-letters or loses that wakeup, a later sweep can recover it again. |
| 20 | + |
| 21 | +The reconcile worker consumes the intent with one conditional update requiring |
| 22 | +`dead_letter`, pending intent, the exact predecessor generation, and the due time |
| 23 | +having elapsed. It increments once, resets attempts and claim ownership, clears |
| 24 | +the intent, and marks the row pending. Duplicate, older, future, permanently |
| 25 | +failed, delivered or deleted jobs cannot reset attempts or resurrect a row. An |
| 26 | +early valid wakeup is deferred; invalid durable timestamps require operator |
| 27 | +repair. A failed or lost post-transition send leaves a pending row that the |
| 28 | +ordinary sweep recovers at the persisted generation. Terminal retention excludes |
| 29 | +unconsumed intents in both its selection and conditional delete. |
| 30 | + |
| 31 | +Delivery admission reads the persisted generation and conditionally claims the |
| 32 | +same generation, endpoint, Activity and due job with a new random processing |
| 33 | +token. Every post-claim state write checks the token, generation and processing |
| 34 | +status, so an older claimant cannot overwrite a replacement claim. This does |
| 35 | +not promise exactly-once remote delivery: an HTTP acceptance followed by failed |
| 36 | +bookkeeping or a crashed worker can still leave ambiguous remote acceptance. |
| 37 | +Activity IDs remain the remote deduplication identity. HTTP success is ACKed |
| 38 | +without an immediate repeat POST even when bookkeeping throws. |
| 39 | + |
| 40 | +## Migration and recovery |
| 41 | + |
| 42 | +Apply migration `0033_delivery_reconciliation.sql` before serving this engine, |
| 43 | +with delivery writers/consumers closed during the transition. It adds columns |
| 44 | +and an index without rebuilding tables or removing history or recipients. |
| 45 | +The old engine persisted reconciliation history only in Queue messages. Existing |
| 46 | +rows therefore receive generation five and no intent; existing statuses and |
| 47 | +attempts stay intact. Active legacy rows can finish their current delivery |
| 48 | +series, but receive no additional reconcile cycles. Older in-flight messages |
| 49 | +cannot lower that conservative value. New producer inserts explicitly choose |
| 50 | +zero; conflicts do not reset a pre-existing budget. The SQL and Drizzle defaults |
| 51 | +both remain five so omitted-generation writers do not silently mint a new budget. |
| 52 | + |
| 53 | +This availability tradeoff avoids claiming an exact recovered generation from |
| 54 | +unavailable history. Historical dead-letter rows and dropped pre-migration DLQ |
| 55 | +notifications require exact operator-data audit and adjudication; this migration |
| 56 | +does not revive them. No automatic backfill infers their intent. Snapshot the |
| 57 | +operator-owned data and inspect pending/terminal jobs before application. Do not |
| 58 | +overlap old consumers: they ignore durable generations and can unconditionally |
| 59 | +revive rows. New code requires the columns. Old code can read the additive schema |
| 60 | +but ignores its bounds and intents, so code rollback alone does not preserve this |
| 61 | +contract. Use a compatible forward repair, or restore a qualified pre-mutation |
| 62 | +snapshot with writers closed and explicitly account for intervening deliveries. |
| 63 | + |
| 64 | +Portable tests model bounded D1 statements; disposable actual-source D1/workerd |
| 65 | +checks qualify SQL/CAS semantics separately. Neither qualifies real Queue |
| 66 | +delivery, federation, cron, monitoring, production migration or operator recovery. |
0 commit comments