DLQ Forensic Durability & Idempotency (epic &75)
On this page
- Status
- Context — why #1181 became an epic
- The core decision (approved)
- C1 design (this MR)
- C2 design (#1181) — approved v2, 2026-07-29
- C4 design (#1198) — approved v2, 2026-07-29
- C3 design (#1197) — approved v2, 2026-07-29
- C5 design (#1199) — approved 2026-07-29
- Addendum design (#1203) — approved v2, 2026-07-29
- Addendum design (#1202) — approved v2, 2026-07-30
- Out of scope (routed)
Status
| Child | Description | Status |
|---|---|---|
C1 (#1196) |
Thread real consumer-queue identity to the dead-letter path ( |
Done (2026-07-28) — MR !1098 merged (f45fd445); RED-proofed routing test + broker-path pin; per-service |
C2 (#1181) |
Per-occurrence dead-letter idempotency: |
Done (2026-07-29) — MR !1100 merged (2cc7116a); RED-proofed twice (U1a stash pin, U1b 8/13→13/13); both code units J-reviewed, findings remediated pre-commit |
C3 (#1197) |
Bounded DLQ retry with durable capture: MQ error taxonomy ( |
Done (2026-07-29) — MR !1103 merged (4a65933e); 17-test live matrix incl. the carried-identity dedup pin; all three code units J-reviewed, findings remediated pre-commit |
C4 (#1198) |
Pin queue-type & delivery-limit policy (ADR-003 amendment) + mechanical enforcement — see § C4 design below |
Done (2026-07-29) — MR !1101 merged (9ad8f702); 7-test live matrix (2 RED, 5 honest pins); mq-topology lint; both code units J-reviewed, findings remediated pre-commit |
C5 (#1199) |
DLQ observability (backlog/parking/quarantine depth gauges, capture + outcome counters), triage runbook, pre-C2 false-alert disclosure — see § C5 design below |
Done (2026-07-29) — MR !1104 merged (42b9d153); the §D7 launch-requirement gauges + outcome counters landed with the triage runbook and the ADR-022 disclosure amendment |
Addendum (#1203) |
Handler-panic containment: per-delivery task boundary ( |
Done (2026-07-30) — MR !1105 merged (f3d4922e); three observed behavioral REDs (events/DLQ/inbox), all four units J-reviewed, findings remediated pre-commit; inbox-invariant scoping honesty routed to #1209 |
Addendum (#1202) |
Per-service broker credentials + least-privilege permissions + operator-owned exchange topology (closes the configure-includes-delete forensic-suppression hole) — see § Addendum design (#1202) below |
Done (2026-07-30) — MR !1106 merged (ba88360d); four units + the F-054 pre-push fix, five J-reviews, two observed REDs; follow-ups #1210/#1211 |
Epic: &75 (child of &74)
Issues: #1196 (C1), #1181 (C2), #1197 (C3), #1198 (C4), #1199 (C5) — C2–C5 carry
Plan::DLQ-FORENSIC; addenda #1202 (broker permission separation, from the C4 scope decision)
and #1203 (handler-panic supervisor death, from the C4 design review); routed out of C5:
#1205 (operator alert routing), #1206 (quarantine drain/replay tooling — the declined optional
scope), #1207 (opentelemetry-observability plan-page reconcile)
Review state: v1 single-MR plan REJECTED by external stop-ship review (2026-07-28); every finding
verified against source in six Explore passes; epic decomposition + the per-occurrence-token core
approved 2026-07-28. C1 is detailed here (ships with this plan); C2–C5 get their own just-in-time
plans, each through plan review (C2 v2 approved 2026-07-29 after a ~20-finding review; C4 v2
approved 2026-07-29 after a ~28-finding review; C3 v2 approved 2026-07-29 after a 5-stop-ship
review that replaced the broker TTL+DLX return hop with the client-driven parking replayer;
C5 approved 2026-07-29 first-pass after the internal adversarial review).
Context — why #1181 became an epic
The original issue asked for (W1f) fixing the DLQ consumer’s log-and-ack (forensic loss on DB
outage) and (W1g) a (envelope_id, original_queue) dedup key on dead_letter_audit. Five
verified facts make that un-implementable as written (anchors at 4b9e3c35):
-
dead_letter_auditis ADR-058 audit-class — "Never hard-deleted while unarchived" (ADR-058 §Audit class); the only sanctioned delete is the archive-then-prune engine. A dedup-backfillDELETEis illegal, and the engine cannot reconcile individual duplicates. AUNIQUE … ON CONFLICT DO NOTHINGno-op insert, however, is compatible with the insert-only invariant. -
The proposed key is incoherent. The broker DLX path stamps
dlq.<consumer-queue>(subscriber.rs:124); the inbox retry-cap path stamps the producersource_service(inbox.rs:317→publisher.rs:125). Same failure, different key; two distinct consumer failures of one fan-out event collapse to one key on the app path. The real consumer-queue name is dropped at thehandler(envelope)boundary (subscriber.rs:455). -
A unique
(envelope_id, queue)key erases evidence. The #1156 threshold counts raw rows per event_type/hour; a phantom ack-failure redelivery and a genuine later re-dead-lettering carry identical(envelope_id, queue)— the key cannot distinguish them, so it would suppress real occurrences. -
W1f needs real machinery. A count-cap-then-ack re-creates the loss; unbounded requeue head-of-line blocks and silently depends on classic-queue-no-delivery-limit semantics that are enforced nowhere (no
x-queue-type, no policy, no ADR-003 commitment). Ack/nack failures today warn-and-continue, leaking prefetch credit (PREFETCH 16) until the consumer stalls. -
False alerts are non-retractable. A pre-fix inflated
count==11alert leaves only anevent_outboxrow + anaudit_logrow — nosecurity_alertsrow, no retraction path. Disclosure (C5), not reconciliation.
The core decision (approved)
Idempotency is a property of an occurrence, keyed on a per-occurrence token minted at
dead-lettering time — stable across redelivery, distinct across genuine re-occurrences.
dead_letter_audit stays the single append-only source of truth: nullable occurrence_token
partial unique index (WHERE occurrence_token IS NOT NULL), insert via
ON CONFLICT (occurrence_token) DO NOTHING RETURNING → Option (None = phantom redelivery →
short-circuit *before the advisory lock/count/alert, making the threshold structurally immune to
redelivery). Zero backfill/UPDATE/DELETE; pre-existing rows keep NULL tokens (historical).
Threshold counting stays on the log — no projection table. Token derivation (application-
controlled dead-lettering vs broker x-death-derived) is resolved in C2’s plan + ADR.
C1 design (this MR)
| # | Decision | Substance |
|---|---|---|
D1 |
Queue identity threaded, no |
|
D2 |
All consumer call sites updated in the same diff |
Every |
D3 |
|
It remains in the |
D4 |
Docs |
ADR-022 amendment (queue-identity threading + the incoherence it retires);
|
C1 tests
-
Live-broker (devstack) RED-provable: drive an envelope through
handle_idempotentlyto the retry cap → assert thedead_letter_auditrow lands withoriginal_queue = "<consumer>.events"(RED today: lands as the producersource_service). -
Broker-path regression pin: a nack-without-requeue dead-letter records the consumer queue (already true — pinned so C1 can’t regress it).
-
Unit:
publish_dlxwrapper still carriessource_serviceas provenance metadata.
C2 design (#1181) — approved v2, 2026-07-29
The token-derivation fork is RESOLVED as derived tokens, no topology change (the epic’s tentatively-preferred application-controlled-DLX rework proved unnecessary; no ADR-003 amendment). v1 of this design was rejected by external review (~20 findings, 10 stop-ship); the v2 rules below carry those remediations (R-numbers cross-reference the review record on MR !1100, merged 2cc7116a, and the ADR-022 amendment).
Occurrence identity
| Source | Rule |
|---|---|
App path ( |
Token derived DETERMINISTICALLY from durable inbox state in |
Broker path ( |
|
Precedence |
Valid x-death FIRST, then valid wrapper, else tokenless (R2 — a replayed wrapped envelope re-dead-lettered by the broker must record as a NEW occurrence). |
Validation |
|
Conflict handling |
|
Threshold window |
|
Accepted bounds (also in #1181’s superseding-AC language + the ADR amendment)
-
Dual-path residue: repeated DLX-publish failure can broker-dead-letter the original delivery too — one terminal failure may record both a
dlxcap:and anxdeath:row (correlated by envelope + queue). Producer-side structural unification re-routed to #1053 by the C3 design decision (2026-07-29 — #1053 owns the retry-ladder redesign the dual ingest depends on; C3’s disposition tier is the single authoritative CONSUMER-side path). -
Same-second fresh-history replay collision (AMQP 1-second timestamps) — real limitation, disclosed (R4).
-
Dedup memory = the audit hot window; post-archive redelivery re-records (R9).
-
Tokenless (raw/foreign) publishes keep the pre-C2 duplicate-row behavior — occurrence idempotency is claimed for CRAIG-produced dead letters only (R9).
-
Token forgery by a write-only-credentialed publisher is bounded by R6+R7: a forged token cannot suppress a divergent genuine record (quarantine); suppressing an identical record is a no-op. Broker permission separation routed to #1202 (originally #1198; re-routed by the C4 scope decision, 2026-07-29).
C4 design (#1198) — approved v2, 2026-07-29
v1 was rejected by external review (~28 findings, 8 blocking); the v2 decisions below carry those remediations (the full review record lands as a note on the C4 MR).
The contract
| Decision | Substance |
|---|---|
Queue type |
ALL CRAIG-declared queues are classic with explicit |
Delivery limit |
Delivery limits are a quorum-queue feature; classic queues do not implement them. Type enforcement transitively neutralizes delivery-limit policies — the machine-checked invariant is the queue type; the no-limit posture follows from it. Residually an operator convention (devstack ships no policies; the deployment guide prohibits delivery-limit / queue-type-affecting policies) — services hold AMQP creds only, so a runtime policy gate is not possible and is not claimed. |
What explicit classic buys |
(1) Immunity to vhost/node |
Enforcement layers |
A declare-time: explicit arg; eager first session ⇒ conflict 406 fails boot. A′
runtime: the reconnect path classifies AMQP 406 → |
Accepted residuals |
Exclusive-variant explicit type is hygiene, not enforcement (uuid-named, quorum structurally impossible). Handler-panic supervisor death — RETIRED by #1203 (task-boundary containment on all three handler paths; ADR-003 residual note updated). Pre-3.13-born in-place-upgraded queues may 406 at boot (documented remedy). Policies remain an operator convention. Permission separation — #1202. |
C3 design (#1197) — approved v2, 2026-07-29
v1 was rejected by external review (5 stop-ships + ~25 findings). The load-bearing reversal: the broker TTL+DLX return hop is GONE — classic-queue dead-lettering is best effort (ADR-003 reserves at-least-once dead-lettering for the quorum gate), so v1’s "no loss ever" was false at the expiry hop. v2 is the client-driven parking replayer. Full normative design: ADR-059.
| Decision | Substance |
|---|---|
Safety invariant |
A delivery is acked ONLY after a committed audit row or a client-confirmed durable publish
(confirms + |
Taxonomy |
|
Parking replayer |
|
Quarantine |
|
Capture schema |
ONE |
Settlement |
|
C5 design (#1199) — approved 2026-07-29
One MR (feat(mq): DLQ observability + triage runbook + false-alert disclosure (#1199)),
units U0–U3. No new ADR: the instruments follow the established otel+stub-twin metrics
pattern (three existing metrics modules, none ADR’d); ADR-059 §D7/§D8/§D9 flip to as-built
and ADR-022 gains the disclosure amendment.
Instruments |
New |
Depth sampler |
Third arm of |
Runbook |
New |
Disclosure + honesty (ADR-022 amendment) |
Pre-C2 inflated |
Tests |
Prometheus-bridge asserts on |
Addendum design (#1203) — approved v2, 2026-07-29
Status: Done (2026-07-30) — MR !1105 merged (f3d4922e); v1 rejected by external review (5 stop-ships), v2 approved and shipped as four J-reviewed units with three observed behavioral REDs.
Handler panics currently unwind the supervisor task on all three handler paths (events
consumer; the DLQ session — where the panic kills consumer + replayer + sampler through the
tokio::join!; the inbox — where an unstamped claim trips the high-severity
inbox_unprocessed_beyond_grace invariant and re-runs the handler below INBOX_MAX_RETRIES
forever). The v2 mechanism is a real per-delivery task boundary (v1’s
futures-lite::catch_unwind was rejected: it guards only the poll — a panicking future
destructor or handler Display still unwinds): one crate-private
contain(fut) → Result<T, String> helper spawns the handler future in its own
immediately-awaited tokio task with error-Display rendering normalized INSIDE, so sync
construction, poll, Display, and destructor panics all resolve to an inspectable JoinError
(payload message bounded at 512 bytes; non-string payloads dropped under a guard). Events
path: panic → correlated error! + the existing 2-strike nack ladder. DLQ path: panic →
TransientPark immediately (no in-session re-invoke; parks under the wire-compatible
reason="transient"; a deliberate error! + the new dlq_handler_panics_total{path}
counter disambiguate code defects from outages — a distinct ParkReason::Panicked is
declined as schema churn). Inbox path: an inner boundary stamps the claim row
(error_count/last_error) so the retry cap engages and the at-cap DLX surface carries the
panic text; new InboxError::HandlerPanicked(String). Pre-1.0 breaking: Fut: 'static on
subscribe/subscribe_exclusive, the new inbox variant — all call sites to be updated in
the same diff, no shims.
ADR-003’s crash-path residual retires at the docs unit; ADR-059 Consequences flip to
as-built (converted panics compose as TransientPark; a deterministic panic quarantines at
the park cap as park_cap_exhausted).
Addendum design (#1202) — approved v2, 2026-07-30
Status: Done (2026-07-30) — MR !1106 merged (ba88360d); v1 rejected by external review (4 stop-ships + ~20 findings), v2 approved after the exchange-ownership steer and shipped as four J-reviewed units + the F-054 pre-push fix, with two observed REDs.
Today one craig user (administrator tag, wildcard grants) is shared by every service and
test, and .env.example ships that identity for seven services (omitting composition’s
block entirely). v1 kept service-side exchange declares and therefore had to grant every
service configure on both shared exchanges — but configure includes DELETE, so a
compromised service could delete/redeclare craig.dlx and silently suppress forensic
delivery without ever reading the DLQ, falsifying ADR-022’s #1181 bound. The user-steered
v2 reversal: exchange ownership moves to the operator/topology plane.
| Decision | Substance |
|---|---|
Operator-owned exchanges (the suppression fix) |
Remove both runtime |
Credential model (9 users, vhost |
8 service principals (devstack passwords = username — a public ACL fixture, hashes
committed via |
Recorded bounds (ADR-003 amendment) |
(1) security’s |
Wiring |
8 compose URL swaps; |
Tests ( |
Mgmt-exact fleet pin: all 9 users' tags + permission triples EQUAL the expectation read
from |
Out of scope (routed)
-
The W1f retry redesign, error taxonomy, parking/quarantine, settlement-failure teardown, malformed-envelope capture — #1197 (C3, ADR-059, § C3 design above).
-
Queue-type/delivery-limit contract — #1198 (C4, § C4 design above).
-
Metrics, runbook, false-alert disclosure — #1199 (C5, § C5 design above).
-
Operator-visible threshold-alert routing — #1205; quarantine drain/replay tooling (the C5-declined optional scope) — #1206; the stale opentelemetry-observability plan page — #1207.
-
Broker permission separation — #1202; handler-panic containment — #1203 (epic addenda).
-
Inbox exactly-once redesign — #1178 (held for contract steer).