Skip to content

feat(consumer): safe requeue default + per-message disposition sentinels#17

Merged
KARTIKrocks merged 2 commits into
mainfrom
feat/consumer-requeue-disposition
Jul 5, 2026
Merged

feat(consumer): safe requeue default + per-message disposition sentinels#17
KARTIKrocks merged 2 commits into
mainfrom
feat/consumer-requeue-disposition

Conversation

@KARTIKrocks

@KARTIKrocks KARTIKrocks commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Flip DefaultConsumerConfig().RequeueOnError from true to false. Previously a handler error requeued the message immediately and forever, bypassing any DLQ — a single poison message hot-looped and head-of-line-blocked the queue. A failed message is now rejected without requeue (dead-lettered if a DLX is configured, else discarded). Behavioral change; restore with WithRequeueOnError(true).

Add ErrRequeue / ErrDrop sentinel errors: a handler can return them (optionally %w-wrapped) to force requeue or no-requeue per message, overriding the default. The decision is a pure requeueDecision() helper, wired into processDelivery.

This also fixes RetryMiddleware causing unbounded redelivery under the default: after in-process retries are exhausted the error is rejected, not requeued. Its doc comment now spells out the in-process cost and the RequeueOnError interaction.

Summary

Motivation

Fixes #

Changes

Checklist

  • fmt, vet, lint, test, build passes (make all)
  • New code has tests where appropriate
  • Breaking changes are documented

Summary by CodeRabbit

  • New Features

    • Added message-level error control so handlers can explicitly request requeue or drop behavior.
    • Documented clearer retry and dead-letter behavior for failed messages.
  • Bug Fixes

    • Changed the default failed-message behavior to avoid automatic requeue loops.
    • Fixed retry handling so exhausted retries now stop redelivery instead of looping indefinitely.
  • Documentation

    • Updated the changelog and README with the new default behavior and configuration examples.

Flip DefaultConsumerConfig().RequeueOnError from true to false. Previously a
handler error requeued the message immediately and forever, bypassing any DLQ —
a single poison message hot-looped and head-of-line-blocked the queue. A failed
message is now rejected without requeue (dead-lettered if a DLX is configured,
else discarded). Behavioral change; restore with WithRequeueOnError(true).

Add ErrRequeue / ErrDrop sentinel errors: a handler can return them (optionally
%w-wrapped) to force requeue or no-requeue per message, overriding the default.
The decision is a pure requeueDecision() helper, wired into processDelivery.

This also fixes RetryMiddleware causing unbounded redelivery under the default:
after in-process retries are exhausted the error is rejected, not requeued. Its
doc comment now spells out the in-process cost and the RequeueOnError interaction.
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@KARTIKrocks, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 38 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8fb126ad-4ee7-4186-916a-09caed2d24e0

📥 Commits

Reviewing files that changed from the base of the PR and between f7da1d6 and 7917c89.

📒 Files selected for processing (3)
  • consumer_test.go
  • integration_test.go
  • middleware.go

Walkthrough

This PR changes DefaultConsumerConfig().RequeueOnError from true to false, adds exported sentinel errors ErrRequeue/ErrDrop for per-message override of requeue behavior, introduces a requeueDecision helper wired into processDelivery, clarifies RetryMiddleware docs, and updates tests/docs accordingly.

Changes

Requeue behavior change

Layer / File(s) Summary
Sentinel errors and config default
rabbitmq.go, consumer.go
Adds exported ErrRequeue/ErrDrop sentinel errors and flips DefaultConsumerConfig().RequeueOnError default to false, with updated godoc.
requeueDecision logic and wiring
consumer.go
Adds requeueDecision(err, defaultRequeue) using errors.Is and wires it into processDelivery's Nack call.
RetryMiddleware documentation
middleware.go
Expands the RetryMiddleware comment to describe retry timing, error propagation, and interaction with RequeueOnError.
Unit and integration tests
consumer_test.go, middleware_test.go, integration_test.go
Adds/updates tests for requeueDecision, default config value, retry disposition preservation, and end-to-end terminal-error/requeue behavior.
Changelog and README
CHANGELOG.md, README.md
Documents the 0.6.0 behavior change, sentinel error usage, and the RetryMiddleware fix.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • KARTIKrocks/rabbitwrap#9: Both PRs modify RetryMiddleware behavior in middleware.go, affecting retry/requeue control flow.

Poem

A rabbit once looped without end in sight,
Requeuing forever, day into night. 🐇
Now ErrDrop and ErrRequeue take the lead,
Defaults turned safe — no more runaway speed.
Nack once, dead-letter, and rest well tonight. 🌙

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description includes useful prose, but the required template sections are mostly left as placeholders and the checklist is unfinished. Fill in the Summary, Motivation, Changes, and Checklist sections with concrete content, including a real Fixes # reference and completed verification items.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change: safer default requeue behavior plus per-message disposition sentinels.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/consumer-requeue-disposition

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov-commenter

codecov-commenter commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@KARTIKrocks

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
consumer.go (1)

508-522: 📐 Maintainability & Code Quality | 🔵 Trivial

Codecov flagged one uncovered line in this function — likely the nackErr branch (Lines 515-519).

That path (delivery.Nack itself failing) is hard to exercise without a way to inject a failing Nack. Do you want me to sketch a test double for Delivery/the underlying channel to cover this branch, or open a tracking issue instead?

As per the PR objectives, "Codecov reported patch coverage of 90% with 1 changed line in consumer.go missing coverage and asked for review of the uncovered line."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@consumer.go` around lines 508 - 522, The uncovered branch is the nack failure
path inside Consumer.processDelivery when delivery.Nack returns an error. Add a
focused unit test for processDelivery that uses a test double for Delivery (or
the underlying channel wrapper) to force Nack(false, requeueDecision(...)) to
fail, and assert that c.config.OnError is invoked with nackErr when AutoAck is
false. Use the existing processDelivery, Delivery.Nack, and requeueDecision
symbols to locate and exercise this branch.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@consumer_test.go`:
- Around line 44-66: The table-driven subtests in TestRequeueDecision are
independent because requeueDecision is pure, so mark each t.Run subtest with
t.Parallel to follow the test style guidance. Update the subtest body inside
TestRequeueDecision so each case runs in parallel without sharing state, keeping
the existing table-driven structure and assertions intact.

In `@integration_test.go`:
- Around line 2125-2131: The negative assertion in the hot-loop check uses a
fixed sleep, which makes the test flaky under slow or contended conditions.
Update the assertion in the integration test around attempts.Load() to use
bounded polling with a short interval and deadline, or compare successive reads
over a couple of polls, so it deterministically verifies the handler never
exceeds one run without relying on a single time.Sleep.

In `@middleware.go`:
- Around line 70-77: Update the godoc on RetryMiddleware to correct the
“dead-lettered” wording: clarify that after retries are exhausted the message is
only dead-lettered if the queue has a DLX configured, otherwise it is discarded,
matching the ErrDrop documentation in rabbitmq.go. Also add a note that
RetryMiddleware retries every non-nil error returned by the handler, including
ErrDrop and ErrRequeue, and that these sentinels only affect the final
disposition after maxRetries is reached, not whether retries occur.

---

Outside diff comments:
In `@consumer.go`:
- Around line 508-522: The uncovered branch is the nack failure path inside
Consumer.processDelivery when delivery.Nack returns an error. Add a focused unit
test for processDelivery that uses a test double for Delivery (or the underlying
channel wrapper) to force Nack(false, requeueDecision(...)) to fail, and assert
that c.config.OnError is invoked with nackErr when AutoAck is false. Use the
existing processDelivery, Delivery.Nack, and requeueDecision symbols to locate
and exercise this branch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4b6ea00f-45f4-4677-ac69-c68e39632575

📥 Commits

Reviewing files that changed from the base of the PR and between e78a6e8 and f7da1d6.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • README.md
  • consumer.go
  • consumer_test.go
  • integration_test.go
  • middleware.go
  • middleware_test.go
  • rabbitmq.go

Comment thread consumer_test.go
Comment on lines +44 to 66
func TestRequeueDecision(t *testing.T) {
tests := []struct {
name string
err error
defaultRequeue bool
want bool
}{
{"nil error, default false", nil, false, false},
{"nil error, default true", nil, true, true},
{"plain error follows default false", errors.New("boom"), false, false},
{"plain error follows default true", errors.New("boom"), true, true},
{"ErrRequeue overrides default false", ErrRequeue, false, true},
{"ErrRequeue wrapped overrides default false", fmt.Errorf("db down: %w", ErrRequeue), false, true},
{"ErrDrop overrides default true", ErrDrop, true, false},
{"ErrDrop wrapped overrides default true", fmt.Errorf("poison: %w", ErrDrop), true, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := requeueDecision(tt.err, tt.defaultRequeue); got != tt.want {
t.Errorf("requeueDecision(%v, %v) = %v, want %v", tt.err, tt.defaultRequeue, got, tt.want)
}
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider t.Parallel() for the table-driven subtests.

requeueDecision is a pure function with no shared state, so each t.Run subtest is safe to parallelize.

♻️ Proposed diff
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
 			if got := requeueDecision(tt.err, tt.defaultRequeue); got != tt.want {
 				t.Errorf("requeueDecision(%v, %v) = %v, want %v", tt.err, tt.defaultRequeue, got, tt.want)
 			}
 		})
 	}

As per path instructions, "Prefer table-driven tests and t.Parallel where safe."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@consumer_test.go` around lines 44 - 66, The table-driven subtests in
TestRequeueDecision are independent because requeueDecision is pure, so mark
each t.Run subtest with t.Parallel to follow the test style guidance. Update the
subtest body inside TestRequeueDecision so each case runs in parallel without
sharing state, keeping the existing table-driven structure and assertions
intact.

Source: Path instructions

Comment thread integration_test.go Outdated
Comment thread middleware.go Outdated
- Cover the previously-untested nack-failure branch in processDelivery with a
  focused unit test (a Delivery with a nil Acknowledger forces Nack to error),
  asserting OnError receives the nack error.
- Replace the hot-loop check's single time.Sleep with bounded polling that fails
  the instant the attempt count climbs past one — deterministic, not timing-luck.
- Correct RetryMiddleware godoc: after retries exhaust, the message is
  dead-lettered only if the queue has a DLX (else discarded), and note that
  every error is retried, with ErrRequeue/ErrDrop affecting only the final
  disposition.
@KARTIKrocks
KARTIKrocks merged commit 4c3e2b9 into main Jul 5, 2026
11 checks passed
@KARTIKrocks
KARTIKrocks deleted the feat/consumer-requeue-disposition branch July 5, 2026 04:09
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