Skip to main content

proxima_core/ring/
mpsc.rs

1#[cfg(feature = "alloc")]
2use alloc::boxed::Box;
3
4#[cfg(feature = "alloc")]
5use super::CapacityError;
6use core::mem::MaybeUninit;
7
8// the ring's atomics + UnsafeCell are cfg-switched to loom under `--features
9// loom` so the loom model checker (tests/loom_ring.rs) explores every
10// interleaving of the Vyukov push/dequeue protocol on STABLE rust — replacing
11// the nightly ThreadSanitizer job. loom's UnsafeCell uses `.with`/`.with_mut`
12// closures instead of `.get()`, so the three raw cell accesses go through the
13// `*_cell` helpers below.
14#[cfg(not(feature = "loom"))]
15use core::cell::UnsafeCell;
16#[cfg(not(feature = "loom"))]
17use core::sync::atomic::{AtomicUsize, Ordering};
18#[cfg(feature = "loom")]
19use loom::cell::UnsafeCell;
20#[cfg(feature = "loom")]
21use loom::sync::atomic::{AtomicUsize, Ordering};
22
23#[cfg(feature = "loom")]
24unsafe fn write_cell<T>(cell: &UnsafeCell<MaybeUninit<T>>, record: T) {
25    cell.with_mut(|ptr| unsafe { (*ptr).write(record) });
26}
27#[cfg(not(feature = "loom"))]
28unsafe fn write_cell<T>(cell: &UnsafeCell<MaybeUninit<T>>, record: T) {
29    unsafe { (*cell.get()).write(record) };
30}
31
32#[cfg(feature = "loom")]
33unsafe fn read_cell<T>(cell: &UnsafeCell<MaybeUninit<T>>) -> T {
34    cell.with(|ptr| unsafe { (*ptr).assume_init_read() })
35}
36#[cfg(not(feature = "loom"))]
37unsafe fn read_cell<T>(cell: &UnsafeCell<MaybeUninit<T>>) -> T {
38    unsafe { (*cell.get()).assume_init_read() }
39}
40
41#[cfg(all(feature = "loom", feature = "alloc"))]
42unsafe fn drop_cell<T>(cell: &UnsafeCell<MaybeUninit<T>>) {
43    cell.with_mut(|ptr| unsafe { (*ptr).assume_init_drop() });
44}
45#[cfg(all(not(feature = "loom"), feature = "alloc"))]
46unsafe fn drop_cell<T>(cell: &UnsafeCell<MaybeUninit<T>>) {
47    unsafe { (*cell.get()).assume_init_drop() };
48}
49
50// One cell per slot: a sequence stamp (the handoff protocol) plus the payload.
51// `sequence` encodes the slot's lap state — a producer may claim it only when
52// `sequence == its claim position`; a consumer frees it by stamping
53// `position + capacity` (the next lap's claim position). This is Dmitry
54// Vyukov's bounded-MPMC cell, run in its full multi-producer / multi-consumer
55// form: both ends linearise their position counter with a CAS.
56struct Cell<T> {
57    sequence: AtomicUsize,
58    data: UnsafeCell<MaybeUninit<T>>,
59}
60
61// The lock-free MPMC algorithm, shared by the alloc `Ring` (Box buffer) and the
62// no-alloc `StaticRing` (inline array). Operates over a `&[Cell<T>]`; capacity
63// and mask derive from the slice length (a power of two >= 2). Single source of
64// truth for the Vyukov protocol — both ring types delegate, so there is one
65// implementation to audit and one for loom to model-check.
66#[inline]
67fn cells_push<T>(cells: &[Cell<T>], enqueue_pos: &AtomicUsize, record: T) -> Result<(), T> {
68    let mask = cells.len() - 1;
69    let mut pos = enqueue_pos.load(Ordering::Relaxed);
70    loop {
71        let cell = &cells[pos & mask];
72        let sequence = cell.sequence.load(Ordering::Acquire);
73        let diff = sequence.wrapping_sub(pos) as isize;
74        if diff == 0 {
75            match enqueue_pos.compare_exchange_weak(
76                pos,
77                pos.wrapping_add(1),
78                Ordering::Relaxed,
79                Ordering::Relaxed,
80            ) {
81                Ok(_) => {
82                    // SAFETY: CAS winner exclusively owns this slot for this lap;
83                    // the consumer freed it (sequence == pos), so no aliasing.
84                    unsafe {
85                        write_cell(&cell.data, record);
86                    }
87                    cell.sequence.store(pos.wrapping_add(1), Ordering::Release);
88                    return Ok(());
89                }
90                Err(actual) => pos = actual,
91            }
92        } else if diff < 0 {
93            return Err(record);
94        } else {
95            pos = enqueue_pos.load(Ordering::Relaxed);
96        }
97    }
98}
99
100#[inline]
101fn cells_dequeue<T>(cells: &[Cell<T>], dequeue_pos: &AtomicUsize) -> Option<T> {
102    let mask = cells.len() - 1;
103    let cap = cells.len();
104    let mut pos = dequeue_pos.load(Ordering::Relaxed);
105    loop {
106        let cell = &cells[pos & mask];
107        let sequence = cell.sequence.load(Ordering::Acquire);
108        let diff = sequence.wrapping_sub(pos.wrapping_add(1)) as isize;
109        if diff == 0 {
110            match dequeue_pos.compare_exchange_weak(
111                pos,
112                pos.wrapping_add(1),
113                Ordering::Relaxed,
114                Ordering::Relaxed,
115            ) {
116                Ok(_) => {
117                    // SAFETY: CAS winner owns this slot; the Acquire observed the
118                    // producer's Release publish, so the payload read is valid and
119                    // no other consumer touches the cell.
120                    let record = unsafe { read_cell(&cell.data) };
121                    cell.sequence
122                        .store(pos.wrapping_add(cap), Ordering::Release);
123                    return Some(record);
124                }
125                Err(actual) => pos = actual,
126            }
127        } else if diff < 0 {
128            return None;
129        } else {
130            pos = dequeue_pos.load(Ordering::Relaxed);
131        }
132    }
133}
134
135/// Fixed-capacity bounded ring with **lock-free multi-producer, multi-consumer**
136/// access.
137///
138/// Any number of threads may call [`Ring::push`] concurrently; ownership of
139/// each slot is linearised by a CAS on `enqueue_pos`, so two producers never
140/// touch the same cell. Likewise any number of threads may [`Ring::dequeue`]
141/// (or drain via [`Drainer::drain_into`]) concurrently; each pop is linearised
142/// by a CAS on `dequeue_pos`, so two consumers never read the same cell. This
143/// lets a background drainer and an overflowing producer (elastic
144/// producer-assist) drain the same ring at once. Capacity is rounded up to a
145/// power of two (minimum 2) at construction; no resize occurs. Full pushes
146/// return the record via `Err` without blocking; empty dequeues return `None`.
147#[cfg(feature = "alloc")]
148pub struct Ring<T> {
149    buf: Box<[Cell<T>]>,
150    // monotonically increasing claim/read counters; slot index is `pos & mask`.
151    enqueue_pos: AtomicUsize,
152    dequeue_pos: AtomicUsize,
153    mask: usize,
154    cap: usize,
155}
156
157// SAFETY: Ring<T> is Send when T is Send — cells are accessed only through the
158// Vyukov sequence protocol, which establishes the happens-before edges below.
159#[cfg(feature = "alloc")]
160unsafe impl<T: Send> Send for Ring<T> {}
161
162// SAFETY: &Ring<T> is Sync when T is Send. Producers synchronise slot ownership
163// via the `enqueue_pos` CAS (exactly one winner per position) and publish the
164// payload via the cell's `sequence` Release; a consumer's Acquire load of the
165// same `sequence` observes the completed write. Consumers synchronise slot
166// ownership via the `dequeue_pos` CAS (exactly one winner per position), so two
167// consumers never read the same cell. Freeing is the mirror: the consumer's
168// Release store is observed by the next producer's Acquire.
169#[cfg(feature = "alloc")]
170unsafe impl<T: Send> Sync for Ring<T> {}
171
172#[cfg(feature = "alloc")]
173impl<T> Ring<T> {
174    /// Allocate a ring. `cap` is rounded up to a power of two, minimum 2 (the
175    /// Vyukov protocol is degenerate at capacity 1: `pos` and `pos + 1` alias
176    /// the lone cell). Returns [`CapacityError`] only for `cap == 0`.
177    pub fn new(cap: usize) -> Result<Self, CapacityError> {
178        if cap == 0 {
179            return Err(CapacityError);
180        }
181        Ok(Self::build(cap))
182    }
183
184    /// Allocate a ring without the `Result` — `cap` is clamped to at least 1,
185    /// then rounded up to a power of two (minimum 2) exactly as [`new`](Self::new).
186    /// Use when the capacity comes from a validated config or a `max(1)` clamp, so
187    /// `new`'s `cap == 0` arm is unreachable and threading a `Result` through every
188    /// caller is noise.
189    #[must_use]
190    pub fn with_capacity(cap: usize) -> Self {
191        Self::build(cap.max(1))
192    }
193
194    // shared allocation core; `cap >= 1` is the caller's precondition (`new`
195    // guards 0, `with_capacity` clamps) so this cannot fail.
196    fn build(cap: usize) -> Self {
197        let cap = cap.next_power_of_two().max(2);
198        let buf = (0..cap)
199            .map(|index| Cell {
200                sequence: AtomicUsize::new(index),
201                data: UnsafeCell::new(MaybeUninit::uninit()),
202            })
203            .collect::<Box<[_]>>();
204
205        Self {
206            buf,
207            enqueue_pos: AtomicUsize::new(0),
208            dequeue_pos: AtomicUsize::new(0),
209            mask: cap - 1,
210            cap,
211        }
212    }
213
214    /// Push one record. Safe to call from any number of threads concurrently.
215    ///
216    /// On success the record is consumed. If the ring is at capacity the record
217    /// is handed back via `Err(record)` (no blocking) so the caller can retry it
218    /// under a lossless overflow policy instead of losing it.
219    pub fn push(&self, record: T) -> Result<(), T> {
220        cells_push(&self.buf, &self.enqueue_pos, record)
221    }
222
223    /// Pop one record in FIFO order. Safe to call from any number of threads
224    /// concurrently (multi-consumer).
225    ///
226    /// Returns `None` if the ring is empty (no blocking). The CAS on
227    /// `dequeue_pos` is the linearisation point: exactly one consumer wins each
228    /// position, so two consumers never read the same cell. This is the mirror
229    /// of [`push`](Self::push) — see the cell `sequence` protocol there.
230    pub fn dequeue(&self) -> Option<T> {
231        cells_dequeue(&self.buf, &self.dequeue_pos)
232    }
233
234    /// Number of items currently in the ring (snapshot, not linearizable).
235    pub fn len(&self) -> usize {
236        let enqueue = self.enqueue_pos.load(Ordering::Acquire);
237        let dequeue = self.dequeue_pos.load(Ordering::Acquire);
238        enqueue.wrapping_sub(dequeue)
239    }
240
241    /// True if the ring contains no items (snapshot).
242    pub fn is_empty(&self) -> bool {
243        self.len() == 0
244    }
245
246    /// The ring's actual capacity (the power-of-two `new` rounded up to).
247    pub fn cap(&self) -> usize {
248        self.cap
249    }
250}
251
252#[cfg(feature = "alloc")]
253impl<T> Drop for Ring<T> {
254    fn drop(&mut self) {
255        // exclusive &mut self, so a relaxed load is a plain read (no contention);
256        // get_mut is unavailable on loom atomics, load works on both.
257        let dequeue = self.dequeue_pos.load(Ordering::Relaxed);
258        let enqueue = self.enqueue_pos.load(Ordering::Relaxed);
259
260        let mut pos = dequeue;
261        while pos != enqueue {
262            let slot = pos & self.mask;
263            let published = self.buf[slot].sequence.load(Ordering::Relaxed) == pos.wrapping_add(1);
264            if published {
265                // SAFETY: exclusive &mut self, so no producer is mid-write; a
266                // sequence of pos+1 means this slot holds a published, unread item.
267                unsafe {
268                    drop_cell(&self.buf[slot].data);
269                }
270            }
271            pos = pos.wrapping_add(1);
272        }
273    }
274}
275
276// Slots ahead to prefetch the drain output buffer. The store stream into the
277// caller's buffer is latency-bound (read-for-ownership) once it exceeds L1; at
278// ~6 ns/op and ~100 ns miss latency the cover distance is ~16 (latency / op).
279// Tuned: K=16 takes drain_into from 79 -> 6.3 ns/elem at 1M (bench_ring_decompose).
280#[cfg(feature = "alloc")]
281const PREFETCH_DISTANCE: usize = 16;
282
283/// Batch-drain handle for a [`Ring<T>`]. The ring is multi-consumer, so any
284/// number of `Drainer`s (or bare [`Ring::dequeue`] callers) may drain the same
285/// ring concurrently while producers push — each pop is CAS-linearised on
286/// `dequeue_pos`.
287#[cfg(feature = "alloc")]
288pub struct Drainer<'ring, T> {
289    ring: &'ring Ring<T>,
290}
291
292#[cfg(feature = "alloc")]
293impl<'ring, T> Drainer<'ring, T> {
294    /// Wrap a ring reference in a drainer.
295    pub fn new(ring: &'ring Ring<T>) -> Self {
296        Self { ring }
297    }
298
299    /// Move up to `out.len()` items out of the ring into `out` in FIFO order.
300    ///
301    /// Returns the number of items written. Each item is claimed via the
302    /// multi-consumer [`Ring::dequeue`] CAS, so concurrent drainers split the
303    /// stream without ever reading the same cell; the loop stops at the first
304    /// empty pop (another consumer may have raced us to the tail).
305    pub fn drain_into(&mut self, out: &mut [T]) -> usize {
306        let mut count = 0;
307        while count < out.len() {
308            match self.ring.dequeue() {
309                Some(record) => {
310                    // the store stream into the caller's buffer is latency-bound
311                    // (RFO) past L1; prefetch the slot we will write K ahead so it
312                    // does not stall, keeping the drain flat at large batch sizes.
313                    let ahead = count + PREFETCH_DISTANCE;
314                    if ahead < out.len() {
315                        // SAFETY: ahead < out.len(), so the pointer is in bounds;
316                        // it is only passed to a prefetch hint, never dereferenced.
317                        let slot = unsafe { out.as_ptr().add(ahead) };
318                        crate::arch::prefetch_for_write(slot.cast());
319                    }
320                    out[count] = record;
321                    count += 1;
322                }
323                None => break,
324            }
325        }
326        count
327    }
328}
329
330/// No-alloc, const-capacity bounded ring with lock-free MPMC access — the
331/// bare-metal tier of [`Ring`]. The `[Cell<T>; N]` buffer lives **inline** (no
332/// heap), so it compiles and runs where `alloc` is unavailable
333/// (DPDK/SPDK/embedded). Same Vyukov algorithm as [`Ring`] — both delegate to the
334/// shared `cells_*` fns, so loom's model-check of that algorithm covers this too.
335///
336/// `N` MUST be a power of two >= 2 (asserted in [`StaticRing::new`]); the
337/// protocol needs it for the `pos & (N - 1)` slot index.
338///
339/// [`new`](StaticRing::new) is `const`, so the whole no-alloc tier can back a
340/// `static` on bare metal with no heap and no lazy `OnceCell`/runtime init:
341/// `static RING: StaticRing<T, N> = StaticRing::new();`.
342/// [`StaticBoundedQueue`](crate::ring::StaticBoundedQueue) and the pipe-forms
343/// `StaticSinkFront` inherit that through here.
344pub struct StaticRing<T, const N: usize> {
345    buf: [Cell<T>; N],
346    enqueue_pos: AtomicUsize,
347    dequeue_pos: AtomicUsize,
348}
349
350// SAFETY: identical protocol to `Ring`; only WHERE the cells live changes (inline
351// vs heap), not HOW they synchronise. See `Ring`'s Send/Sync safety notes.
352unsafe impl<T: Send, const N: usize> Send for StaticRing<T, N> {}
353unsafe impl<T: Send, const N: usize> Sync for StaticRing<T, N> {}
354
355impl<T, const N: usize> StaticRing<T, N> {
356    /// Construct an empty ring, seeding each cell's `sequence` to its own index
357    /// (the Vyukov empty-slot test is `seq == pos`, slot by slot). `const`, so a
358    /// bare-metal caller can put one straight in a `static`; a non-power-of-two
359    /// `N` then fails the BUILD rather than the first call.
360    ///
361    /// loom's atomics register with the model checker on construction and are
362    /// therefore not const-constructible, so the `--features loom` build gets
363    /// the same body without `const` (same cfg split as `write_cell` above).
364    #[must_use]
365    #[cfg(not(feature = "loom"))]
366    pub const fn new() -> Self {
367        assert!(
368            N >= 2 && N.is_power_of_two(),
369            "StaticRing capacity N must be a power of two >= 2"
370        );
371        let mut buf = [const {
372            Cell {
373                sequence: AtomicUsize::new(0),
374                data: UnsafeCell::new(MaybeUninit::uninit()),
375            }
376        }; N];
377        let mut index = 0;
378        while index < N {
379            buf[index].sequence = AtomicUsize::new(index);
380            index += 1;
381        }
382        Self {
383            buf,
384            enqueue_pos: AtomicUsize::new(0),
385            dequeue_pos: AtomicUsize::new(0),
386        }
387    }
388
389    #[must_use]
390    #[cfg(feature = "loom")]
391    pub fn new() -> Self {
392        assert!(
393            N >= 2 && N.is_power_of_two(),
394            "StaticRing capacity N must be a power of two >= 2"
395        );
396        Self {
397            buf: core::array::from_fn(|index| Cell {
398                sequence: AtomicUsize::new(index),
399                data: UnsafeCell::new(MaybeUninit::uninit()),
400            }),
401            enqueue_pos: AtomicUsize::new(0),
402            dequeue_pos: AtomicUsize::new(0),
403        }
404    }
405
406    /// Push one record; `Err(record)` if full (no blocking). MPMC-safe.
407    pub fn push(&self, record: T) -> Result<(), T> {
408        cells_push(&self.buf, &self.enqueue_pos, record)
409    }
410
411    /// Pop one record in FIFO order; `None` if empty. MPMC-safe.
412    pub fn dequeue(&self) -> Option<T> {
413        cells_dequeue(&self.buf, &self.dequeue_pos)
414    }
415
416    /// Items currently buffered (snapshot, not linearizable).
417    #[must_use]
418    pub fn len(&self) -> usize {
419        self.enqueue_pos
420            .load(Ordering::Acquire)
421            .wrapping_sub(self.dequeue_pos.load(Ordering::Acquire))
422    }
423
424    #[must_use]
425    pub fn is_empty(&self) -> bool {
426        self.len() == 0
427    }
428
429    #[must_use]
430    pub const fn capacity(&self) -> usize {
431        N
432    }
433}
434
435impl<T, const N: usize> Default for StaticRing<T, N> {
436    fn default() -> Self {
437        Self::new()
438    }
439}
440
441impl<T, const N: usize> Drop for StaticRing<T, N> {
442    fn drop(&mut self) {
443        // exclusive access in Drop; dequeue drains + drops each buffered record.
444        // Empty slots hold uninit data that dequeue never reads (protocol-gated).
445        while self.dequeue().is_some() {}
446    }
447}
448
449// Ring/StaticRing's atomics + UnsafeCell are cfg-swapped to loom under
450// `--features loom` (see the module doc comment above) — those only work
451// inside an actual loom::model(...) closure, which these plain #[test]
452// functions don't provide.
453#[cfg(all(test, not(feature = "loom")))]
454#[allow(clippy::unwrap_used, clippy::expect_used)]
455mod tests {
456    extern crate std;
457
458    use std::sync::Arc;
459    use std::sync::atomic::{AtomicUsize, Ordering};
460    use std::thread;
461
462    use super::StaticRing;
463
464    // Ring/Drainer are alloc-gated (see their #[cfg(feature = "alloc")]
465    // definitions above); nested so a no-alloc build still exercises
466    // StaticRing's tests below instead of losing this whole file's coverage.
467    #[cfg(feature = "alloc")]
468    mod ring_tests {
469        use super::*;
470        use std::sync::Arc;
471        use std::thread;
472
473        use super::super::{Drainer, Ring};
474        use alloc::collections::VecDeque;
475        use alloc::vec;
476        use alloc::vec::Vec;
477
478        #[proxima::test]
479        #[case::push_all_drain_all(8)]
480        #[case::small_cap(2)]
481        #[case::large_cap(64)]
482        async fn happy_push_n_drain_n_in_order(#[case] cap: usize) {
483            let ring = Ring::new(cap).unwrap();
484            for item in 0..cap {
485                ring.push(item as u32).unwrap();
486            }
487
488            let mut out = vec![0u32; cap];
489            let mut drainer = Drainer::new(&ring);
490            let received = drainer.drain_into(&mut out);
491
492            assert_eq!(received, cap);
493            for (idx, value) in out.iter().enumerate() {
494                assert_eq!(*value, idx as u32);
495            }
496        }
497
498        #[proxima::test]
499        #[case::small(2)]
500        #[case::many(16)]
501        async fn sad_push_into_full_ring_returns_full(#[case] cap: usize) {
502            let ring = Ring::<u64>::new(cap).unwrap();
503            for item in 0..ring.cap() {
504                ring.push(item as u64).unwrap();
505            }
506
507            // a full ring hands the record back so a lossless caller can retry it.
508            assert_eq!(ring.push(99), Err(99));
509        }
510
511        // Worked example (paper proof): cap=4, fill to capacity, 5th push is Full
512        // (never overwrites slot 0's unconsumed item), drain recovers FIFO, then the
513        // freed slot accepts a new lap. This is the locked test for the Vyukov
514        // full-detection + reclaim derivation.
515        #[test]
516        fn worked_example_fifo_full_at_cap_then_reclaim() {
517            let ring = Ring::new(4).unwrap();
518            assert_eq!(ring.cap(), 4);
519
520            for value in 0..4u32 {
521                ring.push(value).unwrap();
522            }
523            assert_eq!(ring.push(99), Err(99));
524
525            let mut out = [0u32; 4];
526            let mut drainer = Drainer::new(&ring);
527            assert_eq!(drainer.drain_into(&mut out), 4);
528            assert_eq!(out, [0, 1, 2, 3]);
529
530            // slot 0 (and the rest) freed for the next lap.
531            ring.push(7).unwrap();
532            let mut out2 = [0u32; 1];
533            assert_eq!(drainer.drain_into(&mut out2), 1);
534            assert_eq!(out2[0], 7);
535        }
536
537        #[test]
538        fn edge_min_cap_push_full_drain_push_again() {
539            let ring = Ring::new(2).unwrap();
540
541            ring.push(42u32).unwrap();
542            ring.push(43u32).unwrap();
543            assert_eq!(ring.push(99u32), Err(99));
544
545            let mut out = [0u32; 2];
546            let mut drainer = Drainer::new(&ring);
547            assert_eq!(drainer.drain_into(&mut out), 2);
548            assert_eq!(out, [42, 43]);
549
550            ring.push(7u32).unwrap();
551            let mut out2 = [0u32; 1];
552            assert_eq!(drainer.drain_into(&mut out2), 1);
553            assert_eq!(out2[0], 7);
554        }
555
556        #[test]
557        fn cap_rounds_up_to_power_of_two_min_two() {
558            assert_eq!(Ring::<u8>::new(1).unwrap().cap(), 2);
559            assert_eq!(Ring::<u8>::new(3).unwrap().cap(), 4);
560            assert_eq!(Ring::<u8>::new(1000).unwrap().cap(), 1024);
561            assert_eq!(Ring::<u8>::new(4096).unwrap().cap(), 4096);
562        }
563
564        #[test]
565        fn with_capacity_is_infallible_and_clamps_zero_to_min() {
566            // same pow2 rounding as `new`, but no Result — and 0 clamps to the min
567            // rather than erroring, for callers whose capacity is already nonzero.
568            assert_eq!(Ring::<u8>::with_capacity(0).cap(), 2);
569            assert_eq!(Ring::<u8>::with_capacity(1).cap(), 2);
570            assert_eq!(Ring::<u8>::with_capacity(3).cap(), 4);
571            let ring = Ring::with_capacity(1000);
572            assert_eq!(ring.cap(), 1024);
573            ring.push(7u32).unwrap();
574            assert_eq!(ring.dequeue(), Some(7));
575        }
576
577        #[test]
578        fn drop_items_inside_ring_are_dropped_properly() {
579            let counter = Arc::new(AtomicUsize::new(0));
580
581            struct Counted(Arc<AtomicUsize>);
582            impl Drop for Counted {
583                fn drop(&mut self) {
584                    self.0.fetch_add(1, Ordering::Relaxed);
585                }
586            }
587
588            {
589                let ring = Ring::new(4).unwrap();
590                // Counted isn't Debug, so assert on is_ok() rather than unwrap().
591                assert!(ring.push(Counted(Arc::clone(&counter))).is_ok());
592                assert!(ring.push(Counted(Arc::clone(&counter))).is_ok());
593                // ring goes out of scope here with 2 items still inside
594            }
595
596            assert_eq!(counter.load(Ordering::Relaxed), 2);
597        }
598
599        #[test]
600        fn concurrency_single_producer_order_preserved() {
601            const ITEMS: u32 = 10_000;
602            let ring = Arc::new(Ring::<u32>::new(512).unwrap());
603
604            let producer_ring = Arc::clone(&ring);
605            let producer = thread::spawn(move || {
606                let mut pushed = 0u32;
607                while pushed < ITEMS {
608                    if producer_ring.push(pushed).is_ok() {
609                        pushed += 1;
610                    } else {
611                        thread::yield_now();
612                    }
613                }
614            });
615
616            let consumer_ring = Arc::clone(&ring);
617            let consumer = thread::spawn(move || {
618                let mut received = Vec::with_capacity(ITEMS as usize);
619                let mut buf = vec![0u32; 64];
620                let mut drainer = Drainer::new(&*consumer_ring);
621                while received.len() < ITEMS as usize {
622                    let count = drainer.drain_into(&mut buf);
623                    received.extend_from_slice(&buf[..count]);
624                    if count == 0 {
625                        thread::yield_now();
626                    }
627                }
628                received
629            });
630
631            producer.join().expect("producer panicked");
632            let received = consumer.join().expect("consumer panicked");
633
634            assert_eq!(received.len(), ITEMS as usize);
635            for (idx, value) in received.iter().enumerate() {
636                assert_eq!(*value, idx as u32);
637            }
638        }
639
640        // The multi-producer safety proof. P producers (>> ring slots, to force many
641        // producers onto each cell) each push a disjoint id range; every record
642        // carries a redundant checksum so a torn write is detectable. One consumer
643        // drains concurrently. Invariants asserted: no torn record (checksum holds),
644        // and the drained id multiset is exactly the pushed set (no loss, no dupe) —
645        // which is precisely what the old non-atomic SPSC push violated when two
646        // threads shared a ring.
647        #[test]
648        fn mpsc_many_producers_no_loss_no_tear() {
649            #[derive(Clone, Copy)]
650            struct Tagged {
651                id: u64,
652                check: u64,
653            }
654            const MAGIC: u64 = 0x9E37_79B9_7F4A_7C15;
655            fn make(id: u64) -> Tagged {
656                Tagged {
657                    id,
658                    check: id ^ MAGIC,
659                }
660            }
661
662            const PRODUCERS: u64 = 8;
663            const PER_PRODUCER: u64 = 4_000;
664            const TOTAL: usize = (PRODUCERS * PER_PRODUCER) as usize;
665
666            for _round in 0..5 {
667                // small ring => every cell is shared by several producers.
668                let ring = Arc::new(Ring::<Tagged>::new(16).unwrap());
669
670                let producers: Vec<_> = (0..PRODUCERS)
671                    .map(|producer_id| {
672                        let ring = Arc::clone(&ring);
673                        thread::spawn(move || {
674                            let base = producer_id * PER_PRODUCER;
675                            let mut sent = 0u64;
676                            while sent < PER_PRODUCER {
677                                if ring.push(make(base + sent)).is_ok() {
678                                    sent += 1;
679                                } else {
680                                    thread::yield_now();
681                                }
682                            }
683                        })
684                    })
685                    .collect();
686
687                let consumer_ring = Arc::clone(&ring);
688                let consumer = thread::spawn(move || {
689                    let mut seen: Vec<bool> = vec![false; TOTAL];
690                    let mut received = 0usize;
691                    let mut buf = vec![make(0); 32];
692                    let mut drainer = Drainer::new(&*consumer_ring);
693                    while received < TOTAL {
694                        let count = drainer.drain_into(&mut buf);
695                        if count == 0 {
696                            thread::yield_now();
697                            continue;
698                        }
699                        for tagged in buf.iter().take(count) {
700                            assert_eq!(tagged.check, tagged.id ^ MAGIC, "torn record");
701                            let id = tagged.id as usize;
702                            assert!(!seen[id], "duplicate id {id}");
703                            seen[id] = true;
704                            received += 1;
705                        }
706                    }
707                    seen
708                });
709
710                for producer in producers {
711                    producer.join().expect("producer panicked");
712                }
713                let seen = consumer.join().expect("consumer panicked");
714                assert!(seen.into_iter().all(|hit| hit), "missing ids");
715            }
716        }
717
718        // Worked example (paper proof) for the MPMC dequeue: cap=2, two slots full,
719        // two consumers race. Between them they recover {A, B} once each in FIFO
720        // order, a third dequeue is empty, and the freed slots accept the next lap.
721        // This is the locked test for the Vyukov MPMC dequeue CAS derivation
722        // (docs/tracing/discipline.md, "Lossless backpressure").
723        #[test]
724        fn worked_example_mpmc_two_consumers_fifo_then_reclaim() {
725            let ring = Ring::new(2).unwrap();
726            ring.push(b'A').unwrap();
727            ring.push(b'B').unwrap();
728            assert_eq!(
729                ring.push(b'C'),
730                Err(b'C'),
731                "full ring hands the record back"
732            );
733
734            // two consumers between them pop both items, FIFO, once each.
735            let first = ring.dequeue().expect("first pop");
736            let second = ring.dequeue().expect("second pop");
737            assert_eq!([first, second], [b'A', b'B'], "FIFO order across consumers");
738            assert_eq!(ring.dequeue(), None, "empty after both consumed");
739
740            // the freed slots accept the next lap.
741            ring.push(b'D').unwrap();
742            assert_eq!(ring.dequeue(), Some(b'D'));
743        }
744
745        // The multi-CONSUMER safety proof — the dequeue mirror of
746        // mpsc_many_producers_no_loss_no_tear. P producers push a disjoint id range;
747        // C consumers drain concurrently. Every record carries a redundant checksum
748        // (torn-write detector); each consumer marks ids in a shared seen-set under a
749        // no-duplicate assertion. Invariants: no torn record, no id consumed twice
750        // (the dequeue CAS gives each cell to exactly one consumer), and the union of
751        // all consumers' drains == the pushed set (no loss). This is exactly what a
752        // non-atomic single-consumer drain run multi-consumer would violate.
753        #[test]
754        fn mpmc_many_producers_many_consumers_no_loss_no_tear() {
755            #[derive(Clone, Copy)]
756            struct Tagged {
757                id: u64,
758                check: u64,
759            }
760            const MAGIC: u64 = 0x9E37_79B9_7F4A_7C15;
761            fn make(id: u64) -> Tagged {
762                Tagged {
763                    id,
764                    check: id ^ MAGIC,
765                }
766            }
767
768            const PRODUCERS: u64 = 8;
769            const CONSUMERS: usize = 4;
770            const PER_PRODUCER: u64 = 4_000;
771            const TOTAL: usize = (PRODUCERS * PER_PRODUCER) as usize;
772
773            for _round in 0..5 {
774                // small ring => every cell is shared by several producers AND consumers.
775                let ring = Arc::new(Ring::<Tagged>::new(16).unwrap());
776                // shared seen-set: one slot per id, asserted set-once by whichever
777                // consumer pops it. A torn dequeue CAS would let two consumers claim
778                // the same cell and trip the duplicate assertion.
779                let seen: Arc<Vec<AtomicUsize>> =
780                    Arc::new((0..TOTAL).map(|_| AtomicUsize::new(0)).collect());
781                let received = Arc::new(AtomicUsize::new(0));
782
783                let producers: Vec<_> = (0..PRODUCERS)
784                    .map(|producer_id| {
785                        let ring = Arc::clone(&ring);
786                        thread::spawn(move || {
787                            let base = producer_id * PER_PRODUCER;
788                            let mut sent = 0u64;
789                            while sent < PER_PRODUCER {
790                                if ring.push(make(base + sent)).is_ok() {
791                                    sent += 1;
792                                } else {
793                                    thread::yield_now();
794                                }
795                            }
796                        })
797                    })
798                    .collect();
799
800                let consumers: Vec<_> = (0..CONSUMERS)
801                    .map(|_| {
802                        let ring = Arc::clone(&ring);
803                        let seen = Arc::clone(&seen);
804                        let received = Arc::clone(&received);
805                        thread::spawn(move || {
806                            while received.load(Ordering::Relaxed) < TOTAL {
807                                match ring.dequeue() {
808                                    Some(tagged) => {
809                                        assert_eq!(tagged.check, tagged.id ^ MAGIC, "torn record");
810                                        let prior = seen[tagged.id as usize]
811                                            .fetch_add(1, Ordering::Relaxed);
812                                        assert_eq!(prior, 0, "duplicate id {}", tagged.id);
813                                        received.fetch_add(1, Ordering::Relaxed);
814                                    }
815                                    None => thread::yield_now(),
816                                }
817                            }
818                        })
819                    })
820                    .collect();
821
822                for producer in producers {
823                    producer.join().expect("producer panicked");
824                }
825                for consumer in consumers {
826                    consumer.join().expect("consumer panicked");
827                }
828                assert!(
829                    seen.iter().all(|hit| hit.load(Ordering::Relaxed) == 1),
830                    "every id consumed exactly once"
831                );
832            }
833        }
834
835        #[proxima::test]
836        #[case::tiny(4, 100)]
837        #[case::medium(32, 1_000)]
838        #[case::large(256, 10_000)]
839        async fn property_random_push_drain_interleaving_preserves_fifo(
840            #[case] cap: usize,
841            #[case] total_ops: usize,
842        ) {
843            let ring = Ring::new(cap).unwrap();
844            let mut drainer = Drainer::new(&ring);
845            let mut expected_queue: VecDeque<u32> = VecDeque::new();
846            let mut next_push_value = 0u32;
847            let mut drain_buf = vec![0u32; cap];
848
849            for op_idx in 0..total_ops {
850                let do_push = op_idx % 3 != 2;
851
852                if do_push && expected_queue.len() < cap {
853                    if ring.push(next_push_value).is_ok() {
854                        expected_queue.push_back(next_push_value);
855                        next_push_value = next_push_value.wrapping_add(1);
856                    }
857                } else {
858                    let count = drainer.drain_into(&mut drain_buf);
859                    for drained in drain_buf.iter().take(count) {
860                        let expected = expected_queue.pop_front().expect("queue underflow");
861                        assert_eq!(*drained, expected);
862                    }
863                }
864            }
865        }
866
867        #[test]
868        fn invalid_cap_zero_returns_error() {
869            let result = Ring::<u32>::new(0);
870            assert!(matches!(result, Err(crate::ring::CapacityError)));
871        }
872    }
873
874    // the no-alloc tier's reason to exist: a bare-metal caller puts a ring
875    // straight in a `static` — no heap, no lazy OnceCell, no runtime init. That
876    // this compiles at all is the proof; the round trip proves the const path
877    // seeds the same per-index cell sequences the protocol needs.
878    #[test]
879    fn static_ring_backs_a_static_with_no_lazy_init() {
880        static RING: StaticRing<u32, 4> = StaticRing::new();
881        assert_eq!(RING.capacity(), 4);
882        for value in [11, 22, 33, 44] {
883            RING.push(value).unwrap();
884        }
885        assert_eq!(
886            RING.push(55),
887            Err(55),
888            "a const-built ring bounds like any other"
889        );
890        assert_eq!(RING.dequeue(), Some(11));
891    }
892
893    #[test]
894    fn static_ring_fifo_roundtrip() {
895        let ring = StaticRing::<u32, 4>::new();
896        assert_eq!(ring.capacity(), 4);
897        assert!(ring.is_empty());
898        for value in [10, 20, 30] {
899            ring.push(value).unwrap();
900        }
901        assert_eq!(ring.len(), 3);
902        assert_eq!(ring.dequeue(), Some(10));
903        assert_eq!(ring.dequeue(), Some(20));
904        assert_eq!(ring.dequeue(), Some(30));
905        assert_eq!(ring.dequeue(), None);
906    }
907
908    #[test]
909    fn static_ring_full_hands_record_back_without_blocking() {
910        let ring = StaticRing::<u32, 4>::new();
911        for value in [1, 2, 3, 4] {
912            ring.push(value).unwrap();
913        }
914        // at capacity: the record comes back via Err, never dropped, never blocks.
915        assert_eq!(ring.push(5), Err(5));
916        assert_eq!(ring.len(), 4);
917    }
918
919    #[test]
920    #[should_panic(expected = "power of two")]
921    fn static_ring_non_power_of_two_panics() {
922        let _ring = StaticRing::<u32, 6>::new();
923    }
924
925    #[test]
926    fn static_ring_drop_drains_buffered_records() {
927        struct Counted(Arc<AtomicUsize>);
928        impl Drop for Counted {
929            fn drop(&mut self) {
930                self.0.fetch_add(1, Ordering::Relaxed);
931            }
932        }
933
934        let drops = Arc::new(AtomicUsize::new(0));
935        {
936            let ring = StaticRing::<Counted, 8>::new();
937            for _ in 0..3 {
938                assert!(ring.push(Counted(Arc::clone(&drops))).is_ok());
939            }
940        } // ring dropped with 3 buffered — its Drop must drain them
941        assert_eq!(drops.load(Ordering::Relaxed), 3);
942    }
943
944    #[test]
945    fn static_ring_concurrent_mpmc_no_loss_no_dup() {
946        const PRODUCERS: usize = 4;
947        const PER: u32 = 10_000;
948        let total = PRODUCERS * PER as usize;
949
950        let ring = Arc::new(StaticRing::<u32, 1024>::new());
951        let seen = Arc::new(AtomicUsize::new(0));
952        let sum = Arc::new(AtomicUsize::new(0));
953        let mut handles = std::vec::Vec::new();
954
955        for producer in 0..PRODUCERS {
956            let ring = Arc::clone(&ring);
957            handles.push(thread::spawn(move || {
958                for index in 0..PER {
959                    let value = producer as u32 * PER + index;
960                    while ring.push(value).is_err() {
961                        std::hint::spin_loop();
962                    }
963                }
964            }));
965        }
966        for _ in 0..2 {
967            let ring = Arc::clone(&ring);
968            let seen = Arc::clone(&seen);
969            let sum = Arc::clone(&sum);
970            handles.push(thread::spawn(move || {
971                loop {
972                    if let Some(value) = ring.dequeue() {
973                        seen.fetch_add(1, Ordering::Relaxed);
974                        sum.fetch_add(value as usize, Ordering::Relaxed);
975                    } else if seen.load(Ordering::Relaxed) >= total {
976                        break;
977                    } else {
978                        std::hint::spin_loop();
979                    }
980                }
981            }));
982        }
983        for handle in handles {
984            handle.join().unwrap();
985        }
986
987        assert_eq!(seen.load(Ordering::Relaxed), total, "no item lost");
988        // every distinct value 0..total appeared exactly once → sum is unique-sum.
989        let expected: usize = (0..total).sum();
990        assert_eq!(
991            sum.load(Ordering::Relaxed),
992            expected,
993            "no duplicate or torn read"
994        );
995    }
996}