1use std::collections::BTreeMap;
4use std::fmt;
5use std::sync::{Arc, Mutex, Weak};
6
7use crate::accounting::{LimitError, Reservation, ResourceClass, ResourceLedger};
8
9pub type CapabilityId = u64;
10pub type CapabilityGeneration = u64;
14pub type SessionGeneration = u64;
15
16const NON_RECYCLING_CAPABILITY_GENERATION: CapabilityGeneration = 1;
17
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum CapabilityKind {
20 TcpSocket,
21 TcpListener,
22 UnixSocket,
23 UnixListener,
24 UdpSocket,
25 TlsTransport,
26 Http2Connection,
27 Http2Stream,
28}
29
30impl CapabilityKind {
31 pub const fn is_socket(self) -> bool {
32 matches!(
33 self,
34 Self::TcpSocket
35 | Self::TcpListener
36 | Self::UnixSocket
37 | Self::UnixListener
38 | Self::UdpSocket
39 | Self::Http2Connection
40 )
41 }
42
43 pub const fn is_connection(self) -> bool {
44 matches!(
45 self,
46 Self::TcpSocket | Self::UnixSocket | Self::Http2Connection
47 )
48 }
49}
50
51#[derive(Clone, Debug, Eq, PartialEq)]
52pub enum CapabilityBackend {
53 Native { local_id: String },
54 Kernel { socket_id: u64 },
55}
56
57#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58pub enum CapabilityLifecycle {
59 Allocating,
60 Open,
61 Closing,
62 Failed,
63 Closed,
64}
65
66#[derive(Clone, Debug, Eq, PartialEq)]
67pub struct CapabilitySnapshot {
68 pub session_generation: SessionGeneration,
69 pub id: CapabilityId,
70 pub generation: CapabilityGeneration,
71 pub kind: CapabilityKind,
72 pub backend: CapabilityBackend,
73 pub lifecycle: CapabilityLifecycle,
74 pub referenced: bool,
75}
76
77#[derive(Debug)]
78pub enum CapabilityError {
79 Limit(LimitError),
80 IdExhausted,
81 RegistryClosed,
82 Stale {
83 id: CapabilityId,
84 supplied_generation: CapabilityGeneration,
85 },
86 WrongSession {
87 id: CapabilityId,
88 expected: SessionGeneration,
89 actual: SessionGeneration,
90 },
91 WrongKind {
92 id: CapabilityId,
93 expected: CapabilityKind,
94 actual: CapabilityKind,
95 },
96 InvalidTransition {
97 id: CapabilityId,
98 from: CapabilityLifecycle,
99 to: CapabilityLifecycle,
100 },
101 Poisoned,
102}
103
104impl fmt::Display for CapabilityError {
105 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
106 match self {
107 Self::Limit(error) => error.fmt(formatter),
108 Self::IdExhausted => formatter.write_str(
109 "ERR_AGENTOS_CAPABILITY_ID_EXHAUSTED: capability id space exhausted",
110 ),
111 Self::RegistryClosed => formatter.write_str(
112 "ERR_AGENTOS_CAPABILITY_REGISTRY_CLOSED: VM capability admission is closed",
113 ),
114 Self::Stale {
115 id,
116 supplied_generation,
117 } => write!(
118 formatter,
119 "ERR_AGENTOS_STALE_CAPABILITY: capability {id} generation {supplied_generation} is not active"
120 ),
121 Self::WrongSession {
122 id,
123 expected,
124 actual,
125 } => write!(
126 formatter,
127 "ERR_AGENTOS_CAPABILITY_SESSION: capability {id} belongs to VM generation {actual}, not {expected}"
128 ),
129 Self::WrongKind {
130 id,
131 expected,
132 actual,
133 } => write!(
134 formatter,
135 "ERR_AGENTOS_CAPABILITY_KIND: capability {id} has kind {actual:?}, expected {expected:?}"
136 ),
137 Self::InvalidTransition { id, from, to } => write!(
138 formatter,
139 "ERR_AGENTOS_CAPABILITY_LIFECYCLE: capability {id} cannot transition from {from:?} to {to:?}"
140 ),
141 Self::Poisoned => formatter.write_str(
142 "ERR_AGENTOS_CAPABILITY_REGISTRY_POISONED: capability registry lock poisoned",
143 ),
144 }
145 }
146}
147
148impl std::error::Error for CapabilityError {}
149
150impl From<LimitError> for CapabilityError {
151 fn from(value: LimitError) -> Self {
152 Self::Limit(value)
153 }
154}
155
156#[derive(Debug)]
157struct CapabilityEntry {
158 snapshot: CapabilitySnapshot,
159}
160
161#[derive(Debug)]
162struct RegistryState {
163 next_id: CapabilityId,
164 open: bool,
165 pending: usize,
166 entries: BTreeMap<CapabilityId, CapabilityEntry>,
167}
168
169#[derive(Debug)]
170struct RegistryInner {
171 session_generation: SessionGeneration,
172 ledger: Arc<ResourceLedger>,
173 state: Mutex<RegistryState>,
174 admission_changed: tokio::sync::Notify,
175 settled: tokio::sync::Notify,
176}
177
178#[derive(Clone, Debug)]
179pub struct CapabilityRegistry {
180 inner: Arc<RegistryInner>,
181}
182
183impl CapabilityRegistry {
184 pub fn new(session_generation: SessionGeneration, ledger: Arc<ResourceLedger>) -> Self {
185 Self {
186 inner: Arc::new(RegistryInner {
187 session_generation,
188 ledger,
189 state: Mutex::new(RegistryState {
190 next_id: 1,
191 open: true,
192 pending: 0,
193 entries: BTreeMap::new(),
194 }),
195 admission_changed: tokio::sync::Notify::new(),
196 settled: tokio::sync::Notify::new(),
197 }),
198 }
199 }
200
201 pub fn session_generation(&self) -> SessionGeneration {
202 self.inner.session_generation
203 }
204
205 pub fn resources(&self) -> Arc<ResourceLedger> {
206 Arc::clone(&self.inner.ledger)
207 }
208
209 pub fn reserve(&self, kind: CapabilityKind) -> Result<PendingCapability, CapabilityError> {
211 {
212 let state = self
213 .inner
214 .state
215 .lock()
216 .map_err(|_| CapabilityError::Poisoned)?;
217 if !state.open {
218 return Err(CapabilityError::RegistryClosed);
219 }
220 }
221 let capability = self.inner.ledger.reserve(ResourceClass::Capabilities, 1)?;
222 let ready_handle = self.inner.ledger.reserve(ResourceClass::ReadyHandles, 1)?;
226 let socket = kind
227 .is_socket()
228 .then(|| self.inner.ledger.reserve(ResourceClass::Sockets, 1))
229 .transpose()?;
230 let connection = kind
231 .is_connection()
232 .then(|| self.inner.ledger.reserve(ResourceClass::Connections, 1))
233 .transpose()?;
234 {
235 let mut state = self
236 .inner
237 .state
238 .lock()
239 .map_err(|_| CapabilityError::Poisoned)?;
240 if !state.open {
241 return Err(CapabilityError::RegistryClosed);
242 }
243 state.pending = state
244 .pending
245 .checked_add(1)
246 .ok_or(CapabilityError::IdExhausted)?;
247 }
248 Ok(PendingCapability {
249 registry: Arc::clone(&self.inner),
250 kind,
251 reservations: vec![Some(capability), Some(ready_handle), socket, connection]
252 .into_iter()
253 .flatten()
254 .collect(),
255 counted: true,
256 })
257 }
258
259 pub async fn reserve_when_available(
263 &self,
264 kind: CapabilityKind,
265 ) -> Result<PendingCapability, CapabilityError> {
266 loop {
267 let changed = self.inner.ledger.capacity_changed();
268 let admission_changed = self.inner.admission_changed.notified();
269 match self.reserve(kind) {
270 Ok(pending) => return Ok(pending),
271 Err(CapabilityError::Limit(error)) if error.requested > error.limit => {
272 return Err(CapabilityError::Limit(error));
273 }
274 Err(CapabilityError::Limit(_)) => {
275 tokio::select! {
276 _ = changed => {}
277 _ = admission_changed => {}
278 }
279 }
280 Err(error) => return Err(error),
281 }
282 }
283 }
284
285 pub fn snapshot(&self, id: CapabilityId) -> Option<CapabilitySnapshot> {
286 self.inner
287 .state
288 .lock()
289 .ok()?
290 .entries
291 .get(&id)
292 .map(|entry| entry.snapshot.clone())
293 }
294
295 pub fn snapshots(&self) -> Vec<CapabilitySnapshot> {
296 self.inner
297 .state
298 .lock()
299 .map(|state| {
300 state
301 .entries
302 .values()
303 .map(|entry| entry.snapshot.clone())
304 .collect()
305 })
306 .unwrap_or_else(|_| {
307 eprintln!(
308 "ERR_AGENTOS_CAPABILITY_REGISTRY_POISONED: failed to snapshot capability registry"
309 );
310 Vec::new()
311 })
312 }
313
314 pub fn close_admission(&self) -> Result<(), CapabilityError> {
315 self.inner
316 .state
317 .lock()
318 .map_err(|_| CapabilityError::Poisoned)?
319 .open = false;
320 self.inner.admission_changed.notify_waiters();
321 Ok(())
322 }
323
324 pub fn active_len(&self) -> usize {
325 self.inner
326 .state
327 .lock()
328 .map(|state| state.entries.len())
329 .unwrap_or(0)
330 }
331
332 pub fn outstanding_len(&self) -> usize {
333 self.inner
334 .state
335 .lock()
336 .map(|state| state.pending.saturating_add(state.entries.len()))
337 .unwrap_or(usize::MAX)
338 }
339
340 pub async fn wait_empty(&self) {
341 loop {
342 let settled = self.inner.settled.notified();
343 if self.outstanding_len() == 0 {
344 return;
345 }
346 settled.await;
347 }
348 }
349}
350
351#[derive(Debug)]
352pub struct PendingCapability {
353 registry: Arc<RegistryInner>,
354 kind: CapabilityKind,
355 reservations: Vec<Reservation>,
356 counted: bool,
357}
358
359impl PendingCapability {
360 pub fn commit(
363 mut self,
364 backend: CapabilityBackend,
365 ) -> Result<CapabilityLease, CapabilityError> {
366 let (id, generation) = {
367 let mut state = self
368 .registry
369 .state
370 .lock()
371 .map_err(|_| CapabilityError::Poisoned)?;
372 if !state.open {
373 return Err(CapabilityError::RegistryClosed);
374 }
375 if state.pending == 0 {
376 eprintln!(
377 "ERR_AGENTOS_CAPABILITY_ACCOUNTING_UNDERFLOW: pending commit without admission"
378 );
379 return Err(CapabilityError::Poisoned);
380 }
381 let id = state.next_id;
382 let next_id = id.checked_add(1).ok_or(CapabilityError::IdExhausted)?;
383 state.pending -= 1;
384 self.counted = false;
385 state.next_id = next_id;
386 let generation = NON_RECYCLING_CAPABILITY_GENERATION;
387 let replaced = state.entries.insert(
388 id,
389 CapabilityEntry {
390 snapshot: CapabilitySnapshot {
391 session_generation: self.registry.session_generation,
392 id,
393 generation,
394 kind: self.kind,
395 backend,
396 lifecycle: CapabilityLifecycle::Open,
397 referenced: true,
398 },
399 },
400 );
401 if let Some(previous) = replaced {
402 eprintln!(
403 "ERR_AGENTOS_CAPABILITY_ID_REUSED: monotonic capability id {id} was already active"
404 );
405 state.entries.insert(id, previous);
406 return Err(CapabilityError::Poisoned);
407 }
408 (id, generation)
409 };
410 Ok(CapabilityLease {
411 registry: Arc::downgrade(&self.registry),
412 id,
413 generation,
414 reservations: std::mem::take(&mut self.reservations),
415 })
416 }
417}
418
419impl Drop for PendingCapability {
420 fn drop(&mut self) {
421 if self.counted {
422 match self.registry.state.lock() {
423 Ok(mut state) => {
424 if state.pending == 0 {
425 eprintln!(
426 "ERR_AGENTOS_CAPABILITY_ACCOUNTING_UNDERFLOW: pending reservation released at zero"
427 );
428 } else {
429 state.pending -= 1;
430 }
431 }
432 Err(_) => eprintln!(
433 "ERR_AGENTOS_CAPABILITY_REGISTRY_POISONED: pending reservation release failed"
434 ),
435 }
436 self.counted = false;
437 self.registry.settled.notify_waiters();
438 }
439 }
440}
441
442#[derive(Debug)]
443pub struct CapabilityLease {
444 registry: Weak<RegistryInner>,
445 id: CapabilityId,
446 generation: CapabilityGeneration,
447 reservations: Vec<Reservation>,
448}
449
450impl CapabilityLease {
451 pub fn id(&self) -> CapabilityId {
452 self.id
453 }
454
455 pub fn generation(&self) -> CapabilityGeneration {
456 self.generation
457 }
458
459 pub fn validate(
462 &self,
463 session_generation: SessionGeneration,
464 kind: CapabilityKind,
465 ) -> Result<(), CapabilityError> {
466 let registry = self
467 .registry
468 .upgrade()
469 .ok_or(CapabilityError::RegistryClosed)?;
470 let state = registry
471 .state
472 .lock()
473 .map_err(|_| CapabilityError::Poisoned)?;
474 let entry = state.entries.get(&self.id).ok_or(CapabilityError::Stale {
475 id: self.id,
476 supplied_generation: self.generation,
477 })?;
478 if entry.snapshot.generation != self.generation {
479 return Err(CapabilityError::Stale {
480 id: self.id,
481 supplied_generation: self.generation,
482 });
483 }
484 if entry.snapshot.session_generation != session_generation {
485 return Err(CapabilityError::WrongSession {
486 id: self.id,
487 expected: session_generation,
488 actual: entry.snapshot.session_generation,
489 });
490 }
491 if entry.snapshot.kind != kind {
492 return Err(CapabilityError::WrongKind {
493 id: self.id,
494 expected: kind,
495 actual: entry.snapshot.kind,
496 });
497 }
498 Ok(())
499 }
500
501 pub fn set_referenced(&self, referenced: bool) -> Result<(), CapabilityError> {
502 self.update(|snapshot| snapshot.referenced = referenced)
503 }
504
505 pub fn transition(&self, to: CapabilityLifecycle) -> Result<(), CapabilityError> {
506 let registry = self
507 .registry
508 .upgrade()
509 .ok_or(CapabilityError::RegistryClosed)?;
510 let mut state = registry
511 .state
512 .lock()
513 .map_err(|_| CapabilityError::Poisoned)?;
514 let entry = state
515 .entries
516 .get_mut(&self.id)
517 .ok_or(CapabilityError::Stale {
518 id: self.id,
519 supplied_generation: self.generation,
520 })?;
521 if entry.snapshot.generation != self.generation {
522 return Err(CapabilityError::Stale {
523 id: self.id,
524 supplied_generation: self.generation,
525 });
526 }
527 let from = entry.snapshot.lifecycle;
528 if !valid_transition(from, to) {
529 return Err(CapabilityError::InvalidTransition {
530 id: self.id,
531 from,
532 to,
533 });
534 }
535 entry.snapshot.lifecycle = to;
536 Ok(())
537 }
538
539 fn update(&self, update: impl FnOnce(&mut CapabilitySnapshot)) -> Result<(), CapabilityError> {
540 let registry = self
541 .registry
542 .upgrade()
543 .ok_or(CapabilityError::RegistryClosed)?;
544 let mut state = registry
545 .state
546 .lock()
547 .map_err(|_| CapabilityError::Poisoned)?;
548 let entry = state
549 .entries
550 .get_mut(&self.id)
551 .ok_or(CapabilityError::Stale {
552 id: self.id,
553 supplied_generation: self.generation,
554 })?;
555 if entry.snapshot.generation != self.generation {
556 return Err(CapabilityError::Stale {
557 id: self.id,
558 supplied_generation: self.generation,
559 });
560 }
561 update(&mut entry.snapshot);
562 Ok(())
563 }
564}
565
566fn valid_transition(from: CapabilityLifecycle, to: CapabilityLifecycle) -> bool {
567 from == to
568 || matches!(
569 (from, to),
570 (CapabilityLifecycle::Open, CapabilityLifecycle::Closing)
571 | (CapabilityLifecycle::Open, CapabilityLifecycle::Failed)
572 | (CapabilityLifecycle::Open, CapabilityLifecycle::Closed)
573 | (CapabilityLifecycle::Closing, CapabilityLifecycle::Closed)
574 | (CapabilityLifecycle::Failed, CapabilityLifecycle::Closed)
575 )
576}
577
578impl Drop for CapabilityLease {
579 fn drop(&mut self) {
580 if let Some(registry) = self.registry.upgrade() {
581 match registry.state.lock() {
582 Ok(mut state) => {
583 let remove = state
584 .entries
585 .get(&self.id)
586 .is_some_and(|entry| entry.snapshot.generation == self.generation);
587 if remove {
588 state.entries.remove(&self.id);
589 } else {
590 eprintln!(
591 "ERR_AGENTOS_CAPABILITY_RELEASE_STALE: capability={} generation={}",
592 self.id, self.generation
593 );
594 }
595 }
596 Err(_) => eprintln!(
597 "ERR_AGENTOS_CAPABILITY_REGISTRY_POISONED: capability={} release failed",
598 self.id
599 ),
600 }
601 registry.settled.notify_waiters();
602 }
603 self.reservations.clear();
604 }
605}
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610 use crate::accounting::ResourceLimit;
611
612 fn registry(maximum: usize) -> (Arc<ResourceLedger>, CapabilityRegistry) {
613 let ledger = Arc::new(ResourceLedger::root(
614 "vm-1",
615 [
616 (
617 ResourceClass::Capabilities,
618 ResourceLimit::new(maximum, "runtime.capabilities.maxPerVm"),
619 ),
620 (
621 ResourceClass::Sockets,
622 ResourceLimit::new(maximum, "runtime.resources.maxSockets"),
623 ),
624 (
625 ResourceClass::Connections,
626 ResourceLimit::new(maximum, "runtime.resources.maxConnections"),
627 ),
628 ],
629 ));
630 let registry = CapabilityRegistry::new(7, Arc::clone(&ledger));
631 (ledger, registry)
632 }
633
634 #[test]
635 fn admission_precedes_commit_and_drop_reconciles() {
636 let (ledger, registry) = registry(1);
637 let pending = registry.reserve(CapabilityKind::TcpSocket).unwrap();
638 assert_eq!(ledger.usage(ResourceClass::Sockets).used, 1);
639 assert!(registry.reserve(CapabilityKind::TcpSocket).is_err());
640 let lease = pending
641 .commit(CapabilityBackend::Native {
642 local_id: String::from("socket-1"),
643 })
644 .unwrap();
645 assert_eq!(registry.active_len(), 1);
646 assert_eq!(registry.snapshot(lease.id()).unwrap().session_generation, 7);
647 drop(lease);
648 assert_eq!(registry.active_len(), 0);
649 assert!(ledger.is_zero());
650 }
651
652 #[test]
653 fn failed_backend_allocation_rolls_back_pending_reservation() {
654 let (ledger, registry) = registry(1);
655 drop(registry.reserve(CapabilityKind::UdpSocket).unwrap());
656 assert!(ledger.is_zero());
657 assert_eq!(registry.active_len(), 0);
658 }
659
660 #[test]
661 fn alias_validation_rejects_wrong_vm_generation_and_kind() {
662 let (_ledger, registry) = registry(1);
663 let lease = registry
664 .reserve(CapabilityKind::TcpSocket)
665 .expect("reserve TCP capability")
666 .commit(CapabilityBackend::Native {
667 local_id: String::from("socket-1"),
668 })
669 .expect("commit TCP capability");
670
671 lease
672 .validate(7, CapabilityKind::TcpSocket)
673 .expect("matching generation and kind");
674 assert!(matches!(
675 lease.validate(8, CapabilityKind::TcpSocket),
676 Err(CapabilityError::WrongSession {
677 expected: 8,
678 actual: 7,
679 ..
680 })
681 ));
682 assert!(matches!(
683 lease.validate(7, CapabilityKind::UdpSocket),
684 Err(CapabilityError::WrongKind {
685 expected: CapabilityKind::UdpSocket,
686 actual: CapabilityKind::TcpSocket,
687 ..
688 })
689 ));
690 }
691
692 #[test]
693 fn close_admission_rejects_pending_commit_without_leaking() {
694 let (ledger, registry) = registry(1);
695 let pending = registry.reserve(CapabilityKind::TcpListener).unwrap();
696 registry.close_admission().unwrap();
697 assert!(pending
698 .commit(CapabilityBackend::Kernel { socket_id: 4 })
699 .is_err());
700 assert!(ledger.is_zero());
701 }
702
703 #[tokio::test]
704 async fn close_admission_wakes_capacity_waiters_with_typed_error() {
705 let (ledger, registry) = registry(1);
706 let held = registry
707 .reserve(CapabilityKind::TcpSocket)
708 .expect("fill registry");
709 let waiting_registry = registry.clone();
710 let waiter = tokio::spawn(async move {
711 waiting_registry
712 .reserve_when_available(CapabilityKind::TcpSocket)
713 .await
714 });
715 tokio::task::yield_now().await;
716 assert!(!waiter.is_finished());
717 registry.close_admission().expect("close admission");
718 let error = tokio::time::timeout(std::time::Duration::from_secs(1), waiter)
719 .await
720 .expect("close must wake waiter")
721 .expect("waiter task")
722 .expect_err("closed registry must reject admission");
723 assert!(matches!(error, CapabilityError::RegistryClosed));
724 drop(held);
725 assert!(ledger.is_zero());
726 }
727
728 #[tokio::test]
729 async fn non_retryable_zero_limit_returns_without_waiting_or_leaking() {
730 let ledger = Arc::new(ResourceLedger::root(
731 "vm-zero-sockets",
732 [
733 (
734 ResourceClass::Capabilities,
735 ResourceLimit::new(1, "runtime.capabilities.maxPerVm"),
736 ),
737 (
738 ResourceClass::Sockets,
739 ResourceLimit::new(0, "runtime.resources.maxSockets"),
740 ),
741 (
742 ResourceClass::Connections,
743 ResourceLimit::new(1, "runtime.resources.maxConnections"),
744 ),
745 ],
746 ));
747 let registry = CapabilityRegistry::new(7, Arc::clone(&ledger));
748
749 let error = tokio::time::timeout(
750 std::time::Duration::from_millis(100),
751 registry.reserve_when_available(CapabilityKind::TcpSocket),
752 )
753 .await
754 .expect("an impossible admission must not wait for capacity")
755 .expect_err("zero socket limit must reject admission");
756 let CapabilityError::Limit(error) = error else {
757 panic!("expected typed resource limit error");
758 };
759 assert_eq!(error.resource, ResourceClass::Sockets);
760 assert_eq!(error.requested, 1);
761 assert_eq!(error.limit, 0);
762 assert_eq!(error.config_path, "runtime.resources.maxSockets");
763 assert!(ledger.is_zero());
764 assert_eq!(registry.outstanding_len(), 0);
765 }
766
767 #[test]
768 fn capability_ids_are_monotonic_and_never_recycled() {
769 let (_ledger, registry) = registry(1);
770 let first = registry
771 .reserve(CapabilityKind::UdpSocket)
772 .expect("first reservation")
773 .commit(CapabilityBackend::Native {
774 local_id: String::from("udp-1"),
775 })
776 .expect("first capability");
777 assert_eq!(first.id(), 1);
778 assert_eq!(first.generation(), NON_RECYCLING_CAPABILITY_GENERATION);
779 drop(first);
780
781 let second = registry
782 .reserve(CapabilityKind::UdpSocket)
783 .expect("second reservation")
784 .commit(CapabilityBackend::Native {
785 local_id: String::from("udp-2"),
786 })
787 .expect("second capability");
788 assert_eq!(second.id(), 2);
789 assert_eq!(second.generation(), NON_RECYCLING_CAPABILITY_GENERATION);
790 assert!(registry.snapshot(1).is_none());
791 }
792
793 #[tokio::test]
794 async fn wait_empty_includes_pending_and_committed_ownership() {
795 let (ledger, registry) = registry(2);
796 let pending = registry
797 .reserve(CapabilityKind::UdpSocket)
798 .expect("pending capability");
799 let lease = registry
800 .reserve(CapabilityKind::TcpListener)
801 .expect("committed capability")
802 .commit(CapabilityBackend::Kernel { socket_id: 8 })
803 .expect("commit");
804 assert_eq!(registry.outstanding_len(), 2);
805 let waiter = tokio::spawn({
806 let registry = registry.clone();
807 async move { registry.wait_empty().await }
808 });
809 tokio::task::yield_now().await;
810 assert!(!waiter.is_finished());
811 drop(pending);
812 tokio::task::yield_now().await;
813 assert!(!waiter.is_finished());
814 drop(lease);
815 tokio::time::timeout(std::time::Duration::from_secs(1), waiter)
816 .await
817 .expect("empty notification")
818 .expect("wait task");
819 assert!(ledger.is_zero());
820 }
821
822 #[tokio::test]
823 async fn id_exhaustion_reconciles_pending_waiters() {
824 let (_, registry) = registry(1);
825 let pending = registry
826 .reserve(CapabilityKind::TcpSocket)
827 .expect("pending capability");
828 registry.inner.state.lock().expect("registry state").next_id = u64::MAX;
829 let waiter = tokio::spawn({
830 let registry = registry.clone();
831 async move { registry.wait_empty().await }
832 });
833 tokio::task::yield_now().await;
834
835 let error = pending
836 .commit(CapabilityBackend::Native {
837 local_id: String::from("exhausted"),
838 })
839 .expect_err("exhausted id must reject commit");
840 assert!(matches!(error, CapabilityError::IdExhausted));
841 tokio::time::timeout(std::time::Duration::from_secs(1), waiter)
842 .await
843 .expect("pending release must wake waiter")
844 .expect("wait task");
845 assert_eq!(registry.outstanding_len(), 0);
846 }
847}