📝 docs(spec): design async executor cancellation
This commit is contained in:
@@ -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、调用方和测试全部迁移完成。
|
||||
- 约定范围内构建与测试通过。
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user