📝 docs(spec): design async executor cancellation

This commit is contained in:
csh
2026-07-12 19:48:10 +08:00
parent a042bf2efc
commit 627613f586
2 changed files with 204 additions and 2 deletions
@@ -0,0 +1,202 @@
# AsyncExecutor 协作式取消设计
## 背景
`lsp.scheduler.async_executor` 当前通过 Taskflow 执行后台任务,但任务句柄只
弱引用执行状态,快速任务完成后句柄立即失效;句柄取消按任务 ID 回查,可能
误取消同 ID 的替代任务;排队任务即使已取消仍会执行。任务注册表同时承担
“当前 ID”与“全部活跃任务”两种职责,导致等待、统计和重复 ID 语义不稳定。
本设计不保留现有 API 兼容层,直接建立明确的任务生命周期、结果和协作式
取消契约。
## 目标
- 任务句柄在任务完成后仍可等待并读取最终结果。
- 句柄取消只影响该句柄对应的任务实例。
- 按 ID 取消只影响该 ID 当前注册的任务实例。
- 已取消的排队任务不执行任务体。
- 运行中的长任务通过 `std::stop_token` 协作退出。
- 重复 ID 的全部活跃任务被准确等待和计数。
- 任务成功、取消和失败使用单一结果模型表达。
- 隐藏执行状态、同步原语和句柄构造细节。
- API namespace 与 Module 名保持一致。
## 非目标
- 不强制终止正在运行的线程。
- 不为旧的 `TaskClosure`、callback 或 `WaitForTask` 提供兼容重载。
- 不在本任务中启用 LSP `$/cancelRequest` 的请求级异步分发;本设计只保证
scheduler 及其后台索引任务具备正确取消能力。
- 不修改 Taskflow 本身。
## 公共 API
Module 名保持:
```cpp
export module lsp.scheduler.async_executor;
```
公共 API 迁移到:
```cpp
namespace lsp::scheduler::async_executor
```
任务结果统一为:
```cpp
enum class TaskStatus
{
kCompleted,
kCancelled,
kFailed,
};
struct TaskResult
{
TaskStatus status;
std::optional<std::string> value;
std::exception_ptr error;
};
```
任务 closure 只产生业务值,最终状态由 executor 根据停止请求和异常统一
生成,避免任务体自行构造相互矛盾的状态:
```cpp
using TaskClosure =
std::function<std::optional<std::string>(std::stop_token)>;
using TaskCallback = std::function<void(const TaskResult&)>;
```
`TaskHandle` 对外提供:
```cpp
bool Valid() const;
bool Cancel() const;
std::optional<TaskResult> Wait() const;
std::optional<TaskResult> TryGetResult() const;
```
- 默认构造句柄无效,`Wait``TryGetResult` 返回 `std::nullopt`
- 有效句柄完成后仍保持有效。
- `Cancel` 仅在首次成功请求取消且任务尚未完成时返回 `true`
- `Wait` 等待任务体和 callback 都结束,并返回最终任务结果。
- `TryGetResult` 不阻塞;尚未完成时返回 `std::nullopt`
- 构造函数为私有,仅 `AsyncExecutor` 可以创建有效句柄。
删除 `AsyncExecutor::WaitForTask`。调用方必须保存 `TaskHandle` 来等待特定
任务;按字符串 ID 只用于取消当前实例,不承担历史结果查询。
## 内部状态与所有权
`TaskHandle` 通过 `std::shared_ptr` 强持有私有嵌套状态。状态至少包含:
- `std::stop_source`:生成 stop token 并接受取消请求。
- `TaskPhase``kPending``kRunning``kCompleted`
- `std::optional<TaskResult>`:最终结果。
- mutex、condition variable 和 callback 完成标记。
- task ID 与开始时间。
状态定义不导出,外部不能构造或修改同步字段。
`AsyncExecutor` 使用两份索引:
- `current_tasks_``task_id -> state`,只表示该 ID 当前实例,供
`Cancel(id)` 使用。
- `active_tasks_`:以任务实例唯一标识保存全部未完成状态,供
`WaitAll``GetRunningTaskCount` 与统计使用。
提交同 ID 新任务时,请求旧实例停止,然后替换 `current_tasks_`;旧实例仍
留在 `active_tasks_`,直到任务体和 callback 都完成。
## 取消与执行流程
### 排队取消
worker 获取任务状态后,在持锁状态下检查 stop token:
- 已请求停止:不调用 closure,直接产生 `kCancelled`
- 未请求停止:状态从 `kPending` 转为 `kRunning`,随后调用 closure。
因此取消和开始执行之间只有一个明确的同步竞态点;取消先获得状态锁时,
任务体保证不会执行。
### 运行中取消
任务开始后,`Cancel` 调用 `stop_source.request_stop()`。executor 不强制停止
线程;closure 接收 `std::stop_token`,在文件枚举、索引和符号加载循环的
安全边界调用 `stop_requested()` 并尽快返回。
如果运行中收到停止请求,即使 closure 已产生普通值,executor 仍将最终
状态归类为 `kCancelled`,避免向 callback 报告成功。
### 完成与 callback
executor 捕获任务异常并生成 `kFailed`,保留原始 `exception_ptr`。任务结果
先写入状态,再调用 callback;callback 异常被捕获并记录,但不改写已经确定
的任务结果。只有 callback 返回或异常被处理后,状态才标记为完全完成、从
两个注册表注销并唤醒等待者。
callback 不得从自身调用 `Wait``WaitAll`;该约束写入接口注释和测试命名,
避免自等待死锁。
## 并发数与生命周期
传入并发数 0 或 `std::thread::hardware_concurrency()` 返回 0 时,实际 worker
数收敛到 1,再构造 `tf::Executor`
`AsyncExecutor` 析构时调用 `WaitAll``WaitAll` 等待所有活跃状态完成,并再
调用 Taskflow `wait_for_all()`,确保 callback 中提交的后续任务也已退出。
析构期间不允许其他线程继续调用 `Submit`;这是对象生命周期的基本前置条件。
## 调用方迁移
所有 `scheduler::AsyncExecutor``scheduler::TaskHandle` 等引用迁移到
`scheduler::async_executor::*`。所有 `Submit` closure 增加
`std::stop_token` 参数,callback 改为接收 `const TaskResult&`
工作区加载、系统库加载、文件索引和 workspace folder 变更任务将 stop token
传入长循环;循环在每个文件或目录边界检查停止请求。只执行一次且不可拆分的
第三方调用在调用前后检查 token,不尝试中断其内部线程。
## 清理
- 删除公开的 `detail::ExecutionState``detail::ActiveEntry`
-`ExecutorMetrics` 作为明确公共结果类型保留在
`lsp::scheduler::async_executor`
- 删除未使用的 `ActiveEntry::callback``ActiveEntry::start_time`
`kStatusLogInterval`
- 测试 Module 的入口同步放入自身命名空间,不再导出全局 `Run`
## 测试设计
测试遵循 TDD,先在旧实现上观察失败,再实现新语义:
- 无延迟任务完成后,handle 仍有效且可读取结果。
- `Wait` 返回任务体与 callback 完成后的最终结果。
- 同 ID 替换后,旧 handle 取消不影响新任务。
- 排队任务被取消后,任务体从未执行。
- 运行中任务观察 stop token 并协作退出。
- 同 ID 两个活跃实例被计数为 2`WaitAll` 等待两者。
- 并发数 0 可以正常执行任务。
- task 异常生成 `kFailed` 并保留异常。
- callback 异常不会导致等待死锁,也不会改写任务结果。
- 成功、取消、失败统计与最终状态一致。
迁移完成后运行 scheduler 测试、provider 测试和生产服务器构建;LSP 传输
冒烟测试确认后台初始化和 workspace 操作没有回归。
## 完成条件
- 快速完成任务的 handle 不失效。
- 任一 handle 只能取消自己的状态。
- 已取消的排队 closure 不执行。
- 长任务可以通过 stop token 提前退出。
- `GetRunningTaskCount``WaitAll` 覆盖重复 ID 的所有实例。
- 不存在 `WaitForTask`、公开执行状态或未使用调度字段。
- 并发数始终至少为 1。
- namespace、调用方和测试全部迁移完成。
- 约定范围内构建与测试通过。
+2 -2
View File
@@ -52,8 +52,8 @@
## Workflow State
<!-- workflow-state:start -->
phase: done
spec: docs/superpowers/specs/2026-07-12-text-coordinates-utf16-design.md
phase: planning
spec: docs/superpowers/specs/2026-07-12-async-executor-cancellation-design.md
plan: docs/superpowers/plans/2026-07-12-text-coordinates-utf16.md
executor: executing-plans
constraints: karpathy-guidelines,.agents,AGENT_RULES