Skip to content

Fix sticky handlers executing multiple times with global partitioning#2334

Merged
jeremydmiller merged 1 commit intomainfrom
fix/2303-sticky-handler-duplicate-execution
Mar 22, 2026
Merged

Fix sticky handlers executing multiple times with global partitioning#2334
jeremydmiller merged 1 commit intomainfrom
fix/2303-sticky-handler-duplicate-execution

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Closes #2303

Summary

When a message type was configured with both GlobalPartitioned() routes and explicit Publish().ToLocalQueue().AddStickyHandler() routes, sticky handlers executed multiple times per message. The GlobalPartitionedRoute fans out to sticky handler queues via companion local queues, but the explicit local queue routes also delivered to the same queues — causing duplicate execution.

Defense in depth with three fixes:

  1. MessageRouter<T>.DeduplicateRoutes() — Primary fix. When a GlobalPartitionedRoute is present, removes explicit local queue routes targeting sticky handler queues since the fanout already delivers to them.

  2. GlobalPartitionedRoute.StickyHandlerFanoutUris — Exposes which local queue URIs the fanout targets, enabling deduplication at the router level.

  3. FanoutMessageHandler — Safety net. Deduplicates target URIs with a HashSet to prevent double delivery when the same queue is reachable via multiple paths.

Reproduction

A Kafka-based test (sticky_handlers_with_global_partitioning.dual_publish_to_kafka_and_local_sticky_should_not_double_execute) reproduces the exact pattern from the issue:

  • Before fix: Handler A=2, Handler B=2 (each executed twice)
  • After fix: Handler A=1, Handler B=1 (each executed once)

Test plan

  • CoreTests: 1,189 passed, 0 failed
  • Kafka reproduction test confirms fix (handlers execute exactly once)
  • All Kafka compliance tests pass (54/54)
  • Solution compiles cleanly

🤖 Generated with Claude Code

…#2303)

When a message type had both GlobalPartitioned routes (which fan out from
companion local queues to sticky handler queues) AND explicit
Publish().ToLocalQueue().AddStickyHandler() routes, the message reached
each sticky handler via both paths, causing duplicate execution.

Defense in depth with three fixes:

1. MessageRouter<T>.DeduplicateRoutes(): When a GlobalPartitionedRoute is
   present, removes explicit local queue routes targeting sticky handler
   queues since the fanout already delivers to them.

2. GlobalPartitionedRoute.StickyHandlerFanoutUris: Exposes which local
   queue URIs the fanout will target, enabling deduplication.

3. FanoutMessageHandler: Deduplicates target URIs with a HashSet to
   prevent double delivery when the same queue is reachable via multiple
   paths.

Closes #2303

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sticky handlers in modular monolith re-execute multiple times with global partitioning enabled

1 participant