中央赈粮令与多机构原子执行
南明风格的场景里,中央赈济机构签发一份赈粮令:国库拨出 600 单位粮食,县级粮务机构 登记地方执行情况。中央机构可以读取两个机构的记录,但只能写自己的状态;它需要确认 两边执行的是同一份赈粮令,结果也符合预期。
这个示例回答的问题是:多个插件各自拥有状态,怎样共同执行一份赈粮令,做到要么全部 提交、要么整步回滚?
机构名称只是示例代码里的标签。参伍没有内置朝廷、部院或州县类型,上层应用自行建模 自己的机构。
示例涉及的引擎部件
Section titled “示例涉及的引擎部件”- 三个模拟插件(
SimulationPlugin),各自拥有一种带类型的 领域记录(DomainRecordType)。 - 在同一边界的第 7、10、12 结算阶段运行的边界系统
(
BoundarySystemContract)。 - 插件之间通过规范化输入(canonical ingress)传递工作,
使用
BoundaryDirective::SchedulePluginIngress。 - 原子提交:任一系统出错,本边界的所有写入一起回滚。
- 使用同一组插件完成快照恢复和精确重放。
cargo run -p canwu-api --example governance_transition输出:
relief_order=relief-order-1646 treasury_grain=600 county_grain=600 exact_replay=ok只有 main 中的断言全部通过后才会打印这一行:第一个边界生成两条输入,第二个边界
提交两条记录,恢复的快照和重放的运行都与原运行一致。
赈粮令的流转
Section titled “赈粮令的流转”查看图表源码
sequenceDiagram
participant Host as 上层应用
participant Central as case-relief-central
participant Treasury as case-relief-treasury
participant County as case-relief-county
Host->>Central: issue-relief-order
Note over Central: 边界 1 第 7 阶段:publish-order 写入 ReliefOrder 清单
Central->>Treasury: execute-relief-order
Central->>County: execute-relief-order
Note over Treasury,County: 边界 2 第 10 阶段:两个机构各自创建 ReliefAction
Note over Central: 边界 2 第 12 阶段:audit-order 核对两条记录
Note over Central,County: 全部通过则提交,任一出错则整个边界回滚
代码都在
governance_transition.rs
中,只依赖 canwu-api。
1. 三个插件,三种记录
Section titled “1. 三个插件,三种记录”| 插件 | 拥有的记录 | 边界系统 |
|---|---|---|
case-relief-central |
ReliefOrder 清单(case.relief / order) |
publish-order、audit-order |
case-relief-treasury |
国库 ReliefAction(case.relief.treasury / action) |
prepare-treasury |
case-relief-county |
县级 ReliefAction(case.relief.county / action) |
prepare-county |
每个插件在 register 中调用 registrar.register_record_schema 注册自己的记录
schema。清单为每个机构写明:应当执行的系统、应产生的记录版本、处置结果,以及预期
载荷的哈希:
struct ReliefOrder { order_id: String, issued_by: String, treasury_system: String, treasury_version: u64, treasury_disposition: String, treasury_hash: String, county_system: String, county_version: u64, county_disposition: String, county_hash: String,}action_hash 用 canonical_hash 对预期的 ReliefAction 载荷
(status: "committed"、grain_units: 600)计算哈希。
2. 签发赈粮令
Section titled “2. 签发赈粮令”main 用三个插件创建模拟,把签发输入入队,然后结算第一个边界:
canwu.enqueue_plugin_ingress(canwu_api::PluginIngressRequest::new( CENTRAL_PLUGIN, ISSUE_INGRESS, SimTime::EPOCH, json!({"order_id": ORDER_ID}),))?;
let first = canwu.settle_boundary(BoundaryRequest::at(SimTime::EPOCH))?;assert_eq!(first.generated_ingress.len(), 2);3. 发布清单(边界 1,第 7 阶段)
Section titled “3. 发布清单(边界 1,第 7 阶段)”publish_order 运行在 BoundaryPhase::DomainDeltaProposal。它用
DomainRecordMutation::Create 创建清单,并为每个机构安排一条
execute-relief-order 输入:
BoundaryDirective::SchedulePluginIngress { target_plugin: TREASURY_PLUGIN.to_owned(), after: SimDuration::ZERO, packet_type: EXECUTE_INGRESS.to_owned(), priority: 0, payload: json!({"order_id": ORDER_ID}), affected: Vec::new(),},系统契约在 plugin_ingress_targets 中声明了这两个目标。零延迟安排的输入在下一个
边界准入,所以边界 1 结束时只有清单,两个机构的记录都还没有。
4. 各机构写自己的记录(边界 2,第 10 阶段)
Section titled “4. 各机构写自己的记录(边界 2,第 10 阶段)”prepare_treasury 和 prepare_county 都调用 prepare_owner,运行在
BoundaryPhase::HistoricalCandidateEvaluation。它只读取发给本插件的输入
(owned_order_ingress),用 validate_order 核对清单,其中包括即将写入的载荷
哈希,然后创建自己的记录。系统契约声明了 StateVisibility::SameBoundary,同一边界
第 12 阶段的审计因此能读到新记录。
5. 中央审计(边界 2,第 12 阶段)
Section titled “5. 中央审计(边界 2,第 12 阶段)”audit_order 运行在 BoundaryPhase::StrategicAggregation,契约里只声明读取、没有
写入。它逐个加载机构记录;记录缺失、版本不是 1、已失效、所属插件不对,或哈希与清单
不一致时返回错误:
if record.version != 1 || !record.is_active() || record.owner != expected_plugin || actual_hash != expected_hash || record.payload != json!({"status": "committed", "grain_units": 600}){ return Err(CanwuError::new( ErrorCode::InvalidBoundary, format!("central audit found an invalid {label} disposition"), ));}任一系统返回错误,整个边界失败,两个机构的记录随之回滚。边界 1 中审计看到签发输入 就直接返回,因为此时两个机构还没有行动。
6. 恢复与重放
Section titled “6. 恢复与重放”let snapshot = canwu.snapshot_json()?;let restored = Canwu::from_snapshot_json_with_plugins(&snapshot, &plugins)?;let replayed = Canwu::replay_from_journal(&plugins, &canwu.replay_journal())?;assert_eq!(restored.snapshot(), canwu.snapshot());assert_eq!(replayed.snapshot(), canwu.snapshot());恢复和重放都要传入同一组插件,因为快照记录了产生该状态的插件版本。
值得注意的地方
Section titled “值得注意的地方”- 每个机构只写自己插件拥有的记录。中央机构通过清单和输入协调,并读取结果。
- 零延迟安排的工作落在下一个边界,所以这份赈粮令用了两个边界:先发布,再执行和审计。
- 审计让这一步成为全有或全无:任何一个机构出错或缺席,两条记录都不会提交。
- 数量不对。 在
prepare_county中传入&action_payload(500)。哈希与county_hash不一致,validate_order返回错误,第二次settle_boundary失败,main以错误退出。 - 机构缺席。 让
prepare_county直接返回Ok(BoundaryProposal::default())。 审计报错central audit is missing the county relief disposition,国库的记录 也一起回滚。 - 换成自己的机构。 把
ReliefOrder换成自己的官署或政令 schema,并把签发输入 接到角色的权限和角色知识上。
示例之外:转移清单
Section titled “示例之外:转移清单”示例中手写的清单与审计是普通的应用层做法。参伍也把同样的保证做成了内置的
转移清单(TransitionManifest):
- 协调插件在第 7、10 或 12 阶段的系统中用
BoundaryDirective::RegisterTransitionManifest登记清单。套用到本例,lineage_id是赈粮令 ID,参与者是国库和县级插件;每个参与者列出转移前 (expected_pre)和转移后(expected_post)应有的记录版本,取代载荷哈希。 - 在就绪边界的第 10 阶段,每个参与者用
BoundaryDirective::StageTransitionWrite暂存自己的写入。 - 第 11 阶段,模拟内核先审计就绪的清单再提交:所有参与者都已暂存且版本相符则提交;只有部分 参与者暂存或版本不符,整个边界回滚;无人暂存则清单过期,协调者可以登记新的尝试。
- 第 12 阶段的系统以
TransitionAuditRecord读取审计结果。
无人暂存的清单只会过期,边界照常提交;只有其他参与者已暂存时才能发现缺席的机构,所以清单至少要有两个参与者。
打开可运行示例
阅读一致性测试
阅读仓库中的案例说明