Skip to main content

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}