fix: serialize scheduler transitions and coalesce misfires
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user