Archived
为什么:d209af8 基线在 main.py:4594 用 threading.Thread(...).start() 启动 _task_monitor_loop;8446166「移除 Flask」把实现搬进 services/runtime.py 时 只搬了函数定义、丢了启动点,6288a98 拆包后落到 services/tasks.py,HEAD 仍 无任何调用方——函数体完好,但一次都没跑过。 后果(自该次迁移起持续失效): - 心跳超时不再暂停离线设备的批量任务,排队名额无法释放 - 离线 >30 分钟 / 运行 >4 小时的任务不再自动取消 - 终态任务记录、tasks/ 孤儿目录、downloads 缓存不再回收 - ledger_recover_orphans 停摆,预扣未退款的账永久悬挂在 open 状态 改法:新增 start_task_monitor(),加锁保证幂等(重复调用返回 False), 在 create_app 的 lifespan 里用 asyncio.to_thread 启动;新增 start_monitor 开关供测试显式关闭。裸 TestClient(app) 不进上下文管理器、不触发 lifespan, 故既有测试不受影响。 验证:pytest 新增 3 例(幂等 / lifespan 启动 / 可跳过);端到端 uvicorn 冒烟 日志出现「任务监控线程已启动」。
68 lines
2.6 KiB
Python
68 lines
2.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""任务监控线程的启动契约。
|
|
|
|
背景(回归):Flask 单体时代该线程由模块级代码拉起,8446166「移除 Flask」
|
|
把实现搬进 services 包时启动点丢失,`_task_monitor_loop` 此后全项目零引用,
|
|
监控循环一次都没跑过。这里钉住两条契约:
|
|
1. 启动是幂等的(多 worker / 多次构建应用只起一个线程);
|
|
2. 应用 lifespan 会启动它(否则心跳超时暂停、超时取消、账本孤儿对账
|
|
等全部静默失效)。
|
|
"""
|
|
from fastapi.testclient import TestClient
|
|
|
|
from core import db as core_db
|
|
from services import tasks as services_tasks
|
|
|
|
|
|
def _isolate_data_dir(monkeypatch, tmp_path):
|
|
"""把站点数据目录指向临时目录,避免 lifespan 的 init_db 触碰真实库。"""
|
|
from auth_system import db as auth_db
|
|
|
|
data_dir = tmp_path / 'data'
|
|
data_dir.mkdir(parents=True, exist_ok=True)
|
|
for mod in (auth_db, core_db):
|
|
monkeypatch.setattr(mod, 'DATA_DIR', data_dir, raising=False)
|
|
monkeypatch.setattr(mod, 'DB_PATH', data_dir / 'site.db', raising=False)
|
|
monkeypatch.setattr(auth_db, 'BACKUP_DIR', data_dir / 'backups', raising=False)
|
|
monkeypatch.setattr(auth_db, 'SECRET_KEY_PATH', data_dir / 'secret.key', raising=False)
|
|
return data_dir
|
|
|
|
|
|
def test_start_task_monitor_is_idempotent(monkeypatch):
|
|
"""重复调用只启动一次(空转的 loop 替身,避免真的跑监控循环)。"""
|
|
monkeypatch.setattr(services_tasks, '_MONITOR_STARTED', False)
|
|
monkeypatch.setattr(services_tasks, '_task_monitor_loop', lambda: None)
|
|
|
|
assert services_tasks.start_task_monitor() is True
|
|
assert services_tasks.start_task_monitor() is False
|
|
|
|
|
|
def test_lifespan_starts_monitor(monkeypatch, tmp_path):
|
|
"""应用 lifespan 启动监控线程(该启动点曾在迁移中丢失)。"""
|
|
from web.app import create_app
|
|
|
|
_isolate_data_dir(monkeypatch, tmp_path)
|
|
calls: list[int] = []
|
|
monkeypatch.setattr(
|
|
services_tasks, 'start_task_monitor', lambda: calls.append(1) or True)
|
|
|
|
with TestClient(create_app()) as client:
|
|
assert client.get('/health').status_code == 200
|
|
|
|
assert calls == [1]
|
|
|
|
|
|
def test_lifespan_can_skip_monitor(monkeypatch, tmp_path):
|
|
"""start_monitor=False 时不启动(供测试或嵌入式构建使用)。"""
|
|
from web.app import create_app
|
|
|
|
_isolate_data_dir(monkeypatch, tmp_path)
|
|
calls: list[int] = []
|
|
monkeypatch.setattr(
|
|
services_tasks, 'start_task_monitor', lambda: calls.append(1) or True)
|
|
|
|
with TestClient(create_app(start_monitor=False)) as client:
|
|
assert client.get('/health').status_code == 200
|
|
|
|
assert calls == []
|