Skip to content

GH-3950: assert that listeners resume after the connection dies underneath them - #3961

Merged
jeremydmiller merged 1 commit into
mainfrom
gh-3950-connection-recovery-coverage
Aug 16, 2026
Merged

jeremydmiller merged 1 commit into
mainfrom
gh-3950-connection-recovery-coverage

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Addresses suggestion 2 of #3950 — "Confirm Wolverine recovers."

Why

A rejected delivery tag on a busy channel can cost the whole connection: SessionManager.Lookup does an indexer read on a dictionary it may have just mutated, throws KeyNotFoundException on the receive loop, and RabbitMQ.Client escalates that into a library-initiated close (code=541). Wolverine cannot catch it — it is raised on the client's own MainLoop thread.

So the only part Wolverine owns is what happens next, and as the issue puts it, that part was observed to work and asserted nowhere. This turns it into a guarantee.

What it does

Kills the connection from the broker side via the management API rather than provoking the 541 race. The race is timing-dependent and would make a flaky test, and a forced close is indistinguishable from Wolverine's point of view — an unsolicited shutdown it did not initiate. What's pinned is the recovery contract, not the client bug.

Asserting on ReconnectAttempts alone would be too weak: a recovered connection is not a recovered consumer. ConnectionMonitor has to rebuild every tracked channel agent on recovery, and #3370 is exactly the case where an agent falling out of that list was one drop away from being permanently ghosted. So the real assertion is that a message published after the kill is still received — with a baseline send first, so a failure can't be read as "it never worked".

Two things I measured rather than assumed

Both of which my first attempt got wrong, and both now encoded in the test:

  • /api/connections lags the actual TCP connect on RabbitMQ 4.x. Measured locally as empty at 4s and populated at 8s, with the host already up and passing traffic. Reading it once reported "no connection to kill" against a perfectly healthy host, so the lookup polls.
  • The kill is scoped by a per-test ClientProvidedName, so a run cannot take down a concurrently executing test's connections. Bobcat runs this project across several worker processes against one broker, which makes that a correctness requirement rather than tidiness.

Verification

New test, repeated isolated runs 4/4, ~10s each
Full Wolverine.RabbitMQ.Tests 503/503, with the kill running alongside everything else

The suite result matters more than usual here: a test that force-closes broker connections is exactly the kind that can perturb its neighbours, so I ran the whole project rather than the class.

Not included

  • Suggestion 1 (report the SessionManager.Lookup defect upstream to rabbitmq-dotnet-client) — that's an outward-facing action, so I've left it for you to file.
  • Suggestion 3 (whether a rejected settle should proactively quiesce the channel) — a behavior change rather than coverage, worth its own PR and its own discussion.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JG8Un6iNeyXECKJk3jo5uC

…neath them

The issue's second suggestion. A rejected delivery tag on a busy channel can cost
the whole RabbitMQ connection -- RabbitMQ.Client's SessionManager.Lookup does an
indexer read on a dictionary it may have just mutated, throws
KeyNotFoundException on the receive loop, and escalates that to a
library-initiated close (code=541). Wolverine cannot catch it; it is raised on the
client's own MainLoop thread. So the only part Wolverine owns is what happens
next, and that part was observed to work and asserted nowhere.

Kills the connection from the BROKER side through the management API rather than
provoking the 541 race. The race is timing dependent and would make a flaky test,
and a forced close is indistinguishable from Wolverine's point of view -- an
unsolicited shutdown it did not initiate. What is pinned here is the recovery
contract, not the client bug.

Asserting on ReconnectAttempts alone would be too weak: a recovered CONNECTION
does not mean a recovered CONSUMER. ConnectionMonitor has to rebuild every tracked
channel agent on recovery, and #3370 is the case where an agent falling out of
that list was one connection drop away from being permanently ghosted. So the
real assertion is that a message published after the kill is still received, with
a baseline send first so a failure cannot be read as "it never worked".

Two things measured rather than assumed while writing this, both of which the
first attempt got wrong:

 - /api/connections is fed by RabbitMQ's connection tracking and LAGS the actual
   TCP connect on 4.x. Locally it was empty at 4s and populated at 8s with the
   host already up and passing traffic. Reading it once reported "no connection to
   kill" against a perfectly healthy host, so the lookup polls.
 - The kill is scoped by a per-test ClientProvidedName, so a run cannot take down
   a concurrently executing test's connections. Bobcat runs this project across
   several worker processes against one broker, which makes that a correctness
   requirement rather than tidiness.

Local: the new test 4/4 across repeated isolated runs (~10s each), and the full
Wolverine.RabbitMQ.Tests suite 503/503 with the kill running alongside everything
else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JG8Un6iNeyXECKJk3jo5uC
@jeremydmiller
jeremydmiller merged commit a484968 into main Aug 16, 2026
37 checks passed
esond pushed a commit to esond/wolverine that referenced this pull request Aug 18, 2026
Covers JasperFxGH-3956 (store agnostic document contracts), JasperFxGH-3954 (durability agents no
longer assigned to incapable nodes), JasperFxGH-3953 (persistence provider owns the chain's
transaction), JasperFxGH-3955 (idle reaper latching durable endpoints), and the three RabbitMQ
changes: the community DrainWaitForPrefetch opt-in (JasperFx#3796), its non-terminal-stop
follow-up (JasperFx#3960), and the JasperFxGH-3950 pair (JasperFx#3961 coverage, JasperFx#3964 channel quiesce).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JG8Un6iNeyXECKJk3jo5uC
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