Skip to content

fix: Batch MarkAsDeadLetterAsync increments RetryCount in NetEvolve.Pulse.EntityFramework #855

Description

@samtrion

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

  • A failing test is written first: dead-lettering a Processing message with RetryCount = 2 through the EF batch overload leaves RetryCount = 2 (it is 3 today).
  • The test runs the single and batch EF dead-letter paths and checks that both leave the same RetryCount.
  • Line 284 of EntityFrameworkOutboxRepository.cs passes 0, and the new test passes.
  • Existing outbox repository tests stay green.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    type:bugIndicates an issue or flaw that needs to be fixed.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions