Skip to main content

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

第 12 章 —— 等待世界的回应

本章承诺: 你会让一条 graph 在人工审批或 webhook 尚未到达时释放执行资源,读懂真实的挂起与恢复结果,并能识别错误关联键、重复事件和 0.9.8-RC1 的已知语义边界。

学习目标

学完本章,你能够:

  1. 用业务语言解释阻塞、轮询和挂起的资源差异。
  2. GraphResult 判断 graph 是失败、完成还是仍在等待。
  3. 根据外部回应的形状,在 waitawait 之间做选择。
  4. 用一次单因素实验验证错误 correlation key 和重复事件的处理结果。

这一章只增加一个变量

前一章的循环仍发生在一次活跃执行里:取一页、处理一页、再取下一页。本章只增加一件事——下一步的前提暂时不在系统手里

客服经理可能两小时后批准退款,支付平台可能十分钟后推送回调,仓库也可能明天才发送到货事件。graph 不能一直占着工作线程等世界回答,也不能每秒追问一次数据库“有结果了吗”。它需要记住自己等什么,然后把控制权交还给运行时。

先遇到问题:一张等待经理批准的工单

客户 C-17 说退款迟迟未到账。系统创建工单 T-2048、完成分类并通知经理,然后必须等待批准结果。规则是:经理批准后才发确认;48 小时没有回应则进入超时处置。

在看代码前,先预测三种实现会发生什么:

做法等待期间占用外部系统压力重启后的难点
阻塞线程 48 小时一个执行线程和关联状态进程消失,内存等待也消失
每 5 秒轮询一次定时任务、连接和查询需要恢复轮询进度和去重
挂起并等待信号一条等待记录需要持久化身份、状态和关联条件

第三种方案不是“睡得更聪明”。它改变了控制模型:执行先到达稳定边界并返回,外部回应稍后触发另一段执行。

图 12-1:定时等待与事件等待是同一种挂起机制的两种入口

先跑一次:看到真实的挂起边界

书稿自带的 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 key wait

sendConfirmation=CANCELLED 也不是业务拒绝。它表示本轮调度在挂起边界收束,依赖等待节点的下游尚未运行。第二遍信号到达后,等待节点和确认节点才变为 COMPLETED

观察结论: “成功但挂起”是合法的中间结果。只检查 isSuccess() 会把尚未完成的流程误报为完成。

Probe 源码在 WaitingForWorldProbeTest.java。它是书稿的可复核观察,不代替 BLOGE 自身的耐久性、并发或生产验收。

挂起到底保存了什么

把挂起想成餐厅的取餐牌。顾客不需要站在窗口占着服务员;餐厅只要记住“哪一单、等什么、结果送回哪里”。BLOGE 的对应关系是:

现实对象BLOGE 中的身份作用
一笔流程executionId找回同一次 graph 执行
一个等待点nodeId找到恢复入口
取餐牌号码suspend/correlation key防止回应送错流程
已完成步骤checkpoint/status恢复时不重做已完成工作
外部回应signal payload成为等待节点的输出

这五样东西缺一不可。只有 payload 而没有执行身份,系统不知道唤醒谁;只有执行身份而没有等待节点,系统不知道从哪里继续;没有已完成状态,恢复可能重复执行已经发生的 effect。

两个入口:waitawait

它们共享“挂起后恢复”这一机制,但回答不同问题。

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

图 12-2:外部事件从 webhook 经过关联记录恢复原 execution

读图时抓住两次控制权转移:

  1. execute 到达等待点后写入等待事实,并把“仍在等什么”返回给调用方。
  2. Webhook 到达后,publishEvent 先按事件名和业务键查找等待记录;匹配成功才向原 executionId + nodeId 发送 signal,继续下游。

因此 webhook endpoint 不是“继续执行函数”的公开入口。它只负责认证外部消息、提取稳定业务键、提供幂等消息 ID,并调用事件发布入口。真正的恢复身份由关联记录给出。

原理:恢复不是从头再跑

收到 signal 后,运行时应满足三个不变量:

  1. 已完成节点保持已完成,不因恢复而无条件重做。
  2. Signal payload 成为等待节点的输出,下游通过普通依赖读取它。
  3. 同一等待条件只能完成一次;迟到或重复消息不能改写第一次已接受的结果。

在 probe 第二遍运行中,Map.of("decision", "approved", "approver", "manager-7") 被写成 waitApproval 的输出,因此 sendConfirmation 能继续读取 decisionapprover。这就是“外部世界的回答重新进入 graph 数据流”的具体位置。

耐久化恢复还需要 execution checkpoint、wait store 和可重建的 graph definition。那是下一章的唯一新增变量;本章只建立进程内可观察的挂起协议。

单因素破坏一:把 correlation key 写错

保持事件名和 payload 不变,只把 orderIdO-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-9020.9.8-RC1EventDeduplicationTest 验证:相同 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,等于关闭去重。

RC1 边界:源码意图与实际行为不一致时必须停下

Probe 揭示了一个不能掩盖的差距:远端最新 examples 的工单 DSL 声明 signal_key = ctx.ticketId,但实际挂起输出是 {waitApproval=wait}。在固定的 0.9.8-RC1 源码中,WaitAwaitCompiler 解析了 signal_keyon_timeout,当前 compileWait 路径却只按 duration 构造 WaitOperator,没有把这两项绑定到运行时 Operator。

这意味着本章能证实:

  • ticket graph 在 RC1 上能够到达 SUSPENDED,并可通过节点 signal 恢复。
  • GraphResult 能区分成功、挂起和下游尚未运行。
  • RC1 核心测试覆盖事件关联、AND/OR 和重复事件。

本章不能证实:

  • 该 companion DSL 声明的动态 signal_key=T-2048 已被 RC1 执行。
  • on_timeout 中的业务 payload 会在 48 小时后按示例自动注入。
  • 进程退出后仍能恢复;这需要下一章的 durable store 和冷恢复证据。

此外,远端 examples 的 POM 仍固定 0.3.1,且依赖已经更名的 bloge-core-ext。直接构建会先在依赖解析处失败;即使覆盖为 0.9.8-RC1 也找不到该旧 artifact 名。不要把 examples 文件存在写成 RC1 companion 全量通过。

工程判断: 当 DSL、编译器和运行输出三者不一致时,以固定 commit 上的运行观察为准,记录差距并停止扩大承诺。

迁移到你的系统

在你熟悉的业务里找一个“下一步不在本系统手里”的动作,例如医院等待患者同意、物流等待海关放行、HR 等待候选人签约。产出一张四行责任卡:

项目你要填写的内容
等待身份executionId 和等待节点如何保存
业务关联键哪个稳定字段能把回应送回正确实例
重复身份发送方提供哪个 message/event ID
停止条件多久超时,超时后谁有权决定结果

不要写实现代码,先让业务和平台负责人共同确认这四行。若“超时后自动批准”没有业务授权,就停在未定义状态,不要让框架替业务方发明答案。

实验:把轮询改成事件等待

选择一段现有轮询逻辑,只做以下改动:

  1. 输入:一个稳定业务键、一种外部事件和一个可接受超时。
  2. 修改范围:把轮询节点替换为一个 await,保留下游业务节点不动。
  3. 运行:先发送错误 key,确认没有恢复;再发送正确 key,确认只恢复一次;最后重复相同消息 ID。
  4. 产物:保存三次观察、关联键来源和超时责任人。
  5. 停止条件:业务方无法确认超时结果,或发送方不能提供稳定重复身份时,停止实现并记录缺口。

实验验收卡

  • 预期与观察: wait 后结果 suspended,正确 signal 恢复同一执行。
  • 失败与恢复: 发送错误 key 或重复事件;恢复正确 key 并幂等处理。
  • 证明边界: 证明 suspend/signal/resume,不证明 durable store 已配置。
  • 练习合同: 工单事件;只改 correlation key;交付挂起和恢复结果;错误 key 不推进、正确 key 只推进一次即停止。

回顾

  • 挂起释放活跃执行资源,但保留恢复所需的身份和状态。
  • wait 面向时间或直接 signal;await 面向事件名与业务键关联。
  • isSuccess=trueisSuspended=true 可以同时成立。
  • 错误 key 应保持无人匹配,重复事件应被幂等层吸收。
  • 示例意图、编译器实现和实际输出必须三方对齐;不对齐时缩小承诺。

下一步只增加一个变量:第 13 章——持久执行会持久化恢复所需事实,让满足条件的进程在重启后继续同一笔执行。

精确事实入口

Coding Agent: Open the versioned task guide.