nexus_timer/store.rs
1//! Slab storage traits for instance-based (non-ZST) slabs.
2//!
3//! Single trait hierarchy: [`SlabStore`] provides allocation and deallocation.
4//! [`BoundedStore`] extends it with fallible allocation for callers who want
5//! graceful error handling.
6//!
7//! # Allocation and OOM
8//!
9//! [`SlabStore::alloc`] always returns a valid slot. For unbounded slabs this
10//! is guaranteed by growth. For bounded slabs, exceeding capacity **panics**.
11//!
12//! This is a deliberate design choice: bounded capacity is a deployment
13//! constraint, not a runtime negotiation. If you hit the limit, your capacity
14//! planning is wrong and the system should fail loudly — the same way a
15//! process panics on OOM. Silently dropping timers or events is worse than
16//! crashing.
17//!
18//! Use [`BoundedStore::try_alloc`] if you need graceful error handling at
19//! specific call sites.
20
21use nexus_slab::Full;
22use nexus_slab::Slot;
23use nexus_slab::{bounded, unbounded};
24
25// Re-export concrete slab types so downstream crates (nexus-rt) can name
26// them in type defaults without adding nexus-slab as a direct dependency.
27pub use bounded::Slab as BoundedSlab;
28pub use unbounded::Slab as UnboundedSlab;
29
30// =============================================================================
31// Traits
32// =============================================================================
33
34/// Base trait for slab storage — allocation, deallocation, and value extraction.
35///
36/// # Allocation
37///
38/// [`alloc`](Self::alloc) always returns a valid slot:
39///
40/// - **Unbounded slabs** grow as needed — allocation never fails.
41/// - **Bounded slabs** panic if capacity is exceeded. This is intentional:
42/// running out of pre-allocated capacity is a capacity planning error,
43/// equivalent to OOM. The system should crash loudly rather than silently
44/// drop work.
45///
46/// For fallible allocation on bounded slabs, use [`BoundedStore::try_alloc`].
47///
48/// # Safety
49///
50/// Implementors must uphold:
51///
52/// - `free` must drop the value and return the slot to the freelist.
53/// - `take` must move the value out and return the slot to the freelist.
54/// - The slot must have been allocated from `self`.
55pub unsafe trait SlabStore {
56 /// The type stored in each slot.
57 type Item;
58
59 /// Allocates a slot with the given value.
60 ///
61 /// # Panics
62 ///
63 /// Panics if the store is at capacity (bounded slabs only). This is a
64 /// capacity planning error — size your slabs for peak load.
65 fn alloc(&self, value: Self::Item) -> Slot<Self::Item>;
66
67 /// Drops the value and returns the slot to the freelist.
68 fn free(&self, slot: Slot<Self::Item>);
69
70 /// Moves the value out and returns the slot to the freelist.
71 fn take(&self, slot: Slot<Self::Item>) -> Self::Item;
72}
73
74/// Bounded (fixed-capacity) storage — provides fallible allocation.
75///
76/// Use [`try_alloc`](Self::try_alloc) when you need graceful error handling.
77/// For the common case where capacity exhaustion is a fatal error, use
78/// [`SlabStore::alloc`] directly (it panics on bounded-full).
79pub trait BoundedStore: SlabStore {
80 /// Attempts to allocate a slot with the given value.
81 ///
82 /// Returns `Err(Full(value))` if storage is at capacity.
83 fn try_alloc(&self, value: Self::Item) -> Result<Slot<Self::Item>, Full<Self::Item>>;
84}
85
86// =============================================================================
87// Impls for bounded::Slab
88// =============================================================================
89
90// SAFETY: bounded::Slab::free drops the value and returns to freelist.
91// bounded::Slab::take moves value out and returns to freelist.
92unsafe impl<T> SlabStore for bounded::Slab<T> {
93 type Item = T;
94
95 #[inline]
96 fn alloc(&self, value: T) -> Slot<T> {
97 self.try_alloc(value).unwrap_or_else(|full| {
98 // Drop the value inside Full, then panic.
99 drop(full);
100 panic!(
101 "bounded slab: capacity exceeded (type: {})",
102 std::any::type_name::<T>(),
103 );
104 })
105 }
106
107 #[inline]
108 fn free(&self, slot: Slot<T>) {
109 bounded::Slab::free(self, slot)
110 }
111
112 #[inline]
113 fn take(&self, slot: Slot<T>) -> T {
114 bounded::Slab::take(self, slot)
115 }
116}
117
118impl<T> BoundedStore for bounded::Slab<T> {
119 #[inline]
120 fn try_alloc(&self, value: T) -> Result<Slot<T>, Full<T>> {
121 bounded::Slab::try_alloc(self, value)
122 }
123}
124
125// =============================================================================
126// Impls for unbounded::Slab
127// =============================================================================
128
129// SAFETY: unbounded::Slab::free drops the value and returns to freelist.
130// unbounded::Slab::take moves value out and returns to freelist.
131unsafe impl<T> SlabStore for unbounded::Slab<T> {
132 type Item = T;
133
134 #[inline]
135 fn alloc(&self, value: T) -> Slot<T> {
136 unbounded::Slab::alloc(self, value)
137 }
138
139 #[inline]
140 fn free(&self, slot: Slot<T>) {
141 unbounded::Slab::free(self, slot)
142 }
143
144 #[inline]
145 fn take(&self, slot: Slot<T>) -> T {
146 unbounded::Slab::take(self, slot)
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn bounded_store_roundtrip() {
156 // SAFETY: single-threaded test, slab contract upheld
157 let slab = unsafe { bounded::Slab::<u64>::with_capacity(16) };
158 let slot = SlabStore::alloc(&slab, 42);
159 assert_eq!(*slot, 42);
160 let val = SlabStore::take(&slab, slot);
161 assert_eq!(val, 42);
162 }
163
164 #[test]
165 fn unbounded_store_roundtrip() {
166 // SAFETY: single-threaded test, slab contract upheld
167 let slab = unsafe { unbounded::Slab::<u64>::with_chunk_capacity(16) };
168 let slot = SlabStore::alloc(&slab, 99);
169 assert_eq!(*slot, 99);
170 SlabStore::free(&slab, slot);
171 }
172
173 #[test]
174 fn bounded_try_alloc_graceful() {
175 // SAFETY: single-threaded test, slab contract upheld
176 let slab = unsafe { bounded::Slab::<u64>::with_capacity(1) };
177 let s1 = BoundedStore::try_alloc(&slab, 1).unwrap();
178 let err = BoundedStore::try_alloc(&slab, 2).unwrap_err();
179 assert_eq!(err.into_inner(), 2);
180 SlabStore::free(&slab, s1);
181 }
182
183 #[test]
184 #[should_panic(expected = "capacity exceeded")]
185 fn bounded_alloc_panics_on_full() {
186 // SAFETY: single-threaded test, slab contract upheld
187 let slab = unsafe { bounded::Slab::<u64>::with_capacity(1) };
188 let _s1 = SlabStore::alloc(&slab, 1);
189 let _s2 = SlabStore::alloc(&slab, 2); // panics
190 }
191}