Files
MediaCrawler/tests/test_monitor_runner.py
T
butubb 5552e2a2b8
Deploy VitePress site to Pages / build (push) Canceled after 0s
Deploy VitePress site to Pages / Deploy (push) Canceled after 0s
fix(monitor): 抖音任务永远「运行中」—— page.evaluate 卡在一个死掉的标签页上,而我没给超时
用户报「一直在运行中」。库里那条 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(含超时原因)。
2026-10-10 17:51:54 +08:00

111 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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 "")