# 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 定义里。 这也是把它们做成纯函数的第二个理由。