本文为OpenClaw教程系列之任务模型入门,系统讲解任务生命周期关键节点、状态机设计方法与多任务执行顺序(DAG依赖),并给出取消/暂停/超时、重试与幂等、checkpoint断点续跑等可落地实践与示例,帮助你构建可观测、可维护的任务执行体系。
本章目标:把“任务”当成可控的产品
在 OpenClaw 的实战里,“任务(Task)”并不是一句抽象概念,而是一套可被调度、可被暂停/恢复、可被重试/回滚、可被观测(日志/指标/事件)的执行单元。你写的每个任务,最终都会经历一段明确的生命周期,并在一个状态机中流转;调度器再根据依赖关系和执行顺序把任务串成可运行的链路。
本章围绕 OpenClaw任务模型入门:任务生命周期、状态机与执行顺序 展开,目标是让你能:
- 看懂并设计一个任务的“出生—执行—结束”的全流程
- 用状态机把“成功/失败/取消/重试/超时”等分支收敛成可维护的逻辑
- 在多任务编排时,明确依赖关系与执行顺序,避免“并发写错/先后顺序颠倒/重复执行”等典型事故
说明:本文按 OpenClaw教程 系列背景编写,聚焦任务模型,不展开整套路线目录。
1. 任务生命周期:从创建到归档的关键节点
在 OpenClaw 的语境里,一个任务通常会经历以下阶段(不同项目实现可能会合并/拆分某些环节,但思想一致):
1.1 创建(Created)
触发方式常见有三种:
- 用户触发:例如点击“开始部署/开始分析”。
- 定时触发:例如每天 2:00 生成报表。
- 事件触发:例如收到消息队列事件后创建任务。
创建阶段的关键产物:
task_id:全局唯一payload/input:任务输入(参数、上下文)metadata:如创建人、来源系统、幂等键、优先级、期望超时时间
建议:创建阶段就写入“幂等字段”(比如 idempotency_key),否则后续重试/重复触发会难以治理。
1.2 入队/可调度(Queued / Schedulable)
任务创建后,不一定立刻运行。它可能被放入队列等待资源:
- 等待并发槽位(全局并发/租户并发/某类任务并发)
- 等待依赖任务完成(DAG 依赖)
- 等待外部条件(比如资源就绪、锁可用)
建议:把“等待”当成一种正式状态,而不是在运行线程里忙等(busy wait)。这能显著降低资源消耗并提升可观测性。
1.3 启动与初始化(Starting / Initializing)
调度器选中任务后进入启动阶段,通常会执行:
- 参数校验(避免运行到一半才发现参数错)
- 上下文装载(读取配置、凭据、工作目录)
- 申请锁/租约(确保同一资源不会被多个任务同时修改)
建议:初始化失败要尽可能早失败,并输出明确错误(参数缺失、权限不足、资源不存在),避免误判成“运行失败”。
1.4 运行(Running)
运行阶段是任务的主体逻辑。常见做法是把任务拆成多个“步骤(Step)”:
- Step A:拉取输入数据
- Step B:处理/转换
- Step C:落库/发布结果
这样做的价值:
- 可插入检查点(checkpoint),支持断点续跑
- 可针对单步做重试策略(例如网络请求可重试,写入落库不可盲目重试)
1.5 收尾(Finishing / Finalizing)
不管成功失败,都应该有收尾逻辑:
- 释放锁/租约
- 清理临时文件
- 上报指标与事件(耗时、失败原因、重试次数)
- 写入最终状态
建议:收尾逻辑要“尽力而为但不阻塞最终落盘”。例如清理失败不应导致任务状态永远卡在 Running。
1.6 结束(Succeeded / Failed / Canceled / TimedOut)
任务最终会落到一个终态(Terminal State)。终态一旦写入,通常不再改变(除非人为“重开任务”创建新的 run)。
建议:把“失败原因(error_code/error_message)”结构化存储,便于统计与告警。
2. 状态机设计:把复杂分支变成可推理的规则
2.1 为什么必须用状态机
没有状态机的任务系统常见问题:
- “执行到哪一步了”只能靠日志猜
- 重试时不知道该从头跑还是从中间跑
- 并发情况下重复执行,结果被覆盖
- 取消操作不可控:有时取消了仍在写数据
状态机的目标是:任何时刻都能回答这三个问题:
- 任务当前在哪个状态?
- 允许发生哪些状态迁移?
- 发生迁移时要执行哪些副作用(写库、发事件、释放资源)?
2.2 推荐的核心状态集合(可落地版本)
你可以从一个“足够用且易维护”的状态集合开始:
CREATED:已创建QUEUED:等待调度STARTING:启动中RUNNING:运行中PAUSED:暂停(可选)RETRY_WAIT:等待重试(可选)SUCCEEDED:成功(终态)FAILED:失败(终态)CANCELED:取消(终态)TIMED_OUT:超时(终态)
其中 PAUSED/RETRY_WAIT 不是必需,但对实战很有帮助:
- PAUSED:用于人工介入、等待外部确认、或限流暂停
- RETRY_WAIT:明确展示“我会再试一次,但不是现在”
2.3 状态迁移规则(建议用表驱动)
用表的方式定义迁移,比在代码里到处 if/else 更稳。
示例迁移(节选):
CREATED → QUEUED:创建后入队QUEUED → STARTING:被调度器选中STARTING → RUNNING:初始化完成RUNNING → SUCCEEDED:全部步骤成功RUNNING → FAILED:不可恢复错误RUNNING → RETRY_WAIT:可恢复错误且未超过重试上限RETRY_WAIT → QUEUED:到达重试时间,再次入队RUNNING → CANCELED:接收到取消信号并安全停止RUNNING → TIMED_OUT:超过截止时间(deadline)
关键约束:
- 终态不可再迁移:一旦
SUCCEEDED/FAILED/CANCELED/TIMED_OUT,只允许“新建一次运行”(新 task_run 或新 task_id),不要直接改回 RUNNING。 - 迁移要原子化:状态更新与关键副作用(例如写入开始时间、结束时间、错误码)要尽可能在同一事务或同一原子更新里完成。
- 重复事件要幂等:同一个“完成事件/取消事件”到两次,不应导致二次结算。
3. 执行顺序:单任务步骤与多任务依赖如何编排
任务模型里有两层“顺序”:
- 任务内顺序:一个 Task 里 Step1/Step2/Step3 的先后
- 任务间顺序:TaskA 完成后才能 TaskB,或 A/B 并行后再汇聚
3.1 任务内:用“步骤序号 + 检查点”保证可恢复
建议把任务拆为明确步骤,并记录 current_step 或 checkpoint:
step=FETCH:下载数据step=PROCESS:处理step=COMMIT:提交结果
落地建议:
- 每个 Step 开始前写入
current_step,结束后写入step_done=true或推进到下一个 step。 任何会产生外部副作用的步骤(例如写库、调用支付、发货)必须设计幂等:
- 使用业务幂等键(订单号、请求号)
- 或在本地记录“已提交”标记,避免重复提交
重试时策略明确:
FETCH/PROCESS通常可重试COMMIT若无幂等保证,禁止自动重试,改为人工介入或补偿流程
3.2 任务间:用 DAG 定义依赖,避免“隐式顺序”
多任务编排推荐用 DAG(有向无环图)表达:
- A → B:B 依赖 A
- A → C:C 依赖 A
- B、C → D:D 等 B/C 都完成
执行顺序的确定规则(通用做法):
- 入度为 0(无未完成依赖)的任务可进入
QUEUED - 调度器从可运行集合中按策略挑选(优先级/公平/资源约束)
- 某任务成功后,减少其下游任务入度;入度为 0 的下游任务变为可调度
建议:在存储层显式保存依赖关系和完成计数,而不是靠“轮询查询所有上游是否完成”。轮询在规模上去后会很痛。
4. 取消、暂停、超时:最容易写崩的三件事
4.1 取消(Cancel)要“可中断且可收尾”
取消并不等于立刻杀线程。更可靠的方式是:
- 写入取消信号(例如
cancel_requested=true) - 运行中的任务在“安全点”检查该信号
若可安全停止,则:
- 停止后续步骤
- 执行收尾
- 迁移到
CANCELED
安全点示例:
- 每个 Step 开始前
- 长循环处理每 N 条数据
- 外部调用前后
这样你能避免:取消后仍在写入数据库,造成数据不一致。
4.2 暂停(Pause)用于“人为或系统性限流”
暂停与取消的差别:暂停希望未来可以恢复继续跑。
建议做法:
PAUSED只发生在任务处于QUEUED或RUNNING且到达安全点- 恢复时从
PAUSED → QUEUED,由调度器重新拉起
如果你的任务内有 checkpoint,那么恢复时可以从上次 checkpoint 接着执行。
4.3 超时(Timeout)建议用 Deadline 而不是 Duration
- Duration:从开始算 30 分钟
- Deadline:最晚在某个时间点之前完成(例如基于创建时间 + SLA)
在重试存在时,deadline 更清晰:即使重试多次,也不能无限延期。
落地建议:
- 创建时写入
deadline_at - 调度前检查是否已过期:过期则直接
TIMED_OUT - 运行中也周期检查:超过 deadline 则请求停止,最终进入
TIMED_OUT
5. 重试与失败:把“可恢复失败”与“不可恢复失败”分开
一个成熟的任务系统不会把所有失败都当成一类。
5.1 失败分类建议
- 可恢复失败(Retryable):网络抖动、临时 503、锁冲突、依赖服务超时
- 不可恢复失败(Fatal):参数非法、权限不足、数据格式不支持、业务规则拒绝
5.2 重试策略(建议你直接照这个模板落地)
- 最大重试次数:
max_attempts(例如 3~5) - 退避策略:指数退避(1s/2s/4s/8s)+ 抖动(jitter)
- 可重试白名单:只对指定错误码重试
- 失败后进入
RETRY_WAIT,到点再回QUEUED
示例:
- attempt=1 失败(503)→
RETRY_WAIT,next_run=now+2s - attempt=2 失败(503)→
RETRY_WAIT,next_run=now+4s - attempt=3 失败(参数缺失)→ 直接
FAILED(不再重试)
5.3 失败后的“补偿”而不是“盲目重试”
当任务包含不可幂等的外部副作用时,失败处理更应该是补偿:
- 已创建资源但后续失败:需要删除资源
- 已写入部分数据:需要回滚或标记为无效
建议把补偿做成独立任务(Compensation Task),与主任务解耦,状态机更清晰。
6. 实战示例:一个“数据导入任务”的生命周期与顺序设计
假设你要实现:把用户上传的 CSV 导入数据库,并生成导入报告。
6.1 任务拆分
Task:
ImportCsvTask- Step1
VALIDATE:检查文件存在、表头合法、大小限制 - Step2
PARSE:解析 CSV,逐批转换 - Step3
UPSERT:按幂等键写入(例如user_id) - Step4
REPORT:生成报告并通知用户
- Step1
6.2 状态流转(一次成功的 run)
- CREATED(创建任务,记录 file_id、creator、deadline)
- QUEUED(等待 worker)
- STARTING(拉取文件、申请导入锁:同一个 file_id 只能导入一次)
RUNNING
- VALIDATE done
- PARSE done
- UPSERT done
- REPORT done
- SUCCEEDED(写入结束时间、导入条数、跳过条数、报告链接)
6.3 失败与重试(一次网络抖动的 run)
如果在 REPORT 里调用通知服务 503:
- RUNNING(REPORT 失败,错误码=NOTIFY_503,可重试)
- RETRY_WAIT(2s)
- QUEUED
- STARTING
- RUNNING(从 checkpoint=REPORT 继续,而不是重跑 UPSERT)
- SUCCEEDED
这就体现了:生命周期 + 状态机 + 执行顺序(含 checkpoint)组合在一起的价值。
7. 观测与排障:用状态机输出“人能读懂”的信息
任务系统最终要服务人(开发、运维、业务)。建议至少做到:
7.1 每次状态迁移都记录事件
记录字段建议:
task_id/run_idfrom_state/to_statetimestampreason_code(例如 CANCEL_REQUESTED、RETRYABLE_ERROR、DEADLINE_EXCEEDED)operator(系统/用户/调度器)
7.2 为每个 Step 打点
至少包括:
- step_name
- start_at / end_at
- input_size(可选)
- output_size(可选)
- error_code(可选)
这样当用户问“为什么导入这么慢”,你能立刻回答慢在 PARSE 还是 UPSERT。
8. 本章小结:你写任务时的检查清单
在 OpenClaw教程 的任务模型里,你可以用下面清单自查:
- 是否定义了清晰的生命周期节点(创建/入队/启动/运行/收尾/终态)?
- 是否有明确的状态机与迁移规则(终态不可逆、迁移原子、事件幂等)?
- 任务内是否拆分 Step,并记录 checkpoint,支持断点续跑?
- 多任务编排是否用显式依赖(DAG),避免隐式顺序?
- 是否设计了取消/暂停/超时的可控行为(安全点检查 + 必要收尾)?
- 重试是否只针对可恢复错误,并有退避与上限?
- 是否有足够的观测数据(状态迁移事件、step 耗时、错误码)?
做到这些,你的任务不但“能跑”,还会“好管、可扩展、可定位问题”。
Prev:OpenClaw项目结构拆解:目录约定、配置文件与资源管理