Skip to main content

SharedRing

Struct SharedRing 

Source
pub struct SharedRing { /* private fields */ }

Implementations§

Source§

impl SharedRing

Source

pub fn create( path: impl AsRef<Path>, capacity: usize, ) -> Result<Self, RingError>

Create or initialise a new ring backed by path. capacity must be a power of two. The file is truncated to the exact size needed. Use SharedRing::open to attach to an existing ring without re-initialising.

Source

pub fn create_anon(capacity: usize) -> Result<Self, RingError>

Create an anonymous in-memory ring with no backing file. Same byte layout + concurrency protocol as SharedRing::create, but the mapping is private to this process so cross-process visibility is not available.

Use when: one-shot scripts, in-process pipelines, tests that do not need cross-process or disk-persistent semantics. Skips the file create + ftruncate + first-page-fault cost create pays (~600 us on Zen+ R7 2700 / Windows 11), so short-lived sessions amortise much faster.

Do NOT use when: another process needs to attach to the same ring (use SharedRing::create + SharedRing::open for that path), or when durability across restart matters.

Source

pub fn create_from_shm(shm: ShmFile, capacity: usize) -> Result<Self, RingError>

Build a fresh ring on top of a named RAM-resident shared-memory backing. Cross-process visible via the logical_name of the underlying ShmFile; never touches the page cache. The ShmFile must be sized to at least ring_file_size(capacity) bytes.

Source

pub fn open_from_shm( shm: ShmFile, expected_capacity: usize, ) -> Result<Self, RingError>

Open an existing named ShmFs-backed ring. Validates magic + capacity. Does NOT re-initialize.

Source

pub fn create_in_region<R: RegionOwner>( region: R, capacity: usize, ) -> Result<Self, RingError>

Build a fresh Vyukov MPMC ring laid out in caller-owned memory (huge / large pages, or any RegionOwner). The region must hold at least ring_file_size(capacity) bytes; the ring owns it for its lifetime so the pages stay mapped. This is the global- FIFO MPMC primitive on large pages; the sharded grid (SharedRingMpmc::create_grid_in_region) is the per-producer-FIFO counterpart.

Source

pub fn open_in_region<R: RegionOwner>( region: R, expected_capacity: usize, ) -> Result<Self, RingError>

Attach to an existing Vyukov ring already laid out in region (e.g. a named LargePageSection another process created). Validates the header; does NOT re-initialise.

Source

pub fn into_lazy(path: impl Into<PathBuf>, capacity: usize) -> LazySharedRing

Wrap this ring in a LazySharedRing so subsequent attaches at the same path can be deferred until first use. The eagerly- constructed ring stays valid; this helper just hands you the type’s lazy constructor for symmetry.

Source

pub fn open( path: impl AsRef<Path>, expected_capacity: usize, ) -> Result<Self, RingError>

Open an existing ring at path. Validates magic + capacity. Returns RingError::LayoutMismatch when the file’s size does not match a ring of expected_capacity slots, OR when the on-disk header reports different magic / capacity.

Source

pub fn capacity(&self) -> usize

Source

pub fn header(&self) -> &RingHeader

Source

pub fn try_push(&self, payload: &[u8]) -> Result<(), RingError>

Try to push payload into the ring. Returns Err(Full) when the ring is full.

Source

pub fn try_push_spsc(&self, payload: &[u8]) -> Result<(), RingError>

Single-producer fast path: skip the CAS on producer_seq.

Caller contract: the caller guarantees only one thread / one process is calling try_push_spsc on this ring at a time. Concurrent producers will corrupt the counter; use try_push for MPMC.

Saves the compare_exchange_weak on producer_seq that the MPMC path needs to defend against racing producers. Two atomics per push (1 Acquire load on the slot’s sequence + 1 Release store on the slot’s sequence) plus one Release store on producer_seq, vs the MPMC path’s 1 load + 1 CAS + 1 load + 1 store. Net: ~25% less atomic traffic per push.

Also skips the per-op Observation push to the sidecar ring. Use try_push when you want sidecar observability on the hot path.

Source

pub fn try_pop_spsc(&self, out: &mut [u8]) -> Result<usize, RingError>

Single-consumer fast path: skip the CAS on consumer_seq.

Caller contract: the caller guarantees only one thread / one process is calling try_pop_spsc on this ring at a time. Concurrent consumers will corrupt the counter; use try_pop for MPMC.

Same mirror-image savings as try_push_spsc: two atomics + one Release store per pop, no CAS, no sidecar observation push.

Source

pub fn next_pop_signal(&self) -> &AtomicU64

The publish signal for the consumer’s NEXT pop: the sequence atom of the slot at the current consumer position. A producer publishing that slot Release-stores this exact atom, so a monitor-wait armed on it wakes on the publish. Recompute after every successful pop - the position (and therefore the slot) advances.

Source

pub fn try_pop(&self, out: &mut [u8]) -> Result<usize, RingError>

Try to pop one payload into out. On success, returns the number of bytes written. On Err(Empty), the ring is empty.

Source

pub fn flush(&self) -> Result<(), RingError>

Force the underlying file’s dirty pages to disk. Only meaningful for file-backed rings; no-op for anonymous and ShmFs-backed rings (which never touch disk).

Source

pub fn flush_async(&self) -> Result<(), RingError>

Non-blocking flush; lets the OS schedule the writeback. Only meaningful for file-backed rings; no-op otherwise.

Source

pub fn producer_seq(&self) -> u64

Current producer sequence number (monotonic; wraps via modulo-capacity on slot index).

Source

pub fn consumer_seq(&self) -> u64

Current consumer sequence number.

Source

pub fn approx_len(&self) -> usize

Approximate items waiting to be drained.

Source

pub fn next_stuck_slot(&self, from: u64) -> Option<u64>

Find the first slot in the claimed-but-undrained window [consumer_seq, producer_seq) whose sequence number is stuck at pos instead of having advanced to pos + 1 (published). Returns Some(pos) for the first stuck position, None if every claimed slot has been published.

Use for: sidecar-driven recovery from a producer that crashed between claiming a slot (CAS on producer_seq) and publishing it (Release-store on slot.sequence). The window where a crash leaves a permanent hole is narrow but real for any Vyukov MPMC; this is the scan that finds those holes.

Hot-path cost: zero. This method is only called by the sidecar when its Empty-observation analysis decides a ring is stuck. try_push and try_pop never touch it.

Scan cost: O(producer_seq - consumer_seq) in the worst case (typically small; if the window is large the ring is already saturated and the scan dominates nothing).

Source

pub fn heal_stuck_slot(&self, pos: u64) -> Result<bool, RingError>

Heal a slot stuck in the claimed-but-never-published state by advancing its sequence number from pos to pos + 1. The next consumer at this position drains the slot in normal try_pop order; its payload bytes are whatever the dying producer happened to write before crashing (or initial zeros if the producer crashed before any payload write).

Caller contract: the caller must independently confirm that the producer which claimed this slot will never publish it (process dead, lease expired, application-level timeout elapsed). SharedRing does not record per-slot producer identity, so this method cannot make that determination on its own. Calling without dead-producer confirmation will data-race a live producer that is about to publish; the race is benign for the CAS itself (the producer’s Release publishes the same value pos + 1 we are trying to publish, so the CAS just returns Ok(false)) but the consumer drains a slot the producer never finished writing.

Where the dead-producer signal comes from: the canonical signal is HeartbeatTable + FailoverWatchdog. Register each producer with a heartbeat; the watchdog declares a process dead when its heartbeat goes stale beyond the grace period, then walks the rings that producer touched and calls heal_stuck_slot(pos) for each stuck position next_stuck_slot returns.

Returns: Ok(true) if the slot was stuck and is now healed (CAS succeeded; consumer can drain it). Ok(false) if the slot was not stuck (sequence already at pos + 1 or beyond, or pos outside the [consumer_seq, producer_seq) window). Returns Err only on PayloadTooLarge style protocol misuse.

Hot-path cost: zero. Only invoked from sidecar recovery. The heal itself is one atomic CAS on the slot’s sequence number; no payload write, no other state touched.

Trait Implementations§

Source§

impl AdaptiveInstance for SharedRing

Source§

fn header(&self) -> &HandshakeHeader

Source§

fn ring(&self) -> &ObservationRing

Source§

fn make_policy(&self) -> Box<dyn Policy>

Source§

fn apply_migration(&self, new_tag: u32)

Called by the sidecar when the policy returns a new strategy tag. Default implementation: just set the tag on the header. Primitives that need heavier migration (data-layout swap) override this to perform the swap before (or after) updating the tag.
Source§

impl MessageTransport for SharedRing

Source§

fn try_push(&self, payload: &[u8]) -> Result<(), TransportError>

Push a payload of length <= PAYLOAD_BYTES. Returns Err(Full) if the transport is at capacity.
Source§

fn try_pop(&self, out: &mut [u8]) -> Result<usize, TransportError>

Pop a payload into out (which must be >= PAYLOAD_BYTES long). Returns the byte count written on success, or Err(Empty) if there is nothing to take.
Source§

impl Send for SharedRing

Source§

impl Sync for SharedRing

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.