Skip to main content

ferrum_interfaces/vnext/admission/
checkpoint.rs

1//! Coordinator-owned logical capacity for immutable state copies. Physical
2//! backing and plan lifetime checks remain the resource layer's responsibility.
3
4use super::*;
5
6/// Process-local identity of one independent checkpoint capacity claim.
7/// Identifiers are issued only by the owning coordinator and are never reused.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
9pub struct CheckpointAuthorityId {
10    coordinator_id: LogicalAdmissionCoordinatorId,
11    serial: u64,
12}
13
14impl CheckpointAuthorityId {
15    pub const fn coordinator_id(self) -> LogicalAdmissionCoordinatorId {
16        self.coordinator_id
17    }
18
19    pub const fn serial(self) -> u64 {
20        self.serial
21    }
22}
23
24#[derive(Debug)]
25pub enum CheckpointCapacityClaimDecision {
26    Claimed(LogicalCheckpointLease),
27    Skipped(CheckpointRetentionSkipReason),
28    Deferred(AdmissionDeferred),
29    PermanentRejected(AdmissionRejected),
30}
31
32/// Optional retention policy decisions, independent of device pressure or
33/// ordinary capacity-domain availability. A skipped capture does not wait.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum CheckpointRetentionSkipReason {
36    Disabled,
37    Capacity {
38        requested_bytes: u64,
39        retained_bytes: u64,
40        maximum_bytes: u64,
41    },
42}
43
44#[derive(Debug, PartialEq, Eq)]
45struct CheckpointClaimRecord {
46    claims: CapacityVector,
47    retained_bytes: u64,
48}
49
50#[derive(Debug)]
51pub(super) struct CheckpointClaimLedger {
52    next_serial: u64,
53    live: BTreeMap<CheckpointAuthorityId, CheckpointClaimRecord>,
54    capacity: Option<CheckpointCapacityPolicy>,
55    retained_bytes: u64,
56    closed: bool,
57}
58
59impl CheckpointClaimLedger {
60    pub(super) fn new(capacity: Option<CheckpointCapacityPolicy>) -> Self {
61        Self {
62            next_serial: 1,
63            live: BTreeMap::new(),
64            capacity,
65            retained_bytes: 0,
66            closed: false,
67        }
68    }
69
70    pub(super) fn count(&self) -> u64 {
71        // The supported Rust targets have at most 64-bit address spaces.
72        self.live.len() as u64
73    }
74}
75
76/// A checkpoint's independent domain claim. This lease has no request or
77/// sequence parent and confers no device allocation or execution authority.
78///
79/// The resource owner must release its physical extents before this logical
80/// lease. A completion fence may retain that owner after cache eviction.
81#[derive(Debug)]
82#[must_use = "checkpoint capacity remains charged until its owner is released"]
83pub struct LogicalCheckpointLease {
84    inner: Arc<CoordinatorInner>,
85    authority: CheckpointAuthorityId,
86    claims: CapacityVector,
87    retained_bytes: u64,
88    released: bool,
89}
90
91impl LogicalAdmissionCoordinator {
92    /// Aligned bytes reserved or retained by every live checkpoint lease,
93    /// including owners outside the cache index. This is a limit within pool
94    /// residency, not an additional DeviceCapacityBudget charge.
95    pub fn checkpoint_retained_bytes(&self) -> Result<u64, VNextError> {
96        Ok(self.inner.lock_state()?.checkpoint_claims.retained_bytes)
97    }
98
99    /// Claims plan-derived checkpoint demand in the existing domain ledger.
100    /// The caller must hold the plan lifecycle guard and commit this together
101    /// with prepared physical backing. This is not a product allocation API.
102    pub(crate) fn try_claim_checkpoint(
103        &self,
104        demand: &AdmissionDemand,
105        retained_bytes: u64,
106    ) -> Result<CheckpointCapacityClaimDecision, VNextError> {
107        if demand.immediate_claim.is_empty() {
108            return Err(invalid_admission(
109                "checkpoint claim requires non-empty demand",
110            ));
111        }
112        // Resource domains are denominated in their actual aligned physical
113        // bytes. Never accept a caller's smaller fee for those same claims.
114        let claimed_bytes = demand
115            .immediate_claim
116            .entries()
117            .iter()
118            .try_fold(0_u64, |sum, claim| sum.checked_add(claim.units.get()))
119            .ok_or_else(|| {
120                admission_fault(
121                    DynamicAdmissionFaultKind::ArithmeticOverflow,
122                    "checkpoint domain byte sum overflows u64",
123                )
124            })?;
125        if retained_bytes == 0 || retained_bytes != claimed_bytes {
126            return Err(invalid_admission(
127                "checkpoint retention fee must equal the complete aligned domain claims",
128            ));
129        }
130        let mut state = self.inner.lock_mutation()?;
131        if state.poisoned {
132            return Err(admission_fault(
133                DynamicAdmissionFaultKind::Poisoned,
134                "coordinator is fail-closed",
135            ));
136        }
137        if state.checkpoint_claims.closed {
138            return Err(invalid_admission("checkpoint admission is closed"));
139        }
140        let Some(policy) = state.checkpoint_claims.capacity else {
141            return Ok(CheckpointCapacityClaimDecision::Skipped(
142                CheckpointRetentionSkipReason::Disabled,
143            ));
144        };
145        let evaluation = evaluate_demand(&state, demand)?;
146        if !evaluation.permanent.is_empty() {
147            return Ok(CheckpointCapacityClaimDecision::PermanentRejected(
148                AdmissionRejected {
149                    immediate_requested: demand.immediate_claim.clone(),
150                    fit_requested: demand.fit_requirement.clone(),
151                    maximum: state.snapshot(self.id()),
152                    blockers: evaluation.permanent,
153                },
154            ));
155        }
156        if !evaluation.blockers.is_empty() {
157            let action = deferred_action(demand, evaluation.growth_required);
158            let wait_condition =
159                state.wait_condition_for_blockers(self.id(), &evaluation.blockers)?;
160            let snapshot = state.snapshot(self.id());
161            return Ok(CheckpointCapacityClaimDecision::Deferred(
162                AdmissionDeferred {
163                    immediate_requested: demand.immediate_claim.clone(),
164                    fit_requested: demand.fit_requirement.clone(),
165                    release_epoch: snapshot.release_epoch,
166                    capacity_epoch: snapshot.capacity_epoch,
167                    available: snapshot,
168                    blockers: evaluation.blockers,
169                    action,
170                    wait_condition,
171                },
172            ));
173        }
174
175        let current_retained = state.checkpoint_claims.retained_bytes;
176        let maximum_bytes = policy.maximum_retained_bytes();
177        let Some(remaining) = maximum_bytes.checked_sub(current_retained) else {
178            state.poisoned = true;
179            self.inner.epoch_tx.send_replace(state.epochs(self.id()));
180            return Err(admission_fault(
181                DynamicAdmissionFaultKind::Poisoned,
182                "checkpoint retained capacity exceeds its immutable policy",
183            ));
184        };
185        if retained_bytes > remaining {
186            return Ok(CheckpointCapacityClaimDecision::Skipped(
187                CheckpointRetentionSkipReason::Capacity {
188                    requested_bytes: retained_bytes,
189                    retained_bytes: current_retained,
190                    maximum_bytes,
191                },
192            ));
193        }
194        let next_retained = current_retained
195            .checked_add(retained_bytes)
196            .ok_or_else(|| {
197                admission_fault(
198                    DynamicAdmissionFaultKind::ArithmeticOverflow,
199                    "checkpoint retained byte usage overflows u64",
200                )
201            })?;
202        let serial = state.checkpoint_claims.next_serial;
203        let next_serial = serial
204            .checked_add(1)
205            .filter(|_| serial != 0)
206            .ok_or_else(|| {
207                admission_fault(
208                    DynamicAdmissionFaultKind::AuthorityExhausted,
209                    "checkpoint authority serial is exhausted",
210                )
211            })?;
212        let authority = CheckpointAuthorityId {
213            coordinator_id: self.id(),
214            serial,
215        };
216        if state.checkpoint_claims.live.contains_key(&authority) {
217            return Err(invalid_admission("checkpoint authority is already live"));
218        }
219        state
220            .release_epoch
221            .checked_add(u64::from(state.active_requests))
222            .and_then(|epoch| epoch.checked_add(u64::from(state.active_sequences)))
223            .and_then(|epoch| epoch.checked_add(state.active_child_claims))
224            .and_then(|epoch| epoch.checked_add(state.checkpoint_claims.count()))
225            .and_then(|epoch| epoch.checked_add(1))
226            .ok_or_else(|| {
227                admission_fault(
228                    DynamicAdmissionFaultKind::EpochExhausted,
229                    "release epoch cannot represent every outstanding lease release",
230                )
231            })?;
232        let mut next_usage = Vec::with_capacity(demand.immediate_claim.entries().len());
233        for entry in demand.immediate_claim.entries() {
234            let domain = state
235                .domains
236                .get(&entry.domain)
237                .expect("checkpoint demand domains were validated");
238            let used = domain.used.checked_add(entry.units.get()).ok_or_else(|| {
239                admission_fault(
240                    DynamicAdmissionFaultKind::ArithmeticOverflow,
241                    "checkpoint capacity usage overflows u64",
242                )
243            })?;
244            if domain.availability_epoch == u64::MAX {
245                return Err(admission_fault(
246                    DynamicAdmissionFaultKind::EpochExhausted,
247                    "checkpoint domain cannot publish its eventual release",
248                ));
249            }
250            next_usage.push((entry.domain, used));
251        }
252        let claims = demand.immediate_claim.clone();
253        // Prepare both owned vectors before the first ledger mutation.
254        let recorded_claims = CheckpointClaimRecord {
255            claims: claims.clone(),
256            retained_bytes,
257        };
258        state
259            .checkpoint_claims
260            .live
261            .insert(authority, recorded_claims);
262        state.checkpoint_claims.next_serial = next_serial;
263        state.checkpoint_claims.retained_bytes = next_retained;
264        for (domain, used) in next_usage {
265            state
266                .domains
267                .get_mut(&domain)
268                .expect("validated domain")
269                .used = used;
270        }
271        Ok(CheckpointCapacityClaimDecision::Claimed(
272            LogicalCheckpointLease {
273                inner: Arc::clone(&self.inner),
274                authority,
275                claims,
276                retained_bytes,
277                released: false,
278            },
279        ))
280    }
281
282    pub(crate) fn owns_checkpoint_claim(&self, lease: &LogicalCheckpointLease) -> bool {
283        !lease.released
284            && lease.authority.coordinator_id == self.id()
285            && Arc::ptr_eq(&self.inner, &lease.inner)
286    }
287
288    /// Prevents new checkpoint claims without invalidating outstanding owners.
289    /// Resource-layer plan shutdown must call this under its lifecycle gate.
290    pub(crate) fn close_checkpoint_admission(&self) -> Result<(), VNextError> {
291        let mut state = self.inner.lock_mutation()?;
292        if state.poisoned {
293            return Err(admission_fault(
294                DynamicAdmissionFaultKind::Poisoned,
295                "coordinator is fail-closed",
296            ));
297        }
298        state.checkpoint_claims.closed = true;
299        Ok(())
300    }
301}
302
303impl LogicalCheckpointLease {
304    pub fn coordinator_id(&self) -> LogicalAdmissionCoordinatorId {
305        self.inner.id
306    }
307
308    pub const fn authority(&self) -> CheckpointAuthorityId {
309        self.authority
310    }
311
312    pub fn claims(&self) -> &CapacityVector {
313        &self.claims
314    }
315
316    /// Independently retained aligned extents, not another device allocation.
317    pub const fn retained_bytes(&self) -> u64 {
318        self.retained_bytes
319    }
320
321    fn release_inner(&mut self) -> bool {
322        if self.released {
323            return true;
324        }
325        let mut state = match self.inner.lock_mutation() {
326            Ok(state) => state,
327            Err(_) => return false,
328        };
329        let valid = !state.poisoned
330            && self.authority.coordinator_id == self.inner.id
331            && state
332                .checkpoint_claims
333                .live
334                .get(&self.authority)
335                .is_some_and(|record| {
336                    record.claims == self.claims && record.retained_bytes == self.retained_bytes
337                })
338            && self.retained_bytes > 0
339            && self
340                .claims
341                .entries()
342                .iter()
343                .try_fold(0_u64, |sum, claim| sum.checked_add(claim.units.get()))
344                == Some(self.retained_bytes)
345            && state.checkpoint_claims.retained_bytes >= self.retained_bytes
346            && state.checkpoint_claims.capacity.is_some_and(|policy| {
347                state.checkpoint_claims.retained_bytes <= policy.maximum_retained_bytes()
348            })
349            && self.claims.entries().iter().all(|claim| {
350                state.domains.get(&claim.domain).is_some_and(|domain| {
351                    domain.used >= claim.units.get() && domain.availability_epoch < u64::MAX
352                })
353            });
354        let next_release_epoch = state.release_epoch.checked_add(1);
355        if !valid || next_release_epoch.is_none() {
356            state.poisoned = true;
357            self.inner
358                .epoch_tx
359                .send_replace(state.epochs(self.inner.id));
360            return false;
361        }
362        for claim in self.claims.entries() {
363            let domain = state
364                .domains
365                .get_mut(&claim.domain)
366                .expect("validated domain");
367            domain.used -= claim.units.get();
368            domain.availability_epoch += 1;
369        }
370        state.checkpoint_claims.live.remove(&self.authority);
371        state.checkpoint_claims.retained_bytes -= self.retained_bytes;
372        state.release_epoch = next_release_epoch.expect("validated release epoch");
373        // Logical availability is published here; this says nothing about
374        // device residency or completion of physical backing release.
375        self.inner
376            .epoch_tx
377            .send_replace(state.epochs(self.inner.id));
378        drop(state);
379        self.released = true;
380        true
381    }
382}
383
384impl Drop for LogicalCheckpointLease {
385    fn drop(&mut self) {
386        let _ = self.release_inner();
387    }
388}
389
390#[cfg(test)]
391mod tests;