Skip to content

fix: move inbox attribute addition outside static pipeline cache#4061

Open
holytshirt wants to merge 5 commits intomasterfrom
fix/async-pipeline-duplicate-inbox
Open

fix: move inbox attribute addition outside static pipeline cache#4061
holytshirt wants to merge 5 commits intomasterfrom
fix/async-pipeline-duplicate-inbox

Conversation

@holytshirt
Copy link
Copy Markdown
Member

@holytshirt holytshirt commented Apr 4, 2026

Summary

  • Moves AddGlobalInboxAttributes / AddGlobalInboxAttributesAsync calls outside the static s_preAttributesMemento cache block in both BuildPipeline and BuildAsyncPipeline
  • The cache now only stores the handler's declared pipeline attributes — inbox attributes are applied fresh each time based on the current _inboxConfiguration
  • Previously, inbox attributes were cached alongside handler attributes, causing cross-contamination between pipeline builders with different inbox configurations:
    • Tests without inbox config would pick up cached inbox attributes from earlier tests → ArgumentOutOfRangeException when the handler factory couldn't create UseInboxHandlerAsync<T>
    • Tests with inbox config hitting a cache populated without inbox → inbox handler never added → inbox assertions failed
    • On cache miss with inbox config, the async version had a duplicate call that added the inbox handler twice

Test plan

  • CI core tests pass (557 total, 547 passed, 0 failed)
  • AsyncCommandProcessorPublishObservabilityTests — previously failing, now pass
  • CommandProcessorBuildDefaultInboxPublishAsyncTests — previously failing, now passes
  • No regressions in other pipeline builder tests

🤖 Generated with Claude Code

codescene-delta-analysis[bot]

This comment was marked as outdated.

@holytshirt holytshirt force-pushed the fix/async-pipeline-duplicate-inbox branch from 7d03745 to e8d5ce7 Compare April 4, 2026 21:44
codescene-delta-analysis[bot]

This comment was marked as outdated.

@holytshirt holytshirt added the Bug label Apr 4, 2026
@holytshirt holytshirt force-pushed the fix/async-pipeline-duplicate-inbox branch from e8d5ce7 to ee1ccc2 Compare April 4, 2026 21:59
codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@holytshirt holytshirt changed the title fix: remove duplicate AddGlobalInboxAttributesAsync call in async pipeline builder fix: move inbox attribute addition outside static pipeline cache Apr 4, 2026
holytshirt and others added 2 commits April 5, 2026 00:22
…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>
@holytshirt holytshirt force-pushed the fix/async-pipeline-duplicate-inbox branch from f5134dd to 4c2955b Compare April 4, 2026 23:22
codescene-delta-analysis[bot]

This comment was marked as outdated.

- 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>
codescene-delta-analysis[bot]

This comment was marked as outdated.

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>
codescene-delta-analysis[bot]

This comment was marked as outdated.

…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>
Copy link
Copy Markdown

@codescene-delta-analysis codescene-delta-analysis bot left a comment

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
Copy link
Copy Markdown

claude bot commented Apr 6, 2026

Code Review

The PR has evolved well from the initial version. Here is a fresh assessment of the current state.

What is now correct

Cache 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 observations

FindHandlerMethod() 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

Area Status
Cache contamination fix Correct
Async duplicate-call fix Correct
Mutation of cached attributes Fixed
Both cache-reversal directions tested Now covered
Redundant null check Removed
Double FindHandlerMethod() reflection per call Minor follow-up
s_postAttributesMemento never populated Pre-existing follow-up

The fix is correct and complete. Ready to merge.

Copy link
Copy Markdown
Member

@iancooper iancooper left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@holytshirt
Copy link
Copy Markdown
Member Author

@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.

@iancooper
Copy link
Copy Markdown
Member

I guess

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants