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;