feat(celery): finalize in-flight task on worker eviction so the chord proceeds - #1285
feat(celery): finalize in-flight task on worker eviction so the chord proceeds#1285ocervell wants to merge 5 commits into
Conversation
… proceeds On a K8s pod eviction (SIGTERM -> grace -> SIGKILL) the worker running a task dies. With task_acks_late on the Redis broker, its message is only redelivered after the broker visibility timeout (hours, on the long-task pool) — stalling the surrounding chord/workflow that whole time. Unlike a child OOM (where the master survives and task_reject_on_worker_lost requeues immediately), a whole-pod eviction has no surviving master, so Redis can only fall back to the visibility timeout. Catch the worker_shutting_down signal and raise a flag the running task's monitor thread polls. On shutdown it stops the task early via the existing stop_process path, so the task returns its partial results through the normal completion path and the chord proceeds in seconds instead of waiting for the visibility timeout. - celery_signals.py: SHUTDOWN_FLAG + worker_shutting_down_handler (wired unconditionally); stale flag cleared on worker boot. - command.py: the monitor thread checks the flag (same self-stop used for timeout/memory limits) and caps its poll interval (MONITOR_POLL_SECONDS=5) so an eviction is caught well within the pod's terminationGracePeriodSeconds. - tests: flag lifecycle + a behavioral test (a running command stops early and emits the eviction warning). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P5vSjfkBuGAAHdKxHS3ySm
…late test The integration suite caught a real leak: SHUTDOWN_FLAG is a machine-global file, so once any worker fired worker_shutting_down (e.g. between test files) the flag persisted and every later task whose monitor polled once self-aborted — slow tasks failed, fast ones that finished before the first poll passed. Prod never hit it (1 task = 1 pod = fresh /tmp), but any shared/long-lived worker does. - command.py: clear the flag at monitor start, so it only means "shutdown raised *during this run*", not a stale flag from a previous worker/task. - celery_signals.py (CodeRabbit): ensure the flag's parent dir exists and catch + log OSError instead of swallowing all exceptions. - test_eviction.py (CodeRabbit): isolate SHUTDOWN_FLAG to a per-test temp path; add a regression test that a flag set *before* a task starts does not stop it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P5vSjfkBuGAAHdKxHS3ySm
…ace) The behavioral test set the flag once after a fixed 2s wait, but the monitor now clears any pre-existing flag at startup; on a slow CI runner the monitor started *after* that single set and wiped it, so the task never stopped and the test failed. Re-raise the flag in a loop until the task stops, so the monitor's poll sees it once it is running regardless of start timing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P5vSjfkBuGAAHdKxHS3ySm
The real-subprocess behavioral test was environment-dependent: in CI the monitor thread didn't stop the live `sleep` reliably, so the test flaked (and the stale test passed for the wrong reason). Replace both with deterministic tests that exercise _monitor_process directly against a bare Command — one asserts it calls stop_process(exit_ok=True) + emits the eviction Warning when the flag is set, the other asserts it clears a stale flag at start (the integration-leak regression). No subprocess, no timing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P5vSjfkBuGAAHdKxHS3ySm
The real-file version of the stale-flag test flaked on CI (patched temp path + bare-__new__ Command), while the real behaviour is already proven green by the integration suite. Assert that _monitor_process calls clear_shutdown_flag() at start via a mock instead of inspecting the filesystem — deterministic and environment-independent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P5vSjfkBuGAAHdKxHS3ySm
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
WalkthroughAdds a file-based shutdown flag mechanism triggered by Celery's worker_shutting_down signal, enabling the command monitor thread to detect worker eviction, terminate running processes gracefully with partial results, and reduces monitor polling interval. Includes new unit tests covering the flag lifecycle and monitor behavior. ChangesWorker Eviction Self-Finalization
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Parked for later (owner decision). Concern: not enough confidence in the in-flight runner's behavior on worker shutdown — what happens to the running Python runner / external tool process, partial results, and how finalize-early interacts with redelivery. Too many edge cases to merge without more validation. Safe to park: this only helps the graceful SIGTERM (k8s eviction / drain / rolling update) path — a hard worker death (SIGKILL/OOM) is handled by acks_late redelivery + the worker-loss cap (#1284) and the broker visibility-timeout fallback. So the platform works correctly without this; it just finalizes slower on graceful eviction. Revisit when we can validate the runner/partial-results semantics end-to-end. |
Re-extracted from the closed #1203 — like the cap PR (#1284), this generic worker-reliability fix was folded into the
ai-resiliencybranch (#1241, AI-task hardening, basecanary) and its standalone PR closed. It's general infra (applies to every runner, not AI), so it belongs onmainindependently. Cherry-picked clean onto currentmain(CI-retrigger noise commits dropped).What it does
On a graceful worker shutdown (SIGTERM — k8s pod eviction / node drain / rolling update),
worker_shutting_downsets a flag and the running task's monitor thread stops the task early and finalizes it — so the chord'smark_runner_completedstill fires and the workflow proceeds, instead of the task hanging until the broker visibility timeout (hours on the long pool). Works on any pool (monitor-thread based). Per-task flag is cleared at start so a stale flag can't finalize a fresh task.Validation
Exercised end-to-end earlier (SIGTERM → task stopped ~9s vs ~60s, chord proceeds, 5/5).
tests/unit/test_eviction.py: 3 passed;test_celery.py: 17 passed on currentmain; flake8 clean.Note
feature:worker-reliability. Companion: #1284 (worker-loss cap). #1202 (on_build identity) is already onmain(in the pending 0.40.0).🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes