From b653fd724a871af19200afff8a745ed2ae41eaea Mon Sep 17 00:00:00 2001 From: mochamgx <1114149@qq.com> Date: Fri, 25 Sep 2026 10:05:02 +0800 Subject: [PATCH] fix(cron): guard None repeat.completed when recording a job run `repeat.get("completed", 0)` only falls back to 0 when the KEY is absent. When the key exists with value None (a job created but never successfully recorded a run), it returns None, and the following `completed += 1` raises: unsupported operand type(s) for +=: 'NoneType' and 'int' Effect: the run is never recorded (last_run_at/last_status stay null) and every fire logs a scheduling error. The job itself executes fine. Same latent hazard at the `completed < times` comparison, which a None value would also fail on. Fix all three sibling sites with `repeat.get("completed") or 0`. (cherry picked from commit 0c5024de901900a747849ca44750327b56477f3b) --- cron/jobs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cron/jobs.py b/cron/jobs.py index 3bb559b96f..734f3cb215 100644 --- a/cron/jobs.py +++ b/cron/jobs.py @@ -2360,7 +2360,7 @@ def _advance_after_run(job: Dict[str, Any], now: str) -> None: if repeat: times = repeat.get("times") finite = times is not None and times > 0 - completed = repeat.get("completed", 0) + completed = repeat.get("completed") or 0 # Finite one-shots were pre-claimed by claim_dispatch() (completed already incremented) — # do not double-count; recurring jobs and direct callers still get the increment. if not (kind == "once" and finite and completed > 0): @@ -2541,7 +2541,7 @@ def claim_dispatch(job_id: str) -> bool: # Recurring jobs use advance_next_run(); no/infinite repeat limit always dispatches. if job.get("schedule", {}).get("kind") != "once" or times is None or times <= 0: return True - completed = repeat.get("completed", 0) + completed = repeat.get("completed") or 0 label = job.get("name", job.get("id", "?")) if completed >= times: if job.get("last_run_at") is not None: @@ -3098,7 +3098,7 @@ def _oneshot_dispatch_limit_reached(job: Dict[str, Any], scan: _DueScan) -> bool process is still running it (a run outliving the run_claim TTL is slow, not stale).""" repeat = job.get("repeat") or {} times = repeat.get("times") - completed = repeat.get("completed", 0) + completed = repeat.get("completed") or 0 if times is None or times <= 0 or completed < times: return False name = job.get("name", job.get("id", "?"))