Files
tsl-devkit/docs/superpowers/specs/2026-07-12-async-executor-cancellation-design.md
T

203 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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、调用方和测试全部迁移完成。
- 约定范围内构建与测试通过。