ConsumerHeartbeat intermittently reports more processed messages than were sent. Seen twice, on different data rows, on two different branches:
| PR |
Row |
Expected |
Actual |
| #291 |
(7, 45, 280, 3, inMemoryDb: true, enableChaos: true) |
7 |
8 |
| #294 (commit conversion) |
(7, 45, 180, 3, inMemoryDb: false, enableChaos: false) |
7 |
9 |
Why it is the test, not the change
- Passes in isolation. 3/3 both times, including the row that had just failed. It only fails inside the full 200+ test run.
- The second occurrence is on the non-chaos row, so injected faults are not required to trigger it.
- It exercises the synchronous consumer —
ConsumerCancelWorkShared uses creator.CreateConsumer(...). The branch it failed on converts only the asynchronous consumer's commit; ProcessMessage.cs and MessageProcessing.cs are untouched in that diff.
The race
ConsumerCancelWorkShared.RunConsumerInternal asserts messageCount == processedCount. The scenario is deliberately a race: a "bad" consumer takes messages, is disposed mid-flight, the heartbeat monitor resets its in-flight messages, and a good consumer picks them up. If the monitor resets a message the good consumer is already working — or the bad consumer completed one before disposal — the message is processed twice and the count overshoots.
Under full-suite load the workers are slower, which widens the window. The overshoot size tracks that: 8 the first time, 9 the second.
What to consider
The assertion may simply be wrong for what the scenario can guarantee. The test is about the heartbeat resetting stalled work, and reprocessing is the mechanism it relies on — so an exact-equality count is a stronger claim than the design supports. IsGreaterThanOrEqualTo(messageCount, ...) plus a separate assertion that every message reached a terminal state may be what was meant.
Worth deciding deliberately rather than re-running until green, which is what happened on #291.
Related
ConsumerHeartbeatintermittently reports more processed messages than were sent. Seen twice, on different data rows, on two different branches:(7, 45, 280, 3, inMemoryDb: true, enableChaos: true)(7, 45, 180, 3, inMemoryDb: false, enableChaos: false)Why it is the test, not the change
ConsumerCancelWorkSharedusescreator.CreateConsumer(...). The branch it failed on converts only the asynchronous consumer's commit;ProcessMessage.csandMessageProcessing.csare untouched in that diff.The race
ConsumerCancelWorkShared.RunConsumerInternalassertsmessageCount == processedCount. The scenario is deliberately a race: a "bad" consumer takes messages, is disposed mid-flight, the heartbeat monitor resets its in-flight messages, and a good consumer picks them up. If the monitor resets a message the good consumer is already working — or the bad consumer completed one before disposal — the message is processed twice and the count overshoots.Under full-suite load the workers are slower, which widens the window. The overshoot size tracks that: 8 the first time, 9 the second.
What to consider
The assertion may simply be wrong for what the scenario can guarantee. The test is about the heartbeat resetting stalled work, and reprocessing is the mechanism it relies on — so an exact-equality count is a stronger claim than the design supports.
IsGreaterThanOrEqualTo(messageCount, ...)plus a separate assertion that every message reached a terminal state may be what was meant.Worth deciding deliberately rather than re-running until green, which is what happened on #291.
Related
ConsumerHeartbeatandConsumerCancelWorkare also the two scenarios with no asynchronous-consumer equivalent (audited 2026-09-10), so whatever is decided here should shape theConsumerAsyncHeartbeattest that Remaining synchronous calls on the async receive path — blocks the next release #284's heartbeat conversion needs.