存档、重放与派生分支
本页介绍如何保存和加载模拟运行、为长期运行的模拟保存紧凑历史、精确重放一次运行,以及从某个存档点派生分支去尝试另一组输入。
参伍的存档不只是世界数据,还包括命令、事件、结算边界记录、随机抽样、插件语义环境和状态承诺,加载时据此验证状态。插件语义环境指本次运行中每个插件的名称、版本和语义哈希;加载和重放时提供的插件集合必须与之匹配。
查看图表源码
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() 与原运行相同。派生分支从相同状态出发,之后各自发展。
先确定恢复目标
Section titled “先确定恢复目标”| 目标 | 保存 | 加载 | 上层应用要做的事 |
|---|---|---|---|
| 普通存档或服务重启 | 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() |
— | 把派生分支当作新的平行现实 |
保存与恢复快照(节选)
Section titled “保存与恢复快照(节选)”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 示例把这段流程与派生分支、精确重放放在一起运行:
cargo run -p canwu-reference-world --example starter检查点日志与精确重放
Section titled “检查点日志与精确重放”检查点日志(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() 把当前时点的模拟复制成一个独立的模拟,适合比较另一组命令、策略或研究假设。派生分支照常通过命令和时间接口推进;输入一旦不同,它就是新的平行现实,与原运行的精确重放是两回事。
向外部发送效果:外发队列
Section titled “向外部发送效果:外发队列”需要送达外部系统的边界输出会出现在 outbox_entries() 中。每个 OutboxEntry 都有稳定的 delivery_id,由运行、结算边界、事件和输出序号派生而来。
- 在自己的存储中保存每个
delivery_id,以及对应的投递状态和确认状态。 - 按“至少一次”投递,超时或重启后可以安全重试。
- 接收方按
delivery_id去重,因为同一次网络调用可能发生不止一次。 - 精确重放会生成相同的
delivery_id,投递仍由上层应用负责。
在紧凑模式(CompactedCanwu)下,seal_evidence() 返回封存后的 EvidenceJournalSegment。请把这个日志段与投递、确认状态一起保存。封存或压缩证据,并不表示效果已经投递或确认。
高级存储与归档
Section titled “高级存储与归档”只有当快照体积或热历史增长真正成为问题时,才需要分页检查点、StatePageProvider、决策历史归档和插件归档参与者。到那时,上层应用必须:
- 按内容标识保存每个状态页,并原样返回相同字节;缺页必须报错,不能当作空数据;
- 保留每个检查点所需的连续日志前缀和归档保留根;
- 为每个插件恢复相同的归档契约和语义哈希;
- 删除冷数据前,先从已提交的根做可达性检查。
格式编号、分页存储契约和归档不变量见仓库中的版本与持久化契约。
版本与存档兼容
Section titled “版本与存档兼容”- 需要保存存档的应用,请固定到确切的发布版本或 Git 修订。
- 插件语义哈希变化意味着行为变化。次版本发布(例如 0.12 → 0.13)可能同时改变多个扩展的语义哈希,使用这些扩展的旧存档随之无法加载。
- 参伍不迁移旧存档。要跨引擎版本保留历史,请运行自己的导出与导入流程,或保留写出这些数据的旧引擎。
- 新版本会增加枚举变体和结构体字段,穷举
match和结构体字面量可能需要随之修改。
各版本的具体变化见版本与持久化契约。
- 固定引擎版本和插件语义环境。
- 完整走一遍:保存、重启进程、加载,再比较检查点哈希。
- 做一次精确重放,比较最终检查点哈希。
- 模拟一次外发超时,确认按
delivery_id重试不会重复产生外部效果。 - 写明版本升级时旧存档如何处理:导出后重新导入、保留旧引擎读取,还是放弃。