fix: 恢复任务监控线程的启动点(迁移中丢失,超时取消与账本对账静默失效)

为什么: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 冒烟
日志出现「任务监控线程已启动」。
This commit is contained in:
snowy-yang committed 2026-09-26 18:53:40 +08:00
1 parent 095171e0b7
commit 0b25f215a2
3 files changed
+137 -20

No files matched your search

+67
View File
@@ -0,0 +1,67 @@
# -*- 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 == []