feat(scheduler): 定时同步全面页面化(子进程跑页面脚本,API直调退出调度防风控)
- 每日 full: bash scripts/full_sync_all.sh;每4h messages: scrape_via_page --entity messages - 子进程隔离+asyncio.Lock互斥+非零退出只记JSON日志;自动补DB_URL环境 - ENTITY_FUNCS/run_once/CLI保留为手动调试,不再挂定时触发器 - README同步更新(Docker镜像需Playwright+Chromium);413测试绿 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -437,8 +437,11 @@ async def test_start_registers_two_jobs_with_expected_triggers(engine, session_m
|
||||
# IntervalTrigger exposes its interval as a timedelta on .interval`.
|
||||
assert interval.interval.total_seconds() == 6 * 3600
|
||||
|
||||
# The full-sync job passes args=["all"] (so it hits _run_all).
|
||||
assert full_call.kwargs["args"] == ["all"]
|
||||
# 2026-08-20 起:两个 job 都打到 _safe_page_run(页面方式子进程),
|
||||
# full 传 "full"、messages 传 "messages"。
|
||||
assert full_call.args[0].__name__ == "_safe_page_run"
|
||||
assert full_call.kwargs["args"] == ["full"]
|
||||
assert msg_call.args[0].__name__ == "_safe_page_run"
|
||||
assert msg_call.kwargs["args"] == ["messages"]
|
||||
|
||||
# coalesce=True + max_instances=1 protects against overlap.
|
||||
@@ -510,3 +513,73 @@ def _mk_result(
|
||||
upserted=upserted,
|
||||
error=error,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Page-based scheduled jobs (2026-08-20 — API 直调退出定时任务,全部页面方式防风控)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class _FakeProc:
|
||||
def __init__(self, rc: int = 0) -> None:
|
||||
self._rc = rc
|
||||
|
||||
async def wait(self) -> int:
|
||||
return self._rc
|
||||
|
||||
|
||||
async def test_safe_page_run_full_invokes_bash_full_sync(engine, monkeypatch):
|
||||
"""full job = bash scripts/full_sync_all.sh(页面方式全量序列,子进程隔离)。"""
|
||||
sched = SyncScheduler(engine=engine)
|
||||
captured: dict = {}
|
||||
|
||||
async def fake_exec(*args, cwd=None, env=None):
|
||||
captured["args"] = list(args)
|
||||
captured["cwd"] = cwd
|
||||
return _FakeProc(0)
|
||||
|
||||
monkeypatch.setattr(scheduler_module.asyncio, "create_subprocess_exec", fake_exec)
|
||||
monkeypatch.setattr(scheduler_module.shutil, "which", lambda name: "/usr/bin/bash")
|
||||
|
||||
await sched._safe_page_run("full")
|
||||
|
||||
assert captured["args"][0] == "/usr/bin/bash"
|
||||
assert captured["args"][1].endswith("full_sync_all.sh")
|
||||
# cwd 指向 yunke-server(脚本的相对路径基准)
|
||||
assert str(captured["cwd"]).replace("\\", "/").endswith("yunke-server")
|
||||
|
||||
|
||||
async def test_safe_page_run_messages_builds_scrape_command(engine, monkeypatch):
|
||||
"""messages job = python scripts/scrape_via_page.py --entity messages。"""
|
||||
sched = SyncScheduler(engine=engine, max_conversations_per_run=123)
|
||||
captured: dict = {}
|
||||
|
||||
async def fake_exec(*args, cwd=None, env=None):
|
||||
captured["args"] = list(args)
|
||||
captured["env"] = env
|
||||
return _FakeProc(0)
|
||||
|
||||
monkeypatch.setattr(scheduler_module.asyncio, "create_subprocess_exec", fake_exec)
|
||||
|
||||
await sched._safe_page_run("messages")
|
||||
|
||||
args = captured["args"]
|
||||
assert args[1].endswith("scrape_via_page.py")
|
||||
i = args.index("--entity")
|
||||
assert args[i + 1] == "messages"
|
||||
j = args.index("--max-conversations")
|
||||
assert args[j + 1] == "123"
|
||||
|
||||
|
||||
async def test_safe_page_run_nonzero_rc_is_logged_not_raised(engine, monkeypatch, capsys):
|
||||
"""子进程非零退出只记日志不抛(调度器保住下一个周期)。"""
|
||||
sched = SyncScheduler(engine=engine)
|
||||
monkeypatch.setattr(scheduler_module.shutil, "which", lambda name: "/usr/bin/bash")
|
||||
|
||||
async def fake_exec(*args, cwd=None, env=None):
|
||||
return _FakeProc(3)
|
||||
|
||||
monkeypatch.setattr(scheduler_module.asyncio, "create_subprocess_exec", fake_exec)
|
||||
|
||||
await sched._safe_page_run("full") # 不应抛异常
|
||||
out = capsys.readouterr().out
|
||||
assert '"rc": 3' in out or "'rc': 3" in out or "rc" in out
|
||||
|
||||
Reference in New Issue
Block a user