Skip to main content

kevy_ring/
lib.rs

1//! kevy-ring — a lock-free, bounded **single-producer / single-consumer** ring.
2//!
3//! One producer pushes, one consumer pops, with no locks and no per-message
4//! allocation: a fixed power-of-two slot array plus two monotonic cursors. It is
5//! the cross-core transport primitive for [kevy-rt]'s shared-nothing,
6//! thread-per-core runtime (the Seastar/Scylla model) — each ordered pair of
7//! cores gets its own ring, so a hot reactor never contends a lock on the hop.
8//!
9//! The single-producer / single-consumer contract is enforced *by the type
10//! system*: [`push`](Producer::push) and [`pop`](Consumer::pop) take `&mut self`,
11//! and [`Producer`]/[`Consumer`] are distinct owned halves, so the compiler
12//! guarantees at most one thread pushes and one pops. That keeps the ordering
13//! requirements minimal — a single `Release`/`Acquire` pair per operation.
14//!
15//! Pure Rust, zero dependencies. The lock-free buffer needs `UnsafeCell` +
16//! atomics, so this crate is not `#![forbid(unsafe_code)]`; every `unsafe` block
17//! documents the SPSC invariant it relies on (no C, no FFI — see the kevy
18//! pure-Rust principle). Part of the [kevy] key–value server.
19//!
20//! [kevy]: https://crates.io/crates/kevy
21//! [kevy-rt]: https://crates.io/crates/kevy-rt
22//!
23//! # Example
24//!
25//! ```
26//! let (mut tx, mut rx) = kevy_ring::ring::<u32>(4);
27//! assert!(tx.push(1).is_ok());
28//! assert!(tx.push(2).is_ok());
29//! assert_eq!(rx.pop(), Some(1));
30//! assert_eq!(rx.pop(), Some(2));
31//! assert_eq!(rx.pop(), None);
32//! ```
33//!
34//! Producer and consumer move to different threads:
35//!
36//! ```
37//! let (mut tx, mut rx) = kevy_ring::ring::<u64>(1024);
38//! let prod = std::thread::spawn(move || {
39//!     for i in 0..10_000u64 {
40//!         while tx.push(i).is_err() {
41//!             std::hint::spin_loop(); // ring full — let the consumer drain
42//!         }
43//!     }
44//! });
45//! let mut next = 0u64;
46//! while next < 10_000 {
47//!     if let Some(v) = rx.pop() {
48//!         assert_eq!(v, next); // FIFO, nothing lost or reordered
49//!         next += 1;
50//!     }
51//! }
52//! prod.join().unwrap();
53//! ```
54
55#![warn(missing_docs)]
56// `--cfg loom` is a known custom cfg used by tests/loom.rs to swap atomics +
57// UnsafeCell + Arc for loom's instrumented versions. Tell rustc not to
58// warn about the unrecognized cfg name on normal builds.
59#![allow(unexpected_cfgs)]
60
61// Loom-compat shim: under `--cfg loom` the model checker substitutes its own
62// instrumented atomics + `UnsafeCell` + `Arc`, and `tests/loom.rs` enumerates
63// every possible interleaving of the SPSC algorithm against this exact crate.
64// Under a normal build the imports are the std originals — zero overhead.
65//
66// `UnsafeCellExt::with_mut` unifies the two APIs: loom's `UnsafeCell` only
67// exposes `with_mut`; std's exposes `.get()`. We give std a tiny shim that
68// delegates to `.get()` so the same call-site (`cell.with_mut(|p| ...)`) works
69// in both modes. No production code change in the hot path.
70mod sync {
71    #[cfg(loom)]
72    pub use loom::cell::UnsafeCell;
73    #[cfg(loom)]
74    pub use loom::sync::Arc;
75    #[cfg(loom)]
76    pub use loom::sync::atomic::{AtomicUsize, Ordering};
77
78    #[cfg(not(loom))]
79    pub use std::cell::UnsafeCell;
80    #[cfg(not(loom))]
81    pub use std::sync::Arc;
82    #[cfg(not(loom))]
83    pub use std::sync::atomic::{AtomicUsize, Ordering};
84}
85
86#[cfg(not(loom))]
87trait UnsafeCellExt<T> {
88    fn with_mut<F, R>(&self, f: F) -> R
89    where
90        F: FnOnce(*mut T) -> R;
91}
92#[cfg(not(loom))]
93impl<T> UnsafeCellExt<T> for std::cell::UnsafeCell<T> {
94    // The shim must vanish at every SPSC-ring hot site: with_mut wraps a
95    // single `UnsafeCell::get()` (one ptr load), so loom-vs-std API parity
96    // can't justify any call overhead in the producer/consumer fast loops.
97    #[allow(clippy::inline_always)]
98    #[inline(always)]
99    fn with_mut<F, R>(&self, f: F) -> R
100    where
101        F: FnOnce(*mut T) -> R,
102    {
103        f(self.get())
104    }
105}
106
107use std::mem::MaybeUninit;
108use sync::{Arc, AtomicUsize, Ordering, UnsafeCell};
109
110/// Pad to a cache line so the producer's `tail` and consumer's `head` never
111/// share one — otherwise each side's store would invalidate the other's cache
112/// line (false sharing) and erase the point of a lock-free ring. 128 bytes
113/// covers Apple-silicon's 128-byte prefetch pairs as well as x86's 64-byte line.
114#[repr(align(128))]
115struct CachePadded<T>(T);
116
117struct Ring<T> {
118    /// `capacity` slots; only indices in `[head, tail)` (mod capacity) are init.
119    buf: Box<[UnsafeCell<MaybeUninit<T>>]>,
120    /// `capacity - 1`; `capacity` is a power of two so `idx & mask` wraps.
121    mask: usize,
122    /// Next index to pop. Owned by the consumer; read by the producer.
123    head: CachePadded<AtomicUsize>,
124    /// Next index to push. Owned by the producer; read by the consumer.
125    tail: CachePadded<AtomicUsize>,
126}
127
128// SAFETY: the SPSC contract (enforced by `&mut self` on push/pop and the split
129// Producer/Consumer halves) means the producer only ever writes the slot at
130// `tail` and advances `tail`, while the consumer only reads the slot at `head`
131// and advances `head`. Those index ranges are disjoint, so the `UnsafeCell`
132// accesses never alias. A `T: Send` may thus cross the producer→consumer thread
133// boundary, making the shared `Ring` safe to `Send` and `Sync`.
134unsafe impl<T: Send> Send for Ring<T> {}
135unsafe impl<T: Send> Sync for Ring<T> {}
136
137impl<T> Ring<T> {
138    fn with_capacity(cap: usize) -> Self {
139        // At least 2 slots; round up to a power of two for masking.
140        let cap = cap.max(2).next_power_of_two();
141        let mut v = Vec::with_capacity(cap);
142        for _ in 0..cap {
143            v.push(UnsafeCell::new(MaybeUninit::uninit()));
144        }
145        Ring {
146            buf: v.into_boxed_slice(),
147            mask: cap - 1,
148            head: CachePadded(AtomicUsize::new(0)),
149            tail: CachePadded(AtomicUsize::new(0)),
150        }
151    }
152}
153
154impl<T> Drop for Ring<T> {
155    fn drop(&mut self) {
156        // Drop the elements still queued (indices `[head, tail)`); the rest are
157        // uninitialized and must not be touched.
158        let head = self.head.0.load(Ordering::Relaxed);
159        let tail = self.tail.0.load(Ordering::Relaxed);
160        let mut i = head;
161        while i != tail {
162            // SAFETY: `i` is in `[head, tail)`, so this slot holds an initialized
163            // `T` that no one else will read (we have `&mut self`).
164            self.buf[i & self.mask].with_mut(|p| unsafe {
165                (*p).assume_init_drop();
166            });
167            i = i.wrapping_add(1);
168        }
169    }
170}
171
172/// The sending half. `Send` (move to the producer thread); only this half pushes.
173pub struct Producer<T> {
174    inner: Arc<Ring<T>>,
175    /// Cached snapshot of the consumer's `head`. Stale-OK: a value the
176    /// consumer has already advanced past is still safe to treat as "head"
177    /// (the available-slot count we compute is then a conservative lower
178    /// bound, never letting us overwrite a live slot). Refreshed from the
179    /// shared `head` only when the cached count says the ring is full.
180    /// This is the SPSC fast-path lever — it amortises the cross-cache-line
181    /// `Acquire` load on the consumer's cursor over many pushes.
182    head_cache: usize,
183}
184
185/// The receiving half. `Send` (move to the consumer thread); only this half pops.
186pub struct Consumer<T> {
187    inner: Arc<Ring<T>>,
188    /// Cached snapshot of the producer's `tail`. Stale-OK in the same way as
189    /// [`Producer::head_cache`]: a value below the truth still lets us pop
190    /// safely (we just see fewer items than really exist; the next refresh
191    /// catches up). Refreshed only when the cached count says the ring is
192    /// empty.
193    tail_cache: usize,
194}
195
196/// Create a ring holding at least `capacity` items (rounded up to a power of
197/// two, minimum 2), returning its producer and consumer halves.
198pub fn ring<T>(capacity: usize) -> (Producer<T>, Consumer<T>) {
199    let r = Arc::new(Ring::with_capacity(capacity));
200    (
201        Producer {
202            inner: r.clone(),
203            head_cache: 0,
204        },
205        Consumer {
206            inner: r,
207            tail_cache: 0,
208        },
209    )
210}
211
212impl<T> Producer<T> {
213    /// Push `val`. Returns `Err(val)` (handing the value back) if the ring is
214    /// full, so the caller can retry after the consumer drains.
215    pub fn push(&mut self, val: T) -> Result<(), T> {
216        let r = &*self.inner;
217        // `tail` is ours: a plain Relaxed load suffices.
218        let tail = r.tail.0.load(Ordering::Relaxed);
219        // Fast path: trust the cached consumer head. If the cache says we
220        // have room, skip the shared `Acquire` load entirely.
221        if tail.wrapping_sub(self.head_cache) > r.mask {
222            // Cache says full — refresh from the shared cursor. `Acquire`
223            // pairs with the consumer's `Release` store of `head` so we
224            // observe slots it has freed before we reuse them.
225            self.head_cache = r.head.0.load(Ordering::Acquire);
226            if tail.wrapping_sub(self.head_cache) > r.mask {
227                return Err(val); // really full
228            }
229        }
230        // SAFETY: slot `tail & mask` is free (outside `[head, tail)`); we are
231        // the only producer, so no one else writes it.
232        r.buf[tail & r.mask].with_mut(|p| unsafe {
233            (*p).write(val);
234        });
235        // `Release` publishes the slot write to the consumer's `Acquire` of `tail`.
236        r.tail.0.store(tail.wrapping_add(1), Ordering::Release);
237        Ok(())
238    }
239
240    /// Whether the next [`push`](Self::push) would fail (ring full). Advisory:
241    /// the consumer may free a slot immediately after this returns.
242    pub fn is_full(&self) -> bool {
243        let r = &*self.inner;
244        let tail = r.tail.0.load(Ordering::Relaxed);
245        let head = r.head.0.load(Ordering::Acquire);
246        tail.wrapping_sub(head) > r.mask
247    }
248
249    /// Total slot count (a power of two ≥ 2).
250    pub fn capacity(&self) -> usize {
251        self.inner.mask + 1
252    }
253}
254
255impl<T> Consumer<T> {
256    /// Pop the oldest item, or `None` if the ring is empty.
257    pub fn pop(&mut self) -> Option<T> {
258        let r = &*self.inner;
259        // `head` is ours: Relaxed.
260        let head = r.head.0.load(Ordering::Relaxed);
261        // Fast path: trust the cached producer tail. If the cache says we
262        // have items, skip the shared `Acquire` load entirely.
263        if head == self.tail_cache {
264            // Cache says empty — refresh from the shared cursor. `Acquire`
265            // pairs with the producer's `Release` store of `tail` so the
266            // slot write is visible before we read it.
267            self.tail_cache = r.tail.0.load(Ordering::Acquire);
268            if head == self.tail_cache {
269                return None; // really empty
270            }
271        }
272        // SAFETY: slot `head & mask` is in `[head, tail)`, initialized by the
273        // producer; we are the only consumer, so we read it exactly once.
274        let val = r.buf[head & r.mask].with_mut(|p| unsafe { (*p).assume_init_read() });
275        // `Release` frees the slot for the producer's `Acquire` of `head`.
276        r.head.0.store(head.wrapping_add(1), Ordering::Release);
277        Some(val)
278    }
279
280    /// Whether the next [`pop`](Self::pop) would return `None` (ring empty).
281    /// Advisory: the producer may push immediately after this returns.
282    pub fn is_empty(&self) -> bool {
283        let r = &*self.inner;
284        let head = r.head.0.load(Ordering::Relaxed);
285        let tail = r.tail.0.load(Ordering::Acquire);
286        head == tail
287    }
288
289    /// Number of items currently queued. Advisory under concurrent producing.
290    pub fn len(&self) -> usize {
291        let r = &*self.inner;
292        let tail = r.tail.0.load(Ordering::Acquire);
293        let head = r.head.0.load(Ordering::Relaxed);
294        tail.wrapping_sub(head)
295    }
296
297    /// Total slot count (a power of two ≥ 2).
298    pub fn capacity(&self) -> usize {
299        self.inner.mask + 1
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306    use std::sync::atomic::{AtomicBool, Ordering};
307
308    #[test]
309    fn capacity_rounds_up_to_power_of_two() {
310        let (tx, rx) = ring::<u8>(3);
311        assert_eq!(tx.capacity(), 4);
312        // Consumer-side capacity must report the same slot count as the
313        // producer (both inspect the shared mask).
314        assert_eq!(rx.capacity(), tx.capacity());
315        let (tx, _rx) = ring::<u8>(1);
316        assert_eq!(tx.capacity(), 2); // minimum
317        let (tx, _rx) = ring::<u8>(1024);
318        assert_eq!(tx.capacity(), 1024);
319    }
320
321    #[test]
322    fn fifo_order_and_full_empty() {
323        let (mut tx, mut rx) = ring::<u32>(4); // 4 slots
324        assert!(rx.is_empty());
325        for i in 0..4 {
326            assert!(tx.push(i).is_ok());
327        }
328        assert!(tx.is_full());
329        assert_eq!(tx.push(99), Err(99)); // full → handed back
330        for i in 0..4 {
331            assert_eq!(rx.pop(), Some(i)); // FIFO
332        }
333        assert_eq!(rx.pop(), None);
334        assert!(rx.is_empty());
335    }
336
337    #[test]
338    fn wraps_around_many_times() {
339        // Push/pop far more than capacity to exercise index wrap.
340        let (mut tx, mut rx) = ring::<usize>(2);
341        for i in 0..10_000 {
342            assert!(tx.push(i).is_ok());
343            assert_eq!(rx.pop(), Some(i));
344        }
345        assert_eq!(rx.pop(), None);
346    }
347
348    #[test]
349    fn len_tracks_occupancy() {
350        let (mut tx, mut rx) = ring::<u8>(8);
351        assert_eq!(rx.len(), 0);
352        tx.push(1).unwrap();
353        tx.push(2).unwrap();
354        assert_eq!(rx.len(), 2);
355        rx.pop().unwrap();
356        assert_eq!(rx.len(), 1);
357    }
358
359    use std::sync::Arc as StdArc;
360
361    // Drop-counting payload used by `drops_queued_elements_exactly_once`.
362    struct Bomb(StdArc<AtomicUsize>);
363    impl Drop for Bomb {
364        fn drop(&mut self) {
365            self.0.fetch_add(1, Ordering::SeqCst);
366        }
367    }
368
369    #[test]
370    fn drops_queued_elements_exactly_once() {
371        // A payload that bumps a shared counter on drop; verify the ring's Drop
372        // releases exactly the still-queued items (no leak, no double free).
373        let dropped = StdArc::new(AtomicUsize::new(0));
374        {
375            let (mut tx, mut rx) = ring::<Bomb>(8);
376            for _ in 0..5 {
377                assert!(tx.push(Bomb(dropped.clone())).is_ok());
378            }
379            // Consume 2 (those drop now), leave 3 queued for the ring's Drop.
380            drop(rx.pop());
381            drop(rx.pop());
382            assert_eq!(dropped.load(Ordering::SeqCst), 2);
383            drop(tx);
384            drop(rx); // last handle → Ring dropped → remaining 3 dropped
385        }
386        assert_eq!(dropped.load(Ordering::SeqCst), 5);
387    }
388
389    #[test]
390    fn spsc_stress_across_threads() {
391        // Producer and consumer on separate threads; a small ring forces many
392        // full/empty transitions. Every item must arrive exactly once, in order.
393        // Miri interprets ~1000x slower than native; 2k iterations still cross
394        // the full/empty boundary dozens of times, which is what the race
395        // detector needs — large N only adds wall-clock, not coverage.
396        const N: u64 = if cfg!(miri) { 2_000 } else { 1_000_000 };
397        let (mut tx, mut rx) = ring::<u64>(64);
398        let producer = std::thread::spawn(move || {
399            for i in 0..N {
400                while tx.push(i).is_err() {
401                    std::hint::spin_loop();
402                }
403            }
404        });
405        let mut next = 0u64;
406        while next < N {
407            match rx.pop() {
408                Some(v) => {
409                    assert_eq!(v, next, "out-of-order or lost value");
410                    next += 1;
411                }
412                None => std::hint::spin_loop(),
413            }
414        }
415        producer.join().unwrap();
416        assert_eq!(next, N);
417    }
418
419    #[test]
420    fn stress_with_intermittent_consumer() {
421        // Consumer occasionally stalls so the ring fills and the producer must
422        // back off — exercises the full path under real contention.
423        // Small N under miri for the same reason as `spsc_stress_across_threads`.
424        const N: u64 = if cfg!(miri) { 2_000 } else { 200_000 };
425        let (mut tx, mut rx) = ring::<u64>(16);
426        let done = Arc::new(AtomicBool::new(false));
427        let done_p = done.clone();
428        let producer = std::thread::spawn(move || {
429            for i in 0..N {
430                while tx.push(i).is_err() {
431                    std::thread::yield_now();
432                }
433            }
434            done_p.store(true, Ordering::Release);
435        });
436        let mut next = 0u64;
437        let mut spins = 0u64;
438        loop {
439            if let Some(v) = rx.pop() {
440                assert_eq!(v, next);
441                next += 1;
442                spins += 1;
443                if spins.is_multiple_of(1000) {
444                    std::thread::yield_now(); // let the ring fill up
445                }
446            } else {
447                if done.load(Ordering::Acquire) && rx.is_empty() {
448                    break;
449                }
450                std::thread::yield_now();
451            }
452        }
453        producer.join().unwrap();
454        assert_eq!(next, N);
455    }
456}