Fix RabbitMQ agent permanently latched Disconnected after a channel-only shutdown (#3171)#3187
Merged
Merged
Conversation
#3171) A RabbitMQ channel can shut down while the underlying connection stays alive and without surfacing a callback exception (a channel-only shutdown). Previously the agent latched into AgentState.Disconnected forever: EnsureInitiated() short-circuited on the stale, non-null channel and HandleChannelShutdownAsync never rebuilt. Senders then threw "... is disconnected" on every send and listeners silently stopped consuming until a process restart. - EnsureInitiated() now treats a non-null but closed channel as dead and rebuilds it (tearing down the stale channel under the lock first), so senders heal lazily on the next send. The under-lock IsOpen re-check avoids racing the existing callback-exception rebuild. - Add a HandleUnexpectedChannelShutdown() hook fired for non-Application channel shutdowns. RabbitMqListener overrides it to eagerly rebuild and re-consume (listeners sit blocked and cannot self-heal lazily), guarded on ConnectionIsLive so a full connection drop is left to the ConnectionMonitor recovery path. ReconnectedAsync() is hardened to tolerate cancelling consumers on an already-dead channel. - Regression coverage for both the sender and listener recovery paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jun 23, 2026
Closed
This was referenced Jun 29, 2026
This was referenced Jul 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3171.
Problem
A RabbitMQ channel can shut down while the underlying connection stays alive — and without surfacing a callback exception (a channel-only shutdown). When that happens the agent latches into
AgentState.Disconnectedand never recovers until the process restarts:RabbitMqSender.SendAsyncthrowsInvalidOperationException: The RabbitMQ agent for ... is disconnectedon every send; the durable outbox retries forever.Root cause (confirmed against
main): the two channel-failure handlers were asymmetric.HandleChannelExceptionAsync(callback exception) tears down, nullsChannel, rebuilds, setsConnected. ✅HandleChannelShutdownAsync(channel shutdown) only setState = Disconnectedand returned without nullingChannelor rebuilding.EnsureInitiated()then short-circuited on the stale, non-null channel (if (Channel is not null) return;), so it never rebuilt. The only recovery paths both miss this case: callback-exception recovery needs an exception, andConnectionMonitor/ReconnectedAsyncneeds the whole connection to drop.Fix
EnsureInitiated()now treats a non-null-but-closed channel as dead: it rebuilds wheneverChannelis missing or notIsOpen, tearing down the stale channel under the lock first. The under-lockIsOpenre-check avoids racing the existing callback-exception rebuild and double-restarts, and theApplication-initiator close is left alone. This heals senders lazily on the nextSendAsync— no fire-and-forget tasks.HandleUnexpectedChannelShutdown()hook is invoked fromHandleChannelShutdownAsyncfor non-Applicationshutdowns. Listeners sit blocked on the broker and never callEnsureInitiated()again on their own, soRabbitMqListeneroverrides it to eagerly rebuild and re-consume viaReconnectedAsync(). It's guarded onConnectionIsLiveso a full connection drop is left to theConnectionMonitorrecovery path (no double restart).RabbitMqListener.ReconnectedAsync()is hardened to tolerate cancelling consumers on an already-dead channel before tearing down and rebuilding.Tests
Bug_3171_channel_only_shutdown_recoverycovers both paths against a live broker:sender_recovers_after_a_channel_only_shutdown— closes only the channel, asserts the agent latchesDisconnectedwith a stale closed channel, then recovers onEnsureInitiated()and a real send succeeds.listener_resumes_consuming_after_an_unexpected_channel_shutdown— forces a broker-initiated (non-Application) channel close and asserts the listener rebuilds on a fresh channel and resumes consuming end-to-end.Both tests fail without the fix (the sender throws immediately; the listener times out at 30s, never recovering) and pass with it. The surrounding RabbitMQ lifecycle suite (
end_to_end,exclusive_listeners,rate_limiting_end_to_end) passes; the only other failures observed were the pre-existing shared-broker flakes (fan_out/direct_exchange) which pass in isolation.🤖 Generated with Claude Code