Skip to main content

subetha_cxc/
spsc_ring.rs

1//! `SpscRingCore` - Lamport 1983 single-producer / single-consumer
2//! ring backed by a memory-mapped file.
3//!
4//! This is the SPSC-specialised counterpart of
5//! [`SharedRing`](crate::SharedRing). Where `SharedRing` carries
6//! the Vyukov MPMC protocol (per-slot sequence number, CAS on the
7//! producer / consumer counters), `SpscRingCore` strips the protocol
8//! down to its SPSC minimum: a head counter the producer owns, a
9//! tail counter the consumer owns, and payload-only slots.
10//!
11//! # Per-op atomic budget
12//!
13//! Push:
14//!  1. `head.load(Relaxed)` - owner-private; no cross-thread contention.
15//!  2. `tail.load(Acquire)` - read the consumer's position to check full.
16//!  3. write payload (non-atomic memcpy into the slot).
17//!  4. `head.store(head + 1, Release)` - publish to the consumer.
18//!
19//! That is **one Acquire load + one Release store** of cross-thread-
20//! visible atomics, plus one owner-private Relaxed load. Vyukov MPMC
21//! on the same shape needs four cross-thread atomics (load + CAS on
22//! `producer_seq`, then load + store on the slot's sequence number).
23//! Halving the atomic budget is where the Lamport SPSC win comes from.
24//!
25//! Pop mirrors push.
26//!
27//! # False sharing
28//!
29//! `head` and `tail` live on separate 64-byte cache lines. The
30//! producer writes head every push; the consumer writes tail every
31//! pop. Co-locating them would invalidate the peer's cache line on
32//! every op and crater throughput.
33//!
34//! # Crash recovery
35//!
36//! Producer crash: if the sole producer dies between writing payload
37//! and the Release-store on `head`, the head counter never advances
38//! and the consumer sees no new item. The slot at `head % cap`
39//! contains partial / garbage bytes, but the consumer never reads
40//! it because head was not published. There is no stuck-slot
41//! pathology to heal - unlike `SharedRing`'s Vyukov protocol, the
42//! producer never "claims" a slot before publishing.
43//!
44//! Consumer crash: same shape; tail does not advance, head keeps
45//! growing, ring fills, producer eventually returns `Full`.
46//!
47//! In SPSC there is no second producer to take over from the dead
48//! one, so producer-side recovery is "restart the sole producer".
49//! No heal_stuck_slot equivalent is needed or possible here.
50
51use std::cell::UnsafeCell;
52use std::fs::{File, OpenOptions};
53use std::path::Path;
54use std::sync::atomic::{AtomicU64, Ordering};
55
56use memmap2::{MmapMut, MmapOptions};
57
58use crate::shared_ring::RingError;
59
60/// Magic number identifying a Lamport SPSC ring header. ASCII
61/// "SPSC" + version byte.
62pub const SPSC_MAGIC: u64 = 0x5350_5343_0000_0001;
63
64/// Each slot is exactly one cache line. Payload-only (no per-slot
65/// atomic), so the full 64 bytes are available to the caller.
66pub const SPSC_SLOT_SIZE: usize = 64;
67
68/// Payload bytes per slot.
69pub const SPSC_PAYLOAD_BYTES: usize = SPSC_SLOT_SIZE;
70
71/// Header layout for a Lamport SPSC ring. Three cache lines:
72/// metadata, then producer-owned `head`, then consumer-owned `tail`.
73/// Separate cache lines for `head` and `tail` eliminate false
74/// sharing between producer and consumer hot paths.
75#[repr(C, align(64))]
76pub struct SpscHeader {
77    pub magic: u64,
78    pub capacity: u64,
79    pub slot_size: u64,
80    /// Pad metadata line out to 64 bytes.
81    _pad_meta: [u8; 64 - 24],
82    /// Producer-owned head counter; consumer reads via Acquire.
83    pub head: AtomicU64,
84    _pad_head: [u8; 64 - 8],
85    /// Consumer-owned tail counter; producer reads via Acquire.
86    pub tail: AtomicU64,
87    _pad_tail: [u8; 64 - 8],
88}
89
90#[repr(C, align(64))]
91pub struct SpscSlot {
92    pub payload: UnsafeCell<[u8; SPSC_PAYLOAD_BYTES]>,
93}
94
95unsafe impl Sync for SpscSlot {}
96
97/// Total file size for a ring of `capacity` payload slots, including
98/// the three-cache-line header.
99pub const fn spsc_ring_file_size(capacity: usize) -> usize {
100    std::mem::size_of::<SpscHeader>() + capacity * SPSC_SLOT_SIZE
101}
102
103/// Caller-owned memory a ring can be laid out in: huge / large pages,
104/// or any region. The ring takes ownership (keeping it mapped) and
105/// writes its header + slots into the region's bytes. Implemented for
106/// `HugepageRegion` (Linux), `LargePageRegion` / `LargePageSection`
107/// (Windows) - so a ring can sit on 2 MB / 1 GB pages and shed the TLB
108/// pressure of thousands of 4 KB pages, which matters once you have
109/// many rings or one very large one.
110/// The ring header is `align(64)`, so the region base must be
111/// 64-byte aligned. Page-backed regions (huge / large pages, mmap)
112/// satisfy this by construction; a hand-rolled region must align its
113/// buffer or the constructor returns
114/// [`RingError::LayoutMismatch`](crate::shared_ring::RingError).
115pub trait RegionOwner: Send + Sync + 'static {
116    /// Pointer to the start of the region (must be 64-byte aligned).
117    fn region_ptr(&mut self) -> *mut u8;
118    /// Region length in bytes (must be >= the ring's file size).
119    fn region_len(&self) -> usize;
120}
121
122/// Cache-line / header alignment every ring layout requires.
123const REGION_ALIGN: usize = 64;
124
125/// Backing-store discriminator for `SpscRingCore`. The variant
126/// holds the underlying memory owner so it stays alive for the
127/// lifetime of the ring; the raw byte access goes through
128/// `SpscRingCore::raw_ptr`. The held values are intentionally
129/// never read directly (lifetime extension only).
130#[allow(dead_code)]
131enum SpscBacking {
132    /// Anonymous in-process memory.
133    Anon(MmapMut),
134    /// File-backed (cross-process via page cache).
135    File(File, MmapMut),
136    /// Named RAM-resident shared memory (cross-process, no page cache).
137    Shm(crate::shm_file::ShmFile),
138    /// Caller-owned region (huge / large pages, or any `RegionOwner`).
139    Region(Box<dyn RegionOwner>),
140}
141
142/// Lamport SPSC ring core. Used as the storage backing
143/// [`SharedRingSpsc`](crate::SharedRingSpsc); applications normally
144/// reach for the typed `Producer` / `Consumer` halves rather than
145/// this raw core.
146pub struct SpscRingCore {
147    /// Owns the underlying memory; never accessed directly after
148    /// construction (raw_ptr captures the pointer once).
149    _backing: SpscBacking,
150    /// Stable byte pointer into the backing for the lifetime of self.
151    /// Header lives at byte 0; slots start at offset
152    /// `size_of::<SpscHeader>()`. Total mapped size is
153    /// `spsc_ring_file_size(capacity)`.
154    raw_ptr: *mut u8,
155    capacity: usize,
156}
157
158unsafe impl Send for SpscRingCore {}
159unsafe impl Sync for SpscRingCore {}
160
161fn init_spsc_layout(mmap: &mut MmapMut, capacity: usize) {
162    unsafe { init_spsc_layout_raw(mmap.as_mut_ptr(), capacity) };
163}
164
165/// Backing-agnostic layout init. Zeroes the whole region (the zeroed
166/// slots are the empty payload state and the cursors start at zero),
167/// writes the geometry fields, then the magic, last, because attachers
168/// spin on it. Caller guarantees that `ptr` points to at least
169/// `spsc_ring_file_size(capacity)` bytes of mutable, suitably-aligned
170/// memory.
171unsafe fn init_spsc_layout_raw(ptr: *mut u8, capacity: usize) {
172    unsafe {
173        std::ptr::write_bytes(ptr, 0, spsc_ring_file_size(capacity));
174        let header_ptr = ptr as *mut SpscHeader;
175        (*header_ptr).capacity = capacity as u64;
176        (*header_ptr).slot_size = SPSC_SLOT_SIZE as u64;
177        std::ptr::write_volatile(&raw mut (*header_ptr).magic, SPSC_MAGIC);
178    }
179}
180
181impl SpscRingCore {
182    /// Anonymous in-memory ring (in-process only). Fastest construction;
183    /// skips file create + ftruncate + first-page-fault.
184    pub fn create_anon(capacity: usize) -> Result<Self, RingError> {
185        assert!(capacity.is_power_of_two() && capacity >= 2,
186                "capacity must be pow2 >= 2");
187        let total = spsc_ring_file_size(capacity);
188        let mut mmap = MmapOptions::new().len(total).map_anon()?;
189        init_spsc_layout(&mut mmap, capacity);
190        let raw_ptr = mmap.as_mut_ptr();
191        Ok(Self {
192            _backing: SpscBacking::Anon(mmap),
193            raw_ptr, capacity,
194        })
195    }
196
197    /// File-backed ring; cross-process visibility via the OS page cache.
198    /// Obtains the ring at `path`: initializes an empty one if the path
199    /// does not yet exist and attaches to it if it does. Attaching
200    /// leaves queued items and both cursors in place; a ring built with
201    /// a different capacity is a `LayoutMismatch`.
202    /// [`reset`](Self::reset) reinitializes.
203    pub fn create(path: impl AsRef<Path>, capacity: usize) -> Result<Self, RingError> {
204        assert!(capacity.is_power_of_two() && capacity >= 2,
205                "capacity must be pow2 >= 2");
206        let total = spsc_ring_file_size(capacity);
207        let (file, mut mmap) = crate::mmf_attach::create_or_attach(
208            path.as_ref(),
209            total,
210            |ptr| unsafe { init_spsc_layout_raw(ptr, capacity) },
211            |ptr| unsafe { (*(ptr as *const SpscHeader)).magic == SPSC_MAGIC },
212        )?;
213        let header = unsafe { &*(mmap.as_ptr() as *const SpscHeader) };
214        if header.magic != SPSC_MAGIC
215            || header.capacity != capacity as u64
216            || header.slot_size != SPSC_SLOT_SIZE as u64
217        {
218            return Err(RingError::LayoutMismatch);
219        }
220        let raw_ptr = mmap.as_mut_ptr();
221        Ok(Self {
222            _backing: SpscBacking::File(file, mmap),
223            raw_ptr, capacity,
224        })
225    }
226
227    /// Truncate the ring at `path` and initialize an empty one,
228    /// discarding queued items live peers hold. For a caller that
229    /// knows it owns the path.
230    pub fn reset(path: impl AsRef<Path>, capacity: usize) -> Result<Self, RingError> {
231        assert!(capacity.is_power_of_two() && capacity >= 2,
232                "capacity must be pow2 >= 2");
233        let total = spsc_ring_file_size(capacity);
234        let (file, mut mmap) = crate::mmf_attach::reset(
235            path.as_ref(),
236            total,
237            |ptr| unsafe { init_spsc_layout_raw(ptr, capacity) },
238        )?;
239        let raw_ptr = mmap.as_mut_ptr();
240        Ok(Self {
241            _backing: SpscBacking::File(file, mmap),
242            raw_ptr, capacity,
243        })
244    }
245
246    /// Open an existing file-backed ring. Validates magic + capacity.
247    pub fn open(path: impl AsRef<Path>, expected_capacity: usize) -> Result<Self, RingError> {
248        let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
249        let total = spsc_ring_file_size(expected_capacity);
250        let actual_len = file.metadata()?.len();
251        if (actual_len as usize) < total {
252            return Err(RingError::LayoutMismatch);
253        }
254        let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
255        let header = unsafe { &*(mmap.as_ptr() as *const SpscHeader) };
256        if header.magic != SPSC_MAGIC
257            || header.capacity != expected_capacity as u64
258            || header.slot_size != SPSC_SLOT_SIZE as u64
259        {
260            return Err(RingError::LayoutMismatch);
261        }
262        let raw_ptr = mmap.as_mut_ptr();
263        Ok(Self {
264            _backing: SpscBacking::File(file, mmap),
265            raw_ptr, capacity: expected_capacity,
266        })
267    }
268
269    /// Build a fresh ring on top of a named RAM-resident
270    /// shared-memory backing. Cross-process visible via the
271    /// `logical_name` of the underlying `ShmFile`; never touches the
272    /// page cache. The `ShmFile` must be sized to at least
273    /// `spsc_ring_file_size(capacity)` bytes.
274    pub fn create_from_shm(
275        mut shm: crate::shm_file::ShmFile,
276        capacity: usize,
277    ) -> Result<Self, RingError> {
278        assert!(capacity.is_power_of_two() && capacity >= 2,
279                "capacity must be pow2 >= 2");
280        let total = spsc_ring_file_size(capacity);
281        if shm.len() < total {
282            return Err(RingError::LayoutMismatch);
283        }
284        // Initialize the layout in the shared region.
285        let slice = shm.as_mut_slice();
286        let raw_ptr = slice.as_mut_ptr();
287        unsafe {
288            init_spsc_layout_raw(raw_ptr, capacity);
289        }
290        Ok(Self {
291            _backing: SpscBacking::Shm(shm),
292            raw_ptr, capacity,
293        })
294    }
295
296    /// Open an existing named ShmFs-backed ring. Validates magic +
297    /// capacity. Does NOT re-initialize the layout - the layout must
298    /// already be present from a prior `create_from_shm` on the same
299    /// logical name.
300    pub fn open_from_shm(
301        mut shm: crate::shm_file::ShmFile,
302        expected_capacity: usize,
303    ) -> Result<Self, RingError> {
304        let total = spsc_ring_file_size(expected_capacity);
305        if shm.len() < total {
306            return Err(RingError::LayoutMismatch);
307        }
308        let slice = shm.as_mut_slice();
309        let raw_ptr = slice.as_mut_ptr();
310        let header = unsafe { &*(raw_ptr as *const SpscHeader) };
311        if header.magic != SPSC_MAGIC
312            || header.capacity != expected_capacity as u64
313            || header.slot_size != SPSC_SLOT_SIZE as u64
314        {
315            return Err(RingError::LayoutMismatch);
316        }
317        Ok(Self {
318            _backing: SpscBacking::Shm(shm),
319            raw_ptr, capacity: expected_capacity,
320        })
321    }
322
323    /// Build a fresh ring laid out in caller-owned memory (huge / large
324    /// pages, or any [`RegionOwner`]). The region must be at least
325    /// `spsc_ring_file_size(capacity)` bytes; the ring owns it for its
326    /// lifetime so the pages stay mapped.
327    pub fn create_in_region<R: RegionOwner>(
328        mut region: R, capacity: usize,
329    ) -> Result<Self, RingError> {
330        assert!(capacity.is_power_of_two() && capacity >= 2,
331                "capacity must be pow2 >= 2");
332        if region.region_len() < spsc_ring_file_size(capacity) {
333            return Err(RingError::LayoutMismatch);
334        }
335        let raw_ptr = region.region_ptr();
336        if !(raw_ptr as usize).is_multiple_of(REGION_ALIGN) {
337            return Err(RingError::LayoutMismatch);
338        }
339        unsafe { init_spsc_layout_raw(raw_ptr, capacity) };
340        Ok(Self {
341            _backing: SpscBacking::Region(Box::new(region)),
342            raw_ptr, capacity,
343        })
344    }
345
346    /// Attach to an existing ring already laid out in `region` - e.g. a
347    /// `LargePageSection` another process created under the same name.
348    /// Validates the header and does NOT re-initialise.
349    pub fn open_in_region<R: RegionOwner>(
350        mut region: R, expected_capacity: usize,
351    ) -> Result<Self, RingError> {
352        if region.region_len() < spsc_ring_file_size(expected_capacity) {
353            return Err(RingError::LayoutMismatch);
354        }
355        let raw_ptr = region.region_ptr();
356        if !(raw_ptr as usize).is_multiple_of(REGION_ALIGN) {
357            return Err(RingError::LayoutMismatch);
358        }
359        let header = unsafe { &*(raw_ptr as *const SpscHeader) };
360        if header.magic != SPSC_MAGIC
361            || header.capacity != expected_capacity as u64
362            || header.slot_size != SPSC_SLOT_SIZE as u64
363        {
364            return Err(RingError::LayoutMismatch);
365        }
366        Ok(Self {
367            _backing: SpscBacking::Region(Box::new(region)),
368            raw_ptr, capacity: expected_capacity,
369        })
370    }
371
372    /// Capacity in slots (always a power of 2).
373    pub fn capacity(&self) -> usize { self.capacity }
374
375    fn header(&self) -> &SpscHeader {
376        unsafe { &*(self.raw_ptr as *const SpscHeader) }
377    }
378
379    fn slot(&self, idx: usize) -> &SpscSlot {
380        let slots_base = unsafe {
381            self.raw_ptr.add(std::mem::size_of::<SpscHeader>())
382        };
383        let masked = idx & (self.capacity - 1);
384        unsafe { &*(slots_base.add(masked * SPSC_SLOT_SIZE) as *const SpscSlot) }
385    }
386
387    /// Producer's published index. Cross-thread visible.
388    pub fn head(&self) -> u64 { self.header().head.load(Ordering::Acquire) }
389
390    /// Consumer's published index. Cross-thread visible.
391    pub fn tail(&self) -> u64 { self.header().tail.load(Ordering::Acquire) }
392
393    /// The producer's publish signal: the head counter the
394    /// consumer-side monitor-wait arms on. The producer's
395    /// Release-store to this atom on every push is the wake.
396    pub fn head_signal(&self) -> &AtomicU64 {
397        &self.header().head
398    }
399
400    /// Number of items waiting (`head - tail`).
401    pub fn approx_len(&self) -> usize {
402        let h = self.head();
403        let t = self.tail();
404        h.saturating_sub(t) as usize
405    }
406
407    /// SPSC push. **Caller is the sole producer** (enforced by the
408    /// `Producer` newtype that owns this ring via `Arc`). Lamport
409    /// pattern: read tail to check fullness, write payload, Release-
410    /// store head to publish.
411    pub fn try_push(&self, payload: &[u8]) -> Result<(), RingError> {
412        if payload.len() > SPSC_PAYLOAD_BYTES {
413            return Err(RingError::PayloadTooLarge);
414        }
415        let header = self.header();
416        let head = header.head.load(Ordering::Relaxed);
417        let tail = header.tail.load(Ordering::Acquire);
418        if head.wrapping_sub(tail) >= self.capacity as u64 {
419            return Err(RingError::Full);
420        }
421        let slot = self.slot(head as usize);
422        // Copy stays on `ptr::copy_nonoverlapping`: at one-line
423        // sizes the baseline inlined movups codegen beats the
424        // dispatched wide-register kernel by ~25% (the dispatch
425        // branch + call cost more than the lanes save; measured by
426        // examples/cacheline_probe.rs).
427        unsafe {
428            let dst = (*slot.payload.get()).as_mut_ptr();
429            std::ptr::copy_nonoverlapping(payload.as_ptr(), dst, payload.len());
430            if payload.len() < SPSC_PAYLOAD_BYTES {
431                std::ptr::write_bytes(
432                    dst.add(payload.len()), 0,
433                    SPSC_PAYLOAD_BYTES - payload.len(),
434                );
435            }
436        }
437        header.head.store(head + 1, Ordering::Release);
438        // The slot line's next reader is the consumer core; demote
439        // it toward the shared LLC (NOP without CLDEMOTE support).
440        crate::cache_ops::cldemote(slot as *const SpscSlot as *const u8);
441        Ok(())
442    }
443
444    /// SPSC pop. **Caller is the sole consumer.** Lamport pattern:
445    /// read head to check emptiness, read payload, Release-store tail
446    /// to free the slot.
447    pub fn try_pop(&self, out: &mut [u8]) -> Result<usize, RingError> {
448        if out.len() < SPSC_PAYLOAD_BYTES {
449            return Err(RingError::PayloadTooLarge);
450        }
451        let header = self.header();
452        let tail = header.tail.load(Ordering::Relaxed);
453        let head = header.head.load(Ordering::Acquire);
454        if tail == head {
455            return Err(RingError::Empty);
456        }
457        let slot = self.slot(tail as usize);
458        unsafe {
459            let src = (*slot.payload.get()).as_ptr();
460            std::ptr::copy_nonoverlapping(src, out.as_mut_ptr(), SPSC_PAYLOAD_BYTES);
461        }
462        header.tail.store(tail + 1, Ordering::Release);
463        // The freed slot's next toucher is the producer core.
464        crate::cache_ops::cldemote(slot as *const SpscSlot as *const u8);
465        Ok(SPSC_PAYLOAD_BYTES)
466    }
467
468    /// Peek the next slot WITHOUT copying or releasing it. Returns
469    /// a [`PeekedSlot`] guard that derefs to `&[u8]` pointing
470    /// directly into the mapped region. Caller passes this slice to
471    /// downstream consumers (e.g. quinn's `SendStream::write_all`)
472    /// without an intermediate copy. When done, call
473    /// [`PeekedSlot::confirm`] to advance the consumer position and
474    /// release the slot. Drop without confirming leaves the slot
475    /// in place; the next `peek_slot` returns it again.
476    ///
477    /// Returns `None` when the ring is empty. **Caller is the sole
478    /// consumer.**
479    pub fn peek_slot(&self) -> Option<PeekedSlot<'_>> {
480        let header = self.header();
481        let tail = header.tail.load(Ordering::Relaxed);
482        let head = header.head.load(Ordering::Acquire);
483        if tail == head {
484            return None;
485        }
486        let slot = self.slot(tail as usize);
487        let payload_ptr = unsafe { (*slot.payload.get()).as_ptr() };
488        let payload_slice = unsafe {
489            std::slice::from_raw_parts(payload_ptr, SPSC_PAYLOAD_BYTES)
490        };
491        Some(PeekedSlot {
492            ring: self,
493            tail,
494            payload: payload_slice,
495        })
496    }
497
498    /// Force any dirty MMF pages to disk. Only meaningful for the
499    /// file-backed mode; no-op on anonymous and ShmFs mappings
500    /// (which never touch disk).
501    pub fn flush(&self) -> Result<(), RingError> {
502        match &self._backing {
503            SpscBacking::File(_, mmap) => {
504                mmap.flush()?;
505            }
506            SpscBacking::Anon(_)
507            | SpscBacking::Shm(_)
508            | SpscBacking::Region(_) => {
509                // No disk to flush to (region-backed rings live in
510                // huge / large pages or other caller-owned RAM).
511            }
512        }
513        Ok(())
514    }
515}
516
517/// Zero-copy view into the next consumer slot of an [`SpscRingCore`].
518///
519/// Derefs to `&[u8]` pointing INTO the mapped region; pass that
520/// slice directly to downstream consumers (network egress, file
521/// writers) without an intermediate stack copy. Call
522/// [`PeekedSlot::confirm`] when done to release the slot;
523/// dropping without confirming leaves the slot in place.
524pub struct PeekedSlot<'a> {
525    ring: &'a SpscRingCore,
526    tail: u64,
527    payload: &'a [u8],
528}
529
530impl<'a> PeekedSlot<'a> {
531    /// The slot's payload bytes. Same as the `Deref` impl; explicit
532    /// method form for clarity at call sites.
533    pub fn as_slice(&self) -> &[u8] { self.payload }
534
535    /// Length of the payload region (always [`SPSC_PAYLOAD_BYTES`]).
536    pub fn len(&self) -> usize { self.payload.len() }
537
538    /// Whether the payload is empty (always false for a valid peek;
539    /// method exists for clippy's `len_without_is_empty`).
540    pub fn is_empty(&self) -> bool { self.payload.is_empty() }
541
542    /// Release the slot, advancing the consumer position.
543    pub fn confirm(self) {
544        let header = self.ring.header();
545        header.tail.store(self.tail + 1, Ordering::Release);
546    }
547}
548
549impl<'a> std::ops::Deref for PeekedSlot<'a> {
550    type Target = [u8];
551    fn deref(&self) -> &[u8] { self.payload }
552}
553
554#[cfg(test)]
555mod tests {
556    use super::*;
557    use std::sync::Arc;
558    use std::thread;
559
560    #[test]
561    fn single_thread_round_trip() {
562        let ring = SpscRingCore::create_anon(8).unwrap();
563        let payload = [0xABu8; SPSC_PAYLOAD_BYTES];
564        ring.try_push(&payload).unwrap();
565        let mut out = [0u8; SPSC_PAYLOAD_BYTES];
566        ring.try_pop(&mut out).unwrap();
567        assert_eq!(out, payload);
568        assert_eq!(ring.try_pop(&mut out).unwrap_err(), RingError::Empty);
569    }
570
571    /// A second create attaches with queued items in place; reset is
572    /// what strips them.
573    #[test]
574    fn second_create_attaches_and_keeps_items() {
575        let p = std::env::temp_dir().join(format!(
576            "subetha-spsc-attach-{}.bin", std::process::id(),
577        ));
578        std::fs::remove_file(&p).ok();
579
580        let ring = SpscRingCore::create(&p, 8).unwrap();
581        let payload = [0x5Eu8; SPSC_PAYLOAD_BYTES];
582        ring.try_push(&payload).unwrap();
583
584        let ring2 = SpscRingCore::create(&p, 8).unwrap();
585        let mut out = [0u8; SPSC_PAYLOAD_BYTES];
586        ring2.try_pop(&mut out).unwrap();
587        assert_eq!(out, payload, "attach lost a queued item");
588        assert!(matches!(
589            SpscRingCore::create(&p, 4),
590            Err(RingError::LayoutMismatch),
591        ));
592
593        // Windows refuses to truncate a mapped file, so every handle
594        // goes before the reset.
595        drop(ring);
596        drop(ring2);
597        let fresh = SpscRingCore::reset(&p, 8).unwrap();
598        assert_eq!(fresh.try_pop(&mut out).unwrap_err(), RingError::Empty,
599                   "reset kept a queued item");
600        drop(fresh);
601        std::fs::remove_file(&p).ok();
602    }
603
604    #[test]
605    fn shm_round_trip() {
606        use crate::shm_file::ShmFile;
607        let nonce = std::time::SystemTime::now()
608            .duration_since(std::time::UNIX_EPOCH)
609            .map(|d| d.as_nanos())
610            .unwrap_or(0);
611        let name = format!("spsc_shm_rt_{}_{}", std::process::id(), nonce);
612        let capacity = 8;
613        let size = spsc_ring_file_size(capacity);
614        let shm = ShmFile::create_or_open_named(&name, size)
615            .expect("shm create");
616        let ring = SpscRingCore::create_from_shm(shm, capacity).unwrap();
617
618        let payload = [0xCDu8; SPSC_PAYLOAD_BYTES];
619        ring.try_push(&payload).unwrap();
620        let mut out = [0u8; SPSC_PAYLOAD_BYTES];
621        ring.try_pop(&mut out).unwrap();
622        assert_eq!(out, payload);
623    }
624
625    #[test]
626    fn shm_cross_handle_visibility() {
627        use crate::shm_file::ShmFile;
628        let nonce = std::time::SystemTime::now()
629            .duration_since(std::time::UNIX_EPOCH)
630            .map(|d| d.as_nanos())
631            .unwrap_or(0);
632        let name = format!("spsc_shm_xshare_{}_{}", std::process::id(), nonce);
633        let capacity = 8;
634        let size = spsc_ring_file_size(capacity);
635
636        // Producer side: create the ring (initialises layout).
637        let shm_a = ShmFile::create_or_open_named(&name, size).expect("shm A");
638        let producer_ring = SpscRingCore::create_from_shm(shm_a, capacity).unwrap();
639
640        // Consumer side: open the SAME named region; layout already
641        // initialised so use open_from_shm.
642        let shm_b = ShmFile::create_or_open_named(&name, size).expect("shm B");
643        let consumer_ring = SpscRingCore::open_from_shm(shm_b, capacity).unwrap();
644
645        // Push via A, pop via B - cross-handle visibility through
646        // the shared RAM region.
647        let payload = [0x42u8; SPSC_PAYLOAD_BYTES];
648        producer_ring.try_push(&payload).unwrap();
649        let mut out = [0u8; SPSC_PAYLOAD_BYTES];
650        consumer_ring.try_pop(&mut out).unwrap();
651        assert_eq!(out, payload);
652    }
653
654    #[test]
655    fn fills_to_capacity_then_full() {
656        let ring = SpscRingCore::create_anon(4).unwrap();
657        for i in 0..4u8 {
658            ring.try_push(&[i; SPSC_PAYLOAD_BYTES]).unwrap();
659        }
660        assert_eq!(
661            ring.try_push(&[99u8; SPSC_PAYLOAD_BYTES]).unwrap_err(),
662            RingError::Full,
663        );
664        let mut out = [0u8; SPSC_PAYLOAD_BYTES];
665        ring.try_pop(&mut out).unwrap();
666        ring.try_push(&[99u8; SPSC_PAYLOAD_BYTES]).unwrap();
667    }
668
669    #[test]
670    fn two_thread_high_volume_round_trip() {
671        let ring = Arc::new(SpscRingCore::create_anon(64).unwrap());
672        let ring_p = ring.clone();
673        let ring_c = ring.clone();
674        const N: u32 = 100_000;
675
676        let producer = thread::spawn(move || {
677            for i in 0..N {
678                let mut buf = [0u8; SPSC_PAYLOAD_BYTES];
679                buf[..4].copy_from_slice(&i.to_le_bytes());
680                while ring_p.try_push(&buf).is_err() {
681                    std::hint::spin_loop();
682                }
683            }
684        });
685
686        let consumer = thread::spawn(move || {
687            let mut out = [0u8; SPSC_PAYLOAD_BYTES];
688            let mut sum: u64 = 0;
689            let mut received: u32 = 0;
690            while received < N {
691                if ring_c.try_pop(&mut out).is_ok() {
692                    sum += u32::from_le_bytes(out[..4].try_into().unwrap()) as u64;
693                    received += 1;
694                } else {
695                    std::hint::spin_loop();
696                }
697            }
698            sum
699        });
700
701        producer.join().unwrap();
702        let sum = consumer.join().unwrap();
703        let expected: u64 = (0..N).map(u64::from).sum();
704        assert_eq!(sum, expected);
705    }
706
707    #[test]
708    fn peek_drop_without_confirm_leaves_item_in_place() {
709        let ring = SpscRingCore::create_anon(8).unwrap();
710        let payload = [0x5Au8; SPSC_PAYLOAD_BYTES];
711        ring.try_push(&payload).unwrap();
712
713        // Peek and drop WITHOUT confirming: the slot must stay.
714        {
715            let peek = ring.peek_slot().unwrap();
716            assert_eq!(peek.as_slice(), &payload[..]);
717        }
718        assert_eq!(ring.approx_len(), 1,
719                   "dropping a peek must not consume the slot");
720
721        // The next peek returns the same item; confirming releases it.
722        let peek = ring.peek_slot().unwrap();
723        assert_eq!(peek.as_slice(), &payload[..]);
724        peek.confirm();
725        assert!(ring.peek_slot().is_none());
726        let mut out = [0u8; SPSC_PAYLOAD_BYTES];
727        assert_eq!(ring.try_pop(&mut out).unwrap_err(), RingError::Empty);
728    }
729
730    /// A 64-byte-aligned heap region exercises the `RegionOwner` wiring
731    /// with no huge-page privilege needed. The element type forces the
732    /// Vec's buffer onto a cache-line boundary, matching what page-
733    /// backed regions give for free. The huge-page (Linux) and large-
734    /// page (Windows) backings travel the exact same `create_in_region`
735    /// path, proven end to end in `examples/large_page_ring.rs`.
736    #[repr(C, align(64))]
737    #[derive(Clone, Copy)]
738    struct Block64([u8; 64]);
739
740    struct HeapRegion {
741        blocks: Vec<Block64>,
742    }
743    impl HeapRegion {
744        fn new(bytes: usize) -> Self {
745            Self { blocks: vec![Block64([0u8; 64]); bytes.div_ceil(64)] }
746        }
747    }
748    impl RegionOwner for HeapRegion {
749        fn region_ptr(&mut self) -> *mut u8 {
750            self.blocks.as_mut_ptr() as *mut u8
751        }
752        fn region_len(&self) -> usize { self.blocks.len() * 64 }
753    }
754
755    #[test]
756    fn create_in_region_round_trips() {
757        let cap = 16usize;
758        let region = HeapRegion::new(spsc_ring_file_size(cap));
759        let ring = SpscRingCore::create_in_region(region, cap).unwrap();
760        assert_eq!(ring.capacity(), cap);
761
762        // Three full laps, so head/tail wrap past capacity and the
763        // region-laid-out slots are reused in place.
764        let mut out = [0u8; SPSC_PAYLOAD_BYTES];
765        for round in 0..3u64 {
766            for i in 0..cap as u64 {
767                let v = round * cap as u64 + i;
768                let mut p = [0u8; SPSC_PAYLOAD_BYTES];
769                p[..8].copy_from_slice(&v.to_le_bytes());
770                ring.try_push(&p).unwrap();
771            }
772            for i in 0..cap as u64 {
773                ring.try_pop(&mut out).unwrap();
774                let got = u64::from_le_bytes(out[..8].try_into().unwrap());
775                assert_eq!(got, round * cap as u64 + i);
776            }
777        }
778    }
779
780    #[test]
781    fn create_in_region_rejects_short_region() {
782        // One cache line short of the layout the ring needs.
783        let cap = 16usize;
784        let short = spsc_ring_file_size(cap) - 64;
785        let region = HeapRegion::new(short);
786        assert!(region.region_len() < spsc_ring_file_size(cap));
787        // SpscRingCore is not Debug, so match rather than unwrap_err.
788        assert!(matches!(
789            SpscRingCore::create_in_region(region, cap),
790            Err(RingError::LayoutMismatch),
791        ));
792    }
793
794    #[test]
795    fn open_in_region_attaches_to_initialised_layout() {
796        // Lay a ring out in a region, push an item, then attach a second
797        // handle to the SAME bytes via open_in_region (no re-init) and
798        // drain through it - the cross-process LargePageSection path in
799        // miniature, with a heap region standing in for the section.
800        let cap = 8usize;
801        let bytes = spsc_ring_file_size(cap);
802        // 64-byte-aligned backing both views map (a named section in
803        // miniature; two processes would each hold their own view).
804        let mut whole: Vec<Block64> = vec![Block64([0u8; 64]); bytes.div_ceil(64)];
805        let base = whole.as_mut_ptr() as *mut u8;
806        unsafe { init_spsc_layout_raw(base, cap) };
807
808        // Two non-owning views over the same bytes (this is what two
809        // processes mapping one named section would each hold).
810        struct ViewRegion { ptr: *mut u8, len: usize }
811        unsafe impl Send for ViewRegion {}
812        unsafe impl Sync for ViewRegion {}
813        impl RegionOwner for ViewRegion {
814            fn region_ptr(&mut self) -> *mut u8 { self.ptr }
815            fn region_len(&self) -> usize { self.len }
816        }
817
818        let producer = SpscRingCore::open_in_region(
819            ViewRegion { ptr: base, len: bytes }, cap,
820        ).unwrap();
821        let consumer = SpscRingCore::open_in_region(
822            ViewRegion { ptr: base, len: bytes }, cap,
823        ).unwrap();
824
825        let payload = [0x42u8; SPSC_PAYLOAD_BYTES];
826        producer.try_push(&payload).unwrap();
827        let mut out = [0u8; SPSC_PAYLOAD_BYTES];
828        consumer.try_pop(&mut out).unwrap();
829        assert_eq!(out, payload);
830        // `whole` is declared before the views, so scope order drops it
831        // LAST - the backing bytes outlive both ring handles.
832    }
833
834    #[test]
835    fn open_round_trips_with_file() {
836        let p = std::env::temp_dir().join(format!(
837            "subetha-test-spsc-{}.bin", std::process::id(),
838        ));
839        std::fs::remove_file(&p).ok();
840        {
841            let _r = SpscRingCore::create(&p, 16).unwrap();
842        }
843        let r2 = SpscRingCore::open(&p, 16).unwrap();
844        assert_eq!(r2.capacity(), 16);
845        std::fs::remove_file(&p).ok();
846    }
847}