Repository navigation
[Docs] RequeueCount: the code counts deliveries, the docs count requeues; a clarification of which is intended #4366
Description
Describe the bug
This is actually not a bug, but discrepancy between the docs and the actual Brighter behavior.
Reading Proactor.RequeueMessage / Reactor.RequeueMessage alongside the docs, the two describe the counter differently, and it is not obvious which one is intended. The pump increments first and then compares with >=:
message.Header.UpdateHandledCount();
if (DiscardRequeuedMessagesEnabled() && message.HandledCountReached(RequeueCount)) // Header.HandledCount >= requeueCount
return RejectMessage(...);
return Channel.RequeueAsync(message, delay ?? RequeueDelay);So a message is rejected on the failure whose ordinal equals RequeueCount: with requeueCount: 3 the handler runs 3 times and the message is requeued twice, and requeueCount: 1 rejects on the first DeferMessageAction, the same as requeueCount: 0. This looks deliberate: the core test MessageDispatch/Proactor/When_a_requeue_count_threshold_for_commands_has_been_reached.cs expects exactly that (RequeueCount = 3, two messages, SendCount == 6), and #4341 talks about the same counter as a "delivery budget".
The user-facing text reads as "number of requeues" instead:
Subscription.RequeueCountXML doc: "the number of times that we can requeue a message before we abandon it as poison pill"; constructor parameter: "The number of times you want to requeue a message before dropping it".- Docs
contents/ErrorHandlingOptions.md: "The maximum number of times a message can be requeued ... When the count is exceeded, the message is rejected", "A value of0means the message is rejected on the firstDeferMessageActionwithout any requeue", and the worked example "requeue the message up to 3 times. On the 4th failure, the message is rejected and routed toorders.dlq". - Docs
contents/HandlerFailure.md: "the maximum number of times a message can be requeued before it is treated as a poison message ... When the count is exceeded". - The subscription tables on the transport pages (RabbitMQ, SQS, Service Bus, Kafka, Redis, MsSql, PostgreSQL, MQTT, RocketMQ, GCP Pub/Sub, in-memory): "Times a message is requeued before it is treated as a poison pill; -1 is unlimited".
Measured on 10.7.0 with an always-deferring handler on the in-memory transport (test below):
requeueCount |
handler runs (docs) | handler runs (actual) | requeues (actual) |
|---|---|---|---|
| 0 | 1 | 1 | 0 |
| 1 | 2 | 1 | 0 |
| 3 | 4 | 3 | 2 |
Either reading is fine on its own; it would just help if the docs and the code said the same thing, since someone configuring requeueCount: 3 from the docs will expect one more attempt than they get. The fact that 0 and 1 behave identically may also be worth a look. Two possible resolutions, whichever matches the intent:
- Code, if "number of requeues" is the intended meaning:
HandledCount > RequeueCount(one line inMessage.HandledCountReached, or the two pump call sites) with the threshold test updated toSendCount == 8;-1is unaffected becauseDiscardRequeuedMessagesEnabled()guards it. - Docs, if "delivery budget" is the intended meaning: say that
requeueCountis the total number of deliveries, soNgivesN - 1requeues and1behaves like0, in the XML doc, the two pages and the transport tables.
To Reproduce
xUnit, in-memory transport, Paramore.Brighter 10.7.0, mirrors the core threshold test but counts handler executions:
public sealed class AlwaysDeferHandlerAsync : RequestHandlerAsync<DeferredCommand>
{
public static int Attempts;
public override Task<DeferredCommand> HandleAsync(DeferredCommand command, CancellationToken cancellationToken = default)
{
Attempts++;
throw new DeferMessageAction();
}
}
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(3)]
public async Task When_RequeueCount_Is_N_The_Message_Should_Be_Requeued_N_Times_And_Rejected_On_Failure_N_Plus_1(int requeueCount)
{
// CommandProcessor with the handler above; ChannelAsync over InMemoryMessageConsumer(routingKey, bus, TimeProvider.System, ackTimeout: 30 s)
var pump = new Proactor(commandProcessor, _ => typeof(DeferredCommand), mapperRegistry, new EmptyMessageTransformerFactoryAsync(),
new InMemoryRequestContextFactory(), channel)
{
Channel = channel, TimeOut = TimeSpan.FromMilliseconds(500), RequeueCount = requeueCount, RequeueDelay = TimeSpan.Zero,
};
bus.Enqueue(/* one MT_COMMAND message */);
var run = Task.Factory.StartNew(() => pump.Run(), TaskCreationOptions.LongRunning);
await Task.Delay(TimeSpan.FromSeconds(3));
channel.Enqueue(/* MT_QUIT */);
await run;
Assert.Equal(requeueCount + 1, AlwaysDeferHandlerAsync.Attempts);
}Result:
requeueCount: 0 passed
requeueCount: 1 Expected: 2 Actual: 1
requeueCount: 3 Expected: 4 Actual: 3
Exceptions (if any)
None.
Further technical details
- Brighter version: 10.7.0
dotnet --info: SDK 10.0.400, host 10.0.11, RID win-x64- OS: Windows 11 (10.0.26200)
- Related: SQS: requeueCount is inert — requeue does not persist handled-count, so ADR 0038's direct DLQ send is unreachable on budget exhaustion #4341 (uses "delivery budget" for the same counter), Message pump dead-letters with "Handle Count Exceeded for message " (empty id) on transports that never set x-original-message-id #4363 (dead-letter description on budget exhaustion)
Analysis and the repro were prepared with help of an AI assistant.
Activity
iancooper commentedon Sep 26, 2026
This is a docs issue, as we are essentially thinking about OnceOnly with 1 and 0 as a default of NoRetries, which are semantically equivalent
[-]RequeueCount: the code counts deliveries, the docs count requeues; a clarification of which is intended[/-][+][Docs] RequeueCount: the code counts deliveries, the docs count requeues; a clarification of which is intended[/+]