执行模型
Flow 把一次工作流运行保存为追加式事件流。进程内的调用栈、局部变量和异步任务都不属于持久状态。Worker 每次接手运行时,从历史重建快照,再询问运行时代码下一步要做什么。
这套模型的直接结果是,运行可以在任意一次事件提交后换到另一台机器。恢复依赖历史和兼容的运行时代码,不依赖原进程留下的内存。
一次重放循环
每个循环分为四个阶段。
- 读取某个运行的全部已提交事件,投影出
WorkflowRunSnapshot。 - 调用
FlowRuntime::run_workflow(),传入定义、输入和历史。 - 校验返回的一个
RuntimeCommand,并按预期序号追加对应事件。 - 继续重放,或停在持久等待,或形成终态。
运行时代码每次只返回一个决定。批量步骤和批量子工作流仍然是一个原子命令,整批身份先通过校验,之后才开始推进成员。
快照是投影,不是第二份真相
WorkflowRunSnapshot 汇总当前状态、步骤、等待、Hook、信号、子工作流、进度和终态结果。它可以用于接口返回和运维查询,但不能覆盖历史。
存储实现必须按序返回完整事件流。只保存最新快照会丢掉重放、审计和并发判定需要的信息。
确定性具体约束在哪里
Flow 不要求工作流函数成为纯数学函数,但要求它对同一份历史发出同一个持久决定。以下字段会在重放时逐项比对。
- 步骤 ID、步骤名、输入和重试策略
- 等待 ID 与 UTC 截止时间
- Hook ID、令牌和元数据
- 信号等待 ID 与信号名
- 子工作流 ID、定义、输入和取消策略
- 续段输入、补丁标记和运行版本身份
已经安排过的身份再次出现但参数不同,会得到非确定性错误。运行时不会静默采用新值。
在 schedule_steps 中,如果一个兄弟步骤耗尽重试并要求终止运行,Flow 会先为
所有尚未落定的兄弟持久化 step_cancelled,再写入运行终态。取消原因可能说明
外部副作用结果未知;主机在再次重试前必须用幂等键完成对账。
工作流决定过程中不要读取当前时间、随机数、环境变量、网络响应或可变全局状态。需要这些值时,把读取动作放进步骤,再从持久步骤输出中取回。
步骤的物理交付是至少一次
步骤输出只有在 StepCompleted 提交后才可见。进程可能在外部调用成功之后、事件提交之前退出。替代 Worker 看不到成功记录,会再次执行同一个尝试。
因此每个产生外部副作用的步骤都需要业务幂等键。一个常用组合是运行 ID、步骤 ID 和目标资源 ID。
Flow 保证已提交的成功步骤不会在重放时再次调用。它无法替外部数据库或支付接口完成跨系统原子提交。
并发写入怎样收敛
事件存储通过 append_if_sequence() 实现乐观并发。Worker 读取到序号 7 后,只能以期望序号 7 追加下一批事件。若另一个 Worker 已经提交序号 8,过期写入会返回 EventConflict。
引擎遇到可恢复的序号竞争时重新读取历史并重放。业务代码无需锁住整条运行,存储也不会用后到的事件覆盖先到的决定。
暂停不会占用 Worker
以下状态会释放当前计算资源。
- 等待未来 UTC 时间
- 等待延迟重试到期
- 等待命名信号
- 等待 Hook 回调
- 等待第一类子工作流结束
定时等待和重试由 FlowScheduler 扫描持久索引,再派发精确到运行的恢复任务。信号与 Hook 由外部入口主动派发。暂停期间不需要保留线程、Future 或进程。
运行状态与终态
常见活动状态包括 Pending、Running、Suspended 和 Cancelling。终态结果使用 WorkflowTerminalOutcome 表达。
普通进程退出不应制造终态。替代 Worker 可以继续非终态运行。
长历史和代码发布
长循环使用 continue_as_new() 关闭当前事件流,并以新输入创建后继流。后继运行继承完整 WorkflowSpec,不能借续段切换代码版本或补丁标记。
滚动发布时,runtime_build_id 把运行固定到能够重放它的代码。补丁标记把一次代码分支选择写进新运行定义。两项机制都在第一次决定之前建立权威,避免新旧 Worker 对同一历史作出不同解释。
设计时的自查
面对一段工作流代码,可以逐项确认。
- 这个值在重放时是否可能变化。
- 变化的值是否已经来自步骤、信号或 Hook 的持久输出。
- 每个决定是否有稳定且只表达一个含义的 ID。
- 外部副作用重复执行时,目标系统是否能够去重。
- 运行等待时是否能完全释放当前进程。
只要其中一项答案不清楚,就先画出对应事件和恢复点,再写运行时代码。
