用户报「一直在运行中」。库里那条 run 是真的卡住了:日志里一条采集输出都没有,
说明它卡在 collect 里、还没走到任何日志。探针定位到:
标签页: [''] ← 一个 URL 为空的标签页,渲染进程已卡死
context.cookies(): OK, 90 个 ← cookie 读得到
page.evaluate('navigator.userAgent'): **永远不返回**
而问浏览器要身份(UA + client hints)是采集的**第一步**,`page.evaluate` 又**没设超时** ——
于是整轮挂在那儿,run 永远停在「运行中」。
三处修复,各挡一层:
1. `page.evaluate` / `context.cookies()` 全部加超时(8 秒)。卡住就跳过,不再无限等。
2. 不假设第一个标签页是好的:逐个试、优先抖音页;全都不行就临时开一个干净页问完关掉。
拿不到就退回库里那份 cookie —— **不编造指纹**,那比没有更糟。
3. **进程内那条路补上整体超时**:爬虫那条靠 `run_and_wait(timeout=...)` 兜底,这条路
没有子进程、没人管,里面任何一次卡住都会变成永久的「运行中」。
测试 +6:卡死的页会被跳过(真 sleep,验的正是超时)、没 UA 的页跳过、全不行时开临时页
并关掉它、优先抖音页;以及整轮卡住时 run 不会停在 running(含超时原因)。
111 lines
4.1 KiB
Python
111 lines
4.1 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""monitor runner —— 尤其是抖音那条(不走子进程的)路的运行状态流转。
|
||
|
||
这条路的地位特殊:它不经过 ``crawler_manager``,所以爬虫那套「退出码 / 日志尾巴」的
|
||
约定它一个都不沾。凡是写在那里面的东西,这条路都得单独有一份。
|
||
"""
|
||
|
||
import asyncio
|
||
|
||
import pytest
|
||
import pytest_asyncio
|
||
from sqlalchemy import select
|
||
|
||
from tools.time_util import get_current_timestamp
|
||
|
||
from api.monitor import db as monitor_db
|
||
from api.monitor import runner as runner_module
|
||
from api.monitor.models import (
|
||
MODE_CREATOR,
|
||
RUN_RUNNING,
|
||
MonitorRun,
|
||
MonitorTarget,
|
||
MonitorTask,
|
||
)
|
||
|
||
|
||
@pytest_asyncio.fixture
|
||
async def db(tmp_path):
|
||
monitor_db.set_sqlite_path(tmp_path / "monitor.db")
|
||
await monitor_db.init_db()
|
||
yield monitor_db
|
||
await monitor_db.dispose_engine()
|
||
|
||
|
||
async def _make_douyin_task() -> int:
|
||
async with monitor_db.get_session() as session:
|
||
now = get_current_timestamp()
|
||
task = MonitorTask(
|
||
name="dy", platform="dy", mode=MODE_CREATOR, enabled=True,
|
||
interval_minutes=360, max_notes_count=20, enable_comments=False,
|
||
max_comments_count=20, run_timeout_seconds=3600,
|
||
notify_enabled=False, notify_failures=False,
|
||
created_at=now, updated_at=now,
|
||
)
|
||
session.add(task)
|
||
await session.flush()
|
||
session.add(
|
||
MonitorTarget(
|
||
task_id=task.id, kind=MODE_CREATOR, external_id="MS4w-sec",
|
||
xsec_token="", xsec_source="", raw_value="MS4w-sec",
|
||
label="x", enabled=True, created_at=now,
|
||
)
|
||
)
|
||
return task.id
|
||
|
||
|
||
class TestDouyinRunStatus:
|
||
@pytest.mark.asyncio
|
||
async def test_the_run_is_marked_running_before_collecting(self, db, monkeypatch):
|
||
"""**采集开始之前**,run 就必须已经是 running。
|
||
|
||
这一行原先只写在爬虫那条分支里,于是抖音路上 run 一直停在 pending —— 一旦中途
|
||
出事(异常、或进程被重启),界面上就是一个永远「排队中」的幽灵,而且 recover()
|
||
当时也只收 running、够不着它。
|
||
"""
|
||
task_id = await _make_douyin_task()
|
||
seen = {}
|
||
|
||
async def fake_collect(out_dir, **kwargs):
|
||
async with monitor_db.get_session() as session:
|
||
run = await session.scalar(select(MonitorRun).order_by(MonitorRun.id))
|
||
seen["status"] = run.status
|
||
return {
|
||
"notes": 0,
|
||
"comments": 0,
|
||
"errors": ["故意失败"],
|
||
"jsonl_dir": str(out_dir),
|
||
}
|
||
|
||
monkeypatch.setattr(runner_module.douyin_fetch, "collect", fake_collect)
|
||
|
||
await runner_module.execute_task(task_id, trigger="manual")
|
||
|
||
assert seen["status"] == RUN_RUNNING
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_a_hanging_collect_does_not_leave_the_run_running(self, db, monkeypatch):
|
||
"""进程内那条路也要有超时。
|
||
|
||
爬虫那条靠 ``run_and_wait(timeout=...)`` 兜底,这条路没有子进程、没人管 ——
|
||
里面任何一次卡住(实测过 ``page.evaluate`` 打在一个卡死的标签页上不返回)都会让
|
||
run 永远停在「运行中」,界面上看起来就是任务卡死了。
|
||
"""
|
||
task_id = await _make_douyin_task()
|
||
async with monitor_db.get_session() as session:
|
||
task = await session.get(MonitorTask, task_id)
|
||
task.run_timeout_seconds = 1 # 把超时压到 1 秒,别让测试真等
|
||
|
||
async def hanging_collect(out_dir, **kwargs):
|
||
await asyncio.sleep(60)
|
||
raise AssertionError("不该走到这里")
|
||
|
||
monkeypatch.setattr(runner_module.douyin_fetch, "collect", hanging_collect)
|
||
|
||
await runner_module.execute_task(task_id, trigger="manual")
|
||
|
||
async with monitor_db.get_session() as session:
|
||
run = await session.scalar(select(MonitorRun).order_by(MonitorRun.id))
|
||
assert run.status != RUN_RUNNING
|
||
assert "超时" in (run.error_message or "") or "超过" in (run.error_message or "")
|