fix: move inbox attribute addition outside static pipeline cache#4061
fix: move inbox attribute addition outside static pipeline cache#4061
Conversation
7d03745 to
e8d5ce7
Compare
e8d5ce7 to
ee1ccc2
Compare
…eline builder The async BuildAsyncPipeline method had a duplicate call to AddGlobalInboxAttributesAsync outside the cache-miss block, causing static cache pollution between tests. Tests without inbox configuration would pick up cached inbox attributes from earlier tests, then fail with ArgumentOutOfRangeException when the handler factory couldn't create UseInboxHandlerAsync. The sync BuildPipeline method correctly only calls AddGlobalInboxAttributes once inside the cache-miss block. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The static cache was storing inbox attributes alongside handler attributes, causing cross-contamination between pipeline builders with different inbox configurations. Now the cache only stores the handler's declared attributes, and inbox attributes are applied fresh each time based on the current _inboxConfiguration. This fixes both the original observability test failures (leaked inbox attributes) and the default inbox publish test (inbox added twice on cache miss). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
f5134dd to
4c2955b
Compare
- PushOntoAttributeList no longer mutates cached RequestHandlerAttribute objects via Step++. The inbox attribute enters at step 0 and existing attributes keep their original step values, preserving correct ordering without side effects on the static cache. - Remove redundant null checks in AddGlobalInboxAttributes(Async) where _inboxConfiguration was checked twice (early return then dead throw). - Add regression test covering cache isolation: build a pipeline with inbox config then without, assert no UseInboxHandler leakage. Covers both sync and async paths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PushOntoAttributeList now sets the inbox attribute's step to min(existing steps) - 1, guaranteeing it is always outermost in the pipeline. This matters when handlers use step: 0 attributes (e.g. RejectMessageOnError, DontAckOnError, DeferMessageOnError) — without this, both the inbox and the handler attribute would sit at step 0 with undefined relative order. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ision, naming - Add reverse-scenario tests (no-inbox → with-inbox) for both sync and async - Use backtick suffix in assertions for precise handler type matching - Rename test class to match file naming convention - Add safety comment on PushOntoAttributeList about fresh instance requirement Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Code ReviewThe PR has evolved well from the initial version. Here is a fresh assessment of the current state. What is now correctCache isolation fix - AddGlobalInboxAttributes(Async) is applied after the cache block, so s_preAttributesMemento stores only the handler declared attributes. Per-instance inbox configuration is applied dynamically on every call. Async duplicate-call removed - The original BuildAsyncPipeline added the inbox attribute twice on cache miss. Now both sync and async paths are symmetric. PushOntoAttributeList no longer mutates cached objects - Replacing handler.Step++ with minStep - 1 is a significant correctness improvement. The old code incremented Step on RequestHandlerAttribute instances held inside the static cache, so step values grew unboundedly across the lifetime of a service. The new approach computes the inbox step relative to the original unmodified cached values on each call. Both reversal directions tested - All four test scenarios are now present: with-inbox to without-inbox, without-inbox to with-inbox, and both async equivalents. This was the main gap in earlier iterations. Redundant null check removed - The dead null guard after the early return is gone from both AddGlobalInboxAttributes and AddGlobalInboxAttributesAsync. Remaining observationsFindHandlerMethod() called twice per add - In AddGlobalInboxAttributes(Async), FindHandlerMethod() is called once for HasNoInboxAttributesInPipeline() and once for HasExistingUseInboxAttributesInPipeline(). Since this now runs on every pipeline build (not just cache misses), the double reflection hit is more frequent than before. Caching the MethodInfo in a local variable would halve the reflection cost. Low priority follow-up. s_postAttributesMemento is never populated (pre-existing) - In both BuildPipeline and BuildAsyncPipeline, when post-attributes are not in the cache they are computed but never added via TryAdd. Post-attributes are therefore re-reflected on every call. This predates the PR but worth a follow-up issue. Step underflow is benign but undocumented - When existing attributes include step 0, minStep - 1 equals -1. Ordering remains correct (inbox sorts last in descending order, meaning outermost in the pipeline). The existing comment explains the invariant but noting that steps can be negative would help maintainers not be surprised by diagnostic output. Summary
The fix is correct and complete. Ready to merge. |
iancooper
left a comment
There was a problem hiding this comment.
TBH I never really liked the global inbox attribute but we came under such pressure from the CTO at Huddle to add it. The problem it creates is why not global outbox, resilience pipeline etc, which gets into fluent over attribute syntax questions
|
@iancooper are we okay with the order being negative, I assume it's not a problem, from reviewing the other code I think it's fine. |
|
I guess |
|
Let us know when you are done |
Code ReviewSummaryThis PR fixes a genuine cache-contamination bug in What's Good
Issues and Suggestions1. Test isolation — parallel execution risk (medium)Each test starts with Suggestion: Add a 2. Step value passed to inbox attribute constructors is immediately discarded (minor)var useInboxAttribute = new UseInboxAttribute(
step: 0, // ← this value is overwritten unconditionally in PushOntoAttributeList
...
Suggestion (optional): Either update the comment here to say the initial step is overwritten, or change the 3.
|
Code ReviewOverviewThis PR fixes a genuine cache-contamination bug in What Works Well
Issues and Suggestions1. var useInboxAttribute = new UseInboxAttribute(
step: 0, // <-- immediately overwritten by PushOntoAttributeList
...
2. Integer underflow edge case in requestHandlerAttribute.Step = minStep == int.MaxValue ? 0 : minStep - 1;If 3. // BuildPipeline (and BuildAsyncPipeline) — post-attributes block
if (!s_postAttributesMemento.TryGetValue(..., out ... postAttributes))
{
postAttributes = ... // computed
// ← s_postAttributesMemento.TryAdd is never called here
}The read path checks the cache but the write path ( 4. || implicitHandler.FindHandlerMethod().HasNoInboxAttributesInPipeline()
|| implicitHandler.FindHandlerMethod().HasExistingUseInboxAttributesInPipeline()If SummaryThe core fix is correct and the test suite for the new behaviour is thorough. The main thing worth addressing before merge is clarifying the misleading 🤖 Generated with Claude Code |
Publish dispatches handlers via Parallel.ForEach/parallel tasks, but the tests used a plain Dictionary which is not thread-safe. This caused intermittent corruption ([null]=null entries) and missing handler keys. Also fixed wrong cache clear (MyCommand → MyEvent) in sync test. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
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.
Code Review — PR #4061: Fix inbox attribute addition outside static pipeline cacheOverall assessment: Approve with minor notes. The fix is correct, well-targeted, and backed by solid regression tests. A few observations below. What the PR doesThe The fix correctly moves the inbox attribute injection to after the cache read/write, so the cache only stores handler-declared attributes, and inbox is applied fresh on every pipeline build from the instance's Code qualityPositives:
Observations and suggestions1. In both var useInboxAttribute = new UseInboxAttribute(step: 0, ...);But 2. Performance regression on hot paths (minor, worth noting)
This is a correct trade-off for correctness, but it's worth benchmarking in scenarios with high 3. Test isolation via static cache The new test class correctly calls 4. Post-cache After a cache hit, Test coverageThe 4 new tests in SummaryThe fix is correct and complete. The 🤖 Generated with Claude Code |
Summary
AddGlobalInboxAttributes/AddGlobalInboxAttributesAsynccalls outside the statics_preAttributesMementocache block in bothBuildPipelineandBuildAsyncPipeline_inboxConfigurationArgumentOutOfRangeExceptionwhen the handler factory couldn't createUseInboxHandlerAsync<T>Test plan
AsyncCommandProcessorPublishObservabilityTests— previously failing, now passCommandProcessorBuildDefaultInboxPublishAsyncTests— previously failing, now passes🤖 Generated with Claude Code