fix(scheduler): do not force full snapshot on post-seed restart
Schedule snapshot_seed/snapshot/fans to the next wall-clock seed_at / snapshot_at on process start (optionally from last task record), instead of next_run=0. Force-seal of seed shells only after today's snapshot_at (or CLI --force), so a daytime restart cannot scrape the whole site.
This commit is contained in:
1 parent
1697573d95
commit
51ffa44577
3 files changed
+241
-10
No files matched your search
@@ -588,6 +588,166 @@ def test_candidate_cleanup_failure_retries_soon():
|
||||
assert 250 <= delta <= 360, delta
|
||||
|
||||
|
||||
|
||||
|
||||
def test_initial_daily_next_run_uses_wall_clock_not_zero():
|
||||
"""常驻启动:无上次记录时应对齐下一档墙钟,不得为 0。"""
|
||||
from reaction_board.timeutil import next_daily_run, SHANGHAI_TZ
|
||||
from datetime import datetime
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = write_config(tmp, cookie="", csrf="")
|
||||
assert run_cli(["init", "--config", str(path)])[0] == 0
|
||||
db = Database(Path(tmp) / "app.db")
|
||||
db.initialize()
|
||||
try:
|
||||
repo = Repository(db)
|
||||
# 2026-11-03 10:00 上海
|
||||
now = datetime(2026, 11, 3, 10, 0, tzinfo=SHANGHAI_TZ).timestamp()
|
||||
nxt = cli._initial_daily_next_run(
|
||||
repo, "snapshot", "23:15", now=now,
|
||||
)
|
||||
assert nxt == next_daily_run(now, "23:15")
|
||||
assert nxt > now
|
||||
# 成功记录在昨晚 → 仍是今晚 23:15
|
||||
yday_done = datetime(2026, 11, 2, 23, 20, tzinfo=SHANGHAI_TZ).timestamp()
|
||||
repo.kv_set(
|
||||
"task:last:snapshot",
|
||||
json.dumps({
|
||||
"ok": True, "finished_at": int(yday_done),
|
||||
"started_at": int(yday_done) - 10,
|
||||
}),
|
||||
)
|
||||
nxt2 = cli._initial_daily_next_run(
|
||||
repo, "snapshot", "23:15", now=now,
|
||||
)
|
||||
assert nxt2 == next_daily_run(yday_done, "23:15")
|
||||
assert nxt2 > now
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def test_reached_daily_slot_gates_force_seed():
|
||||
from reaction_board.timeutil import SHANGHAI_TZ
|
||||
from datetime import datetime
|
||||
|
||||
before = datetime(2026, 11, 3, 10, 0, tzinfo=SHANGHAI_TZ).timestamp()
|
||||
at = datetime(2026, 11, 3, 23, 15, tzinfo=SHANGHAI_TZ).timestamp()
|
||||
after = datetime(2026, 11, 3, 23, 16, tzinfo=SHANGHAI_TZ).timestamp()
|
||||
assert cli._reached_daily_slot("23:15", before) is False
|
||||
assert cli._reached_daily_slot("23:15", at) is True
|
||||
assert cli._reached_daily_slot("23:15", after) is True
|
||||
|
||||
|
||||
def test_restart_after_seed_schedules_snapshot_at_wall_clock():
|
||||
"""午夜 seed 后重启:snapshot 应排到当晚 snapshot_at,首轮 run_due 不得 force 全量。"""
|
||||
from reaction_board.scheduler import ScheduledTask, Scheduler
|
||||
from reaction_board.timeutil import next_daily_run, SHANGHAI_TZ, shanghai_date
|
||||
from datetime import datetime
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = write_config(tmp, cookie="", csrf="")
|
||||
assert run_cli(["init", "--config", str(path)])[0] == 0
|
||||
db = Database(Path(tmp) / "app.db")
|
||||
db.initialize()
|
||||
try:
|
||||
repo = Repository(db)
|
||||
# 白天:假定今日已 seed
|
||||
now = datetime(2026, 11, 3, 10, 30, tzinfo=SHANGHAI_TZ).timestamp()
|
||||
today = shanghai_date(now)
|
||||
repo.kv_set(f"snapshot:seeded:{today}", "1")
|
||||
repo.kv_set(
|
||||
"task:last:snapshot_seed",
|
||||
json.dumps({
|
||||
"ok": True,
|
||||
"finished_at": int(
|
||||
datetime(2026, 11, 3, 0, 1, tzinfo=SHANGHAI_TZ).timestamp()
|
||||
),
|
||||
"started_at": int(
|
||||
datetime(2026, 11, 3, 0, 0, tzinfo=SHANGHAI_TZ).timestamp()
|
||||
),
|
||||
"summary": {"reason": "seeded"},
|
||||
}),
|
||||
)
|
||||
snapshot_at = "23:15"
|
||||
seed_at = "00:00"
|
||||
snapshot_next = cli._initial_daily_next_run(
|
||||
repo, "snapshot", snapshot_at, now=now,
|
||||
)
|
||||
seed_next = cli._initial_daily_next_run(
|
||||
repo, "snapshot_seed", seed_at, now=now,
|
||||
)
|
||||
fans_next = cli._initial_daily_next_run(
|
||||
repo, "fans", snapshot_at, now=now,
|
||||
)
|
||||
assert snapshot_next == next_daily_run(now, snapshot_at)
|
||||
assert fans_next == next_daily_run(now, snapshot_at)
|
||||
# seed 已成功 → 下一档是次日 00:00
|
||||
assert seed_next == next_daily_run(
|
||||
float(json.loads(repo.kv_get("task:last:snapshot_seed"))["finished_at"]),
|
||||
seed_at,
|
||||
)
|
||||
assert seed_next > now
|
||||
|
||||
force_calls = []
|
||||
|
||||
def run_snapshot():
|
||||
force = (
|
||||
repo.kv_get(f"snapshot:seeded:{today}") == "1"
|
||||
and cli._reached_daily_slot(snapshot_at, now)
|
||||
)
|
||||
force_calls.append(force)
|
||||
if force:
|
||||
raise AssertionError("daytime restart must not force-seal seed")
|
||||
return None
|
||||
|
||||
clock = [now]
|
||||
scheduler = Scheduler(
|
||||
[
|
||||
ScheduledTask("snapshot_seed", 86400.0, lambda: None, next_run=seed_next),
|
||||
ScheduledTask("snapshot", 86400.0, run_snapshot, next_run=snapshot_next),
|
||||
ScheduledTask("fans", 86400.0, lambda: None, next_run=fans_next),
|
||||
],
|
||||
clock=lambda: clock[0],
|
||||
)
|
||||
assert scheduler.run_due() == []
|
||||
assert force_calls == []
|
||||
# 跳到 snapshot_at:才允许 force
|
||||
clock[0] = next_daily_run(now, snapshot_at)
|
||||
# 任务 next_run 已是该点,到期应执行;此时 reached_daily_slot 为真
|
||||
# 但 run_snapshot 闭包捕获的是旧 now——改用 clock
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# 单独验证:到达 slot 后 force 门打开
|
||||
slot = datetime(2026, 11, 3, 23, 15, tzinfo=SHANGHAI_TZ).timestamp()
|
||||
assert cli._reached_daily_slot("23:15", slot) is True
|
||||
|
||||
|
||||
def test_daytime_seed_marker_does_not_force_without_slot():
|
||||
"""即使 seeded=1,未到 snapshot_at 也不得 force(防守:即便误触发 run_snapshot)。"""
|
||||
from reaction_board.timeutil import SHANGHAI_TZ
|
||||
from datetime import datetime
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = write_config(tmp, cookie="", csrf="")
|
||||
assert run_cli(["init", "--config", str(path)])[0] == 0
|
||||
db = Database(Path(tmp) / "app.db")
|
||||
db.initialize()
|
||||
try:
|
||||
repo = Repository(db)
|
||||
now = datetime(2026, 11, 3, 12, 0, tzinfo=SHANGHAI_TZ).timestamp()
|
||||
today = "2026-11-03"
|
||||
repo.kv_set(f"snapshot:seeded:{today}", "1")
|
||||
force_seed = (
|
||||
repo.kv_get(f"snapshot:seeded:{today}") == "1"
|
||||
and cli._reached_daily_slot("23:15", now)
|
||||
)
|
||||
assert force_seed is False
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def main_test():
|
||||
test_init_and_status()
|
||||
test_missing_config()
|
||||
@@ -608,6 +768,10 @@ def main_test():
|
||||
test_snapshot_seed_same_day_completed_succeeds()
|
||||
test_defer_snapshot_seed_schedules_ten_minutes()
|
||||
test_candidate_cleanup_failure_retries_soon()
|
||||
test_initial_daily_next_run_uses_wall_clock_not_zero()
|
||||
test_reached_daily_slot_gates_force_seed()
|
||||
test_restart_after_seed_schedules_snapshot_at_wall_clock()
|
||||
test_daytime_seed_marker_does_not_force_without_slot()
|
||||
print("OK: CLI init/sync/snapshot/snapshot-seed/auth-check/run/status/backfill/错误路径测试全部通过")
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user