Dont Ack Action#4019
Conversation
12 ADR files had duplicate numbers (0023-0038 range) due to independent branches picking the same numbers. Renumbered duplicates to 0039-0050, keeping the chronologically first file at each number. Updated all cross-references in specs and other ADRs. Also assigned numbers to the two 00xx placeholder ADRs (0043, 0048). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add DontAckAction exception that signals the message pump to not acknowledge a message, allowing it to be re-presented on the next loop iteration. Includes DontAckDelay property on MessagePump and catch block in Reactor between DeferMessageAction and RejectMessageAction. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds the async counterpart to the Reactor DontAckAction support: catch DontAckAction in the Proactor event loop, unwrap from TargetInvocationException in TranslateMessageAsync, and add SendAsync override to SpyDontAckCommandProcessor for async tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ctor Add DontAckAction detection to the AggregateException handler in both Reactor and Proactor message pumps, matching the existing pattern for DeferMessageAction and RejectMessageAction flags. Add Publish/PublishAsync overrides to SpyDontAckCommandProcessor for event-path testing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds DontAckOnErrorAttribute and DontAckOnErrorHandler (sync and async) that catch unhandled exceptions in the handler pipeline and wrap them in DontAckAction, causing the message to remain unacknowledged on the channel. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a FeatureSwitch is off and dontAck is true, throw DontAckAction instead of silently consuming the message. This causes the message pump to leave the message unacknowledged on the channel for re-delivery. Existing behavior (dontAck: false default) is unchanged. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Nack operation to channel and consumer interfaces so queue transports (RMQ, SQS, Azure Service Bus) can immediately unlock messages on DontAckAction instead of waiting for visibility timeout. Stream transports implement Nack as no-op. Updates requirements (FR-10 through FR-13), ADR §6 amendment, and tasks (Phases 6-8). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…on handling Add Nack/NackAsync to IAmAMessageConsumerSync, IAmAMessageConsumerAsync, IAmAChannelSync, and IAmAChannelAsync interfaces. Implement in Channel, ChannelAsync, and InMemoryMessageConsumer (remove from locked messages and re-enqueue to bus). Update Reactor DontAckAction catch blocks to call Channel.Nack(message) before incrementing unacceptable count. - Test: When_A_Handler_Throws_DontAck_Action_Should_Nack_The_Message - Updated Phase 1 tests to expect nacked messages on bus Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add await Channel.NackAsync(message) to both DontAckAction catch blocks in Proactor (direct catch and AggregateException dontAck branch), mirroring the Reactor's synchronous Nack calls from Phase 6 Task 1. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- RMQ Sync: Nack calls BasicNack with requeue=true - RMQ Async: NackAsync calls BasicNackAsync with requeue=true - SQS/SQS V4: NackAsync sets visibility timeout to zero for immediate redelivery - Azure Service Bus: NackAsync calls AbandonMessageAsync to release the lock - Added AbandonAsync to IServiceBusReceiverWrapper and ServiceBusReceiverWrapper Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Kafka: no-op (not committing offset is sufficient for redelivery) - Redis: no-op (LPOP is destructive, cannot un-pop) - MQTT: no-op (pub/sub has no acknowledgment concept) - GCP Pub/Sub: no-op for both stream and pull consumers (not acking suffices) - Fix pre-existing SW003 in Kafka Reject/RejectAsync catch blocks Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All 536 core tests pass. Phases 1-8 complete. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pull Request Review: DontAckAction ImplementationOverviewThis PR introduces a StrengthsArchitectural Consistency
Code Quality
Test Coverage
Issues & Concerns1. Inconsistent Delay Handling in Reactor
|
Pull Request Review: DontAck ActionSummaryThis PR introduces a ✅ Strengths1. Excellent Architectural Consistency
2. Comprehensive Documentation
3. Strong Test Coverage
4. Safety Mechanisms
5. Broad Transport Support
🔍 Areas for Consideration1. Potential Tight Loop in Some Transports (Medium Priority)Location: When Observation: The delay is configurable ( Recommendation: Consider documenting recommended 2. Nack Implementation Consistency Across Transports (Low-Medium Priority)Locations: Various messaging gateway files The Nack implementations vary across transports:
Observation: This is appropriate - each transport has different ack/nack semantics. However, the behavior differences could surprise users. Recommendation: Consider adding a table to the ADR or documentation showing how Nack behaves on each transport and any gotchas (e.g., Kafka offset not committing means consumer needs to restart to see message again). 3. Error Handling in DontAckOnErrorHandler (Low Priority)Location: catch (Exception ex)
{
throw new DontAckAction(ex.Message, ex);
}This catches all exceptions, including Issue: If an inner handler throws Recommendation: Add exception type filtering: catch (Exception ex) when (ex is not DeferMessageAction
and not RejectMessageAction
and not InvalidMessageAction
and not DontAckAction
and not ConfigurationException)
{
throw new DontAckAction(ex.Message, ex);
}4. Missing Nack in Channel Wrappers (Low Priority)Location: The Observation: This is consistent with how 5. Reactor.cs Line 259 - Blocking Call (Low Priority)Location: Thread.Sleep(DontAckDelay);In the
Recommendation: Use 6. Test Doubles Organization (Very Low Priority)Location: Various test files The test doubles ( Recommendation: Consider making the counter instance-based or using thread-safe increments if parallel test execution is supported. 🔐 Security ConsiderationsNo security vulnerabilities identified. The implementation:
⚡ Performance Considerations
📋 Test Coverage AssessmentExcellent coverage across:
Missing tests (minor):
🎯 RecommendationsMust Fix (Before Merge)
Should Consider
Nice to Have
📝 Additional NotesPositive Implementation Details
ADR QualityThe ADR is well-written and follows the project's established format. It clearly explains:
ConclusionThis is a high-quality PR that adds a useful feature to the framework. The implementation follows established patterns, includes comprehensive tests, and is well-documented. The main concern is the exception filtering in Recommendation: Approve with minor changes requested for the exception filtering issue. Review conducted using Claude Code following Brighter's design principles and architectural patterns. |
Pull Request Review: DontAckActionSummaryThis PR introduces a Code Quality Assessment: ✅ ExcellentStrengths1. Exceptional Documentation
2. Architectural Consistency
3. Comprehensive Implementation
4. Safety Mechanisms
|
Observations & Suggestions (Part 2)1. Logging Consistency (Minor)The inner exception is logged only in the direct catch block (Reactor.cs:313-314) but not in the AggregateException handler (lines 253-261). Consider capturing the inner exception in the aggregate handler and logging it there as well for consistency. 2. Transport Implementation - Error Handling
3. FeatureSwitchAttribute ConstructorThe attribute now has 5 parameters with dontAck. Good that it defaults to false for backward compatibility. The named-parameter pattern in examples is the right approach. 4. Potential Race Condition (Low Risk)InMemoryMessageConsumer Nack removes from _lockedMessages and re-enqueues. In high-throughput scenarios with multiple consumers, there's a tiny window for double processing. This is acceptable for in-memory testing transports. Security ConsiderationsNo Issues Identified
Performance ConsiderationsPositive:
Watch For:
|
Testing Assessment & Recommendations (Part 3)Test Coverage: Excellent
Missing (Low Priority):
RecommendationsBefore Merge
Post-Merge
ConclusionRecommendation: ✅ APPROVE with Minor Suggestions This is a high-quality implementation that:
The suggested improvements are minor and non-blocking. The code is production-ready. Well done! The ADR in particular is a model for how to document architectural decisions. Reviewed by Claude Code following Brighter's CLAUDE.md guidelines |
MsSql, Postgres, and RocketMQ consumers were missing Nack/NackAsync after merging master which introduced these transports. Also adds AbandonAsync to FakeServiceBusReceiverWrapper test double. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pull Request Review: DontAckAction FeatureSummaryThis PR introduces a ✅ Strengths1. Excellent Architectural Consistency
2. Comprehensive Implementation
3. Strong Test Coverage
4. Good Developer Experience
🔍 Code Quality ObservationsMinor Issues1. Thread.Sleep in Reactor (Blocking Call)// In Reactor.cs
Thread.Sleep(DontAckDelay);While this is acceptable for the synchronous Reactor, consider documenting that this blocks the pump thread. The async Proactor correctly uses 2. Exception Swallowing in DontAckOnErrorHandlercatch (Exception ex)
{
throw new DontAckAction(ex.Message, ex);
}This converts ALL exceptions to DontAckAction, which changes the semantics. Consider:
Recommendation: Document this behavior clearly in the handler's XML comments, or consider a whitelist/blacklist approach for exception types that should NOT be converted. 3. Missing Validation in FeatureSwitchHandler_dontAck = initializerList.Length > 2 && initializerList[2] is true;If Documentation Gaps4. Nack Implementation Behavior Not Fully DocumentedThe transport-specific Nack implementations have good inline comments, but the interface documentation could be clearer about:
Example from SQS implementation - good error handling, but errors are logged and rethrown: catch (Exception ex)
{
Log.ErrorNackingMessage(s_logger, ex, message.Id, receiptHandle, _queueName);
throw;
}Question: Should Nack failures stop the pump, or should they be swallowed with a warning? 5. ADR Could Mention Resource ImplicationsThe ADR is excellent but could mention:
🔒 Security ConsiderationsNo Critical Issues FoundHowever, consider these edge cases:
⚡ Performance ConsiderationsPositive Aspects
Potential Concerns1. Repeated Message Processing OverheadEach DontAckAction triggers:
For high-frequency DontAckActions, this overhead compounds. Consider:
2. In-Memory Queue GrowthFor public void Nack(Message message)
{
_lockedMessages.TryRemove(message.Id, out _);
_bus.Enqueue(message);
}If DontAckActions are frequent, the in-memory queue could grow unbounded. 📋 Testing AssessmentCoverage is StrongThe tests cover:
Potential Test Gaps
🎯 RecommendationsHigh Priority
Medium Priority
Low Priority
✨ Overall AssessmentThis is a high-quality PR that demonstrates excellent software engineering practices:
The minor issues identified are primarily about defensive programming and documentation clarity rather than fundamental design flaws. Recommendation: Approve with minor suggestions. The identified issues are non-blocking and could be addressed in follow-up PRs if needed. 📚 References
|
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Code Review: DontAckAction (PR #4019)This is a well-motivated feature that fills a genuine gap in Brighter's message-handling story — particularly for feature-switch polling and intentional retry-forever patterns. The ADR is thorough, the implementation follows existing conventions, and test coverage across both Reactor/Proactor paths is solid. A few issues worth addressing before merge: 🔴 Critical:
|
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Reactor is a synchronous pump, so Thread.Sleep is more idiomatic and avoids unnecessary Task allocation. This also makes the direct DontAckAction catch consistent with the AggregateException path which already used Thread.Sleep. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The direct catch(DontAckAction) paths logged the inner exception for operator diagnostics, but the AggregateException paths discarded the exception reference by only keeping a bool flag. Changed to capture the DontAckAction instance so the inner exception is logged consistently across all four handler paths (Reactor/Proactor × direct/aggregate). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…er delay) Resolve conflicts preserving DontAck Nack methods in MsSql, RMQ Sync, and RocketMQ consumers alongside master's new DLQ routing and delayed requeue support. Fix Brighter.slnx to include Paramore.Brighter.Testing in Debug builds. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Health Improved
(2 files improve in Code Health)
Gates Failed
Prevent hotspot decline
(1 hotspot with Code Duplication)
Enforce critical code health rules
(1 file with Low Cohesion)
Enforce advisory code health rules
(4 files with Complex Method, Code Duplication)
Gates Passed
1 Quality Gates Passed
See analysis details in CodeScene
Reason for failure
| Prevent hotspot decline | Violations | Code Health Impact | |
|---|---|---|---|
| RmqMessageConsumer.cs | 1 rule in this hotspot | no change | Suppress |
| Enforce critical code health rules | Violations | Code Health Impact | |
|---|---|---|---|
| KafkaMessageConsumer.cs | 1 critical rule | 6.87 → 6.10 | Suppress |
| Enforce advisory code health rules | Violations | Code Health Impact | |
|---|---|---|---|
| AzureServiceBusConsumer.cs | 2 advisory rules | 9.21 → 8.43 | Suppress |
| Proactor.cs | 1 advisory rule | 7.98 → 7.81 | Suppress |
| Reactor.cs | 1 advisory rule | 7.98 → 7.81 | Suppress |
| RmqMessageConsumer.cs | 1 advisory rule | no change | Suppress |
View Improvements
| File | Code Health Impact | Categories Improved |
|---|---|---|
| InMemoryMessageConsumer.cs | 8.96 → 9.54 | Code Duplication |
| PostgresMessageConsumer.cs | 6.86 → 7.29 | Overall Code Complexity |
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.
| { | ||
| var stop = false; | ||
| var defer = false; | ||
| DontAckAction? dontAck = null; |
There was a problem hiding this comment.
❌ Getting worse: Complex Method
Run increases in cyclomatic complexity from 41 to 49, threshold = 9
| { | ||
| var stop = false; | ||
| var defer = false; | ||
| DontAckAction? dontAck = null; |
There was a problem hiding this comment.
❌ Getting worse: Complex Method
EventLoop increases in cyclomatic complexity from 41 to 49, threshold = 9
| return Task.CompletedTask; | ||
| } | ||
|
|
||
| /// <summary> |
There was a problem hiding this comment.
❌ New issue: Low Cohesion
This module has at least 3 different responsibilities amongst its 35 functions, threshold = 3
PR Review: DontAckAction (#4019)This is a well-thought-out feature that fills a real gap in Brighter's message pump behavior. The ADR is thorough, the exception-as-signal pattern is consistent with existing actions, and the transport-specific nack implementations are well-differentiated. Below are the findings, grouped by severity. Issues to Address Before Merge1.
|
* feat: add the adr for dontack * chore: renumber duplicate ADRs to ensure unique sequential numbering 12 ADR files had duplicate numbers (0023-0038 range) due to independent branches picking the same numbers. Renumbered duplicates to 0039-0050, keeping the chronologically first file at each number. Updated all cross-references in specs and other ADRs. Also assigned numbers to the two 00xx placeholder ADRs (0043, 0048). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add DontAckAction exception and Reactor handling Add DontAckAction exception that signals the message pump to not acknowledge a message, allowing it to be re-presented on the next loop iteration. Includes DontAckDelay property on MessagePump and catch block in Reactor between DeferMessageAction and RejectMessageAction. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add DontAckAction handling to Proactor message pump Adds the async counterpart to the Reactor DontAckAction support: catch DontAckAction in the Proactor event loop, unwrap from TargetInvocationException in TranslateMessageAsync, and add SendAsync override to SpyDontAckCommandProcessor for async tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: handle DontAckAction in AggregateException for Reactor and Proactor Add DontAckAction detection to the AggregateException handler in both Reactor and Proactor message pumps, matching the existing pattern for DeferMessageAction and RejectMessageAction flags. Add Publish/PublishAsync overrides to SpyDontAckCommandProcessor for event-path testing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add DontAckOnError pipeline attribute for sync and async handlers Adds DontAckOnErrorAttribute and DontAckOnErrorHandler (sync and async) that catch unhandled exceptions in the handler pipeline and wrap them in DontAckAction, causing the message to remain unacknowledged on the channel. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add dontAck option to FeatureSwitch attributes When a FeatureSwitch is off and dontAck is true, throw DontAckAction instead of silently consuming the message. This causes the message pump to leave the message unacknowledged on the channel for re-delivery. Existing behavior (dontAck: false default) is unchanged. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: mark spec 0020 verification complete after full test suite pass Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: amend spec 0020 and ADR 0038 to add transport-level Nack support Add Nack operation to channel and consumer interfaces so queue transports (RMQ, SQS, Azure Service Bus) can immediately unlock messages on DontAckAction instead of waiting for visibility timeout. Stream transports implement Nack as no-op. Updates requirements (FR-10 through FR-13), ADR §6 amendment, and tasks (Phases 6-8). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add Nack to channel/consumer interfaces and Reactor DontAckAction handling Add Nack/NackAsync to IAmAMessageConsumerSync, IAmAMessageConsumerAsync, IAmAChannelSync, and IAmAChannelAsync interfaces. Implement in Channel, ChannelAsync, and InMemoryMessageConsumer (remove from locked messages and re-enqueue to bus). Update Reactor DontAckAction catch blocks to call Channel.Nack(message) before incrementing unacceptable count. - Test: When_A_Handler_Throws_DontAck_Action_Should_Nack_The_Message - Updated Phase 1 tests to expect nacked messages on bus Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add NackAsync to Proactor DontAckAction handling Add await Channel.NackAsync(message) to both DontAckAction catch blocks in Proactor (direct catch and AggregateException dontAck branch), mirroring the Reactor's synchronous Nack calls from Phase 6 Task 1. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: implement Nack for queue transports (RMQ, SQS, Azure Service Bus) - RMQ Sync: Nack calls BasicNack with requeue=true - RMQ Async: NackAsync calls BasicNackAsync with requeue=true - SQS/SQS V4: NackAsync sets visibility timeout to zero for immediate redelivery - Azure Service Bus: NackAsync calls AbandonMessageAsync to release the lock - Added AbandonAsync to IServiceBusReceiverWrapper and ServiceBusReceiverWrapper Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: implement no-op Nack for stream/pub-sub transports - Kafka: no-op (not committing offset is sufficient for redelivery) - Redis: no-op (LPOP is destructive, cannot un-pop) - MQTT: no-op (pub/sub has no acknowledgment concept) - GCP Pub/Sub: no-op for both stream and pull consumers (not acking suffices) - Fix pre-existing SW003 in Kafka Reject/RejectAsync catch blocks Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: mark spec 0020 all phases complete with verification All 536 core tests pass. Phases 1-8 complete. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add missing Nack implementations for transports added after merge MsSql, Postgres, and RocketMQ consumers were missing Nack/NackAsync after merging master which introduced these transports. Also adds AbandonAsync to FakeServiceBusReceiverWrapper test double. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: mark spec 0020 DontAckAction as complete Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: correct typo in FeatureSwitchAsyncAttribute XML doc Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use Thread.Sleep instead of Task.Delay in Reactor DontAck path The Reactor is a synchronous pump, so Thread.Sleep is more idiomatic and avoids unnecessary Task allocation. This also makes the direct DontAckAction catch consistent with the AggregateException path which already used Thread.Sleep. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: log inner exception in AggregateException DontAck paths The direct catch(DontAckAction) paths logged the inner exception for operator diagnostics, but the AggregateException paths discarded the exception reference by only keeping a bool flag. Changed to capture the DontAckAction instance so the inner exception is logged consistently across all four handler paths (Reactor/Proactor × direct/aggregate). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Brighter has a policy of load shedding in case of an error. That is, when a handler throws an error, our default approach is to acknowledge it, shed the load, and consume the next message. This prevents a poison pill message from blocking the pump thread.
Additions include a blocking retry via [UsePolicy] or [UseResiliencePipeline] that blocks until the policy "gives up" and we then ack again, and DeferMessageAction, which allows you to requeue with a delay (if supported).
However, sometimes, users want to keep retrying on an error, i.e., don't ack. Now this is possible, via a RetryForever policy, but it doesn't work so well for scenarios like a FeatureSwitch
To support these are are adding a DontAckAction. This exception, when thrown, instructs the pump to pause and then attempt to resend the message.
Note that there are several risks here, such as locks timing out on a queue, so we may need to think about what happens when a nack is available on the transport, and that would be preferable as it makes it available to other consumers.