fix(worker): add hub.reset() to synloop on connection error#10204
Conversation
The asynloop already calls hub.reset() when an exception occurs during the event loop, cleaning up stale file descriptors and callbacks from the old connection. The synloop (used by gevent/eventlet pools) lacked this cleanup, leaving stale state that could prevent consumer re-registration after broker reconnection. This adds the same hub.reset() pattern from asynloop to synloop, guarded by a None check since hub may not always be available in the sync path. Three new tests verify: reset on error, no reset on graceful shutdown, and safe handling when hub is None. Fixes #9191 Related: #8030, #9631 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
There was a problem hiding this comment.
Pull request overview
This PR adds hub.reset() cleanup to the synloop (used by gevent/eventlet pools) when a connection error occurs, matching the existing cleanup pattern in asynloop. This addresses "catatonic worker" issues where stale hub state from a dropped broker connection prevented proper consumer re-registration after reconnection.
Changes:
- Wrap the
synloopmain loop intry/except Exceptionto callhub.reset()on error, with aNoneguard since the hub may not always be available in the sync path - Add three focused unit tests covering: hub reset on connection error, no reset on graceful shutdown, and safe handling when hub is
None
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| celery/worker/loops.py | Adds hub.reset() error handling to synloop, mirroring asynloop's cleanup pattern |
| t/unit/worker/test_loops.py | Adds three tests for the new hub reset behavior in synloop |
You can also share your feedback on Copilot code review. Take the survey.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #10204 +/- ##
=======================================
Coverage 88.23% 88.24%
=======================================
Files 153 153
Lines 19573 19581 +8
Branches 2249 2250 +1
=======================================
+ Hits 17270 17279 +9
Misses 2004 2004
+ Partials 299 298 -1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. |
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…s in synloop Add test_hub_reset_error_logs_exception to assert the error message is actually logged, complementing the existing test that only verified hub.reset() was called. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
themavik
left a comment
There was a problem hiding this comment.
Reviewed the changes — the fix is minimal and targeted. Good contribution.
themavik
left a comment
There was a problem hiding this comment.
Reviewed the changes — the approach looks correct and addresses the reported issue.
themavik
left a comment
There was a problem hiding this comment.
Calling hub.reset() from synloop on errors mirrors asynloop cleanup so gevent/eventlet pools do not keep dead callbacks across reconnect (#9191). nit: if hub.reset() itself fails you log that exception but still re-raise the original loop error, so ops have to grep logs for reset failures separately.
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
Add
hub.reset()cleanup to thesynloop(used by gevent/eventlet pools) when a connection error occurs, matching the cleanup already present inasynloop.Root cause
When a broker connection drops during
asynloop,hub.reset()is called in theexcept Exceptionblock (loops.py:107-108), cleaning up stale file descriptors and callbacks. Thesynlooplacked this cleanup entirely. With gevent/eventlet pools, stale state from the old connection persisted across reconnection attempts, preventing proper consumer re-registration.This is especially problematic because gevent disables
broker_connection_timeout(consumer.py:230-234), meaning a silent Redis death can leave the worker stuck indrain_eventsindefinitely. When it eventually reconnects, the stale hub state prevents the new consumers from registering properly.Fix
Wrap the
synloopmain loop intry/except Exceptionand callhub.reset()on error, matching theasynlooppattern. The hub is guarded againstNonesince it may not always be available in the sync path.Companion PR
_on_disconnectthat removes theon_poll_startcallback fromon_tick, preventing consumer re-registration after reconnection.Together, these two PRs address the "catatonic worker" problem from both sides:
Related issues
Test plan
test_hub_reset_on_connection_error— verifieshub.reset()is called onsocket.errortest_hub_not_reset_on_graceful_shutdown— verifies no reset on normal exittest_hub_reset_with_none_hub— verifies safe handling when hub isNone