跳转到内容

存档、重放与派生分支

本页介绍如何保存和加载模拟运行、为长期运行的模拟保存紧凑历史、精确重放一次运行,以及从某个存档点派生分支去尝试另一组输入。

参伍的存档不只是世界数据,还包括命令、事件、结算边界记录、随机抽样、插件语义环境和状态承诺,加载时据此验证状态。插件语义环境指本次运行中每个插件的名称、版本和语义哈希;加载和重放时提供的插件集合必须与之匹配。

持久化路径:运行中的模拟可以保存为快照或检查点日志并用相同插件恢复,可以从重放日志精确重放,也可以派生为平行现实. 查看图表源码.
查看图表源码
flowchart LR
  Run["运行中的模拟"] -->|"snapshot_json()"| Snap["快照"]
  Run -->|"checkpoint_journal_json()"| CJ["检查点 + 后续日志"]
  Run -->|"replay_journal()"| Journal["ReplayJournal"]
  Run -->|"fork()"| Fork["派生分支:新输入,平行现实"]
  Snap -->|"from_snapshot_json_with_plugins()"| Restored["恢复后的模拟"]
  CJ -->|"from_checkpoint_journal_json_with_plugins()"| Restored
  Journal -->|"replay_from_journal()"| Replayed["重放得到的模拟"]
  Plugins["匹配的插件集合"] -.-> Restored
  Plugins -.-> Replayed

恢复或重放得到的模拟,checkpoint_hash() 与原运行相同。派生分支从相同状态出发,之后各自发展。

目标 保存 加载 上层应用要做的事
普通存档或服务重启 snapshot_json() Canwu::from_snapshot_json_with_plugins() 保存完整快照,加载时提供相同的插件集合
长期运行的紧凑存储 checkpoint_journal_json() Canwu::from_checkpoint_journal_json_with_plugins() 保存检查点及其后连续不断的日志
审计或精确重建 replay_journal() Canwu::replay_from_journal() 或 replay_from_journal_json() 保存完整日志,并保留匹配的插件集合
向外部系统发送效果 outbox_entries() — 至少投递一次,按 delivery_id 去重,并保存确认状态
从同一时点尝试另一组输入 fork() — 把派生分支当作新的平行现实
use canwu_api::Canwu;
use canwu_reference_world::{ReferenceWorldPlugin, demo_scenario};
let (scenario, _) = demo_scenario()?;
let plugin = ReferenceWorldPlugin;
let canwu = Canwu::new_with_plugins(35, scenario, &[&plugin])?;
let json = canwu.snapshot_json()?;
let restored = Canwu::from_snapshot_json_with_plugins(&json, &[&plugin])?;
assert_eq!(restored.checkpoint_hash(), canwu.checkpoint_hash());

加载时会验证快照的格式、引擎版本和插件描述,任何一项不符都会失败。存档中一旦有插件领域记录,加载时就要传入同样的插件。

starter 示例把这段流程与派生分支、精确重放放在一起运行:

Terminal window
cargo run -p canwu-reference-world --example starter

检查点日志(CheckpointJournal)把一个经过验证的检查点,与其后连续不断的证据打包在一起。它从检查点出发,只携带之后记录的证据,适合长期运行。

精确重放根据日志中的初始场景、输入和已记录证据,重建同一次模拟运行:

let checkpoint_journal = canwu.checkpoint_journal_json()?;
let from_checkpoint =
Canwu::from_checkpoint_journal_json_with_plugins(&checkpoint_journal, &[&plugin])?;
assert_eq!(from_checkpoint.checkpoint_hash(), canwu.checkpoint_hash());
let journal = canwu.replay_journal();
let replayed = Canwu::replay_from_journal(&[&plugin], &journal)?;
assert_eq!(replayed.checkpoint_hash(), canwu.checkpoint_hash());

精确重放的规则:

  • 场景取自日志;replay_from_journal() 只接收插件集合和日志。
  • 运行身份、引擎版本、插件语义环境和已记录证据都必须匹配。
  • 重放直接使用已记录的结果:决策、随机抽样和外部答复都从日志读取,副作用也不会再发送一次。

fork() 把当前时点的模拟复制成一个独立的模拟,适合比较另一组命令、策略或研究假设。派生分支照常通过命令和时间接口推进;输入一旦不同,它就是新的平行现实,与原运行的精确重放是两回事。

需要送达外部系统的边界输出会出现在 outbox_entries() 中。每个 OutboxEntry 都有稳定的 delivery_id,由运行、结算边界、事件和输出序号派生而来。

  1. 在自己的存储中保存每个 delivery_id,以及对应的投递状态和确认状态。
  2. 按“至少一次”投递,超时或重启后可以安全重试。
  3. 接收方按 delivery_id 去重,因为同一次网络调用可能发生不止一次。
  4. 精确重放会生成相同的 delivery_id,投递仍由上层应用负责。

在紧凑模式(CompactedCanwu)下,seal_evidence() 返回封存后的 EvidenceJournalSegment。请把这个日志段与投递、确认状态一起保存。封存或压缩证据,并不表示效果已经投递或确认。

只有当快照体积或热历史增长真正成为问题时,才需要分页检查点、StatePageProvider、决策历史归档和插件归档参与者。到那时,上层应用必须:

  • 按内容标识保存每个状态页,并原样返回相同字节;缺页必须报错,不能当作空数据;
  • 保留每个检查点所需的连续日志前缀和归档保留根;
  • 为每个插件恢复相同的归档契约和语义哈希;
  • 删除冷数据前,先从已提交的根做可达性检查。

格式编号、分页存储契约和归档不变量见仓库中的版本与持久化契约。

  • 需要保存存档的应用,请固定到确切的发布版本或 Git 修订。
  • 插件语义哈希变化意味着行为变化。次版本发布(例如 0.12 → 0.13)可能同时改变多个扩展的语义哈希,使用这些扩展的旧存档随之无法加载。
  • 参伍不迁移旧存档。要跨引擎版本保留历史,请运行自己的导出与导入流程,或保留写出这些数据的旧引擎。
  • 新版本会增加枚举变体和结构体字段,穷举 match 和结构体字面量可能需要随之修改。

各版本的具体变化见版本与持久化契约。

  • 固定引擎版本和插件语义环境。
  • 完整走一遍:保存、重启进程、加载,再比较检查点哈希。
  • 做一次精确重放,比较最终检查点哈希。
  • 模拟一次外发超时,确认按 delivery_id 重试不会重复产生外部效果。
  • 写明版本升级时旧存档如何处理:导出后重新导入、保留旧引擎读取,还是放弃。