Refuse a competing Marten daemon under managed distribution (GH-3388)#3400
Merged
Conversation
Combining Wolverine-managed event subscription distribution with an explicit Marten-side daemon -- MartenDaemonModeIsSolo(), or AddAsyncDaemon(Solo|HotCold) -- is never valid. Marten hosts its own ProjectionCoordinator (an IHostedService that starts the shards) while Wolverine's distribution drives the same daemon through its own coordinator. The two compete, and the symptom is not an error but a HANG: a tracked catch-up that never completes. Nothing in the code that stalls points back at the registration that caused it. It cost a user a day (GH-3388), and his ask was exactly this: "a one-line warning would have saved me a day." The check runs at host start, not in MartenIntegration.Configure, because Marten applies AddAsyncDaemon's mode through ConfigureMarten -- which runs in registration order -- so a daemon choice made AFTER IntegrateWithWolverine is not yet visible while the integration is configuring. At start-up the store's options are final, whatever the call order. Both orders are covered by tests. This reverses the GH-3290 contract that an explicit daemon choice is "never overwritten". Preserving that choice silently is strictly worse than refusing it, because what it preserves is a deadlock. Our own TestHelpers fixtures had precisely this misconfiguration -- five of them called AddMarten() a second time on the SAME main store and hung AddAsyncDaemon(DaemonMode.Solo) off it, having already enabled managed distribution on the first call. They passed only because those tests warm the daemon before driving it, which is exactly why the cold path in GH-3388 went unnoticed. Fixed; the guard is what surfaced them. MartenTests 518/518. Full wolverine.slnx Release build clean. Refs #3388. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jul 13, 2026
This was referenced Jul 14, 2026
This was referenced Jul 17, 2026
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.
Refs #3388. For 6.18.0.
Why
Combining Wolverine-managed event subscription distribution with an explicit Marten-side daemon —
MartenDaemonModeIsSolo(), orAddAsyncDaemon(Solo|HotCold)— is never valid. Marten hosts its ownProjectionCoordinator(anIHostedServicethat starts the shards) while Wolverine's distribution drives the same daemon through its coordinator. The two compete, and the symptom is not an error but a hang: a tracked catch-up that never completes.This is what @uniquelau spent a day chasing in #3388 — his fixture had exactly this pair, and nothing in the code that stalls points back at the registration that caused it. His own ask when he found it:
It now fails at startup, naming the call to remove.
Where the check lives, and why
At host start, not in
MartenIntegration.Configure. Marten appliesAddAsyncDaemon's mode throughServices.ConfigureMarten(...), which runs in registration order — so a daemon choice made afterIntegrateWithWolverineis not yet visible while the integration is configuring. (I tried the obvious in-Configurecheck first; it caught the before-order and silently missed the after-order.) By start-up the store's options are final whatever the call order, and both orders are covered by tests.This reverses a GH-3290 contract
Bug_3290previously asserted that an explicit daemon choice is "never overwritten", in both call orders. Those two tests are now flipped to assert the throw. Preserving the user's choice silently is strictly worse than refusing it, because what it preserves is a deadlock. An explicit daemon choice without managed distribution is untouched and still honored — there is nothing to compete with.The uncomfortable part: our own fixtures were misconfigured
The guard immediately failed five of our own
TestHelpersfixtures. Each calledAddMarten()a second time on the same main store and hungAddAsyncDaemon(DaemonMode.Solo)off it, having already enabled managed distribution on the first call:They passed only because those tests warm the daemon before driving it — which is precisely why the cold path in #3388 went unnoticed for so long. Fixed here. The guard is what surfaced them, which is a decent argument that it earns its keep.
Verification
Bug_32907/7 (both call orders rejected; Solo without managed distribution still honored).MartenTests518/518.wolverine.slnxRelease build clean (0 warnings, 0 errors).🤖 Generated with Claude Code