Skip to content

fix(batching): run direct + batch (and multiple batch) handlers under Separated mode (supersedes #3075)#3077

Merged
jeremydmiller merged 3 commits into
mainfrom
feat-3075-batching-separated-supersede
Jun 11, 2026
Merged

fix(batching): run direct + batch (and multiple batch) handlers under Separated mode (supersedes #3075)#3077
jeremydmiller merged 3 commits into
mainfrom
feat-3075-batching-separated-supersede

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Supersedes #3075 by @outofrange-consulting (Geoffrey MARC), preserving the original commits and co-authorship, and adds a refactoring pass + test reorganization on top.

Original fix (from #3075)

Under MultipleHandlerBehavior.Separated, a message type T with both a direct Handle(T) and a BatchMessagesOf<T>() batch handler silently shadowed the batch — the direct handler won the single local-queue executor slot and the batch never ran. A companion case: a batched array type with multiple Handle(T[]) handlers threw NoHandlerForEndpointException because the produced batch re-enqueued onto a queue none of the per-handler sticky queues owned.

The fix (Separated mode only; Classic unchanged):

  • Dedicated batch queue (reassignBatchQueuesThatCollideWithHandlers in WolverineRuntime.HostService): when an element type has both a direct handler and a batch definition, move the batch onto its own <convention>-batch queue.
  • Additive local routing (LocalRouting.FindRoutes): fan the element type out to the batch queue in addition to the direct handler's queue.
  • Endpoint-aware executor (BuildFor): the dedicated batch queue always resolves to the BatchingProcessor; external-listener arrivals relay to both local queues; a produced batch with multiple Separated handlers fans out to every sticky queue.
  • HandlerGraph.BuildFanoutHandler: shared FanoutMessageHandler<T> construction.

Docs: "Combining a direct handler with batching" section in docs/guide/handlers/batching.md.

What this PR adds on top

Refactoring (no behavior change):

  • WolverineRuntime.Routing.cs — extracted LocalRouting.FindSeparatedBatchEndpoint(...) so FindRoutes reads as "discover senders, then add the batch endpoint if there is one."
  • Wolverine.ExecutorFactory.cs — the three inline Separated batch checks in BuildFor are now intention-named helpers (tryBuildDedicatedBatchQueueHandler, tryBuildExternalArrivalFanoutHandler, tryBuildMultipleBatchHandlerFanout), reducing BuildFor to a short null-coalescing resolution chain. (HandlerGraph.BuildFanoutHandler and the HostService extraction already landed in the original commits.)

Test reorganization — moved the two regression files out of CoreTests/Bugs into the feature suites:

  • End-to-end behavior → CoreTests.Acceptance.batching_with_separated_handlers (alongside batch_processing)
  • Routing/executor-resolution mechanics → CoreTests.Runtime.Routing.separated_batch_routing (alongside the routing suite)
  • Shared handler/message types live with the batching suite and are reused by the routing suite.

Verification

6 relocated tests pass; 52-test batching/routing/separated/sticky regression passes; wolverine.slnx -c Release builds clean (0 warnings, 0 errors).

Closes #3075

🤖 Generated with Claude Code

Geoffrey MARC and others added 3 commits June 11, 2026 16:42
…mode

When a message type has both a direct Handle(T) handler and a BatchMessagesOf<T>() configuration, the direct handler silently shadowed the batch: routing (LocalRouting.FindRoutes) and executor resolution (Executor.Build / ExecutorFactory.BuildFor) only consulted the batch definitions when there was no direct handler for the element type, so the batch handler never ran.

Under MultipleHandlerBehavior.Separated, treat the per-message handler and the batch handler as independent consumers of the element type: move the batch onto a dedicated '-batch' local queue so it no longer collides with the direct handler's queue, fan the element type out to both queues for in-process publishes, and relay to both local queues for messages arriving from an external transport listener.
…ed mode

Under MultipleHandlerBehavior.Separated, when the batched array type has more
than one Handle(T[]) handler, Wolverine splits them onto per-handler sticky
queues. The BatchingProcessor re-enqueues a single produced batch onto the
batch's own execution queue, which is none of those sticky queues, so executor
resolution threw NoHandlerForEndpointException and no batch handler ran.

Resolve the batch's execution queue to a fan-out handler that relays the
produced batch (T[] or a custom batch message type) to every sticky Handle(T[])
queue, so each batch handler runs independently. Works both with and without a
direct Handle(T) handler (the dedicated -batch queue case). Classic mode is
unchanged: multiple Handle(T[]) handlers are still combined into one chain.
… out of /Bugs

Follow-up refactor on top of the Separated-mode batching fix. No behavior change.

- WolverineRuntime.Routing.cs: extract LocalRouting.FindSeparatedBatchEndpoint(...)
  for the work that locates an element type's dedicated batch queue, so FindRoutes
  reads as "discover senders, then add the batch endpoint if there is one."
- Wolverine.ExecutorFactory.cs: extract the three inline Separated batch checks in
  BuildFor into intention-named helpers — tryBuildDedicatedBatchQueueHandler,
  tryBuildExternalArrivalFanoutHandler, tryBuildMultipleBatchHandlerFanout — so the
  method body is a short null-coalescing resolution chain.
  (HandlerGraph.BuildFanoutHandler and the HostService
  reassignBatchQueuesThatCollideWithHandlers extraction already landed in the
  original commits.)
- Move the two regression files out of CoreTests/Bugs into the feature suites:
  end-to-end behavior -> CoreTests.Acceptance.batching_with_separated_handlers
  (alongside batch_processing); routing/executor-resolution mechanics ->
  CoreTests.Runtime.Routing.separated_batch_routing (alongside the routing suite).
  The shared handler/message types live with the batching suite and are reused by
  the routing suite.

Co-Authored-By: Geoffrey MARC <geoffrey.marc@consulting-for.edenred.com>
Co-Authored-By: Claude Opus 4.8 (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.

1 participant