User Story
As an operator inspecting dead-lettered outbox messages, I want RetryCount to have the same value no matter which code path or provider dead-lettered the message, so that dead-letter inspection and statistics show the real number of failed attempts.
Problem
In NetEvolve.Pulse.EntityFramework, the two MarkAsDeadLetterAsync overloads change RetryCount differently:
- The single overload (
src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxRepository.cs:249-257) calls UpdateByQueryAsync(..., OutboxMessageStatus.DeadLetter, 0, errorMessage, ...). retryIncrement is 0 (line 256).
- The batch overload (
src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxRepository.cs:277-285) calls UpdateByIdsAsync(messageIds, now, null, null, OutboxMessageStatus.DeadLetter, 1, errorMessage, ...). retryIncrement is 1 (line 284).
Every executor adds retryIncrement to the stored value: SetProperty(p => p.RetryCount, p => p.RetryCount + retryIncrement) in BulkOutboxRepositoryExecutor, and entity.RetryCount += retryIncrement in the tracking and MySQL executors.
The batch path runs in production. src/NetEvolve.Pulse/Outbox/OutboxProcessorHostedService.cs:528 and :565 dead-letter every message where RetryCount + 1 >= GetEffectiveMaxRetryCount(...). The single path at :422-424 uses the same threshold and does not increment.
Scenario with MaxRetryCount = 3: a message fails its third attempt while its stored RetryCount is 2.
- EF batch path: stored
RetryCount = 3.
- EF single path: stored
RetryCount = 2.
- Every other provider, single or batch: stored
RetryCount = 2. For example, SQL Server usp_MarkOutboxMessageDeadLetter (src/NetEvolve.Pulse.SqlServer/Scripts/OutboxMessage.sql:237-252) sets only Status, Error and UpdatedAt. The PostgreSQL, SQLite, MongoDB and Cosmos DB dead-letter updates do not change RetryCount either. They increment it only in MarkAsFailedAsync.
The impact is limited. A dead-lettered message is not retried again, and replay resets RetryCount to 0. Only the RetryCount shown in dead-letter inspection and statistics is off by one. No existing test checks RetryCount after an EF batch dead-letter.
Specification
Project contract, IOutboxRepository.MarkAsDeadLetterAsync(IReadOnlyCollection<Guid>, string, CancellationToken) in src/NetEvolve.Pulse.Extensibility/Outbox/IOutboxRepository.cs:151-168:
The default implementation calls MarkAsDeadLetterAsync(Guid, string, CancellationToken) once per id sequentially, resulting in one storage round trip per message. Implementations SHOULD override this overload with a single set-based storage operation (e.g., UPDATE ... WHERE Id IN (...)) to reduce round trips for large batches.
The batch override exists only to save round trips. Its effect on each message must match the single overload, and the single overload does not change RetryCount.
Requirements
- Pass
retryIncrement: 0 to UpdateByIdsAsync in the batch MarkAsDeadLetterAsync overload of EntityFrameworkOutboxRepository.
- Make no other behavior changes. The single overload and the other providers are already correct.
Acceptance Criteria
User Story
As an operator inspecting dead-lettered outbox messages, I want
RetryCountto have the same value no matter which code path or provider dead-lettered the message, so that dead-letter inspection and statistics show the real number of failed attempts.Problem
In
NetEvolve.Pulse.EntityFramework, the twoMarkAsDeadLetterAsyncoverloads changeRetryCountdifferently:src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxRepository.cs:249-257) callsUpdateByQueryAsync(..., OutboxMessageStatus.DeadLetter, 0, errorMessage, ...).retryIncrementis0(line 256).src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxRepository.cs:277-285) callsUpdateByIdsAsync(messageIds, now, null, null, OutboxMessageStatus.DeadLetter, 1, errorMessage, ...).retryIncrementis1(line 284).Every executor adds
retryIncrementto the stored value:SetProperty(p => p.RetryCount, p => p.RetryCount + retryIncrement)inBulkOutboxRepositoryExecutor, andentity.RetryCount += retryIncrementin the tracking and MySQL executors.The batch path runs in production.
src/NetEvolve.Pulse/Outbox/OutboxProcessorHostedService.cs:528and:565dead-letter every message whereRetryCount + 1 >= GetEffectiveMaxRetryCount(...). The single path at:422-424uses the same threshold and does not increment.Scenario with
MaxRetryCount = 3: a message fails its third attempt while its storedRetryCountis2.RetryCount = 3.RetryCount = 2.RetryCount = 2. For example, SQL Serverusp_MarkOutboxMessageDeadLetter(src/NetEvolve.Pulse.SqlServer/Scripts/OutboxMessage.sql:237-252) sets onlyStatus,ErrorandUpdatedAt. The PostgreSQL, SQLite, MongoDB and Cosmos DB dead-letter updates do not changeRetryCounteither. They increment it only inMarkAsFailedAsync.The impact is limited. A dead-lettered message is not retried again, and replay resets
RetryCountto0. Only theRetryCountshown in dead-letter inspection and statistics is off by one. No existing test checksRetryCountafter an EF batch dead-letter.Specification
Project contract,
IOutboxRepository.MarkAsDeadLetterAsync(IReadOnlyCollection<Guid>, string, CancellationToken)insrc/NetEvolve.Pulse.Extensibility/Outbox/IOutboxRepository.cs:151-168:The batch override exists only to save round trips. Its effect on each message must match the single overload, and the single overload does not change
RetryCount.Requirements
retryIncrement: 0toUpdateByIdsAsyncin the batchMarkAsDeadLetterAsyncoverload ofEntityFrameworkOutboxRepository.Acceptance Criteria
Processingmessage withRetryCount = 2through the EF batch overload leavesRetryCount = 2(it is3today).RetryCount.EntityFrameworkOutboxRepository.cspasses0, and the new test passes.