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 其他部分一致:
30s、5m、2h、24h、PT15M(ISO-8601)。按 lint 允许的写法即可。
Guard 运算符目录
Guard 表达式由 bloge-dsl 的 ExpressionEvaluator 解析,跟 DSL 里其他
when 子句、条件边用的是同一个求值器。
运算符
| 类别 | 运算符 | 说明 |
|---|---|---|
| 算术 | + - * / % | 都是数值运算;+ 不会 拼接字符串,用字符串插值。 |
| 比较 | == != < <= > >= | == / != 支持任意可比类型;数值比较遵循 Java widening。 |
| 逻辑 | && || ! | 短路求值。! 是唯一的逻辑一元。 |
| 一元 | -(取负) | 仅数值。 |
| 空合并 | ?? | a ?? b:a 非 null 时返回 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 在编译期能识别明显的不匹配。 - 布尔化 ——
null、false、0、""和空集合都是假;其余皆真。 - 求值顺序 —— 同一个事件下有多个
on EVENT when …时,从上到下 尝试,第一个真值的赢。最特化的 guard 写在前面。
配置项
| 配置项 | 默认 | 调高的场景 | 调低的场景 |
|---|---|---|---|
max_transitions | 100 | 自动迁移多,或者有受控的重试回路。 | 业务本应该很短;失控循环说明缺终态。 |
max_state_visits | 10 | 单状态合理地重入(例如重试预算),外层另有计数。 | 状态最多进入一次,调低能更快失败。 |
global_timeout | 未设置 | 业务没有自然 deadline,但你想要跨重启的强制过期。 | 每个状态已有自己的 timeout,机器级再设反而会盖住状态级 bug。 |
状态 timeout | 未设置 | 状态会无限等待外部事件,需要兜底。 | 状态本就靠 on * 自动迁出,设置反而徒增噪音。 |
触发上限会发生什么: 执行器把状态置为
FAILED,触发onStateMachineComplete并带FAILED标记,然后写最后一次 checkpoint。 之后再来的信号会被拒绝。
失败与拒绝结果
状态机 API 使用类型化 exception,而不是一个通用错误码 enum。跨服务边界时请保留类型,不要解析 message。
| 结果 | 类型化信号 | 含义 |
|---|---|---|
| Event 没有合法迁移 | UnhandledEventException | 当前 state 无法消费该 event |
| 完成后或非法时点收到 signal | StateMachineSignalRejectedException | 实例状态拒绝 signal |
| State graph 失败 | StateGraphExecutionException | 所选 state 内部工作失败 |
| 迁移或访问预算耗尽 | StateMachineTransitionLimitExceededException / StateVisitLimitExceededException | 安全上限终止失控机器 |
| Deadline 到期 | StateMachineTimeoutExceededException | State 或全局 timeout 结束执行 |
| 持久定义变化 | StateMachineVersionMismatchException | Checkpoint 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 EVENT、on *、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 时帮助
很大。
| 字段 | 类型 | 说明 |
|---|---|---|
instanceId | String | 稳定标识,由 execute(def, ctx, instanceId) 给出或自动生成。 |
stateMachineName | String | state machine 块上声明的 name。 |
currentStateId | String | 机器所在状态;已完成时是最后的终态。 |
status | StateMachineStatus | RUNNING / WAITING_EVENT / COMPLETED / FAILED。 |
totalTransitions | int | 每次迁移加 1,受 max_transitions 约束。 |
stateVisitCount | Map<String, Integer> | 每个状态被进入的次数,受 max_state_visits 约束。 |
stateOutputs | Map<String, Map<String, Object>> | ctx.stateName.output.nodeId 的命名空间树。 |
sharedContext | Map<String, Object> | 跨状态共享、由算子写入的上 下文。 |
history | List<StateExecutionRecord> | 每个进入并退出的状态都有一条记录,旧的在前。 |
startedAt | Instant | execute(...) 时的 wall clock。 |
lastTransitionAt | Instant | 最近一次迁移的 wall clock。 |
checkpointedAt | Instant | 最近一次写入的 wall clock。 |
stateTimeoutDeadline | Instant? | 当前状态有 timeout 时设置,否则 null。 |
globalTimeoutDeadline | Instant? | 机器有 global_timeout 时设置。 |
两个 deadline 字段都会持久化,这样崩溃后恢复 loop 可以按 原始 wall clock 重新挂定时器,而不是 "now + timeout"。这是持久化状态机能跨重启 保持正确性的关键。
SM / Session / 普通 graph
写 DSL 之前用这张表挑选合适的原语。
| 问题 | 是 → 用 |
|---|---|
| 工作负载是否会停留在一组有限的具名生命周期阶段、停留时长不固定、可能回路? | 状态机 (第 15 章) |
| 工作负载是否是与外部反复来回的对话形态,每一轮处理逻辑相似? | Session (第 14 章) |
| 工作负载是否 两者兼有 —— 阶段里包含对话? | 组合 (第 16 章),按信号来源选外层。 |
| 依赖关系是否是一遍跑完的纯 DAG? | 普通 graph (从 第 2 章 开始) |
| 是否有一个长时间运行的算子在增量产出? | 流式 (附录 C) |
当 变化的是阶段 而不是数据形态时,状态机就是合适的工具。如果分支 是被工作项的 schema 驱动的,DAG 更简单。