Skip to content

fix: scope dispatch plans per queue and serialize RPC replies through the consumer channel gate#288

Merged
Vulthil merged 1 commit into
mainfrom
fix/rabbitmq-dispatch-scoping
Jul 4, 2026
Merged

fix: scope dispatch plans per queue and serialize RPC replies through the consumer channel gate#288
Vulthil merged 1 commit into
mainfrom
fix/rabbitmq-dispatch-scoping

Conversation

@Vulthil

@Vulthil Vulthil commented Jul 4, 2026

Copy link
Copy Markdown
Owner

This fixes three defects in the RabbitMQ consume/publish paths.

1. Consuming the same message type on two queues ran every queue's consumers on every delivery. Execution plans were assembled into a single registry shared by all queues on the bus, keyed only by message URN, and dispatch ran all handlers in the plan with no per-queue filter. With queues A and B both consuming OrderCreated, a delivery on A also ran B's consumers — N× execution per publish, and cross-queue retry policies/partition lanes applied to the wrong queue's consumers. The bus now builds one plan cache per queue (fresh per start attempt, preserving the startup-retry idempotency), so a worker's plan lookups resolve only the handlers its own queue registered. The core MessageExecutionRegistry keeps its accumulate-per-instance behavior — the in-memory test harness depends on it, since it dispatches each produced message exactly once — and its docs now spell out which model a transport should use. Its request-consumer uniqueness check is now keyed per (queue, URN), matching what its exception message always claimed.

2. RPC reply publishes bypassed the consumer channel's write gate. Every write on a worker's shared channel (ack, nack, retry republish, fault publish) serializes through the worker's channel gate — except request/reply responses, which received the raw IChannel and published directly. On an RPC queue with ConcurrencyLimit > 1, a multi-frame reply publish could interleave with a parallel ack on the same channel; RabbitMQ channels are documented as not safe for concurrent use. Handler dispatch now receives a gate-wrapped publish delegate instead of the channel, so replies serialize with the settles in the same domain, and handlers can no longer perform arbitrary channel operations.

3. The message-type/URN caches raced on the publish hot path. Singleton publishers lazily register unconfigured message types per operation into plain Dictionary fields. Concurrent first publishes of distinct types — a normal startup burst — corrupted the maps (reproduced: "Operations that change non-concurrent collections must have exclusive access" and lost URN-index writes). The caches are now ConcurrentDictionary with GetOrAdd, converging concurrent first lookups on a single configuration instance while keeping the hot read path allocation-free and preserving explicit-configuration overwrite semantics.

New regression tests cover per-queue dispatch isolation (two queues, one type: each consumer runs exactly once per fanned-out publish and never on the other queue's delivery), queue-scoped plan lookup, per-queue RPC uniqueness, reply serialization under parallel dispatch (a recording channel asserts no interleaved writes), and concurrent first-lookup stress on the configuration cache. All six fail on the previous code. Existing bus, worker, and test-harness suites pass unmodified; no public API changes.

Backport to v1.0: recommended — internal-only fixes for defects present since 1.0.0.

…he channel gate, and make publish-path type caches thread-safe
@Vulthil Vulthil added the backport v1.0 Backport this PR to the v1.0 servicing branch label Jul 4, 2026
@Vulthil
Vulthil merged commit 879cd7a into main Jul 4, 2026
7 checks passed
@Vulthil

Vulthil commented Jul 4, 2026

Copy link
Copy Markdown
Owner Author

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

Labels

backport v1.0 Backport this PR to the v1.0 servicing branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant