BLOGE
0.9.8-RC1在线版 · 事实校验 2026-09-15 · English
第 12 章 —— 等待世界的回应
本章承诺: 你会让一条 graph 在人工审批或 webhook 尚未到达时释放执行资源,读懂真实的挂起与恢复结果,并能识别错误关联键、重复事件和
0.9.8-RC1的已知语义边界。
学习目标
学完本章,你能够:
- 用业务语言解释阻塞、轮询和挂起的资源差异。
- 从
GraphResult判断 graph 是失败、完成还是仍在等待。 - 根据外 部回应的形状,在
wait与await之间做选择。 - 用一次单因素实验验证错误 correlation key 和重复事件的处理结果。
这一章只增加一个变量
前一章的循环仍发生在一次活跃执行里:取一页、处理一页、再取下一页。本章只增加一件事——下一步的前提暂时不在系统手里。
客服经理可能两小时后批准退款,支付平台可能十分钟后推送回调,仓库也可能明天才发送到货事件。graph 不能一直占着工作线程等世界回答,也不能每秒追问一次数据库“有结果了吗”。它需要记住自己等什么,然后把控制权交还给运行时。
先遇到问题:一张等待经理批准的工单
客户 C-17 说退款迟迟未到账。系统创建工单 T-2048、完成分类并通知经理,然后必须等待批准结果。规则是:经理批准后才发确认;48 小时没有回应则进入超时处置。
在看代码前,先预测三种实现会发生什么:
| 做法 | 等待期 间占用 | 外部系统压力 | 重启后的难点 |
|---|---|---|---|
| 阻塞线程 48 小时 | 一个执行线程和关联状态 | 无 | 进程消失,内存等待也消失 |
| 每 5 秒轮询一次 | 定时任务、连接和查询 | 高 | 需要恢复轮询进度和去重 |
| 挂起并等待信号 | 一条等待记录 | 低 | 需要持久化身份、状态和关联条件 |
第三种方案不是“睡得更聪明”。它改变了控制模型:执行先到达稳定边界并返回,外部回应稍后触发另一段执行。
先跑一次:看到真实的挂起边界
书稿自带的 probe 会直接加载远端 examples 中的 ticket-approval-wait.bloge,为文档型 Operator 提供最小替身,然后执行两遍:第一遍停在挂起边界,第二遍收到人工审批信号后继续。
如果本机还没有安装 0.9.8-RC1 依赖,先从固定的 BLOGE submodule 安装:
cd submodule/bloge
mvn -pl bloge-core,bloge-dsl -am -DskipTests install
回到书稿根目录后运行:
cd probes/ch11-waiting
mvn test
2026-09-14 在 BLOGE commit cc38fbe5 上的关键输出如下;随机 executionId 和完整 Maven 日志已省略:
boundary.isSuccess=true
boundary.isSuspended=true
boundary.suspendedNodes={waitApproval=wait}
boundary.waitApproval=SUSPENDED
boundary.sendConfirmation=CANCELLED
resume.isSuccess=true
resume.isSuspended=false
resume.waitApproval=COMPLETED
resume.sendConfirmation=COMPLETED
先只读前三行:
isSuccess=true表示此刻没有节点错误,不表示业务流程已经结束。isSuspended=true表示至少一个节点正在等待信号。suspendedNodes同时给出节点身份waitApproval和本次需要匹配的 suspend keywait。
sendConfirmation=CANCELLED 也不是业务拒绝。它表示本轮调度在挂起边界收束,依赖等待节点的下游尚未运行。第二遍信号到达后,等待节点和确认节点才变为 COMPLETED。
观察结论: “成功但挂起”是合法的中间结果。只检查
isSuccess()会把尚未完成的流程误报为完成。
Probe 源码在 WaitingForWorldProbeTest.java。它是书稿的可复核观察,不代替 BLOGE 自身的耐久性、并发或生产验收。
挂起到底保存了什么
把挂起想成餐厅的取餐牌。顾客不需要站在窗口占着服务员;餐厅只要记住“哪一单、等什么、结果送回哪里”。BLOGE 的对应关系是:
| 现实对象 | BLOGE 中的身份 | 作用 |
|---|---|---|
| 一笔流程 | executionId | 找回同一次 graph 执行 |
| 一个等待点 | nodeId | 找到恢复入口 |
| 取餐牌号码 | suspend/correlation key | 防止回应送错流程 |
| 已完成步骤 | checkpoint/status | 恢复时不重做已完成工作 |
| 外部回应 | signal payload | 成为等待节点的输出 |
这五样东西缺一不可。只有 payload 而没有执行身份,系统不知道唤醒谁;只有执行身份而没有等待节点,系统不知道从哪里继续;没有已完成状态,恢复可能重复执行已经发生的 effect。
两个入口:wait 与 await
它们共享“挂起后恢复”这一机制,但回答不同问题。
wait:时间或直接信号决定下一步
工单示例的核心差异只有这一段:
wait waitApproval = 48h after notifyManager {
signal_key = ctx.ticketId
on_timeout {
decision = "auto-approved"
approver = "system"
}
}
语义意图是:notifyManager 完成后挂起;人工处理可以直接向 waitApproval 发送结果,计时器到期也可以产生超时结果。适合明确的时间点、延迟或某个已知等待节点。
await:事件名称和业务键共同决定下一步
支付平台不会知道 BLOGE 的 executionId。它只会推送“订单 O-42 的支付已确认”。这时使用 payment-wait.bloge 中的事件关联:
await awaitPayment {
event "payment.confirmed" where orderId = createOrder.output.orderId
timeout = 15m
on_timeout { status = "timeout" }
}
Webhook handler 把外部协议翻译成运行时事件:
engine.publishEvent(
"payment.confirmed",
orderId,
Map.of("status", "confirmed", "transactionId", transactionId),
webhookMessageId
);
eventName 说明发生了什么,orderId 说明属于哪一笔业务,webhookMessageId 用于识别重复投递。不要把这三种身份压成一个字符串。
一张图串起 suspend、webhook、correlation 和 resume
读图时抓住两次控制权转移:
execute到达等待点后写入等待事实,并把“仍在等什么”返回给调用方。- Webhook 到达后,
publishEvent先按事件名和业务键查找等待记录;匹配成功才向原executionId + nodeId发送 signal,继续下游。
因此 webhook endpoint 不是“继续执行函数”的公开入口。它只负责认证外部消息、提取稳定业务键、提供幂等消息 ID,并调用事件发布入口。真正的恢复身份由关联记录给出。
原理:恢复不是从头再跑
收到 signal 后,运行时应满足三个不变量:
- 已完成节点保持已完成,不因恢复而无条件重做。
- Signal payload 成为等待节点的输出,下游通过普通依赖读取它。
- 同一等待条件只能完成一次;迟到或重复消息不能改写第一次已接受的结果。
在 probe 第二遍运行中,Map.of("decision", "approved", "approver", "manager-7") 被写成 waitApproval 的输出,因此 sendConfirmation 能继续读取 decision 和 approver。这就是“外部世界的回答重新进入 graph 数据流”的具体位置。
耐久化恢复还需要 execution checkpoint、wait store 和可重建的 graph definition。那是下一章的唯一新增变量;本章只建立进程内可观察的挂起协议。
单因素破坏一:把 correlation key 写错
保持事件名和 payload 不变,只把 orderId 从 O-42 改成 O-24:
var matched = engine.publishEvent(
"payment.confirmed", "O-24", payload, "msg-901");
预期观察不是异常,也不是恢复错误订单,而是:
matched=[]
awaitPayment=WAITING
downstream.started=false
没有匹配项时应保持原 correlation 为 WAITING。恢复动作是核对 webhook 中的业务键提取规则,再用一个新的消息 ID 重放正确事件;不要修改等待记录去迎合错误消息。
这个反例防住的是“串单”。在支付、物流和审批系统里,唤醒错误流程通常比晚几分钟更危险。
单因素破坏二:重复投递同一事件
这次 key 正确,但两次投递使用相同 webhookMessageId=msg-902。0.9.8-RC1 的 EventDeduplicationTest 验证:相同 idempotency key 不会再次应用部分事件,也不会产生第二次 signal。
2026-09-14 的聚焦测试结果:
EventDeduplicationTest Tests run: 5, Failures: 0
EventCorrelationAndOrTest Tests run: 3, Failures: 0
SuspendSignalTest Tests run: 1, Failures: 0
GraphResultContractTest Tests run: 4, Failures: 0
Total Tests run: 13, Failures: 0
重复事件不是罕见异常,而是 webhook 的正常交付特征。幂等消息 ID 应来自发送方的稳定消息身份;用接收时间或随机 UUID 现造一个 ID,等于关闭去重。