feat(platform): hold/postpone — a fourth delivery outcome for backing off - #487
Draft
behinddwalls wants to merge 1 commit into
Draft
feat(platform): hold/postpone — a fourth delivery outcome for backing off#487behinddwalls wants to merge 1 commit into
behinddwalls wants to merge 1 commit into
Conversation
… 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
force-pushed
the
preetam/consumer-backoff
branch
from
August 1, 2026 17:22
d7dfedb to
1578ec0
Compare
behinddwalls
force-pushed
the
preetam/consumer-hold-impl
branch
from
August 1, 2026 17:22
04600c7 to
770b376
Compare
This was referenced Aug 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
postponedflag 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_ignoredcounter). 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