fix(redis): do not remove on_poll_start from on_tick on disconnect#2492
Conversation
|
@ChickenBenny please have a look |
There was a problem hiding this comment.
Pull request overview
Fixes a Redis transport reconnection race where a stale disconnect handler could remove the newly-registered poll callback from the event loop, leaving workers alive but no longer consuming after a broker restart.
Changes:
- Stop removing
on_poll_startfromhub.on_tickduring Redis disconnect handling to avoid the reconnection race. - Update the Redis transport unit test to assert
on_poll_startremains registered after disconnect. - Expand inline commentary/test docstrings to document the “catatonic worker” failure mode and why
on_poll_startmust remain.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
kombu/transport/redis.py |
Keeps on_poll_start registered on disconnect to prevent the on-tick callback race that breaks consumption after reconnect. |
t/unit/transport/test_redis.py |
Adjusts the disconnect cleanup test to assert on_poll_start stays in on_tick regardless of fds state. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2492 +/- ##
==========================================
+ Coverage 82.37% 82.44% +0.06%
==========================================
Files 79 79
Lines 10119 10142 +23
Branches 1160 1165 +5
==========================================
+ Hits 8336 8362 +26
+ Misses 1580 1579 -1
+ Partials 203 201 -2 ☔ View full report in Codecov by Harness. |
fd9ee39 to
7145b6e
Compare
themavik
left a comment
There was a problem hiding this comment.
Keeping on_poll_start on loop.on_tick and pruning cycle._fd_to_chan in _on_disconnect targets the celery#8030 catatonic-worker race; the fileno() >= 0 guard and raw-fd branch cover the stale-map cases your tests assert.
|
LGTM! |
are you handling supporting PR for this or yours are different ones beside this? |
There was a problem hiding this comment.
Pull request overview
Fixes a Redis transport reconnection race where a stale disconnect callback can remove the event-loop poll trigger (on_poll_start), leaving workers alive but no longer consuming after broker restarts.
Changes:
- Stop removing
on_poll_startfromloop.on_tickduring Redis disconnect handling. - Add disconnect-time pruning of poller fd tracking (
cycle._fd_to_chan) to prevent re-registering stale sockets. - Expand Redis transport unit tests to cover new disconnect cleanup behavior and edge cases.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
kombu/transport/redis.py |
Adjusts disconnect cleanup to keep on_poll_start registered and prunes _fd_to_chan entries. |
t/unit/transport/test_redis.py |
Updates and adds tests validating on_tick behavior and fd pruning across multiple disconnect scenarios. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
auvipy
left a comment
There was a problem hiding this comment.
please cross check the review comments, and failing tests
The _on_disconnect handler in register_with_event_loop removed the
on_poll_start callback from loop.on_tick when the Redis connection
dropped. This caused a race condition during reconnection: if a stale
channel's _on_disconnect fired after a new channel had already
re-registered its on_poll_start, the new callback was removed, leaving
the worker alive but unable to consume any tasks ("catatonic worker").
on_poll_start is idempotent — when there are no active file descriptors
it simply does nothing. Removing it from on_tick was a premature
optimization that created a window for the race condition.
This fix removes the on_tick.remove() call from _on_disconnect, keeping
on_poll_start registered through the full reconnection cycle.
Fixes celery/celery#8030
Related: celery/celery#9191, celery/celery#9631
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Add four tests covering the _on_disconnect callback registered by register_with_event_loop: - socket with fileno(): loop.remove() called and fd pruned from cycle._fd_to_chan - socket without fileno() (raw int fd): fd itself used as key - fileno() raises OSError (closed socket): error swallowed gracefully - fd not in _fd_to_chan: KeyError swallowed gracefully All branches introduced in the Copilot patch (aaf75bd) are now covered, resolving the codecov/patch failure on PR #2492. Co-Authored-By: Claude Opus 4.6 <[email protected]>
When a socket is closed but not yet garbage-collected, fileno() returns -1 rather than raising OSError. The previous pruning code would attempt to delete key -1 from cycle._fd_to_chan (silently failing via KeyError), leaving the original positive fd — which maps to a stale socket — still registered. The next on_poll_start tick would then re-register that stale fd. Fix by checking `raw_fd >= 0` before treating the return value as a valid key. Also simplify the pruning path by accessing cycle._fd_to_chan directly (the attribute is always present on MultiChannelPoller) instead of going through a `hasattr` guard. Add test_register_with_event_loop__on_disconnect__fileno_negative to cover this path, bringing patch coverage to 100% on all new _on_disconnect branches. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
In async Redis mode, Connection.disconnect() clears _sock to None before invoking _on_connection_disconnect. Without this fix, stale entries remain in cycle._fd_to_chan and get re-registered by the next on_poll_start tick. Adds conservative scan of _fd_to_chan matching channels by their client/subclient connection identity. Uses tuple unpacking to match the real _fd_to_chan data structure (channel, type). Tests cover both client.connection and subclient.connection match paths, plus verifies unrelated channels are left intact. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
0cafd57 to
c393447
Compare
|
@auvipy Done — addressed the Copilot review feedback in commit c393447: Code change: Added an Note: Tests: Two new tests covering:
Existing test fix: Updated Regarding All checks pass: 149 tests green, pre-commit clean. |
Celery 5.7.0 includes upstream fixes for the catatonic worker bug where workers reconnect after a Redis broker restart but fail to re-register their queue consumers (celery/celery#10204, celery/kombu#2492). This replaces the previous worker-health-check CLI approach with a version bump, as recommended by maintainers.
Summary
Fix race condition in Redis transport reconnection that causes "catatonic worker" — a worker that is alive (responds to ping) but silently stops consuming tasks after a broker restart.
Root cause
_on_disconnectinregister_with_event_loopremoves theon_poll_startcallback fromloop.on_tickwhen a Redis connection drops. During reconnection, a new channel re-registers its ownon_poll_start. But if_on_disconnectfrom the stale channel fires after the new registration, it removes the new callback, leaving the event loop without any poll trigger. The worker stays alive but_register_BRPOPis never called — tasks pile up in Redis and are never consumed.Fix
Remove the
loop.on_tick.remove(on_poll_start)call from_on_disconnect.on_poll_startis idempotent — when there are no active file descriptors,cycle.fdsis empty and it simply does nothing. Removing it fromon_tickwas a premature optimization that created a window for the race condition.Companion PR
hub.reset()tosynloop(gevent/eventlet path), which was missing the cleanup thatasynloopalready had.Related issues
Test plan
test_register_with_event_loop__on_disconnect__loop_cleanupto verifyon_poll_startstays inon_tickafter disconnect