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

+29
View File
@@ -4,6 +4,7 @@
import os
import shutil
import threading
import time
import traceback
@@ -242,6 +243,34 @@ def _task_monitor_loop() -> None:
_MONITOR_ERR_TS['n'] = 0
_MONITOR_STARTED = False
_MONITOR_START_LOCK = threading.Lock()
def start_task_monitor() -> bool:
"""启动任务监控线程(幂等)。返回本次是否真正启动了线程。
为什么需要显式启动:Flask 单体时代由模块级 `threading.Thread(target=
_task_monitor_loop, daemon=True).start()` 拉起(基线 main.py:4594),
8446166「移除 Flask」把实现搬进 services 包时**启动点丢失**,此后
`_task_monitor_loop` 全项目零引用,监控循环一次都没跑过。后果:
心跳超时不再暂停离线设备的批量任务;离线 >30 分钟 / 运行 >4 小时的任务
不再自动取消;终态任务记录与 tasks/ 目录、downloads 缓存不再回收;
`ledger_recover_orphans` 不再执行——预扣未退款的账永久悬挂在 open 状态。
现改由应用 lifespan 调用(见 web.app._make_lifespan),恢复设计意图。
幂等:多 worker 或测试多次构建应用时只启动一个线程。
"""
global _MONITOR_STARTED
with _MONITOR_START_LOCK:
if _MONITOR_STARTED:
return False
threading.Thread(target=_task_monitor_loop, name='task-monitor', daemon=True).start()
_MONITOR_STARTED = True
api_service.logger.info('任务监控线程已启动')
return True
def _touch_device(device_id: str) -> None:
"""记录设备心跳时间(设备表容量上限——device_id 是客户端任意串,
登录用户可提交海量随机 ID 使字典无界增长,超限清最旧;