From d6d3ee8584bda05a873001a18998224f04fdaaae Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Fri, 18 Sep 2026 03:58:30 -0700 Subject: [PATCH] fix(cron): terminal-worker reaper lives in scheduler_detached_worker, not the scheduler facade cron/scheduler.py is already past the facade line-count gate; the reap helper is a detached-worker teardown concern with an existing topical home. The waiter late-imports it so behaviour and the sync run_one_job contract are unchanged. --- cron/scheduler.py | 21 +++------------------ cron/scheduler_detached_worker.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/cron/scheduler.py b/cron/scheduler.py index f0ed6c93ae..11db5b040f 100644 --- a/cron/scheduler.py +++ b/cron/scheduler.py @@ -3096,23 +3096,6 @@ def _run_one_job_body( reset_terminal_scope(_terminal_scope_token) -def _reap_terminal_worker_in_background(process: subprocess.Popen) -> None: - """Keep the reap contract when the waiter returns before the worker exits. - - The ledger turning terminal lets the waiter return while the worker is - still in final teardown. The gateway remains the worker's parent, so if - nobody calls ``wait()`` afterwards the worker lingers as a zombie (STAT=Z) - under the gateway until it is restarted (#114509). A short-lived daemon - thread holds that single responsibility and ends with the process exit - it waits for. - """ - threading.Thread( - target=process.wait, - name=f"cron-worker-reap-{getattr(process, 'pid', '?')}", - daemon=True, - ).start() - - def _wait_for_external_cron_worker_body( process: subprocess.Popen, *, @@ -3139,7 +3122,9 @@ def _wait_for_external_cron_worker_body( returncode = process.wait(timeout=1.0) except subprocess.TimeoutExpired: if _is_terminal(): - _reap_terminal_worker_in_background(process) + from cron.scheduler_detached_worker import reap_terminal_worker_in_background + + reap_terminal_worker_in_background(process) return True continue # The worker can commit its terminal row and exit between the first diff --git a/cron/scheduler_detached_worker.py b/cron/scheduler_detached_worker.py index 921a5e6f20..39cdf72328 100644 --- a/cron/scheduler_detached_worker.py +++ b/cron/scheduler_detached_worker.py @@ -9,6 +9,8 @@ overlap behind #102827. The worker's Future owns the teardown instead. from __future__ import annotations import concurrent.futures +import subprocess +import threading from typing import Optional @@ -32,3 +34,20 @@ def defer_teardown_to_running_worker( # Runs inline if the worker finished between done() and here — still exactly once. future.add_done_callback(_finish) return True + + +def reap_terminal_worker_in_background(process: subprocess.Popen) -> None: + """Keep the reap contract when the waiter returns before the worker exits. + + The ledger turning terminal lets ``_wait_for_external_cron_worker_body`` + return while the worker is still in final teardown. The gateway remains the + worker's parent, so if nobody calls ``wait()`` afterwards the worker lingers + as a zombie (STAT=Z) under the gateway until it is restarted (#114509). A + short-lived daemon thread holds that single responsibility and ends with + the process exit it waits for. + """ + threading.Thread( + target=process.wait, + name=f"cron-worker-reap-{getattr(process, 'pid', '?')}", + daemon=True, + ).start()