BLOGE
0.9.8-RC1在线版 · 事实校验 2026-09-15 · English
第 26 章 —— Durable 与 Temporal 验证
本章承诺: 你会用有限 action plan 验证同一条 retained execution 的暂停、信号、恢复和逻辑时间,并把「最终结果」「不能提前发生」「限定时间内必须发生」写成不同的可观察合同。
学习目标
- 使用
EXECUTE、SIGNAL、ADVANCE_TIME和RESUME,不把长流程压成一次方法调用。 - 区分
DurableVerificationRunner的本地执行报告和DurableSourceBoundVerifier的源码绑定证据。 - 用
DurableTemporalContract表达 step、trace 和 effect 的有限事实。 - 识别 streaming、timer、checkpoint 和非协作超时的停止边界。
最终 output 看不到等待过程
人工复核的最终结果可能都是 approved,但两个流程仍然不同:一个在收到人工决定后才通知,另一个在等待期间提前通知。只比较最终 output 会把第二个错误漏掉。
DurableVerificationRunner 为一个 Case 创建真实 DurableGraphEngine 和 retained session。默认使用隔离的 in-memory durable stores。action 按列表串行执行,并绑定同一个 execution identity;逻辑时间由 verifier 控制,不依赖测试运行时经过了多少墙钟时间。
Action plan 是有限脚本,不是工作流语言
下面是「开始后暂停,24 小时内收到人工批准」路径:
schemaVersion: 1
actions:
- actionId: start
type: EXECUTE
- actionId: wait-before-decision
type: ADVANCE_TIME
duration: PT23H
- actionId: approve
type: SIGNAL
nodeId: approval
signalData: approved
约束比语法更重要:
- action 数量必须在 1 到 64 之间。
- 第一条且只有第一条 action 必须是
EXECUTE。 ADVANCE_TIME的 ISO-8601 duration 必须为正。SIGNAL必须精确命中当前 suspended node。RESUME只用于已经失败且已写入 checkpoint 的 execution;仍在 suspended 的 execution 应先SIGNAL。CANCEL没有稳定 durable seam,sidecar 会返回DURABLE_ACTION_UNSUPPORTED。
超时路径应使用另一份 action plan,把逻辑时间推进到 PT24H 或业务确认的边界。不要把「收到信号」和「没有收到信号」硬塞进一条线性 plan;它们是两个业务 Case。
同时检查 step 与事件顺序
schemaVersion: 1
stepExpectations:
- expectationId: initially-suspended
afterActionId: start
outcome: SUSPENDED
suspendedNodeIds: [approval]
traceExpectations:
- expectationId: notify-after-approval
matcher: MUST_NOT_OCCUR_BEFORE
event: {type: NODE_COMPLETED, graphName: approval-flow, nodeId: notification}
afterActionId: approve
- expectationId: notify-within-five-minutes
matcher: MUST_OCCUR_WITHIN_LOGICAL_TIME
event: {type: NODE_COMPLETED, graphName: approval-flow, nodeId: notification}
within: PT5M
stepExpectations 检查某个 action 后的 outcome 和 suspended set。traceExpectations 使用 BLOGE listener 记录的 node identity、action 和逻辑时间偏移;它不执行脚本,也不匹配异常文本或原始业务 payload。
需要验证业务 effect 的重复发送时,使用 temporal v2:
schemaVersion: 2
effectExpectations:
- expectationId: ledger-booked-once
matcher: EXACTLY_ONCE_EFFECT_KEY
effect: {portId: ledger.book, key: LA-42}
所有参与 Operator 必须提供完整 dependency manifest,目标 EffectPort 必须声明稳定 idempotencyKey(input)。成功、失败和被拒绝的调用都计数,因为调用方看到失败之前,外部系统可能已经接受 effect。报告只保存类型化摘要,不保存原始 key。
运行本地 durable 验证
DurableVerificationPlan plan = DurableVerificationPlan.load(actionPath);
DurableTemporalContract temporal = DurableTemporalContract.load(temporalPath);
DurableVerificationReport report = DurableVerificationRunner.builder()
.harnessTimeout(Duration.ofSeconds(30))
.build()
.verify(
new DurableVerificationInput(
new VerificationCaseContext("loan-review", "approved-in-time"),
compiledGraph,
initialContext,
compiledOperators),
plan,
temporal);
这份 report 是本地 authoring evidence。若要发布 source-bound durable evidence,使用 DurableSourceBoundVerifier 从 tracked graph 和 sidecar 编译,并提供干净 Git worktree、可冻结的 registry/resolver、DURABLE execution profile,以及 ignored 且不在 classpath 中的输出目录。reader 不调用 bootstrap 重新编译 candidate。
最终 output 相同,事件顺序仍会改判决
运行同一条人工复核 graph 的两个版本。两者最后都是 decision=approved,唯一变化是 notification 的位置。
control: approve → notification → final approved
mutant: notification → approve → final approved
DurableTemporalContractTest.detectsAnEventThatOccurredBeforeItsRequiredActionDespiteFinalSuccess 执行的是 mutant。最后一条 action 仍记录为 COMPLETED,但 report 为 FAIL,唯一 reason code 是 TEMPORAL_EXPECTATION_MISMATCH,并且 notification-after-approval.matched=false。
这一个测试把最终 JSON 会压扁的三件事重新分开:
- durable execution 已经完成;
- 最终业务值确实存在;
- 业务要求的事件顺序仍然被破坏。
BLOGE cc38fbe5 的聚焦运行包含 19 个 temporal-contract tests 和 11 个 durable-scenario tests,共 30 个全部通过,无 failure、error 或 skip。这里的证据只覆盖声明的有限关系,不代表探索过所有可能 interleaving。
Temporal mismatch 是业务 FAIL
合同有效、运行完成,但 notification 在 approve 之前发生时,状态为 FAIL,reason code 为 TEMPORAL_EXPECTATION_MISMATCH。这和 sidecar 无效不同:未知 action、错误 node identity、缺失 dependency manifest 或不支持的能力组合通常是 INVALID。
DURABLE_SIGNAL_TARGET_INVALID——signal 没有精确命中 suspended node。 核对 suspended set;不要把目标放宽成任意节点。DURABLE_RESUME_STATE_INVALID——对 suspended execution 使用了RESUME。 改用SIGNAL;只对失败且有 checkpoint 的 execution 恢复。DURABLE_CHECKPOINT_MISSING——checkpoint 不存在。停止并检查 persistence;不要创建伪 checkpoint。DURABLE_TIMER_SET_AMBIGUOUS——一次推进同时到期多个 timer。当前 Preview fail closed;拆分计划或调整业务模型。STREAM_DURABLE_UNSUPPORTED——streaming graph 进入了 durable wrapper。 使用 streaming 自身的测试边界;不要声明不存在的 durable 能力。TEMPORAL_EXPECTATION_MISMATCH——temporal 合同有效但事实不满足。 把它作为业务FAIL,分析最早偏离事件。
非协作客户代码超过 harness timeout 后,runner 会尝试中断并等待 settlement grace。若任务仍然不退出,报告 DURABLE_HARNESS_ORPHANED,该 runner 随后 poisoned;不要在同一进程继续接纳新 plan。
实验:信号路径与超时路径
- 建立
approved-in-timeplan:EXECUTE → ADVANCE_TIME PT23H → SIGNAL approved。 - 声明
notification不得早于approve,且批准后在业务确认的逻辑时限内完成。 - 建立独立
review-timeoutplan:EXECUTE → ADVANCE_TIME PT24H,预期进入超时路径。 - 先把
notification移到 signal 之前,确认得到FAIL/TEMPORAL_EXPECTATION_MISMATCH;恢复后再验证两条路径。 - 保存 action trace、temporal expectation result 和第一条 reason code;不保存原始信号 payload 到报告。
实验停止条件:如果图包含未分类 streaming 节点、custom nested provider 无法静态检查,或 effect inventory 不完整,不创建 session。先修复能力声明,再讨论业务结果。
现实迁移:保险理赔等待补件
保险理赔可能都得到最终 APPROVED,但缺失材料是在复核期限前到达,还是在系统
已经升级人工后才到达,业务含义不同。把材料到达建模为 signal,把服务时限建模为
logical time,再声明“升级不得早于批准”的 temporal relation。必须保留事件轨迹;
最终理赔对象无法区分这两段历史。