Attempt counter in Postgres, surviving every re-publish #485

Merged
toasterson merged 4 commits from claude/wi-019fa5a0-attempt-counter-in-postgres-surviving-ev into main 2026-08-07 16:40:51 +00:00
Owner

Anima work item 019fa5a0-1360-7f93-9463-3be3a86a88e5.

What to build

The authoritative attempt counter for a dispatch, held in Postgres and incremented by the server at publish time. End to end: migration, increment on publish, read-back into the payload's attempt field, and a test proving the count survives a drain-and-re-publish cycle.

Pushed context

  • ADR 0025 (docs/adr/0025-broker-arbitrates-execution-two-fabrics.md).
  • Why not the broker's own counters. RabbitMQ's x-death header resets when a drainer publishes a fresh message, so a clean-and-requeue loop would count to N forever and never trip. Stock EasyNetQ's error envelope carries no count either; the community pattern adds one to headers by hand. A count that lives only in a payload is not a count.
  • What breaks if this is wrong: ADR 0003's loop guard silently stops working, and the only evidence would be work looping in production. This is the counter ADR 0029's layers read.
  • The dedupe key is (work_item_id, phase_id, base_revision); attempt sits deliberately outside it (ADR 0026), so this counter distinguishes a real retry from a redelivery.
  • Latest migration is 068 in crates/anima-db/migrations/.

Acceptance criteria

  • The counter increments once per genuine dispatch, never on redelivery
  • Draining to the error queue and re-publishing preserves the count
  • The payload's attempt always reflects the Postgres value
  • Nothing reads an attempt count from a broker header

Blocked by

  • 019fa59d-6964-71a3-b439-059f09c14aab (error-queue drainer)
  • 019fa59a-ef3d-7b11-807a-a8330d20f616 (dispatch payload + idempotency key, ADR 0026 Plan)
Anima work item `019fa5a0-1360-7f93-9463-3be3a86a88e5`. ## What to build The authoritative attempt counter for a dispatch, held in Postgres and incremented by the server at publish time. End to end: migration, increment on publish, read-back into the payload's `attempt` field, and a test proving the count survives a drain-and-re-publish cycle. ## Pushed context - ADR 0025 (`docs/adr/0025-broker-arbitrates-execution-two-fabrics.md`). - **Why not the broker's own counters.** RabbitMQ's `x-death` header resets when a drainer publishes a *fresh* message, so a clean-and-requeue loop would count to N forever and never trip. Stock EasyNetQ's error envelope carries no count either; the community pattern adds one to headers by hand. A count that lives only in a payload is not a count. - **What breaks if this is wrong:** ADR 0003's loop guard silently stops working, and the only evidence would be work looping in production. This is the counter ADR 0029's layers read. - The dedupe key is `(work_item_id, phase_id, base_revision)`; `attempt` sits deliberately **outside** it (ADR 0026), so this counter distinguishes a real retry from a redelivery. - Latest migration is 068 in `crates/anima-db/migrations/`. ## Acceptance criteria - [ ] The counter increments once per genuine dispatch, never on redelivery - [ ] Draining to the error queue and re-publishing preserves the count - [ ] The payload's `attempt` always reflects the Postgres value - [ ] Nothing reads an attempt count from a broker header ## Blocked by - 019fa59d-6964-71a3-b439-059f09c14aab (error-queue drainer) - 019fa59a-ef3d-7b11-807a-a8330d20f616 (dispatch payload + idempotency key, ADR 0026 Plan)
toasterson changed title from WIP: Attempt counter in Postgres, surviving every re-publish to Attempt counter in Postgres, surviving every re-publish 2026-08-04 06:09:19 +00:00
toasterson force-pushed claude/wi-019fa5a0-attempt-counter-in-postgres-surviving-ev from 59800b5167 to a0f8f5487f 2026-08-06 18:53:20 +00:00 Compare
toasterson force-pushed claude/wi-019fa5a0-attempt-counter-in-postgres-surviving-ev from a0f8f5487f to e24fb37f5e 2026-08-06 19:29:36 +00:00 Compare
toasterson force-pushed claude/wi-019fa5a0-attempt-counter-in-postgres-surviving-ev from e24fb37f5e to f0a7005daa 2026-08-06 20:48:11 +00:00 Compare
toasterson force-pushed claude/wi-019fa5a0-attempt-counter-in-postgres-surviving-ev from f0a7005daa to 503cb1a58f 2026-08-06 21:24:22 +00:00 Compare
- Fix manual_filter clippy warning in ci_config.rs
- Fix collapsible_if clippy warnings in handlers.rs and runner.rs
- Correct migration reference in publish_dispatch example (094 and 097, not 094 and 095)

These clippy errors were preventing CI from passing (exit code 101).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
toasterson/Anima!485
No description provided.