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 fn maintain_for_admission_deferred(
494 &self,
495 deferred: &AdmissionDeferred,
496 ) -> Result<DynamicDeferredMaintenanceOutcome, VNextError> {
497 let coordinator_id = self.pools.logical_admission.id();
498 if coordinator_id != deferred.epochs().coordinator_id()
499 || coordinator_id != deferred.wait_condition().coordinator_id()
500 {
501 return Err(invalid_resource(
502 "logical admission deferral belongs to another coordinator",
503 ));
504 }
505 if deferred.action() != DeferredAction::AwaitBackingGrowth {
506 return Err(invalid_resource(
507 "logical admission deferral does not request backing growth",
508 ));
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 let current = self.pools.logical_admission.snapshot()?;
517 let mut requested_by_pool = BTreeMap::<DynamicBackingPoolId, u64>::new();
518 for blocker in deferred
519 .blockers()
520 .iter()
521 .filter(|blocker| blocker.kind() == CapacityShortfallKind::BackingGrowthRequired)
522 {
523 let domain = blocker.domain().ok_or_else(|| {
524 invalid_resource("backing-growth blocker contains no capacity domain")
525 })?;
526 let pool_id = pools_by_domain.get(&domain).ok_or_else(|| {
527 invalid_resource("backing-growth blocker references a non-pool domain")
528 })?;
529 let current_total = current
530 .domains()
531 .iter()
532 .find(|snapshot| snapshot.domain() == domain)
533 .ok_or_else(|| {
534 invalid_resource("backing-growth blocker references an unknown domain")
535 })?
536 .total()
537 .get();
538 let missing = blocker.requested().get().saturating_sub(current_total);
539 if missing == 0 {
540 continue;
541 }
542 requested_by_pool
543 .entry(pool_id.clone())
544 .and_modify(|bytes| *bytes = (*bytes).max(missing))
545 .or_insert(missing);
546 }
547 if requested_by_pool.is_empty() {
548 return Ok(DynamicDeferredMaintenanceOutcome::RetryAdmission {
549 current_epochs: self.pools.logical_admission.epochs()?,
550 });
551 }
552 let requests = requested_by_pool
553 .into_iter()
554 .map(|(pool_id, bytes)| DynamicPoolGrowthRequest::new(pool_id, bytes))
555 .collect::<Result<Vec<_>, _>>()?;
556 let logical_snapshot =
557 self.wait_snapshot_for_pool_ids(requests.iter().map(|request| request.pool_id()))?;
558 let mut capacity_blocked = None;
559 let mut maintenance_boundary = None;
560 let growth = self.maintain_deferred_pools(
561 requests
562 .into_iter()
563 .map(DynamicPoolGrowthIntent::Additional)
564 .collect(),
565 &mut capacity_blocked,
566 &mut maintenance_boundary,
567 deferred.immediate_requested(),
568 &[],
569 );
570 match growth {
571 Ok(receipt) => Ok(DynamicDeferredMaintenanceOutcome::Maintained(receipt)),
572 Err(VNextError::DeviceCapacityUnavailable(_)) => self.capacity_wait_outcome(
573 logical_snapshot,
574 capacity_blocked.expect("typed capacity failure retains its exact observation"),
575 maintenance_boundary
576 .expect("typed capacity rebalance failure retains its maintenance boundary"),
577 ),
578 Err(VNextError::DynamicPoolResidentUnavailable(pressure)) => {
579 self.pool_resident_wait_outcome(logical_snapshot, pressure)
580 }
581 Err(error) => Err(error),
582 }
583 }
584
585 pub fn release_quarantined_chunks(
589 &self,
590 ) -> Result<DynamicPoolQuarantineReleaseReceipt, VNextError> {
591 let pools = self.pools.pools.values().cloned().collect::<Vec<_>>();
592 let _maintenance = pools
593 .iter()
594 .map(|pool| {
595 pool.maintenance
596 .lock()
597 .map_err(|_| invalid_resource("dynamic pool maintenance authority is poisoned"))
598 })
599 .collect::<Result<Vec<_>, _>>()?;
600 let mut states = pools
601 .iter()
602 .map(|pool| {
603 pool.state
604 .lock()
605 .map_err(|_| invalid_resource("dynamic backing pool is poisoned"))
606 })
607 .collect::<Result<Vec<_>, _>>()?;
608 let pool_totals = states
609 .iter()
610 .map(|state| {
611 let bytes = state.quarantined.iter().try_fold(0_u64, |total, chunk| {
612 total
613 .checked_add(chunk.backing._grant.bytes())
614 .ok_or_else(|| invalid_resource("released quarantine bytes overflow u64"))
615 })?;
616 Ok((state.quarantined.len(), bytes))
617 })
618 .collect::<Result<Vec<_>, VNextError>>()?;
619 let released_chunks = pool_totals.iter().try_fold(0_usize, |total, (count, _)| {
620 total
621 .checked_add(*count)
622 .ok_or_else(|| invalid_resource("released quarantine count overflows usize"))
623 })?;
624 let released_bytes = pool_totals.iter().try_fold(0_u64, |total, (_, bytes)| {
625 total
626 .checked_add(*bytes)
627 .ok_or_else(|| invalid_resource("released quarantine bytes overflow u64"))
628 })?;
629 let mut released = Vec::with_capacity(pools.len());
630 let mut receipts = Vec::new();
631 for ((pool, state), (pool_chunks, pool_bytes)) in
632 pools.iter().zip(states.iter_mut()).zip(pool_totals)
633 {
634 if pool_chunks == 0 {
635 continue;
636 }
637 let chunks = std::mem::take(&mut state.quarantined);
638 debug_assert_eq!(chunks.len(), pool_chunks);
639 receipts.push(DynamicPoolQuarantineRelease {
640 pool_id: pool.domain.pool_id().clone(),
641 released_chunks: pool_chunks,
642 released_bytes: pool_bytes,
643 });
644 released.push(chunks);
645 }
646 drop(states);
647 drop(released);
648 Ok(DynamicPoolQuarantineReleaseReceipt {
649 pools: receipts,
650 released_chunks,
651 released_bytes,
652 })
653 }
654}