fix(batching): run both a direct and a batch handler under MultipleHandlerBehavior.Separated#3075
Closed
outofrange-consulting wants to merge 2 commits into
Conversation
added 2 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.
Member
|
Superseded by #3077, which keeps your two commits (and co-authorship) and adds a small refactor — extracting the Separated batch-resolution checks in |
Member
|
Just superseded by #3077 |
jeremydmiller
added a commit
that referenced
this pull request
Jun 11, 2026
…upersede fix(batching): run direct + batch (and multiple batch) handlers under Separated mode (supersedes #3075)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When a message type
Thas both a directHandle(T)handler and aBatchMessagesOf<T>()configuration (with a
Handle(T[])batch handler), the direct handler silently shadows the batch —the batched handler never runs, with no error or warning at startup or codegen.
Root cause: routing and executor resolution only consult the batch definitions when there is no
direct handler for the element type:
LocalRouting.FindRoutesreturns early whenHandlerGraph.CanHandle(messageType)is true; thebatch-queue route lives only in the
elsebranch.Executor.Build(...)andWolverineRuntimeIExecutorFactory.BuildFor(Type, Endpoint)resolveHandlerFor(...)first and fall back to aBatchDefinitiononly when that isnull.Because the default
BatchMessagesOf<T>()queue is the element type's own convention queue(
FindQueueForMessageType(T)), the batch and the direct handler also collide on a single localqueue, and a local queue pipeline resolves exactly one executor per message type.
This came up in a real app that batches domain events for an integration-event publisher while also
keeping a per-message telemetry handler for the same events.
Fix
Scoped to
MultipleHandlerBehavior.Separated, where the per-message handler and the batch handlerare naturally two independent consumers of the element type. Classic mode behavior is unchanged.
WolverineRuntime.HostService): when an element type has both a directhandler and a batch definition under Separated mode, move the batch onto its own local queue
(
<convention-name>-batch) so it no longer collides with the direct handler's queue. Only thecurrently-broken conflict case is touched.
LocalRouting.FindRoutes): fan the element type out to the batchqueue in addition to the direct handler's queue.
ExecutorFactory.BuildFor(Type, Endpoint)): the dedicated batchqueue always resolves to the
BatchingProcessor, even when a direct handler exists. Also, anelement type in this conflict case arriving from an external transport listener is relayed
to both local queues via a fan-out handler, so external arrivals run both handlers too.
HandlerGraph.BuildFanoutHandler: small refactor extracting theFanoutMessageHandler<T>construction so both the existing sticky-handler fan-out and the new external-arrival relay share it.
Net effect: under Separated mode a
T— whether published in-process or received from an externallistener — runs both the direct handler and the batch handler independently.
Tests
src/Testing/CoreTests/Bugs/Bug_separated_batch_and_single_handler.cs:-batchqueue, routing fansTout to both queues, and the batchqueue resolves to
BatchingProcessor<T>;Tarriving from an external (non-local) listener resolves to a fan-out to both local queues.Existing
batch_processing,sticky_message_handlers, routing, partitioning, local-transport andexecutor suites pass unchanged. Full
wolverine.slnxbuilds clean in Release.Docs
Added a "Combining a direct handler with batching" section to
docs/guide/handlers/batching.mddocumenting the Separated-mode behavior and the dedicated
-batchqueue.Update — multiple batched handlers under Separated
Follow-up commit extending the same Separated-mode scope.
Problem
Under
Separated, a batched array type with more than oneHandle(T[])handler has its handlerssplit onto per-handler sticky queues. The
BatchingProcessorre-enqueues a single produced batchonto the batch's own execution queue, which is none of those sticky queues, so
ExecutorFactory.BuildForthrewNoHandlerForEndpointExceptionand no batch handler ran. This isorthogonal to the direct-handler collision above — it reproduces even with no direct
Handle(T).Fix
In
ExecutorFactory.BuildFor, when the endpoint is the batch's local execution queue and the producedbatch-message type's chain has multiple Separated sticky handlers, resolve a
FanoutMessageHandler<T[]>that relays the produced batch to every sticky
Handle(T[])queue, so each runs independently. Reusesthe existing
HandlerGraph.BuildFanoutHandler. Covers custom batch message types too (keyed offIMessageBatcher.BatchMessageType, not just arrays).Classicmode is unchanged — multipleHandle(T[])handlers are still combined into one logical chain.Tests
src/Testing/CoreTests/Bugs/Bug_separated_multiple_batch_handlers.cs:produced_array_fans_out_to_each_sticky_handler_queue—BuildFor(T[], batchQueue)resolves toFanoutMessageHandler<T[]>;separated_multiple_batch_handlers_all_run— twoHandle(T[])handlers both run;separated_direct_handler_plus_multiple_batch_handlers_all_run— directHandle(T)and both batch handlers all run.Regression suites (batch, sticky, separated, partitioning, routing, local queues, executor) pass.
Full
wolverine.slnxbuilds clean in Release (176 projects, 0 warnings).