Skip to main content

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;