use std::cell::UnsafeCell;
use std::fs::{File, OpenOptions};
use std::path::Path;
use std::sync::atomic::{AtomicU64, Ordering};
use memmap2::{MmapMut, MmapOptions};
pub const RING_MAGIC: u64 = 0x4150_4D46_0000_0001;
pub const SLOT_SIZE: usize = 64;
pub const PAYLOAD_BYTES: usize = SLOT_SIZE - std::mem::size_of::<u64>();
#[repr(C, align(64))]
pub struct RingHeader {
pub magic: u64,
pub capacity: u64,
pub slot_size: u64,
pub epoch: AtomicU64,
_pad_meta: [u8; 64 - 32],
pub producer_seq: AtomicU64,
_pad_prod: [u8; 64 - 8],
pub consumer_seq: AtomicU64,
_pad_cons: [u8; 64 - 8],
}
#[repr(C, align(64))]
pub struct Slot {
pub sequence: AtomicU64,
pub payload: UnsafeCell<[u8; PAYLOAD_BYTES]>,
}
unsafe impl Sync for Slot {}
pub const fn ring_file_size(capacity: usize) -> usize {
std::mem::size_of::<RingHeader>() + capacity * SLOT_SIZE
}
pub struct SharedRingSpsc;
pub struct Producer {
inner: std::sync::Arc<crate::spsc_ring::SpscRingCore>,
_not_sync: std::marker::PhantomData<std::cell::Cell<()>>,
}
pub struct Consumer {
inner: std::sync::Arc<crate::spsc_ring::SpscRingCore>,
_not_sync: std::marker::PhantomData<std::cell::Cell<()>>,
}
impl SharedRingSpsc {
pub fn create_anon_pair(capacity: usize) -> Result<(Producer, Consumer), RingError> {
let ring = std::sync::Arc::new(
crate::spsc_ring::SpscRingCore::create_anon(capacity)?,
);
Ok((
Producer { inner: ring.clone(), _not_sync: std::marker::PhantomData },
Consumer { inner: ring, _not_sync: std::marker::PhantomData },
))
}
pub fn create_pair(
path: impl AsRef<Path>,
capacity: usize,
) -> Result<(Producer, Consumer), RingError> {
let ring = std::sync::Arc::new(
crate::spsc_ring::SpscRingCore::create(path, capacity)?,
);
Ok((
Producer { inner: ring.clone(), _not_sync: std::marker::PhantomData },
Consumer { inner: ring, _not_sync: std::marker::PhantomData },
))
}
pub fn open_pair(
path: impl AsRef<Path>,
expected_capacity: usize,
) -> Result<(Producer, Consumer), RingError> {
let ring = std::sync::Arc::new(
crate::spsc_ring::SpscRingCore::open(path, expected_capacity)?,
);
Ok((
Producer { inner: ring.clone(), _not_sync: std::marker::PhantomData },
Consumer { inner: ring, _not_sync: std::marker::PhantomData },
))
}
}
impl Producer {
pub fn try_push(&self, payload: &[u8]) -> Result<(), RingError> {
self.inner.try_push(payload)
}
pub fn capacity(&self) -> usize { self.inner.capacity() }
pub fn head(&self) -> u64 { self.inner.head() }
}
impl Consumer {
pub fn try_pop(&self, out: &mut [u8]) -> Result<usize, RingError> {
self.inner.try_pop(out)
}
pub fn capacity(&self) -> usize { self.inner.capacity() }
pub fn tail(&self) -> u64 { self.inner.tail() }
}
pub struct LazySharedRing {
path: std::path::PathBuf,
capacity: usize,
inner: std::sync::OnceLock<SharedRing>,
}
impl LazySharedRing {
pub fn new(path: impl Into<std::path::PathBuf>, capacity: usize) -> Self {
assert!(capacity.is_power_of_two() && capacity >= 2,
"capacity must be pow2 >= 2");
Self {
path: path.into(),
capacity,
inner: std::sync::OnceLock::new(),
}
}
pub fn get(&self) -> Result<&SharedRing, RingError> {
if let Some(ring) = self.inner.get() {
return Ok(ring);
}
let ring = SharedRing::create(&self.path, self.capacity)?;
match self.inner.set(ring) {
Ok(()) => Ok(self.inner.get().expect("OnceLock just populated")),
Err(_lost) => Ok(self.inner.get().expect("another thread populated")),
}
}
pub fn is_initialised(&self) -> bool {
self.inner.get().is_some()
}
pub fn try_push(&self, payload: &[u8]) -> Result<(), RingError> {
self.get()?.try_push(payload)
}
pub fn try_pop(&self, out: &mut [u8]) -> Result<usize, RingError> {
self.get()?.try_pop(out)
}
}
fn init_ring_layout(mmap: &mut MmapMut, capacity: usize) {
unsafe { init_ring_layout_raw(mmap.as_mut_ptr(), capacity) };
}
unsafe fn init_ring_layout_raw(ptr: *mut u8, capacity: usize) {
let header_ptr = ptr as *mut RingHeader;
unsafe {
std::ptr::write(header_ptr, RingHeader {
magic: RING_MAGIC,
capacity: capacity as u64,
slot_size: SLOT_SIZE as u64,
epoch: AtomicU64::new(0),
_pad_meta: [0; 64 - 32],
producer_seq: AtomicU64::new(0),
_pad_prod: [0; 64 - 8],
consumer_seq: AtomicU64::new(0),
_pad_cons: [0; 64 - 8],
});
}
let slots_base = unsafe { ptr.add(std::mem::size_of::<RingHeader>()) };
for i in 0..capacity {
let slot_ptr = unsafe { slots_base.add(i * SLOT_SIZE) as *mut Slot };
unsafe {
std::ptr::write(slot_ptr, Slot {
sequence: AtomicU64::new(i as u64),
payload: UnsafeCell::new([0; PAYLOAD_BYTES]),
});
}
}
}
#[allow(dead_code)]
enum SharedRingBacking {
Anon(MmapMut),
File(File, MmapMut),
Shm(crate::shm_file::ShmFile),
Region(Box<dyn crate::spsc_ring::RegionOwner>),
}
pub struct SharedRing {
_backing: SharedRingBacking,
raw_ptr: *mut u8,
capacity: usize,
header_sidecar: subetha_core::HandshakeHeader,
ring_sidecar: Box<subetha_core::ObservationRing>,
}
unsafe impl Send for SharedRing {}
unsafe impl Sync for SharedRing {}
impl subetha_sidecar::AdaptiveInstance for SharedRing {
fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
Box::new(subetha_sidecar::NoMigrationPolicy)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RingError {
Full,
Empty,
LayoutMismatch,
PayloadTooLarge,
NotStamped,
NotDrainer,
StaleBacklog,
IoError(std::io::ErrorKind),
}
impl From<std::io::Error> for RingError {
fn from(e: std::io::Error) -> Self { Self::IoError(e.kind()) }
}
impl SharedRing {
pub fn create(path: impl AsRef<Path>, capacity: usize) -> Result<Self, RingError> {
assert!(capacity.is_power_of_two() && capacity >= 2,
"capacity must be pow2 >= 2");
let total = ring_file_size(capacity);
let file = OpenOptions::new()
.read(true).write(true).create(true).truncate(true)
.open(path.as_ref())?;
file.set_len(total as u64)?;
let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
init_ring_layout(&mut mmap, capacity);
let raw_ptr = mmap.as_mut_ptr();
Ok(Self {
_backing: SharedRingBacking::File(file, mmap),
raw_ptr, capacity,
header_sidecar: subetha_core::HandshakeHeader::new(),
ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
})
}
pub fn create_anon(capacity: usize) -> Result<Self, RingError> {
assert!(capacity.is_power_of_two() && capacity >= 2,
"capacity must be pow2 >= 2");
let total = ring_file_size(capacity);
let mut mmap = MmapOptions::new().len(total).map_anon()?;
init_ring_layout(&mut mmap, capacity);
let raw_ptr = mmap.as_mut_ptr();
Ok(Self {
_backing: SharedRingBacking::Anon(mmap),
raw_ptr, capacity,
header_sidecar: subetha_core::HandshakeHeader::new(),
ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
})
}
pub fn create_from_shm(
mut shm: crate::shm_file::ShmFile,
capacity: usize,
) -> Result<Self, RingError> {
assert!(capacity.is_power_of_two() && capacity >= 2,
"capacity must be pow2 >= 2");
let total = ring_file_size(capacity);
if shm.len() < total {
return Err(RingError::LayoutMismatch);
}
let slice = shm.as_mut_slice();
let raw_ptr = slice.as_mut_ptr();
unsafe { init_ring_layout_raw(raw_ptr, capacity) };
Ok(Self {
_backing: SharedRingBacking::Shm(shm),
raw_ptr, capacity,
header_sidecar: subetha_core::HandshakeHeader::new(),
ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
})
}
pub fn open_from_shm(
mut shm: crate::shm_file::ShmFile,
expected_capacity: usize,
) -> Result<Self, RingError> {
let total = ring_file_size(expected_capacity);
if shm.len() < total {
return Err(RingError::LayoutMismatch);
}
let slice = shm.as_mut_slice();
let raw_ptr = slice.as_mut_ptr();
let header = unsafe { &*(raw_ptr as *const RingHeader) };
if header.magic != RING_MAGIC
|| header.capacity != expected_capacity as u64
|| header.slot_size != SLOT_SIZE as u64
{
return Err(RingError::LayoutMismatch);
}
Ok(Self {
_backing: SharedRingBacking::Shm(shm),
raw_ptr, capacity: expected_capacity,
header_sidecar: subetha_core::HandshakeHeader::new(),
ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
})
}
pub fn create_in_region<R: crate::spsc_ring::RegionOwner>(
mut region: R, capacity: usize,
) -> Result<Self, RingError> {
assert!(capacity.is_power_of_two() && capacity >= 2,
"capacity must be pow2 >= 2");
if region.region_len() < ring_file_size(capacity) {
return Err(RingError::LayoutMismatch);
}
let raw_ptr = region.region_ptr();
if !(raw_ptr as usize).is_multiple_of(std::mem::align_of::<RingHeader>()) {
return Err(RingError::LayoutMismatch);
}
unsafe { init_ring_layout_raw(raw_ptr, capacity) };
Ok(Self {
_backing: SharedRingBacking::Region(Box::new(region)),
raw_ptr, capacity,
header_sidecar: subetha_core::HandshakeHeader::new(),
ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
})
}
pub fn open_in_region<R: crate::spsc_ring::RegionOwner>(
mut region: R, expected_capacity: usize,
) -> Result<Self, RingError> {
if region.region_len() < ring_file_size(expected_capacity) {
return Err(RingError::LayoutMismatch);
}
let raw_ptr = region.region_ptr();
if !(raw_ptr as usize).is_multiple_of(std::mem::align_of::<RingHeader>()) {
return Err(RingError::LayoutMismatch);
}
let header = unsafe { &*(raw_ptr as *const RingHeader) };
if header.magic != RING_MAGIC
|| header.capacity != expected_capacity as u64
|| header.slot_size != SLOT_SIZE as u64
{
return Err(RingError::LayoutMismatch);
}
Ok(Self {
_backing: SharedRingBacking::Region(Box::new(region)),
raw_ptr, capacity: expected_capacity,
header_sidecar: subetha_core::HandshakeHeader::new(),
ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
})
}
pub fn into_lazy(path: impl Into<std::path::PathBuf>, capacity: usize) -> LazySharedRing {
LazySharedRing::new(path, capacity)
}
pub fn open(path: impl AsRef<Path>, expected_capacity: usize) -> Result<Self, RingError> {
let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
let total = ring_file_size(expected_capacity);
let actual_len = file.metadata()?.len();
if (actual_len as usize) < total {
return Err(RingError::LayoutMismatch);
}
let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
crate::mmf_warm::warm_mmap(&mut mmap);
let header = unsafe { &*(mmap.as_ptr() as *const RingHeader) };
if header.magic != RING_MAGIC
|| header.capacity != expected_capacity as u64
|| header.slot_size != SLOT_SIZE as u64
{
return Err(RingError::LayoutMismatch);
}
let raw_ptr = mmap.as_mut_ptr();
Ok(Self {
_backing: SharedRingBacking::File(file, mmap),
raw_ptr, capacity: expected_capacity,
header_sidecar: subetha_core::HandshakeHeader::new(),
ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
})
}
#[inline]
pub fn capacity(&self) -> usize { self.capacity }
#[inline]
pub fn header(&self) -> &RingHeader {
unsafe { &*(self.raw_ptr as *const RingHeader) }
}
#[inline]
fn slot(&self, idx: usize) -> &Slot {
let slots_base = unsafe {
self.raw_ptr.add(std::mem::size_of::<RingHeader>())
};
unsafe { &*(slots_base.add((idx & (self.capacity - 1)) * SLOT_SIZE) as *const Slot) }
}
pub fn try_push(&self, payload: &[u8]) -> Result<(), RingError> {
if payload.len() > PAYLOAD_BYTES {
return Err(RingError::PayloadTooLarge);
}
let header = self.header();
loop {
let pos = header.producer_seq.load(Ordering::Relaxed);
let slot = self.slot(pos as usize);
let seq = slot.sequence.load(Ordering::Acquire);
let diff = seq as i64 - pos as i64;
if diff == 0 {
crate::cache_ops::prefetchw(
&header.producer_seq as *const _ as *const u8,
);
if header.producer_seq.compare_exchange_weak(
pos, pos + 1, Ordering::Relaxed, Ordering::Relaxed,
).is_ok() {
unsafe {
let dst = (*slot.payload.get()).as_mut_ptr();
std::ptr::copy_nonoverlapping(payload.as_ptr(), dst, payload.len());
if payload.len() < PAYLOAD_BYTES {
std::ptr::write_bytes(
dst.add(payload.len()), 0,
PAYLOAD_BYTES - payload.len(),
);
}
}
slot.sequence.store(pos + 1, Ordering::Release);
crate::cache_ops::cldemote(slot as *const Slot as *const u8);
self.ring_sidecar
.push_op(crate::sidecar_ops::ring::OP_PUSH, 0);
return Ok(());
}
} else if diff < 0 {
self.ring_sidecar
.push_op(crate::sidecar_ops::ring::OP_PUSH, 1); return Err(RingError::Full);
} else {
std::hint::spin_loop();
}
}
}
pub fn try_push_spsc(&self, payload: &[u8]) -> Result<(), RingError> {
if payload.len() > PAYLOAD_BYTES {
return Err(RingError::PayloadTooLarge);
}
let header = self.header();
let pos = header.producer_seq.load(Ordering::Relaxed);
let slot = self.slot(pos as usize);
let seq = slot.sequence.load(Ordering::Acquire);
if seq != pos {
return Err(RingError::Full);
}
unsafe {
let dst = (*slot.payload.get()).as_mut_ptr();
std::ptr::copy_nonoverlapping(payload.as_ptr(), dst, payload.len());
if payload.len() < PAYLOAD_BYTES {
std::ptr::write_bytes(
dst.add(payload.len()), 0,
PAYLOAD_BYTES - payload.len(),
);
}
}
header.producer_seq.store(pos + 1, Ordering::Relaxed);
slot.sequence.store(pos + 1, Ordering::Release);
crate::cache_ops::cldemote(slot as *const Slot as *const u8);
Ok(())
}
pub fn try_pop_spsc(&self, out: &mut [u8]) -> Result<usize, RingError> {
if out.len() < PAYLOAD_BYTES {
return Err(RingError::PayloadTooLarge);
}
let header = self.header();
let pos = header.consumer_seq.load(Ordering::Relaxed);
let slot = self.slot(pos as usize);
let seq = slot.sequence.load(Ordering::Acquire);
if seq != pos + 1 {
return Err(RingError::Empty);
}
unsafe {
let src = (*slot.payload.get()).as_ptr();
std::ptr::copy_nonoverlapping(src, out.as_mut_ptr(), PAYLOAD_BYTES);
}
header.consumer_seq.store(pos + 1, Ordering::Relaxed);
slot.sequence.store(pos + self.capacity as u64, Ordering::Release);
crate::cache_ops::cldemote(slot as *const Slot as *const u8);
Ok(PAYLOAD_BYTES)
}
pub fn next_pop_signal(&self) -> &AtomicU64 {
let pos = self.header().consumer_seq.load(Ordering::Relaxed);
&self.slot(pos as usize).sequence
}
pub fn try_pop(&self, out: &mut [u8]) -> Result<usize, RingError> {
if out.len() < PAYLOAD_BYTES {
return Err(RingError::PayloadTooLarge);
}
let header = self.header();
loop {
let pos = header.consumer_seq.load(Ordering::Relaxed);
let slot = self.slot(pos as usize);
let seq = slot.sequence.load(Ordering::Acquire);
let diff = seq as i64 - (pos + 1) as i64;
if diff == 0 {
crate::cache_ops::prefetchw(
&header.consumer_seq as *const _ as *const u8,
);
if header.consumer_seq.compare_exchange_weak(
pos, pos + 1, Ordering::Relaxed, Ordering::Relaxed,
).is_ok() {
unsafe {
let src = (*slot.payload.get()).as_ptr();
std::ptr::copy_nonoverlapping(src, out.as_mut_ptr(), PAYLOAD_BYTES);
}
slot.sequence.store(pos + self.capacity as u64, Ordering::Release);
crate::cache_ops::cldemote(slot as *const Slot as *const u8);
self.ring_sidecar
.push_op(crate::sidecar_ops::ring::OP_POP, 0);
return Ok(PAYLOAD_BYTES);
}
} else if diff < 0 {
self.ring_sidecar
.push_op(crate::sidecar_ops::ring::OP_POP, 2); return Err(RingError::Empty);
} else {
std::hint::spin_loop();
}
}
}
pub fn flush(&self) -> Result<(), RingError> {
match &self._backing {
SharedRingBacking::File(_, mmap) => {
mmap.flush()?;
}
SharedRingBacking::Anon(_)
| SharedRingBacking::Shm(_)
| SharedRingBacking::Region(_) => {}
}
Ok(())
}
pub fn flush_async(&self) -> Result<(), RingError> {
match &self._backing {
SharedRingBacking::File(_, mmap) => {
mmap.flush_async()?;
}
SharedRingBacking::Anon(_)
| SharedRingBacking::Shm(_)
| SharedRingBacking::Region(_) => {}
}
Ok(())
}
pub fn producer_seq(&self) -> u64 {
self.header().producer_seq.load(Ordering::Acquire)
}
pub fn consumer_seq(&self) -> u64 {
self.header().consumer_seq.load(Ordering::Acquire)
}
pub fn approx_len(&self) -> usize {
let p = self.producer_seq();
let c = self.consumer_seq();
p.saturating_sub(c) as usize
}
pub fn next_stuck_slot(&self, from: u64) -> Option<u64> {
let producer_seq = self.header().producer_seq.load(Ordering::Acquire);
let consumer_seq = self.header().consumer_seq.load(Ordering::Acquire);
let start = from.max(consumer_seq);
for pos in start..producer_seq {
let slot = self.slot(pos as usize);
let seq = slot.sequence.load(Ordering::Acquire);
if seq == pos {
return Some(pos);
}
}
None
}
pub fn heal_stuck_slot(&self, pos: u64) -> Result<bool, RingError> {
let header = self.header();
let producer_seq = header.producer_seq.load(Ordering::Acquire);
let consumer_seq = header.consumer_seq.load(Ordering::Acquire);
if pos < consumer_seq || pos >= producer_seq {
return Ok(false);
}
let slot = self.slot(pos as usize);
match slot.sequence.compare_exchange(
pos,
pos + 1,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => {
self.ring_sidecar
.push_op(crate::sidecar_ops::ring::OP_PUSH, 4); Ok(true)
}
Err(_) => Ok(false),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
fn tmp_path(name: &str) -> std::path::PathBuf {
let mut p = std::env::temp_dir();
let pid = std::process::id();
p.push(format!("subetha-test-{name}-{pid}.bin"));
p
}
#[test]
fn create_open_round_trip() {
let p = tmp_path("create-open");
{
let _r = SharedRing::create(&p, 16).unwrap();
}
let r2 = SharedRing::open(&p, 16).unwrap();
assert_eq!(r2.capacity(), 16);
std::fs::remove_file(&p).ok();
}
fn create_stuck_slot(ring: &SharedRing, pos: u64) {
let header = ring.header();
header.producer_seq.store(pos + 1, Ordering::Release);
assert_eq!(
ring.slot(pos as usize).sequence.load(Ordering::Acquire),
pos,
"test setup: slot.sequence must still be at initial value",
);
}
#[test]
fn stuck_slot_blocks_consumer_then_heal_unblocks() {
let ring = SharedRing::create_anon(8).unwrap();
create_stuck_slot(&ring, 0);
let mut out = [0u8; PAYLOAD_BYTES];
assert_eq!(ring.try_pop(&mut out).unwrap_err(), RingError::Empty);
assert_eq!(ring.try_pop(&mut out).unwrap_err(), RingError::Empty);
assert_eq!(
ring.approx_len(),
1,
"producer_seq advanced past 0 but consumer sees 0",
);
let stuck = ring.next_stuck_slot(0).expect("scan must find pos=0");
assert_eq!(stuck, 0);
assert!(ring.heal_stuck_slot(stuck).unwrap());
let n = ring.try_pop(&mut out).expect("healed slot must drain");
assert_eq!(n, PAYLOAD_BYTES);
assert_eq!(ring.consumer_seq(), 1, "consumer advanced past the heal");
assert!(ring.next_stuck_slot(0).is_none());
for i in 1..5u8 {
ring.try_push(&[i; 8]).unwrap();
ring.try_pop(&mut out).unwrap();
}
}
#[test]
fn heal_non_stuck_slot_returns_false() {
let ring = SharedRing::create_anon(4).unwrap();
ring.try_push(&[1u8; 8]).unwrap();
assert!(!ring.heal_stuck_slot(0).unwrap(),
"heal of an already-published slot must be a no-op");
assert!(!ring.heal_stuck_slot(5).unwrap(),
"heal of out-of-window position must be a no-op");
}
#[test]
fn heal_loses_race_with_concurrent_producer_publish() {
let ring = SharedRing::create_anon(4).unwrap();
ring.try_push(&[0xCDu8; 8]).unwrap();
assert!(!ring.heal_stuck_slot(0).unwrap());
let mut out = [0u8; PAYLOAD_BYTES];
ring.try_pop(&mut out).unwrap();
assert_eq!(&out[..8], &[0xCDu8; 8]);
}
#[test]
fn next_stuck_slot_scans_only_claimed_window() {
let ring = SharedRing::create_anon(8).unwrap();
assert!(ring.next_stuck_slot(0).is_none());
ring.try_push(&[1u8; 8]).unwrap();
ring.try_push(&[2u8; 8]).unwrap();
assert!(ring.next_stuck_slot(0).is_none());
create_stuck_slot(&ring, 2);
assert_eq!(ring.next_stuck_slot(0), Some(2));
}
#[test]
fn spsc_fast_path_round_trip() {
let ring = std::sync::Arc::new(SharedRing::create_anon(16).unwrap());
let ring_p = ring.clone();
let ring_c = ring.clone();
const N: u32 = 1_000;
let producer = thread::spawn(move || {
for i in 0..N {
let mut buf = [0u8; PAYLOAD_BYTES];
buf[..4].copy_from_slice(&i.to_le_bytes());
while ring_p.try_push_spsc(&buf).is_err() {
std::hint::spin_loop();
}
}
});
let consumer = thread::spawn(move || {
let mut out = [0u8; PAYLOAD_BYTES];
let mut sum: u64 = 0;
let mut received = 0u32;
while received < N {
if ring_c.try_pop_spsc(&mut out).is_ok() {
sum += u32::from_le_bytes(out[..4].try_into().unwrap()) as u64;
received += 1;
} else {
std::hint::spin_loop();
}
}
sum
});
producer.join().unwrap();
let sum = consumer.join().unwrap();
let expected: u64 = (0..N).map(|i| i as u64).sum();
assert_eq!(sum, expected, "SPSC fast-path lost or duplicated items");
}
#[test]
fn spsc_fast_path_reports_full_on_lap() {
let ring = SharedRing::create_anon(4).unwrap();
for i in 0..4u8 {
ring.try_push_spsc(&[i; 8]).unwrap();
}
assert_eq!(
ring.try_push_spsc(&[99u8; 8]).unwrap_err(),
RingError::Full,
);
let mut out = [0u8; PAYLOAD_BYTES];
ring.try_pop_spsc(&mut out).unwrap();
ring.try_push_spsc(&[99u8; 8]).unwrap();
}
#[test]
fn anon_ring_pushes_and_pops() {
let ring = SharedRing::create_anon(8).unwrap();
assert_eq!(ring.capacity(), 8);
let payload = [0xAB; PAYLOAD_BYTES];
ring.try_push(&payload).unwrap();
let mut out = [0u8; PAYLOAD_BYTES];
let n = ring.try_pop(&mut out).unwrap();
assert_eq!(n, PAYLOAD_BYTES);
assert_eq!(out, payload);
assert_eq!(ring.try_pop(&mut out).unwrap_err(), RingError::Empty);
}
#[test]
fn anon_ring_fills_to_capacity() {
let ring = SharedRing::create_anon(4).unwrap();
for i in 0..4u32 {
let mut p = [0u8; PAYLOAD_BYTES];
p[..4].copy_from_slice(&i.to_le_bytes());
ring.try_push(&p).unwrap();
}
assert_eq!(ring.try_push(&[0u8; PAYLOAD_BYTES]).unwrap_err(), RingError::Full);
}
#[test]
fn lazy_ring_defers_setup_until_first_use() {
let p = tmp_path("lazy-defer");
let lazy = LazySharedRing::new(&p, 8);
assert!(!lazy.is_initialised());
lazy.try_push(&[1u8; 8]).unwrap();
assert!(lazy.is_initialised());
let mut out = [0u8; PAYLOAD_BYTES];
let n = lazy.try_pop(&mut out).unwrap();
assert_eq!(n, PAYLOAD_BYTES);
assert_eq!(&out[..1], &[1u8]);
std::fs::remove_file(&p).ok();
}
#[test]
fn lazy_ring_never_materialises_when_unused() {
let p = tmp_path("lazy-never-used");
let lazy = LazySharedRing::new(&p, 8);
drop(lazy);
assert!(!p.exists(), "lazy ring touched the filesystem despite no use");
}
#[test]
fn lazy_ring_get_caches_reference() {
let p = tmp_path("lazy-cache");
let lazy = LazySharedRing::new(&p, 8);
let r1 = lazy.get().unwrap() as *const SharedRing;
let r2 = lazy.get().unwrap() as *const SharedRing;
assert_eq!(r1, r2, "OnceLock returned different instances across calls");
std::fs::remove_file(&p).ok();
}
#[test]
fn open_rejects_wrong_capacity() {
let p = tmp_path("wrong-cap");
let _r = SharedRing::create(&p, 16).unwrap();
match SharedRing::open(&p, 32) {
Err(RingError::LayoutMismatch) => {}
other => panic!("expected LayoutMismatch, got {:?}",
other.as_ref().err()),
}
std::fs::remove_file(&p).ok();
}
#[test]
fn single_thread_push_pop_round_trip() {
let p = tmp_path("spsc-rt");
let r = SharedRing::create(&p, 8).unwrap();
for i in 0..8u8 {
let payload = [i, i, i, i];
r.try_push(&payload).unwrap();
}
assert_eq!(r.try_push(&[42; 4]).unwrap_err(), RingError::Full);
let mut buf = [0u8; PAYLOAD_BYTES];
for i in 0..8u8 {
let n = r.try_pop(&mut buf).unwrap();
assert_eq!(n, PAYLOAD_BYTES);
assert_eq!(&buf[..4], &[i, i, i, i]);
}
assert_eq!(r.try_pop(&mut buf).unwrap_err(), RingError::Empty);
std::fs::remove_file(&p).ok();
}
#[test]
fn mpmc_concurrent_push_pop_preserves_count() {
let p = tmp_path("mpmc");
let r = std::sync::Arc::new(SharedRing::create(&p, 1024).unwrap());
let producers = 4;
let consumers = 4;
let per_producer = 5_000usize;
let total = producers * per_producer;
let mut handles = vec![];
for pid in 0..producers {
let r = r.clone();
handles.push(thread::spawn(move || {
for i in 0..per_producer {
let v = ((pid as u32) << 24) | (i as u32);
let bytes = v.to_le_bytes();
while r.try_push(&bytes).is_err() {
std::hint::spin_loop();
}
}
}));
}
let consumed = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
for _ in 0..consumers {
let r = r.clone();
let consumed = consumed.clone();
handles.push(thread::spawn(move || {
let mut buf = [0u8; PAYLOAD_BYTES];
loop {
if consumed.load(std::sync::atomic::Ordering::Acquire) >= total {
return;
}
if r.try_pop(&mut buf).is_ok() {
consumed.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
}
}
}));
}
for h in handles { h.join().unwrap(); }
assert_eq!(consumed.load(std::sync::atomic::Ordering::Acquire), total);
std::fs::remove_file(&p).ok();
}
#[test]
fn disk_persistence_data_survives_reopen() {
let p = tmp_path("disk-persist");
{
let r = SharedRing::create(&p, 4).unwrap();
r.try_push(&[1, 2, 3, 4]).unwrap();
r.try_push(&[5, 6, 7, 8]).unwrap();
r.flush().unwrap();
}
let r2 = SharedRing::open(&p, 4).unwrap();
let mut buf = [0u8; PAYLOAD_BYTES];
let _val = r2.try_pop(&mut buf).unwrap();
assert_eq!(&buf[..4], &[1, 2, 3, 4]);
let _val = r2.try_pop(&mut buf).unwrap();
assert_eq!(&buf[..4], &[5, 6, 7, 8]);
std::fs::remove_file(&p).ok();
}
#[test]
fn cross_handle_in_process_sees_writes() {
let p = tmp_path("cross-handle");
let producer = SharedRing::create(&p, 16).unwrap();
let consumer = SharedRing::open(&p, 16).unwrap();
producer.try_push(b"abc").unwrap();
let mut buf = [0u8; PAYLOAD_BYTES];
let _val = consumer.try_pop(&mut buf).unwrap();
assert_eq!(&buf[..3], b"abc");
std::fs::remove_file(&p).ok();
}
#[test]
fn approx_len_tracks_outstanding() {
let p = tmp_path("approx-len");
let r = SharedRing::create(&p, 16).unwrap();
assert_eq!(r.approx_len(), 0);
r.try_push(&[1]).unwrap();
r.try_push(&[2]).unwrap();
r.try_push(&[3]).unwrap();
assert_eq!(r.approx_len(), 3);
let mut buf = [0u8; PAYLOAD_BYTES];
let _val = r.try_pop(&mut buf).unwrap();
assert_eq!(r.approx_len(), 2);
std::fs::remove_file(&p).ok();
}
#[test]
fn payload_too_large_rejected() {
let p = tmp_path("payload-too-large");
let r = SharedRing::create(&p, 4).unwrap();
let oversized = vec![0u8; PAYLOAD_BYTES + 1];
assert_eq!(r.try_push(&oversized).unwrap_err(), RingError::PayloadTooLarge);
std::fs::remove_file(&p).ok();
}
#[repr(C, align(64))]
#[derive(Clone, Copy)]
struct Block64([u8; 64]);
#[test]
fn create_in_region_round_trips() {
let cap = 16usize;
let bytes = ring_file_size(cap);
let mut blocks = vec![Block64([0u8; 64]); bytes.div_ceil(64)];
struct R { ptr: *mut u8, len: usize }
unsafe impl Send for R {}
unsafe impl Sync for R {}
impl crate::spsc_ring::RegionOwner for R {
fn region_ptr(&mut self) -> *mut u8 { self.ptr }
fn region_len(&self) -> usize { self.len }
}
let ring = SharedRing::create_in_region(
R { ptr: blocks.as_mut_ptr() as *mut u8, len: bytes }, cap,
).unwrap();
assert_eq!(ring.capacity(), cap);
let mut out = [0u8; PAYLOAD_BYTES];
for round in 0..2u64 {
for i in 0..cap as u64 {
let v = round * cap as u64 + i;
let mut buf = [0u8; PAYLOAD_BYTES];
buf[..8].copy_from_slice(&v.to_le_bytes());
ring.try_push(&buf).unwrap();
}
for i in 0..cap as u64 {
ring.try_pop(&mut out).unwrap();
assert_eq!(
u64::from_le_bytes(out[..8].try_into().unwrap()),
round * cap as u64 + i,
);
}
}
}
#[test]
fn open_in_region_attaches_to_initialised_layout() {
let cap = 8usize;
let bytes = ring_file_size(cap);
let mut blocks = vec![Block64([0u8; 64]); bytes.div_ceil(64)];
let base = blocks.as_mut_ptr() as *mut u8;
unsafe { init_ring_layout_raw(base, cap) };
struct View { ptr: *mut u8, len: usize }
unsafe impl Send for View {}
unsafe impl Sync for View {}
impl crate::spsc_ring::RegionOwner for View {
fn region_ptr(&mut self) -> *mut u8 { self.ptr }
fn region_len(&self) -> usize { self.len }
}
let producer = SharedRing::open_in_region(
View { ptr: base, len: bytes }, cap,
).unwrap();
let consumer = SharedRing::open_in_region(
View { ptr: base, len: bytes }, cap,
).unwrap();
let mut buf = [0u8; PAYLOAD_BYTES];
buf[..4].copy_from_slice(&0xABCD_u32.to_le_bytes());
producer.try_push(&buf).unwrap();
let mut out = [0u8; PAYLOAD_BYTES];
consumer.try_pop(&mut out).unwrap();
assert_eq!(out[..4], buf[..4]);
}
}