subms_spsc_ring_buffer/features/mpmc_disruptor.rs
1//! Multi-producer multi-consumer ring with sequence-barrier gating.
2//!
3//! Based on the LMAX Disruptor pattern. Three sequences:
4//!
5//! - `producer_cursor`: next index a producer will try to claim. CAS'd up
6//! by `try_claim`.
7//! - `published[idx]`: per-slot atomic published flag. A claimed slot is
8//! marked published once the producer finishes writing.
9//! - `consumer_cursor[i]`: per-consumer next index to read. Owned by that
10//! consumer; advanced after a successful read.
11//!
12//! Producer gating: a producer cannot claim slot `s` until the slowest
13//! consumer has passed `s - capacity`. Consumer gating: a consumer cannot
14//! read slot `s` until `published[s & mask]` matches the round it's in.
15//! `published` stores the absolute sequence value of the last publish so
16//! producers and consumers can distinguish round N from round N+1 in the
17//! same slot.
18//!
19//! Independent of the base SPSC structures - this is a separate ring with
20//! different invariants.
21
22use std::cell::UnsafeCell;
23use std::mem::MaybeUninit;
24use std::sync::Arc;
25use std::sync::atomic::{AtomicI64, Ordering};
26
27const UNPUBLISHED: i64 = -1;
28
29struct DisruptorInner<T> {
30 buf: Box<[UnsafeCell<MaybeUninit<T>>]>,
31 /// `published[i]` holds the absolute sequence number of the last
32 /// publish into slot `i`, or `UNPUBLISHED`.
33 published: Box<[AtomicI64]>,
34 /// Next sequence a producer will try to claim (incremented via CAS).
35 producer_cursor: AtomicI64,
36 /// One cursor per consumer. Producers gate behind the slowest.
37 consumer_cursors: Box<[AtomicI64]>,
38 capacity: i64,
39 mask: i64,
40}
41
42unsafe impl<T: Send> Sync for DisruptorInner<T> {}
43unsafe impl<T: Send> Send for DisruptorInner<T> {}
44
45impl<T> Drop for DisruptorInner<T> {
46 fn drop(&mut self) {
47 // Drop any item that was published but not consumed.
48 for slot in self.published.iter() {
49 let seq = slot.load(Ordering::Relaxed);
50 if seq != UNPUBLISHED {
51 let idx = (seq & self.mask) as usize;
52 // Only drop if no consumer has passed this sequence.
53 let min_consumer = self
54 .consumer_cursors
55 .iter()
56 .map(|c| c.load(Ordering::Relaxed))
57 .min()
58 .unwrap_or(-1);
59 if seq > min_consumer {
60 unsafe {
61 (*self.buf[idx].get()).assume_init_drop();
62 }
63 }
64 }
65 }
66 }
67}
68
69/// Builder + constructor. Use `MpmcDisruptor::with_consumers`.
70pub struct MpmcDisruptor;
71
72impl MpmcDisruptor {
73 /// Build a disruptor with `consumer_count` consumers and at least
74 /// `requested_capacity` slots (rounded up to power of two, floor 2).
75 /// All consumers see every published item (broadcast); for work-stealing
76 /// semantics use `MpscFanIn` with a single consumer per producer instead.
77 pub fn with_consumers<T: Send + 'static>(
78 requested_capacity: usize,
79 consumer_count: usize,
80 ) -> (DisruptorProducer<T>, Vec<DisruptorConsumer<T>>) {
81 assert!(consumer_count >= 1, "need at least one consumer");
82 let cap = requested_capacity.max(2).next_power_of_two();
83 let mut buf = Vec::with_capacity(cap);
84 let mut published = Vec::with_capacity(cap);
85 for _ in 0..cap {
86 buf.push(UnsafeCell::new(MaybeUninit::<T>::uninit()));
87 published.push(AtomicI64::new(UNPUBLISHED));
88 }
89 let mut cursors = Vec::with_capacity(consumer_count);
90 for _ in 0..consumer_count {
91 cursors.push(AtomicI64::new(-1));
92 }
93 let inner = Arc::new(DisruptorInner {
94 buf: buf.into_boxed_slice(),
95 published: published.into_boxed_slice(),
96 producer_cursor: AtomicI64::new(-1),
97 consumer_cursors: cursors.into_boxed_slice(),
98 capacity: cap as i64,
99 mask: (cap - 1) as i64,
100 });
101 let producer = DisruptorProducer {
102 inner: inner.clone(),
103 };
104 let consumers = (0..consumer_count)
105 .map(|i| DisruptorConsumer {
106 inner: inner.clone(),
107 idx: i,
108 next: 0,
109 })
110 .collect();
111 (producer, consumers)
112 }
113}
114
115/// Multi-producer side. Cheap to `Clone`; share across producer threads.
116pub struct DisruptorProducer<T> {
117 inner: Arc<DisruptorInner<T>>,
118}
119
120impl<T> Clone for DisruptorProducer<T> {
121 fn clone(&self) -> Self {
122 Self {
123 inner: self.inner.clone(),
124 }
125 }
126}
127
128unsafe impl<T: Send> Send for DisruptorProducer<T> {}
129unsafe impl<T: Send> Sync for DisruptorProducer<T> {}
130
131impl<T> DisruptorProducer<T> {
132 /// Try to publish a value. Returns the input back on contention OR
133 /// when the slowest consumer hasn't caught up yet.
134 pub fn try_publish(&self, value: T) -> Result<(), T> {
135 loop {
136 let cur = self.inner.producer_cursor.load(Ordering::Relaxed);
137 let next = cur + 1;
138
139 // Is the slot we'd claim still in-flight for some consumer?
140 // Find the slowest consumer and check.
141 let mut min_consumer = i64::MAX;
142 for c in self.inner.consumer_cursors.iter() {
143 let v = c.load(Ordering::Acquire);
144 if v < min_consumer {
145 min_consumer = v;
146 }
147 }
148 if next - min_consumer > self.inner.capacity {
149 // Ring full from the slowest consumer's perspective.
150 return Err(value);
151 }
152
153 // Claim the slot via CAS.
154 match self.inner.producer_cursor.compare_exchange_weak(
155 cur,
156 next,
157 Ordering::AcqRel,
158 Ordering::Relaxed,
159 ) {
160 Ok(_) => {
161 // We own slot `next`. Wait until the previous occupant
162 // of slot index (next & mask) was fully consumed - it
163 // either is UNPUBLISHED (first round) or carries a
164 // sequence number `next - capacity`, which means it's
165 // safe to overwrite ONLY if all consumers have read it.
166 // The min_consumer check above already guarantees this
167 // for the round; but other producers may have claimed
168 // higher slots after we read the cursors. Re-confirm
169 // by waiting for the slot's `published` to be cleared
170 // for the previous round.
171 let idx = (next & self.inner.mask) as usize;
172 let prev_round = next - self.inner.capacity;
173 if prev_round >= 0 {
174 // Spin until the slot is no longer holding the
175 // prior round (consumer cursors must have advanced
176 // past it). This is the rare slow-path; uncontested
177 // claims skip it entirely.
178 while self
179 .inner
180 .consumer_cursors
181 .iter()
182 .any(|c| c.load(Ordering::Acquire) < prev_round)
183 {
184 std::hint::spin_loop();
185 }
186 }
187
188 unsafe {
189 (*self.inner.buf[idx].get()).write(value);
190 }
191 // Publish: store our absolute sequence so consumers can
192 // distinguish round-0 from round-1 in the same slot.
193 self.inner.published[idx].store(next, Ordering::Release);
194 return Ok(());
195 }
196 Err(_) => {
197 // Contention; loop and retry.
198 std::hint::spin_loop();
199 }
200 }
201 }
202 }
203
204 pub fn capacity(&self) -> usize {
205 self.inner.capacity as usize
206 }
207}
208
209/// One consumer side. Each consumer sees every published item.
210pub struct DisruptorConsumer<T> {
211 inner: Arc<DisruptorInner<T>>,
212 idx: usize,
213 next: i64,
214}
215
216unsafe impl<T: Send> Send for DisruptorConsumer<T> {}
217
218impl<T: Clone> DisruptorConsumer<T> {
219 /// Returns a clone of the next published item, or `None` if not ready.
220 /// `Clone` because each consumer of a broadcast disruptor sees the
221 /// same item; we cannot move out of the slot.
222 pub fn try_consume(&mut self) -> Option<T> {
223 let seq = self.next;
224 let idx = (seq & self.inner.mask) as usize;
225 if self.inner.published[idx].load(Ordering::Acquire) != seq {
226 return None;
227 }
228 // Item is published; clone it out and advance.
229 let v = unsafe { (*self.inner.buf[idx].get()).assume_init_ref() }.clone();
230 self.next = seq + 1;
231 self.inner.consumer_cursors[self.idx].store(seq, Ordering::Release);
232 Some(v)
233 }
234
235 pub fn capacity(&self) -> usize {
236 self.inner.capacity as usize
237 }
238}
239
240#[cfg(test)]
241#[path = "mpmc_disruptor_tests.rs"]
242mod tests;