BLOGE
0.9.9-RC1在线版 · 编制 2026-11-02 · English
第 15 章 —— 状态机
承诺: 读完本章后,你将知道什么时候一个工作流已经超出了单张 DAG 的表达能力,需要用
state_machine来描述;你也会学会如何建模事件驱动迁移、超时和守卫条件,并让生命周期状态显式可见,而不是藏在一堆分支和标志位里。
学习目标
- 识别什么时候问题更适合用命名状态和迁移来表达,而不是继续堆分支 graph。
- 定义
initial、普通和terminal状态,并把状态内部的本地工作放进嵌入式 graph。 - 分清事件迁移、守卫迁移、自动迁移和超时迁移各自的职责。
- 在上线前就设置好
max_transitions和max_state_visits这类安全上限,避免坏循环变成生产事故。 - 知道什么时候该用状态机,什么时候 graph 或
session仍然更简单。
前置条件
- 第 5 章——做决策的分支 —— 状态迁移解决的问题和 graph 分支不同
- 第 12 章 —— 等待世界的回应 —— 事件驱动迁移仍然依赖同样的外部信号模型
- 第 13 章 —— 持久执行 —— 状态机的检查点与超时也站在持久执行基础上
- 第 14 章 —— 多轮 Session —— 有助于对比“对话轮次”和“生命周期状态”
源示例
| 文件 | 展示内容 |
|---|---|
ch14/order-lifecycle-state-machine.bloge | draft → review → processing → completed 的订单生命周期,并带超时回退 |
ch14/ticket-state-machine.bloge | 带 assign / escalate / resolve / close 路径的客服工单生命周期 |
OrderLifecycleStateMachineExample.java | 使用 StateMachineBuilder 的 Java Fluent API 版本 |
bloge-state-ext/README.md | 状态机运行时模型、超时语义和嵌套 session 说明 |
ReviewStateMachineWithSessionExample.java | 一个“状态机内嵌 session”的预告,下一章会展开 |
为什么这很重要
Graph 最擅长回答的问题是:在当前依赖关系下,什么现在可以执行?
状态机擅长回答的是另一个问题:这个业务对象现在处于什么状态?哪个事件会把它推进到下一个状态?
当流程存在回退、反复进入某个状态,或者必须让非实现人员也能一眼读懂生命周期时,这个区别就会变得非常关键。
例如:
- 一个订单可以从
draft进入pendingReview,又因为拒绝回到draft - 一张工单可以从
open进入triaging,再决定去assigned或escalated - 一个暂停中的 review 状态可以因为超时自动回退到前一个状态
这些规则也可以用分支、标志位和循环编码在一张 graph 中,但生命周期会隐含在 node 名称和条件表达式里。state_machine 则直接表示命名状态和状态迁移。
心智模型
状态机本质上是一个围绕每个状态局部工作而建立的事件驱动外壳。
| 组成部分 | 责任 |
|---|---|
StateMachineDef | 整个状态机的不可变定义 |
state | 一个命名的生命周期步骤,例如 draft、review、processing |
嵌入式 graph | 在该状态内部要执行的本地工作 |
| 迁移 | 在事件、超时或自动条件满足时前往另一个状态的规则 |
StateMachineExecutor | 启动状态机并递送外部事件 |
StateMachineCheckpoint | 可序列化快照,用于恢复与持久化 |
四种迁移样式最重要:
| 迁移样式 | 语法 | 适合什么 |
|---|---|---|
| 事件 迁移 | on approve -> processing | 外部事件应该推动生命周期前进 |
| 守卫迁移 | on * when ... -> approved | 当前状态输出本身决定接下来去哪 |
| 自动迁移 | on * -> completed | 当前状态完成后应立即前往下一个状态 |
| 超时迁移 | timeout = 24h + on_timeout -> draft | 长时间无响应时需要自动回退、关闭或失败转移 |
状态机负责让生命周期状态可见;状态内部的 graph 负责完成该状态下的局部工作。
第一个可运行的示例
orderLifecycle 是最适合入门的第一个状态机:
这 44 行 definition 保留完整,因为读者需要把全部 state、event、timeout 和终止 迁移看成同一个生命周期;拆成孤立片段会重新制造本章要消除的 ownership 歧义。
state_machine orderLifecycle {
max_transitions = 25
max_state_visits = 5
timeout = 72h
state draft [initial] {
graph {
node initOrder : InitOrderOperator {
input {
orderId = ctx.orderId
customerId = ctx.customerId
}
}
}
on submit -> pendingReview
}
state pendingReview {
graph {
node reviewOrder : ReviewOrderOperator {
input {
orderId = ctx.draft.output.initOrder.orderId
}
}
}
on approve -> processing
on reject -> draft
timeout = 24h
on_timeout -> draft
}
state processing {
graph {
node fulfillOrder : FulfillmentOperator {
input {
orderId = ctx.pendingReview.output.reviewOrder.orderId
}
}
}
on * -> completed
}
state completed [terminal] { }
}
用生命周期的方式读它,而不是用 DAG 的方式读它:
- 从
draft开始 - 等待
submit - 迁移到
pendingReview - 再根据
approve、reject或超时决定去向 processing状态内部工作结束后,自动迁移到completed
共享 definition 不是正在运行的订单
orderLifecycle 定义每笔订单可以做什么。order-42 是一个当前停在
pendingReview 的 instance;另一实例可以同时停在 processing。混淆这两
个概念,是状态泄漏最直接的来源。
把一个 instance 读成被接受的事件序列
| 时间 | 事件 | 接受后的 instance 事实 |
|---|---|---|
t0 | create | currentState=draft,draft visits = 1 |
t1 | submit | currentState=pendingReview,review graph output 已保留 |
t2 | approve | currentState=processing,transition count = 2 |
t3 | wildcard completion | currentState=completed,terminal = true |
definition 包含 state id、graph、guard、timeout 与允许的 transition。instance 包含 current state、visit/transition count、state-local output、已接受事件历史 和 checkpoint identity。definition 可以缓存共享;instance 必须按订单隔离。
修改 definition 后,要追问旧 instance 怎么办
在 definition 中增加 manualReview,不会让 order-42 自动移动过去。必须有
事件选中允许的 transition;durable restore 还必须判断旧 checkpoint 是否与
新 definition 兼容。因此,definition versioning 是迁移决策,不是给 live state
直接赋值。
拆解分析
状态负责局部工作,迁移负责生命周期规则
尽量保持下面的职责边界:
graph { ... }只描述当前状态内部要做什么on ... -> target只描述什么时候切换到下一个状态
一旦这两个职责混在一起,定义就会迅速变得难懂。一个简单判断标准是:
- 如果它是在描述一次进入状态后,数据如何依赖与流动,就放进状态内部 graph
- 如果它是在描述什么时候改变生命周期状态,就放进迁移规则
事件迁移、自动迁移、守卫迁移和超时迁移
state_machine 提供了不止一种前进方式:
- 事件迁移:
on approve -> processing - 自动迁移:
on * -> completed - 守卫迁移:
on * when ctx.review.output... -> approved - 超时迁移:
timeout = 24h与on_timeout -> draft
状态级超时底层会通过一个合成的超时事件来实现。若执行器使用了专门的 TimerService,等待中的实例可以异步过期;如果没有,超时仍然会在下一次 execute(...) 或 signal(...) 时被强制检查。
输出是按状态命名空间组织的
状态输出不是被全局摊平,而是挂在状态 ID 下。
这就是为什么订单示例里要这样写:
orderId = ctx.draft.output.initOrder.orderId
而不是:
orderId = ctx.initOrder.output.orderId