fix: LLM 任务硬超时 + 消息落库异步化,避免主循环饥饿#1707
Conversation
- TaskConfig 新增 hard_timeout 字段(默认 180s);LLMOrchestrator 5 个公开入口统一套 asyncio.wait_for,超时抛 LLMTaskTimeoutError(继承 ModelAttemptFailed)复用切模型链路 - DEFAULT_TASK_CONFIG_TEMPLATES 9 个任务块种子化 hard_timeout:planner=120、utils/learner/emoji/voice=60、embedding=30、其余 180 - MessageUtils 新增 store_message_to_db_async(asyncio.to_thread 包装);message_gateway / heartflow_message_processor / bot / send_service 4 个调用点改为 await,消除主事件循环里同步 SQLAlchemy session 的阻塞 修复 LLM 单次调用被上游代理 TPM 静默排队卡死、叠加同步落库阻塞导致主循环饥饿、plugin.health 30s 超时连锁杀 napcat 的故障
PR review 反馈两处问题修正: - hard_timeout 原先包在 _execute_request 整体外面,超时会取消整个 for 循环导致内部 except ModelAttemptFailed 进不去,"切下一个模型" fallback 失效且 usage_penalty -1 清理被绕过造成负载均衡计数器泄露。改为在 _execute_request 循环内对单次 _attempt_request_on_model 套 asyncio.wait_for,TimeoutError 转 LLMTaskTimeoutError(继承 ModelAttemptFailed)由现有 except 分支自动接住,复用切模型链路与计数器清理 - DEFAULT_TASK_CONFIG_TEMPLATES 新增的 5 个最小条目(learner/emoji/vlm/voice/embedding)只写了 hard_timeout,但 build_default_model_templates 用 task_template["model_list"] 直接索引,会让 create_default_model_config 在新部署时 KeyError。补 model_list=[]
PR review 第二轮反馈两处问题修正: - build_single_model_task / summary_importer 重建 TaskConfig 时漏传 hard_timeout,导致 A_Memorix 单模型路由从 planner 模板派生时 120s 被静默退回 180s 默认值;同步带上字段 - _build_task_config_signature 未把 hard_timeout 纳入签名,热重载只改超时时签名不变会被误判为旧配置;签名元组追加 hard_timeout - _attempt_request_on_model_with_timeout 内层 warning 与 _execute_request 的 except ModelAttemptFailed 分支日志重复(LLMTaskTimeoutError.message 已含 task/model/timeout 信息),删除内层日志
PR review 第三轮反馈:to_thread 默认线程池有多个 worker,突发消息流量下多个 worker 会并发拿 SQLAlchemy session 写入;底层 SQLite WAL 仅允许单写、busy_timeout 1s 不足以缓冲所有突发,会触发 `database is locked`。 新增 module-level `_DB_WRITE_LOCK = asyncio.Lock()`,store_message_to_db_async 在 持有 lock 后再 to_thread,把写入串行化;仍保持同步 SQLAlchemy session 在线程池里 执行,事件循环不阻塞。
PR review 第四轮反馈两处问题修正: - await_task_with_interrupt 只在 interrupt_flag set 时 cancel child task;外层 asyncio.wait_for 触发 hard_timeout 时仅 CancelledError 从 sleep 抛出,child task 不被取消导致上游 httpx 请求继续在后台跑、占用连接 / token。加 try/finally 在所有 退出路径上 cancel 未完成的 child task - LLMTaskTimeoutError 构造里写死 original_exception=None 丢失了原始 asyncio.TimeoutError 上下文。__init__ 接收 original_exception 并向 ModelAttemptFailed 透传;调用点传 e
PR review 第五轮反馈两处问题修正: - _execute_request 内 `last_exception = e.original_exception or e` 会展开 original_exception, 上一轮加的 `original_exception=e` 让所有模型都超时时上层最终收到 raw asyncio.TimeoutError 而非承诺的 LLMTaskTimeoutError。撤回 original_exception 传递,原始 TimeoutError 仍通过 `raise ... from e` 的 __cause__ 链保留供 traceback / 调试使用 - update_message_id 是同步 DB 写入,在 echo_message_process(async)里被裸调,违反本次 消息落库异步化目标。新增 update_message_id_async 走同一把 _DB_WRITE_LOCK + to_thread; bot.py:473 调用点改为 await
PR review 第六轮反馈:module-level `asyncio.Lock()` 在测试场景(pytest-asyncio 每用例独立 loop)会绑定到第一个使用它的 loop,后续切换 loop 时跨 loop 复用。 改为 `_get_db_write_lock()` 懒初始化,首次需要时再构造,避免 import 期触发; 也与项目内其他 lock 全为函数/实例级的风格保持一致。
PR review 第七轮反馈:finally 里只 cancel 不 await,子任务在后台延迟清理, 若 cancel 后子任务清理过程中再抛异常会变成「Task exception was never retrieved」 警告污染日志。cancel 后用 contextlib.suppress 抑制 CancelledError 与清理异常 再 await,让子任务生命周期完全收敛在本函数内。
PR review 第八轮反馈两处问题合并修正: - gemini 建议改用 functools.lru_cache(maxsize=None) 简化 lazy init,比手写 `if None then create` 更 Pythonic - sourcery 指出手写 lazy init 存在理论 race(两个协程同时见 None 各自建 Lock), lru_cache 内部用 thread lock 自动消除该 race - 多 loop 场景下 Lock 仍绑定首次使用所在 loop(lru_cache 也一样),但 MaiBot 单进程单 event loop 不会触发;在 docstring 显式注明该假设供后续维护参考
PR review 第九轮反馈实事求是修正:之前我注释里写「假设 MaiBot 是单进程单 event loop」 但实际上 bot.py 主 loop、WebUI 在另一个线程的独立 loop、asyncio.run 临时 loop 等 都同时存在;asyncio.Lock 只能互斥同一个 loop 内的协程,对跨 loop / 跨线程并发写 完全形同虚设。 改成 module-level threading.Lock: - 在 to_thread worker 线程内同步持有,不依赖 event loop - 进程级互斥,任何 loop / 任何线程发起的 SQLite 写都被串行 - 不再需要 lru_cache / lazy init 兜底跨 loop 风险,代码更直白
PR review 第十轮反馈两处问题修正: - _DB_WRITE_THREAD_LOCK 之前只在 async wrapper 里持有,sync store_message_to_db / update_message_id 的直接调用方(如 pytests/test_message_image_persistence.py、 迁移脚本)会绕过锁形成竞争。把 with _DB_WRITE_THREAD_LOCK 移入两个 sync 方法 本体,所有写入路径共享同一套串行化;async wrapper 简化为 to_thread 直透传 - await_task_with_interrupt finally 里的 contextlib.suppress(Exception) 太宽, 会静默掉子任务清理代码里的 AttributeError / TypeError 等编程错误。改成 try/except CancelledError + Exception (debug 日志),CancelledError 静默, 其他异常以 debug 记录避免完全淹没。同步移除文件顶部不再需要的 contextlib 导入
…db-store fix: LLM任务级 hard_timeout 与消息落库异步化
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
WalkthroughPR 在多个层面引入任务级 hard_timeout(配置、模板、异常、orchestrator 包装与路由传播),并将消息写入路径改为受进程级锁保护的同步实现加 asyncio.to_thread 的异步透传,同时把调用方切换为 await 异步接口。 ChangesTask-Level Hard Timeout Control System
Async Message Storage Serialization
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/common/utils/utils_message.py (1)
14-15: ⚡ Quick win请按项目规范重排导入顺序与分组。
新增的标准库导入应与其他标准库导入放在同一分组,并整体保持“标准库/第三方/本地模块”分块顺序,避免后续 Ruff 导入规则不一致。
As per coding guidelines,
**/*.py: "Place standard library and third-party library imports before local module imports" and "Separate import blocks with a single blank line".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/common/utils/utils_message.py` around lines 14 - 15, 调整 src/common/utils/utils_message.py 的导入顺序与分组:将新加的标准库导入 asyncio 和 threading 与文件中其它标准库导入放在同一“标准库”块,确保 import 块遵循“标准库 / 第三方 / 本地模块”顺序,并在各块之间保留一个空行以符合项目规范和 Ruff 规则;查找文件中涉及 asyncio 或 threading 的引用确保不破坏相对导入或本地模块顺序(例如与同文件或同包内的本地模块导入保持分隔)。
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/common/utils/utils_message.py`:
- Around line 14-15: 调整 src/common/utils/utils_message.py 的导入顺序与分组:将新加的标准库导入
asyncio 和 threading 与文件中其它标准库导入放在同一“标准库”块,确保 import 块遵循“标准库 / 第三方 /
本地模块”顺序,并在各块之间保留一个空行以符合项目规范和 Ruff 规则;查找文件中涉及 asyncio 或 threading
的引用确保不破坏相对导入或本地模块顺序(例如与同文件或同包内的本地模块导入保持分隔)。
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 138bcb46-f3e4-4bbb-9a47-5d009463e729
📒 Files selected for processing (12)
src/A_memorix/core/utils/model_routing.pysrc/A_memorix/core/utils/summary_importer.pysrc/chat/heart_flow/heartflow_message_processor.pysrc/chat/message_receive/bot.pysrc/common/utils/utils_message.pysrc/config/default_model_config.pysrc/config/model_configs.pysrc/llm_models/exceptions.pysrc/llm_models/model_client/adapter_base.pysrc/llm_models/utils_model.pysrc/plugin_runtime/host/message_gateway.pysrc/services/send_service.py
main分支 禁止修改,请确认本次提交的分支 不是main分支src/A_memorix,我确认已阅读src/A_memorix/MODIFICATION_POLICY.md,不涉及则无需勾选其他信息
Summary by CodeRabbit
发布说明
新功能
改进