1use std::collections::BTreeMap;
4use std::fmt;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::{Arc, Mutex};
7
8use crate::metrics::{BufferMetricClass, ResourceMetricClass, RuntimeMetrics};
9
10#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
12pub enum ResourceClass {
13 Capabilities,
14 ReadyHandles,
15 Sockets,
16 Connections,
17 BufferedBytes,
18 Datagrams,
19 HandleCommands,
20 HandleCommandBytes,
21 BridgeCalls,
22 BridgeRequestBytes,
23 BridgeResponseBytes,
24 AsyncCompletions,
25 AsyncCompletionBytes,
26 UdpDatagrams,
27 UdpBytes,
28 TlsBytes,
29 Timers,
30 Tasks,
31 ExecutorSlots,
32 ExecutorBytes,
33 Http2Connections,
34 Http2Streams,
35 Http2BufferedBytes,
36 Http2HeaderBytes,
37 Http2DataBytes,
38 Http2Commands,
39 Http2CommandBytes,
40 Http2Events,
41 Http2EventBytes,
42}
43
44impl ResourceClass {
45 pub const ALL: [Self; 29] = [
46 Self::Capabilities,
47 Self::ReadyHandles,
48 Self::Sockets,
49 Self::Connections,
50 Self::BufferedBytes,
51 Self::Datagrams,
52 Self::HandleCommands,
53 Self::HandleCommandBytes,
54 Self::BridgeCalls,
55 Self::BridgeRequestBytes,
56 Self::BridgeResponseBytes,
57 Self::AsyncCompletions,
58 Self::AsyncCompletionBytes,
59 Self::UdpDatagrams,
60 Self::UdpBytes,
61 Self::TlsBytes,
62 Self::Timers,
63 Self::Tasks,
64 Self::ExecutorSlots,
65 Self::ExecutorBytes,
66 Self::Http2Connections,
67 Self::Http2Streams,
68 Self::Http2BufferedBytes,
69 Self::Http2HeaderBytes,
70 Self::Http2DataBytes,
71 Self::Http2Commands,
72 Self::Http2CommandBytes,
73 Self::Http2Events,
74 Self::Http2EventBytes,
75 ];
76
77 pub const fn name(self) -> &'static str {
78 match self {
79 Self::Capabilities => "capabilities",
80 Self::ReadyHandles => "readyHandles",
81 Self::Sockets => "sockets",
82 Self::Connections => "connections",
83 Self::BufferedBytes => "bufferedBytes",
84 Self::Datagrams => "datagrams",
85 Self::HandleCommands => "handleCommands",
86 Self::HandleCommandBytes => "handleCommandBytes",
87 Self::BridgeCalls => "bridgeCalls",
88 Self::BridgeRequestBytes => "bridgeRequestBytes",
89 Self::BridgeResponseBytes => "bridgeResponseBytes",
90 Self::AsyncCompletions => "asyncCompletions",
91 Self::AsyncCompletionBytes => "asyncCompletionBytes",
92 Self::UdpDatagrams => "udpDatagrams",
93 Self::UdpBytes => "udpBytes",
94 Self::TlsBytes => "tlsBytes",
95 Self::Timers => "timers",
96 Self::Tasks => "tasks",
97 Self::ExecutorSlots => "executorSlots",
98 Self::ExecutorBytes => "executorBytes",
99 Self::Http2Connections => "http2Connections",
100 Self::Http2Streams => "http2Streams",
101 Self::Http2BufferedBytes => "http2BufferedBytes",
102 Self::Http2HeaderBytes => "http2HeaderBytes",
103 Self::Http2DataBytes => "http2DataBytes",
104 Self::Http2Commands => "http2Commands",
105 Self::Http2CommandBytes => "http2CommandBytes",
106 Self::Http2Events => "http2Events",
107 Self::Http2EventBytes => "http2EventBytes",
108 }
109 }
110}
111
112#[derive(Clone, Debug, Eq, PartialEq)]
113pub struct ResourceLimit {
114 pub maximum: usize,
115 pub config_path: String,
116}
117
118impl ResourceLimit {
119 pub fn new(maximum: usize, config_path: impl Into<String>) -> Self {
120 Self {
121 maximum,
122 config_path: config_path.into(),
123 }
124 }
125}
126
127#[derive(Clone, Debug, Eq, PartialEq)]
128pub struct LimitError {
129 pub scope: String,
130 pub resource: ResourceClass,
131 pub used: usize,
132 pub requested: usize,
133 pub limit: usize,
134 pub config_path: String,
135}
136
137impl fmt::Display for LimitError {
138 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
139 write!(
140 formatter,
141 "ERR_AGENTOS_RESOURCE_LIMIT: scope={} resource={} used={} requested={} limit={}; raise {}",
142 self.scope,
143 self.resource.name(),
144 self.used,
145 self.requested,
146 self.limit,
147 self.config_path
148 )
149 }
150}
151
152impl std::error::Error for LimitError {}
153
154#[derive(Clone, Debug, Eq, PartialEq)]
155pub struct ResourceUsage {
156 pub used: usize,
157 pub limit: Option<usize>,
158}
159
160#[derive(Debug, Default)]
161struct CounterState {
162 used: usize,
163 warning_active: bool,
164}
165
166#[derive(Debug)]
167struct LedgerState {
168 counters: BTreeMap<ResourceClass, CounterState>,
169}
170
171#[derive(Debug)]
172struct LedgerInner {
173 scope: String,
174 limits: BTreeMap<ResourceClass, ResourceLimit>,
175 state: Mutex<LedgerState>,
176 capacity_changed: tokio::sync::Notify,
177 integrity_failed: AtomicBool,
178 metrics: Option<RuntimeMetrics>,
179}
180
181#[derive(Clone, Debug)]
184pub struct ResourceLedger {
185 inner: Arc<LedgerInner>,
186 parent: Option<Arc<ResourceLedger>>,
187}
188
189impl ResourceLedger {
190 pub fn root(
191 scope: impl Into<String>,
192 limits: impl IntoIterator<Item = (ResourceClass, ResourceLimit)>,
193 ) -> Self {
194 Self::new(scope, limits, None, None)
195 }
196
197 pub fn root_with_metrics(
198 scope: impl Into<String>,
199 limits: impl IntoIterator<Item = (ResourceClass, ResourceLimit)>,
200 metrics: RuntimeMetrics,
201 ) -> Self {
202 Self::new(scope, limits, None, Some(metrics))
203 }
204
205 pub fn child(
206 scope: impl Into<String>,
207 limits: impl IntoIterator<Item = (ResourceClass, ResourceLimit)>,
208 parent: Arc<ResourceLedger>,
209 ) -> Self {
210 Self::new(scope, limits, Some(parent), None)
211 }
212
213 fn new(
214 scope: impl Into<String>,
215 limits: impl IntoIterator<Item = (ResourceClass, ResourceLimit)>,
216 parent: Option<Arc<ResourceLedger>>,
217 metrics: Option<RuntimeMetrics>,
218 ) -> Self {
219 Self {
220 inner: Arc::new(LedgerInner {
221 scope: scope.into(),
222 limits: limits.into_iter().collect(),
223 state: Mutex::new(LedgerState {
224 counters: BTreeMap::new(),
225 }),
226 capacity_changed: tokio::sync::Notify::new(),
227 integrity_failed: AtomicBool::new(false),
228 metrics,
229 }),
230 parent,
231 }
232 }
233
234 pub fn scope(&self) -> &str {
235 &self.inner.scope
236 }
237
238 pub fn reserve(
241 &self,
242 resource: ResourceClass,
243 amount: usize,
244 ) -> Result<Reservation, LimitError> {
245 let mut allocations = Vec::with_capacity(if self.parent.is_some() { 2 } else { 1 });
246 if let Some(parent) = &self.parent {
247 parent.reserve_into(resource, amount, &mut allocations)?;
248 }
249 if let Err(error) = self.reserve_local(resource, amount) {
250 release_allocations(&mut allocations);
251 return Err(error);
252 }
253 if amount != 0 {
254 allocations.push(Allocation {
255 ledger: Arc::clone(&self.inner),
256 resource,
257 amount,
258 });
259 }
260 Ok(Reservation {
261 resource,
262 amount,
263 allocations,
264 })
265 }
266
267 fn reserve_into(
268 &self,
269 resource: ResourceClass,
270 amount: usize,
271 allocations: &mut Vec<Allocation>,
272 ) -> Result<(), LimitError> {
273 if let Some(parent) = &self.parent {
274 parent.reserve_into(resource, amount, allocations)?;
275 }
276 if let Err(error) = self.reserve_local(resource, amount) {
277 release_allocations(allocations);
278 return Err(error);
279 }
280 if amount != 0 {
281 allocations.push(Allocation {
282 ledger: Arc::clone(&self.inner),
283 resource,
284 amount,
285 });
286 }
287 Ok(())
288 }
289
290 fn reserve_local(&self, resource: ResourceClass, amount: usize) -> Result<(), LimitError> {
291 if amount == 0 {
292 return Ok(());
293 }
294 let mut state = self.inner.state.lock().unwrap_or_else(|poisoned| {
295 eprintln!(
296 "ERR_AGENTOS_RESOURCE_LEDGER_POISONED: recovering scope={} resource={}",
297 self.inner.scope,
298 resource.name()
299 );
300 poisoned.into_inner()
301 });
302 let counter = state.counters.entry(resource).or_default();
303 let requested_total = counter.used.checked_add(amount);
304 if let Some(limit) = self.inner.limits.get(&resource) {
305 if requested_total.is_none_or(|total| total > limit.maximum) {
306 return Err(LimitError {
307 scope: self.inner.scope.clone(),
308 resource,
309 used: counter.used,
310 requested: amount,
311 limit: limit.maximum,
312 config_path: limit.config_path.clone(),
313 });
314 }
315 }
316 counter.used = requested_total.unwrap_or(usize::MAX);
317 maybe_warn(&self.inner, resource, counter);
318 observe_usage(&self.inner, resource, counter.used);
319 Ok(())
320 }
321
322 pub fn usage(&self, resource: ResourceClass) -> ResourceUsage {
323 let state = self.inner.state.lock().unwrap_or_else(|poisoned| {
324 eprintln!(
325 "ERR_AGENTOS_RESOURCE_LEDGER_POISONED: recovering scope={} resource={}",
326 self.inner.scope,
327 resource.name()
328 );
329 poisoned.into_inner()
330 });
331 ResourceUsage {
332 used: state
333 .counters
334 .get(&resource)
335 .map_or(0, |counter| counter.used),
336 limit: self.inner.limits.get(&resource).map(|limit| limit.maximum),
337 }
338 }
339
340 pub fn configured_limit(&self, resource: ResourceClass) -> Option<ResourceLimit> {
341 self.inner.limits.get(&resource).cloned()
342 }
343
344 pub fn capacity_available(&self, resource: ResourceClass, amount: usize) -> bool {
349 if let Some(parent) = &self.parent {
350 if !parent.capacity_available(resource, amount) {
351 return false;
352 }
353 }
354 if amount == 0 {
355 return true;
356 }
357 let state = self.inner.state.lock().unwrap_or_else(|poisoned| {
358 eprintln!(
359 "ERR_AGENTOS_RESOURCE_LEDGER_POISONED: recovering capacity probe scope={} resource={}",
360 self.inner.scope,
361 resource.name()
362 );
363 poisoned.into_inner()
364 });
365 let used = state
366 .counters
367 .get(&resource)
368 .map_or(0, |counter| counter.used);
369 self.inner.limits.get(&resource).is_none_or(|limit| {
370 used.checked_add(amount)
371 .is_some_and(|total| total <= limit.maximum)
372 })
373 }
374
375 pub fn is_zero(&self) -> bool {
376 let state = self.inner.state.lock().unwrap_or_else(|poisoned| {
377 eprintln!(
378 "ERR_AGENTOS_RESOURCE_LEDGER_POISONED: recovering scope={}",
379 self.inner.scope
380 );
381 poisoned.into_inner()
382 });
383 state.counters.values().all(|counter| counter.used == 0)
384 }
385
386 pub fn integrity_ok(&self) -> bool {
387 !self.inner.integrity_failed.load(Ordering::Acquire)
388 }
389
390 pub async fn capacity_changed(&self) {
393 let local_changed = self.inner.capacity_changed.notified();
394 if let Some(parent) = &self.parent {
395 let parent_changed = parent.inner.capacity_changed.notified();
396 tokio::select! {
397 _ = local_changed => {}
398 _ = parent_changed => {}
399 }
400 } else {
401 local_changed.await;
402 }
403 }
404
405 pub async fn reserve_when_available(
409 &self,
410 resource: ResourceClass,
411 amount: usize,
412 ) -> Result<Reservation, LimitError> {
413 loop {
414 let local_changed = self.inner.capacity_changed.notified();
418 let parent_changed = self
419 .parent
420 .as_ref()
421 .map(|parent| parent.inner.capacity_changed.notified());
422 match self.reserve(resource, amount) {
423 Ok(reservation) => return Ok(reservation),
424 Err(error) if amount > error.limit => return Err(error),
425 Err(_) => {
426 if let Some(parent_changed) = parent_changed {
427 tokio::select! {
428 _ = local_changed => {}
429 _ = parent_changed => {}
430 }
431 } else {
432 local_changed.await;
433 }
434 }
435 }
436 }
437 }
438}
439
440fn maybe_warn(inner: &LedgerInner, resource: ResourceClass, counter: &mut CounterState) {
441 let Some(limit) = inner.limits.get(&resource) else {
442 return;
443 };
444 let near =
447 counter.used != 0 && counter.used.saturating_mul(100) >= limit.maximum.saturating_mul(80);
448 if near && !counter.warning_active {
449 counter.warning_active = true;
450 eprintln!(
451 "WARN_AGENTOS_RESOURCE_NEAR_LIMIT: scope={} resource={} used={} limit={} config={}",
452 inner.scope,
453 resource.name(),
454 counter.used,
455 limit.maximum,
456 limit.config_path
457 );
458 }
459}
460
461#[derive(Debug)]
462struct Allocation {
463 ledger: Arc<LedgerInner>,
464 resource: ResourceClass,
465 amount: usize,
466}
467
468fn release_allocation(allocation: &Allocation) {
469 let mut state = allocation.ledger.state.lock().unwrap_or_else(|poisoned| {
470 eprintln!(
471 "ERR_AGENTOS_RESOURCE_LEDGER_POISONED: recovering release scope={} resource={}",
472 allocation.ledger.scope,
473 allocation.resource.name()
474 );
475 poisoned.into_inner()
476 });
477 let counter = state.counters.entry(allocation.resource).or_default();
478 if allocation.amount > counter.used {
479 eprintln!(
480 "ERR_AGENTOS_RESOURCE_ACCOUNTING_UNDERFLOW: scope={} resource={} used={} release={}",
481 allocation.ledger.scope,
482 allocation.resource.name(),
483 counter.used,
484 allocation.amount
485 );
486 allocation
487 .ledger
488 .integrity_failed
489 .store(true, Ordering::Release);
490 counter.used = 0;
491 } else {
492 counter.used -= allocation.amount;
493 }
494 if let Some(limit) = allocation.ledger.limits.get(&allocation.resource) {
495 if counter.used.saturating_mul(100) < limit.maximum.saturating_mul(70) {
496 counter.warning_active = false;
497 }
498 }
499 observe_usage(&allocation.ledger, allocation.resource, counter.used);
500 allocation.ledger.capacity_changed.notify_one();
503}
504
505fn observe_usage(inner: &LedgerInner, resource: ResourceClass, used: usize) {
506 let Some(metrics) = &inner.metrics else {
507 return;
508 };
509 match resource {
510 ResourceClass::Capabilities => {
511 metrics.observe_resource(ResourceMetricClass::Capabilities, used)
512 }
513 ResourceClass::ReadyHandles => {
514 metrics.observe_resource(ResourceMetricClass::ReadyHandles, used)
515 }
516 ResourceClass::Sockets => metrics.observe_resource(ResourceMetricClass::Sockets, used),
517 ResourceClass::Connections => {
518 metrics.observe_resource(ResourceMetricClass::Connections, used)
519 }
520 ResourceClass::BufferedBytes => metrics.observe_buffer(BufferMetricClass::Native, used),
521 ResourceClass::Datagrams => metrics.observe_resource(ResourceMetricClass::Datagrams, used),
522 ResourceClass::HandleCommands => {
523 metrics.observe_resource(ResourceMetricClass::HandleCommands, used)
524 }
525 ResourceClass::HandleCommandBytes => {
526 metrics.observe_buffer(BufferMetricClass::Native, used)
527 }
528 ResourceClass::BridgeCalls => {
529 metrics.observe_resource(ResourceMetricClass::BridgeCalls, used)
530 }
531 ResourceClass::BridgeRequestBytes | ResourceClass::BridgeResponseBytes => {
532 metrics.observe_buffer(BufferMetricClass::Bridge, used)
533 }
534 ResourceClass::AsyncCompletions => {
535 metrics.observe_resource(ResourceMetricClass::AsyncCompletions, used)
536 }
537 ResourceClass::AsyncCompletionBytes => {
538 metrics.observe_buffer(BufferMetricClass::Bridge, used)
539 }
540 ResourceClass::UdpDatagrams => {
541 metrics.observe_resource(ResourceMetricClass::Datagrams, used)
542 }
543 ResourceClass::UdpBytes => metrics.observe_buffer(BufferMetricClass::Datagram, used),
544 ResourceClass::TlsBytes => metrics.observe_buffer(BufferMetricClass::Tls, used),
545 ResourceClass::Timers => metrics.observe_resource(ResourceMetricClass::Timers, used),
546 ResourceClass::Tasks => metrics.observe_resource(ResourceMetricClass::Tasks, used),
547 ResourceClass::ExecutorSlots => {}
548 ResourceClass::ExecutorBytes => metrics.observe_buffer(BufferMetricClass::Executor, used),
549 ResourceClass::Http2BufferedBytes => metrics.observe_buffer(BufferMetricClass::Http2, used),
550 ResourceClass::Http2Connections => {
551 metrics.observe_resource(ResourceMetricClass::Http2Connections, used)
552 }
553 ResourceClass::Http2Streams => {
554 metrics.observe_resource(ResourceMetricClass::Http2Streams, used)
555 }
556 ResourceClass::Http2HeaderBytes
557 | ResourceClass::Http2DataBytes
558 | ResourceClass::Http2Commands
559 | ResourceClass::Http2CommandBytes
560 | ResourceClass::Http2Events
561 | ResourceClass::Http2EventBytes => {}
562 }
563}
564
565fn release_allocations(allocations: &mut Vec<Allocation>) {
566 for allocation in allocations.drain(..).rev() {
567 release_allocation(&allocation);
568 }
569}
570
571#[derive(Debug)]
574pub struct Reservation {
575 resource: ResourceClass,
576 amount: usize,
577 allocations: Vec<Allocation>,
578}
579
580#[derive(Clone)]
584pub struct SharedReservation(Arc<Reservation>);
585
586impl SharedReservation {
587 pub fn new(reservation: Reservation) -> Self {
588 Self(Arc::new(reservation))
589 }
590
591 pub fn resource(&self) -> ResourceClass {
592 self.0.resource()
593 }
594
595 pub fn amount(&self) -> usize {
596 self.0.amount()
597 }
598}
599
600impl fmt::Debug for SharedReservation {
601 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
602 formatter
603 .debug_struct("SharedReservation")
604 .field("resource", &self.resource())
605 .field("amount", &self.amount())
606 .finish_non_exhaustive()
607 }
608}
609
610impl PartialEq for SharedReservation {
611 fn eq(&self, other: &Self) -> bool {
612 Arc::ptr_eq(&self.0, &other.0)
613 }
614}
615
616impl Eq for SharedReservation {}
617
618impl Reservation {
619 pub fn resource(&self) -> ResourceClass {
620 self.resource
621 }
622
623 pub fn amount(&self) -> usize {
624 self.amount
625 }
626
627 pub fn split(&mut self, amount: usize) -> Option<Self> {
629 if amount > self.amount {
630 return None;
631 }
632 self.amount -= amount;
633 let mut allocations = Vec::with_capacity(self.allocations.len());
634 for allocation in &mut self.allocations {
635 allocation.amount -= amount;
636 allocations.push(Allocation {
637 ledger: Arc::clone(&allocation.ledger),
638 resource: allocation.resource,
639 amount,
640 });
641 }
642 Some(Self {
643 resource: self.resource,
644 amount,
645 allocations,
646 })
647 }
648
649 pub fn merge(&mut self, mut other: Self) -> Result<(), Self> {
651 if self.resource != other.resource || self.allocations.len() != other.allocations.len() {
652 return Err(other);
653 }
654 if self
655 .allocations
656 .iter()
657 .zip(&other.allocations)
658 .any(|(left, right)| !Arc::ptr_eq(&left.ledger, &right.ledger))
659 {
660 return Err(other);
661 }
662 let Some(total) = self.amount.checked_add(other.amount) else {
663 return Err(other);
664 };
665 for (left, right) in self.allocations.iter_mut().zip(&other.allocations) {
666 left.amount += right.amount;
667 }
668 self.amount = total;
669 other.amount = 0;
670 other.allocations.clear();
671 Ok(())
672 }
673}
674
675impl Drop for Reservation {
676 fn drop(&mut self) {
677 release_allocations(&mut self.allocations);
678 self.amount = 0;
679 }
680}
681
682#[cfg(test)]
683mod tests {
684 use super::*;
685
686 fn limit(maximum: usize) -> [(ResourceClass, ResourceLimit); 1] {
687 [(
688 ResourceClass::BufferedBytes,
689 ResourceLimit::new(maximum, "runtime.resources.maxSocketBufferedBytes"),
690 )]
691 }
692
693 #[test]
694 fn child_reservation_charges_and_releases_both_scopes() {
695 let process = Arc::new(ResourceLedger::root("process", limit(10)));
696 let vm = ResourceLedger::child("vm-1", limit(6), Arc::clone(&process));
697 let reservation = vm.reserve(ResourceClass::BufferedBytes, 6).unwrap();
698 assert_eq!(process.usage(ResourceClass::BufferedBytes).used, 6);
699 assert_eq!(vm.usage(ResourceClass::BufferedBytes).used, 6);
700 let error = vm.reserve(ResourceClass::BufferedBytes, 1).unwrap_err();
701 assert_eq!(error.scope, "vm-1");
702 assert_eq!(process.usage(ResourceClass::BufferedBytes).used, 6);
703 drop(reservation);
704 assert!(process.is_zero());
705 assert!(vm.is_zero());
706 }
707
708 #[test]
709 fn failed_child_admission_rolls_back_parent() {
710 let process = Arc::new(ResourceLedger::root("process", limit(10)));
711 let vm = ResourceLedger::child("vm-1", limit(2), Arc::clone(&process));
712 let error = vm.reserve(ResourceClass::BufferedBytes, 3).unwrap_err();
713 assert_eq!(error.scope, "vm-1");
714 assert!(process.is_zero());
715 assert!(vm.is_zero());
716 }
717
718 #[test]
719 fn capacity_probe_checks_parent_and_child_without_changing_usage() {
720 let process = Arc::new(ResourceLedger::root("process", limit(3)));
721 let vm = ResourceLedger::child("vm-1", limit(5), Arc::clone(&process));
722 let held = process
723 .reserve(ResourceClass::BufferedBytes, 2)
724 .expect("reserve process capacity");
725
726 assert!(vm.capacity_available(ResourceClass::BufferedBytes, 1));
727 assert!(!vm.capacity_available(ResourceClass::BufferedBytes, 2));
728 assert_eq!(process.usage(ResourceClass::BufferedBytes).used, 2);
729 assert_eq!(vm.usage(ResourceClass::BufferedBytes).used, 0);
730
731 drop(held);
732 assert!(vm.capacity_available(ResourceClass::BufferedBytes, 2));
733 }
734
735 #[test]
736 fn split_and_merge_transfer_without_counter_drift() {
737 let ledger = ResourceLedger::root("vm-1", limit(10));
738 let mut source = ledger.reserve(ResourceClass::BufferedBytes, 8).unwrap();
739 let transferred = source.split(3).unwrap();
740 assert_eq!(source.amount(), 5);
741 assert_eq!(transferred.amount(), 3);
742 assert_eq!(ledger.usage(ResourceClass::BufferedBytes).used, 8);
743 source.merge(transferred).unwrap();
744 assert_eq!(source.amount(), 8);
745 assert_eq!(ledger.usage(ResourceClass::BufferedBytes).used, 8);
746 drop(source);
747 assert!(ledger.is_zero());
748 }
749
750 #[tokio::test]
751 async fn impossible_async_reservation_returns_typed_error() {
752 let ledger = ResourceLedger::root("vm-1", limit(4));
753 let error = ledger
754 .reserve_when_available(ResourceClass::BufferedBytes, 5)
755 .await
756 .unwrap_err();
757 assert_eq!(error.resource, ResourceClass::BufferedBytes);
758 assert_eq!(error.requested, 5);
759 assert_eq!(error.limit, 4);
760 assert_eq!(
761 error.config_path,
762 "runtime.resources.maxSocketBufferedBytes"
763 );
764 }
765
766 #[tokio::test]
767 async fn child_waiter_wakes_when_only_parent_capacity_changes() {
768 let process = Arc::new(ResourceLedger::root("process", limit(1)));
769 let vm = Arc::new(ResourceLedger::child(
770 "vm-1",
771 limit(2),
772 Arc::clone(&process),
773 ));
774 let held = process
775 .reserve(ResourceClass::BufferedBytes, 1)
776 .expect("fill parent");
777 let waiting_vm = Arc::clone(&vm);
778 let waiter = tokio::spawn(async move {
779 waiting_vm
780 .reserve_when_available(ResourceClass::BufferedBytes, 1)
781 .await
782 });
783 tokio::task::yield_now().await;
784 assert!(!waiter.is_finished());
785 drop(held);
786 let reservation = tokio::time::timeout(std::time::Duration::from_secs(1), waiter)
787 .await
788 .expect("parent release must wake child waiter")
789 .expect("waiter task")
790 .expect("reservation");
791 drop(reservation);
792 assert!(process.is_zero());
793 assert!(vm.is_zero());
794 }
795
796 #[test]
797 fn accounting_underflow_latches_integrity_failure() {
798 let ledger = ResourceLedger::root("vm-1", limit(1));
799 let inner = Arc::clone(&ledger.inner);
800 let malformed = Reservation {
801 resource: ResourceClass::BufferedBytes,
802 amount: 1,
803 allocations: vec![Allocation {
804 ledger: inner,
805 resource: ResourceClass::BufferedBytes,
806 amount: 1,
807 }],
808 };
809 drop(malformed);
810 assert!(!ledger.integrity_ok());
811 assert!(ledger.is_zero());
812 }
813}