从无锡发出、按路线投递的通信
无锡的一位指挥官亲自携带一封密封信件,收件人只有一位。程序把这次投递运行两次:第一次,指挥官知道的收件地址在无锡的投递区;第二次,地址在北京的投递区。每条路线都按指挥官自己知道的站点、时刻表和地址规划。
这个示例回答的问题是:信件怎样只凭承运人所知的信息穿过交通网络,同时把路线和每一步投递都记录下来?
网络是合成数据,站名、时刻表和行程时长都是演示用的。
示例涉及的引擎部件
Section titled “示例涉及的引擎部件”canwu-correspondence模拟领域扩展。其中的CorrespondencePlugin与canwu-information的InformationPlugin一起运行。一次通信(CorrespondenceOperation)就是一条定向消息,连同它的投递和运输过程。- 持有人相对知识:站点、连接和地址记在某个持有人(
KnowledgeHolderRef)的账本里,规划时在一个有记录的读取切面(KnowledgeReadCut)上读取这份账本。参见读取持有人知识。 canwu-routing的plan_route与canwu-transport的TransportExecution:后者逐段执行,并在路段之间记录保管权交接Handoff。参见路线规划与运输。canwu-information的信息生命周期:发送记录(Dispatch)及其投递尝试(DeliveryAttempt)。参见编码传递与中途截取。- 由操作定址随机抽样决定的自动通信机会。
cargo run -p canwu-correspondence --example routed_correspondence输出:
Wuxi local delivery: 1 leg(s), arrival minute 30 wuxi/hub -> wuxi/delivery/recipient via HorseWuxi to Beijing: 2 leg(s), arrival minute 3060 wuxi/hub -> beijing/station via Rail beijing/station -> beijing/delivery/recipient via Horsearrival minute 是计划中的 estimated_arrival_at,从运行开始按分钟计。北京方案在第 1 小时搭上直达列车,两天后、即第 2940 分钟到达北京站,再骑马两小时:
2940 + 120 = 3060。经南京的路线到北京站要晚一天。
查看图表源码
flowchart TD
K["承运人账本(本例即发送者):站点、时刻表、地址"]
O["通信机会或决策票据"]
C["initiate_correspondence_v1"]
P["在读取切面上按账本规划 RoutePlan"]
A["发送记录激活,创建 DeliveryAttempt"]
L["逐段运输,每个中转站记录 Handoff"]
D["投递尝试送达,发送记录完成"]
X["灾害:同一投递尝试的新 ItineraryRevision"]
I["截取:写入 Access 记录,投递继续"]
F["承运人被扣押或迟到:投递尝试失败"]
R["发送者提交 resolve_correspondence_v1"]
O --> C --> P
K --> P
P --> A --> L --> D
L -. 灾害 .-> X
X -. 改道 .-> L
L -. 截取 .-> I
L -. 扣押或超期 .-> F
F -.-> R
R -. 重试,新建投递尝试 .-> P
实线是示例实际走过的路径;虚线是事故与恢复,由本页末尾介绍的测试覆盖。
代码在
routed_correspondence.rs
及其夹具
examples/support/mod.rs
中。run(long_distance) 为每个请求新建一个模拟。传给 Canwu::new_with_plugins 的
1940 是随机种子。
1. 准备信件
Section titled “1. 准备信件”scenario_with_prepared_dispatch 从已弃用的兼容场景 Canwu::demo(1) 中取出指挥官(发送者)和观察者(observer,收件人),再添加 sealed-letter 渠道、信件内容、表现形式(representation)和一条发送记录:
&DispatchPayload { status: DispatchStatus::Prepared, target: DispatchTarget::Addressed(vec![recipient.clone()]), prepared_at: snapshot.initial_time, dispatched_at: None, completed_at: None,},插件只接受处于 Prepared、收件人正好是这一位,且发送者和渠道配置(channel
profile)都与请求一致的发送记录。
2. 给承运人一张地图
Section titled “2. 给承运人一张地图”network_seed 返回一份 NetworkKnowledgeSeed。北京版本知道一趟直达列车、一条经南京的路线和两条最后一程的道路。直达列车用 TraversalModel::Departures,在第 0、5、10 天的第 1 小时发车:
connection_departure( "wuxi-beijing-direct", "wuxi/hub", "beijing/station", TransferMode::Rail, departure, SimDuration::days(2),),种子里还有一条 KnownAddress,把收件人对应到 beijing/delivery/recipient(本地运行时为 wuxi/delivery/recipient)。run 以 KNOWLEDGE_INGRESS 为
KnowledgeHolderRef::Person(sender) 提交这份种子,插件把每个端点、连接和地址作为知识记录写入发送者的账本。
3. 提供通信机会
Section titled “3. 提供通信机会”在同一个边界里,run 还提交了 OPPORTUNITY_INGRESS:
CommunicationOpportunityRequest { operation_key: operation_key.to_owned(), sender: EntityRef::Person(sender), candidates: vec![recipient.clone()], reason: "routine correspondence".to_owned(), probability_per_mille: 1_000, automatic: true,}插件先做一次操作定址随机抽样,判断机会是否出现,再抽一次,从 candidates 中选出收件人,所以重放时结果相同。这里 probability_per_mille 为 1_000(必然发生),候选人又只有一位,因此每次都选中观察者,机会记录的状态为 SelectedAutomatic。
4. 发出通信命令
Section titled “4. 发出通信命令”let request = InitiateCorrespondenceRequest { operation_key: operation_key.to_owned(), sender: EntityRef::Person(sender), recipient, carrier: KnowledgeHolderRef::Person(sender), channel_profile: "sealed-letter".to_owned(), origin: RoutingNodeRef::new("wuxi/hub"), due_at: canwu.time() + SimDuration::days(10), prepared_dispatch, delivery_attempt_operation: InformationOperationId::new( "example.correspondence", format!("{operation_key}-attempt"), ), routing_policy: canwu_api::RoutingPolicy::default(), capacity_admission: CorrespondenceCapacityAdmission::Unconstrained, execution_id: canwu_api::TransportExecutionId(if long_distance { 2 } else { 1 }), automatic_opportunity: Some(opportunity_ref(operation_key)), carrier_delegation: None,};correspondence_command 把它包装成插件命令 initiate_correspondence_v1,由
Issuer::System 以 CommandAuthority::no_responsible_actor(...) 签发。只有存在 key、发送者和收件人都相同的 SelectedAutomatic 机会时,插件才接受这种权限。承运人就是发送者,所以 carrier_delegation 为 None。
5. 规划并激活
Section titled “5. 规划并激活”plugin.rs
中的 settle_start 读取承运人的账本,解析地址,然后规划路线:
let (snapshot, address) = carrier_planning_snapshot( view, &admitted.request.carrier, &admitted.request.recipient, context.at,)?;let route_plan = plan_route( &snapshot, &RoutingRequest { origin: admitted.request.origin.clone(), destination: address.destination.clone(), departure_at: context.at, policy: admitted.request.routing_policy.clone(), },).map_err(|error| invalid_record(error.to_string()))?;默认的 RoutingPolicy 选最早到达的路线。插件把计划、地址、读取切面和规划证据存入
CorrespondenceOperation,把通信机会标为 Consumed,再请信息插件激活发送记录。激活时创建第 1 次投递尝试,截止时间取请求中的 due_at。
6. 逐段运送
Section titled “6. 逐段运送”插件按各段的计划时间自己调度 ProgressAction::StartLeg 和 CompleteLeg 输入,并拒绝上层应用提交的进度。非最后一段完成时,complete_leg 在该站记录一次计划内的
Handoff;北京那次运行在 beijing/station 记录一次。最后一段到达时:
let status = if context.at <= operation.current_due_at { DeliveryAttemptStatus::Delivered} else { DeliveryAttemptStatus::Failed};投递尝试送达后,插件完成发送记录,操作变为 CorrespondenceStatus::Settled。run
最多推进 80 个边界,直到
typed_domain_record(&correspondence_operation_ref(operation_key)) 显示 Settled,再由 print_plan 打印 route_plan。
值得注意的地方
Section titled “值得注意的地方”- 两个请求走的是同一套插件代码,区别只在种子数据和地址。
- 计划只用承运人账本里的连接。
RoutePlan.estimated_arrival_at是估计值,DeliveryAttempt.due_at是截止时间。迟到会让投递尝试失败,操作标为DeadlineMissed,due_at保持原值。- 每一步都有记录:带读取切面的规划历史、运输路段与交接,以及带版本的发送记录和投递尝试。
- 去掉直达列车。 在
network_seed中删除wuxi-beijing-direct。北京方案会变成经nanjing/station的三段路线,在第 4500 分钟到达:第 25 小时到南京,第 73 小时到北京站,再骑马两小时。 - 错过截止时间。 把
due_at改成canwu.time() + SimDuration::days(1),让循环在operation.status.is_terminal()时返回,并打印operation.status。本地信件照常结算;北京信件在第 3060 分钟到达,晚于第 1440 分钟的截止时间,以DeadlineMissed结束。 - 压制通信机会。 把
probability_per_mille设为0。机会记为Suppressed,插件拒绝命令,run等不到操作出现,最终 panic。
示例在两封信都结算后就结束了。生命周期的其余部分在 crate 中实现,并由测试覆盖:
routed_delivery.rs
覆盖通过决策发信、事故和恢复;
gap_g25_correspondence_delegated_carrier.rs
覆盖受托承运人;
gap_g26_correspondence_carrier_seized.rs
覆盖承运人扣押。这些测试大多最后会恢复快照,并用 Canwu::replay_from_journal 重建运行(精确重放)。
- 通过决策发信。
correspondence_decision_ticket构造一张决策票据,其中的send选项携带通信命令。决策来源是发送者本人时,插件接受这条命令;routed_delivery.rs走的就是这条路径。 - 事故。 应用通过
INCIDENT_INGRESS提交带probability_per_mille的CorrespondenceIncidentRequest。插件用操作定址随机抽样判定是否发生,并把事故记在操作上;在不适用的状态下到达的事故作为已抑制的证据保留。 - 恢复。 发送者的决策命令
resolve_correspondence_v1带一个CorrespondenceRecoveryAction。ReplanCurrentAttempt在承运人得知新连接后,让处于WaitingForRoute的投递尝试继续。RetryDelivery发起投递重试:新建一次投递尝试,以DomainReference角色"previous_attempt"指向旧尝试,并使用新的截止时间和TransportExecution。FinalizeDispatch完成发送记录,操作以Failed或DeadlineMissed结束。在此之前,投递尝试失败后发送记录保持Active。 - 受托承运人。 受托承运人先以自己的命令权限签发
delegate_carrier_v1,其中的DelegationClaimV1把承运人写为performed_by、发送者写为performed_for,并列出carry_correspondence能力。从下一个边界起,发送者在InitiateCorrespondenceRequest::carrier_delegation中引用这条命令。此后规划读取承运人的账本,引擎不向发送者发布其中任何内容。声明必须覆盖每次发送,包括重试;同一承运人为同一发送者签发的新委托会取代旧委托。 - 运力。
CorrespondenceCapacityAdmission只有Unconstrained一个变体,规划和投递都忽略运力。运力池属于canwu-movement,参见路线规划与运输。
打开可运行示例
阅读投递、改道与重试测试
阅读受托承运人测试
阅读承运人扣押测试