fix(scheduler): daily slot off-plan + cross-midnight resume (#8)

- Cross-midnight: unfinished yesterday seals ASAP with yesterday's stat_date;
  force seal ignores today's snapshot_at gate when stat_date < today.
- Off-plan already_completed: compare completion started_at (not finished_at)
  to today's planned slot; earlier → still run at slot for all daily tasks.
- Cold start: bootstrap task:state from task:last / seed markers.
- ensure_pending: do not glue new due onto a different day's unfinished state.
- run_if_needed(force): keep_scraped True for seal, False for CLI --force.
- Scheduler due_reader: pick up disk due_at each loop (seed→evening seal).
This commit is contained in:
bot-ci committed 2026-11-03 02:35:13 +08:00
1 parent dcf37c4a1d
commit f6bef59e07
7 files changed
+630 -24

No files matched your search

+7
View File
@@ -59,6 +59,7 @@ class Scheduler:
clock=time.time,
sleeper=time.sleep,
logger=None,
due_reader: Callable[[str], float | None] | None = None,
) -> None:
if not tasks:
raise ValueError("至少需要一个调度任务")
@@ -69,6 +70,8 @@ class Scheduler:
self._clock = clock
self._sleeper = sleeper
self.logger = logger
# 每轮从落盘 due_at 刷新 next_run(seed 挂当晚 seal 等同进程可见)
self._due_reader = due_reader
def _log(self, level: str, event: str, **fields) -> None:
if self.logger is not None:
@@ -78,6 +81,10 @@ class Scheduler:
current = self._clock() if now is None else now
results: list[TaskResult] = []
for task in self.tasks:
if self._due_reader is not None:
disk_due = self._due_reader(task.name)
if disk_due is not None:
task.next_run = float(disk_due)
if task.next_run > current:
continue
started = current