Skip to main content

subetha_cxc/
mpmc_ring.rs

1//! `SharedRingMpmc` - composed multi-producer / multi-consumer
2//! ring built from N independent Lamport SPSC rings, with M
3//! consumers partitioning the rings round-robin.
4//!
5//! Architecture:
6//!
7//!  * N producers, each sole-writer to its own
8//!    [`SpscRingCore`]. Per-push cost is one Acquire load + one
9//!    Release store (pure Lamport).
10//!  * M consumers, each statically assigned a subset of the N
11//!    producer rings (round-robin: consumer `i` gets producer rings
12//!    `i, i+M, i+2M, ...`). Each consumer is the sole drainer of
13//!    its subset, so the consumer-side CAS Vyukov MPMC needs is
14//!    not present here either.
15//!
16//! Per-pop cost (when the chosen ring has an item):
17//!  - 1 Acquire load + 1 Release store on the consumer's subset
18//!    ring. Pure Lamport.
19//!
20//! Per-pop cost (worst case, every ring in the subset is empty):
21//!  - 1 Acquire load per ring in the subset (`ceil(N/M)` checks),
22//!    then `Err(Empty)`.
23//!
24//! # When this is the right MPMC primitive
25//!
26//! `SharedRingMpmc` is the **default-recommended MPMC primitive**
27//! when callers do NOT need global FIFO order across all
28//! producers. It preserves **per-producer FIFO** (items from one
29//! producer arrive at one consumer in push order), but items from
30//! different producers can interleave at different consumers
31//! arbitrarily.
32//!
33//! Use [`SharedRing`](crate::SharedRing) (Vyukov MPMC) if global
34//! FIFO across all producers is required. The Vyukov primitive is
35//! somewhat slower under contention - this grid runs ~1.3-1.6x
36//! faster at the same total buffer - but gives total ordering.
37//!
38//! # Compile-time contracts
39//!
40//! - Producer handles ([`MpmcProducer`]): `!Sync + !Clone + Send`.
41//!   Each handle is the sole writer for its ring; the compiler
42//!   guarantees one producer per ring at one time.
43//! - Consumer handles ([`MpmcConsumer`]): `!Sync + !Clone + Send`.
44//!   Each handle is the sole drainer of its assigned subset; the
45//!   compiler guarantees one consumer per subset.
46//!
47//! Callers receive `Vec<MpmcProducer>` and `Vec<MpmcConsumer>` at
48//! construction and move each handle to its dedicated thread.
49
50use std::cell::Cell;
51use std::marker::PhantomData;
52use std::path::Path;
53use std::sync::Arc;
54use std::sync::atomic::{AtomicUsize, Ordering};
55
56use crate::shared_ring::RingError;
57use crate::spsc_ring::SpscRingCore;
58
59/// Factory for an MPMC grid composed from N Lamport SPSC rings
60/// partitioned across M consumers.
61pub struct SharedRingMpmc;
62
63/// One producer handle. Sole writer to one underlying SPSC ring.
64pub struct MpmcProducer {
65    inner: Arc<SpscRingCore>,
66    _not_sync: PhantomData<Cell<()>>,
67}
68
69/// One consumer handle. Sole drainer of a subset of producer rings
70/// (round-robin assignment from the factory).
71pub struct MpmcConsumer {
72    rings: Vec<Arc<SpscRingCore>>,
73    next_drain: AtomicUsize,
74    _not_sync: PhantomData<Cell<()>>,
75}
76
77impl SharedRingMpmc {
78    /// Anonymous in-memory MPMC grid: `n_producers` rings,
79    /// `n_consumers` consumer handles. Consumer `i` drains
80    /// producer rings `i`, `i + n_consumers`, `i + 2*n_consumers`,
81    /// and so on.
82    ///
83    /// Constraints: `n_consumers >= 1`, `n_producers >= n_consumers`
84    /// (so every consumer has at least one ring; idle consumers
85    /// are a configuration smell, not a feature).
86    pub fn create_anon_grid(
87        n_producers: usize,
88        n_consumers: usize,
89        capacity: usize,
90    ) -> Result<(Vec<MpmcProducer>, Vec<MpmcConsumer>), RingError> {
91        assert!(n_consumers >= 1, "n_consumers must be >= 1");
92        assert!(
93            n_producers >= n_consumers,
94            "n_producers ({n_producers}) must be >= n_consumers ({n_consumers}); \
95             every consumer needs at least one ring to drain",
96        );
97
98        let mut rings: Vec<Arc<SpscRingCore>> = Vec::with_capacity(n_producers);
99        for _ in 0..n_producers {
100            rings.push(Arc::new(SpscRingCore::create_anon(capacity)?));
101        }
102        build_grid(rings, n_consumers)
103    }
104
105    /// MPMC grid laid out in ONE caller-owned region (huge / large
106    /// pages). All `n_producers` SPSC lanes are carved back-to-back
107    /// from the single region, so the whole grid sits on a handful of
108    /// 2 MB / 1 GB pages instead of `n_producers` separate small
109    /// mappings - the case where large pages actually shed TLB
110    /// pressure. The region must hold at least
111    /// `spsc_ring_file_size(capacity) * n_producers` bytes.
112    ///
113    /// This is the per-producer-FIFO MPMC primitive on large pages;
114    /// [`SharedRing::create_in_region`](crate::SharedRing::create_in_region)
115    /// is the global-FIFO (Vyukov) counterpart.
116    pub fn create_grid_in_region<R: crate::spsc_ring::RegionOwner>(
117        mut region: R,
118        n_producers: usize,
119        n_consumers: usize,
120        capacity: usize,
121    ) -> Result<(Vec<MpmcProducer>, Vec<MpmcConsumer>), RingError> {
122        assert!(n_consumers >= 1, "n_consumers must be >= 1");
123        assert!(n_producers >= n_consumers,
124            "n_producers must be >= n_consumers");
125        let lane_bytes = crate::spsc_ring::spsc_ring_file_size(capacity);
126        let need = lane_bytes
127            .checked_mul(n_producers)
128            .ok_or(RingError::LayoutMismatch)?;
129        if region.region_len() < need {
130            return Err(RingError::LayoutMismatch);
131        }
132        // Capture the base pointer while we still hold the region
133        // exclusively, then move it behind an Arc every lane keeps
134        // alive. The mapping address is stable across the move - it is
135        // an OS mapping, not the struct's own address.
136        let base = region.region_ptr();
137        let whole: Arc<dyn std::any::Any + Send + Sync> = Arc::new(region);
138
139        let mut rings: Vec<Arc<SpscRingCore>> = Vec::with_capacity(n_producers);
140        for i in 0..n_producers {
141            let lane = SubRegion {
142                _whole: Arc::clone(&whole),
143                ptr: unsafe { base.add(i * lane_bytes) },
144                len: lane_bytes,
145            };
146            rings.push(Arc::new(SpscRingCore::create_in_region(lane, capacity)?));
147        }
148        build_grid(rings, n_consumers)
149    }
150
151    /// File-backed MPMC grid: one file per producer ring; the
152    /// file path for ring `i` is `path_prefix.{i}.bin`.
153    pub fn create_grid(
154        path_prefix: impl AsRef<Path>,
155        n_producers: usize,
156        n_consumers: usize,
157        capacity: usize,
158    ) -> Result<(Vec<MpmcProducer>, Vec<MpmcConsumer>), RingError> {
159        assert!(n_consumers >= 1, "n_consumers must be >= 1");
160        assert!(n_producers >= n_consumers,
161            "n_producers must be >= n_consumers");
162        let base = path_prefix.as_ref().to_path_buf();
163        let mut rings: Vec<Arc<SpscRingCore>> = Vec::with_capacity(n_producers);
164        for i in 0..n_producers {
165            let path = ring_path(&base, i);
166            rings.push(Arc::new(SpscRingCore::create(&path, capacity)?));
167        }
168        build_grid(rings, n_consumers)
169    }
170
171    /// Open an existing file-backed grid.
172    pub fn open_grid(
173        path_prefix: impl AsRef<Path>,
174        n_producers: usize,
175        n_consumers: usize,
176        expected_capacity: usize,
177    ) -> Result<(Vec<MpmcProducer>, Vec<MpmcConsumer>), RingError> {
178        assert!(n_consumers >= 1, "n_consumers must be >= 1");
179        assert!(n_producers >= n_consumers,
180            "n_producers must be >= n_consumers");
181        let base = path_prefix.as_ref().to_path_buf();
182        let mut rings: Vec<Arc<SpscRingCore>> = Vec::with_capacity(n_producers);
183        for i in 0..n_producers {
184            let path = ring_path(&base, i);
185            rings.push(Arc::new(SpscRingCore::open(&path, expected_capacity)?));
186        }
187        build_grid(rings, n_consumers)
188    }
189}
190
191fn build_grid(
192    rings: Vec<Arc<SpscRingCore>>,
193    n_consumers: usize,
194) -> Result<(Vec<MpmcProducer>, Vec<MpmcConsumer>), RingError> {
195    let producers: Vec<MpmcProducer> = rings
196        .iter()
197        .map(|r| MpmcProducer {
198            inner: Arc::clone(r),
199            _not_sync: PhantomData,
200        })
201        .collect();
202
203    // Round-robin assign producer rings to consumer subsets.
204    let mut consumer_rings: Vec<Vec<Arc<SpscRingCore>>> =
205        (0..n_consumers).map(|_| Vec::new()).collect();
206    for (producer_idx, ring) in rings.iter().enumerate() {
207        consumer_rings[producer_idx % n_consumers].push(Arc::clone(ring));
208    }
209
210    let consumers: Vec<MpmcConsumer> = consumer_rings
211        .into_iter()
212        .map(|subset| MpmcConsumer {
213            rings: subset,
214            next_drain: AtomicUsize::new(0),
215            _not_sync: PhantomData,
216        })
217        .collect();
218
219    Ok((producers, consumers))
220}
221
222fn ring_path(prefix: &std::path::Path, i: usize) -> std::path::PathBuf {
223    let mut s = prefix.as_os_str().to_owned();
224    s.push(format!(".{i}.bin"));
225    std::path::PathBuf::from(s)
226}
227
228/// One back-to-back slice of a shared backing region, handed to a
229/// single grid lane. Holds an `Arc` to the whole region so the mapping
230/// outlives every lane carved from it; `ptr` is this lane's start
231/// (`base + lane_index * lane_bytes`).
232struct SubRegion {
233    _whole: Arc<dyn std::any::Any + Send + Sync>,
234    ptr: *mut u8,
235    len: usize,
236}
237
238// SAFETY: each lane owns a disjoint, non-overlapping byte range of the
239// shared region, and the Arc keeps the mapping alive. The raw pointer
240// points into OS-mapped memory whose address is stable for the
241// region's whole lifetime.
242unsafe impl Send for SubRegion {}
243unsafe impl Sync for SubRegion {}
244
245impl crate::spsc_ring::RegionOwner for SubRegion {
246    fn region_ptr(&mut self) -> *mut u8 { self.ptr }
247    fn region_len(&self) -> usize { self.len }
248}
249
250impl MpmcProducer {
251    /// Push one payload to this producer's ring. Pure Lamport SPSC.
252    pub fn try_push(&self, payload: &[u8]) -> Result<(), RingError> {
253        self.inner.try_push(payload)
254    }
255
256    /// Capacity of this producer's ring (always a power of 2).
257    pub fn capacity(&self) -> usize {
258        self.inner.capacity()
259    }
260
261    /// Current head of this producer's ring (own published position).
262    pub fn head(&self) -> u64 {
263        self.inner.head()
264    }
265}
266
267impl MpmcConsumer {
268    /// Drain one item from this consumer's assigned subset of
269    /// producer rings, round-robin. Returns `Ok` on the first
270    /// non-empty ring; `Err(Empty)` only if every ring in the
271    /// subset is empty.
272    pub fn try_pop(&self, out: &mut [u8]) -> Result<usize, RingError> {
273        let n = self.rings.len();
274        let start = self.next_drain.load(Ordering::Relaxed);
275        for i in 0..n {
276            let idx = (start + i) % n;
277            if let Ok(bytes) = self.rings[idx].try_pop(out) {
278                self.next_drain.store((idx + 1) % n, Ordering::Relaxed);
279                return Ok(bytes);
280            }
281        }
282        Err(RingError::Empty)
283    }
284
285    /// Number of producer rings assigned to this consumer.
286    pub fn n_rings(&self) -> usize {
287        self.rings.len()
288    }
289
290    /// Approximate total items waiting across this consumer's subset.
291    pub fn approx_subset_len(&self) -> usize {
292        self.rings.iter().map(|r| r.approx_len()).sum()
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    use crate::spsc_ring::SPSC_PAYLOAD_BYTES;
300    use std::thread;
301
302    #[test]
303    fn create_anon_grid_round_trip() {
304        // 4 producers, 2 consumers; each consumer drains 2 rings.
305        let (producers, consumers) =
306            SharedRingMpmc::create_anon_grid(4, 2, 8).unwrap();
307        assert_eq!(producers.len(), 4);
308        assert_eq!(consumers.len(), 2);
309        // Round-robin assignment: consumer 0 gets rings 0, 2;
310        // consumer 1 gets rings 1, 3.
311        assert_eq!(consumers[0].n_rings(), 2);
312        assert_eq!(consumers[1].n_rings(), 2);
313
314        for (i, p) in producers.iter().enumerate() {
315            let mut buf = [0u8; SPSC_PAYLOAD_BYTES];
316            buf[..4].copy_from_slice(&(i as u32).to_le_bytes());
317            p.try_push(&buf).unwrap();
318        }
319
320        // Consumer 0 should see items 0 + 2; consumer 1 should see 1 + 3.
321        let mut c0_seen = Vec::new();
322        let mut c1_seen = Vec::new();
323        let mut out = [0u8; SPSC_PAYLOAD_BYTES];
324        while consumers[0].try_pop(&mut out).is_ok() {
325            c0_seen.push(u32::from_le_bytes(out[..4].try_into().unwrap()));
326        }
327        while consumers[1].try_pop(&mut out).is_ok() {
328            c1_seen.push(u32::from_le_bytes(out[..4].try_into().unwrap()));
329        }
330        c0_seen.sort();
331        c1_seen.sort();
332        assert_eq!(c0_seen, vec![0, 2]);
333        assert_eq!(c1_seen, vec![1, 3]);
334    }
335
336    #[test]
337    fn concurrent_mpmc_loses_no_items() {
338        const N_PRODUCERS: usize = 4;
339        const N_CONSUMERS: usize = 2;
340        const PER_PRODUCER: u32 = 10_000;
341
342        let (producers, consumers) =
343            SharedRingMpmc::create_anon_grid(N_PRODUCERS, N_CONSUMERS, 64).unwrap();
344
345        let producer_handles: Vec<_> = producers
346            .into_iter()
347            .enumerate()
348            .map(|(pid, p)| {
349                thread::spawn(move || {
350                    for i in 0..PER_PRODUCER {
351                        let mut buf = [0u8; SPSC_PAYLOAD_BYTES];
352                        buf[..4].copy_from_slice(&(pid as u32).to_le_bytes());
353                        buf[4..8].copy_from_slice(&i.to_le_bytes());
354                        while p.try_push(&buf).is_err() {
355                            std::hint::spin_loop();
356                        }
357                    }
358                })
359            })
360            .collect();
361
362        let target_per_consumer = (PER_PRODUCER as usize * N_PRODUCERS / N_CONSUMERS) as u32;
363        let consumer_handles: Vec<_> = consumers
364            .into_iter()
365            .map(|c| {
366                thread::spawn(move || -> (u32, std::collections::HashMap<u32, u32>) {
367                    let mut next: std::collections::HashMap<u32, u32> = Default::default();
368                    let mut total: u32 = 0;
369                    let mut out = [0u8; SPSC_PAYLOAD_BYTES];
370                    while total < target_per_consumer {
371                        if c.try_pop(&mut out).is_ok() {
372                            let pid = u32::from_le_bytes(out[..4].try_into().unwrap());
373                            let seq = u32::from_le_bytes(out[4..8].try_into().unwrap());
374                            let expected = next.entry(pid).or_insert(0);
375                            assert_eq!(*expected, seq,
376                                "per-producer FIFO violated for producer {pid}: expected {} got {}",
377                                expected, seq);
378                            *expected += 1;
379                            total += 1;
380                        } else {
381                            std::hint::spin_loop();
382                        }
383                    }
384                    (total, next)
385                })
386            })
387            .collect();
388
389        for h in producer_handles {
390            h.join().unwrap();
391        }
392        let mut grand_total: u32 = 0;
393        for h in consumer_handles {
394            let (t, _next) = h.join().unwrap();
395            grand_total += t;
396        }
397        assert_eq!(grand_total, PER_PRODUCER * N_PRODUCERS as u32);
398    }
399
400    #[test]
401    fn create_grid_in_region_round_trip() {
402        // Carve all four SPSC lanes from ONE heap-backed region (the
403        // large-page path in miniature; a heap region needs no
404        // privilege). Round-trip integrity proves the lanes occupy
405        // disjoint, non-overlapping byte ranges.
406        use crate::spsc_ring::{spsc_ring_file_size, RegionOwner};
407        // 64-byte-aligned heap backing (a page-backed region gives this
408        // for free; a Box<[u8]> would not).
409        #[repr(C, align(64))]
410        #[derive(Clone, Copy)]
411        struct Block64([u8; 64]);
412        struct HeapRegion(Vec<Block64>);
413        impl RegionOwner for HeapRegion {
414            fn region_ptr(&mut self) -> *mut u8 {
415                self.0.as_mut_ptr() as *mut u8
416            }
417            fn region_len(&self) -> usize { self.0.len() * 64 }
418        }
419
420        let (n_prod, n_cons, cap) = (4usize, 2usize, 8usize);
421        let bytes = spsc_ring_file_size(cap) * n_prod;
422        let region = HeapRegion(vec![Block64([0u8; 64]); bytes.div_ceil(64)]);
423        let (producers, consumers) =
424            SharedRingMpmc::create_grid_in_region(region, n_prod, n_cons, cap)
425                .unwrap();
426        assert_eq!(producers.len(), 4);
427        assert_eq!(consumers.len(), 2);
428
429        // Stamp each lane with its producer index, drain everything,
430        // and confirm all four arrived exactly once.
431        for (i, p) in producers.iter().enumerate() {
432            let mut buf = [0u8; SPSC_PAYLOAD_BYTES];
433            buf[..4].copy_from_slice(&(i as u32).to_le_bytes());
434            p.try_push(&buf).unwrap();
435        }
436        let mut seen = Vec::new();
437        let mut out = [0u8; SPSC_PAYLOAD_BYTES];
438        for c in &consumers {
439            while c.try_pop(&mut out).is_ok() {
440                seen.push(u32::from_le_bytes(out[..4].try_into().unwrap()));
441            }
442        }
443        seen.sort();
444        assert_eq!(seen, vec![0, 1, 2, 3]);
445    }
446}