Skip to content

Refuse a competing Marten daemon under managed distribution (GH-3388)#3400

Merged
jeremydmiller merged 1 commit into
mainfrom
managed-distribution-daemon-guard-3388
Jul 13, 2026
Merged

Refuse a competing Marten daemon under managed distribution (GH-3388)#3400
jeremydmiller merged 1 commit into
mainfrom
managed-distribution-daemon-guard-3388

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Refs #3388. For 6.18.0.

Why

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 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:

"when UseWolverineManagedEventSubscriptionDistribution is set together with a non-Disabled AsyncMode, the result is a silent stall rather than a startup diagnostic. A one-line warning … would have saved me a day."

It now fails at startup, naming the call to remove.

Where the check lives, and why

At host start, not in MartenIntegration.Configure. Marten applies AddAsyncDaemon's mode through Services.ConfigureMarten(...), which runs in registration order — so a daemon choice made after IntegrateWithWolverine is not yet visible while the integration is configuring. (I tried the obvious in-Configure check 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_3290 previously 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 TestHelpers fixtures. Each 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:

opts.Services.AddMarten(...).IntegrateWithWolverine(x => x.UseWolverineManagedEventSubscriptionDistribution = true);
opts.Services.AddMarten(...).AddAsyncDaemon(DaemonMode.Solo).IntegrateWithWolverine();   // SAME main store

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_3290 7/7 (both call orders rejected; Solo without managed distribution still honored).
  • Full MartenTests 518/518.
  • Full wolverine.slnx Release build clean (0 warnings, 0 errors).

🤖 Generated with Claude Code

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>
@jeremydmiller
jeremydmiller merged commit ef26afa into main Jul 13, 2026
26 checks passed
This was referenced Jul 14, 2026
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