fix(scheduler): drop cross-task mutex, quiet window + retry
Incident: evening full snapshot could be recorded as ok with already_completed/elapsed=0 after mutual skip with hourly sync, then next_run jumped to the next day. - Full-site snapshot never skips because another task is running - 23:00-01:00 Asia/Shanghai: defer discovery / snapshot_recent / candidate_cleanup to after 01:00; fans never defers - Skip records ok=false + skipped=true (not success / not completed) - Skip/fail reschedule soon (~5m); daily success still next snapshot_at - TaskControl return value drives next_run; system UI shows skip fields
This commit is contained in:
1 parent
39378c65b2
commit
9969e65202
10 files changed
+411
-84
No files matched your search
@@ -3,6 +3,9 @@
|
||||
|
||||
P0 只注册收藏夹同步任务;P1/P3 会在同一调度器上追加快照、粉丝和发现任务。
|
||||
调度只负责“什么时候运行”,具体任务保持幂等,并在自己的服务层处理重试。
|
||||
|
||||
任务 ``fn`` 可返回 :class:`TaskControl` 以覆盖 ``ok`` / ``next_run``
|
||||
(用于静默窗延后、失败补跑等);未返回时仍按 ``next_run_fn`` / ``interval`` 排期。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -12,6 +15,15 @@ from dataclasses import dataclass
|
||||
from typing import Any, Callable, Sequence
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaskControl:
|
||||
"""任务可选返回值:覆盖成功标记与下次运行时间。"""
|
||||
|
||||
ok: bool = True
|
||||
next_run: float | None = None
|
||||
error: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScheduledTask:
|
||||
name: str
|
||||
@@ -70,23 +82,33 @@ class Scheduler:
|
||||
continue
|
||||
started = current
|
||||
error: str | None = None
|
||||
control: TaskControl | None = None
|
||||
try:
|
||||
task.fn()
|
||||
raw = task.fn()
|
||||
except Exception as exc: # noqa: BLE001 单任务失败不终止调度
|
||||
error = str(exc)
|
||||
self._log("error", "scheduler.task_error", task=task.name, error=error)
|
||||
else:
|
||||
if isinstance(raw, TaskControl):
|
||||
control = raw
|
||||
self._log("info", "scheduler.task_ok", task=task.name)
|
||||
finished = self._clock()
|
||||
if task.next_run_fn is not None:
|
||||
if control is not None and control.next_run is not None:
|
||||
task.next_run = float(control.next_run)
|
||||
elif task.next_run_fn is not None:
|
||||
task.next_run = task.next_run_fn(started)
|
||||
else:
|
||||
task.next_run = started + task.interval
|
||||
ok = error is None
|
||||
if control is not None:
|
||||
ok = bool(control.ok) and error is None
|
||||
if control.error and error is None:
|
||||
error = control.error
|
||||
result = TaskResult(
|
||||
name=task.name,
|
||||
started_at=started,
|
||||
finished_at=finished,
|
||||
ok=error is None,
|
||||
ok=ok,
|
||||
error=error,
|
||||
)
|
||||
results.append(result)
|
||||
|
||||
Reference in new issue
Block a user