ferrum_interfaces/vnext/admission/
checkpoint.rs1use super::*;
5
6#[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#[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 self.live.len() as u64
73 }
74}
75
76#[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 pub fn checkpoint_retained_bytes(&self) -> Result<u64, VNextError> {
96 Ok(self.inner.lock_state()?.checkpoint_claims.retained_bytes)
97 }
98
99 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 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 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 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 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 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;