ferrum_interfaces/vnext/execution/
checkpoint_capacity.rs1use std::num::NonZeroU64;
8
9use super::{
10 invalid_plan, AllocationKind, AllocationLifetime, BTreeMap, BTreeSet, BufferUsage, Deserialize,
11 DynamicBackingPoolId, DynamicResourceDescriptor, MemoryPlan, PlanNode, ResourceId,
12 SequenceCheckpointLayout, Serialize, VNextError,
13};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(deny_unknown_fields)]
21pub struct CheckpointCapacityPolicy {
22 maximum_retained_bytes: NonZeroU64,
23}
24
25impl CheckpointCapacityPolicy {
26 pub fn new(maximum_retained_bytes: u64) -> Result<Self, VNextError> {
27 Ok(Self {
28 maximum_retained_bytes: NonZeroU64::new(maximum_retained_bytes)
29 .ok_or_else(|| invalid_plan("checkpoint capacity must be non-zero"))?,
30 })
31 }
32
33 pub const fn maximum_retained_bytes(&self) -> u64 {
34 self.maximum_retained_bytes.get()
35 }
36
37 fn pool_ceiling(&self, descriptor: &DynamicResourceDescriptor) -> u64 {
38 let quantum = descriptor.physical_allocation_quantum_bytes();
39 self.maximum_retained_bytes() / quantum * quantum
41 }
42}
43
44pub(super) fn derive_checkpoint_growth_ceilings(
48 policy: Option<&CheckpointCapacityPolicy>,
49 layout: Option<&SequenceCheckpointLayout>,
50 descriptors: &[DynamicResourceDescriptor],
51) -> Result<BTreeMap<DynamicBackingPoolId, u64>, VNextError> {
52 let (Some(policy), Some(layout)) = (policy, layout) else {
53 return Ok(BTreeMap::new());
54 };
55 let descriptors = descriptors
56 .iter()
57 .map(|descriptor| (descriptor.base_resource_id(), descriptor))
58 .collect::<BTreeMap<_, _>>();
59 let resources = layout
60 .states()
61 .iter()
62 .map(|state| state.resource_id())
63 .collect::<BTreeSet<_>>();
64 let mut ceilings = BTreeMap::new();
65 for resource in resources {
66 let descriptor = descriptors
67 .get(resource)
68 .ok_or_else(|| invalid_plan("checkpoint state has no memory descriptor"))?;
69 if descriptor.lifetime() != AllocationLifetime::Sequence
70 || descriptor.usage() != BufferUsage::State
71 || *descriptor.kind() != AllocationKind::Value
72 {
73 return Err(invalid_plan(
74 "checkpoint growth requires certified Sequence state value storage",
75 ));
76 }
77 let ceiling = policy.pool_ceiling(descriptor);
78 if ceiling != 0 {
79 if ceilings
80 .insert(descriptor.pool_id().clone(), ceiling)
81 .is_some_and(|previous| previous != ceiling)
82 {
83 return Err(invalid_plan(
84 "checkpoint resources disagree on their physical pool quantum",
85 ));
86 }
87 }
88 }
89 Ok(ceilings)
90}
91
92impl MemoryPlan {
93 pub(super) fn with_checkpoint_capacity(
96 mut self,
97 policy: Option<CheckpointCapacityPolicy>,
98 layout: Option<&SequenceCheckpointLayout>,
99 nodes: &[PlanNode],
100 retained_completion_resources: &BTreeSet<ResourceId>,
101 ) -> Result<Self, VNextError> {
102 if policy.is_none() && self.checkpoint_capacity.is_none() {
103 return Ok(self);
104 }
105 let dynamic_capacity = self
106 .usable_capacity_bytes
107 .checked_sub(self.static_bytes)
108 .ok_or_else(|| invalid_plan("static memory exceeds usable capacity"))?;
109 let ceilings =
110 derive_checkpoint_growth_ceilings(policy.as_ref(), layout, &self.dynamic_descriptors)?;
111 let reusable = self
112 .reusable_execution
113 .as_ref()
114 .map(|plan| plan.pool_workspace_ceilings())
115 .transpose()?
116 .unwrap_or_default();
117 self.dynamic_pools = Self::derive_dynamic_pools_with_checkpoint(
118 &self.dynamic_descriptors,
119 nodes,
120 dynamic_capacity,
121 &reusable,
122 retained_completion_resources,
123 &ceilings,
124 )?;
125 self.checkpoint_capacity = policy;
126 self.validate()?;
127 Ok(self)
128 }
129}
130
131pub(super) fn validate_checkpoint_pool_ceiling(
135 policy: Option<&CheckpointCapacityPolicy>,
136 members: &[&DynamicResourceDescriptor],
137 ceiling: u64,
138) -> Result<(), VNextError> {
139 if ceiling == 0 {
140 return Ok(());
141 }
142 let policy =
143 policy.ok_or_else(|| invalid_plan("checkpoint pool growth has no capacity policy"))?;
144 if !members.iter().any(|descriptor| {
145 descriptor.lifetime() == AllocationLifetime::Sequence
146 && descriptor.usage() == BufferUsage::State
147 && *descriptor.kind() == AllocationKind::Value
148 && policy.pool_ceiling(descriptor) == ceiling
149 }) {
150 return Err(invalid_plan(
151 "checkpoint pool growth differs from its State allocation quantum or cap",
152 ));
153 }
154 Ok(())
155}