Repository navigation
fix: RMQ.Sync producer selects confirms and subscribes handlers once per channel - #4576
Merged
Merged
Conversation
…per channel Root cause: RmqMessageProducer subscribed OnPublishSucceeded/OnPublishFailed to BasicAcks/BasicNacks and called ConfirmSelect() on every Send, with no per-channel guard, so a long-lived channel accumulated one handler pair and one confirm.select round trip per Send. The producer now tracks the channel it set up and compares by reference, since the Sync gateway replaces a closed channel rather than nulling it. It detaches from that channel on IOException and in Dispose (before the base disposes the channel), and only waits for confirms when the current channel is in confirm mode. Fixes #4567 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SpjnqGWbARr3Z3ofGc6C2x
iancooper
requested review from
DevJonny,
holytshirt and
preardon
as code owners
October 10, 2026 13:15
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.
Symptom
The RMQ.Sync
RmqMessageProducersubscribed its publisher-confirm handlers toBasicAcks/BasicNacks, and calledConfirmSelect(), on everySend. On a long-lived channel:+=copies the whole list, so the cost was O(N²).multiple=trueframe, each of those calls scanned and sorted the pending confirmations.confirm.selectround trip.Correctness was not affected. Since #4562, each confirmation is claimed with
TryRemovebefore it is raised, so none were lost or repeated.Confirmed root cause
The handler subscriptions and the
ConfirmSelect()call inSendWithDelayhad no guard for whether the channel was already set up.RmqMessageGateway.ConnectToBrokerreturns early while the channel is open, so every send reused the sameIModeland added another subscription and anotherconfirm.select. Nothing ever unsubscribed the handlers: not the IOException path, and notDispose.The cause was proven by tracing the code; the full trace is in
bugfixes/0054-rmq-sync-confirm-resubscribe/bugfix.md.Fix
_confirmChannel) and compares by reference, not by null. The Sync gateway never setsChannelback to null: it replaces a closed channel with a new instance, including when the pooled connection is dropped with no IOException. On a new channel, the producer unsubscribes from the old one, callsConfirmSelect(), then subscribes both handlers.Dispose'sfinally, beforebase.Disposedisposes the channel.DisposecallsWaitForConfirmsonly when the current channel is the one in confirm mode, instead of relying on a_confirmsSelectedflag that stayed true forever once set.The issue suggested mirroring RMQ.Async. That is only partly right. RMQ.Async guards on
Channel is not null, and itsChannelis only set back to null on dispose, so after a reset it never subscribes the replacement channel. Copying that check would have broken confirms on Sync after any reset.A first version detached the handlers after the confirmation callbacks finished. That broke
When_gateway_cleanup_throws_should_release_its_connection: whenWaitForConfirmsthrew, the detach was skipped, and a secondDisposethen removed handlers from a disposed model, which throwsObjectDisposedException. Detaching in thefinallyfixes this.Tests
The tests run against a live RabbitMQ broker and use the
CoalescingConfirmsRmqChanneltest double from #4562. The double now counts subscribers andConfirmSelectcalls, and records unsubscribes.When_sending_several_messages_on_one_channel_should_select_confirms_once: was red (ConfirmSelectcalled 3 times, not 1).When_the_channel_resets_should_move_confirm_handlers_to_the_new_channel: was red (the failed channel kept 2 handlers).When_the_shared_connection_is_dropped_should_select_confirms_on_the_new_channel: covers silent replacement. It passed as soon as it was written, and stays as a guard against a null-style check.Full
Paramore.Brighter.RMQ.Sync.Testsrun on net9.0 and net10.0: 200 passed and 1 skipped per framework. The only failures are 9 mTLS acceptance tests per framework, which need client certificates (./tests/generate-test-certs.sh) that were not present locally.Out of scope: follow-ups found during diagnosis
TryAddfailure is ignored, so a new message's confirmation can be lost, and the new channel's ack can confirm the old message instead. RMQ.Async has the same gap.finally.Fixes #4567
🤖 Generated with Claude Code
https://claude.ai/code/session_01SpjnqGWbARr3Z3ofGc6C2x