Skip to main content

yip_io/
spsc.rs

1//! Lock-free Single-Producer Single-Consumer (SPSC) bounded ring buffer.
2//!
3//! Provides zero-lock inter-shard datagram routing with cache-line padded
4//! atomic head and tail pointers to prevent false sharing across CPU cores.
5
6use crossbeam_utils::CachePadded;
7use std::cell::{Cell, UnsafeCell};
8use std::marker::PhantomData;
9use std::mem::MaybeUninit;
10use std::sync::atomic::{AtomicUsize, Ordering};
11use std::sync::Arc;
12
13struct RingBuffer<T, const CAP: usize> {
14    /// Producer sequence counter (write index).
15    head: CachePadded<AtomicUsize>,
16    /// Consumer sequence counter (read index).
17    tail: CachePadded<AtomicUsize>,
18    /// Heap-allocated array of uninitialized slot storage.
19    slots: Box<[UnsafeCell<MaybeUninit<T>>]>,
20}
21
22// SAFETY: `RingBuffer` is an SPSC queue where slot access is synchronized
23// by atomic Acquire/Release operations on `head` and `tail`.
24// Items transferred across threads require `T: Send`.
25unsafe impl<T: Send, const CAP: usize> Send for RingBuffer<T, CAP> {}
26
27// SAFETY: `RingBuffer` allows simultaneous access by the single producer
28// and single consumer threads without data races.
29unsafe impl<T: Send, const CAP: usize> Sync for RingBuffer<T, CAP> {}
30
31impl<T, const CAP: usize> Drop for RingBuffer<T, CAP> {
32    fn drop(&mut self) {
33        if std::mem::needs_drop::<T>() {
34            let head = self.head.load(Ordering::Relaxed);
35            let tail = self.tail.load(Ordering::Relaxed);
36            let count = head.wrapping_sub(tail);
37            for i in 0..count {
38                let idx = tail.wrapping_add(i) & (CAP - 1);
39                // SAFETY:
40                // These items were pushed but never popped or dropped.
41                // We have exclusive &mut self access during Drop.
42                unsafe {
43                    let slot = self.slots[idx].get();
44                    (*slot).assume_init_drop();
45                }
46            }
47        }
48    }
49}
50
51/// Producer half of the lock-free SPSC ring buffer.
52pub struct SpscProducer<T, const CAP: usize> {
53    inner: Arc<RingBuffer<T, CAP>>,
54    cached_tail: Cell<usize>,
55    _not_sync: PhantomData<Cell<()>>,
56}
57
58/// Consumer half of the lock-free SPSC ring buffer.
59pub struct SpscConsumer<T, const CAP: usize> {
60    inner: Arc<RingBuffer<T, CAP>>,
61    cached_head: Cell<usize>,
62    _not_sync: PhantomData<Cell<()>>,
63}
64
65impl<T, const CAP: usize> std::fmt::Debug for SpscProducer<T, CAP> {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        f.debug_struct("SpscProducer")
68            .field("capacity", &CAP)
69            .finish()
70    }
71}
72
73impl<T, const CAP: usize> std::fmt::Debug for SpscConsumer<T, CAP> {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        f.debug_struct("SpscConsumer")
76            .field("capacity", &CAP)
77            .finish()
78    }
79}
80
81/// Create a connected lock-free Single-Producer Single-Consumer (SPSC) ring buffer pair.
82///
83/// # Panics
84/// Panics if `CAP` is not greater than zero or is not a power of two.
85pub fn spsc_pair<T, const CAP: usize>() -> (SpscProducer<T, CAP>, SpscConsumer<T, CAP>) {
86    assert!(CAP > 0, "capacity must be greater than zero");
87    assert!(CAP.is_power_of_two(), "capacity must be a power of two");
88
89    let mut v: Vec<UnsafeCell<MaybeUninit<T>>> = Vec::with_capacity(CAP);
90    // SAFETY: `UnsafeCell<MaybeUninit<T>>` has the same layout and size as
91    // `MaybeUninit<T>`, for which uninitialized memory is a valid bit pattern.
92    // We set the vector length to `CAP` matching its allocated capacity.
93    unsafe {
94        v.set_len(CAP);
95    }
96    let slots = v.into_boxed_slice();
97
98    let inner = Arc::new(RingBuffer {
99        head: CachePadded::new(AtomicUsize::new(0)),
100        tail: CachePadded::new(AtomicUsize::new(0)),
101        slots,
102    });
103
104    let producer = SpscProducer {
105        inner: Arc::clone(&inner),
106        cached_tail: Cell::new(0),
107        _not_sync: PhantomData,
108    };
109
110    let consumer = SpscConsumer {
111        inner,
112        cached_head: Cell::new(0),
113        _not_sync: PhantomData,
114    };
115
116    (producer, consumer)
117}
118
119impl<T, const CAP: usize> SpscProducer<T, CAP> {
120    /// Attempts to push an item into the ring buffer.
121    ///
122    /// Returns `Ok(())` on success, or `Err(item)` if the buffer is full.
123    pub fn push(&self, item: T) -> Result<(), T> {
124        let head = self.inner.head.load(Ordering::Relaxed);
125        let mut tail = self.cached_tail.get();
126        if head.wrapping_sub(tail) >= CAP {
127            tail = self.inner.tail.load(Ordering::Acquire);
128            self.cached_tail.set(tail);
129            if head.wrapping_sub(tail) >= CAP {
130                return Err(item);
131            }
132        }
133
134        let idx = head & (CAP - 1);
135        // SAFETY:
136        // 1. We confirmed that `head.wrapping_sub(tail) < CAP`, so slot `idx` is unoccupied.
137        // 2. The previous occupant of slot `idx` (if any) was read by the consumer before
138        //    the consumer stored its `tail` update with Release ordering, which synchronizes
139        //    with our Acquire load of `tail`.
140        // 3. Only this producer writes to slot `idx` and advances `head`.
141        unsafe {
142            let slot = self.inner.slots[idx].get();
143            (*slot).write(item);
144        }
145
146        self.inner
147            .head
148            .store(head.wrapping_add(1), Ordering::Release);
149        Ok(())
150    }
151
152    /// Returns the capacity of the ring buffer.
153    pub fn capacity(&self) -> usize {
154        CAP
155    }
156}
157
158impl<T, const CAP: usize> SpscConsumer<T, CAP> {
159    /// Attempts to pop an item from the ring buffer.
160    ///
161    /// Returns `Some(item)` if an element is available, or `None` if the buffer is empty.
162    pub fn pop(&self) -> Option<T> {
163        let tail = self.inner.tail.load(Ordering::Relaxed);
164        let mut head = self.cached_head.get();
165        if head == tail {
166            head = self.inner.head.load(Ordering::Acquire);
167            self.cached_head.set(head);
168            if head == tail {
169                return None;
170            }
171        }
172
173        let idx = tail & (CAP - 1);
174        // SAFETY:
175        // 1. `head != tail`, so there is at least one published element at sequence `tail`.
176        // 2. The producer wrote to slot `idx` before advancing `head` with Release ordering,
177        //    which synchronizes with our Acquire load of `head`.
178        // 3. Only this consumer reads slot `idx` and advances `tail`.
179        let item = unsafe {
180            let slot = self.inner.slots[idx].get();
181            (*slot).assume_init_read()
182        };
183
184        self.inner
185            .tail
186            .store(tail.wrapping_add(1), Ordering::Release);
187        Some(item)
188    }
189
190    /// Drains up to `max` items from the ring buffer into the provided `out` vector.
191    ///
192    /// Returns the number of items drained.
193    pub fn drain_batch(&self, out: &mut Vec<T>, max: usize) -> usize {
194        if max == 0 {
195            return 0;
196        }
197
198        let tail = self.inner.tail.load(Ordering::Relaxed);
199        let mut head = self.cached_head.get();
200        if head.wrapping_sub(tail) < max {
201            head = self.inner.head.load(Ordering::Acquire);
202            self.cached_head.set(head);
203        }
204
205        let available = head.wrapping_sub(tail);
206        let to_drain = available.min(max);
207        if to_drain == 0 {
208            return 0;
209        }
210
211        out.reserve(to_drain);
212        for i in 0..to_drain {
213            let idx = tail.wrapping_add(i) & (CAP - 1);
214            // SAFETY:
215            // Slot `idx` was written by the producer before `head` was published with Release,
216            // which synchronizes with our Acquire load. It has not yet been popped.
217            let item = unsafe {
218                let slot = self.inner.slots[idx].get();
219                (*slot).assume_init_read()
220            };
221            out.push(item);
222        }
223
224        self.inner
225            .tail
226            .store(tail.wrapping_add(to_drain), Ordering::Release);
227        to_drain
228    }
229
230    /// Returns `true` if the ring buffer contains no items.
231    pub fn is_empty(&self) -> bool {
232        let tail = self.inner.tail.load(Ordering::Relaxed);
233        let head = self.inner.head.load(Ordering::Acquire);
234        self.cached_head.set(head);
235        head == tail
236    }
237}
238
239impl<T, const CAP: usize> Drop for SpscConsumer<T, CAP> {
240    fn drop(&mut self) {
241        while self.pop().is_some() {}
242    }
243}