diff --git a/cron/scheduler.py b/cron/scheduler.py index 3688ca49b7..6580427a6e 100644 --- a/cron/scheduler.py +++ b/cron/scheduler.py @@ -1376,6 +1376,35 @@ def _consume_interrupted_flag(job_id: str, token: Optional[object] = None) -> bo return hit +def _inactivity_watchdog_loop( + *, + get_idle_seconds: Callable[[], float], + limit_s: float, + poll_s: float, + stop: threading.Event, + future_done: Callable[[], bool], +) -> bool: + """Poll job idle time until the limit, stop, or the watched future completes. + + Driven by ``threading.Event.wait`` (a kernel timeout), not asyncio, so a + blocked event-loop / ``run_job`` thread cannot disable this watchdog the + way ``asyncio.sleep`` / ``wait_for`` would (family A of #94285 — the + 4118s-idle-on-a-600s-limit cron hang). Returns True when *limit_s* of + inactivity was observed. + """ + while not stop.wait(poll_s): + if future_done(): + return False + try: + idle = float(get_idle_seconds() or 0.0) + except Exception: + idle = 0.0 + if idle >= limit_s: + return True + return False + + + def _cron_inactivity_seconds() -> float: """Parse HERMES_CRON_TIMEOUT (seconds). 0 = unlimited; bad input = 600.