Skip to content

[Chore] Missing Specifications#4029

Merged
iancooper merged 14 commits into
masterfrom
error_examples
Feb 23, 2026
Merged

[Chore] Missing Specifications#4029
iancooper merged 14 commits into
masterfrom
error_examples

Conversation

@iancooper

Copy link
Copy Markdown
Member

No description provided.

iancooper and others added 14 commits February 23, 2026 09:59
Mark ADR 0051 (error-handling-examples) as Accepted and create
design-approved marker for spec 0021-Error-Examples.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The sample intentionally throws on every 5th message. Without resetting
the unacceptable message count, the pump would shut down. Setting the
window to TimeSpan.Zero resets the count each pump cycle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Four-project sample demonstrating RejectMessageOnErrorAsync on RabbitMQ:
- Greetings library with handler that rejects every 5th message
- GreetingsSender with TimedMessageGenerator IHostedService
- GreetingsReceiverConsole with RmqSubscription and DLQ routing
- DlqConsole consuming rejected messages with rejection metadata

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Align DeferMessageAction with RejectMessageAction and DontAckAction by
adding standard exception constructors and a TimeSpan? Delay property.
This structural change prepares for the DeferMessageOnError backstop
handler without altering existing behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three-project sample demonstrating DeferMessageAction on Kafka:
- Handler throws DeferMessageAction directly (no attribute yet)
- ConcurrentDictionary tracks retries per message ID
- Every 3rd message defers twice then succeeds on 3rd attempt
- Unique groupId kafka-DeferOnError-Sample avoids consumer collisions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DeferMessageOnErrorAttribute decorates a handler method to catch
unhandled exceptions and convert them to DeferMessageAction with a
configurable delay. The delay flows from the attribute through
InitializerParams to the handler.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Async counterpart to DeferMessageOnErrorHandler. Catches unhandled
exceptions in async pipelines and converts them to DeferMessageAction
with a configurable delay from the attribute.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three-project sample demonstrating DeferMessageAction on RabbitMQ:
- Handler throws DeferMessageAction directly (no attribute yet)
- ConcurrentDictionary tracks retries per message ID
- Every 3rd message defers twice then succeeds on 3rd attempt
- InMemorySchedulerFactory provides requeue delay support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verifies DeferMessageOnErrorHandler passes through transparently
when the inner handler completes without throwing an exception.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verifies DeferMessageOnErrorHandlerAsync passes through transparently
when the inner handler completes without throwing an exception.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verifies both sync and async attributes return correct handler types,
timing, step values, and InitializerParams containing the delay.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Both Proactor and Reactor now capture DeferMessageAction.Delay
when catching the exception and pass it to RequeueMessage, which
falls back to RequeueDelay when no delay is specified.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tted)

Three-project sample demonstrating DontAckOnErrorAsync on Kafka:
- Handler uses [DontAckOnErrorAsync(step: 0)], throws every 5th message
- On Kafka, not committing the offset causes re-delivery on next poll
- Unique groupId kafka-DontAckOnError-Sample avoids consumer collisions
- README contrasts Kafka nack (no-op) with RabbitMQ nack (BasicNack)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@iancooper iancooper self-assigned this Feb 23, 2026
@iancooper iancooper merged commit 70b2c20 into master Feb 23, 2026
6 of 7 checks passed

@codescene-delta-analysis codescene-delta-analysis 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.

Gates Passed
4 Quality Gates Passed

See analysis details in CodeScene

Quality Gate Profile: Clean Code Collective
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.

@claude

claude Bot commented Feb 23, 2026

Copy link
Copy Markdown

PR Review: [Chore] Missing Specifications (#4029)

Overall this is a solid and well-scoped addition. The new [DeferMessageOnError] / [DeferMessageOnErrorAsync] attributes complete the symmetry with [DontAckOnError] and [RejectMessageOnError], and the implementation is clean. A few observations below.


Potential Bug: Double-Wrapping of DeferMessageAction

DeferMessageOnErrorHandler<TRequest>.Handle catches Exception broadly, which means it will also catch DeferMessageAction itself. If a handler manually throws DeferMessageAction with a custom delay and is decorated with [DeferMessageOnError(step, delayMs)], the original DeferMessageAction gets wrapped in a new one — and the inner exception's Delay is discarded in favour of the attribute's delay.

// Handler throws DeferMessageAction("...", null, 10_000)  (10-second delay)
// But [DeferMessageOnError(step: 1, delayMilliseconds: 500)] wraps it again
// → DeferMessageAction.Delay = 500ms (not 10 000ms), original delay is lost

A simple fix would be to check ex is DeferMessageAction first and rethrow directly:

catch (Exception ex)
{
    if (ex is DeferMessageAction)
        throw;                         // honour the handler's own delay choice

    Log.UnhandledExceptionDeferringMessage(...);
    throw new DeferMessageAction(ex.Message, ex, _delayMilliseconds);
}

The async handler has the same issue.


Missing Test: Reactor Delay Passthrough

There is an integration-level test verifying that Proactor passes DeferMessageAction.Delay to Channel.RequeueAsync:

tests/.../MessageDispatch/Proactor/When_a_command_handler_throws_a_defer_message_with_delay_Then_message_is_requeued_with_delay.cs

There is no equivalent test for Reactor (the sync message pump). Since both code paths were changed in the same way, a symmetrical test would ensure the sync path doesn't regress.


Minor: InitializeFromAttributeParams Has No Bounds Check

public override void InitializeFromAttributeParams(params object?[] initializerList)
    => _delayMilliseconds = (int?)initializerList[0] ?? 0;

This is safe today because DeferMessageOnErrorAttribute.InitializerParams() always returns a single-element array. But if the method were ever called with an empty list it would throw IndexOutOfRangeException rather than silently defaulting. Adding a guard (initializerList.Length > 0 ? ... : 0) would make this more defensive and consistent with how other handlers in the codebase handle this.


Minor: delayMilliseconds <= 0null Is Implicit

The DeferMessageAction constructor maps delayMilliseconds <= 0 to null (fall back to subscription default). This is the right design, but it isn't documented in the constructor's XML doc comment (or at all). It is easy to misread the intent — a caller who passes 0 expecting "no delay" could be surprised if the subscription's RequeueDelay is, say, 30 seconds.

A brief XML doc note on the constructor (or at least an inline comment) would prevent confusion:

/// <param name="delayMilliseconds">
/// The delay before requeuing. Values &lt;= 0 fall back to the subscription's configured RequeueDelay.
/// </param>

Positive Observations

  • The change from bool defer to DeferMessageAction? deferAction in Proactor/Reactor is clean and makes the intent obvious.
  • Using delay ?? RequeueDelay as the fallback is correct and avoids needing a special sentinel value.
  • Tests cover the happy path (no exception) and failure path (exception → defer) for both sync and async, and the attribute metadata tests are thorough.
  • The four sample projects (Kafka + RabbitMQ × defer + DLQ) provide good working examples for users.
  • partial class for source-generated logging is consistent with the rest of the codebase.

Review generated with Claude Code

@iancooper iancooper deleted the error_examples branch February 23, 2026 16:26
DevJonny pushed a commit to DevJonny/Brighter that referenced this pull request Feb 28, 2026
* Accept ADR 0051 for error handling example applications

Mark ADR 0051 (error-handling-examples) as Accepted and create
design-approved marker for spec 0021-Error-Examples.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add unacceptableMessageLimitWindow to KafkaTaskQueueWithDLQ sample

The sample intentionally throws on every 5th message. Without resetting
the unacceptable message count, the pump would shut down. Setting the
window to TimeSpan.Zero resets the count each pump cycle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add RMQTaskQueueWithDLQ sample for RabbitMQ RejectOnError with DLQ

Four-project sample demonstrating RejectMessageOnErrorAsync on RabbitMQ:
- Greetings library with handler that rejects every 5th message
- GreetingsSender with TimedMessageGenerator IHostedService
- GreetingsReceiverConsole with RmqSubscription and DLQ routing
- DlqConsole consuming rejected messages with rejection metadata

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* tidy: add constructors and Delay property to DeferMessageAction

Align DeferMessageAction with RejectMessageAction and DontAckAction by
adding standard exception constructors and a TimeSpan? Delay property.
This structural change prepares for the DeferMessageOnError backstop
handler without altering existing behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add KafkaDeferOnError sample for Kafka defer with retry counter

Three-project sample demonstrating DeferMessageAction on Kafka:
- Handler throws DeferMessageAction directly (no attribute yet)
- ConcurrentDictionary tracks retries per message ID
- Every 3rd message defers twice then succeeds on 3rd attempt
- Unique groupId kafka-DeferOnError-Sample avoids consumer collisions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add sync DeferMessageOnErrorHandler and attribute

DeferMessageOnErrorAttribute decorates a handler method to catch
unhandled exceptions and convert them to DeferMessageAction with a
configurable delay. The delay flows from the attribute through
InitializerParams to the handler.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add async DeferMessageOnErrorHandlerAsync and attribute

Async counterpart to DeferMessageOnErrorHandler. Catches unhandled
exceptions in async pipelines and converts them to DeferMessageAction
with a configurable delay from the attribute.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add RMQDeferOnError sample for RabbitMQ defer with retry counter

Three-project sample demonstrating DeferMessageAction on RabbitMQ:
- Handler throws DeferMessageAction directly (no attribute yet)
- ConcurrentDictionary tracks retries per message ID
- Every 3rd message defers twice then succeeds on 3rd attempt
- InMemorySchedulerFactory provides requeue delay support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add sync defer handler success path characterization test

Verifies DeferMessageOnErrorHandler passes through transparently
when the inner handler completes without throwing an exception.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add async defer handler success path characterization test

Verifies DeferMessageOnErrorHandlerAsync passes through transparently
when the inner handler completes without throwing an exception.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add attribute configuration tests for DeferMessageOnError

Verifies both sync and async attributes return correct handler types,
timing, step values, and InitializerParams containing the delay.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: pump extracts delay from DeferMessageAction and passes to requeue

Both Proactor and Reactor now capture DeferMessageAction.Delay
when catching the exception and pass it to RequeueMessage, which
falls back to RequeueDelay when no delay is specified.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add KafkaDontAckOnError sample for Kafka nack (offset not committed)

Three-project sample demonstrating DontAckOnErrorAsync on Kafka:
- Handler uses [DontAckOnErrorAsync(step: 0)], throws every 5th message
- On Kafka, not committing the offset causes re-delivery on next poll
- Unique groupId kafka-DontAckOnError-Sample avoids consumer collisions
- README contrasts Kafka nack (no-op) with RabbitMQ nack (BasicNack)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* spec and adr files

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant