Repository navigation
fix(cron): don't treat a busy fire fence as lost claim ownership - #100418
oheckmann74 wants to merge 2 commits into
Conversation
heartbeat_fire_claim() took the per-job fire fence, which the run thread already holds across its side effects (save_job_output, _deliver_result). The heartbeat runs on a different thread, so the fence's thread-local reentrancy does not apply: a delivery slower than _JOBS_LOCK_TIMEOUT_SECONDS made the heartbeat time out and return False, which the scheduler reads as lost ownership and uses to interrupt a healthy run -- surfaced to operators as "Interrupted by shutdown before terminal completion." A heartbeat is not an external side effect. It only compare-and-swaps fire_claim["at"] when fire_claim["by"] still matches expected_owner, and _heartbeat_fire_claim_locked already runs that CAS under _jobs_lock(), which holds both an in-process lock and a cross-process flock. Prefer the fence, but fall back to the CAS rather than reporting a loss never observed. A real takeover still returns False, because "by" no longer matches.
Fails without the fix with the production symptom: Timed out waiting for local fire fence <dir>::<job_id>; failing closed
|
The fix for this bug landed on main via #109310 (merge 9a60a7f): Thanks @oheckmann74 — earliest working fix in the cluster with a red-on-base test. The landed change is the fence-free variant of your fallback (skips the fence rather than trying it first), and the merged regression test follows your test's shape (fence held by a worker thread, heartbeat True on the calling thread, foreign owner still False); you are credited as Co-author on that commit. Closing with credit. |
Fixes #100401.
Problem
heartbeat_fire_claim()takes the per-job fire fence. The run thread already holds that fence around its side effects —save_job_output()and_deliver_result()(_side_effect_fence()). The heartbeat runs on a different thread, so_fire_job_lock's thread-local reentrancy bookkeeping (_fire_fence_lock_state = threading.local()) does not apply and theRLockis genuinely contended.So a delivery slower than
_JOBS_LOCK_TIMEOUT_SECONDS(30s) makes the heartbeat block, time out, and returnFalse. The scheduler cannot tell that apart from a real takeover:and interrupts a perfectly healthy run, which operators see as:
Nothing has shut down, and the work itself already succeeded — in my case a nightly backup that had committed and pushed before being reported as failed.
Production timeline (
deliver: bot-chat:default), the two constants adding to exactly 90s:Delivery was still in flight, holding the fence, when the heartbeat tried to renew.
Fix
A heartbeat is not an external side effect. It only compare-and-swaps
fire_claim["at"]whenfire_claim["by"]still equalsexpected_owner, and_heartbeat_fire_claim_locked()already performs that CAS under_jobs_lock()— which holds both an in-process lock and a cross-process flock on.jobs.lock. The fence adds nothing to its correctness.So: prefer the fence to keep the common path serialized with other owner mutations, but when it is unavailable, fall back to the CAS instead of reporting a loss of ownership that was never observed. A genuine takeover still returns
False, becausebyno longer matches.No change to the scheduler, no change to the constants, no new public API.
Scope
Only jobs whose delivery is slow enough to span a heartbeat tick are affected.
--deliver localnever reproduces it — I confirmed with a 115s job that completes on both patched and unpatched builds. Every job I have seen fail delivers tobot-chat, which may make this worth a look alongside #95307 (alsobot-chat, also a spurious lost fire claim).Testing
tests/cron/test_claim_job_for_fire.py::test_heartbeat_survives_fence_held_by_its_own_runtests/cron/suite: 1081 passed, 1 skipped.test_repeated_heartbeat_errors_cancel_after_bounded_grace, fails identically with and without this change (5/5 runs each) — it assertscalls >= 3against a 0.01s/0.03s timing window.Verified against
main @ 18a76be12; both touched files are byte-identical between that revision and currentmain.