Skip to content

[harness] fix: 修复 AgentScheduler cancel 失效与 worker 取消挂起/泄漏#38

Merged
atoncooper merged 1 commit into
mainfrom
fix/scheduler-cancel-and-cancellation-leak
Jul 23, 2026
Merged

[harness] fix: 修复 AgentScheduler cancel 失效与 worker 取消挂起/泄漏#38
atoncooper merged 1 commit into
mainfrom
fix/scheduler-cancel-and-cancellation-leak

Conversation

@atoncooper

Copy link
Copy Markdown
Owner

背景

AgentScheduler 存在两个 HIGH 级 bug,且整个模块零测试、生产中从未启用(bypass_scheduler=True 默认旁路):

  1. cancel 失效_cancel_in_queue() 恒返回 Falseticket.cancelled 全代码库无写入点 -> 对排队/延迟任务的取消完全不生效。
  2. worker 取消挂起 + active 泄漏_drain_queueasync with semaphore 内 await _execute_with_retry,但后者只 except Exceptionasyncio.CancelledError(BaseException)穿透后跳过 event.set()slot.active -= 1 -> 调用方永久挂起、active 计数泄漏。

改动

  • cancel 重写TicketState 状态机(queued/running/done/cancelled)+ _tickets 登记表;cancel() 仅对 queued 态置 cancelled=Trueevent.set() 唤醒调用方;running/done 返回 False。删除 _cancel_in_queue
  • worker try/finally:保证 event.set()/active-=1/_tickets.pop() 在 CancelledError 穿透时必达;worker 取消设哨兵 {"error":"worker cancelled"} 区分空成功。
  • 解耦 langgraph:提取 app/agent/errors.pyErrorCategory+classify_error,无 langgraph 依赖),handlers.py 改为 re-export,scheduler 模块级直接导入。原 from app.agent.memory.handlers 会拉起 memory/__init__ -> graph -> langgraph,使 scheduler 无法独立单测。
  • 加固TicketState(str, Enum) 替代魔法字符串;QueueFull / _schedule_one 提前返回补 _tickets.pop();多 worker semaphore 等待期间被 cancel 的竞态 recheck;shutdown()_ticketscancel() 原子性注释。
  • 测试:新增 test_agent_scheduler.py(11 测试:cancel 5 + worker 取消 1 + state 契约 1 + retry 4)。解耦后 test_agent_runtime.py(14)恢复可跑(此前被 langgraph 阻塞)。

验证

  • pytest app/test/test_agent_scheduler.py app/test/test_agent_runtime.py -> 25 passed
  • ruff check -> All checks passed
  • py_compile -> 通过
  • scheduler 导入不依赖 langgraph(sys.modules 验证)
  • code-reviewer 复查:两个 HIGH 确认修复正确

影响范围

  • scheduler 生产中 bypass_scheduler=True 默认旁路,本次为潜伏 bug 清除 + 测试补全,不改变生产行为
  • 为后续接线 scheduler(启用限流/重试)扫清障碍。
  • app/agent/errors.py 提取是纯重构,handlers.py re-export 保持 from app.agent.memory.handlers import classify_error 不破。

- cancel() 引入 TicketState 状态机 + _tickets 登记表,仅对 queued 态
  生效并唤醒调用方;删除恒返回 False 的 _cancel_in_queue
- _drain_queue try/finally 保证 event.set()/active-=1/_tickets.pop 在
  CancelledError(BaseException) 穿透时必达,调用方不再永久挂起、active 不泄漏
- 提取 app/agent/errors.py 解耦 scheduler 对 langgraph 的模块级依赖
  (原 handlers.py 路径会拉起 memory/__init__ -> langgraph),
  handlers.py 改为 re-export,scheduler 错误路径也不再依赖 langgraph
- TicketState(str, Enum) 替代魔法字符串;补 QueueFull/_schedule_one 提前
  返回的 _tickets 泄漏修复;worker 取消设哨兵返回值;多 worker semaphore
  recheck cancelled;shutdown 清理 _tickets;cancel 原子性注释
- 新增 test_agent_scheduler.py(11 测试:cancel 5 + worker 取消 1 +
  state 契约 1 + retry 4);解耦后 test_agent_runtime.py(14) 恢复可跑
@atoncooper
atoncooper merged commit 24c43e5 into main Jul 23, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant