Skip to content

Adopt JasperFx.Events 2.36.2: clear resolved daemons on coordinator stop, idempotent AddAsyncDaemon - #5058

Merged
jeremydmiller merged 1 commit into
masterfrom
fix/5056-clear-resolved-daemons
Jul 27, 2026
Merged

Adopt JasperFx.Events 2.36.2: clear resolved daemons on coordinator stop, idempotent AddAsyncDaemon#5058
jeremydmiller merged 1 commit into
masterfrom
fix/5056-clear-resolved-daemons

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #5055, closes #5056.

The bug

At Kubernetes pod shutdown, any second Pause/Stop pass over the projection coordinator — double AddAsyncDaemon hosted-service registration, user pause + host stop, Wolverine quiesce + host stop — fanned StopAllAsync() out over daemons the first pass had already disposed. Reading _cancellation.Token off the disposed CancellationTokenSource threw ObjectDisposedException, which ProjectionCoordinatorBase.PauseAsync logged as Error while trying to stop daemon agents once per daemon on every shutdown (the #5055 report).

There was also a latent second bug: after StopAsync, ResumeAsync and the daemon accessors handed back disposed daemons from the coordinator's cache.

The fix

JasperFx/jasperfx#575 (shipped in 2.36.2, tracked as #574) made the daemon stop path safe after disposal — idempotent Dispose(), StopAllAsync() no-ops on a disposed daemon, PauseAsync logs ObjectDisposedException at Debug — and added the compile-breaking protected abstract void ClearResolvedDaemons() seam called by ProjectionCoordinatorBase.StopAsync after it disposes the resolved daemons.

This PR is the Marten side (#5056):

  • Bump JasperFx / JasperFx.Events / source generators 2.36.1 → 2.36.2
  • ProjectionCoordinator implements ClearResolvedDaemons() by resetting the _daemons cache under _daemonLock, so a repeated Pause/Stop has nothing to fan out over and a later ResumeAsync or daemon accessor builds fresh daemons instead of returning disposed instances
  • ExplicitProjectionCoordinator.StopAsync now disposes its daemons after stopping them and clears its cache the same way; the external manager re-acquires fresh daemons through the accessors (the same pattern the forced catch-up flow already uses)
  • Both AddAsyncDaemon overloads skip re-registration when Marten's own coordinator is already registered, so a doubled call can't register a second IHostedService forwarding that makes the host stop the same coordinator twice

Tests

New Bug_5055_5056_coordinator_double_stop in DaemonTests:

  • double StopAsync on a live coordinator logs no stop daemon agents errors and no ObjectDisposedException
  • StopAllAsync() on an already-disposed daemon no-ops (proves the 2.36.2 upstream guard is in the box)
  • daemon accessors after StopAsync return fresh, usable daemons — not the disposed instances
  • both AddAsyncDaemon overloads are idempotent (single coordinator + single hosted-service registration after a doubled call)

Full DaemonTests suite green on net10.0: 254 passed / 0 failed.

🤖 Generated with Claude Code

…top, idempotent AddAsyncDaemon (#5056)

Closes #5055, closes #5056.

At Kubernetes pod shutdown, any second Pause/Stop pass over the projection
coordinator (double AddAsyncDaemon hosted-service registration, user pause +
host stop, Wolverine quiesce + host stop) fanned StopAllAsync out over daemons
the first pass had already disposed. Reading _cancellation.Token off the
disposed CancellationTokenSource threw ObjectDisposedException, logged as
"Error while trying to stop daemon agents" once per daemon on every shutdown.

JasperFx.Events 2.36.2 (#574/#575) makes the daemon stop path safe
after disposal and adds the abstract ClearResolvedDaemons() seam that
ProjectionCoordinatorBase.StopAsync calls after disposing daemons. This is
the Marten side:

- ProjectionCoordinator implements ClearResolvedDaemons() by resetting the
  _daemons cache, so a repeated Pause/Stop has nothing to fan out over and a
  later ResumeAsync or daemon accessor builds fresh daemons instead of
  handing back disposed instances
- ExplicitProjectionCoordinator.StopAsync now disposes its daemons after
  stopping them and clears its cache the same way
- Both AddAsyncDaemon overloads skip re-registration when Marten's own
  coordinator is already registered, so a doubled call can't register a
  second IHostedService forwarding that stops the same coordinator twice

Co-Authored-By: Claude Fable 5 <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

1 participant