212 lines
5.8 KiB
Markdown
212 lines
5.8 KiB
Markdown
# 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` 视为失败点恢复
|
||
|
||
不在第一版加入:
|
||
- 分布式调度
|
||
- 自动恢复未完成任务
|
||
- 新状态枚举或额外任务队列表
|