跳转到内容

资源与生产系统

canwu-resource 即资源模拟扩展,把粮食、布匹、弹药这类守恒存量记在账户里,只通过有记录的操作改变它们。canwu-production 即生产模拟扩展,用已消费的投入和设施产能完成生产,再把产出交回 canwu-resource 入账。游戏里有会耗尽的存量、多方争用同一批存量,或者要把投入加工成产品时,请读这一页。各项操作的具体规则见生产、资源与运输的职责划分。

crate 负责什么 发布情况
canwu-resource canwu.resource 命名空间:资源与单位版本、账户、受保护存量底线、需求、资源预留、分配腿、转移及其托管量、消费、损耗、履约、操作结果、动用授权、完成租约和持有人报告 已发布到 crates.io
canwu-production canwu.production 命名空间:工艺版本、生产地点、设施、产能分配、工单、生产执行、在制品、设施项目、事故凭证和持有人报告。它不保存任何存量。 已发布到 crates.io
canwu-economy-reference-content 附出处的模型卡、覆盖单元和三个 fixture:synthetic_grain_fixture、ming_workshop_fixture 和 china_industrialization_fixture。其中没有求解器,也没有命令处理器。 已发布到 crates.io
canwu-force-supply-reference 可替换的军事消费者:军队需求、消费意图、战备与短缺后果,以及征发 saga 仅在仓库中
canwu-economy-reference 可替换的组合示例:十四个月的粮食循环(GrainHarness),以及本地稀缺度投影和价格压力投影 仅在仓库中
crate 依赖:canwu-resource 依赖 canwu-api;canwu-production 依赖 canwu-resource 和 canwu-technology;canwu-force-supply-reference 依赖 canwu-resource 和参考内容;canwu-economy-reference 组合 canwu-force-supply-reference 和 canwu-production;上层应用向两个扩展提供输入. 查看图表源码.
查看图表源码
flowchart TB
  Host["上层应用<br/>(参考实现中为 GrainHarness)"]
  Economy["canwu-economy-reference<br/>粮食循环与投影"]
  Force["canwu-force-supply-reference<br/>军队需求与后果"]
  Content["canwu-economy-reference-content<br/>模型卡与 fixture"]
  Production["canwu-production<br/>工艺、设施、工单"]
  Technology["canwu-technology<br/>技术证据"]
  Resource["canwu-resource<br/>守恒存量"]
  Api["canwu-api"]
  Host -- "场景记录、命令、<br/>分配请求" --> Resource
  Host -- "工艺、生产地点、<br/>工单" --> Production
  Economy -- "组合" --> Force
  Economy -- "组合" --> Production
  Force -- "依赖" --> Content
  Force -- "依赖" --> Resource
  Production -- "依赖" --> Resource
  Production -- "依赖" --> Technology
  Resource -- "依赖" --> Api
  Technology -- "依赖" --> Api

canwu-resource 只依赖 canwu-api。canwu-production 另外依赖 canwu-resource 和 canwu-technology。每个生产执行和设施项目都带一个 TechnologyEvidenceBinding:一个精确的 TechniqueRevision,一个 CapabilityQualification 或 ImplementationRecord,工艺设置了 adoption_required 时还要有一个 AdoptionRecord。生产执行开始或设施项目创建时,它引用的每个精确领域记录版本都必须是其记录的当前版本,因此用 seal_evidence 封存过的模拟运行与其精确重放接受相同的命令。项目此后推进时不再重新检查提供方和技术证据(创建时已校验),但仍会重新检查项目的资源证据。canwu-api 没有重新导出这两个扩展,请自行加入 Cargo.toml。

canwu-resource 是库存余额的唯一写入者。生产、军事补给和你自己的领域提交资源操作,再读取结果。canwu-transport 和 canwu-movement 负责运送货物,并提供途中的证据。转移通过 TransportExecutionLink 关联这些证据,货物的余额、损耗和验收仍由 canwu-resource 记录。

两个参考 crate 的用途,是证明两个互相独立的消费者可以共用同一套资源生命周期。机制决策记录了为什么这些证据足以让 canwu-resource 和 canwu-production 作为模拟内核之外的可选扩展发布。实际项目中,请用你自己的领域替换它们。

每个 ResourceAccount 只保存一个数量 balance。可用量、预留量和受保护量都由 ResourceState::account_quantities 从它推导。已经离开来源账户、尚未到达的存量记在对应 ResourceTransfer 的 escrow 字段里,仍然计入总量。ConservationTotalsV1 累计期初总量和每一笔已准入的来源与去向,ResourceState::validate_conservation 用 128 位整数求和,检查下面这个等式:

账户余额 + 转移托管量
+ 已准入消费 + 已准入损耗 + 外部流出
= 期初余额 + 期初托管量
+ 已准入产出 + 外部流入

每一项都只有一条入口:

项 改变它的操作
期初余额 构建场景时的 ResourceState::install_opening_account。运行中用 CreateAccount 创建的账户必须从零开始。
已准入产出 来源为 ResourceCreditSourceV1::Production 的 Credit,只能通过生产产出批次结算
外部流入 来源为 ResourceCreditSourceV1::ExternalInflow 的 Credit,须引用证据
已准入消费 对某条分配腿的 Consume
已准入损耗 账户上的 RecordLoss、针对转移部分托管量的 RecordTransferLoss,或转移的 Lose 处置
外部流出 ExternalOutflow,可以是针对账户的请求,也可以是转移的处置方式,须引用权限证据

另有三条规则保证这个等式可信。单位从不隐式换算,因为每个账户、需求和转移都指定一个确切的资源版本和一个确切的单位版本。每个操作都带一个 ResourceOperationKey:重发同一请求会得到原来的 ResourceOperationOutcome,同一个键配上不同的请求则以幂等冲突失败。违反领域规则的操作会结算为 Rejected 结果,所有数量保持原样。如果候选状态仍然不满足等式,第 8 阶段的检查失败,整个结算边界回滚。

资源类型,都在 canwu-resource 中:

类型 表示什么
ResourceDefinitionRevision、ResourceUnitRevision 带品质和范围的不可变资源定义,以及带比例系数的单位
ResourceAccount 某一确切资源与单位的一份余额,由保管人(一个 KnowledgeHolderRef)持有,可带容量上限、受保护存量底线政策和 place_scope
ProtectedFloorPolicyRevision 不参与普通分配的保留量,例如种粮,以及允许动用底线以下存量的需求类别。政策版本可以由场景预先编写,也可以由账户的保管人在运行中安装
ResourceDemand 申请者提出的需求:数量、最低有用量、PartialFulfillmentPolicy、到期与失效时间、优先级、平局决胜键和 source_policy
ResourceReservation、ResourceAllocationLeg 某个账户为某项需求分出的一份数量;消费者引用分配腿作为精确证据
ResourceTransfer 两个账户之间处于托管中的存量,带 ResourceTransferState、可选的 TransportExecutionLink(当前的运输关联),以及记录它推进时经过的每条运输关联的 link_history
ResourceConsumption、ResourceLoss、ResourceFulfillment 终态证据:用掉了多少、损失了多少、需求满足了多少
ResourceOperationOutcome 每个操作的不可变凭证,状态为 Applied、Rejected 或 Duplicate
ResourceAccessGrantV1 授权方保管人记录在案的同意:被授权方可以在额度和时间窗口内动用它的存量
CompletionLeaseActivationCertificateV1 某个持有人可以执行某项不可逆操作的凭证,锁定该操作依赖的精确记录
ResourceConsumptionBatchRequestV1 在同一份完成租约下结算的最多 256 笔精确分配腿消费,每一项各有自己的结果;见消费批次
ResourceState 内存中的整个资源根状态;持久化时拆成一条活动根记录(ResourceLiveRootV1)、若干常驻终态桶记录(ResourceHotTerminalBucketV1),以及存放账户、动用授权、资源定义、单位和保管人索引条目的若干活动桶记录(ResourceLiveBucketV1)

生产类型,都在 canwu-production 中:

类型 表示什么
ProcessRevision 不可变的工艺:要求组(十二种 ProductionRequirementKind,从 Material 到 FinanceOrganization)、投入、产出、产能需求、工作量和实际产出的上下限
ProductionSite 某个持有人的生产地点,带 ProductionSiteForm,从 Household 到 MultiSiteEnterprise。形态只是数据,没有建筑等级。
FacilityAsset 生产地点上的设施:代际、FacilityLifecycle、以千分比表示的状况、各项能力的产能,以及事故风险
ProductionCapacityAllocation 某个生产执行占用的一段设施产能,时间区间左闭右开
WorkOrder、ProductionExecution 在某地点运行某工艺的工单,以及它的一次执行,带证据、已消费的投入和待结算的产出
WorkInProgress 生产执行的进度,以及所消费投入的证据。存量本身留在 canwu-resource。
FacilityProject 某一代设施的建设或维修
ProductionOperationOutcome 每条生产命令的凭证,处置结果为 Applied、Duplicate 或 Rejected
生命周期:需求得到分配,完成租约授权扣减,存量被消费或转移,生产引用已消费的投入,产出回到 canwu-resource 入账,再把确认返回给生产. 查看图表源码.
查看图表源码
flowchart TB
  Demand["SubmitDemand<br/>申请者的带请求标识命令"] --> Alloc["一次分配<br/>enqueue_resource_allocation"]
  Alloc --> Leg["资源预留与<br/>分配腿"]
  Leg --> Lease["完成租约<br/>激活凭证"]
  Lease --> Consume["Consume<br/>消费与履约"]
  Lease --> Transfer["BeginTransfer<br/>存量进入托管"]
  Transfer --> Arrive["CompleteTransfer<br/>Accept、AcceptLocal、Lose、Return"]
  Consume --> Start["StartExecution<br/>引用已消费的投入"]
  Start --> Complete["CompleteExecution<br/>在制品完工"]
  Complete --> Batch["产出批次<br/>在 canwu-resource 入账"]
  Batch --> Ack["产出确认<br/>生产执行转为 Settled"]

资源操作有四条入口。带请求标识的命令是通过 CommandRequest 提交、带请求 ID 和预期修订版本的命令。每条入口只接受一部分操作:

入口 由谁发送 操作
带请求标识的命令 apply_resource_operation_v1 对自己掌控的存量采取行动的持有人。命令主体必须是掌控目标记录的持有人。 CreateAccount、SubmitDemand、AmendDemand、CancelDemand、BeginTransfer、BeginExchange、CancelTransfer、以 AcceptLocal、Lose 或 Return 处置的 CompleteTransfer、RecordLoss、RecordTransferLoss、ExternalOutflow、外部流入的 Credit、SetProtectedFloor、IssueAccessGrant、RevokeAccessGrant,以及租约的申请和放弃
适配器输入 resource_adapter_operation_v1 提供方插件。请求中的证据字段必须等于数据包引用的精确记录版本,带时间的请求只能在它写明的时间结算。 Consume、AdvanceTransfer、以 Accept、AcceptLocal 或 ExternalOutflow 处置的 CompleteTransfer、RecordLoss、RecordTransferLoss、ExternalOutflow、外部流入的 Credit,以及观察记录
上层应用的 resource_account_rescope_v1(enqueue_resource_account_rescope) 只有上层应用;其他插件安排的数据包什么也不结算。 RescopeAccount
插件自有的数据包 上层应用通过辅助函数发送,或由 canwu-production 发送 分配请求(resource_authorized_allocation_v1)、租约状态转移(resource_completion_operation_v1,包括一步式完成租约)和生产产出批次(resource_production_output_batch_v1)
  1. 准备。上层应用在 ResourceState 中安装资源定义、单位、期初账户、底线政策和报告授权,用 ResourceState::into_records 转成场景中的领域记录(活动根,以及存放账户、动用授权、资源定义和单位的桶记录;into_record 会拒绝含有这些记录的根),再注册 ResourcePlugin::new(adapter_evidence_kinds)。这个列表写明哪些记录种类可以为适配器操作提供证明。生产状态以同样的方式通过 ProductionState::into_initial_record 和 ProductionPlugin 进入。
  2. 需求。申请者发送带请求标识的领域命令 apply_resource_operation_v1,命令用 resource_command 构建。命令处理器检查签发者能否代表命令的 subject、subject 是否掌控每个目标,然后排入一条 resource_command_v1 输入。
  3. 分配。上层应用用 enqueue_resource_allocation 为一个申请者排入一次分配。边界准入这条输入后,第 7 阶段的写入系统先让过期的需求失效,再收集该申请者已到期或有变化的需求;需求数超过请求中的 candidate_limit 时,这次分配失败。随后按优先级从高到低依次满足这些需求,优先级相同时再依次比较到期时间、平局决胜键、准入序号和需求 ID。每分出一份,就写入一条资源预留和一条分配腿。
  4. 租约。执行任何不可逆步骤之前,行动的持有人需要一份已激活的完成租约,通过 enqueue_resource_completion_operation 取得。租约要经过四个操作(申请、授权、准备、激活)才会激活,因为每个参与方各自授权、各自准备:对于生产执行,由 canwu-production 协调租约,canwu-resource 作为参与方加入。如果资源插件是唯一的参与方(例如发起转移、本地验收、损耗或流出),一个 SingleStepLease 操作就能完成全部四步,检查相同,得到的凭证也相同,见完成租约。凭证锁定该步骤依赖的精确账户、分配腿和记录。每次消费、转移发起、转移处置、入账、损耗和流出都必须携带这份凭证。
  5. 扣减。消费者插件用 enqueue_resource_adapter_operation 经适配器输入发送 Consume,并引用授权这次扣减的自有记录的精确版本。另一种方式是来源保管人用 BeginTransfer 发起转移,相应数量移入托管。转移进度和 Accept 以适配器操作的形式到达,并引用运输证据。经过多个路段运送的转移会在运输关联历史中记下每个路段和交接;途中可能损耗一部分托管量,而转移仍然继续。AcceptLocal、Lose 和 Return 以其他方式结束转移。转移的每一步都写明预期的转移修订号,所以同一次到货不会入账两次。
  6. 生产。生产命令(apply_production_operation_v1)创建并批准工单。StartExecution 引用 canwu-resource 已经消费的投入:每个 ResourceInputBinding 写明分配腿、消费记录和对应结果。它还带上产能分配,由第 7 阶段标为 Consumed。AdvanceExecution 推进在制品,CompleteExecution 让生产执行进入 CompletedPendingOutputSettlement,其产出数量可以按实际产出比例缩放。
  7. 产出。在第 12 阶段,生产插件把当前的生产记录版本固定为该执行的 output_source,并安排一条 resource_production_output_batch_v1 输入。在之后的某个边界,资源写入系统要么为批次中的全部产出入账,要么一项也不入账,然后安排一条带 settled_at(入账结算的时刻)的 production_output_ack_v1。在资源记录里放不下的批次会被延后,不会丢失(见跳过的数据包与延后结算),它的各笔入账改到实际结算的时刻。生产插件的第 7 阶段检查每个结果的请求摘要都绑定了 settled_at,保存这些结果和 output_settled_at,释放产能,并把生产执行标为 Settled。
  8. 检查与发布。第 8 阶段校验每个扩展的候选状态;守恒或产能规则被破坏属于引擎不变量失败,边界失败。第 13 阶段发布持有人报告。

StartExecution 会把资源插件永远不可能入账的产出集合作为记录在案的拒绝拒掉:操作键落在保留前缀 resource:completion: 之下、被另一个运行中或等待产出结算的执行占用、或已被资源记录或其归档中的某个操作结果持有,产出超过 MAX_PRODUCTION_OUTPUT_BATCH_CREDITS(64)项,或者产出集合在资源记录的限制下永远放不下(ResourceState::production_output_batch_can_fit)。工艺最多声明 64 项产出,超出的在场景阶段就被拒绝。

分配在资源插件的第 7 阶段写入系统中进行,面对的是持久化的账户和需求。阶段边界与资源分配教程中内核第 6 阶段的资源预留原语(ReservationRequest)是另一套机制,处理在单个边界内声明的供给和请求,canwu-resource 没有使用它。

各个边界系统都由事件驱动,并声明 SameBoundary 可见性:

crate 阶段 系统 做什么
canwu-resource 7 DomainDeltaProposal settle-resource-lifecycle-v1 资源根状态的唯一写入者。应用命令、适配器操作、分配请求、租约状态转移和产出批次,并向受影响的角色发出 canwu.resource.operation_settled.v1。
canwu-resource 8 InvariantValidation validate-resource-invariants-v1 运行 ResourceState::validate,其中包括守恒检查
canwu-resource 12 StrategicAggregation maintain-resource-summary-v1 再运行一次同样的校验
canwu-resource 13 PerspectiveAndReportMaterialization materialize-resource-reports-v1 以 resource_report 知识发布持有人报告
canwu-production 7 DomainDeltaProposal production_lifecycle_apply_v1 生产根状态的唯一写入者。应用命令、租约状态转移和产出确认。
canwu-production 8 InvariantValidation production_capacity_and_lifecycle_validate_v1 检查记录闭合、在制品总量,以及重叠的产能分配之和不超过设施按状况折算后的产能
canwu-production 10 HistoricalCandidateEvaluation production_incident_candidate_evaluation_v1 抽样判定设施事故,并暂存对应的状态转移
canwu-production 11 ConditionalTransitionCommit production_incident_commit_audit_v1 在事故提交后校验状态
canwu-production 12 StrategicAggregation production_output_dispatch_v1 固定 output_source,并安排产出批次
canwu-production 13 PerspectiveAndReportMaterialization production_holder_report_publish_v1 发布持有人报告

生产插件没有为事件注册受众,所以它的事件不会出现在角色相对视图中;持有人通过报告了解生产情况。阶段顺序见结算系统。

要结算许多小额消费的提供方(例如一个从同一座粮仓给每户发口粮的结算衙门),发送一条 ConsumeBatch(ResourceConsumptionBatchRequestV1),而不是为每户各取一份租约:1 到 256 项,每一项是一条精确的分配腿,带它的需求、需求修订号、预期账户修订号和消费标识,整批共用一份消费者证据、一个时刻和一份完成凭证。凭证绑定批次的操作键,并锁定每一项的账户、分配腿和需求(locked_targets),租约的持有人必须是每一项需求的申请者。批次只能经由提供方自己的适配器输入进入;提供方为每一项授权一条类型化的消费意图,其操作键就是该项预留的键 resource_consumption_batch_item_key(batch key, index)。

  • 各项按顺序结算。每一项做单条 Consume 要做的全部检查;没通过的项单独被拒绝(在它的键下记一条被拒绝的 Consume 结果,分配腿仍保持预留),通过的项扣减它的分配腿,并记录消费、履约和一条已应用的结果。多项可以动用同一个账户:预期修订号等于批次开始时所见修订号的项,按批次此前留下的账户状态扣减。
  • 批次键下的批次结果汇总已消费的数量,被拒绝的数量记为 remainder;租约只完成一次、只留一张凭证,然后离开资源根。没有任何项、超过 256 项、凭证没有恰好绑定批次的目标,或某项的键已被占用时,只记一条被拒绝的批次结果,租约保持激活。
  • 余量按整个结果计算:放不进资源根的批次被跳过(archive_backlog,或者在任何提交和释放都腾不出空间时为 reserved_capacity),什么也不记录,所以取租约之前先用 ResourceState::consumption_batch_can_fit(items) 检查。消费掉需求唯一一条分配腿的项留下六条常驻终态记录,批次本身再留两条。
  • ResourceLimitsV1::max_batch_items_per_boundary(规范值 32,即规范归档预算在另一份租约的预留之外还能容纳的数量;硬上限 2,048)限制一个边界结算的项数。单独能放下、但加上本边界已应用的批次就放不下的批次,以 boundary_budget 跳过事件延后到同一时刻的下一个边界,由那个边界结算。要结算满额批次,请同时调高 max_archive_candidates:一个 256 项的批次需要 1,539 个候选名额。
  • 重发的批次由批次结果回答,不论它还在热区还是已归档;各项的键是预留键,其他请求不能使用。一个持有人每模拟分钟仍只能取一份租约,所以同时结算的多个批次要么属于不同持有人,要么各自引用自己的记录。

军事补给参考消费者(canwu-force-supply-reference)是同一生命周期的第二个独立使用者:军队的消费意图经适配器输入变成一条 Consume,资源结果结算之后,短缺后果只应用一次。经济参考插件批准民间消费意图,并完成每个月的月结;粮食、生产与军事补给案例逐步讲解了这两个消费者。这两个参考插件的参考包归档都是有界的:归档提交一经确认,它的凭证就折叠进插件根记录的摘要链;结果放不下的操作以 operation_skipped 事件跳过,等归档提交腾出空间后再发一次,绝不会让边界失败。

受信任的上层应用可以用 resource_state 读取整个资源根状态,并用精确查询核验证据,例如 exact_resource_allocation_leg、exact_resource_operation_outcome 和 exact_resource_fulfillment。玩家和游戏内智能体读取的是持有人报告:

  • ResourceReportGrantV1 是针对一个持有人的显式白名单:一个范围、若干账户、若干需求、是否包含转移细节、置信度、报告周期和延迟。第 13 阶段为每份授权记录一个观察头,resource_report 由这个观察头生成 ResourceReportDtoV1。有延迟的持有人看到的是它观察时刻记录下的余额。报告从不回退到真值,也不会透露需求的来源账户列表。完成租约为持有人预留的报告,在授权没有观察头、或持有人此时被引擎拒绝接受知识时放弃:参与者不带报告归档,也不再排报告唤醒;为引擎拒绝的持有人预留报告的完成操作记为 invalid_authority 拒绝,install_report_grant 拒绝永远不能持有知识的持有人种类(核心组织、资源、路线、领地,或作为实体给出的人物)。
  • 每条库存观察都带有账户所属资源版本的 ResourceScopeId,消费者无法把远处的存量改标成本地存量。
  • resource_access_grant_status 只向授权方或被授权方展示授权及其额度核算。
  • ProductionObserverGrant 让一个持有人以某种角色观察一组生产地点:Operator、LocalOwner 或 RemoteOwner,并可设延迟。production_report 需要一份精确的授权,未获授权的持有人会得到权限错误。
  • 军事补给参考消费者的报告只由持有人观察时点之前可用的类型化知识发布组成。

经济参考实现还提供两个供决策使用的独立读取模型:本地稀缺度投影和价格压力投影。生成它们不会改变任何状态;粮食案例的稀缺度与价格一节介绍了这两个投影。

只需要几个账户或几条记录的受信任的上层应用,不必读取整个资源根。ResourceRootReader::load_run(run) 读取任意 CanwuRun 的活动根;attach、attach_live 和 attach_custodian_accounts 只挂载它们点名的内容,与在边界里一样,attach_all 则挂载整个资源根。

对同一份已提交状态和同样的请求,它读到的内容与 ResourceRootReader::load(view) 读到的相同。resource_state 按记录种类列出各个桶,而不是遍历每一条记录;resource_allocation_leg、exact_resource_allocation_leg、resource_consumption、resource_operation_outcome、resource_access_grant_status、resource_completion_status 等查询只读取活动根和它们点名的记录。它们不再运行整个资源根的完整性检查:自己没有读到的损坏的桶,不会再让答案变成 None 或错误,所以依赖查询来发现损坏的上层应用应当调用 resource_state 或 validate_resource_runtime。

每一步之后都要重新读取的上层应用,可以用 keep 把读取方变成不借用运行的 KeptResourceRoot,再调用 ResourceRootReader::reattach(run, kept):它只解码运行已不再与之共享记录的桶。已读资源根只存在于上层应用的内存中,从不保存。这些都是对所有保管人真值的受信任的上层应用读取;角色看到的仍然只有 resource_report 和上文的持有人报告授权。

canwu-resource、canwu-force-supply-reference 和 canwu-economy-reference 不抽取随机数。分配顺序来自数据:优先级、到期时间、平局决胜键、准入序号和 ID。

canwu-production 声明了一条随机流 production_incident_random_stream(),即该插件名下版本为 1 的 facility-incident 流。只有准入了生产输入的边界才会检查事故,每次抽样都引用那条输入。第 10 阶段对每个到期的设施这样处理:

  • 做一次 0 到 999 的操作定址随机抽样,与设施的 incident_risk_per_mille 比较,小于它即为命中。
  • 命中后,再以 incident_max_severity_per_mille 为上界抽一次,决定状况损失。
  • 状况降到 250 或以下时设施变为 Damaged,否则变为 Degraded。

这两个概率都由场景数据写在 FacilityAsset 上,命令永远不能提供抽样结果。ProductionIncidentTransitionV1 凭证保存抽样结果和来源记录版本,所以恢复和重放时,改动过的抽样会被拒绝。

设施降级后如何处理,由一张决策票据决定:degraded_facility_decision_ticket 构建带 ContinueDegraded、StopForRepair 和 DeferOrder 三个选项的票据。

生产扩展为每个根记录保存一条 canwu.production 中的 ProductionRuntimeRecord:主根记录 canwu.production:runtime,以及把生产地点按范围(比如按县)划分的上层应用为每个范围安装的一条范围根记录 canwu.production:runtime:scope:{scope};每条都是完整的 ProductionState,带自己的 limits 和 scope,随场景一起安装(运行中不能新增范围)。每个数据包都写明自己的根(ProductionCommandEnvelope.scope、批次信封、完成步骤、见证、确认和唤醒上的 scope 成员,以及带 _in_scope 的排队辅助函数);生命周期系统只加载、结算、写入和记录本边界数据包点名的根,再写一条小小的边界活动记录(canwu.production:boundary-activity),告诉后面的系统哪些根的生产生命周期变了、由哪个数据包引起,所以没有生产工作的边界只读一条小记录,64,000 个生产地点的世界结算一个范围的批次和 4,096 个生产地点的世界一样快。

各根之间不共用任何标识:范围根的租约和产出操作键以 {scope}/ 开头,主根的键不含 /,范围 ID 也不含 /,所以两个根永远不会指向同一个资源侧的键;违反这条规则的键是记录在案的拒绝。apply_production_batch_v1 在一条带请求标识的命令里,对同一个根结算同一持有人的许多操作,逐项结算、每项各有结果;expected_runtime_revision 过期、或各项会让根超出 max_operations_per_boundary 的批次,整批带追踪记录被拒绝,可以重新发送;没有写预期修订号的批次,每一项都按它前面各项留下的根状态各自检查。根结算过的东西没有一样会永久留下:已结束的工单、已结束的租约和无人认领的结果作为残留归档(ProductionResidueArchiveRecordV1,三种新的归档键),更早边界的事故凭证折叠进根的事故历史,持有人和地点相同的更新观察头可用之后,被挤出窗口的旧观察头就被清理。被生产插件拒绝的完成步骤、见证、归档提交或确认,不论谁安排的,都带追踪记录被拒绝,被拒绝的数据包不抽取事故、也不发布报告。

资源根状态则分成几部分保存:活动根记录 resource:runtime:v1(ResourceRuntimeRecord,载荷为 ResourceLiveRootV1)保存未关闭的需求及其预留和分配腿、进行中的转移、租约、报告授权和观察头,其余的只在两本账里计数;每条常驻终态记录是某条常驻终态桶 resource:hot-terminal:{bucket}(ResourceHotTerminalBucketV1,每桶约两条)中的一个条目,每个账户、动用授权、资源定义、单位版本和保管人索引条目(ResourceCustodianAccountsV1,某个持有人名下的全部账户)是某条活动桶 resource:live:{bucket}(ResourceLiveBucketV1,每桶约四条)中的一个条目,都由其标识的哈希决定落在哪个桶。两类桶共用一种记录种类 canwu.resource bucket(ResourceBucketRecord)。自 0.20.0 起,每个常驻终态桶以仅凭身份的形式(canwu_identity_evidence_dependencies)声明其记录的精确证据,活动根声明它持有的完成租约、外部参与者和观察头所引用的版本,生产根声明尚无产出结果的生产执行的待处理产出来源,所以封存或接续检查点会保留常驻记录所引用版本的证明,即使该记录之后已更新;在完成租约进行中写出的接续检查点因此可以加载、校验并精确继续。资源状态格式为 6,两类桶的格式为 2。ResourceState::into_records 把状态写成这些记录,from_records 读回来,并检查每个账户都在其保管人的索引中、每条消费都指向它记录的那条履约(ResourceConsumption::fulfillment)。一个边界只读写活动根和它触及的记录所在的桶:资源插件在结算每个数据包之前,先挂载该数据包点名的每条常驻终态记录和活动记录(分配腿的账户、需求的来源账户和动用授权、账户的资源定义和单位)以及这些记录引用的记录;结算一次操作写的记录与账户数无关,池化需求的分配会挂载每一个账户,与资源插件无关的边界完全不读资源根。你自己的插件要读资源根时,用 resource_root_in_view(view, keys) 或 ResourceRootReader 点名需要的常驻终态记录,用 attach_live(keys) 或 attach_custodian_accounts(holder) 点名活动记录,或者用 whole_resource_root_in_view 读全部桶,并在活动根的读取声明之外再声明 resource_hot_terminal_state_key() 的读取;点名一条消费的读取方通过闭包直接得到它的履约,不必搜索。上层应用用 resource_state 读整个根。已结束的租约立即离开资源根(它的凭证留到归档提交取走为止,它的标识仍不能再开第二份租约),所以 resource_completion_status 不再找得到它;上层应用用 enqueue_resource_completion_expiry_tick 让授权过期,enqueue_resource_completion_operation 会拒绝上层应用发来的 Expire,因为上层应用无法知道哪个边界会准入它。快照把这些记录和内核状态存放在一起。恢复和重放请使用包装函数,它们会拒绝伪造的摘要、守恒失败、修订号不符和缺失的精确证据:

  • 资源:from_resource_snapshot_json、from_resource_checkpoint_journal、replay_resource_from_journal,用其他方式加载后调用 validate_resource_runtime;
  • 生产:存在归档状态时,使用 from_production_snapshot_json_with_archives、from_production_checkpoint_journal_with_archives、replay_production_from_journal_with_archives 和 validate_production_runtime_with_archives。

带 _with_archive_store 和 _with_archives 的恢复函数以 Arc<dyn … + Send + Sync> 接收每个存储并挂到运行上:运行和它的派生分支共用这个存储,分支可以移到别的线程,所以请把被持有的存储的映射放在 Mutex 或 RwLock 后面。只借给某一次调用的存储(例如传给 prepare_resource_archive 的)按引用传入,不需要这个约束。

精确重放读取已记录的命令和输入,所以排入它们的上层应用代码和决策控制者都不会再次运行。详见存档、重放与派生分支。

完成的工作会在资源根里留下终态记录:操作结果、已关闭的需求和预留、已结算的转移,以及各种凭证。它们占用归档候选预算,每次恢复都要校验它们,所以上层应用要把终态记录移到按内容寻址的归档里,每次做一次归档提交:

  1. ResourceState::prepare_resource_archive(candidate_limit, store) 选出热终态记录,并准备好对应的归档对象。它要读取归档存储来续写查找索引,所以要传入你用来归档的存储。模拟运行的第一次提交还没有索引可读,这一步传 () 就够了。
  2. enqueue_resource_archive 写入这些对象,通过批次的 store_and_verify 校验,然后把这次提交排入队列,由下一个边界应用。
  3. 提交结算之后,用 finalize_resource_archive_retention 确认。它从状态里的保留句柄读取提交是怎样结算的,所以在之后的提交把它的凭证归档之后也能确认。

生产扩展为自己的归档提供同样的协议:prepare_production_archive(candidate_limit, store)、enqueue_production_archive 和 finalize_production_archive_retention。从 0.16.0 起,生产归档在自己的命名空间下有同样的查找索引,一次提交最多带 16 个 blob、每个 64 条记录,已应用提交的维护凭证由下一次提交归档,所以根记录最多只留一条,一次模拟运行可以做任意多次提交;archived_production_terminal_record 查找已归档的记录。财政扩展和两个参考经济插件也按同样的方式归档。

  • 容量。 一次资源归档提交最多带 64 页、16,384 条记录;一次模拟运行可以做任意多次归档提交。提交只对准备它时的那个根生效,根由状态修订号、已准入序号、常驻终态记录数和归档头的摘要指明;请从整个根(resource_state)准备提交。
  • 查找。 每个资源操作都要问归档:自己的操作键以前用过没有。这样,即使操作结果已经移出记录,重发的操作也不会再次生效。每次提交都会续写一份累积的归档查找索引,这项检查只读归档头目录,再沿索引每层读一个经过认证的页。在记录的测量中,已归档约一千条记录时,一次未命中读 4 个对象,约一百万条时读 7 个,与提交次数无关。归档对象缺失或损坏属于错误,会让边界失败,存储能提供该对象后边界才会结算;它绝不会被当成“没有归档过”。
  • 顺序与容量。 同一个边界里,归档提交和保留确认(即 finalize_resource_archive_retention 排入的输入)先于资源插件的其他输入结算。归档提交不会因为空间不够而失败。如果别的提交先结算,使某次提交失效,这次提交什么也不改变,它的凭证在你确认之后移除;重复的确认结算为空操作。
  • 保留好查找页。 恢复、精确重放或派生分支会读取沿途每个归档头的查找索引,引擎从不删除查找页。每个准备好的批次都列出 superseded_lookup_pages,即新索引不再用到的页。只有当你放弃从这次提交之前的时间点恢复、重放和派生分支时才能删除它们,而且要等这次提交以 Applied 结算,并且已经有一份在它之后保存的持久快照或检查点。live_resource_archive_lookup_pages 和 live_production_archive_lookup_pages 可以重新算出实时运行仍会读取的页。

普通结算从不扫描冷历史。生产的恢复校验通过资源归档的查找索引解析每个已归档的资源投入,每个投入只读一条索引路径和一个 blob,并且只要资源归档有归档头就把它审计一次。

上层应用或持有人能发送的任何数据包都不会让资源边界失败。因可预期的原因无法结算的数据包什么也不改变,操作键保持空闲,由一个 canwu.resource.ingress_skipped.v1 事件(RESOURCE_INGRESS_SKIPPED_EVENT,用 ResourceIngressSkipV1::from_event 读回)指明它的输入 ID、携带的操作键和原因,边界带着其他数据包照常提交:

原因 含义 怎样才能结算
operation_key_taken 另一个请求持有这个键,在资源记录或归档中 无法结算:换一个键重发
reserved_operation_key 不是完成操作的请求使用了 resource:completion: 之下的键 无法结算:改用前缀之外的键
failed_settlement_checks 数据包无法解码,或未通过它的入口在结算时做的检查(带请求标识的命令、提供方绑定、凭证、边界时间、当前证据,或生产产出批次中被拒绝的一笔入账) 改正后的数据包,可以沿用原来的键
archive_backlog 结果放不进归档候选或热操作结果预算,而归档全部热候选的一次提交能腾出空间 提交应用之后重发同一请求
reserved_capacity 即使积压为空,结果也放不下:空间被已准入租约的预留或报告尚未发布的已完成参与方占着,或者预算本身小于这项工作(详情会说明何时永远腾不出空间) 等租约完成、释放或过期后重发同一请求,或调大 max_archive_candidates
root_limit 结果越过了资源记录的其他限制,实际上就是操作可以使用的状态字节上限份额(ResourceLimitsV1::operation_state_bytes,即上限减去 MAINTENANCE_STATE_BYTE_RESERVE) 资源记录变小之后重发同一请求

空间按结果计算,所以被拒绝的操作只需要它结果的空间,已应用的操作需要它新增记录和预留的空间。结果已经记录在案的重发什么也不结算,也不留事件。领域层面的拒绝(修订号过期、目标不存在、预算不允许的租约)仍然是该操作记录在案的拒绝结果。

其他插件安排的工作绝不会被交回上层应用,因为那个插件可能不会再发一次。生产产出批次,以及插件安排的、以后仍能作为同一步骤结算的完成步骤(Prepare、Abort、Release,以及外部参与方的准备、完成和释放),放不进资源记录、而一次归档提交或租约释放能腾出空间时,会被延后:资源插件把同样的工作作为内部的 resource_deferred_packet_v1 数据包排给自己,一分钟后到期,之后每次延后翻倍,最长 1,024 分钟,把批次的各笔入账改到那个时刻,并发出带 deferred_until 的跳过事件。等待积压或预留空间的工作会一直重试,直到结束短缺的提交或释放到来;等待字节上限的工作在 1,024 分钟的那次重试之后被跳过;任何提交或释放都腾不出空间的工作,以及绑定了边界时间的步骤(申请、授权、激活、消费、过期、一步式租约),立即被跳过。延后的工作等待期间输入队列不为空,所以运行不能封存;上层应用的归档维护结束这段等待。因空间不够被跳过的过期检查不再安排后续检查,下一次应用的归档提交会恢复它,enqueue_resource_completion_expiry_tick 也可以手动排入一次。

因空间不够、键被占用、或等待期间产出账户修订号变化而被跳过的批次,会让它的执行停在 CompletedPendingOutputSettlement;生产插件在执行开始时就拒绝会必然导致这种情况的键和产出集合。仍会让资源边界失败的只有无效记录或引擎不变量:查找键时归档对象缺失、伪造或无法读取,伪造的归档提交或保留确认,结算后因限制以外的原因无法通过校验的资源记录,一个边界内超出字节预留的维护写入,以及算术或编码失败。

生产插件遵循同样的规则:无法解码的数据包以 canwu.production.ingress_refused.v1 被拒绝(确认为 canwu.production.output_ack_refused.v1),操作数据包只有在由带请求标识的生产命令的准入排入时才结算,生产检查拒绝的完成步骤或接续见证什么也不结算,失效或重复的归档提交结算为空操作。仍会让生产边界失败的只有来自其他插件的伪造归档数据包、失败的存储读取、计数器溢出和根记录 256 MiB 的字节上限。

  • 内容:资源与单位版本;期初账户及其保管人、容量和 place_scope;底线政策;工艺版本;生产地点;设施及其事故风险。canwu-economy-reference-content 展示了一个内容包,其中的 ResourceCapabilityRevision 把矿点放在从 Potential 到 DeliveredAccepted 的某个 ResourceCapabilityStage 上。
  • 权限:谁可以创建账户,谁可以以哪个申请者的名义提交需求,谁可以签发动用授权。Pooled 或 ExactAccounts 来源策略只负责选择账户;要动用其他保管人的存量,需要动用授权和 Granted 策略。
  • 时机与顺序:何时为每个申请者排入一次分配,以及每项需求的优先级和平局决胜键。粮食循环示例从决策票据中取得优先级。
  • 证据种类:传给 ResourcePlugin::new 的记录种类、每个工艺版本的 realization_evidence_kinds,以及动用授权和流出所引用的权限记录。
  • 消费者:你的插件在 resource_consumption_intents 映射中批准消费意图,并对结果作出反应。
  • 运输:通过 canwu-transport 和 canwu-movement 管理行程与执行,由它们的证据验收到货。
  • 降级设施票据的控制者,以及货币、市场、价格、工资、战斗公式和客户端界面。
限制 取值
ResourceLimitsV1::canonical() 账户、需求、转移各 8,192 个(硬上限 262,144;池化需求的分配会读取每一个账户,每个约 30 微秒,所以规范账户数保持不变,要调高它的上层应用应改为动用精确账户或动用授权);资源定义和单位版本各 16,384 个;每次分配 2,048 个候选、2,048 个待处理需求(硬上限各 16,384);每个边界 32 个消费批次条目(max_batch_items_per_boundary,硬上限 2,048)、256 份报告;8,192 条热操作结果(硬上限 16,384);256 条热终态归档候选(硬上限 16,384;维护凭证预算不得小于它);状态 256 MiB,按活动根加全部常驻终态条目和活动条目计算
MAX_DEMAND_SOURCE_ACCOUNTS ExactAccounts 或 Granted 列表最多 256 个账户
MAX_RESOURCE_ACCESS_GRANTS 每个资源状态最多 16,384 份动用授权,已撤销的也计入
MAX_RESOURCE_TRANSFER_LINKS 一项转移的运输关联历史最多 16 条,其中在途的最多 14 条;再有中间推进会以 limit_exceeded 被拒绝,到达、退回或终态处置则总会被接受。每条运输关联让活动根中未结束的转移增加约 286 字节(只测量过一次,标识长 20 到 30 个字符)
MAX_RESOURCE_CONSUMPTION_BATCH_ITEMS 一个消费批次最多 256 项;每一项关闭其需求时留下六条常驻终态记录,批次本身再留两条
完成租约 每个生命周期 16 条终态凭证(MAX_COMPLETION_RECEIPTS_PER_LIFECYCLE);每个权限主体 16 个、全局 1,024 个待处理申请;始终未激活的授权在 8 个边界后过期(PREACTIVATION_LEASE_TTL_BOUNDARIES);已应用的一步式完成租约占用 2 + 16 个归档候选名额和 1 + 16 个热操作结果名额,被拒绝的只占它结果的名额
受保护存量底线 一条资源记录最多 4,096 个政策版本,预先编写的和按内容寻址的合计(MAX_PROTECTED_FLOOR_POLICY_REVISIONS);按内容寻址的版本最多 16 个覆盖类别,每个 1 到 160 字节
生产产出批次 同一份凭证下 1 到 64 笔入账
ProductionLimitsV1::canonical(),按根计算,由场景定下并对照 maximum() 校验 生产地点、设施、工单、生产执行和产能分配各 16,384 个(上限各 65,536);操作结果 32,768 条(上限 131,072);观察头、待处理条目和到期归档条目各 16,384;被挤出窗口的观察头 8,192(max_observation_rollover);每个边界记录 4,096 次操作和批次项(max_operations_per_boundary,上限 65,536),每批 1,024 项(max_batch_items,上限 4,096,不得超过操作上限);每个边界 512 次事故检查、64 份报告;状态 256 MiB。除了按整个世界计数的 max_observation_records 和 max_observation_records_per_holder,每个上限都属于各根自己,多个根加起来就是世界的预算
工艺与报告规模 64 个要求组(MAX_REQUIREMENT_GROUPS),每组 16 个备选(MAX_REQUIREMENT_ALTERNATIVES),每份报告 256 条事实和阻塞项(MAX_REPORT_FACTS)
投影 8 个来源适配器(MAX_TYPED_SOURCE_ADAPTERS),256 条观察事实(MAX_OBSERVATION_FACTS),32 个价格因素(MAX_PRICE_FACTORS)

触及上限时返回错误、被拒绝的结果或跳过的数据包,存量不会改变。每份租约都在任何扣减发生之前预留好自己的终态记录名额,所以热归档已满时,已被接受的工作仍能完成;下一份租约(四步或一步式)会以 reserved_capacity 被跳过,直到租约释放空间或调大 max_archive_candidates。

最需要留意的是归档候选预算 max_archive_candidates。结果放不下的操作什么也不结算,由带 archive_backlog(归档提交能腾出空间)或 reserved_capacity(腾不出)的跳过事件指明;它不会被记录为拒绝,操作键保持空闲,空间恢复后由发送方重发。遇到 archive_backlog 时,请排入一次选中全部候选的提交,也可以就在同一个边界里,因为归档提交先结算;从同一份记录准备的第二次提交,会在第一次提交应用后失效。一次提交最多腾出“预算减一”条记录的空间,因为它自己的维护凭证仍占一个候选名额,超出的请求会再次被跳过,排在下一次提交之后结算。被跳过的请求不会被排入任何队列,所以长时间的短缺只会产生事件和重发,绝不会让运行停下。

16,384 这个上限是为突发留的余量,不是日常的工作规模。结算操作的边界不管积压多少都花同样的代价(记录的测量中,0 条和 4,000 条常驻记录时都约 0.2 毫秒、8 KB 日志),因为它只写活动根和它触及的桶。积压的代价在归档时支付:应用提交的边界每归档一条记录就要重写一个桶,每条约 25 微秒、1.3 KB 日志(4,054 条 103 毫秒,16,054 条 524 毫秒),准备提交要读全部常驻记录,恢复要校验它们。积压很大时请分批提交,每批确认之后再准备下一批;另外,一次 64 页的提交如果失效,会在活动根里留下约 13 KB 的保留句柄,所以接近字节上限的根大约只放得下五次未确认的这种失效提交。

终端窗口
cargo run -p canwu-economy-reference --example grain_loop
cargo run -p canwu-resource --example resource_lifecycle
cargo run -p canwu-resource --example completion_lease
cargo run -p canwu-production --example process_constraints
cargo test -p canwu-resource --test resource_contract
cargo test -p canwu-production --test g3_contract
cargo test -p canwu-force-supply-reference --test g4_contract
cargo test -p canwu-economy-reference --test grain_harness

grain_loop 以 JSON 输出 GrainLoopSummary;由于没有转移还停留在托管中,其中的 conservation_closing 等于 final_stock(2,206)。粮食、生产与军事补给案例逐月讲解了这个示例。resource_lifecycle 不启动引擎,直接构建一个 ResourceState 并为一项需求分配;completion_lease 最后输出 lease state: Activated;process_constraints 输出一个机器工艺的三个阻塞项(ToolsMachines、Energy 和 Maintenance)。

resource_contract 测试覆盖守恒、受保护存量底线、转移托管和租约。g3_contract 测试覆盖产能重叠、只在确认之后才结算的产出、在第 10 阶段暂存并在第 11 阶段提交的事故,以及降级设施票据。g4_contract 中的 requisition_keeps_resource_force_externality_and_ack_as_distinct_steps 完整走一遍征发 saga,grain_harness 中的 snapshot_checkpoint_journal_and_fork_continue_identically 检查快照、检查点日志和派生分支得到相同的检查点哈希。