Files
peter e49b77dbcd fix(scheduler): move the single-run claim into _job (cover both paths)
The atomic claim added in trigger_job only covered the manual path; a scheduled
fire sets running via _job without claiming, so a manual trigger racing it could
still double-run, and the claim (set outside _job) could leak if _job threw
before its own cleanup. Move _claim_running to the top of _job — the one point
both the scheduled fire and the manual trigger pass through — and release it in
_job's finally, so exactly one run wins whichever path it comes from and a
failure can never wedge the job "running". trigger_job's _is_running check is now
just a best-effort fast answer for the UI. Unit-test _claim_running (and
is_messageable_user).
2026-07-23 03:50:04 +02:00

25 lines
979 B
Python

"""_claim_running is the single-run guard that both the scheduled fire and the manual trigger go
through (at the top of _job): holding _activity_lock across the check-and-set means exactly one
caller can hold a job's "running" claim at a time, and it's claimable again once released."""
from app import scheduler
_JOB = "unit_test_claim" # not a real job id — exercises the primitive without touching live jobs
def _release():
scheduler._record(_JOB, running=False)
def test_claim_is_exclusive_until_released():
_release()
# First caller claims it.
assert scheduler._claim_running(_JOB) is True
# A second caller is refused while the first still holds it (the TOCTOU the fix closes).
assert scheduler._claim_running(_JOB) is False
assert scheduler._claim_running(_JOB) is False
# _job's finally releases the claim...
_release()
# ...and now it can be claimed again.
assert scheduler._claim_running(_JOB) is True
_release()