Files
storage-labos/flashops/docs/decisions/0003-own-state-machine-over-temporal.md
T
yuanshuai c91a64fddb
CI / Python 3.12 (push) Waiting to run
CI / Python 3.9 (push) Waiting to run
chore(repo): initialize team collaboration repository
2026-07-27 20:40:12 +08:00

2.5 KiB

ADR-0003:自研状态机,不用 Temporal / Airflow

  • 日期:2026-07-27
  • 状态:已接受(方案 §6.1 把 Temporal 列为演进建议,这里给出不在首版用的理由)

背景

工作流编排有成熟方案:Temporal、Airflow、Prefect、OpenTAP。 自研状态机通常是坏味道。

决定

首版自研:engine/states.py 转移表 + engine/state_machine.py 纯函数

  • engine/runtime.py 异步循环。

理由

产品的差异化恰好落在通用引擎不管的那一层:

  1. 恢复语义不是重试。 Temporal 的重试模型是"再调一次这个 activity"。 我们需要的是"主机蓝屏了 → 带外触发主板 Reset → 等主机起来 → 验证 DUT 重新枚举 → 从检查点续跑"。这是跨越进程、跨越主机生死的物理恢复, 不是函数重试。硬套通用引擎会变成在 activity 里塞一堆状态判断,比自研更难维护。

  2. 非幂等步骤默认不重跑。 通用引擎的默认值是"重试是安全的"。 我们这里最贵的一条规则是"刷写步骤挂了不要自动重跑"。 跟框架默认值对着干,不如自己控制。

  3. 冻结现场是一等状态。 数据完整性失败要立刻停止一切覆盖性动作并保留现场。 通用引擎里这是"失败",我们这里它是一个需要保持很久、等人来看的活状态。

  4. 可测试性。 纯函数状态机能把"30 秒心跳超时后带外在线则走 L3"这种规则 在毫秒内测完。挂在 Temporal 上要起测试环境。后面几波会疯狂往状态机加分支, 测试成本是决定性因素。

  5. 依赖成本:Temporal 需要自己的数据库和服务集群。客户是内网私有化部署, 多一个必须运维的组件就多一个交付摩擦。

代价

  • 跨节点调度、持久化定时器、版本化工作流这些 Temporal 白送的能力要自己做。 这一波不需要(单工位),Phase 3 多工位时会痛。
  • 自研状态机容易随时间腐化成 if 堆。缓解:转移表是纯数据、decide() 是纯函数、 每加一个分支必须配单测,三条规矩写进了 docs/state-machine.md §6

什么时候推翻

多工位跨节点调度成为瓶颈时(Phase 3,预计 20+ 工位)。 届时替换范围仅限 runtime.py——states.py / state_machine.py / recovery.py 是纯逻辑,可以原样搬进 Temporal 的 workflow 定义里。 这也是把它们做成纯函数的第二个理由。