sched: crash-safe admin ordering, create watermark, poll freshness (T13 fix)
This commit is contained in:
@@ -582,6 +582,22 @@ class Scheduler:
|
||||
self._execute_guarded(run.id, now)
|
||||
|
||||
def _poll_one(self, sched: Any, now: datetime) -> str:
|
||||
# Fresh read per schedule: the tick lists schedules up front while
|
||||
# same-process administration may commit an edit, pause, resume, or
|
||||
# delete mid-tick. All admission decisions below (policies,
|
||||
# revision, flags) use this fresh copy, never the listing
|
||||
# snapshot. Two narrow residuals remain: calendar iteration uses
|
||||
# the tick-start occurrence source (a trigger edit takes effect
|
||||
# on the next tick), and an admission decision already in flight
|
||||
# cannot observe a concurrent edit — admission stays idempotent
|
||||
# per (schedule, instant), so the worst case is one run admitted
|
||||
# under just-superseded terms, never a duplicate or replay.
|
||||
try:
|
||||
sched = self.schedule_store.get_schedule(sched.id)
|
||||
except KeyError:
|
||||
# Soft deletes never remove the file; a vanishing schedule is
|
||||
# unexpectedly gone — treat it as deleted work, never admit.
|
||||
return "deleted"
|
||||
if sched.deleted:
|
||||
if self.schedule_store.get_candidate(sched.id) is not None:
|
||||
self.schedule_store.save_candidate(None, schedule_id=sched.id)
|
||||
@@ -806,22 +822,33 @@ class Scheduler:
|
||||
|
||||
# -- administration ---------------------------------------------------
|
||||
def resume_schedule(self, sid: str, now: datetime) -> None:
|
||||
"""Unpause: resume selects the next future occurrence."""
|
||||
"""Unpause: resume selects the next future occurrence.
|
||||
|
||||
Crash-safe ordering shared with the API surface: the candidate
|
||||
is cleared and the watermark advances BEFORE the flag flip is
|
||||
persisted, so a crash can only leave the schedule paused
|
||||
(retryable) and never an unpaused flag whose span backfills.
|
||||
"""
|
||||
self._require_ownership()
|
||||
self.schedule_store.get_schedule(sid)
|
||||
self.schedule_store.save_candidate(None, schedule_id=sid)
|
||||
consumed = self.schedule_store.get_consumed(sid) or EPOCH
|
||||
self.schedule_store.save_consumed(sid, max(consumed, now))
|
||||
sched = self.schedule_store.get_schedule(sid)
|
||||
sched.paused = False
|
||||
self.schedule_store.save_schedule(sched)
|
||||
consumed = self.schedule_store.get_consumed(sid) or EPOCH
|
||||
self.schedule_store.save_consumed(sid, max(consumed, now))
|
||||
self.schedule_store.save_candidate(None, schedule_id=sid)
|
||||
|
||||
def edit_schedule(self, sid: str, now: datetime) -> None:
|
||||
"""Definition edit: new revision, discard old candidates, no backfill."""
|
||||
"""Definition edit: new revision, discard old candidates, no backfill.
|
||||
|
||||
Crash-safe ordering shared with the API surface: old candidates
|
||||
are discarded (with a superseded row) and the watermark advances
|
||||
BEFORE the revision bump is persisted, so a crash can only leave
|
||||
the edit unapplied under the old revision (retryable) and never
|
||||
a bumped revision that backfills pre-edit instants on restart.
|
||||
"""
|
||||
self._require_ownership()
|
||||
sched = self.schedule_store.get_schedule(sid)
|
||||
sched.revision += 1
|
||||
sched.updated_at = now
|
||||
self.schedule_store.save_schedule(sched)
|
||||
old = self.schedule_store.get_candidate(sid)
|
||||
if old is not None:
|
||||
self._record(
|
||||
@@ -829,11 +856,15 @@ class Scheduler:
|
||||
sched_id=sid,
|
||||
intended=old.intended_at,
|
||||
reason="schedule-edit",
|
||||
revision=sched.revision,
|
||||
revision=sched.revision + 1,
|
||||
)
|
||||
self.schedule_store.save_candidate(None, schedule_id=sid)
|
||||
consumed = self.schedule_store.get_consumed(sid) or EPOCH
|
||||
self.schedule_store.save_consumed(sid, max(consumed, now))
|
||||
sched = self.schedule_store.get_schedule(sid)
|
||||
sched.revision += 1
|
||||
sched.updated_at = now
|
||||
self.schedule_store.save_schedule(sched)
|
||||
|
||||
|
||||
def _admission_intended(run_store: Any, run_id: str) -> datetime | None:
|
||||
|
||||
Reference in New Issue
Block a user