Skip to content

feat(platform): hold/postpone — a fourth delivery outcome for backing off - #487

Merged
behinddwalls merged 1 commit into
mainfrom
preetam/consumer-hold-impl
Aug 4, 2026
Merged

feat(platform): hold/postpone — a fourth delivery outcome for backing off#487
behinddwalls merged 1 commit into
mainfrom
preetam/consumer-hold-impl

Conversation

@behinddwalls

@behinddwalls behinddwalls commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Why?

Queue controllers have no way to say "this message is fine, but it must wait." The outcome model is ack/nack/reject, so waiting stages ack and republish fresh copies of their own messages via PublishAfter, with message-id minting to dodge the publish dedup, retry accounting reset every cycle, and loop liveness hanging on a publish succeeding. Designed in doc/rfc/consumer-hold.md (previous commit).

What?

The messagequeue extension Delivery gains Postpone(delayMs): the delivery finalizes, the message becomes invisible for the delay, and it acts as a partition barrier — the mysql poll loop stops scanning the partition at a postponed row instead of skipping past it (nacked rows keep skip-and-continue, so failures never halt a partition). A new postponed flag on queue_delivery_state makes the post-postpone redelivery exempt from the retry_count increment and resets the count, so deliberate waits never burn the DLQ budget while real failures still dead-letter.

The consumer framework's Delivery view gains Hold(delayMs): an intent-recording call with no I/O. On a nil return from Process the framework postpones instead of acking (metric op postpone); an error return wins over a recorded hold (hold_ignored counter). A failed postpone write is abandoned like a failed ack — the visibility timeout lapses into a normal redelivery, so hold-loop liveness is framework-owned.

Controller unit tests across submitqueue/stovepipe/runway previously passed the extension mock as consumer.Delivery, which only worked structurally; they now use the consumer-facing mock (which has Hold).

Test Plan

make test (83 targets) — includes new consumer hold outcome tests, mysql MarkPostponed/GetDeliveryState store tests, and poll-loop barrier tests. ✅ bazel test //test/integration/extension/messagequeue/... — new end-to-end tests: postpone blocks the partition until due then redelivers in order as attempt 1; a postpone resets the budget but subsequent real failures still dead-letter. ✅ make fmt, make gazelle, make mocks.

Issues

Stack

  1. docs(rfc): consumer hold — postpone a delivery as a partition barrier #486
  2. @ feat(platform): hold/postpone — a fourth delivery outcome for backing off #487
  3. feat(stovepipe): migrate process and buildsignal waits to the hold primitive #488
  4. feat(orchestrator): migrate the buildsignal poll loop to the hold primitive #489
  5. feat(messagequeue)!: remove PublishAfter and the visible_after column #490
  6. feat(messagequeue)!: drop the requeue delay parameter from Nack #491
  7. feat(consumergate)!: rebuild the gate on the postpone primitive #492

Base automatically changed from preetam/consumer-backoff to main August 4, 2026 23:21
@behinddwalls
behinddwalls force-pushed the preetam/consumer-hold-impl branch from 770b376 to 72a436d Compare August 4, 2026 23:21
… off

## Summary

### Why?

Queue controllers have no way to say "this message is fine, but it must wait." The outcome model is ack/nack/reject, so waiting stages ack and republish fresh copies of their own messages via PublishAfter, with message-id minting to dodge the publish dedup, retry accounting reset every cycle, and loop liveness hanging on a publish succeeding. Designed in doc/rfc/consumer-hold.md (previous commit).

### What?

The messagequeue extension Delivery gains Postpone(delayMs): the delivery finalizes, the message becomes invisible for the delay, and it acts as a partition barrier — the mysql poll loop stops scanning the partition at a postponed row instead of skipping past it (nacked rows keep skip-and-continue, so failures never halt a partition). A new `postponed` flag on queue_delivery_state makes the post-postpone redelivery exempt from the retry_count increment and resets the count, so deliberate waits never burn the DLQ budget while real failures still dead-letter.

The consumer framework's Delivery view gains Hold(delayMs): an intent-recording call with no I/O. On a nil return from Process the framework postpones instead of acking (metric op `postpone`); an error return wins over a recorded hold (`hold_ignored` counter). A failed postpone write is abandoned like a failed ack — the visibility timeout lapses into a normal redelivery, so hold-loop liveness is framework-owned.

Controller unit tests across submitqueue/stovepipe/runway previously passed the extension mock as consumer.Delivery, which only worked structurally; they now use the consumer-facing mock (which has Hold).

## Test Plan

✅ `make test` (83 targets) — includes new consumer hold outcome tests, mysql MarkPostponed/GetDeliveryState store tests, and poll-loop barrier tests. ✅ `bazel test //test/integration/extension/messagequeue/...` — new end-to-end tests: postpone blocks the partition until due then redelivers in order as attempt 1; a postpone resets the budget but subsequent real failures still dead-letter. ✅ `make fmt`, `make gazelle`, `make mocks`.
@behinddwalls
behinddwalls force-pushed the preetam/consumer-hold-impl branch from 72a436d to a0851e6 Compare August 4, 2026 23:39
@behinddwalls
behinddwalls added this pull request to the merge queue Aug 4, 2026
Merged via the queue into main with commit c863edd Aug 4, 2026
15 checks passed
@behinddwalls
behinddwalls deleted the preetam/consumer-hold-impl branch August 4, 2026 23:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants