← [ BACK_TO_NOTES ]

Cancellation Propagation

Cancellation Propagation 是服务端接受显式 CancelRun 后,将取消意图传播到实际执行链路的协作机制;它必须与浏览器断线/request context 取消分离。

#concept #evergreen #backend #architecture

[!info] related notes

Cancellation Propagation

一句话定义

Cancellation Propagation 是在服务端已经确认“这个 Run 应该被取消”之后,把 cancellation signal 逐层传播给正在运行的 LLM、Agent loop、tool 和资源清理逻辑。

它解决的是“取消意图怎样真正停止工作”,而不是“如何根据网络断线猜用户想取消”。

两条链必须分开

Transport disconnect

Browser network lost / reader aborted
→ HTTP/SSE transport ends
→ request context may be cancelled

这只说明连接生命周期结束,不自动代表:

Run should be cancelled

对于 detached/durable Agent execution,正确策略可能是让 Run 继续并允许后续 recovery。

Explicit business cancellation

User clicks Stop
→ CancelRun(run_id) command
→ server validates authority and current Run state
→ durable transition to cancelled
→ propagate cancellation to live execution

只有第二条链才是业务取消传播的起点。

Propagation 的典型结构

CancelRun accepted
      ↓
Run cancellation registry / execution handle
      ↓
context.CancelFunc / cancellation token
      ↓
┌──────────────┬──────────────┬──────────────┐
↓              ↓              ↓
LLM stream     Agent loop     Tool execution
stop reading   stop stepping  cooperative abort
└──────────────┴──────────────┴──────────────┘
      ↓
cleanup / terminal reconciliation

核心是 cooperative cancellation:每个下游边界都要支持 signal 或提供可取消 API。

Go Context 的角色

Go context.Context 很适合承载 cancellation signal:

executionCtx, cancel := context.WithCancel(parent)
defer cancel()

// 注册到 run_id 对应的 active execution
registerRunCancellation(runID, cancel)

显式 CancelRun 时,通过 run_id 找到对应 cancel function:

CancelRun(R7)
→ lookup active execution R7
→ cancel()

之后下游通过:

select {
case <-ctx.Done():
    // cooperative stop
default:
}

或把 ctx 继续传给支持取消的 HTTP/DB/LLM client。

关键边界:request context 可以被用于普通请求生命周期,但 durable Run 不应仅因浏览器 transport 消失就自动绑定成业务取消。

Python / Agent Runtime 边界

Go 传播 cancellation 后,Python/Agent runtime 仍需要真正支持停止:

cancel signal
→ stop scheduling next Agent step
→ stop/abort LLM stream where provider supports it
→ attempt to cancel cancellable tool calls
→ avoid emitting new non-terminal business work after cancellation

如果某个外部工具不支持取消,系统只能停止等待/停止后续步骤,或等待该操作自然完成;不能承诺任意 side effect 都能瞬间回滚。

State Transition 与 Execution Stop 是两个问题

一个可靠取消通常同时包含:

1. Authority/state:
   Run durable state becomes cancelled

2. Execution:
   live computation receives cancellation signal

只做第 1 个:后台任务可能继续浪费资源。

只做第 2 个:数据库/其他客户端不知道为什么 Run 停了,也缺少可恢复的 terminal fact。

因此两者需要协作。

BodySense 当前实践

BodySense 当前使用独立的 CancelRun 路径,而不是把 SSE disconnect 当业务取消:

POST /consultation-runs/:id/cancel
→ Runtime.CancelRun
→ RunService / RunRepository 做授权与状态转换
→ active/waiting_user → cancelled
→ registered execution cancellation is triggered where active
→ runtime emits/persists run.cancelled

相反,transport disconnect 后客户端通过 durable Runtime Event Log 做 recovery/catch-up。

主要代码边界:

apps/api/internal/consultation/runtime.go
apps/api/internal/service/run_service.go
apps/api/internal/repository/run_repository.go

常见坑

  • 用 req.Context().Done() 直接解释成“用户点击停止”。
  • 服务端已经标 cancelled,但没有向实际执行传播 signal。
  • 只停 LLM stream,却让正在运行的 tool/子任务继续创建新副作用。
  • 把 cancellation 当成 rollback:已经提交到外部世界的副作用通常不能自动撤销。
  • 不做幂等:重复 CancelRun 导致重复 cleanup/terminal event。
  • Cancel 与 Complete race 没有原子状态裁决。

最短记忆

Disconnect
= transport fact

CancelRun
= business command

Cancellation propagation
= accepted cancel intent → active execution/resources

run.cancelled
= durable terminal fact
创建于 2026/6/30 更新于 2026/9/10
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本