Skip to content

Latch receiver after stopping listener on shutdown - #2391

Merged
jeremydmiller merged 2 commits into
JasperFx:mainfrom
simplisafe-oss:latch-receiver-after-stopping-listener-on-shutdown
Mar 31, 2026
Merged

jeremydmiller merged 2 commits into
JasperFx:mainfrom
simplisafe-oss:latch-receiver-after-stopping-listener-on-shutdown

Conversation

@benjamin-alexander-simplisafe

Copy link
Copy Markdown
Contributor

Currently, wolverine latches all receivers immediately when shutdown is requested. This means that any messages consumed by listeners after that happens are deferred, which (for the same reasons as in #2347) can have poor performance characteristics.

This change moves latching each receiver to after its respective listener is stopped, so all consumed messages get a fair shot at being processed.

This also moves to shutting down the listeners in parallel so that they don't wait to latch before the previous ones finish shutting down. This was also done to the ParallelListener, with similar exception aggregation to the CompoundListener. I think this is a general improvement to the shutdown flow, but also avoids extra bad behavior from moving latching.

This avoids deferring messages during shutdown, which current happens
when the listener receives messages after the receiver is latched.
This should speed up the shutdown process in general (and,
importantly, means that the previous commit won't stall shutdown
by waiting to latch listener B until listener A is stopped and drained.
Technically this was a risk before, but since deferring messages is a
fast operation it wasn't as important). This should be safe, since each
StopAsync is wrapped in a try-catch. Also updated the ParallelListener
to stop children in parallel, which avoids one error from preventing
other attempts (similar to the CompoundListener) and should further
speed things up.
@jeremydmiller

Copy link
Copy Markdown
Member

@benjamin-alexander-simplisafe Hey, I'm going to try to get this in for the next release, but as I'm sure you completely understand, I need to review this one carefully!

@jeremydmiller
jeremydmiller merged commit f0e2d98 into JasperFx:main Mar 31, 2026
16 of 19 checks passed
jeremydmiller added a commit that referenced this pull request May 7, 2026
…lease-ownership loop

Closes GH-2671.

`DurableReceiver.executeWithRetriesAsync` was an unbounded `while (true)`
loop that ignored both the retry budget and the cancellation token. On
host shutdown, when the Npgsql `DbDataSource` had already been disposed
by DI ordering, every retry hammered a dead socket and emitted an
Error-level `SocketException` log line — sigridbra reported a flood of
these on application shutdown after upgrading from 5.22 to 5.27 (the
underlying bug was always latent, but PR #2391's shutdown reordering
made `DrainAsync.ReleaseIncomingAsync` reliably reach the dead socket).

Three changes to the helper:

1. Bounded retries — cap at `MaxReleaseRetries = 5`. After the budget
   is exhausted, log a single Error and return rather than spinning
   forever. Any owned envelopes left as `owner_id = node_id` are
   reclaimed by `DurabilityAgent`'s recovery polling on the next live
   node, so silently giving up is functionally safe for this
   best-effort cleanup path.

2. Cancellation-aware exit — when `_settings.Cancellation` is
   signalled (which happens on `DurabilitySettings.Cancel()` during
   shutdown), bail out of the retry loop on the very first failure.
   Retrying is futile when the data source is being torn down by the
   host. The cancellation token is also passed to `Task.Delay` for
   the backoff so we don't sit in a sleep through the entire shutdown
   sequence.

3. Demoted log level on cancelled exit — these failures are expected
   during teardown (the connection pool is gone) and don't deserve
   Error-level noise. Outside cancellation, the existing Error log is
   preserved with attempt counters added for diagnostics.

Regression coverage: new
`durable_receiver_release_incoming_during_shutdown` test class with
two cases:
* `drain_terminates_within_seconds_when_inbox_release_throws_repeatedly`
  asserts `DrainAsync` finishes inside a 5-second budget when the
  inbox throws `SocketException` on every call — the pre-fix code
  loops forever here.
* `drain_exits_immediately_when_cancellation_is_signalled` cancels
  `DurabilitySettings.Cancel()` first and asserts the drain exits
  inside one second, validating the shutdown short-circuit path.

Tests: full `Runtime.WorkerQueues` suite green (25/25) on net9.0 — the
existing receiver tests are unaffected, only the retry shape changed.

Co-Authored-By: Claude Opus 4.7 (1M context) <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

Development

Successfully merging this pull request may close these issues.

2 participants