Repository navigation
fix(messaging): publish on a live channel after a reconnect during the declare - #2844
ishan-1010 wants to merge 1 commit into
Conversation
…e declare With retry=True the declare runs in a nested ensure. When that reconnects it closes the channel the producer was bound to, and _publish kept using the channel it read before the declare. Revive the producer onto the connection's live channel and prepare the message again. Fixes celery#2841.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2844 +/- ##
=======================================
Coverage 85.77% 85.78%
=======================================
Files 80 80
Lines 11595 11599 +4
Branches 1403 1404 +1
=======================================
+ Hits 9946 9950 +4
Misses 1428 1428
Partials 221 221 ☔ View full report in Codecov by Harness. |
MehrazRumman
left a comment
There was a problem hiding this comment.
Read 67bdbaf against #2841. The cause is as described. _imaybe_declare() runs the declare through entity.channel.connection.client.ensure(...). On a recoverable error, Connection.ensure() calls collect(), which closes every channel on the connection, including the producer's. Then it revives only the entity, onto the new default_channel. _publish() then publishes on the channel it read before the declare. Checking channel.connection after the declare and reviving the producer fixes this for producers created from a Connection.
Preparing the message again is safe. Nothing between the two prepare_message() calls changes headers or properties, and on the virtual transports prepare_message() only uses setdefault() on properties, so calling it twice gives the same message.
All 37 checks pass on 67bdbaf.
One case is still left, inline.
| maybe_declare(entity, retry=retry, **retry_policy) | ||
| if channel.connection is None: | ||
| # A reconnect during the declare closed our channel. | ||
| self.revive(self.connection.default_channel) |
There was a problem hiding this comment.
This still fails for a producer created from a channel, such as Producer(conn.channel()). Its __connection__ is None, so Producer.connection falls back to self.channel.connection.client. Here the channel is already closed, so that raises AttributeError, the property catches it and returns None, and this line raises AttributeError: 'NoneType' object has no attribute 'default_channel'. That's still not a recoverable error, so the outer ensure gives up as before.
conn.Producer() and kombu's ProducerPool, which Celery uses, pass the Connection, so they're covered. Code that passes a channel isn't.
Reading the connection before the declare, while the channel is still open, would cover both:
channel = self.channel
connection = self.connection
...
if channel.connection is None:
# A reconnect during the declare closed our channel.
self.revive(connection.default_channel)A second case in the test, with Producer(conn.channel()), would show it. As written now it should fail with the NoneType error above.
Fixes #2841.
With
retry=True,Producer._publishdeclares the queue through a nestedensure(added in #2174). When that reconnects, the producer's channel gets closed but_publishkeeps publishing on the one it read before the declare, which raises an AttributeError on Redis and never retries. This revives the producer onto the connection's live channel after the declare and prepares the message again.Tested with the injection from the issue on a real Redis (one ConnectionError in
_queue_bind). On main the publish raises and the queue stays empty, with this change the queue length is 1. The new unit test uses the memory transport and fails without the change.@auvipy