Skip to main content

vane_core/
spsc.rs

1//! Cacheline-padded, lock-free SPSC ring for cross-thread signaling (`TH-02`).
2//!
3//! Used for the control-plane → worker command path and worker → control
4//! result path. One producer, one consumer; `try_push`/`try_pop` only, so
5//! neither side ever blocks. Zero `SeqCst`; the handoff edge is the standard
6//! acquire/release message-passing pair.
7//!
8//! Loom model-checking: `tests/loom_spsc.rs`.
9
10use std::cell::UnsafeCell;
11use std::sync::atomic::{AtomicUsize, Ordering};
12
13/// Pads to a cache line so the two cursors never false-share.
14#[repr(align(64))]
15#[derive(Default)]
16struct Padded(AtomicUsize);
17
18/// A bounded SPSC ring. `CAP` must be a power of two.
19pub struct SpscRing<T, const CAP: usize> {
20    mask: usize,
21    head: Padded, // producer cursor (slots written)
22    tail: Padded, // consumer cursor (slots read)
23    slots: Box<[Slot<T>]>,
24}
25
26struct Slot<T> {
27    sequence: AtomicUsize,
28    value: UnsafeCell<Option<T>>,
29}
30
31// SAFETY: single producer + single consumer, enforced by &mut / &self split.
32unsafe impl<T: Send, const CAP: usize> Send for SpscRing<T, CAP> {}
33// SAFETY: shared access is atomics-only; payloads move via the sequence
34// protocol (one consumer takes ownership).
35unsafe impl<T: Send, const CAP: usize> Sync for SpscRing<T, CAP> {}
36
37impl<T, const CAP: usize> SpscRing<T, CAP> {
38    /// Creates the ring.
39    #[must_use]
40    pub fn new() -> Self {
41        const {
42            assert!(CAP.is_power_of_two(), "CAP must be a power of two");
43        }
44        let slots = (0..CAP)
45            .map(|i| Slot {
46                sequence: AtomicUsize::new(i),
47                value: UnsafeCell::new(None),
48            })
49            .collect::<Vec<_>>();
50        Self {
51            mask: CAP - 1,
52            head: Padded::default(),
53            tail: Padded::default(),
54            slots: slots.into_boxed_slice(),
55        }
56    }
57
58    /// Producer: attempts to enqueue, returns `Some(value)` when full.
59    #[inline]
60    pub fn try_push(&self, value: T) -> Option<T> {
61        let pos = self.head.0.load(Ordering::Relaxed);
62        let slot = &self.slots[pos & self.mask];
63        if slot.sequence.load(Ordering::Acquire) != pos {
64            return Some(value); // full
65        }
66        // SAFETY: sequence == pos means the slot is empty and exclusively
67        // claimable by the (single) producer.
68        unsafe {
69            *slot.value.get() = Some(value);
70        }
71        slot.sequence.store(pos.wrapping_add(1), Ordering::Release);
72        self.head.0.store(pos.wrapping_add(1), Ordering::Relaxed);
73        None
74    }
75
76    /// Consumer: attempts to dequeue, `None` when empty.
77    #[inline]
78    pub fn try_pop(&self) -> Option<T> {
79        let pos = self.tail.0.load(Ordering::Relaxed);
80        let slot = &self.slots[pos & self.mask];
81        if slot.sequence.load(Ordering::Acquire) != pos + 1 {
82            return None; // empty
83        }
84        // SAFETY: sequence == pos + 1 means a value was published (release)
85        // and this (single) consumer owns the take.
86        let value = unsafe { (*slot.value.get()).take() };
87        slot.sequence
88            .store(pos.wrapping_add(CAP), Ordering::Release);
89        self.tail.0.store(pos.wrapping_add(1), Ordering::Relaxed);
90        value
91    }
92
93    /// Approximate occupancy (Relaxed, hint only).
94    #[must_use]
95    pub fn len(&self) -> usize {
96        self.head
97            .0
98            .load(Ordering::Relaxed)
99            .wrapping_sub(self.tail.0.load(Ordering::Relaxed))
100    }
101
102    /// `true` when the ring appears empty.
103    #[must_use]
104    pub fn is_empty(&self) -> bool {
105        self.len() == 0
106    }
107}
108
109impl<T, const CAP: usize> Default for SpscRing<T, CAP> {
110    fn default() -> Self {
111        Self::new()
112    }
113}
114
115/// Pairs a ring with its two ends for handing to separate threads.
116#[must_use]
117pub fn channel<T, const CAP: usize>() -> (SpscSender<T, CAP>, SpscReceiver<T, CAP>) {
118    let ring = std::sync::Arc::new(SpscRing::new());
119    (
120        SpscSender {
121            ring: std::sync::Arc::clone(&ring),
122        },
123        SpscReceiver { ring },
124    )
125}
126
127/// Producer handle.
128pub struct SpscSender<T, const CAP: usize> {
129    ring: std::sync::Arc<SpscRing<T, CAP>>,
130}
131
132impl<T, const CAP: usize> SpscSender<T, CAP> {
133    /// Enqueues without blocking; `Err(value)` when full.
134    ///
135    /// # Errors
136    /// Returns the value back when the ring is full (lock-free bound).
137    #[inline]
138    pub fn send(&self, value: T) -> Result<(), T> {
139        match self.ring.try_push(value) {
140            None => Ok(()),
141            Some(back) => Err(back),
142        }
143    }
144}
145
146/// Consumer handle.
147pub struct SpscReceiver<T, const CAP: usize> {
148    ring: std::sync::Arc<SpscRing<T, CAP>>,
149}
150
151impl<T, const CAP: usize> SpscReceiver<T, CAP> {
152    /// Dequeues without blocking.
153    #[inline]
154    pub fn recv(&self) -> Option<T> {
155        self.ring.try_pop()
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn spsc_basic() {
165        let (tx, rx) = channel::<u64, 4>();
166        tx.send(1).expect("fits");
167        tx.send(2).expect("fits");
168        assert_eq!(rx.recv(), Some(1));
169        assert_eq!(rx.recv(), Some(2));
170        assert_eq!(rx.recv(), None);
171    }
172
173    #[test]
174    fn full_returns_back() {
175        let (tx, rx) = channel::<u8, 2>();
176        tx.send(1).unwrap();
177        tx.send(2).unwrap();
178        assert_eq!(tx.send(3), Err(3));
179        assert_eq!(rx.recv(), Some(1));
180        tx.send(3).expect("space now");
181        assert_eq!(rx.recv(), Some(2));
182        assert_eq!(rx.recv(), Some(3));
183    }
184
185    #[test]
186    fn wraparound() {
187        let (tx, rx) = channel::<usize, 4>();
188        for i in 0..10_000usize {
189            tx.send(i).expect("loop drains");
190            assert_eq!(rx.recv(), Some(i));
191        }
192    }
193
194    #[test]
195    fn cross_thread_sum() {
196        let (tx, rx) = channel::<u64, 1024>();
197        std::thread::scope(|s| {
198            s.spawn(|| {
199                for i in 0..100_000u64 {
200                    while tx.send(i).is_err() {
201                        std::hint::spin_loop();
202                    }
203                }
204            });
205            let got: u64 = (0..100_000u64)
206                .map(|_| {
207                    loop {
208                        if let Some(v) = rx.recv() {
209                            break v;
210                        } else {
211                            std::hint::spin_loop();
212                        }
213                    }
214                })
215                .sum();
216            assert_eq!(got, 100_000u64 * 99_999 / 2);
217        });
218    }
219}