fix: serialize scheduler transitions and coalesce misfires

This commit is contained in:
lda
2026-09-09 19:21:03 +07:00 Verified
parent 787a3c433f
commit 953543c5cb
6 changed files with 242 additions and 8 deletions
+60
View File
@@ -38,6 +38,7 @@ from wf_scheduling.ownership import (
describe_unsupported_layout,
)
from wf_scheduling.prepare import InvocationPreparer, PreparationRejected
from wf_scheduling.store import schedule_store_transaction
UTC = timezone.utc
SCAN_CAP = 100
@@ -303,6 +304,23 @@ class Scheduler:
return None
def _admit(self, sched: Any, intended: datetime, now: datetime) -> str | None:
"""Admit one occurrence under the schedule store's local transaction."""
with schedule_store_transaction(self.schedule_store):
return self._admit_locked(sched, intended, now)
def _admit_locked(
self, sched: Any, intended: datetime, now: datetime
) -> str | None:
# The poller's listing is only a fairness snapshot. Re-read under the
# admission transaction so an edit committed before this point wins;
# an edit after this point waits and affects a later occurrence.
try:
current = self.schedule_store.get_schedule(sched.id)
except KeyError:
return None
if current.revision != sched.revision:
return "schedule-changed"
sched = current
if getattr(sched, "blocked_reason", None):
raise BlockedSchedule(getattr(sched, "blocked_reason"))
if not sched.enabled or sched.deleted or sched.paused:
@@ -820,6 +838,48 @@ class Scheduler:
)
self.schedule_store.save_consumed(sched.id, now)
return "span-skipped"
if (
sched.misfire == "latest"
and due
and any(
(now - instant).total_seconds() > sched.lateness_allowance_s
for instant in due
)
):
# A bounded catch-up can still contain several missed instants.
# Coalesce them before the per-instant loop so ``latest`` never
# turns a short downtime into a replay burst.
latest = due[-1]
old = self.schedule_store.get_candidate(sched.id)
if old is not None and old.intended_at != latest:
self._record(
kind="superseded",
sched_id=sched.id,
intended=old.intended_at,
reason=f"coalesced-into:{latest.isoformat()}",
revision=sched.revision,
now=now,
)
self.schedule_store.save_candidate(
PendingCandidate(
schedule_id=sched.id,
intended_at=latest,
revision=sched.revision,
),
schedule_id=sched.id,
)
self._record(
kind="interval-summary",
sched_id=sched.id,
reason="coalesced-missed-span",
revision=sched.revision,
interval=(consumed, now),
count=-1,
now=now,
)
self.schedule_store.save_consumed(sched.id, now)
held = self._admit_held_candidate(sched, now)
return "skipped-overlap" if held is None else held
last_result = "idle"
for instant in due:
stored_consumed = self.schedule_store.get_consumed(sched.id) or EPOCH