You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
No supported way to force async projections to catch up under Wolverine-managed event subscription distribution (marten#4904 left the active path to the coordinator) #3697
Under UseWolverineManagedEventSubscriptionDistribution there is no supported way to force async projections to catch up in an integration test, so every suite has to reimplement "quiesce the agents, then force catch-up" — and racing the still-running daemon while doing so.
Background
marten#4904 changed IHost.ForceAllMartenDaemonActivityToCatchUpAsync() to a passive read-only wait when the store runs in DaemonMode.ExternallyManaged, which is the mode Wolverine puts Marten in under Wolverine-managed event-subscription distribution. It no longer builds projections on demand, so a "caught up" wait can return before the read models exist, and the issue explicitly left the active path ("quiesce the external agents, then force catch-up") to the external coordinator. As far as we can tell Wolverine does not implement that path, so there is nothing to call.
What we ended up doing
Our integration suite (sharded multi-tenant store, UseTenantPartitionedEvents, agents distributed by Wolverine) drives it by hand:
varcoordinator=services.GetRequiredService<IProjectionCoordinator>();awaitcoordinator.PauseAsync();// stop agent assignment so CatchUp is the sole writertry{foreach(vardaemoninawaitcoordinator.AllDaemonsAsync()){awaitdaemon.StopAllAsync();awaitdaemon.CatchUpAsync(cancellation);}}finally{awaitcoordinator.StartAsync(cancellation);}
It works, but "sole writer" is not actually guaranteed: the running daemon and the forced catch-up still overlap briefly, and per test run we see a handful of
ProgressionProgressOutOfOrderException ("multiple processes try to process the projection"), and
23505: duplicate key value violates unique constraint "pk_mt_event_progression", Key (name)=(<projection>:V<n>:All:<tenant>) already exists — the same race on the first progression row of a shard, where the batch still INSERTs rather than UPDATEs.
Both are transient (a retried catch-up updates the row that now exists), so we retry a few times and then swallow exactly those two. Every suite that uses Wolverine-managed distribution presumably needs the same private workaround, including the knowledge of which exceptions are "just the race".
To be clear: we are not asking you to make the duplicate key go away in our workaround — we are asking for the supported path so the workaround is unnecessary.
Ask
Something like IProjectionCoordinator.ForceCatchUpAsync(CancellationToken) (or making Marten's ForceAllMartenDaemonActivityToCatchUpAsync delegate to the coordinator when the store is ExternallyManaged) that:
suspends agent assignment cluster-wide for the duration, so the forced catch-up really is the only writer of each progression row;
drives every locally reachable daemon to catch up, including per-(database, tenant) shards;
restores the previous assignment state afterwards.
Even without cluster-wide coordination, a single-node/test-oriented variant would remove the two failure modes above, because the insert/update race only exists while both writers are live.
Happy to test a prerelease against a suite that reproduces this a few times per run.
Thanks for the detailed writeup — the two exceptions you're catching turned out to be the most useful part of it.
We already implement this path; it just wasn't reachable from where you're calling. Wolverine added the coordinator-driven catch-up in GH-3349, but only wired it up as a TrackActivity() stage — PauseThenCatchUpOnMartenDaemonActivity(). If your suite isn't driving the work through a tracked session, there was nothing to call, which is exactly the gap you hit.
awaithost.PauseThenCatchUpOnMartenDaemonActivityAsync();awaithost.PauseThenCatchUpOnMartenDaemonActivityAsync(CatchUpMode.AndDoNothing);// leave them pausedawaithost.PauseThenCatchUpOnMartenDaemonActivityAsync<IMyStore>();// ancillary store
Also available off IServiceProvider.
On the race: it isn't one you need to retry around — it's produced by the workaround itself. daemon.CatchUpAsync() under a paused coordinator is a second writer of each progression row, which is what raises ProgressionProgressOutOfOrderException and the 23505 on pk_mt_event_progression's first insert. The supported path never calls CatchUpAsync. It resumes the agents that already own those shards and waits for non-stale data, so there is exactly one writer and neither error has anything to arise from. That also answers your ask #1 more strongly than "suspend assignment for the duration" would have — sole-writer is structural rather than best-effort.
Ask #2 (per-(database, tenant) shards) is covered — WaitForNonStaleProjectionDataAsync walks every database in the store. Ask #3 is CatchUpMode, applied in a finally so a timeout still restores state.
This is going out in 6.24.1 tonight. Would appreciate you running it against the suite that reproduces this a few times per run — you offered, and that's the one place we can't reproduce your sharded multi-tenant shape locally.
Activity
jeremydmiller commentedon Jul 30, 2026
Thanks for the detailed writeup — the two exceptions you're catching turned out to be the most useful part of it.
We already implement this path; it just wasn't reachable from where you're calling. Wolverine added the coordinator-driven catch-up in GH-3349, but only wired it up as a
TrackActivity()stage —PauseThenCatchUpOnMartenDaemonActivity(). If your suite isn't driving the work through a tracked session, there was nothing to call, which is exactly the gap you hit.PR #3735 adds the standalone entry point:
Also available off
IServiceProvider.On the race: it isn't one you need to retry around — it's produced by the workaround itself.
daemon.CatchUpAsync()under a paused coordinator is a second writer of each progression row, which is what raisesProgressionProgressOutOfOrderExceptionand the23505onpk_mt_event_progression's first insert. The supported path never callsCatchUpAsync. It resumes the agents that already own those shards and waits for non-stale data, so there is exactly one writer and neither error has anything to arise from. That also answers your ask #1 more strongly than "suspend assignment for the duration" would have — sole-writer is structural rather than best-effort.Ask #2 (per-(database, tenant) shards) is covered —
WaitForNonStaleProjectionDataAsyncwalks every database in the store. Ask #3 isCatchUpMode, applied in afinallyso a timeout still restores state.This is going out in 6.24.1 tonight. Would appreciate you running it against the suite that reproduces this a few times per run — you offered, and that's the one place we can't reproduce your sharded multi-tenant shape locally.
Merge pull request #3735 from JasperFx/gh-3697/marten-force-catchup
feat(marten): standalone force-catch-up under Wolverine-managed distr…