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
@@ -34,7 +34,14 @@ from ..analytics import (
|
||||
)
|
||||
from ..config import ConfigStore
|
||||
from ..database import Database
|
||||
from ..timeutil import day_bounds, next_daily_run, shanghai_date, shanghai_datetime
|
||||
from ..timeutil import (
|
||||
day_bounds,
|
||||
interval_next_run,
|
||||
next_after_snapshot_quiet_window,
|
||||
next_daily_run,
|
||||
shanghai_date,
|
||||
shanghai_datetime,
|
||||
)
|
||||
from .cache import DASHBOARD_CACHE, TTL_FANS, TTL_VIDEOS, build_cache_key
|
||||
|
||||
TRACKED_STATUSES = ("active", "unfaved")
|
||||
@@ -1971,9 +1978,10 @@ class DashboardQueries:
|
||||
"fans": int(next_daily_run(now, cfg.schedule.snapshot_at)),
|
||||
"snapshot": int(next_daily_run(now, cfg.schedule.snapshot_at)),
|
||||
"snapshot_seed": int(next_daily_run(now, cfg.schedule.seed_at)),
|
||||
"discover": int(now + discover_interval),
|
||||
"snapshot_recent": int(now + recent_interval),
|
||||
"discover": int(interval_next_run(now, float(discover_interval))),
|
||||
"snapshot_recent": int(interval_next_run(now, float(recent_interval))),
|
||||
}
|
||||
retry_seconds = 300
|
||||
for name, key, at in (
|
||||
("fans", "fans", cfg.schedule.snapshot_at),
|
||||
("snapshot", "snapshot", cfg.schedule.snapshot_at),
|
||||
@@ -1985,19 +1993,41 @@ class DashboardQueries:
|
||||
if not (isinstance(record, dict) and record.get("finished_at")):
|
||||
continue
|
||||
finished_at = int(record["finished_at"])
|
||||
summary = record.get("summary") if isinstance(record.get("summary"), dict) else {}
|
||||
skipped = bool(record.get("skipped") or summary.get("skipped"))
|
||||
failed = record.get("ok") is False
|
||||
if name in {"discover", "snapshot_recent"}:
|
||||
# 只按上次完成推下一档;进行中由 running 展示,不按墙钟抢跑。
|
||||
# 上次完成推下一档;skip/失败短补跑;静默窗推到 01:00 后。
|
||||
interval = (
|
||||
recent_interval if name == "snapshot_recent"
|
||||
else discover_interval
|
||||
)
|
||||
next_runs[name] = finished_at + interval
|
||||
if skipped:
|
||||
next_runs[name] = int(
|
||||
next_after_snapshot_quiet_window(float(finished_at))
|
||||
)
|
||||
elif failed:
|
||||
next_runs[name] = int(finished_at + retry_seconds)
|
||||
else:
|
||||
next_runs[name] = int(
|
||||
interval_next_run(float(finished_at), float(interval))
|
||||
)
|
||||
else:
|
||||
next_runs[name] = int(next_daily_run(finished_at, at))
|
||||
if skipped or failed:
|
||||
# 日更任务失败/skip:短间隔补跑,绝不按成功推到次日。
|
||||
next_runs[name] = int(finished_at + retry_seconds)
|
||||
else:
|
||||
next_runs[name] = int(next_daily_run(finished_at, at))
|
||||
errors = [
|
||||
{"task": name, **record}
|
||||
for name, record in tasks.items()
|
||||
if isinstance(record, dict) and record.get("ok") is False
|
||||
if isinstance(record, dict)
|
||||
and record.get("ok") is False
|
||||
and not record.get("skipped")
|
||||
and not (
|
||||
isinstance(record.get("summary"), dict)
|
||||
and record["summary"].get("skipped")
|
||||
)
|
||||
]
|
||||
counts = {str(row["status"]): int(row["n"]) for row in conn.execute(
|
||||
"SELECT status, COUNT(*) AS n FROM video GROUP BY status"
|
||||
|
||||
@@ -5,6 +5,15 @@ import {
|
||||
escapeHtml, formatCount, formatDateTime, formatNumber,
|
||||
} from "../format.js";
|
||||
|
||||
function taskResultLabel(record) {
|
||||
const summary = record.summary && typeof record.summary === "object" ? record.summary : {};
|
||||
if (record.skipped || summary.skipped) {
|
||||
return `<span class="muted">跳过</span>`;
|
||||
}
|
||||
if (record.ok) return "成功";
|
||||
return `<span class="danger">失败</span>`;
|
||||
}
|
||||
|
||||
function taskRows(tasks) {
|
||||
const entries = Object.entries(tasks || {});
|
||||
if (!entries.length) return emptyState("暂无任务记录", "运行一次同步、快照或发现后会写入最近结果。");
|
||||
@@ -15,12 +24,16 @@ function taskRows(tasks) {
|
||||
${entries.map(([name, record]) => {
|
||||
const summary = record.summary && typeof record.summary === "object"
|
||||
? Object.entries(record.summary)
|
||||
.filter(([key]) => ["added", "removed", "candidates", "notified", "ok", "failed", "total", "completed", "backup_error"].includes(key))
|
||||
.filter(([key]) => [
|
||||
"added", "removed", "candidates", "notified", "ok", "failed",
|
||||
"total", "completed", "backup_error", "skipped", "already_completed",
|
||||
"reason", "elapsed", "success",
|
||||
].includes(key))
|
||||
.map(([key, value]) => `${key}=${value}`).join(" ")
|
||||
: "";
|
||||
return `<tr>
|
||||
<td>${escapeHtml(name)}</td>
|
||||
<td>${record.ok ? "成功" : `<span class="danger">失败</span>`}</td>
|
||||
<td>${taskResultLabel(record)}</td>
|
||||
<td>${escapeHtml(formatDateTime(record.finished_at))}</td>
|
||||
<td class="summary-cell">${escapeHtml(summary || record.error || "—")}</td>
|
||||
</tr>`;
|
||||
@@ -100,7 +113,7 @@ export async function renderSystem(root) {
|
||||
<div><span>新视频小时同步</span><strong>${running.snapshot_recent ? "运行中" : escapeHtml(formatDateTime(next.snapshot_recent))}</strong></div>
|
||||
<div><span>新视频搜索</span><strong>${running.discover || running.discovery ? "运行中" : escapeHtml(formatDateTime(next.discover))}</strong></div>
|
||||
</div>
|
||||
<p class="section-note">进行中显示「运行中」;空闲时按该任务上次完成时间推算下次运行,不按墙钟抢跑。新视频小时同步与全站快照互斥。</p>
|
||||
<p class="section-note">进行中显示「运行中」;空闲时按该任务上次完成时间推算下次运行。23:00–01:00(上海)为全站快照窗:发现 / 小时同步 / 候选清理会延后到 01:00 后;粉丝更新不避开。跳过或失败不会记成成功,会短间隔补跑。</p>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
|
||||
Reference in new issue
Block a user