Skip to main content

subms_mpsc_queue/features/
bounded.rs

1//! Bounded MPSC queue: fixed-capacity ring buffer with backpressure.
2//!
3//! Producers see backpressure via [`BoundedMpscQueue::try_enqueue`]
4//! returning the rejected value when the ring is full. Single consumer
5//! only ([`BoundedMpscQueue::try_dequeue`] takes `&mut self`).
6//!
7//! Layout: power-of-two capacity, per-slot sequence numbers. Producers
8//! CAS the tail to claim a slot, then write the value and bump the
9//! slot's sequence to publish. The consumer reads slots in order and
10//! advances head once each is consumed.
11//!
12//! `try_enqueue` is wait-free in the uncontended case and bounded-retry
13//! under contention (each retry corresponds to a competing producer
14//! that won the CAS).
15
16use std::cell::UnsafeCell;
17use std::mem::MaybeUninit;
18use std::sync::atomic::{AtomicUsize, Ordering};
19
20/// Bounded MPSC ring queue. Capacity is rounded up to the next power
21/// of two (minimum 2) so the modulo can be a bitmask.
22pub struct BoundedMpscQueue<T> {
23    mask: usize,
24    slots: Box<[Slot<T>]>,
25    /// Written only by the consumer. Atomic rather than a plain cell so the
26    /// introspection getters stay sound when a producer thread calls them
27    /// through a shared handle.
28    head: AtomicUsize,
29    tail: AtomicUsize,
30}
31
32struct Slot<T> {
33    seq: AtomicUsize,
34    value: UnsafeCell<MaybeUninit<T>>,
35}
36
37unsafe impl<T: Send> Sync for BoundedMpscQueue<T> {}
38unsafe impl<T: Send> Send for BoundedMpscQueue<T> {}
39
40impl<T> BoundedMpscQueue<T> {
41    /// New empty queue. `capacity` is rounded up to a power of two,
42    /// minimum 2.
43    pub fn new(capacity: usize) -> Self {
44        let cap = capacity.next_power_of_two().max(2);
45        let mut slots = Vec::with_capacity(cap);
46        for i in 0..cap {
47            slots.push(Slot {
48                seq: AtomicUsize::new(i),
49                value: UnsafeCell::new(MaybeUninit::uninit()),
50            });
51        }
52        Self {
53            mask: cap - 1,
54            slots: slots.into_boxed_slice(),
55            head: AtomicUsize::new(0),
56            tail: AtomicUsize::new(0),
57        }
58    }
59
60    /// Capacity (power-of-two; possibly larger than requested).
61    pub fn capacity(&self) -> usize {
62        self.mask + 1
63    }
64
65    /// Monotonic count of slots ever claimed by producers. Safe to read from
66    /// any thread; pair it with [`Self::consumer_index`] to get lag, or sample
67    /// it twice to get throughput without disturbing either end.
68    pub fn producer_index(&self) -> usize {
69        self.tail.load(Ordering::Acquire)
70    }
71
72    /// Monotonic count of slots ever consumed. Safe to read from any thread.
73    pub fn consumer_index(&self) -> usize {
74        self.head.load(Ordering::Acquire)
75    }
76
77    /// Multi-producer push. Returns `Err(value)` when the ring is
78    /// full so the caller can retry, drop, or apply backpressure.
79    pub fn try_enqueue(&self, value: T) -> Result<(), T> {
80        let mut tail = self.tail.load(Ordering::Relaxed);
81        loop {
82            let slot = &self.slots[tail & self.mask];
83            let seq = slot.seq.load(Ordering::Acquire);
84            // Slot is open for write when seq == tail. seq < tail means
85            // a consumer hasn't caught up yet (full); seq > tail means
86            // another producer already claimed this slot.
87            let diff = seq.wrapping_sub(tail) as isize;
88            if diff == 0 {
89                match self.tail.compare_exchange_weak(
90                    tail,
91                    tail.wrapping_add(1),
92                    Ordering::Relaxed,
93                    Ordering::Relaxed,
94                ) {
95                    Ok(_) => {
96                        unsafe { (*slot.value.get()).write(value) };
97                        slot.seq.store(tail.wrapping_add(1), Ordering::Release);
98                        return Ok(());
99                    }
100                    Err(t) => tail = t,
101                }
102            } else if diff < 0 {
103                // Queue is full.
104                return Err(value);
105            } else {
106                // Another producer is ahead; refresh and retry.
107                tail = self.tail.load(Ordering::Relaxed);
108            }
109        }
110    }
111
112    /// Single-consumer pop. Returns `None` when the ring is empty.
113    pub fn try_dequeue(&mut self) -> Option<T> {
114        let head = self.head.load(Ordering::Relaxed);
115        let slot = &self.slots[head & self.mask];
116        let seq = slot.seq.load(Ordering::Acquire);
117        let diff = seq.wrapping_sub(head.wrapping_add(1)) as isize;
118        if diff == 0 {
119            let value = unsafe { (*slot.value.get()).assume_init_read() };
120            // Mark slot ready for the next producer pass.
121            slot.seq
122                .store(head.wrapping_add(self.mask + 1), Ordering::Release);
123            self.head.store(head.wrapping_add(1), Ordering::Release);
124            Some(value)
125        } else {
126            None
127        }
128    }
129
130    /// Borrow the next value without consuming it. `None` when the ring is
131    /// empty. Consumer-side only.
132    pub fn peek(&mut self) -> Option<&T> {
133        let head = self.head.load(Ordering::Relaxed);
134        let slot = &self.slots[head & self.mask];
135        let seq = slot.seq.load(Ordering::Acquire);
136        if seq.wrapping_sub(head.wrapping_add(1)) as isize == 0 {
137            Some(unsafe { (*slot.value.get()).assume_init_ref() })
138        } else {
139            None
140        }
141    }
142
143    /// Drop everything currently readable and return the count. Producers keep
144    /// publishing throughout, so the ring is not guaranteed empty on return.
145    /// Consumer-side only.
146    pub fn clear(&mut self) -> usize {
147        let mut n = 0;
148        while self.try_dequeue().is_some() {
149            n += 1;
150        }
151        n
152    }
153
154    /// Best-effort length. Approximate under producer contention.
155    pub fn len(&self) -> usize {
156        let head = self.head.load(Ordering::Acquire);
157        let tail = self.tail.load(Ordering::Acquire);
158        tail.wrapping_sub(head)
159    }
160
161    pub fn is_empty(&self) -> bool {
162        self.len() == 0
163    }
164
165    /// Best-effort fullness. A `true` can go stale the instant the consumer
166    /// drains a slot, so branch on [`Self::try_enqueue`] instead of this when
167    /// the answer decides whether a push lands.
168    pub fn is_full(&self) -> bool {
169        self.len() >= self.capacity()
170    }
171}
172
173impl<T> Drop for BoundedMpscQueue<T> {
174    fn drop(&mut self) {
175        // Drain remaining initialized slots so their destructors run.
176        self.clear();
177    }
178}
179
180#[cfg(test)]
181#[path = "bounded_tests.rs"]
182mod tests;