Skip to main content

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

第 26 章 —— Durable 与 Temporal 验证

本章承诺: 你会用有限 action plan 验证同一条 retained execution 的暂停、信号、恢复和逻辑时间,并把「最终结果」「不能提前发生」「限定时间内必须发生」写成不同的可观察合同。

学习目标

  1. 使用 EXECUTESIGNALADVANCE_TIMERESUME,不把长流程压成一次方法调用。
  2. 区分 DurableVerificationRunner 的本地执行报告和 DurableSourceBoundVerifier 的源码绑定证据。
  3. DurableTemporalContract 表达 step、trace 和 effect 的有限事实。
  4. 识别 streaming、timer、checkpoint 和非协作超时的停止边界。

最终 output 看不到等待过程

人工复核的最终结果可能都是 approved,但两个流程仍然不同:一个在收到人工决定后才通知,另一个在等待期间提前通知。只比较最终 output 会把第二个错误漏掉。

图 27:Durable action、retained session 与 temporal observation

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 的位置。

图 26-2:最终输出相同,temporal verdict 不同

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

合同有效、运行完成,但 notificationapprove 之前发生时,状态为 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。

实验:信号路径与超时路径

  1. 建立 approved-in-time plan:EXECUTE → ADVANCE_TIME PT23H → SIGNAL approved
  2. 声明 notification 不得早于 approve,且批准后在业务确认的逻辑时限内完成。
  3. 建立独立 review-timeout plan:EXECUTE → ADVANCE_TIME PT24H,预期进入超时路径。
  4. 先把 notification 移到 signal 之前,确认得到 FAIL/TEMPORAL_EXPECTATION_MISMATCH;恢复后再验证两条路径。
  5. 保存 action trace、temporal expectation result 和第一条 reason code;不保存原始信号 payload 到报告。

实验停止条件:如果图包含未分类 streaming 节点、custom nested provider 无法静态检查,或 effect inventory 不完整,不创建 session。先修复能力声明,再讨论业务结果。

现实迁移:保险理赔等待补件

保险理赔可能都得到最终 APPROVED,但缺失材料是在复核期限前到达,还是在系统 已经升级人工后才到达,业务含义不同。把材料到达建模为 signal,把服务时限建模为 logical time,再声明“升级不得早于批准”的 temporal relation。必须保留事件轨迹; 最终理赔对象无法区分这两段历史。

本章边界

  • ADVANCE_TIME 只推进 verifier 持有的逻辑时钟,不模拟真实调度延迟。
  • step/trace 合同证明声明的有限事件关系,不证明所有可能 interleaving。
  • persistent provider 只提供每 Case store 和唯一 mutable-resource namespace;engine、action 顺序和关闭仍由 verifier 持有。
  • source-bound manifest 保存初始 context 的 canonical SHA-256,不保存业务 payload。

实验验收卡

  • 预期与观察: 最终 output 相同,错误事件顺序仍使 temporal expectation FAIL。
  • 失败与恢复: 让 notification 早于 approval;恢复顺序后重放。
  • 证明边界: 证明固定 action plan 的时序,不证明全部并发交错。
  • 练习合同: 审批和通知事件;只交换顺序;交付 matched 前后结果;FAIL 只由顺序解释即停止。

本章小结

长流程的正确性由结果、状态边界和事件时间关系共同构成。有限 action plan 让这些事实可重复,temporal sidecar 让它们可诊断。下一章继续追问:这些 Case 即使全通过,是否真的有能力发现一个错误规则?

下一章:第 27 章 —— 发现验证盲区

Coding Agent: Open the versioned task guide.