fix(runner): detect stale event loop in _ensure_root_task_group to prevent cross-loop reuse - #525
Conversation
|
|
1 similar comment
|
|
|
head_sha: 变更摘要此 PR 修复了 主要改动
|
|
head_sha: 代码审查审查总结
按优先级统计:P0: 0, P1: 0, P2: 1, P3: 1 整体风险评估:低风险。该修复正确解决了 PR 目标场景(loop 先关闭再切新 loop 时的跨循环 RuntimeError),核心逻辑无缺陷。唯一的 P2 问题是活体检测不完整——未检测"旧 loop 存活但不同"的场景,在并发多 loop 驱动同一 Runner 时仍会触发相同的跨循环错误。但该场景本就是 #1444 涉及的更大问题(需重构单例),本 PR 从未声称解决它,且单 loop 正常路径零回归。P3 是测试文件中一个未使用的
💬 仅评论 |
| except RuntimeError: | ||
| owner_loop = None | ||
| if owner_loop is not None and not owner_loop.is_closed(): | ||
| return # owner still alive on a live loop → safe to reuse |
There was a problem hiding this comment.
head_sha: a1395a3261c11c5546377dd3f58958d6f6cdd39e
🟡 Medium Priority
changed line → affected behavior/contract → failure mode → suggested fix
变更行:runner.py:165 的条件仅判断 owner_loop.is_closed(),注释(:155)明确写了 "closed/foreign event loop",但代码只检测了 "closed",未检测 "foreign"。
失效场景:当旧事件循环仍存活、但与当前运行循环不是同一个时(例如两路 app 分别在 loop1/loop2 上并发驱动同一个 GLOBAL_RUNNER,或 loop1 未关闭就切到 loop2),owner.get_loop() 返回 loop1,loop1.is_closed() 为 False,条件成立 → return,复用绑定在 loop1 上的 owner task。后续 stop() → _close_root_task_group 的 await owner(:203)会在 loop2 上 await 一个绑定 loop1 的 task,抛出 RuntimeError: Task ... got Future ... attached to a different loop。
触发条件:旧 loop 存活且不同于当前 loop。本 PR 处理了 loop1 先关闭再切 loop2 的串行场景,但未覆盖 loop1 未关闭就切 loop2 的场景(参见 #1444 多线程并发)。
if owner_loop is not None and not owner_loop.is_closed() and owner_loop is asyncio.get_running_loop():
return
这不会影响单 loop 正常路径(此时 is 恒为 True),也不会影响已关闭 loop 的重建路径(is_closed() 先短路为 False)。
建议:在 is_closed() 检查后增加 owner_loop is asyncio.get_running_loop() 检查,确保 owner 不仅 loop 存活、而且与当前运行循环一致。
Paired: GitHub #525 ↔ GitCode !2323
PR 草稿:openjiuwen Runner —
_ensure_root_task_group增事件循环活性检测查证前置(提 PR 前已做的功课)
为避免重复劳动,提交前已查证:
GLOBAL_RUNNER(runner.py:683)模块级单例 +_root_task_group(runner.py:97)单 owner 单 loop 持有 +Runner全@classmethod转发。本 PR 是该 issue 的渐进修复,区别于报告人提的"可实例化隔离 Runner"大改方案——两条路互补。_ensure_root_task_group或 runner task group 复用逻辑,不撞车。develop即可。标题(Title)
fix(runner): detect stale event loop in _ensure_root_task_group to prevent cross-loop task group reuse摘要(Summary)
GLOBAL_RUNNER是模块级单例。当多个 FastAPI app(或TestClient)各自的 lifespan 反复驱动同一全局 Runner、且各跑在独立 asyncio loop 上时,_ensure_root_task_group会盲目复用一个已失效的 root task group(owner task 绑在已关闭的旧 loop 上),导致stop()→_close_root_task_group里await owner抛RuntimeError: Task ... got Future ... attached to a different loop,runner 无法干净停止,后续 lifespan teardown 冒泡成未捕获异常。本 PR 在复用判断里加一行事件循环活性检测:owner task 绑定的 loop 已关闭时,视为失效,丢弃旧 task group 并在当前 loop 重建。是 #245 的渐进修复(不重构单例,单 loop 场景零回归)。
问题复现(Reproduction)
环境:openjiuwen 0.1.16.post1,FastAPI +
TestClient,两个测试文件分别用 session-scope 和 module-scope fixture 各自构建独立 app(lifespan 内await Runner.start()/await Runner.stop())。最小复现(pytest 风格):
关键:单跑任一 fixture 通过;跨 fixture 混跑(先 app A 再 app B)必现。
与 #245 的区别:#245 报的是"多线程并发"挂死(线程各自 loop 并发调
Runner.run_agent);本 PR 补的是"单线程多 app 串行切 loop"场景——根因同源(单例 + 单 loop 绑定),但复现门槛更低(单线程即可触发),且报错文案不同(attached to a different loop,#245 未提及)。根因(Root Cause)
openjiuwen/core/runner/runner.py:152-169:_root_task_group_owner是asyncio.create_task(...)返回的Task,绑定创建它的 loop。GLOBAL_RUNNER(runner.py:683)跨 app/跨 loop 复用。_ensure_root_task_group见字段非 None 直接 return,复用 loop A 的 owner task。stop()→_close_root_task_group(runner.py:171-203)执行await owner(:184)——owner 绑 loop A、当前是 loop B →RuntimeError: attached to a different loop。_close_root_task_group的except Exception(:192)吞成 warning,但 finally 清字段后,下一轮又复用同样失效的 owner(若时序交错),且 teardown 的 RuntimeError 在anyio.move_on_aftershield 外冒泡。修复(Fix)
openjiuwen/core/runner/runner.py,_ensure_root_task_group复用判断加 loop 活性检测:要点:
task.get_loop()(Python 3.10+,asyncio.Task属性)返回绑定 loop;loop.is_closed()判断是否已关闭。try/except RuntimeError兜底极旧 Python 或已销毁 task(get_loop在 task 被回收时可能抛)。_close_root_task_group的 finally 一致),避免重建失败留下半清理状态。兼容性(Compatibility)
asyncio.Task.get_loop()3.10 引入,openjiuwen 要求 3.12+,满足。return复用,零额外开销。Runner.start/stop签名不变)。验证(Verification)
自动测试:建议在 openjiuwen 侧加一个跨 loop 单测:
集成验证(本项目 SmartEquipmentAssistant 的实测):
pytest tests/test_endpoint_contract.py tests/test_rbac.py报RuntimeError: Task ... attached to a different loop(teardown)。conftest.reset_runner()强制清字段)后:通过,耗时 160s→89s(runner 不再反复 stop/start 挣扎)。reset_runner。关联(Related)
_close_root_task_group的await owner(:184)即便 owner 失效也可更早 short-circuit(本 PR 通过避免复用,间接让该路径不再 await 失效 owner)。stop()(:329-357)except Exception吞异常只记 warning——建议对RuntimeError("different loop")至少 debug 级日志,便于排查。提交流程(给提交者自己备忘)
openJiuwen-ai/agent-core到自己账号。develop开分支:fix/runner-ensure-root-task-group-loop-liveness。openjiuwen/core/runner/runner.py的_ensure_root_task_group(按上 diff)。test_ensure_root_task_group_rebuilds_after_loop_close。develop,标题用上 Title,描述填本文档内容,在 PR 描述里显式写Addresses #245(GitHub 自动关联)。bot1-mirror自动镜像到 GitCode,给 @IamCandiceGuo(#245 默认负责人)/ @seanzhang_cn / @xinyu-jiuwen 留言。提交者
SmartEquipmentAssistant 团队(Team-Electric @ gitcode)。基于 tdea-core 多智能体落地实践(详见
OpenJIUWEN框架使用经验总结.md待优化点 #1)。Linked Closing Issues: