跳转至

08 状态机与任务生命周期

难度:中级|前置:数据库、幂等|目标:把隐含状态变成可验证的规则

为什么需要状态机

如果代码中到处出现:

if status == ...

但没有统一定义哪些变化合法,系统很容易进入不可能状态。

状态机由三部分组成:

  • 状态;
  • 事件;
  • 合法迁移。

一个任务状态机

pending → running → succeeded
   │          ├──→ failed
   │          ├──→ waiting_approval → running
   │          └──→ cancelling → cancelled
   └──────────────→ cancelled

每次迁移应该回答:

  • 谁触发?
  • 前置状态是什么?
  • 是否允许重复?
  • 需要写哪些数据?
  • 是否产生外部副作用?
  • 失败如何恢复?

状态与步骤分开

长任务不能只保存一个 status=running。还应记录:

{
  "task_id": "t-123",
  "status": "running",
  "current_step": "analyze",
  "step_version": 4,
  "retry_count": 1
}

任务状态描述整体,步骤记录帮助恢复和排查。

乐观并发控制

两个 Worker 可能同时更新任务。可以使用版本号:

UPDATE tasks
SET status = 'running', version = version + 1
WHERE id = ?
  AND status = 'pending'
  AND version = ?;

受影响行数为 0,说明状态已被别人改变,当前操作不能继续。

状态迁移和副作用

数据库状态变更与外部设备命令、邮件、工单无法天然处于同一事务。常见做法:

  • 幂等 operation_id;
  • Outbox 事件;
  • 执行结果保存;
  • 可重试的补偿;
  • 人工处理终态。

不要设计一个万能 running

需要区分:

  • 等待调度;
  • 正在执行;
  • 等待重试;
  • 等待用户;
  • 等待外部回执;
  • 正在取消。

不同等待原因决定恢复策略。

自测

  1. 状态机与几个 if/else 的本质区别是什么?
  2. 为什么长任务只存 running 不够?
  3. 两个 Worker 同时领取任务如何防止重复?
  4. waiting_approval 为什么应该是显式状态?

完成标准

  • 能为 OTA 或 Agent 任务画状态机
  • 能写出每个迁移的前置条件
  • 能解释版本号的作用
  • 能区分等待重试和等待人工

下一单元:可观测性