cisd/docs/plans/2026-03-10-init-execute-async-resume-design.md
2026-03-11 14:23:11 +08:00

212 lines
5.8 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.

# Init Execute Async Resume Design
**Date:** 2026-03-10
## Goal
解决初始化执行接口同步阻塞导致的请求超时问题,并支持任务在失败后从第一个未成功步骤继续执行,而不是每次都从头开始。
## Scope
本设计覆盖:
- `/api/v1/init/tasks/{taskId}/execute` 改为异步受理
- 初始化任务后台执行模型
- 步骤级断点续跑规则
- 同一任务的重复触发保护
- 现有查询接口与状态语义调整
本设计不覆盖:
- 分布式任务调度
- 多实例间执行协调
- 服务重启后的自动恢复调度
- 新增前端页面
- 初始化步骤编排本身的变更
## Current State
当前执行入口是同步的:
- 控制器直接调用 `InitService.executeTask(taskId)`
- 服务层在请求线程内串行执行全部步骤
- 任一步骤失败后立刻中断
当前问题有两个:
- 任务执行时间较长时HTTP 请求容易超时
- 再次执行时没有跳过已成功步骤的逻辑,会从第一步重新开始
当前已有可复用基础:
- 任务主表 `tms_init_task`
- 任务步骤表 `tms_init_task_step`
- 步骤状态字段:`PENDING` / `RUNNING` / `SUCCESS` / `FAILED`
- 现有步骤详情和日志查询接口
## Design Choice
采用:`应用内异步执行 + 基于步骤状态续跑`
不采用:`继续同步执行`
原因:
- 无法避免请求超时
- 与实际长耗时初始化场景不匹配
不采用:`数据库任务队列 + 独立 worker`
原因:
- 当前需求重点是先解决超时和重跑问题
- 现阶段引入完整调度系统成本过高
## API Design
### Execute API
`POST /api/v1/init/tasks/{taskId}/execute`
接口语义调整为:
- 只负责受理执行请求
- 不等待全部步骤完成
- 返回当前任务快照
返回字段继续复用 `InitTaskExecuteResponse`,但语义变为:
- `taskId`: 任务号
- `status`: 受理后的当前状态,通常为 `RUNNING`
- `totalSteps`: 总步骤数
- `successSteps`: 当前已成功步骤数
前端后续继续通过以下接口轮询:
- `GET /api/v1/init/tasks/{taskId}`
- `GET /api/v1/init/tasks/{taskId}/steps`
- `GET /api/v1/init/tasks/{taskId}/steps/{stepNo}/log`
## Execution Model
### 1. Request thread
请求线程只做以下事情:
- 校验任务存在且有步骤
- 检查是否已经在执行中
- 将任务状态置为 `RUNNING`
- 提交后台执行任务
- 立即返回
### 2. Background worker
后台线程负责真正串行执行步骤:
- 重新加载任务与步骤
- 识别续跑起点
- 逐步执行
- 每步结束后立刻落库状态、消息、日志路径、退出码
- 汇总任务最终状态
### 3. Single-task guard
同一 `taskId` 在一个应用实例内只允许一个执行线程:
- 若任务已在本实例执行中,再次调用 `execute` 不再重复启动
- 接口直接返回当前快照
实现上可使用进程内 `ConcurrentHashMap<String, Future<?>>` 或等价结构维护活动任务。
## Resume Rule
再次执行时采用以下规则:
1. 所有 `SUCCESS` 步骤直接跳过,不重跑
2. 从第一个非 `SUCCESS` 步骤开始继续
3. 后续步骤按原顺序执行
4. 若本次再次失败,当前失败步骤状态记为 `FAILED`,任务状态记为 `FAILED`
### RUNNING residue handling
如果上一次执行过程中服务异常退出,可能残留 `RUNNING` 步骤。
本设计约定:
- 新一轮执行开始前,将残留 `RUNNING` 步骤视为未完成
- 统一重置为 `FAILED`
- 续跑时从第一个非 `SUCCESS` 步骤开始
这样可以保留“上次停在这里”的事实,并避免把 `RUNNING` 误判为仍在健康执行。
## Status Semantics
### Task status
- `PENDING`: 任务已创建,尚未开始
- `RUNNING`: 已受理且后台执行中
- `SUCCESS`: 全部步骤成功
- `FAILED`: 某一步失败并已停止
### Step status
- `PENDING`: 从未执行
- `RUNNING`: 当前正在执行
- `SUCCESS`: 已成功,后续重试跳过
- `FAILED`: 最近一次执行失败,后续重试从此处继续
## Concurrency Policy
### Same task
同一任务重复点击执行:
- 若本实例检测到该任务已在执行,直接返回当前状态
- 不再启动第二个后台线程
### Different tasks
不同任务是否允许并发执行,第一版保持当前默认:
- 允许不同 `taskId` 并行受理
- 不额外加全局串行锁
原因:
- 当前需求只明确要求单任务避免重入
- 是否需要全局串行,取决于目标主机资源和脚本互斥关系,后续再单独设计
## Data Changes
本设计优先复用现有表结构,不新增表字段。
依赖现有字段即可表达:
- 任务当前状态
- 步骤当前状态
- 失败消息
- 日志路径
- 退出码
## Testing Strategy
需要补的测试重点:
1. 异步受理
- `execute` 调用应快速返回
- 后台线程继续推进步骤状态
2. 失败后续跑
- 首次执行在某步失败
- 再次执行时跳过已 `SUCCESS` 步骤
- 从失败步骤重新执行并继续后续步骤
3. 残留 `RUNNING`
- 构造上一次异常中断留下的 `RUNNING` 步骤
- 重试时应从该步骤继续
4. 单任务重复触发保护
- 同一个 `taskId` 在执行中再次调用
- 不应重复启动第二个执行线程
## Risks
1. 应用内异步执行依赖当前进程存活,若服务重启,运行中的任务不会自动恢复。
2. 若底层初始化脚本本身不具备完全幂等性,虽然步骤级跳过已成功步骤可以减少重复执行,但失败步骤重试仍可能触发环境冲突。
3. 当前只做单实例内去重;若后续部署多实例,需要补分布式锁或数据库抢占机制。
## Recommendation
先实现最小闭环:
- `execute` 异步受理
- 单任务进程内防重入
- 从第一个非 `SUCCESS` 步骤续跑
- 残留 `RUNNING` 视为失败点恢复
不在第一版加入:
- 分布式调度
- 自动恢复未完成任务
- 新状态枚举或额外任务队列表