Skip to main content

BLOGE 0.9.8-RC1 在线版 · 事实校验 2026-09-15 · English

附录 D —— 状态机

第 15 章用叙事方式讲状态机,第 16 章讲它与 session 的组合。本附录是 查阅手册 —— 当你编写一台稍微复杂的状态机时,会反复回到这里 查看操作符表、配置项、监听器回调和 checkpoint 字段。


什么时候需要本附录

问题去这里
"怎么用具名状态建模生命周期?"第 15 章
"状态机和 session 怎么嵌套?"第 16 章
"when guard 里允许哪些运算符?"本附录 —— Guard 运算符目录
"checkpoint 写入时引擎持久化了什么?"本附录 —— Checkpoint 结构
"哪个回调能给我 state-enter / state-exit 的时机?"本附录 —— 监听器 SPI 速查
"这个流程到底该用 SM、session 还是普通 graph?"本附录 —— 选型表

如果还没读过第 15 章,先从那里开始。本附录默认你已经知道 [initial]on EVENT -> target[terminal] 是什么意思。


DSL 关键字一览

第 15 章用到的每一个关键字,一张表说清。关键字都写在 state machine 块里,大小写敏感

关键字位置作用
[initial]状态头标记一台机器恰好有一个初始状态,执行从这里开始。
[terminal]状态头标记终态可以有多个,进入任意一个就把状态置为 COMPLETED
on EVENT -> target状态内部等待一个 event 名匹配的信号,收到后迁移到 target
on EVENT when GUARD -> target状态内部同上,但只在 guard 真值时触发。多个 when 按声明顺序匹配。
on *状态内部自动迁移 —— 进入状态后立即触发,在执行器挂起之前。常用于必跑的子图扇出。
timeout = <duration>状态内部单状态超时,需要配合 on_timeout 声明落点。
on_timeout -> target状态内部单状态超时的迁移目标。
global_timeout = <duration>机器顶部整机截止时间。如果触发时没有匹配的 global_transition,状态机进入 FAILED
global_transitions { … }机器顶部对所有非终态生效的迁移。适合可以在任何状态到达的 CANCEL / ERROR 事件。
max_transitions = N机器顶部迁移次数上限,默认 100。超过就 FAILED
max_state_visits = N机器顶部单个状态最多可进入的次数,默认 10

Duration 语法 与 BLOGE 其他部分一致:30s5m2h24hPT15M (ISO-8601)。按 lint 允许的写法即可。


Guard 运算符目录

Guard 表达式由 bloge-dslExpressionEvaluator 解析,跟 DSL 里其他 when 子句、条件边用的是同一个求值器。

运算符

类别运算符说明
算术+ - * / %都是数值运算;+ 不会 拼接字符串,用字符串插值。
比较== != < <= > >=== / != 支持任意可比类型;数值比较遵循 Java widening。
逻辑&& || !短路求值。! 是唯一的逻辑一元。
一元-(取负)仅数值。
空合并??a ?? b:anull 时返回 a,否则返回 b
三元cond ? a : b右结合。
路径.[…]点号路径在上下文 map 里查;索引也适用于 list。

示例

# 与某节点输出做数值比较
on submit when ctx.scoreNode.output.score >= 80 -> approved

# 字符串比较
on classify when ctx.input.tier == "platinum" -> fastTrack

# 空合并 + 三元
on review when (ctx.audit.output.flags ?? []) == [] ? true : false -> noManualReview

# 复合逻辑
on dispatch when ctx.geo.region == "EU" && ctx.user.consent.marketing -> sendEmail

求值规则

  • 路径找不到 —— 没有命中的路径求值为 null。顶层为 null 的 guard 视为 ,迁移被跳过。
  • 类型不匹配 —— 数字和字符串比较结果是 false,不会 抛错。 Lint 在编译期能识别明显的不匹配。
  • 布尔化 —— nullfalse0"" 和空集合都是假;其余皆真。
  • 求值顺序 —— 同一个事件下有多个 on EVENT when … 时,从上到下 尝试,第一个真值的赢。最特化的 guard 写在前面。

配置项

配置项默认调高的场景调低的场景
max_transitions100自动迁移多,或者有受控的重试回路。业务本应该很短;失控循环说明缺终态。
max_state_visits10单状态合理地重入(例如重试预算),外层另有计数。状态最多进入一次,调低能更快失败。
global_timeout未设置业务没有自然 deadline,但你想要跨重启的强制过期。每个状态已有自己的 timeout,机器级再设反而会盖住状态级 bug。
状态 timeout未设置状态会无限等待外部事件,需要兜底。状态本就靠 on * 自动迁出,设置反而徒增噪音。

触发上限会发生什么: 执行器把状态置为 FAILED,触发 onStateMachineComplete 并带 FAILED 标记,然后写最后一次 checkpoint。 之后再来的信号会被拒绝。


失败与拒绝结果

状态机 API 使用类型化 exception,而不是一个通用错误码 enum。跨服务边界时请保留类型,不要解析 message。

结果类型化信号含义
Event 没有合法迁移UnhandledEventException当前 state 无法消费该 event
完成后或非法时点收到 signalStateMachineSignalRejectedException实例状态拒绝 signal
State graph 失败StateGraphExecutionException所选 state 内部工作失败
迁移或访问预算耗尽StateMachineTransitionLimitExceededException / StateVisitLimitExceededException安全上限终止失控机器
Deadline 到期StateMachineTimeoutExceededExceptionState 或全局 timeout 结束执行
持久定义变化StateMachineVersionMismatchExceptionCheckpoint hash 与当前定义不一致
Migration 非法StateMachineMigrationException声明的 state 映射无法应用

如果状态机使用通用 durable store,存储失败还会携带 DurableErrorCode;该 enum 描述 store 语义,不描述状态机业务迁移。


监听器 SPI 速查

StateMachineListener 是审计、指标、调试的统一钩子。所有方法都是 default 空实现 —— 只覆盖你需要的那几个。

回调触发时机典型用途
onStateMachineStart(StateMachineStartEvent)execute(def, ctx) 调用,初始状态进入前。审计开行、span 开启。
onStateEnter(StateEnterEvent)迁移落地后,该状态的图运行前。启动状态耗时计时器、MDC 注入。
onStateExit(StateExitEvent)该状态的图跑完后,选定的迁移触发前。关闭状态耗时计时器。
onTransition(TransitionEvent)选中了一条迁移 —— 涵盖 on EVENTon *on_timeout、global。迁移计数器、结构化日志。
onWaitingForEvent(WaitingForEventEvent)没有自动迁移命中,执行器挂起前。"等待信号" 计量、看板标识。
onSignalReceived(SignalReceivedEvent)机器挂起期间收到 signal(event, payload)信号速率计数、payload 审计。
onStateTimeout(StateTimeoutEvent)状态级 timeout 在事件到来之前触发。单状态超时指标。
onGlobalTimeout(GlobalTimeoutEvent)整机 global_timeout 触发。告警 / on-call 通知。
onCheckpointSaved(CheckpointSavedEvent)一次 checkpoint 已写入存储。复制位点、滞后探测。
onCheckpointRestored(CheckpointRestoredEvent)resumeFromCheckpoint() 把实例恢复出来。"崩溃后恢复" 日志。
onStateMachineComplete(StateMachineCompleteEvent)到达终态 机器进入 FAILED关闭审计、关闭 span。

通过 StateMachineExecutor.Builder.listeners(...) 注册,或在持久化路径 上用 DurableStateMachineManager.Builder.listeners(...)


Checkpoint 结构

StateMachineCheckpoint 就是持久化 SPI 存的那条记录。理解它的结构,在 排查部分恢复或者自己实现 ExecutionCheckpointStateMachineStore 时帮助 很大。

字段类型说明
instanceIdString稳定标识,由 execute(def, ctx, instanceId) 给出或自动生成。
stateMachineNameStringstate machine 块上声明的 name
currentStateIdString机器所在状态;已完成时是最后的终态。
statusStateMachineStatusRUNNING / WAITING_EVENT / COMPLETED / FAILED
totalTransitionsint每次迁移加 1,受 max_transitions 约束。
stateVisitCountMap<String, Integer>每个状态被进入的次数,受 max_state_visits 约束。
stateOutputsMap<String, Map<String, Object>>ctx.stateName.output.nodeId 的命名空间树。
sharedContextMap<String, Object>跨状态共享、由算子写入的上下文。
historyList<StateExecutionRecord>每个进入并退出的状态都有一条记录,旧的在前。
startedAtInstantexecute(...) 时的 wall clock。
lastTransitionAtInstant最近一次迁移的 wall clock。
checkpointedAtInstant最近一次写入的 wall clock。
stateTimeoutDeadlineInstant?当前状态有 timeout 时设置,否则 null
globalTimeoutDeadlineInstant?机器有 global_timeout 时设置。

两个 deadline 字段都会持久化,这样崩溃后恢复 loop 可以按 原始 wall clock 重新挂定时器,而不是 "now + timeout"。这是持久化状态机能跨重启 保持正确性的关键。

Diagram: appendix-d-state-machines figure 1


SM / Session / 普通 graph

写 DSL 之前用这张表挑选合适的原语。

问题是 → 用
工作负载是否会停留在一组有限的具名生命周期阶段、停留时长不固定、可能回路?状态机 (第 15 章)
工作负载是否是与外部反复来回的对话形态,每一轮处理逻辑相似?Session (第 14 章)
工作负载是否 两者兼有 —— 阶段里包含对话?组合 (第 16 章),按信号来源选外层。
依赖关系是否是一遍跑完的纯 DAG?普通 graph (从 第 2 章 开始)
是否有一个长时间运行的算子在增量产出?流式 (附录 C)

变化的是阶段 而不是数据形态时,状态机就是合适的工具。如果分支 是被工作项的 schema 驱动的,DAG 更简单。


真实示例

仓库里有验证过的 fixture 可以端到端阅读。

文件演示什么
order-lifecycle-state-machine.bloge4 个状态、有回退迁移、24h 全局超时。第 15 章的主线案例。
ticket-state-machine.bloge6 个状态、升级路径、多事件流。
order-session-with-state-machine.bloge模式 A —— session 在外层。
review-state-machine-with-session.bloge模式 B —— 状态机在外层(注意信号死锁约束)。

常见错误

错误为什么发生修复
一台机器里有两个 [initial]拷贝草稿时忘了清掉旧标记。编译器会拒绝。提交前先 bloge lint
重试回路触发了 max_transitions重试状态在自旋,没有退出条件。要么调低这个状态的 max_state_visits,要么加一条 on * when ctx.retry.output.count > 3 -> giveUp
signal(event, payload) 静默返回 "no transition matched"。事件名匹配了,但所有 when guard 求值为假。在所有带 guard 的变体后面加一条 on EVENT -> fallback,或在测试里用 MockOperator.invocations() 审计 guard 的路径。
模式 B 的内层 session 想等待外部信号。把内层 session 当成了外层。执行器抛 SM_NESTED_SESSION_AWAITS_SIGNAL。把 session 的外部触点移到外层机器(模式 A) —— 见第 16 章
状态 timeout 触发但缺 on_timeout加状态时忘了兜底。机器直接 FAILED。除非你就想要这个结果,否则 timeout 总要配 on_timeout
Checkpoint 恢复落到错的状态。自定义 ExecutionCheckpointStateMachineStore 返回了陈旧的行。该 SPI 的契约要求 instanceId 写后立即可读。先用 DurableStateMachineManager 默认的 MyBatis 存储作为基线对比。

与正文的对应

状态机的知识分散在多章,这份附录把它收拢:

  • 编写 —— 第 15 章 讲 DSL。
  • 组合 —— 第 16 章 讲两种嵌套模式和信号死锁规则。
  • 持久化 —— 第 13 章 讲存储 SPI;本附录 列出 checkpoint 字段。
  • 测试 —— 第 18 章 讲确定性时间控制 和快照断言。
  • 运维 —— 第 19 章 讲监听器 驱动的指标;本附录提供回调目录。