1use super::dynamic_pool::{DynamicDeviceCapacityBlocked, DynamicPoolGrowthIntent};
2use super::{
3 invalid_resource, AdmissionDeferred, CapacityEpochs, CapacityVector, CapacityWaitCondition,
4 DeviceRuntime, DynamicBackingBlocker, DynamicBackingDeferred, DynamicBackingPackingEnvelope,
5 DynamicBackingPoolId, DynamicChunkQuarantineReason, DynamicPoolGrowthBatchReceipt,
6 DynamicPoolGrowthReceipt, DynamicPoolGrowthRequest, DynamicPoolMaintenanceBoundaryReceipt,
7 DynamicPoolSet, DynamicPoolStatus, VNextError,
8};
9use crate::vnext::{
10 CapacityShortfallKind, CapacityWaitSnapshot, DeferredAction, DynamicBackingPressure,
11 DynamicPoolResidentPressure,
12};
13use serde::Serialize;
14use std::collections::BTreeMap;
15use std::sync::Arc;
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
18pub struct DynamicPoolMaintenanceStatus {
19 epochs: CapacityEpochs,
20 maximum_active_sequences: u32,
21 device_capacity_bytes: u64,
22 effective_device_usable_ceiling_bytes: u64,
23 process_claimed_bytes: u64,
24 budget_device_wide_usable_ceiling_bytes: u64,
25 budget_claimed_bytes: u64,
26 pools: Vec<DynamicPoolStatus>,
27}
28
29impl DynamicPoolMaintenanceStatus {
30 pub const fn epochs(&self) -> CapacityEpochs {
31 self.epochs
32 }
33
34 pub const fn maximum_active_sequences(&self) -> u32 {
35 self.maximum_active_sequences
36 }
37
38 pub const fn device_capacity_bytes(&self) -> u64 {
39 self.device_capacity_bytes
40 }
41
42 pub const fn effective_device_usable_ceiling_bytes(&self) -> u64 {
43 self.effective_device_usable_ceiling_bytes
44 }
45
46 pub const fn process_claimed_bytes(&self) -> u64 {
47 self.process_claimed_bytes
48 }
49
50 pub const fn budget_device_wide_usable_ceiling_bytes(&self) -> u64 {
51 self.budget_device_wide_usable_ceiling_bytes
52 }
53
54 pub const fn budget_claimed_bytes(&self) -> u64 {
55 self.budget_claimed_bytes
56 }
57
58 pub fn pools(&self) -> &[DynamicPoolStatus] {
59 &self.pools
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
64pub enum DynamicDeferredMaintenanceOutcome {
65 RetryAdmission {
66 current_epochs: CapacityEpochs,
67 },
68 WaitForRelease {
69 current_epochs: CapacityEpochs,
70 wait_condition: CapacityWaitCondition,
71 pressure: DynamicBackingPressure,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 maintenance_boundary: Option<DynamicPoolMaintenanceBoundaryReceipt>,
74 },
75 Maintained(DynamicPoolGrowthBatchReceipt),
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
79pub struct DynamicPoolQuarantineRelease {
80 pool_id: DynamicBackingPoolId,
81 released_chunks: usize,
82 released_bytes: u64,
83}
84
85impl DynamicPoolQuarantineRelease {
86 pub fn pool_id(&self) -> &DynamicBackingPoolId {
87 &self.pool_id
88 }
89
90 pub const fn released_chunks(&self) -> usize {
91 self.released_chunks
92 }
93
94 pub const fn released_bytes(&self) -> u64 {
95 self.released_bytes
96 }
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
100pub struct DynamicPoolQuarantineReleaseReceipt {
101 pools: Vec<DynamicPoolQuarantineRelease>,
102 released_chunks: usize,
103 released_bytes: u64,
104}
105
106impl DynamicPoolQuarantineReleaseReceipt {
107 pub fn pools(&self) -> &[DynamicPoolQuarantineRelease] {
108 &self.pools
109 }
110
111 pub const fn released_chunks(&self) -> usize {
112 self.released_chunks
113 }
114
115 pub const fn released_bytes(&self) -> u64 {
116 self.released_bytes
117 }
118}
119
120#[must_use = "dynamic pool maintenance controller must be retained by the plan owner"]
138pub struct DynamicPoolMaintenanceController<R>
139where
140 R: DeviceRuntime,
141{
142 pools: Arc<DynamicPoolSet<R>>,
143}
144
145impl<R> DynamicPoolMaintenanceController<R>
146where
147 R: DeviceRuntime,
148{
149 pub(in crate::vnext::resource) fn new(pools: Arc<DynamicPoolSet<R>>) -> Self {
150 Self { pools }
151 }
152
153 pub fn pool_ids(&self) -> impl ExactSizeIterator<Item = &DynamicBackingPoolId> {
154 self.pools.pools.keys()
155 }
156
157 pub fn status(&self) -> Result<DynamicPoolMaintenanceStatus, VNextError> {
161 let mut pools = Vec::with_capacity(self.pools.pools.len());
162 for pool in self.pools.pools.values() {
163 let mut state = pool
164 .state
165 .lock()
166 .map_err(|_| invalid_resource("dynamic backing pool is poisoned"))?;
167 let live_segments = state.chunks.values().try_fold(0_u64, |total, chunk| {
168 total
169 .checked_add(chunk.live_segments)
170 .ok_or_else(|| invalid_resource("dynamic live segment count overflows u64"))
171 })?;
172 let quarantined_bytes = state.quarantined.iter().try_fold(0_u64, |total, chunk| {
173 total
174 .checked_add(chunk.backing._grant.bytes())
175 .ok_or_else(|| invalid_resource("dynamic quarantine bytes overflow u64"))
176 })?;
177 let live_occupancy = state.live_occupancy;
178 let used_bytes = state
179 .resident_bytes
180 .checked_sub(state.allocator.free_bytes)
181 .ok_or_else(|| invalid_resource("dynamic pool free bytes exceed residency"))?;
182 if live_occupancy.total().physical_bytes() != used_bytes
183 || live_occupancy.total().segment_count() != live_segments
184 {
185 state.poisoned = true;
186 return Err(invalid_resource(
187 "dynamic pool live-claim ledger differs from allocator occupancy",
188 ));
189 }
190 pools.push(DynamicPoolStatus {
191 pool_id: pool.domain.pool_id().clone(),
192 domain_id: pool.domain.domain_id,
193 contract: super::DynamicPoolContractStatus::from_domain(&pool.domain),
194 storage_profile: pool.domain.pool.compatibility().profile(),
195 resident_bytes: state.resident_bytes,
196 pending_growth_bytes: state.pending_growth_bytes,
197 free_bytes: state.allocator.free_bytes,
198 largest_contiguous_bytes: state.allocator.largest_contiguous_bytes(),
199 resident_chunks: state.chunks.len(),
200 live_segments,
201 live_occupancy,
202 quarantined_chunks: state.quarantined.len(),
203 quarantined_bytes,
204 descriptor_mismatch_chunks: state
205 .quarantined
206 .iter()
207 .filter(|chunk| {
208 chunk.reason == DynamicChunkQuarantineReason::DescriptorMismatch
209 })
210 .count(),
211 publication_rejected_chunks: state
212 .quarantined
213 .iter()
214 .filter(|chunk| {
215 chunk.reason == DynamicChunkQuarantineReason::PublicationRejected
216 })
217 .count(),
218 poisoned: state.poisoned,
219 });
220 }
221 let account = &self.pools.budget.account;
222 let state = account
223 .state
224 .lock()
225 .map_err(|_| invalid_resource("device capacity account is poisoned"))?;
226 let effective_device_usable_ceiling_bytes = state
227 .budgets
228 .values()
229 .map(|budget| budget.device_wide_usable_ceiling_bytes)
230 .min()
231 .ok_or_else(|| invalid_resource("device capacity account has no live budget"))?;
232 let budget_claimed_bytes = state
233 .budgets
234 .get(&self.pools.budget.budget_id)
235 .ok_or_else(|| invalid_resource("dynamic pool plan budget is stale"))?
236 .claimed_bytes;
237 Ok(DynamicPoolMaintenanceStatus {
238 epochs: self.pools.logical_admission.epochs()?,
239 maximum_active_sequences: self.pools.maximum_active_sequences(),
240 device_capacity_bytes: account.device_capacity_bytes,
241 effective_device_usable_ceiling_bytes,
242 process_claimed_bytes: state.claimed_bytes,
243 budget_device_wide_usable_ceiling_bytes: self
244 .pools
245 .budget
246 .device_wide_usable_ceiling_bytes,
247 budget_claimed_bytes,
248 pools,
249 })
250 }
251
252 pub fn initialize_pool(
255 &self,
256 pool_id: &DynamicBackingPoolId,
257 ) -> Result<Option<DynamicPoolGrowthReceipt>, VNextError> {
258 let mut receipt = self
259 .pools
260 .maintain_pools(vec![DynamicPoolGrowthIntent::Minimum(pool_id.clone())])?;
261 Ok(receipt.growths.pop())
262 }
263
264 pub fn initialize_pools(
266 &self,
267 pool_ids: &[DynamicBackingPoolId],
268 ) -> Result<DynamicPoolGrowthBatchReceipt, VNextError> {
269 self.pools.maintain_pools(
270 pool_ids
271 .iter()
272 .cloned()
273 .map(DynamicPoolGrowthIntent::Minimum)
274 .collect(),
275 )
276 }
277
278 pub fn grow_pool(
282 &self,
283 pool_id: &DynamicBackingPoolId,
284 requested_bytes: u64,
285 ) -> Result<DynamicPoolGrowthReceipt, VNextError> {
286 let request = DynamicPoolGrowthRequest::new(pool_id.clone(), requested_bytes)?;
287 let mut receipt = self.grow_pools(vec![request])?;
288 receipt
289 .growths
290 .pop()
291 .ok_or_else(|| invalid_resource("single-pool growth produced no receipt"))
292 }
293
294 pub fn grow_pools(
297 &self,
298 requests: Vec<DynamicPoolGrowthRequest>,
299 ) -> Result<DynamicPoolGrowthBatchReceipt, VNextError> {
300 self.pools.maintain_pools(
301 requests
302 .into_iter()
303 .map(DynamicPoolGrowthIntent::Additional)
304 .collect(),
305 )
306 }
307
308 fn wait_snapshot_for_pool_ids<'a>(
309 &self,
310 pool_ids: impl IntoIterator<Item = &'a DynamicBackingPoolId>,
311 ) -> Result<CapacityWaitSnapshot, VNextError> {
312 self.pools.logical_admission.wait_snapshot_for_domains(
313 pool_ids
314 .into_iter()
315 .map(|pool_id| {
316 self.pools
317 .pools
318 .get(pool_id)
319 .map(|pool| pool.domain.domain_id)
320 .ok_or_else(|| {
321 invalid_resource("dynamic maintenance references an unknown pool")
322 })
323 })
324 .collect::<Result<Vec<_>, _>>()?,
325 )
326 }
327
328 fn capacity_wait_outcome(
329 &self,
330 logical_snapshot: CapacityWaitSnapshot,
331 blocked: DynamicDeviceCapacityBlocked,
332 maintenance_boundary: DynamicPoolMaintenanceBoundaryReceipt,
333 ) -> Result<DynamicDeferredMaintenanceOutcome, VNextError> {
334 if maintenance_boundary.pressure() != &blocked.pressure
335 || maintenance_boundary.plan_device_capacity_epoch()
336 != blocked.availability.plan_epoch()
337 || maintenance_boundary.process_device_capacity_epoch()
338 != blocked.availability.process_epoch()
339 {
340 return Err(invalid_resource(
341 "dynamic maintenance boundary differs from its capacity failure",
342 ));
343 }
344 let logical_snapshot = logical_snapshot.narrow_to_domains(blocked.planned_domains)?;
345 let mut observed = logical_snapshot.wait_condition().observed().to_vec();
346 observed.push(blocked.availability.epoch_for_pressure(&blocked.pressure));
347 let wait_condition = CapacityWaitCondition::new(
348 logical_snapshot.wait_condition().coordinator_id(),
349 observed,
350 )?;
351 Ok(DynamicDeferredMaintenanceOutcome::WaitForRelease {
352 current_epochs: logical_snapshot.epochs(),
353 wait_condition,
354 pressure: blocked.pressure.into(),
355 maintenance_boundary: Some(maintenance_boundary),
356 })
357 }
358
359 fn pool_resident_wait_outcome(
360 &self,
361 logical_snapshot: CapacityWaitSnapshot,
362 pressure: DynamicPoolResidentPressure,
363 ) -> Result<DynamicDeferredMaintenanceOutcome, VNextError> {
364 let domain = self
365 .pools
366 .pools
367 .get(pressure.pool_id())
368 .map(|pool| pool.domain.domain_id)
369 .ok_or_else(|| {
370 invalid_resource("dynamic pool resident pressure references an unknown pool")
371 })?;
372 let logical_snapshot = logical_snapshot.narrow_to_domains(vec![domain])?;
373 Ok(DynamicDeferredMaintenanceOutcome::WaitForRelease {
374 current_epochs: logical_snapshot.epochs(),
375 wait_condition: logical_snapshot.wait_condition().clone(),
376 pressure: pressure.into(),
377 maintenance_boundary: None,
378 })
379 }
380
381 fn maintain_deferred_pools(
382 &self,
383 intents: Vec<DynamicPoolGrowthIntent>,
384 capacity_blocked: &mut Option<DynamicDeviceCapacityBlocked>,
385 maintenance_boundary: &mut Option<DynamicPoolMaintenanceBoundaryReceipt>,
386 protected_immediate: &CapacityVector,
387 protected_packing_envelopes: &[DynamicBackingPackingEnvelope],
388 ) -> Result<DynamicPoolGrowthBatchReceipt, VNextError> {
389 let retry_intents = intents.clone();
390 match self
391 .pools
392 .maintain_pools_observed(intents, capacity_blocked)
393 {
394 Err(VNextError::DeviceCapacityUnavailable(pressure)) => {
395 let planned_domains = capacity_blocked
396 .as_ref()
397 .expect("typed capacity failure retains its exact observation")
398 .planned_domains
399 .clone();
400 let blocked = capacity_blocked
401 .as_ref()
402 .expect("typed capacity failure retains its exact observation");
403 let attempt = self.pools.reclaim_idle_chunks_for_pressure(
404 &pressure,
405 blocked.availability,
406 &planned_domains,
407 protected_immediate,
408 protected_packing_envelopes,
409 )?;
410 *maintenance_boundary = Some(attempt.boundary);
411 let Some(rebalance) = attempt.rebalance else {
412 return Err(VNextError::DeviceCapacityUnavailable(pressure));
413 };
414 *capacity_blocked = None;
415 let mut receipt = self
416 .pools
417 .maintain_pools_observed(retry_intents, capacity_blocked)?;
418 receipt.rebalance = Some(rebalance);
419 receipt.maintenance_boundary = maintenance_boundary.clone();
420 Ok(receipt)
421 }
422 outcome => outcome,
423 }
424 }
425
426 pub(super) fn maintain_for_live_deferred(
431 &self,
432 deferred: &DynamicBackingDeferred,
433 ) -> Result<DynamicDeferredMaintenanceOutcome, VNextError> {
434 let coordinator_id = self.pools.logical_admission.id();
435 if coordinator_id != deferred.epochs().coordinator_id()
436 || coordinator_id != deferred.wait_condition().coordinator_id()
437 {
438 return Err(invalid_resource(
439 "dynamic backing deferral belongs to another admission coordinator",
440 ));
441 }
442 if deferred.blockers().is_empty() {
443 return Err(invalid_resource(
444 "dynamic backing deferral contains no blocking pool",
445 ));
446 }
447 let logical_snapshot = self.wait_snapshot_for_pool_ids(
448 deferred
449 .blockers()
450 .iter()
451 .map(DynamicBackingBlocker::pool_id),
452 )?;
453 let mut capacity_blocked = None;
454 let mut maintenance_boundary = None;
455 let growth = self.maintain_deferred_pools(
456 deferred
457 .blockers()
458 .iter()
459 .cloned()
460 .map(DynamicPoolGrowthIntent::RevalidatedDeferral)
461 .collect(),
462 &mut capacity_blocked,
463 &mut maintenance_boundary,
464 deferred.protected_immediate(),
465 deferred.protected_packing_envelopes(),
466 );
467 match growth {
468 Ok(receipt) if receipt.growths().is_empty() => {
469 let current_epochs = self.pools.logical_admission.epochs()?;
470 if current_epochs == deferred.epochs() {
471 return Err(invalid_resource(
472 "dynamic backing maintenance made no progress on an unchanged deferral",
473 ));
474 }
475 Ok(DynamicDeferredMaintenanceOutcome::RetryAdmission { current_epochs })
476 }
477 Ok(receipt) => Ok(DynamicDeferredMaintenanceOutcome::Maintained(receipt)),
478 Err(VNextError::DeviceCapacityUnavailable(_)) => self.capacity_wait_outcome(
479 logical_snapshot,
480 capacity_blocked.expect("typed capacity failure retains its exact observation"),
481 maintenance_boundary
482 .expect("typed capacity rebalance failure retains its maintenance boundary"),
483 ),
484 Err(VNextError::DynamicPoolResidentUnavailable(pressure)) => {
485 self.pool_resident_wait_outcome(logical_snapshot, pressure)
486 }
487 Err(error) => Err(error),
488 }
489 }
490
491 pub(super) fn try_maintain_for_capacity_pressure(
496 &self,
497 deferred: &AdmissionDeferred,
498 ) -> Result<Option<DynamicPoolGrowthBatchReceipt>, VNextError> {
499 let coordinator_id = self.pools.logical_admission.id();
500 if coordinator_id != deferred.epochs().coordinator_id()
501 || coordinator_id != deferred.wait_condition().coordinator_id()
502 {
503 return Err(invalid_resource(
504 "logical admission pressure belongs to another coordinator",
505 ));
506 }
507 if self.pools.logical_admission.snapshot()?.poisoned() {
508 return Err(invalid_resource("logical admission is fail-closed"));
509 }
510 let pools_by_domain = self
511 .pools
512 .pools
513 .values()
514 .map(|pool| (pool.domain.domain_id, pool.domain.pool_id().clone()))
515 .collect::<BTreeMap<_, _>>();
516 if deferred.blockers().iter().any(|blocker| {
519 blocker.kind() == CapacityShortfallKind::ActiveSequenceCeiling
520 || blocker
521 .domain()
522 .is_none_or(|domain| !pools_by_domain.contains_key(&domain))
523 }) {
524 return Ok(None);
525 }
526 let mut required = BTreeMap::<DynamicBackingPoolId, u64>::new();
527 for entry in deferred
528 .immediate_requested()
529 .entries()
530 .iter()
531 .chain(deferred.fit_requested().entries())
532 {
533 let Some(pool_id) = pools_by_domain.get(&entry.domain()) else {
534 return Ok(None);
535 };
536 required
539 .entry(pool_id.clone())
540 .and_modify(|bytes| *bytes = (*bytes).max(entry.units().get()))
541 .or_insert(entry.units().get());
542 }
543 let intents = required
544 .into_iter()
545 .map(|(pool_id, required_free_bytes)| {
546 DynamicPoolGrowthIntent::RevalidatedAdmissionPressure {
547 pool_id,
548 required_free_bytes,
549 }
550 })
551 .collect();
552 self.pools
553 .maintain_pools_observed(intents, &mut None)
554 .map(Some)
555 }
556
557 pub fn maintain_for_admission_deferred(
560 &self,
561 deferred: &AdmissionDeferred,
562 ) -> Result<DynamicDeferredMaintenanceOutcome, VNextError> {
563 let coordinator_id = self.pools.logical_admission.id();
564 if coordinator_id != deferred.epochs().coordinator_id()
565 || coordinator_id != deferred.wait_condition().coordinator_id()
566 {
567 return Err(invalid_resource(
568 "logical admission deferral belongs to another coordinator",
569 ));
570 }
571 if deferred.action() != DeferredAction::AwaitBackingGrowth {
572 return Err(invalid_resource(
573 "logical admission deferral does not request backing growth",
574 ));
575 }
576 let pools_by_domain = self
577 .pools
578 .pools
579 .values()
580 .map(|pool| (pool.domain.domain_id, pool.domain.pool_id().clone()))
581 .collect::<BTreeMap<_, _>>();
582 let current = self.pools.logical_admission.snapshot()?;
583 let mut requested_by_pool = BTreeMap::<DynamicBackingPoolId, u64>::new();
584 for blocker in deferred
585 .blockers()
586 .iter()
587 .filter(|blocker| blocker.kind() == CapacityShortfallKind::BackingGrowthRequired)
588 {
589 let domain = blocker.domain().ok_or_else(|| {
590 invalid_resource("backing-growth blocker contains no capacity domain")
591 })?;
592 let pool_id = pools_by_domain.get(&domain).ok_or_else(|| {
593 invalid_resource("backing-growth blocker references a non-pool domain")
594 })?;
595 let current_total = current
596 .domains()
597 .iter()
598 .find(|snapshot| snapshot.domain() == domain)
599 .ok_or_else(|| {
600 invalid_resource("backing-growth blocker references an unknown domain")
601 })?
602 .total()
603 .get();
604 let missing = blocker.requested().get().saturating_sub(current_total);
605 if missing == 0 {
606 continue;
607 }
608 requested_by_pool
609 .entry(pool_id.clone())
610 .and_modify(|bytes| *bytes = (*bytes).max(missing))
611 .or_insert(missing);
612 }
613 if requested_by_pool.is_empty() {
614 return Ok(DynamicDeferredMaintenanceOutcome::RetryAdmission {
615 current_epochs: self.pools.logical_admission.epochs()?,
616 });
617 }
618 let requests = requested_by_pool
619 .into_iter()
620 .map(|(pool_id, bytes)| DynamicPoolGrowthRequest::new(pool_id, bytes))
621 .collect::<Result<Vec<_>, _>>()?;
622 let logical_snapshot =
623 self.wait_snapshot_for_pool_ids(requests.iter().map(|request| request.pool_id()))?;
624 let mut capacity_blocked = None;
625 let mut maintenance_boundary = None;
626 let growth = self.maintain_deferred_pools(
627 requests
628 .into_iter()
629 .map(DynamicPoolGrowthIntent::Additional)
630 .collect(),
631 &mut capacity_blocked,
632 &mut maintenance_boundary,
633 deferred.fit_requested(),
639 &[],
640 );
641 match growth {
642 Ok(receipt) => Ok(DynamicDeferredMaintenanceOutcome::Maintained(receipt)),
643 Err(VNextError::DeviceCapacityUnavailable(_)) => self.capacity_wait_outcome(
644 logical_snapshot,
645 capacity_blocked.expect("typed capacity failure retains its exact observation"),
646 maintenance_boundary
647 .expect("typed capacity rebalance failure retains its maintenance boundary"),
648 ),
649 Err(VNextError::DynamicPoolResidentUnavailable(pressure)) => {
650 self.pool_resident_wait_outcome(logical_snapshot, pressure)
651 }
652 Err(error) => Err(error),
653 }
654 }
655
656 pub fn release_quarantined_chunks(
660 &self,
661 ) -> Result<DynamicPoolQuarantineReleaseReceipt, VNextError> {
662 let pools = self.pools.pools.values().cloned().collect::<Vec<_>>();
663 let _maintenance = pools
664 .iter()
665 .map(|pool| {
666 pool.maintenance
667 .lock()
668 .map_err(|_| invalid_resource("dynamic pool maintenance authority is poisoned"))
669 })
670 .collect::<Result<Vec<_>, _>>()?;
671 let mut states = pools
672 .iter()
673 .map(|pool| {
674 pool.state
675 .lock()
676 .map_err(|_| invalid_resource("dynamic backing pool is poisoned"))
677 })
678 .collect::<Result<Vec<_>, _>>()?;
679 let pool_totals = states
680 .iter()
681 .map(|state| {
682 let bytes = state.quarantined.iter().try_fold(0_u64, |total, chunk| {
683 total
684 .checked_add(chunk.backing._grant.bytes())
685 .ok_or_else(|| invalid_resource("released quarantine bytes overflow u64"))
686 })?;
687 Ok((state.quarantined.len(), bytes))
688 })
689 .collect::<Result<Vec<_>, VNextError>>()?;
690 let released_chunks = pool_totals.iter().try_fold(0_usize, |total, (count, _)| {
691 total
692 .checked_add(*count)
693 .ok_or_else(|| invalid_resource("released quarantine count overflows usize"))
694 })?;
695 let released_bytes = pool_totals.iter().try_fold(0_u64, |total, (_, bytes)| {
696 total
697 .checked_add(*bytes)
698 .ok_or_else(|| invalid_resource("released quarantine bytes overflow u64"))
699 })?;
700 let mut released = Vec::with_capacity(pools.len());
701 let mut receipts = Vec::new();
702 for ((pool, state), (pool_chunks, pool_bytes)) in
703 pools.iter().zip(states.iter_mut()).zip(pool_totals)
704 {
705 if pool_chunks == 0 {
706 continue;
707 }
708 let chunks = std::mem::take(&mut state.quarantined);
709 debug_assert_eq!(chunks.len(), pool_chunks);
710 receipts.push(DynamicPoolQuarantineRelease {
711 pool_id: pool.domain.pool_id().clone(),
712 released_chunks: pool_chunks,
713 released_bytes: pool_bytes,
714 });
715 released.push(chunks);
716 }
717 drop(states);
718 drop(released);
719 Ok(DynamicPoolQuarantineReleaseReceipt {
720 pools: receipts,
721 released_chunks,
722 released_bytes,
723 })
724 }
725}