fix: move inbox attribute addition outside static pipeline cache#4061
fix: move inbox attribute addition outside static pipeline cache#4061holytshirt wants to merge 5 commits intomasterfrom
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>
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 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 |
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