Skip to content

fix(tasks): preserve current waits after stale generation completion - #567

Open
lizzjin wants to merge 1 commit into
xintaofei:mainfrom
lizzjin:fix/564-preserve-current-generation-waits
Open

fix(tasks): preserve current waits after stale generation completion#567
lizzjin wants to merge 1 commit into
xintaofei:mainfrom
lizzjin:fix/564-preserve-current-generation-waits

Conversation

@lizzjin

@lizzjin lizzjin commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

问题

当同一个任务已经从 generation N 进入 generation N+1 后,generation N 的旧连接仍可能延迟送达 TurnComplete(cancelled)

数据库状态本身已经由 task_id + run_seq + status 的 CAS 保护:旧 generation 的事件不能把新 generation 改成 canceled。但 TaskEngine::on_turn_complete 原先会在执行该数据库 CAS 前,直接按 task_id 删除整个 awaiting 集合:

self.index.lock().await.remove(conn_id);
self.awaiting.lock().await.remove(&task_id);
self.forget_delegation_children_of(conn_id).await;

这里的关键问题是:index 能区分连接及其 generation,而 awaiting 只以 task_id 为 key。因此,旧连接完成时会误删当前 generation 的所有待处理 permission/question 请求。

一个确定性的失败过程如下:

  1. generation N+1 同时存在 permission-apermission-b 两个未处理请求,任务状态为 awaiting_input
  2. generation N 的旧连接延迟送达 TurnComplete(cancelled)
  3. 数据库 CAS 正确拒绝旧 generation 的取消写入,但内存中的 awaiting 集合已经被旧事件清空;
  4. 用户只处理 permission-a 后,track_request 看到空集合,错误地把任务恢复为 running
  5. permission-b 实际仍未处理,agent 仍然阻塞,界面状态与真实运行状态不一致。

这与 #546 修复的数据库状态回退问题相关,但发生在数据库 CAS 之前的内存清理阶段。#546 保证旧事件不能取消当前 generation;本 PR 补上“旧事件也不能清除当前 generation 的等待请求”这一层保护。

改动

  • TaskEngine::on_turn_complete 中复用已有的 retire_connection(conn_id, task_id),替代对 indexawaiting 和 delegation children 的三段直接清理。
  • retire_connection 会先移除结束连接的索引,并在同一 index 锁保护下确认该任务是否仍有其他连接:
    • 如果当前 generation 的连接仍在,就保留该任务的 awaiting 集合;
    • 只有最后一个连接退出时,才清理任务级等待集合;
    • delegation children 仍由同一 helper 清理,原有行为保持不变。
  • 新增回归测试 a_previous_generation_cancel_does_not_clear_current_waits,构造两个 generation 和两个当前等待请求,覆盖完整状态变化。

实现保持为单文件的最小修改,没有改变数据库 schema,也没有扩大 awaiting 的 key 结构。

修复结果

新增测试验证了以下行为:

  1. 当前 generation 注册两个等待请求后,任务进入 AwaitingInput
  2. 旧 generation 的 cancelled 完成事件不会取消当前 generation,也不会清空它的等待集合;
  3. 只解决第一个请求后,任务仍保持 AwaitingInput
  4. 解决最后一个请求后,任务才恢复为 Running

修复前,该回归测试稳定失败,实际状态为 Running、期望状态为 AwaitingInput;连续复现 5 次均失败。应用本修改后测试通过,因此本 PR 覆盖并解决了 #564 描述的状态损坏路径。

测试

以下验证均已完成:

  • 定向回归测试:

    cargo test --locked --no-default-features --lib \
      a_previous_generation_cancel_does_not_clear_current_waits -- --nocapture

    结果:1 passed; 0 failed

  • 服务器端 work_task 相关测试集:

    cargo test --locked --no-default-features --bin codeg-server --lib work_task

    结果:library target 143 passed; 0 failedcodeg-server binary target 0 tests

  • 服务器端严格 Clippy:

    cargo clippy --locked --no-default-features --bin codeg-server --lib -- -D warnings

    结果:通过。

  • 桌面 feature 定向回归测试:

    cargo test -j 1 --lib --features test-utils \
      a_previous_generation_cancel_does_not_clear_current_waits -- --nocapture

    结果:1 passed; 0 failed

  • git diff --check:通过。

补充说明:Rust 1.98.0 下运行全仓库 cargo fmt --all -- --check 会报告大量本 PR 之前已存在的格式差异,因此没有用它改写无关文件;本 PR 仍保持仅修改 src-tauri/src/work_task/engine.rs,且 diff 空白检查通过。

测试环境

  • Base commit:d6eb97e9da508266053911561f17deae81a2f582
  • Codeg:v0.28.1-6-gd6eb97e9
  • 主机:macOS 14.5(23F79),Apple Silicon arm64
  • Docker:29.7.2
  • Rust 镜像:reposteward-runner-rust:latest(arm64)
  • Rust:rustc 1.98.0
  • Cargo:cargo 1.98.0
  • 服务器验证:--no-default-features
  • 桌面验证:一次性容器内安装 GTK/WebKit/Tauri 系统依赖,并使用 -j 1 避免并发链接内存压力

请维护者审查这个连接感知的清理方案,尤其是 retire_connection 在旧、新 generation 连接并存时保留任务级等待集合的语义,以及现有 index -> awaiting 锁顺序是否符合预期。如需将 awaiting 进一步按 (task_id, run_seq) 分区,我也可以根据审查意见继续调整。

Fixes #564

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.

[Bug] 旧 generation 的 cancelled 事件会清空新 generation 的待处理请求

1 participant