Skip to main content

subetha_cxc/
adaptive_ring.rs

1//! `AdaptiveRing` - shape-morphing ring with a pinned-handle layer.
2//!
3//! Single typed ring primitive that morphs its protocol shape at
4//! runtime based on observed peer counts, plus a pinned-handle
5//! layer that drops to near-native primitive speed once the
6//! shape stabilises.
7//!
8//! # Two execution paths
9//!
10//! - [`AdaptiveRing::try_send`] / [`AdaptiveRing::try_recv`] do
11//!   the full atomic dispatch: one Acquire load on the shape tag,
12//!   one branch to the matching backend, then the backend's native
13//!   op. Cost ~3-5 ns above the underlying primitive. Used when
14//!   the shape is uncertain or the caller does not want to manage
15//!   a pin lifetime.
16//! - [`AdaptiveRing::pin_current_shape`] returns a
17//!   [`PinnedRing<'_>`] handle that exposes the current backend
18//!   directly. Hot-loop cost matches the underlying primitive
19//!   ([`SpscRingCore`], [`SharedRingMpsc`](crate::SharedRingMpsc),
20//!   [`SharedRingMpmc`](crate::SharedRingMpmc), or [`SharedRing`])
21//!   plus one Acquire load when the caller calls
22//!   [`PinnedRing::is_still_valid`].
23//!
24//! # Morph trigger
25//!
26//! AUTOMATIC by default: every [`AdaptiveRing::register_producer`] /
27//! [`AdaptiveRing::register_consumer`] / unregister re-morphs the
28//! shape to the live peer counts (read from the shared peer
29//! directory, so registrations in OTHER processes propagate through
30//! the topology epoch the hot paths poll), and registration past
31//! the construction sizing GROWS the per-producer backings on
32//! demand. An explicit [`AdaptiveRing::morph_to`] (or
33//! [`AdaptiveRing::pin_shape`]) is the user override that pins the
34//! shape; a declared [`AdaptiveRing::with_contract`] ceiling is the
35//! only thing that makes registration fallible. Every morph bumps a
36//! generation counter that invalidates outstanding pins; pin
37//! holders see [`PinnedRing::is_still_valid`] return `false` and
38//! re-acquire through the adaptive layer.
39
40use std::cell::Cell;
41use std::marker::PhantomData;
42use std::path::Path;
43use std::sync::{Arc, OnceLock};
44use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU32, AtomicU64, AtomicUsize, Ordering};
45
46use arc_swap::ArcSwap;
47
48use crate::frame_ring::{FrameClass, LayoutHint};
49use crate::frame_region::FrameRegion;
50use crate::peer_directory::{
51    PeerDirectory, CONSUMER_SLOT_CEILING, OWNER_NONE,
52};
53
54use crate::ordering::{
55    default_stamp_kind, ordering_region_size, stamp_now, OrderingMode,
56    OrderingRegion, StampKind, STAMPED_PAYLOAD_BYTES, STAMP_BYTES,
57};
58use crate::qos_policy::Ordering as QosOrdering;
59use crate::shared_ring::{RingError, SharedRing, PAYLOAD_BYTES};
60use crate::spsc_ring::{SpscRingCore, SPSC_PAYLOAD_BYTES};
61
62/// Grace window (in drainer epochs) before a silent merge drainer
63/// becomes preemptible. The sidecar ticks one epoch per scan, so
64/// the default tolerates three missed scans.
65pub const DRAINER_GRACE_EPOCHS: u64 = 3;
66
67/// The four ring shapes this primitive can host. Stored in the
68/// shape tag as the discriminant `u8`.
69#[repr(u8)]
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum RingShape {
72    /// 1 producer + 1 consumer, Lamport SPSC core. Cheapest shape.
73    Spsc = 0,
74    /// N producers + 1 consumer, composed N Lamport SPSC rings.
75    Mpsc = 1,
76    /// N producers + M consumers, composed N x M Lamport grid.
77    Mpmc = 2,
78    /// Vyukov MPMC override; preserves global FIFO across producers.
79    Vyukov = 3,
80}
81
82impl RingShape {
83    fn from_u8(tag: u8) -> Self {
84        match tag {
85            0 => Self::Spsc,
86            1 => Self::Mpsc,
87            2 => Self::Mpmc,
88            3 => Self::Vyukov,
89            _ => panic!("AdaptiveRing shape_tag corrupted: {tag}"),
90        }
91    }
92}
93
94/// Shape-morphing ring with all four backing protocols pre-
95/// allocated so morphs do not allocate on the hot path.
96///
97/// **Caller contract on construction**: `max_producers` and
98/// `max_consumers` are SIZING HINTS - the per-producer backings
99/// pre-allocated up front. Registration past them GROWS the ring
100/// on demand (new backings, published through the shared peer
101/// directory). Registration refuses in two cases, both reported as
102/// `TooMany*`: a ceiling the caller pinned with
103/// [`with_contract`](AdaptiveRing::with_contract), and the peer
104/// directory's own slot ceiling (`PRODUCER_SLOT_CEILING` 4096 /
105/// `CONSUMER_SLOT_CEILING` 256 CONCURRENT peers, slots recycled on
106/// unregister). Growth happens on the registration slow path;
107/// steady-state ops pay one relaxed epoch load.
108/// Sentinel for "no stale shape pending" in `stale_shape_tag`.
109const STALE_NONE: u8 = u8::MAX;
110
111pub struct AdaptiveRing {
112    /// Current shape; one Acquire load per dispatched op.
113    shape_tag: AtomicU8,
114
115    /// The previous shape whose backing may still hold a backlog
116    /// after a morph. Producers never touch it again (they follow
117    /// `shape_tag`); the consumer's pop path drains it FIRST (the
118    /// stale walk) so a morph never moves data and never needs
119    /// target capacity. Stays set until the NEXT morph (which
120    /// requires it drained), giving producer pushes that straddled
121    /// the tag flip a wide grace window to land somewhere the
122    /// consumer still looks. `STALE_NONE` = nothing pending.
123    stale_shape_tag: AtomicU8,
124
125    /// Bumped on every successful morph. Pinned handles capture
126    /// this value at pin time and compare on `is_still_valid`.
127    pin_generation: AtomicU64,
128
129    /// Shared payload region for the self-describing frame path
130    /// ([`send_frame`](Self::send_frame) / [`recv_frame`](Self::recv_frame)).
131    /// Records too large to inline in a ring slot spill here as
132    /// concurrently-allocated blocks; the descriptor in the slot then
133    /// carries the block index. Lazily created on the first oversized
134    /// frame so rings that never send large payloads pay nothing.
135    /// One region serves every shape (SPSC / MPSC / MPMC / Vyukov)
136    /// because its allocator is multi-producer / multi-consumer safe.
137    frame_region: OnceLock<Arc<FrameRegion>>,
138
139    /// SPSC backing: one Lamport SPSC ring.
140    spsc: Arc<SpscRingCore>,
141
142    /// MPSC backing: factory + producer/consumer handles for the
143    /// N-producer single-consumer composed shape.
144    mpsc: Arc<MpscBacking>,
145
146    /// MPMC backing: factory + producer/consumer handles for the
147    /// N x M composed grid.
148    mpmc: Arc<MpmcBacking>,
149
150    /// Vyukov MPMC backing (global-FIFO override).
151    vyukov: Arc<SharedRing>,
152
153    /// Sizing HINTS captured at construction: how many per-producer
154    /// backings are pre-allocated up front. NOT ceilings - the ring
155    /// grows past them on demand. A ceiling exists only when the
156    /// caller declares one via [`with_contract`](Self::with_contract).
157    max_producers: usize,
158    max_consumers: usize,
159
160    /// Per-sub-ring slot capacity, kept for on-demand growth
161    /// (grown backings are created at the same capacity).
162    capacity: usize,
163
164    /// The shared peer directory: cross-process slot claims, ring
165    /// publication, MPMC ring ownership, and the topology epoch the
166    /// hot paths poll.
167    directory: Arc<PeerDirectory>,
168
169    /// Last directory epoch this process synced its arrays + shape
170    /// to. `u64::MAX` = never synced (first op syncs).
171    synced_epoch: AtomicU64,
172
173    /// Serialises in-process growth (file creation + array swap).
174    grow_lock: parking_lot::Mutex<()>,
175
176    /// Whether the composed shape auto-morphs to the active peer counts
177    /// on every register / unregister (the default). Cleared by
178    /// [`pin_shape`](Self::pin_shape) or an explicit
179    /// [`morph_to`](Self::morph_to) when the caller commits to a fixed
180    /// shape - the only cases where the automatic reshape is suppressed.
181    shape_auto: AtomicBool,
182
183    /// Declared ring contract - the user override. `None` (the
184    /// default) means UNBOUNDED: registration never fails, peers grow
185    /// the ring on demand. Set via
186    /// [`with_contract`](Self::with_contract); its ceilings are the
187    /// only source of `TooMany*` errors. Read at attach time
188    /// ([`register_producer`](Self::register_producer)) and by policies
189    /// as a feasible-region filter; never on the hot path.
190    contract: Option<crate::ring_contract::RingContract>,
191
192    /// Ordering substrate, present only on rings constructed via
193    /// [`with_ordering_stamps`](Self::with_ordering_stamps). Fixed
194    /// at construction: a runtime stamping toggle would change slot
195    /// interpretation under in-flight unstamped items. The MERGE
196    /// flag inside the region stays runtime-dynamic because stamps
197    /// are always present once this is `Some`.
198    ordering: Option<Arc<OrderingState>>,
199
200    /// Where the ring backings live; lets `with_ordering_stamps`
201    /// place (or open) the ordering region at the matching locale.
202    backing_id: BackingId,
203
204    /// Sidecar handshake + observation ring (inversion events ride
205    /// these to the sidecar's drain).
206    header_sidecar: subetha_core::HandshakeHeader,
207    ring_sidecar: Box<subetha_core::ObservationRing>,
208}
209
210unsafe impl Send for AdaptiveRing {}
211unsafe impl Sync for AdaptiveRing {}
212
213impl subetha_sidecar::AdaptiveInstance for AdaptiveRing {
214    fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
215    fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
216    fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
217        Box::new(subetha_sidecar::NoMigrationPolicy)
218    }
219}
220
221/// Locale identity captured at construction so the ordering region
222/// can be created (or opened) next to the ring backings.
223enum BackingId {
224    Anon,
225    File { prefix: std::path::PathBuf, created: bool },
226    Shm { prefix: String },
227}
228
229/// Ordering state attached to a stamped ring: the shared region
230/// plus process-local per-consumer inversion bookkeeping.
231struct OrderingState {
232    region: OrderingRegion,
233    /// Per-consumer last-popped stamp + the mode it was popped
234    /// under. Cache-line padded so partitioned MPMC consumers do
235    /// not false-share.
236    seen: Vec<SeenLine>,
237}
238
239#[repr(align(64))]
240struct SeenLine {
241    stamp: AtomicU64,
242    mode_tag: AtomicU32,
243    /// Last drainer-lease generation this consumer verified its
244    /// lease at. Per-pop verification is one load of the region's
245    /// quiet generation line compared against this consumer-local
246    /// value; only a change runs the full lease handshake on the
247    /// stamp-hot header line. `u64::MAX` = never verified.
248    lease_gen: AtomicU64,
249}
250
251impl SeenLine {
252    fn new() -> Self {
253        Self {
254            stamp: AtomicU64::new(0),
255            mode_tag: AtomicU32::new(OrderingMode::Unordered as u32),
256            lease_gen: AtomicU64::new(u64::MAX),
257        }
258    }
259}
260
261/// Drainer-lease token for this process + consumer slot.
262#[inline]
263fn drainer_token(consumer_id: usize) -> u64 {
264    ((std::process::id() as u64) << 32) | (consumer_id as u64 & 0xFFFF_FFFF)
265}
266
267struct MpscBacking {
268    /// Per-producer rings behind an `ArcSwap` so producer growth
269    /// appends without stopping traffic: one guarded load per op
270    /// while unpinned, and pinned handles capture the `Arc` at pin
271    /// time (growth bumps the pin generation).
272    rings: ArcSwap<Vec<Arc<SpscRingCore>>>,
273    next_drain: AtomicUsize,
274}
275
276struct MpmcBacking {
277    rings: ArcSwap<Vec<Arc<SpscRingCore>>>,
278    /// Per-consumer round-robin cursors. Index by consumer_id.
279    /// Each entry is cache-line aligned to keep one consumer's
280    /// writes from invalidating another consumer's L1 line. Sized
281    /// to [`CONSUMER_SLOT_CEILING`] so consumer slots grow / shrink
282    /// with no reallocation.
283    consumer_cursors: Vec<PaddedCursor>,
284}
285
286/// Cache-line-aligned `AtomicUsize` wrapper. Used for MPMC consumer
287/// round-robin cursors so per-consumer writes do not pollute the
288/// L1 cache lines of sibling consumers. The second field rate-limits
289/// that consumer's crash-takeover pid probes.
290#[repr(align(64))]
291struct PaddedCursor(AtomicUsize, AtomicUsize);
292
293fn consumer_cursor_table() -> Vec<PaddedCursor> {
294    (0..CONSUMER_SLOT_CEILING)
295        .map(|_| PaddedCursor(AtomicUsize::new(0), AtomicUsize::new(0)))
296        .collect()
297}
298
299/// Allocate one huge / large page region sized for a ring backing of
300/// `bytes`. Linux uses anonymous 2 MB hugepages (`MAP_HUGETLB`);
301/// Windows uses a `MEM_LARGE_PAGES` region. Both implement
302/// [`RegionOwner`](crate::spsc_ring::RegionOwner), so the ring's
303/// `create_in_region` accepts either. Returns `Err` when hugepages are
304/// unavailable (no reservation / privilege) so the caller can fall back
305/// to a standard backing. Only this allocation is platform-gated; the
306/// `create_hugepage` layout that consumes it is shared.
307#[cfg(target_os = "linux")]
308fn hugepage_region(bytes: usize) -> std::io::Result<crate::hugepages::HugepageRegion> {
309    use crate::hugepages::{HugepageRegion, HugepageSize, HUGEPAGE_2MB};
310    let pages = bytes.div_ceil(HUGEPAGE_2MB).max(1);
311    HugepageRegion::allocate(pages, HugepageSize::Mb2)
312}
313
314#[cfg(windows)]
315fn hugepage_region(bytes: usize) -> std::io::Result<crate::large_pages::LargePageRegion> {
316    use crate::large_pages::{enable_lock_memory_privilege, LargePageRegion};
317    // Enabling the privilege is a precondition; `allocate` rounds
318    // `bytes` up to a whole number of large pages internally.
319    enable_lock_memory_privilege()?;
320    LargePageRegion::allocate(bytes)
321}
322
323#[cfg(any(target_os = "freebsd", target_os = "macos"))]
324fn hugepage_region(bytes: usize) -> std::io::Result<crate::super_pages::SuperPageRegion> {
325    // Superpage-backed: FreeBSD `MAP_ALIGNED_SUPER` (a transparent hint
326    // with no pre-reserved pool), macOS x86_64 `VM_FLAGS_SUPERPAGE_SIZE_2MB`
327    // (the Darwin anonymous-superpage overload). `allocate` rounds `bytes`
328    // up to a whole number of 2 MB superpages and returns Err only when the
329    // aligned mapping cannot be made (or on Apple Silicon, which has no
330    // userspace superpage API), so the caller falls back to `create_anon`.
331    crate::super_pages::SuperPageRegion::allocate(bytes)
332}
333
334impl AdaptiveRing {
335    /// Construct an adaptive ring with all backings pre-allocated.
336    ///
337    /// `max_producers` and `max_consumers` size the composed
338    /// MPSC + MPMC backings; runtime peer registration past these
339    /// maxima is rejected. Initial shape is [`RingShape::Spsc`].
340    pub fn create_anon(
341        max_producers: usize,
342        max_consumers: usize,
343        capacity: usize,
344    ) -> Result<Self, RingError> {
345        assert!(max_producers >= 1, "max_producers must be >= 1");
346        assert!(max_consumers >= 1, "max_consumers must be >= 1");
347
348        let spsc = Arc::new(SpscRingCore::create_anon(capacity)?);
349
350        let mpsc_rings: Vec<Arc<SpscRingCore>> = (0..max_producers)
351            .map(|_| SpscRingCore::create_anon(capacity).map(Arc::new))
352            .collect::<Result<Vec<_>, _>>()?;
353        let mpsc = Arc::new(MpscBacking {
354            rings: ArcSwap::from_pointee(mpsc_rings),
355            next_drain: AtomicUsize::new(0),
356        });
357
358        let mpmc_rings: Vec<Arc<SpscRingCore>> = (0..max_producers)
359            .map(|_| SpscRingCore::create_anon(capacity).map(Arc::new))
360            .collect::<Result<Vec<_>, _>>()?;
361        let mpmc = Arc::new(MpmcBacking {
362            rings: ArcSwap::from_pointee(mpmc_rings),
363            consumer_cursors: consumer_cursor_table(),
364        });
365
366        let vyukov = Arc::new(SharedRing::create_anon(capacity)?);
367
368        let directory = Arc::new(PeerDirectory::create_anon()?);
369        directory.publish_rings(max_producers);
370
371        Ok(Self {
372            shape_tag: AtomicU8::new(RingShape::Spsc as u8),
373            stale_shape_tag: AtomicU8::new(STALE_NONE),
374            pin_generation: AtomicU64::new(0),
375            frame_region: OnceLock::new(),
376            spsc,
377            mpsc,
378            mpmc,
379            vyukov,
380            max_producers,
381            max_consumers,
382            capacity,
383            directory,
384            synced_epoch: AtomicU64::new(u64::MAX),
385            grow_lock: parking_lot::Mutex::new(()),
386            contract: None,
387            shape_auto: AtomicBool::new(true),
388            ordering: None,
389            backing_id: BackingId::Anon,
390            header_sidecar: subetha_core::HandshakeHeader::new(),
391            ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
392        })
393    }
394
395    /// Hugepage / large-page-backed adaptive ring (opt-in). Every
396    /// backing (SPSC, each MPSC + MPMC producer ring, Vyukov) is laid
397    /// out in its own huge / large page region instead of standard 4 KB
398    /// pages, cutting TLB pressure for large rings: a 16 MB ring fits in
399    /// a handful of 2 MB hugepages instead of thousands of 4 KB pages.
400    ///
401    /// Cross-platform: Linux `MAP_HUGETLB`, Windows `MEM_LARGE_PAGES`,
402    /// FreeBSD `MAP_ALIGNED_SUPER`, macOS x86_64 `VM_FLAGS_SUPERPAGE_SIZE_2MB`;
403    /// only the per-backing region allocation is platform-gated, the
404    /// compose-and-wire logic is shared with `create_anon`.
405    ///
406    /// Requires a hugepage reservation (Linux `vm.nr_hugepages`) or the
407    /// `SeLockMemoryPrivilege` (Windows); FreeBSD and macOS need no
408    /// reservation (superpages are a transparent / on-demand hint, macOS
409    /// x86_64 only). Returns `Err` when the backing cannot be allocated so
410    /// the caller can fall back to `create_anon`.
411    #[cfg(any(target_os = "linux", windows, target_os = "freebsd", target_os = "macos"))]
412    pub fn create_hugepage(
413        max_producers: usize,
414        max_consumers: usize,
415        capacity: usize,
416    ) -> Result<Self, RingError> {
417        assert!(max_producers >= 1, "max_producers must be >= 1");
418        assert!(max_consumers >= 1, "max_consumers must be >= 1");
419
420        let spsc_bytes = crate::spsc_ring::spsc_ring_file_size(capacity);
421        let vyukov_bytes = crate::shared_ring::ring_file_size(capacity);
422
423        let spsc = Arc::new(SpscRingCore::create_in_region(
424            hugepage_region(spsc_bytes)?, capacity)?);
425
426        let mut mpsc_rings = Vec::with_capacity(max_producers);
427        for _ in 0..max_producers {
428            mpsc_rings.push(Arc::new(SpscRingCore::create_in_region(
429                hugepage_region(spsc_bytes)?, capacity)?));
430        }
431        let mpsc = Arc::new(MpscBacking {
432            rings: ArcSwap::from_pointee(mpsc_rings),
433            next_drain: AtomicUsize::new(0),
434        });
435
436        let mut mpmc_rings = Vec::with_capacity(max_producers);
437        for _ in 0..max_producers {
438            mpmc_rings.push(Arc::new(SpscRingCore::create_in_region(
439                hugepage_region(spsc_bytes)?, capacity)?));
440        }
441        let mpmc = Arc::new(MpmcBacking {
442            rings: ArcSwap::from_pointee(mpmc_rings),
443            consumer_cursors: consumer_cursor_table(),
444        });
445
446        let vyukov = Arc::new(SharedRing::create_in_region(
447            hugepage_region(vyukov_bytes)?, capacity)?);
448
449        let directory = Arc::new(PeerDirectory::create_anon()?);
450        directory.publish_rings(max_producers);
451
452        Ok(Self {
453            shape_tag: AtomicU8::new(RingShape::Spsc as u8),
454            stale_shape_tag: AtomicU8::new(STALE_NONE),
455            pin_generation: AtomicU64::new(0),
456            frame_region: OnceLock::new(),
457            spsc,
458            mpsc,
459            mpmc,
460            vyukov,
461            max_producers,
462            max_consumers,
463            capacity,
464            directory,
465            synced_epoch: AtomicU64::new(u64::MAX),
466            grow_lock: parking_lot::Mutex::new(()),
467            contract: None,
468            shape_auto: AtomicBool::new(true),
469            ordering: None,
470            // The hugepage backing is anonymous (no path / name); reuse
471            // the Anon id so ordering-region creation stays uniform.
472            // Backings grown past the pre-allocated hint use standard
473            // anonymous pages (hugepage regions are pre-reserved).
474            backing_id: BackingId::Anon,
475            header_sidecar: subetha_core::HandshakeHeader::new(),
476            ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
477        })
478    }
479
480    /// File-backed adaptive ring. One file per backing (SPSC,
481    /// each MPSC producer ring, each MPMC producer ring, Vyukov),
482    /// named `<path_prefix>.{role}.bin` /
483    /// `<path_prefix>.mpsc.{i}.bin` / `<path_prefix>.mpmc.{i}.bin`.
484    pub fn create(
485        path_prefix: impl AsRef<Path>,
486        max_producers: usize,
487        max_consumers: usize,
488        capacity: usize,
489    ) -> Result<Self, RingError> {
490        assert!(max_producers >= 1 && max_consumers >= 1);
491        let base = path_prefix.as_ref();
492
493        let spsc_path = with_suffix(base, ".spsc.bin");
494        let spsc = Arc::new(SpscRingCore::create(&spsc_path, capacity)?);
495
496        let mut mpsc_rings = Vec::with_capacity(max_producers);
497        for i in 0..max_producers {
498            let p = with_suffix(base, &format!(".mpsc.{i}.bin"));
499            mpsc_rings.push(Arc::new(SpscRingCore::create(&p, capacity)?));
500        }
501        let mpsc = Arc::new(MpscBacking {
502            rings: ArcSwap::from_pointee(mpsc_rings),
503            next_drain: AtomicUsize::new(0),
504        });
505
506        let mut mpmc_rings = Vec::with_capacity(max_producers);
507        for i in 0..max_producers {
508            let p = with_suffix(base, &format!(".mpmc.{i}.bin"));
509            mpmc_rings.push(Arc::new(SpscRingCore::create(&p, capacity)?));
510        }
511        let mpmc = Arc::new(MpmcBacking {
512            rings: ArcSwap::from_pointee(mpmc_rings),
513            consumer_cursors: consumer_cursor_table(),
514        });
515
516        let vyukov_path = with_suffix(base, ".vyukov.bin");
517        let vyukov = Arc::new(SharedRing::create(&vyukov_path, capacity)?);
518
519        let directory = Arc::new(
520            PeerDirectory::create(with_suffix(base, ".peers.bin"))?,
521        );
522        directory.publish_rings(max_producers);
523
524        Ok(Self {
525            shape_tag: AtomicU8::new(RingShape::Spsc as u8),
526            stale_shape_tag: AtomicU8::new(STALE_NONE),
527            pin_generation: AtomicU64::new(0),
528            frame_region: OnceLock::new(),
529            spsc,
530            mpsc,
531            mpmc,
532            vyukov,
533            max_producers,
534            max_consumers,
535            capacity,
536            directory,
537            synced_epoch: AtomicU64::new(u64::MAX),
538            grow_lock: parking_lot::Mutex::new(()),
539            contract: None,
540            shape_auto: AtomicBool::new(true),
541            ordering: None,
542            backing_id: BackingId::File {
543                prefix: base.to_path_buf(),
544                created: true,
545            },
546            header_sidecar: subetha_core::HandshakeHeader::new(),
547            ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
548        })
549    }
550
551    /// Open an existing file-backed adaptive ring created by
552    /// another process via [`AdaptiveRing::create`] with the same
553    /// `path_prefix` + sizing. Validates each backing's magic +
554    /// capacity; does NOT re-initialize any layout, so in-flight
555    /// items in the creator's backings survive the attach.
556    ///
557    /// The shape tag + pin generation are process-local: each
558    /// process morphs / pins its own view. Cross-process callers
559    /// coordinate the active shape out-of-band (or follow the
560    /// creator's sidecar) and call [`AdaptiveRing::morph_to`] to
561    /// the agreed shape before pinning.
562    pub fn open(
563        path_prefix: impl AsRef<Path>,
564        max_producers: usize,
565        max_consumers: usize,
566        expected_capacity: usize,
567    ) -> Result<Self, RingError> {
568        assert!(max_producers >= 1 && max_consumers >= 1);
569        let base = path_prefix.as_ref();
570
571        // The peer directory is the source of truth for how many
572        // per-producer backings exist RIGHT NOW - the creator's
573        // hint may have grown since. The caller's count args stay
574        // as pre-open floor hints only.
575        let directory = Arc::new(
576            PeerDirectory::open(with_suffix(base, ".peers.bin"))?,
577        );
578        let n_rings = directory.published().max(1);
579
580        let spsc_path = with_suffix(base, ".spsc.bin");
581        let spsc = Arc::new(SpscRingCore::open(&spsc_path, expected_capacity)?);
582
583        let mut mpsc_rings = Vec::with_capacity(n_rings);
584        for i in 0..n_rings {
585            let p = with_suffix(base, &format!(".mpsc.{i}.bin"));
586            mpsc_rings.push(Arc::new(SpscRingCore::open(&p, expected_capacity)?));
587        }
588        let mpsc = Arc::new(MpscBacking {
589            rings: ArcSwap::from_pointee(mpsc_rings),
590            next_drain: AtomicUsize::new(0),
591        });
592
593        let mut mpmc_rings = Vec::with_capacity(n_rings);
594        for i in 0..n_rings {
595            let p = with_suffix(base, &format!(".mpmc.{i}.bin"));
596            mpmc_rings.push(Arc::new(SpscRingCore::open(&p, expected_capacity)?));
597        }
598        let mpmc = Arc::new(MpmcBacking {
599            rings: ArcSwap::from_pointee(mpmc_rings),
600            consumer_cursors: consumer_cursor_table(),
601        });
602
603        let vyukov_path = with_suffix(base, ".vyukov.bin");
604        let vyukov = Arc::new(SharedRing::open(&vyukov_path, expected_capacity)?);
605
606        Ok(Self {
607            shape_tag: AtomicU8::new(RingShape::Spsc as u8),
608            stale_shape_tag: AtomicU8::new(STALE_NONE),
609            pin_generation: AtomicU64::new(0),
610            frame_region: OnceLock::new(),
611            spsc,
612            mpsc,
613            mpmc,
614            vyukov,
615            max_producers,
616            max_consumers,
617            capacity: expected_capacity,
618            directory,
619            synced_epoch: AtomicU64::new(u64::MAX),
620            grow_lock: parking_lot::Mutex::new(()),
621            contract: None,
622            shape_auto: AtomicBool::new(true),
623            ordering: None,
624            backing_id: BackingId::File {
625                prefix: base.to_path_buf(),
626                created: false,
627            },
628            header_sidecar: subetha_core::HandshakeHeader::new(),
629            ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
630        })
631    }
632
633    /// Construct an AdaptiveRing whose four backings live in named
634    /// RAM-resident shared memory regions (the ShmFs locale).
635    /// Cross-process visible; never touches the page cache.
636    ///
637    /// `name_prefix` becomes part of each backing's logical shm
638    /// name: `{prefix}_spsc`, `{prefix}_mpsc_{i}`,
639    /// `{prefix}_mpmc_{i}`, `{prefix}_vyukov`. The same prefix on
640    /// another process resolves to the same shared memory.
641    pub fn create_shmfs(
642        name_prefix: &str,
643        max_producers: usize,
644        max_consumers: usize,
645        capacity: usize,
646    ) -> Result<Self, RingError> {
647        assert!(max_producers >= 1, "max_producers must be >= 1");
648        assert!(max_consumers >= 1, "max_consumers must be >= 1");
649
650        let spsc_size = crate::spsc_ring::spsc_ring_file_size(capacity);
651        let vyukov_size = crate::shared_ring::ring_file_size(capacity);
652
653        // SPSC backing.
654        let spsc_shm = crate::shm_file::ShmFile::create_or_open_named(
655            &format!("{name_prefix}_spsc"), spsc_size,
656        ).map_err(|_| RingError::PayloadTooLarge)?;
657        let spsc = Arc::new(SpscRingCore::create_from_shm(spsc_shm, capacity)?);
658
659        // MPSC backings.
660        let mut mpsc_rings = Vec::with_capacity(max_producers);
661        for i in 0..max_producers {
662            let shm = crate::shm_file::ShmFile::create_or_open_named(
663                &format!("{name_prefix}_mpsc_{i}"), spsc_size,
664            ).map_err(|_| RingError::PayloadTooLarge)?;
665            mpsc_rings.push(Arc::new(SpscRingCore::create_from_shm(shm, capacity)?));
666        }
667        let mpsc = Arc::new(MpscBacking {
668            rings: ArcSwap::from_pointee(mpsc_rings),
669            next_drain: AtomicUsize::new(0),
670        });
671
672        // MPMC backings (one ring per producer; consumers partition).
673        let mut mpmc_rings = Vec::with_capacity(max_producers);
674        for i in 0..max_producers {
675            let shm = crate::shm_file::ShmFile::create_or_open_named(
676                &format!("{name_prefix}_mpmc_{i}"), spsc_size,
677            ).map_err(|_| RingError::PayloadTooLarge)?;
678            mpmc_rings.push(Arc::new(SpscRingCore::create_from_shm(shm, capacity)?));
679        }
680        let mpmc = Arc::new(MpmcBacking {
681            rings: ArcSwap::from_pointee(mpmc_rings),
682            consumer_cursors: consumer_cursor_table(),
683        });
684
685        // Vyukov backing.
686        let vyukov_shm = crate::shm_file::ShmFile::create_or_open_named(
687            &format!("{name_prefix}_vyukov"), vyukov_size,
688        ).map_err(|_| RingError::PayloadTooLarge)?;
689        let vyukov = Arc::new(SharedRing::create_from_shm(vyukov_shm, capacity)?);
690
691        let directory = Arc::new(PeerDirectory::create_or_open_shm(
692            &format!("{name_prefix}_peers"),
693        )?);
694        directory.publish_rings(max_producers);
695
696        Ok(Self {
697            shape_tag: AtomicU8::new(RingShape::Spsc as u8),
698            stale_shape_tag: AtomicU8::new(STALE_NONE),
699            pin_generation: AtomicU64::new(0),
700            frame_region: OnceLock::new(),
701            spsc, mpsc, mpmc, vyukov,
702            max_producers, max_consumers,
703            capacity,
704            directory,
705            synced_epoch: AtomicU64::new(u64::MAX),
706            grow_lock: parking_lot::Mutex::new(()),
707            contract: None,
708            shape_auto: AtomicBool::new(true),
709            ordering: None,
710            backing_id: BackingId::Shm { prefix: name_prefix.to_owned() },
711            header_sidecar: subetha_core::HandshakeHeader::new(),
712            ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
713        })
714    }
715
716    /// Attach to an AdaptiveRing whose four backings already live in the
717    /// named shared-memory regions a *different* process created with
718    /// [`create_shmfs`](Self::create_shmfs).
719    ///
720    /// The critical difference from `create_shmfs`: this validates each
721    /// backing's magic and attaches WITHOUT re-initialising the layout,
722    /// so a snapshot the creator already enqueued survives the attach.
723    /// (`create_shmfs` unconditionally re-lays-out every backing, which
724    /// zeroes any data already in the region - correct for the creator,
725    /// data-loss for a late attacher.) Use `create_shmfs` in the process
726    /// that owns the region's lifetime and `open_shmfs` in every process
727    /// that joins it afterwards.
728    ///
729    /// The peer directory is the source of truth for how many
730    /// per-producer backings exist right now; `max_producers` /
731    /// `max_consumers` are pre-attach floor hints only. Returns
732    /// [`RingError::LayoutMismatch`] if a backing is absent or its
733    /// header magic / capacity does not match (e.g. the creator has not
734    /// run yet, or ran with a different capacity).
735    pub fn open_shmfs(
736        name_prefix: &str,
737        max_producers: usize,
738        max_consumers: usize,
739        expected_capacity: usize,
740    ) -> Result<Self, RingError> {
741        assert!(max_producers >= 1, "max_producers must be >= 1");
742        assert!(max_consumers >= 1, "max_consumers must be >= 1");
743
744        let spsc_size = crate::spsc_ring::spsc_ring_file_size(expected_capacity);
745        let vyukov_size = crate::shared_ring::ring_file_size(expected_capacity);
746
747        // Directory first: create_or_open_shm only initialises when the
748        // magic is absent, so attaching never wipes the creator's live
749        // claims; its published count is how many per-producer rings
750        // really exist (the creator's hint may have grown since).
751        let directory = Arc::new(PeerDirectory::create_or_open_shm(
752            &format!("{name_prefix}_peers"),
753        )?);
754        let n_rings = directory.published().max(1);
755
756        // SPSC backing - attach, validate magic, NO re-init.
757        let spsc_shm = crate::shm_file::ShmFile::create_or_open_named(
758            &format!("{name_prefix}_spsc"), spsc_size,
759        ).map_err(|_| RingError::PayloadTooLarge)?;
760        let spsc = Arc::new(SpscRingCore::open_from_shm(spsc_shm, expected_capacity)?);
761
762        // MPSC backings.
763        let mut mpsc_rings = Vec::with_capacity(n_rings);
764        for i in 0..n_rings {
765            let shm = crate::shm_file::ShmFile::create_or_open_named(
766                &format!("{name_prefix}_mpsc_{i}"), spsc_size,
767            ).map_err(|_| RingError::PayloadTooLarge)?;
768            mpsc_rings.push(Arc::new(SpscRingCore::open_from_shm(shm, expected_capacity)?));
769        }
770        let mpsc = Arc::new(MpscBacking {
771            rings: ArcSwap::from_pointee(mpsc_rings),
772            next_drain: AtomicUsize::new(0),
773        });
774
775        // MPMC backings (one ring per producer; consumers partition).
776        let mut mpmc_rings = Vec::with_capacity(n_rings);
777        for i in 0..n_rings {
778            let shm = crate::shm_file::ShmFile::create_or_open_named(
779                &format!("{name_prefix}_mpmc_{i}"), spsc_size,
780            ).map_err(|_| RingError::PayloadTooLarge)?;
781            mpmc_rings.push(Arc::new(SpscRingCore::open_from_shm(shm, expected_capacity)?));
782        }
783        let mpmc = Arc::new(MpmcBacking {
784            rings: ArcSwap::from_pointee(mpmc_rings),
785            consumer_cursors: consumer_cursor_table(),
786        });
787
788        // Vyukov backing.
789        let vyukov_shm = crate::shm_file::ShmFile::create_or_open_named(
790            &format!("{name_prefix}_vyukov"), vyukov_size,
791        ).map_err(|_| RingError::PayloadTooLarge)?;
792        let vyukov = Arc::new(SharedRing::open_from_shm(vyukov_shm, expected_capacity)?);
793
794        Ok(Self {
795            shape_tag: AtomicU8::new(RingShape::Spsc as u8),
796            stale_shape_tag: AtomicU8::new(STALE_NONE),
797            pin_generation: AtomicU64::new(0),
798            frame_region: OnceLock::new(),
799            spsc, mpsc, mpmc, vyukov,
800            max_producers, max_consumers,
801            capacity: expected_capacity,
802            directory,
803            synced_epoch: AtomicU64::new(u64::MAX),
804            grow_lock: parking_lot::Mutex::new(()),
805            contract: None,
806            shape_auto: AtomicBool::new(true),
807            ordering: None,
808            backing_id: BackingId::Shm { prefix: name_prefix.to_owned() },
809            header_sidecar: subetha_core::HandshakeHeader::new(),
810            ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
811        })
812    }
813
814    /// Attach the ordering substrate: every subsequent push carries
815    /// an 8-byte stamp in slot bytes `[0..8)` and the payload cap
816    /// drops to [`STAMPED_PAYLOAD_BYTES`] (56 - the same 8 bytes
817    /// Vyukov spends on its per-slot sequence atom). Pops through
818    /// [`try_recv`](Self::try_recv) (and the pinned
819    /// [`ordered_try_pop`](PinnedRing::ordered_try_pop)) strip the
820    /// stamp and hand back payload bytes only.
821    ///
822    /// Stamping is FIXED at construction - call this before any
823    /// traffic. The merge flag inside the ordering region stays
824    /// runtime-dynamic via
825    /// [`set_ordering_mode`](Self::set_ordering_mode).
826    ///
827    /// Stamp-kind selection: invariant-TSC `rdtsc` when the CPUID
828    /// probe passes, the shared counter on x86 without an invariant
829    /// TSC, the monotonic clock on non-x86 hosts. Rings opened with
830    /// [`AdaptiveRing::open`] adopt the creator's stamp kind from
831    /// the region header (validated, never re-initialised).
832    ///
833    /// A stamped ring never morphs to [`RingShape::Vyukov`]: the
834    /// stamped 64-byte slot layout does not fit Vyukov's 56-byte
835    /// slots, and the `GlobalFifo` declaration on a stamped ring is
836    /// served by the merge flag instead of the Vyukov morph.
837    pub fn with_ordering_stamps(self) -> Result<Self, RingError> {
838        self.with_ordering_stamps_impl(None)
839    }
840
841    /// As [`with_ordering_stamps`](Self::with_ordering_stamps) with
842    /// an explicit stamp kind. `StampKind::SharedCounter` is the
843    /// exactness opt-in: stamps form a total order at the price of
844    /// one contended `fetch_add` per push. Opening an existing
845    /// region with a kind that does not match the creator's returns
846    /// [`RingError::LayoutMismatch`].
847    pub fn with_ordering_stamps_kind(self, kind: StampKind) -> Result<Self, RingError> {
848        self.with_ordering_stamps_impl(Some(kind))
849    }
850
851    fn with_ordering_stamps_impl(
852        mut self,
853        kind: Option<StampKind>,
854    ) -> Result<Self, RingError> {
855        if self.ordering.is_some() {
856            return Ok(self);
857        }
858        if self.current_shape() == RingShape::Vyukov {
859            return Err(RingError::LayoutMismatch);
860        }
861        // Stamp lines are sized to the substrate producer-slot
862        // ceiling, not the construction hint, so producer growth
863        // never needs an ordering-region resize. Untouched lines
864        // stay as never-faulted pages; every hot operation indexes
865        // by producer id and the merge gates scan only the
866        // published slot count.
867        let lines = crate::peer_directory::PRODUCER_SLOT_CEILING;
868        let region = match &self.backing_id {
869            BackingId::Anon => OrderingRegion::create_anon(
870                lines,
871                kind.unwrap_or_else(default_stamp_kind),
872            )?,
873            BackingId::File { prefix, created } => {
874                let path = with_suffix(prefix, ".ordering.bin");
875                if *created {
876                    OrderingRegion::create(
877                        &path,
878                        lines,
879                        kind.unwrap_or_else(default_stamp_kind),
880                    )?
881                } else {
882                    let region = OrderingRegion::open(&path, lines)?;
883                    if let Some(k) = kind
884                        && region.stamp_kind() != k
885                    {
886                        return Err(RingError::LayoutMismatch);
887                    }
888                    region
889                }
890            }
891            BackingId::Shm { prefix } => {
892                let size = ordering_region_size(lines);
893                let shm = crate::shm_file::ShmFile::create_or_open_named(
894                    &format!("{prefix}_ordering"),
895                    size,
896                ).map_err(|e| RingError::IoError(e.kind()))?;
897                OrderingRegion::create_shm(
898                    shm,
899                    lines,
900                    kind.unwrap_or_else(default_stamp_kind),
901                )?
902            }
903        };
904        let seen = (0..CONSUMER_SLOT_CEILING).map(|_| SeenLine::new()).collect();
905        self.ordering = Some(Arc::new(OrderingState { region, seen }));
906        Ok(self)
907    }
908
909    /// Current shape.
910    pub fn current_shape(&self) -> RingShape {
911        RingShape::from_u8(self.shape_tag.load(Ordering::Acquire))
912    }
913
914    /// Peek the next slot of the internal SPSC backing without
915    /// copying or releasing. Returns `None` when the active shape
916    /// is not SPSC OR when the ring is empty. Used by zero-copy
917    /// egress paths (e.g. the bridge primitives' `write_all` flow)
918    /// when the active shape supports peek-direct.
919    ///
920    /// The returned [`PeekedSpscSlot`] derefs to `&[u8]` pointing
921    /// INTO the SPSC backing's mmap region. Caller passes that
922    /// slice straight to downstream consumers, then calls
923    /// [`PeekedSpscSlot::confirm`] to release the slot.
924    pub fn peek_spsc_slot(&self) -> Option<PeekedSpscSlot<'_>> {
925        if self.current_shape() != RingShape::Spsc {
926            return None;
927        }
928        self.spsc.peek_slot().map(|inner| PeekedSpscSlot { inner })
929    }
930
931    /// Shape-aware emptiness check across every backing this ring
932    /// currently uses.
933    ///
934    /// - SPSC: the single SPSC backing's head==tail.
935    /// - MPSC: every per-producer SPSC sub-ring is empty.
936    /// - MPMC: every per-producer SPSC sub-ring in the grid is
937    ///   empty (cross-consumer claims are committed by sub-ring
938    ///   pops, so an empty grid means every slot has been
939    ///   consumed).
940    /// - Vyukov: producer_seq == consumer_seq.
941    ///
942    /// Used by capacity-morph wrappers to decide whether a stale
943    /// backing can be dropped. Conservative: a value returning
944    /// `true` is guaranteed empty at the moment of observation
945    /// across all sub-rings; concurrent producers writing into the
946    /// active shape during the check cannot affect a stale-only
947    /// caller because producers only target whichever Arc the
948    /// wrapper's ArcSwap currently points at.
949    pub fn is_empty(&self) -> bool {
950        if let Some(stale) = self.stale_shape()
951            && !self.backing_is_empty(stale)
952        {
953            return false;
954        }
955        self.backing_is_empty(self.current_shape())
956    }
957
958    /// Shape-aware approximate item count across every backing
959    /// currently in use (sum for composed shapes; single ring for
960    /// SPSC / Vyukov). Used by sidecar policies to compute fill
961    /// ratio and decide whether to grow / shrink capacity.
962    pub fn approx_len(&self) -> usize {
963        let stale_len = match self.stale_shape() {
964            Some(stale) if stale != self.current_shape() => {
965                self.backing_approx_len(stale)
966            }
967            _ => 0,
968        };
969        stale_len + self.backing_approx_len(self.current_shape())
970    }
971
972    fn backing_approx_len(&self, shape: RingShape) -> usize {
973        match shape {
974            RingShape::Spsc => self.spsc.approx_len(),
975            RingShape::Mpsc => self.mpsc.rings.load().iter().map(|r| r.approx_len()).sum(),
976            RingShape::Mpmc => self.mpmc.rings.load().iter().map(|r| r.approx_len()).sum(),
977            RingShape::Vyukov => self.vyukov.approx_len(),
978        }
979    }
980
981    /// Capacity of a single underlying sub-ring (per-producer slot
982    /// count). Composed shapes have N or N*M such sub-rings; the
983    /// total slot inventory is `sub_ring_capacity() * n_sub_rings`.
984    /// For SPSC / Vyukov this is the ring's full capacity.
985    pub fn sub_ring_capacity(&self) -> usize {
986        match self.current_shape() {
987            RingShape::Spsc => self.spsc.capacity(),
988            RingShape::Mpsc => self.mpsc.rings.load().first().map(|r| r.capacity()).unwrap_or(0),
989            RingShape::Mpmc => self.mpmc.rings.load().first().map(|r| r.capacity()).unwrap_or(0),
990            RingShape::Vyukov => self.vyukov.capacity(),
991        }
992    }
993
994    /// Total slot inventory across every sub-ring this AdaptiveRing
995    /// currently owns. For SPSC / Vyukov this is the same as
996    /// `sub_ring_capacity()`. For MPSC / MPMC it is
997    /// `sub_ring_capacity() * n_sub_rings`.
998    pub fn total_slot_capacity(&self) -> usize {
999        match self.current_shape() {
1000            RingShape::Spsc => self.spsc.capacity(),
1001            RingShape::Mpsc => self.mpsc.rings.load().iter().map(|r| r.capacity()).sum(),
1002            RingShape::Mpmc => self.mpmc.rings.load().iter().map(|r| r.capacity()).sum(),
1003            RingShape::Vyukov => self.vyukov.capacity(),
1004        }
1005    }
1006
1007    /// Current pin generation. Pinned handles capture this at pin
1008    /// time; a non-equal current value means the pin is stale.
1009    pub fn pin_generation(&self) -> u64 {
1010        self.pin_generation.load(Ordering::Acquire)
1011    }
1012
1013    /// Number of per-producer backings this ring pre-allocated at
1014    /// construction. A HINT, not a ceiling: registration past it
1015    /// grows the backings on demand.
1016    pub fn max_producers(&self) -> usize { self.max_producers }
1017
1018    /// Consumer-count hint captured at construction. Consumer slots
1019    /// are claimed dynamically up to the substrate ceiling.
1020    pub fn max_consumers(&self) -> usize { self.max_consumers }
1021
1022    /// Per-producer backings currently published (pre-allocated +
1023    /// grown), shared across every attached process.
1024    pub fn published_producers(&self) -> usize {
1025        self.directory.published()
1026    }
1027
1028    /// The ring's effective contract. UNBOUNDED unless the caller
1029    /// declared one via [`with_contract`](Self::with_contract) - a
1030    /// declared contract is the ONLY thing that makes registration
1031    /// fallible; the default grows on demand.
1032    pub fn contract(&self) -> crate::ring_contract::RingContract {
1033        self.contract.unwrap_or_else(crate::ring_contract::RingContract::unbounded)
1034    }
1035
1036    /// Declare an explicit ring contract (builder; consumes self,
1037    /// like [`with_ordering_stamps`](Self::with_ordering_stamps)).
1038    /// The contract's count bounds become the attach-time admission
1039    /// check and its ordering / capacity constraints become the
1040    /// feasible-region filter a policy consults.
1041    pub fn with_contract(mut self, contract: crate::ring_contract::RingContract) -> Self {
1042        self.contract = Some(contract);
1043        self
1044    }
1045
1046    /// Map a policy's proposed shape to the nearest contract-legal one,
1047    /// so an auto-morph cannot violate the declared ordering contract
1048    /// by construction. A `Fifo` contract forbids the partitioned
1049    /// per-producer-lane shapes ([`Mpsc`](RingShape::Mpsc),
1050    /// [`Mpmc`](RingShape::Mpmc), which interleave producers); the
1051    /// order-preserving substitute is [`Vyukov`](RingShape::Vyukov) on
1052    /// an unstamped ring. A stamped ring keeps the proposed shape - its
1053    /// global order is served by the `MergeStrict` flag, not a Vyukov
1054    /// morph (whose 56-byte slots do not fit the stamped 64-byte
1055    /// layout). Under the default (unbounded) contract this is the
1056    /// identity, so non-declaring rings are unaffected.
1057    pub fn contract_filtered_shape(&self, target: RingShape) -> RingShape {
1058        if self.contract().permits_shape(target) {
1059            return target;
1060        }
1061        if self.ordering.is_none() {
1062            RingShape::Vyukov
1063        } else {
1064            target
1065        }
1066    }
1067
1068    /// Re-morph the composed shape to the current active peer counts
1069    /// (read from the shared directory, so registrations in OTHER
1070    /// processes drive this process's shape too). Called from every
1071    /// register / unregister and from the topology sync slow path -
1072    /// no background thread required. Suppressed when the caller
1073    /// pinned the shape ([`pin_shape`](Self::pin_shape) or an
1074    /// explicit [`morph_to`](Self::morph_to)), and never disturbs a
1075    /// `Vyukov` shape - that is an ordering decision, not a count
1076    /// decision. Returns `false` only when a needed morph is blocked
1077    /// on an undrained stale backlog (the caller leaves the epoch
1078    /// unsynced so the next op retries).
1079    fn reshape_for_counts(&self) -> bool {
1080        if !self.shape_auto.load(Ordering::Relaxed)
1081            || self.current_shape() == RingShape::Vyukov
1082        {
1083            return true;
1084        }
1085        let p = self.directory.active_producers();
1086        let c = self.directory.active_consumers();
1087        if let Some(target) = DefaultRingShapePolicy::target_shape(p, c)
1088            && target != self.current_shape()
1089        {
1090            return self.morph_shape(self.contract_filtered_shape(target)).is_ok();
1091        }
1092        true
1093    }
1094
1095    /// Pin the composed shape: stop the automatic reshape-on-register so
1096    /// the ring holds whatever shape it currently has. The user override
1097    /// for callers that want a fixed shape. An explicit
1098    /// [`morph_to`](Self::morph_to) pins implicitly.
1099    pub fn pin_shape(&self) {
1100        self.shape_auto.store(false, Ordering::Relaxed);
1101    }
1102
1103    /// Resume the automatic shape (undo [`pin_shape`](Self::pin_shape)
1104    /// / an explicit morph) and re-track the live peer counts. Unlike
1105    /// the automatic reshape - which never disturbs a Vyukov shape -
1106    /// this explicit resume DOES morph a Vyukov ring back to the
1107    /// counts-based composed shape (that is what resuming means).
1108    pub fn resume_auto_shape(&self) {
1109        self.shape_auto.store(true, Ordering::Relaxed);
1110        let p = self.directory.active_producers();
1111        let c = self.directory.active_consumers();
1112        if let Some(target) = DefaultRingShapePolicy::target_shape(p, c) {
1113            self.morph_shape(self.contract_filtered_shape(target)).ok();
1114        }
1115    }
1116
1117    /// Whether the composed shape auto-morphs to the active peer counts
1118    /// (the default). `false` after [`pin_shape`](Self::pin_shape) or an
1119    /// explicit [`morph_to`](Self::morph_to).
1120    pub fn shape_is_auto(&self) -> bool {
1121        self.shape_auto.load(Ordering::Relaxed)
1122    }
1123
1124    /// One relaxed load on the shared topology epoch; on change, run
1125    /// the sync slow path (grow local arrays, reshape). Called at the
1126    /// top of every adaptive-path op so cross-process registrations
1127    /// propagate with no background thread.
1128    #[inline]
1129    fn ensure_synced(&self) {
1130        let e = self.directory.epoch();
1131        if e != self.synced_epoch.load(Ordering::Relaxed) {
1132            self.sync_topology(e);
1133        }
1134    }
1135
1136    #[cold]
1137    fn sync_topology(&self, epoch: u64) {
1138        self.directory.reap_dead_peers();
1139        let arrays_ok = self.refresh_local_arrays().is_ok();
1140        let shape_ok = self.reshape_for_counts();
1141        if arrays_ok && shape_ok {
1142            // Reaping / a racing registrant may have advanced the
1143            // epoch since `epoch` was read; store the STALE value so
1144            // the next op re-syncs to the newer state.
1145            self.synced_epoch.store(epoch, Ordering::Relaxed);
1146        }
1147    }
1148
1149    /// Open (or, for the grower, create) local handles for every
1150    /// published per-producer backing this process has not mapped
1151    /// yet. Growth bumps the pin generation so outstanding pins
1152    /// re-acquire and see the new backings.
1153    fn refresh_local_arrays(&self) -> Result<(), RingError> {
1154        let published = self.directory.published();
1155        if self.mpsc.rings.load().len() >= published {
1156            return Ok(());
1157        }
1158        let _guard = self.grow_lock.lock();
1159        let cur_mpsc = self.mpsc.rings.load_full();
1160        let cur_mpmc = self.mpmc.rings.load_full();
1161        if cur_mpsc.len() >= published {
1162            return Ok(());
1163        }
1164        let mut mpsc_new = (*cur_mpsc).clone();
1165        let mut mpmc_new = (*cur_mpmc).clone();
1166        for i in cur_mpsc.len()..published {
1167            let (a, b) = self.open_ring_backing(i)?;
1168            mpsc_new.push(a);
1169            mpmc_new.push(b);
1170        }
1171        self.mpsc.rings.store(Arc::new(mpsc_new));
1172        self.mpmc.rings.store(Arc::new(mpmc_new));
1173        self.pin_generation.fetch_add(1, Ordering::AcqRel);
1174        Ok(())
1175    }
1176
1177    /// Open the published backing pair for producer slot `i` created
1178    /// by another process (file / shm locales; anonymous backings are
1179    /// single-instance so their published set is always local).
1180    fn open_ring_backing(
1181        &self,
1182        i: usize,
1183    ) -> Result<(Arc<SpscRingCore>, Arc<SpscRingCore>), RingError> {
1184        match &self.backing_id {
1185            BackingId::File { prefix, .. } => {
1186                let a = SpscRingCore::open(
1187                    with_suffix(prefix, &format!(".mpsc.{i}.bin")), self.capacity)?;
1188                let b = SpscRingCore::open(
1189                    with_suffix(prefix, &format!(".mpmc.{i}.bin")), self.capacity)?;
1190                Ok((Arc::new(a), Arc::new(b)))
1191            }
1192            BackingId::Shm { prefix } => {
1193                let size = crate::spsc_ring::spsc_ring_file_size(self.capacity);
1194                let shm_a = crate::shm_file::ShmFile::create_or_open_named(
1195                    &format!("{prefix}_mpsc_{i}"), size,
1196                ).map_err(|e| RingError::IoError(e.kind()))?;
1197                let shm_b = crate::shm_file::ShmFile::create_or_open_named(
1198                    &format!("{prefix}_mpmc_{i}"), size,
1199                ).map_err(|e| RingError::IoError(e.kind()))?;
1200                let a = SpscRingCore::create_from_shm(shm_a, self.capacity)?;
1201                let b = SpscRingCore::create_from_shm(shm_b, self.capacity)?;
1202                Ok((Arc::new(a), Arc::new(b)))
1203            }
1204            // Anonymous backings cannot be published by a peer: any
1205            // growth on this instance created them locally already.
1206            BackingId::Anon => Err(RingError::LayoutMismatch),
1207        }
1208    }
1209
1210    /// Create the backing pair for a NEW producer slot `i` (the
1211    /// grower path; this process claimed the slot, so it is the
1212    /// single creator by construction).
1213    fn create_ring_backing(
1214        &self,
1215        i: usize,
1216    ) -> Result<(Arc<SpscRingCore>, Arc<SpscRingCore>), RingError> {
1217        match &self.backing_id {
1218            BackingId::Anon => {
1219                let a = SpscRingCore::create_anon(self.capacity)?;
1220                let b = SpscRingCore::create_anon(self.capacity)?;
1221                Ok((Arc::new(a), Arc::new(b)))
1222            }
1223            BackingId::File { prefix, .. } => {
1224                let a = SpscRingCore::create(
1225                    with_suffix(prefix, &format!(".mpsc.{i}.bin")), self.capacity)?;
1226                let b = SpscRingCore::create(
1227                    with_suffix(prefix, &format!(".mpmc.{i}.bin")), self.capacity)?;
1228                Ok((Arc::new(a), Arc::new(b)))
1229            }
1230            BackingId::Shm { prefix } => {
1231                let size = crate::spsc_ring::spsc_ring_file_size(self.capacity);
1232                let shm_a = crate::shm_file::ShmFile::create_or_open_named(
1233                    &format!("{prefix}_mpsc_{i}"), size,
1234                ).map_err(|e| RingError::IoError(e.kind()))?;
1235                let shm_b = crate::shm_file::ShmFile::create_or_open_named(
1236                    &format!("{prefix}_mpmc_{i}"), size,
1237                ).map_err(|e| RingError::IoError(e.kind()))?;
1238                let a = SpscRingCore::create_from_shm(shm_a, self.capacity)?;
1239                let b = SpscRingCore::create_from_shm(shm_b, self.capacity)?;
1240                Ok((Arc::new(a), Arc::new(b)))
1241            }
1242        }
1243    }
1244
1245    /// Grow the per-producer backings so slots `< want` all exist:
1246    /// create the missing backing pairs, append them to the local
1247    /// arrays, then publish the new count (Release) so other
1248    /// processes open them on their next epoch sync.
1249    fn grow_rings_to(&self, want: usize) -> Result<(), RingError> {
1250        let _guard = self.grow_lock.lock();
1251        let published = self.directory.published();
1252        let cur_mpsc = self.mpsc.rings.load_full();
1253        let cur_mpmc = self.mpmc.rings.load_full();
1254        let mut mpsc_new = (*cur_mpsc).clone();
1255        let mut mpmc_new = (*cur_mpmc).clone();
1256        // Open backings other processes published first, then create
1257        // this grower's new ones.
1258        for i in cur_mpsc.len()..published {
1259            let (a, b) = self.open_ring_backing(i)?;
1260            mpsc_new.push(a);
1261            mpmc_new.push(b);
1262        }
1263        for i in published..want {
1264            let (a, b) = self.create_ring_backing(i)?;
1265            mpsc_new.push(a);
1266            mpmc_new.push(b);
1267        }
1268        if mpsc_new.len() > cur_mpsc.len() {
1269            self.mpsc.rings.store(Arc::new(mpsc_new));
1270            self.mpmc.rings.store(Arc::new(mpmc_new));
1271            self.pin_generation.fetch_add(1, Ordering::AcqRel);
1272        }
1273        if want > published {
1274            self.directory.publish_rings(want);
1275        }
1276        Ok(())
1277    }
1278
1279    /// Register a new producer. Returns its `producer_id` - a shared
1280    /// slot claim visible to every attached process. Registration
1281    /// GROWS the ring on demand (new per-producer backings past the
1282    /// construction hint) and auto-morphs the composed shape to the
1283    /// new peer counts; it fails only under a caller-declared
1284    /// contract ceiling ([`with_contract`](Self::with_contract)) or at
1285    /// the substrate slot ceiling
1286    /// ([`PRODUCER_SLOT_CEILING`](crate::peer_directory::PRODUCER_SLOT_CEILING)
1287    /// CONCURRENT producers). The id stays valid until
1288    /// [`unregister_producer`](Self::unregister_producer).
1289    pub fn register_producer(&self) -> Result<usize, AdaptiveError> {
1290        let slot = self.directory.claim_producer_slot()
1291            .ok_or(AdaptiveError::TooManyProducers)?;
1292        if let Some(g) = self.contract
1293            && !g.permits_producer(self.directory.active_producers() - 1)
1294        {
1295            self.directory.release_producer_slot(slot);
1296            return Err(AdaptiveError::TooManyProducers);
1297        }
1298        if slot >= self.directory.published()
1299            && self.grow_rings_to(slot + 1).is_err()
1300        {
1301            self.directory.release_producer_slot(slot);
1302            return Err(AdaptiveError::GrowthFailed);
1303        }
1304        self.ensure_synced();
1305        self.reshape_for_counts();
1306        Ok(slot)
1307    }
1308
1309    /// Unregister a producer slot. Caller passes the id returned
1310    /// from [`register_producer`](Self::register_producer). The slot
1311    /// recycles; its backing (and any undrained backlog) stays until
1312    /// the consumer drains it.
1313    pub fn unregister_producer(&self, producer_id: usize) {
1314        self.directory.release_producer_slot(producer_id);
1315        self.reshape_for_counts();
1316    }
1317
1318    /// Register a new consumer. Returns its `consumer_id` - a shared
1319    /// slot claim visible to every attached process. Rebalances MPMC
1320    /// ring ownership toward the new consumer set and auto-morphs
1321    /// the shape. Fails only under a caller-declared contract ceiling
1322    /// or at the substrate consumer-slot ceiling
1323    /// ([`CONSUMER_SLOT_CEILING`]).
1324    pub fn register_consumer(&self) -> Result<usize, AdaptiveError> {
1325        let slot = self.directory.claim_consumer_slot()
1326            .ok_or(AdaptiveError::TooManyConsumers)?;
1327        if let Some(g) = self.contract
1328            && !g.permits_consumer(self.directory.active_consumers() - 1)
1329        {
1330            self.directory.release_consumer_slot(slot);
1331            return Err(AdaptiveError::TooManyConsumers);
1332        }
1333        self.rebalance_ownership();
1334        self.ensure_synced();
1335        self.reshape_for_counts();
1336        Ok(slot)
1337    }
1338
1339    /// Unregister a consumer slot. The leaving consumer transfers
1340    /// its MPMC ring ownership to the remaining consumers itself
1341    /// (it is the single owner, so the direct transfer is safe),
1342    /// then releases the slot.
1343    pub fn unregister_consumer(&self, consumer_id: usize) {
1344        let me = consumer_id as u16;
1345        let remaining: Vec<u16> = self.directory.claimed_consumer_slots()
1346            .into_iter()
1347            .filter(|s| *s != me)
1348            .collect();
1349        let n = self.directory.published();
1350        for r in 0..n {
1351            let (owner, _) = self.directory.ring_owner(r);
1352            if owner == me {
1353                match remaining.get(r % remaining.len().max(1)) {
1354                    Some(to) => self.directory.transfer_ring(r, me, *to),
1355                    None => self.directory.transfer_ring(r, me, OWNER_NONE),
1356                }
1357            }
1358        }
1359        self.directory.release_consumer_slot(consumer_id);
1360        self.reshape_for_counts();
1361    }
1362
1363    /// Spread MPMC ring ownership round-robin over the CURRENT
1364    /// consumer set: unowned rings are claimed directly for their
1365    /// target; owned rings get a pending handoff their current
1366    /// owner applies on its next pop scan (single-writer transfer,
1367    /// so two consumers never drain one Lamport ring concurrently).
1368    fn rebalance_ownership(&self) {
1369        let slots = self.directory.claimed_consumer_slots();
1370        if slots.is_empty() {
1371            return;
1372        }
1373        let n = self.directory.published();
1374        for r in 0..n {
1375            let desired = slots[r % slots.len()];
1376            let (owner, pending) = self.directory.ring_owner(r);
1377            if owner == desired {
1378                continue;
1379            }
1380            if owner == OWNER_NONE {
1381                self.directory.try_claim_ring(r, desired);
1382            } else if pending != desired {
1383                self.directory.request_handoff(r, desired);
1384            }
1385        }
1386    }
1387
1388    /// Current active producer count (shared across processes).
1389    pub fn active_producers(&self) -> usize {
1390        self.directory.active_producers()
1391    }
1392
1393    /// Current active consumer count (shared across processes).
1394    pub fn active_consumers(&self) -> usize {
1395        self.directory.active_consumers()
1396    }
1397
1398    /// Whether this ring carries ordering stamps.
1399    pub fn is_stamped(&self) -> bool {
1400        self.ordering.is_some()
1401    }
1402
1403    /// Stamp kind, when stamped.
1404    pub fn stamp_kind(&self) -> Option<StampKind> {
1405        self.ordering.as_ref().map(|o| o.region.stamp_kind())
1406    }
1407
1408    /// Current ordering mode, when stamped. The mode atom lives in
1409    /// the MMF-resident ordering region, so every process attached
1410    /// to the ring reads the same value - deliberately unlike the
1411    /// process-local shape tag.
1412    pub fn ordering_mode(&self) -> Option<OrderingMode> {
1413        self.ordering.as_ref().map(|o| o.region.mode())
1414    }
1415
1416    /// Flip the ordering mode. The ordered switch is one `Release`
1417    /// store: Off->On retroactively orders the in-flight backlog
1418    /// (stamps were already in the slots), On->Off is immediate. No
1419    /// drain, no data movement, and outstanding pins stay valid -
1420    /// the pinned pop consults the mode atom on every call.
1421    pub fn set_ordering_mode(&self, mode: OrderingMode) -> Result<(), RingError> {
1422        let ord = self.ordering.as_ref().ok_or(RingError::NotStamped)?;
1423        ord.region.set_mode(mode);
1424        Ok(())
1425    }
1426
1427    /// Cross-producer inversions observed at pop since the ordering
1428    /// region was created. Shared across processes.
1429    pub fn inversions(&self) -> u64 {
1430        self.ordering.as_ref().map(|o| o.region.inversions()).unwrap_or(0)
1431    }
1432
1433    /// Watermark heartbeat for an idle producer (MergeStrict
1434    /// liveness). See [`OrderingRegion::refresh_watermark`].
1435    pub fn refresh_watermark(&self, producer_id: usize) -> Result<(), RingError> {
1436        let ord = self.ordering.as_ref().ok_or(RingError::NotStamped)?;
1437        if producer_id >= ord.region.max_producers() {
1438            return Err(RingError::PayloadTooLarge);
1439        }
1440        ord.region.refresh_watermark(producer_id);
1441        Ok(())
1442    }
1443
1444    /// Terminal producer retirement: MergeStrict consumers stop
1445    /// waiting on this producer slot's silence permanently. Call on
1446    /// clean producer exit; the slot must not push afterwards. See
1447    /// [`OrderingRegion::retire_producer`].
1448    pub fn retire_producer(&self, producer_id: usize) -> Result<(), RingError> {
1449        let ord = self.ordering.as_ref().ok_or(RingError::NotStamped)?;
1450        if producer_id >= ord.region.max_producers() {
1451            return Err(RingError::PayloadTooLarge);
1452        }
1453        ord.region.retire_producer(producer_id);
1454        Ok(())
1455    }
1456
1457    /// Voluntarily release the merge-drainer lease held by this
1458    /// process + consumer slot. Returns `Ok(false)` when the lease
1459    /// was not held.
1460    pub fn release_drainer(&self, consumer_id: usize) -> Result<bool, RingError> {
1461        let ord = self.ordering.as_ref().ok_or(RingError::NotStamped)?;
1462        Ok(ord.region.release_drainer(drainer_token(consumer_id)))
1463    }
1464
1465    /// Advance the drainer-lease epoch (dead-drainer takeover after
1466    /// [`DRAINER_GRACE_EPOCHS`] missed beats). The QoS-aware sidecar
1467    /// ticks this once per scan; standalone callers tick it
1468    /// themselves, mirroring `OwnerLease::tick_epoch`.
1469    pub fn tick_drainer_epoch(&self) -> Result<u64, RingError> {
1470        let ord = self.ordering.as_ref().ok_or(RingError::NotStamped)?;
1471        Ok(ord.region.tick_drainer_epoch())
1472    }
1473
1474    /// Direct access to the ordering region for composing wrappers:
1475    /// capacity morphs seed the fresh backing's region from the old
1476    /// one so counter stamps stay monotone across the swap, and
1477    /// E2E harnesses read watermarks / the drainer token directly.
1478    pub fn ordering_region(&self) -> Option<&OrderingRegion> {
1479        self.ordering.as_ref().map(|o| &o.region)
1480    }
1481
1482    /// Adaptive-path push. One Acquire load on the shape tag, one
1483    /// branch, then the native push on the matching backend.
1484    ///
1485    /// `producer_id` selects the producer ring for MPSC / MPMC
1486    /// shapes. For SPSC and Vyukov shapes the id is ignored (except
1487    /// on stamped rings, where it selects the producer's stamp line
1488    /// and must stay below `max_producers`).
1489    ///
1490    /// On stamped rings the payload cap is
1491    /// [`STAMPED_PAYLOAD_BYTES`] and the stamp is prepended
1492    /// transparently; the matching `try_recv` strips it.
1493    pub fn try_send(&self, producer_id: usize, payload: &[u8]) -> Result<(), RingError> {
1494        self.ensure_synced();
1495        let shape = RingShape::from_u8(self.shape_tag.load(Ordering::Acquire));
1496        if let Some(ord) = &self.ordering {
1497            return self.stamped_send_inner(ord, shape, producer_id, payload);
1498        }
1499        match shape {
1500            RingShape::Spsc => self.spsc.try_push(payload),
1501            RingShape::Mpsc => {
1502                let rings = self.mpsc.rings.load();
1503                let ring = rings.get(producer_id)
1504                    .ok_or(RingError::PayloadTooLarge)?; // misuse: producer_id out of range
1505                ring.try_push(payload)
1506            }
1507            RingShape::Mpmc => {
1508                let rings = self.mpmc.rings.load();
1509                let ring = rings.get(producer_id)
1510                    .ok_or(RingError::PayloadTooLarge)?;
1511                ring.try_push(payload)
1512            }
1513            RingShape::Vyukov => self.vyukov.try_push(payload),
1514        }
1515    }
1516
1517    /// Adaptive-path pop. `consumer_id` selects the consumer's
1518    /// round-robin partition for the MPMC shape. For SPSC, MPSC,
1519    /// and Vyukov shapes the id is ignored (one consumer).
1520    ///
1521    /// On stamped rings this is the ordering-aware pop: the stamp
1522    /// is stripped (callers see payload bytes only, `Ok(56)`), the
1523    /// inversion counter runs, and when the ordering mode is
1524    /// `MergeByStamp` / `MergeStrict` the pop k-way-merges ring
1525    /// heads by stamp under the single-drainer lease.
1526    pub fn try_recv(&self, consumer_id: usize, out: &mut [u8]) -> Result<usize, RingError> {
1527        self.ensure_synced();
1528        let shape = RingShape::from_u8(self.shape_tag.load(Ordering::Acquire));
1529        if let Some(ord) = &self.ordering {
1530            return self.ordered_recv_inner(ord, shape, consumer_id, out)
1531                .map(|(n, _stamp)| n);
1532        }
1533        // Stale walk: the previous shape's backlog drains first so
1534        // a morph never strands (or reorders ahead of) in-flight
1535        // items.
1536        if let Some(stale) = self.stale_shape()
1537            && stale != shape
1538            && Self::may_walk_stale(stale, consumer_id)
1539            && let Ok(n) = self.shape_pop(stale, consumer_id, out)
1540        {
1541            return Ok(n);
1542        }
1543        self.shape_pop(shape, consumer_id, out)
1544    }
1545
1546    /// Largest record stored inline in a ring slot by the frame path.
1547    /// Conservative across shapes: the smallest slot payload (Vyukov's
1548    /// [`PAYLOAD_BYTES`] = 56) minus the 5-byte frame header (a class
1549    /// byte plus a `u32` length), so an inlined record fits any shape's
1550    /// slot no matter how the ring morphs.
1551    pub const FRAME_INLINE_BUDGET: usize = PAYLOAD_BYTES - 5;
1552
1553    /// Block size of the lazily-created payload region. A frame larger
1554    /// than both the inline budget and this is rejected with
1555    /// [`RingError::PayloadTooLarge`]; size the region explicitly with
1556    /// [`with_frames`](Self::with_frames) for larger records.
1557    pub const FRAME_DEFAULT_BLOCK_SIZE: usize = 8192;
1558
1559    /// Pre-create and size the frame payload region. Optional: the
1560    /// region is otherwise created lazily at
1561    /// [`FRAME_DEFAULT_BLOCK_SIZE`](Self::FRAME_DEFAULT_BLOCK_SIZE)
1562    /// the first time a record is too large to inline. No-op if the
1563    /// region already exists. Returns the ring for chaining.
1564    pub fn with_frames(self, block_size: usize, block_count: usize) -> Self {
1565        self.frame_region
1566            .get_or_init(|| self.build_frame_region(block_size, block_count));
1567        self
1568    }
1569
1570    fn frame_region(&self) -> &FrameRegion {
1571        self.frame_region.get_or_init(|| {
1572            let blocks = self.spsc.capacity().max(16);
1573            self.build_frame_region(Self::FRAME_DEFAULT_BLOCK_SIZE, blocks)
1574        })
1575    }
1576
1577    /// Build the payload region on the SAME locale as the ring's own
1578    /// backings, so offset frames cross a process boundary. An Anon ring
1579    /// gets a private in-process region; a file- or shm-backed ring gets
1580    /// a SHARED region named off the backing prefix (`<prefix>.frames.bin`
1581    /// / `<prefix>_frames`) that every process attached to the ring maps.
1582    ///
1583    /// The region's locale must match the ring's: a payload above the
1584    /// inline budget spills there, so a private anon region under a file-
1585    /// or shm-backed ring would put the bytes somewhere the peer process
1586    /// cannot map, and no offset frame would cross the boundary.
1587    ///
1588    /// The region is created lazily on the first offset frame -
1589    /// the producer create-or-opens it before pushing the descriptor, so
1590    /// a consumer that create-or-opens it on receipt always finds the
1591    /// already-initialised region (the descriptor it popped proves the
1592    /// producer got there first).
1593    fn build_frame_region(&self, block_size: usize, block_count: usize) -> Arc<FrameRegion> {
1594        let region = match &self.backing_id {
1595            BackingId::Anon => FrameRegion::create_anon(block_size, block_count),
1596            BackingId::Shm { prefix } => FrameRegion::create_or_open_shm(
1597                &format!("{prefix}_frames"),
1598                block_size,
1599                block_count,
1600            ),
1601            BackingId::File { prefix, .. } => FrameRegion::create_or_open_file(
1602                with_suffix(prefix, ".frames.bin"),
1603                block_size,
1604                block_count,
1605            ),
1606        };
1607        Arc::new(region.expect("frame region create-or-open"))
1608    }
1609
1610    /// Frame-path send: carries any payload size on whatever shape the
1611    /// ring is in. Records up to
1612    /// [`FRAME_INLINE_BUDGET`](Self::FRAME_INLINE_BUDGET) go inline in
1613    /// the ring slot; larger ones spill to the shared payload region
1614    /// and the slot carries the block index. Returns which path the
1615    /// record took. `producer_id` selects the backing ring for MPSC /
1616    /// MPMC exactly as [`try_send`](Self::try_send). The same call
1617    /// works at every shape because the descriptor rides the slot and
1618    /// the region is multi-producer / multi-consumer safe.
1619    ///
1620    /// Not available on stamped (ordering) rings - frames and stamps
1621    /// both claim the slot head, so they are mutually exclusive;
1622    /// returns [`RingError::LayoutMismatch`] there.
1623    pub fn send_frame(&self, producer_id: usize, payload: &[u8])
1624        -> Result<FrameClass, RingError>
1625    {
1626        self.send_frame_as(producer_id, payload, LayoutHint::Auto)
1627    }
1628
1629    /// [`send_frame`](Self::send_frame) with an explicit layout
1630    /// override ([`LayoutHint::ForceInline`] / [`LayoutHint::ForceOffset`]).
1631    pub fn send_frame_as(&self, producer_id: usize, payload: &[u8], hint: LayoutHint)
1632        -> Result<FrameClass, RingError>
1633    {
1634        if self.ordering.is_some() {
1635            return Err(RingError::LayoutMismatch);
1636        }
1637        let inline = match hint {
1638            LayoutHint::ForceInline => {
1639                if payload.len() > Self::FRAME_INLINE_BUDGET {
1640                    return Err(RingError::PayloadTooLarge);
1641                }
1642                true
1643            }
1644            LayoutHint::ForceOffset => false,
1645            LayoutHint::Auto => payload.len() <= Self::FRAME_INLINE_BUDGET,
1646        };
1647        let len = payload.len() as u32;
1648        if inline {
1649            // [class:u8][len:u32][payload bytes]; fits the 56-byte
1650            // Vyukov slot, so it fits every shape's slot.
1651            let mut buf = [0u8; PAYLOAD_BYTES];
1652            buf[0] = FrameClass::Inline as u8;
1653            buf[1..5].copy_from_slice(&len.to_le_bytes());
1654            buf[5..5 + payload.len()].copy_from_slice(payload);
1655            self.try_send(producer_id, &buf[..5 + payload.len()])?;
1656            Ok(FrameClass::Inline)
1657        } else {
1658            let region = self.frame_region();
1659            if payload.len() > region.block_size() {
1660                return Err(RingError::PayloadTooLarge);
1661            }
1662            let idx = region.alloc().ok_or(RingError::Full)?;
1663            region.write_block(idx, payload);
1664            // [class:u8][len:u32][block_idx:u32]
1665            let mut buf = [0u8; 9];
1666            buf[0] = FrameClass::Offset as u8;
1667            buf[1..5].copy_from_slice(&len.to_le_bytes());
1668            buf[5..9].copy_from_slice(&idx.to_le_bytes());
1669            match self.try_send(producer_id, &buf) {
1670                Ok(()) => Ok(FrameClass::Offset),
1671                Err(e) => {
1672                    // Descriptor push failed (ring full): return the
1673                    // block so it is not leaked.
1674                    region.free(idx);
1675                    Err(e)
1676                }
1677            }
1678        }
1679    }
1680
1681    /// Frame-path recv: counterpart to [`send_frame`](Self::send_frame).
1682    /// Clears `out` and
1683    /// fills it with the record's payload, transparently reading the
1684    /// payload region and freeing its block for offset records. Returns
1685    /// which path the record took. `consumer_id` selects the consumer
1686    /// partition for MPMC as [`try_recv`](Self::try_recv). Not available
1687    /// on stamped rings.
1688    pub fn recv_frame(&self, consumer_id: usize, out: &mut Vec<u8>)
1689        -> Result<FrameClass, RingError>
1690    {
1691        if self.ordering.is_some() {
1692            return Err(RingError::LayoutMismatch);
1693        }
1694        // SPSC_PAYLOAD_BYTES (64) holds any shape's slot.
1695        let mut slot = [0u8; SPSC_PAYLOAD_BYTES];
1696        self.try_recv(consumer_id, &mut slot)?;
1697        let len = u32::from_le_bytes([slot[1], slot[2], slot[3], slot[4]]) as usize;
1698        out.clear();
1699        if slot[0] == FrameClass::Inline as u8 {
1700            out.extend_from_slice(&slot[5..5 + len]);
1701            Ok(FrameClass::Inline)
1702        } else {
1703            let idx = u32::from_le_bytes([slot[5], slot[6], slot[7], slot[8]]);
1704            let region = self.frame_region();
1705            region.read_block_into(idx, len, out);
1706            region.free(idx);
1707            Ok(FrameClass::Offset)
1708        }
1709    }
1710
1711    /// As [`try_recv`](Self::try_recv) on a stamped ring, also
1712    /// returning the popped item's stamp. This is how consumers
1713    /// assert the ordering guarantee they paid for (monotone stamps
1714    /// under the merge modes) instead of trusting it. Returns
1715    /// [`RingError::NotStamped`] on unstamped rings.
1716    pub fn try_recv_with_stamp(
1717        &self,
1718        consumer_id: usize,
1719        out: &mut [u8],
1720    ) -> Result<(usize, u64), RingError> {
1721        let ord = self.ordering.as_ref().ok_or(RingError::NotStamped)?;
1722        let shape = RingShape::from_u8(self.shape_tag.load(Ordering::Acquire));
1723        self.ordered_recv_inner(ord, shape, consumer_id, out)
1724    }
1725
1726    /// Stamped push: issue the producer's next stamp, lay out
1727    /// `[stamp; 8][payload; <=56]` and push to the active Lamport
1728    /// backing. The watermark advances whether the push lands or
1729    /// returns `Full` - a stamp that failed to publish will never
1730    /// appear, so advancing keeps the MergeStrict in-flight gate
1731    /// live.
1732    fn stamped_send_inner(
1733        &self,
1734        ord: &OrderingState,
1735        shape: RingShape,
1736        producer_id: usize,
1737        payload: &[u8],
1738    ) -> Result<(), RingError> {
1739        if payload.len() > STAMPED_PAYLOAD_BYTES {
1740            return Err(RingError::PayloadTooLarge);
1741        }
1742        if producer_id >= ord.region.max_producers() {
1743            return Err(RingError::PayloadTooLarge);
1744        }
1745        let mpsc_guard;
1746        let mpmc_guard;
1747        let ring: &SpscRingCore = match shape {
1748            RingShape::Spsc => &self.spsc,
1749            RingShape::Mpsc => {
1750                mpsc_guard = self.mpsc.rings.load();
1751                mpsc_guard.get(producer_id).ok_or(RingError::PayloadTooLarge)?
1752            }
1753            RingShape::Mpmc => {
1754                mpmc_guard = self.mpmc.rings.load();
1755                mpmc_guard.get(producer_id).ok_or(RingError::PayloadTooLarge)?
1756            }
1757            // Stamped rings never run the Vyukov backing: the
1758            // stamped 64-byte slot layout does not fit its 56-byte
1759            // slots. morph_to rejects the transition, so this arm is
1760            // a defensive layout error, not a reachable path.
1761            RingShape::Vyukov => return Err(RingError::LayoutMismatch),
1762        };
1763        let stamp = ord.region.next_stamp(producer_id);
1764        let mut buf = [0u8; SPSC_PAYLOAD_BYTES];
1765        buf[..STAMP_BYTES].copy_from_slice(&stamp.to_le_bytes());
1766        buf[STAMP_BYTES..STAMP_BYTES + payload.len()].copy_from_slice(payload);
1767        let result = ring.try_push(&buf[..STAMP_BYTES + payload.len()]);
1768        ord.region.publish_watermark(producer_id, stamp);
1769        result
1770    }
1771
1772    /// Stamped pop: strip the stamp, run the inversion counter, and
1773    /// dispatch per the live ordering mode. Returns the payload
1774    /// length and the popped stamp.
1775    fn ordered_recv_inner(
1776        &self,
1777        ord: &OrderingState,
1778        shape: RingShape,
1779        consumer_id: usize,
1780        out: &mut [u8],
1781    ) -> Result<(usize, u64), RingError> {
1782        if consumer_id >= CONSUMER_SLOT_CEILING {
1783            return Err(RingError::PayloadTooLarge);
1784        }
1785        if out.len() < STAMPED_PAYLOAD_BYTES {
1786            return Err(RingError::PayloadTooLarge);
1787        }
1788        let mode = ord.region.mode();
1789        match mode {
1790            OrderingMode::Unordered => {
1791                let mut buf = [0u8; SPSC_PAYLOAD_BYTES];
1792                // Stale walk first (a stamped ring's backings are
1793                // all Lamport shapes, so the stale pop is the same
1794                // stamped slot layout).
1795                let popped = self.stale_shape()
1796                    .filter(|stale| *stale != shape)
1797                    .filter(|stale| Self::may_walk_stale(*stale, consumer_id))
1798                    .and_then(|stale| {
1799                        self.shape_pop(stale, consumer_id, &mut buf).ok()
1800                    });
1801                if popped.is_none() {
1802                    match shape {
1803                        RingShape::Spsc => self.spsc.try_pop(&mut buf),
1804                        RingShape::Mpsc => self.mpsc_pop(&mut buf),
1805                        RingShape::Mpmc => self.mpmc_pop(consumer_id, &mut buf),
1806                        RingShape::Vyukov => Err(RingError::LayoutMismatch),
1807                    }?;
1808                }
1809                let stamp = u64::from_le_bytes(
1810                    buf[..STAMP_BYTES].try_into().unwrap(),
1811                );
1812                self.note_stamp(ord, consumer_id, mode, stamp);
1813                out[..STAMPED_PAYLOAD_BYTES]
1814                    .copy_from_slice(&buf[STAMP_BYTES..]);
1815                Ok((STAMPED_PAYLOAD_BYTES, stamp))
1816            }
1817            OrderingMode::MergeByStamp | OrderingMode::MergeStrict => {
1818                // Always hold the single-drainer lease: consumers can
1819                // JOIN at runtime, so a static 1-consumer bypass would
1820                // leave a leaseless drainer racing the new joiner's
1821                // leased one. Per-pop verification must stay OFF the
1822                // stamp-hot header line (producers fetch_add it every
1823                // push; each extra consumer load of it costs a cache
1824                // transfer): one load of the quiet lease-generation
1825                // line, compared to a consumer-local cache, and only a
1826                // change (claim / takeover / release / epoch tick)
1827                // runs the full lease handshake.
1828                let seen = &ord.seen[consumer_id];
1829                let lease_gen_now = ord.region.lease_generation();
1830                if seen.lease_gen.load(Ordering::Relaxed) != lease_gen_now {
1831                    if !ord.region.try_acquire_drainer(
1832                        drainer_token(consumer_id),
1833                        DRAINER_GRACE_EPOCHS,
1834                    ) {
1835                        return Err(RingError::NotDrainer);
1836                    }
1837                    seen.lease_gen.store(lease_gen_now, Ordering::Relaxed);
1838                }
1839                // Stale walk: merge within the stale shape's rings
1840                // until that backlog drains, then merge the active
1841                // shape. Stale items predate active items (producers
1842                // switched at the tag flip), so stale-first keeps
1843                // global stamp order across the morph boundary.
1844                if let Some(stale) = self.stale_shape()
1845                    && stale != shape
1846                {
1847                    match self.merge_pop(ord, stale, consumer_id, mode, out) {
1848                        Ok(result) => return Ok(result),
1849                        Err(RingError::Empty) => {}
1850                        Err(e) => return Err(e),
1851                    }
1852                }
1853                self.merge_pop(ord, shape, consumer_id, mode, out)
1854            }
1855        }
1856    }
1857
1858
1859    /// K-way min-stamp merge over the active shape's ring heads:
1860    /// peek every non-empty ring, pick the minimum stamp, confirm
1861    /// exactly that slot, leave every other head unconsumed.
1862    ///
1863    /// Three release gates sit between the scan and the confirm:
1864    ///
1865    /// - **In-flight gate** (both merge modes): a producer whose
1866    ///   `issued` stamp (or reservation floor) is ahead of its
1867    ///   `watermark` holds exactly one stamp in its
1868    ///   reserve-stamp-push window; if it undercuts the candidate,
1869    ///   the pop returns `Empty` until the publish lands (or the
1870    ///   push's `Full` failure advances the watermark). This is the
1871    ///   only bound that survives producer descheduling - the
1872    ///   window stretches to scheduler quanta under preemption or
1873    ///   virtualization, far past any fixed time guard.
1874    /// - **Freshness guard** (time-based stamps, both merge modes,
1875    ///   only when at least one ring is empty): a candidate younger
1876    ///   than the guard window may be raced by a stamp a producer
1877    ///   has not even RESERVED yet (cross-core clock skew); the
1878    ///   merge re-peeks until the candidate ages out (bounded by
1879    ///   the guard, ~2us).
1880    /// - **Watermark gate** (`MergeStrict` only): every EMPTY
1881    ///   in-use ring's watermark must have reached the candidate,
1882    ///   closing the not-yet-reserved case with zero time-semantics
1883    ///   assumptions. This couples release latency to the slowest
1884    ///   producer: idle producers heartbeat via
1885    ///   [`refresh_watermark`](OrderingRegion::refresh_watermark)
1886    ///   and exiting producers call
1887    ///   [`retire_producer`](OrderingRegion::retire_producer), or
1888    ///   the strict consumer stalls on their silence by design.
1889    fn merge_pop(
1890        &self,
1891        ord: &OrderingState,
1892        shape: RingShape,
1893        consumer_id: usize,
1894        mode: OrderingMode,
1895        out: &mut [u8],
1896    ) -> Result<(usize, u64), RingError> {
1897        // Snapshot the composed arrays (they live behind an ArcSwap
1898        // for producer growth); the SPSC arm borrows directly.
1899        let mpsc_guard;
1900        let mpmc_guard;
1901        let rings: &[Arc<SpscRingCore>] = match shape {
1902            RingShape::Spsc => std::slice::from_ref(&self.spsc),
1903            RingShape::Mpsc => {
1904                mpsc_guard = self.mpsc.rings.load();
1905                mpsc_guard.as_slice()
1906            }
1907            RingShape::Mpmc => {
1908                mpmc_guard = self.mpmc.rings.load();
1909                mpmc_guard.as_slice()
1910            }
1911            RingShape::Vyukov => &[],
1912        };
1913        if rings.is_empty() {
1914            return Err(RingError::LayoutMismatch);
1915        }
1916        // The release gates cover every producer slot that has ever
1917        // stamped: the PUBLISHED slot count, not the region's
1918        // ceiling-sized line array (whose untouched tail would cost
1919        // thousands of loads per pop).
1920        let gate_lines = self.directory.published()
1921            .min(ord.region.max_producers());
1922        let kind = ord.region.stamp_kind();
1923        loop {
1924            // Scalar min scan: the per-ring peek atomics dominate
1925            // the cost of each pass, so the comparison work is not
1926            // the bottleneck at realistic producer counts.
1927            let mut best: Option<(usize, u64)> = None;
1928            let mut any_empty = false;
1929            for (i, ring) in rings.iter().enumerate() {
1930                match ring.peek_slot() {
1931                    Some(peek) => {
1932                        let s = u64::from_le_bytes(
1933                            peek[..STAMP_BYTES].try_into().unwrap(),
1934                        );
1935                        if best.is_none_or(|(_, bs)| s < bs) {
1936                            best = Some((i, s));
1937                        }
1938                    }
1939                    None => any_empty = true,
1940                }
1941            }
1942            let Some((idx, stamp)) = best else {
1943                return Err(RingError::Empty);
1944            };
1945
1946            for line in 0..gate_lines {
1947                if ord.region.in_flight_below(line, stamp) {
1948                    return Err(RingError::Empty);
1949                }
1950            }
1951            if mode == OrderingMode::MergeStrict {
1952                for line in 0..gate_lines {
1953                    // In-use slot (has ever stamped; retirement
1954                    // saturates the watermark so retired slots
1955                    // always pass) whose ring is empty: its
1956                    // watermark must have reached the candidate.
1957                    if ord.region.issued(line) != 0
1958                        && rings[line.min(rings.len() - 1)].approx_len() == 0
1959                        && ord.region.watermark(line) < stamp
1960                    {
1961                        return Err(RingError::Empty);
1962                    }
1963                }
1964            }
1965            if any_empty
1966                && let Some(guard) = kind.freshness_guard()
1967                && stamp_now(kind).wrapping_sub(stamp) < guard
1968            {
1969                std::hint::spin_loop();
1970                continue;
1971            }
1972
1973            let peek = rings[idx].peek_slot().expect(
1974                "single drainer holds the lease; a peeked head cannot vanish",
1975            );
1976            let confirmed_stamp = u64::from_le_bytes(
1977                peek[..STAMP_BYTES].try_into().unwrap(),
1978            );
1979            out[..STAMPED_PAYLOAD_BYTES].copy_from_slice(&peek[STAMP_BYTES..]);
1980            peek.confirm();
1981            self.note_stamp(ord, consumer_id, mode, confirmed_stamp);
1982            return Ok((STAMPED_PAYLOAD_BYTES, confirmed_stamp));
1983        }
1984    }
1985
1986    /// Per-consumer inversion accounting. A pop whose stamp
1987    /// undercuts the previous pop's stamp is one cross-producer
1988    /// inversion: the counter bumps in the shared header and an
1989    /// observation rides the sidecar ring. Mode transitions reset
1990    /// the baseline so the retroactive reordering of the backlog at
1991    /// an Off->On flip is not miscounted.
1992    fn note_stamp(
1993        &self,
1994        ord: &OrderingState,
1995        consumer_id: usize,
1996        mode: OrderingMode,
1997        stamp: u64,
1998    ) {
1999        let line = &ord.seen[consumer_id];
2000        if line.mode_tag.swap(mode as u32, Ordering::Relaxed) != mode as u32 {
2001            line.stamp.store(0, Ordering::Relaxed);
2002        }
2003        let last = line.stamp.load(Ordering::Relaxed);
2004        if stamp < last {
2005            ord.region.record_inversion();
2006            self.ring_sidecar
2007                .push_op(crate::sidecar_ops::ordering::OP_ORDER_INVERSION, 0);
2008        }
2009        line.stamp.store(stamp, Ordering::Relaxed);
2010    }
2011
2012    fn mpsc_pop(&self, out: &mut [u8]) -> Result<usize, RingError> {
2013        let rings = self.mpsc.rings.load();
2014        self.mpsc_pop_in(&rings, out)
2015    }
2016
2017    fn mpsc_pop_in(
2018        &self,
2019        rings: &[Arc<SpscRingCore>],
2020        out: &mut [u8],
2021    ) -> Result<usize, RingError> {
2022        let n = rings.len();
2023        if n == 0 {
2024            return Err(RingError::Empty);
2025        }
2026        let start = self.mpsc.next_drain.load(Ordering::Relaxed);
2027        for i in 0..n {
2028            let idx = (start + i) % n;
2029            if let Ok(bytes) = rings[idx].try_pop(out) {
2030                self.mpsc.next_drain.store((idx + 1) % n, Ordering::Relaxed);
2031                return Ok(bytes);
2032            }
2033        }
2034        Err(RingError::Empty)
2035    }
2036
2037    /// MPMC pop through the shared ownership table: this consumer
2038    /// drains exactly the rings whose owner entry names its slot
2039    /// (single-reader invariant), CAS-claims unowned rings on sight
2040    /// (so unregistered-consumer flows keep working), applies
2041    /// pending rebalance handoffs from its own scan (single-writer
2042    /// transfer), and - rate-limited - takes over rings whose owner
2043    /// process died.
2044    fn mpmc_pop(&self, consumer_id: usize, out: &mut [u8]) -> Result<usize, RingError> {
2045        let rings = self.mpmc.rings.load();
2046        self.mpmc_pop_in(&rings, consumer_id, out)
2047    }
2048
2049    fn mpmc_pop_in(
2050        &self,
2051        rings: &[Arc<SpscRingCore>],
2052        consumer_id: usize,
2053        out: &mut [u8],
2054    ) -> Result<usize, RingError> {
2055        let cursor_line = self.mpmc.consumer_cursors.get(consumer_id)
2056            .ok_or(RingError::PayloadTooLarge)?;
2057        let me = consumer_id as u16;
2058        let n = rings.len();
2059        if n == 0 {
2060            return Err(RingError::Empty);
2061        }
2062        let start = cursor_line.0.load(Ordering::Relaxed) % n;
2063        // First stuck ring (owned elsewhere, has items): the crash-
2064        // takeover candidate when the whole scan comes up empty.
2065        let mut stuck: Option<(usize, u16)> = None;
2066        for i in 0..n {
2067            let idx = (start + i) % n;
2068            let (owner, pending) = self.directory.ring_owner(idx);
2069            if owner == me {
2070                if pending != OWNER_NONE
2071                    && self.directory.apply_handoff(idx, me).is_some()
2072                {
2073                    continue; // handed off; not ours to drain anymore
2074                }
2075                if let Ok(bytes) = rings[idx].try_pop(out) {
2076                    cursor_line.0.store((idx + 1) % n, Ordering::Relaxed);
2077                    return Ok(bytes);
2078                }
2079            } else if owner == OWNER_NONE {
2080                if self.directory.try_claim_ring(idx, me)
2081                    && let Ok(bytes) = rings[idx].try_pop(out)
2082                {
2083                    cursor_line.0.store((idx + 1) % n, Ordering::Relaxed);
2084                    return Ok(bytes);
2085                }
2086            } else if stuck.is_none() && rings[idx].approx_len() > 0 {
2087                stuck = Some((idx, owner));
2088            }
2089        }
2090        // Crash takeover, rate-limited: the pid probe is a syscall,
2091        // so only every 1024th empty scan per consumer attempts it.
2092        if let Some((idx, owner)) = stuck {
2093            let probes = cursor_line.1.fetch_add(1, Ordering::Relaxed);
2094            if probes % 1024 == 1023
2095                && self.directory.try_takeover(idx, owner, me)
2096                && let Ok(bytes) = rings[idx].try_pop(out)
2097            {
2098                cursor_line.0.store((idx + 1) % n, Ordering::Relaxed);
2099                return Ok(bytes);
2100            }
2101        }
2102        Err(RingError::Empty)
2103    }
2104
2105    /// Pin the current shape and return a [`PinnedRing`] that
2106    /// exposes the matching backend at native speed. The composed
2107    /// arrays are captured at pin time (zero per-op indirection);
2108    /// producer growth bumps the pin generation, so pin holders see
2109    /// [`PinnedRing::is_still_valid`] `== false` and re-pin to pick
2110    /// up new backings.
2111    pub fn pin_current_shape(&self) -> PinnedRing<'_> {
2112        self.ensure_synced();
2113        let captured_gen = self.pin_generation.load(Ordering::Acquire);
2114        let shape = RingShape::from_u8(self.shape_tag.load(Ordering::Acquire));
2115        PinnedRing {
2116            parent: self,
2117            pinned_generation: captured_gen,
2118            shape,
2119            mpsc_rings: self.mpsc.rings.load_full(),
2120            mpmc_rings: self.mpmc.rings.load_full(),
2121            _not_sync: PhantomData,
2122        }
2123    }
2124
2125    /// Trigger a shape morph. NO data moves: the old shape's
2126    /// backing becomes the STALE backing, producers follow the new
2127    /// `shape_tag` immediately, and the consumer's pop path drains
2128    /// the stale backlog first (the stale walk) before reading from
2129    /// the new shape. This is what makes morphing safe under
2130    /// saturating traffic - there is no transfer to overflow the
2131    /// target's capacity and no second drainer racing the live
2132    /// consumer (each backing keeps exactly one reader).
2133    ///
2134    /// The stale marker stays set until the NEXT morph, which
2135    /// requires the backlog drained ([`RingError::StaleBacklog`]
2136    /// otherwise - the sidecar's scan loop simply retries). Keeping
2137    /// it set gives a producer whose push straddled the tag flip a
2138    /// wide grace window: its item lands in the old backing, which
2139    /// the consumer still walks.
2140    ///
2141    /// Bumps `pin_generation` so outstanding pins see
2142    /// `is_still_valid() == false` and re-acquire. Pinned NATIVE
2143    /// pops (`spsc_try_pop` etc.) are shape-direct and do not walk
2144    /// the stale backing; consumers that pop through pins across
2145    /// morphs use [`AdaptiveRing::try_recv`] or
2146    /// [`PinnedRing::ordered_try_pop`], which do.
2147    ///
2148    /// An explicit `morph_to` is a USER shape decision, so it pins
2149    /// the shape (suppresses the automatic count-driven reshape)
2150    /// until [`resume_auto_shape`](Self::resume_auto_shape).
2151    pub fn morph_to(&self, new_shape: RingShape) -> Result<(), RingError> {
2152        self.shape_auto.store(false, Ordering::Relaxed);
2153        self.morph_shape(new_shape)
2154    }
2155
2156    /// The morph mechanism, shared by the public (pinning)
2157    /// [`morph_to`](Self::morph_to), the automatic count-driven
2158    /// reshape, and policy sidecars.
2159    pub(crate) fn morph_shape(&self, new_shape: RingShape) -> Result<(), RingError> {
2160        let old_shape = RingShape::from_u8(self.shape_tag.load(Ordering::Acquire));
2161        if old_shape == new_shape {
2162            return Ok(());
2163        }
2164
2165        // A stamped ring never runs the Vyukov backing: the stamped
2166        // 64-byte slot layout ([stamp; 8][payload; 56]) does not fit
2167        // Vyukov's 56-byte slots. The GlobalFifo declaration on a
2168        // stamped ring is served by the merge flag
2169        // (set_ordering_mode) instead of this morph.
2170        if self.ordering.is_some() && new_shape == RingShape::Vyukov {
2171            return Err(RingError::LayoutMismatch);
2172        }
2173
2174        // One stale backing at a time: the prior morph's backlog
2175        // must be drained before another shape change.
2176        let prior_stale = self.stale_shape_tag.load(Ordering::Acquire);
2177        if prior_stale != STALE_NONE
2178            && !self.backing_is_empty(RingShape::from_u8(prior_stale))
2179        {
2180            return Err(RingError::StaleBacklog);
2181        }
2182
2183        // Bump the pin generation so existing pins see
2184        // is_still_valid() == false on their next check; then
2185        // publish old-as-stale before the new tag so a pop that
2186        // observes the new shape also sees the stale marker.
2187        self.pin_generation.fetch_add(1, Ordering::AcqRel);
2188        self.stale_shape_tag.store(old_shape as u8, Ordering::Release);
2189        self.shape_tag.store(new_shape as u8, Ordering::Release);
2190        Ok(())
2191    }
2192
2193    /// Whether one shape's backing holds no items right now.
2194    fn backing_is_empty(&self, shape: RingShape) -> bool {
2195        match shape {
2196            RingShape::Spsc => self.spsc.approx_len() == 0,
2197            RingShape::Mpsc => self.mpsc.rings.load().iter().all(|r| r.approx_len() == 0),
2198            RingShape::Mpmc => self.mpmc.rings.load().iter().all(|r| r.approx_len() == 0),
2199            RingShape::Vyukov => self.vyukov.approx_len() == 0,
2200        }
2201    }
2202
2203    /// The stale shape still draining after the last morph, if any.
2204    fn stale_shape(&self) -> Option<RingShape> {
2205        let tag = self.stale_shape_tag.load(Ordering::Acquire);
2206        if tag == STALE_NONE {
2207            None
2208        } else {
2209            Some(RingShape::from_u8(tag))
2210        }
2211    }
2212
2213    /// Whether `consumer_id` may drain a stale backing of `shape`.
2214    /// Single-reader backings (SPSC, MPSC) are walked by consumer 0
2215    /// only; the MPMC grid partitions per consumer and Vyukov pops
2216    /// are CAS-safe for any consumer.
2217    fn may_walk_stale(shape: RingShape, consumer_id: usize) -> bool {
2218        match shape {
2219            RingShape::Spsc | RingShape::Mpsc => consumer_id == 0,
2220            RingShape::Mpmc | RingShape::Vyukov => true,
2221        }
2222    }
2223
2224    /// Unstamped pop from one shape's backing.
2225    fn shape_pop(
2226        &self,
2227        shape: RingShape,
2228        consumer_id: usize,
2229        out: &mut [u8],
2230    ) -> Result<usize, RingError> {
2231        match shape {
2232            RingShape::Spsc => self.spsc.try_pop(out),
2233            RingShape::Mpsc => self.mpsc_pop(out),
2234            RingShape::Mpmc => self.mpmc_pop(consumer_id, out),
2235            RingShape::Vyukov => self.vyukov.try_pop(out),
2236        }
2237    }
2238}
2239
2240fn with_suffix(base: &std::path::Path, suffix: &str) -> std::path::PathBuf {
2241    let mut s = base.as_os_str().to_owned();
2242    s.push(suffix);
2243    std::path::PathBuf::from(s)
2244}
2245
2246/// Error type for AdaptiveRing registration / morph operations.
2247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2248pub enum AdaptiveError {
2249    /// `register_producer` refused: a caller-declared contract
2250    /// ceiling ([`AdaptiveRing::with_contract`]) or the substrate
2251    /// slot ceiling. Never returned by the unpinned default below
2252    /// the substrate ceiling - registration grows the ring instead.
2253    TooManyProducers,
2254    /// `register_consumer` refused: same two cases on the consumer
2255    /// axis.
2256    TooManyConsumers,
2257    /// Producer registration claimed a slot but creating / opening
2258    /// the grown backing failed (I/O); the slot was released.
2259    GrowthFailed,
2260}
2261
2262/// Handle pinned to one shape of the parent [`AdaptiveRing`].
2263/// Hot-path ops bypass the adaptive dispatch and call the native
2264/// backend directly. The pin holder periodically calls
2265/// [`is_still_valid`](Self::is_still_valid) to check whether a
2266/// morph has invalidated this pin; on `false` the caller releases
2267/// and re-acquires via [`AdaptiveRing::pin_current_shape`].
2268pub struct PinnedRing<'a> {
2269    parent: &'a AdaptiveRing,
2270    pinned_generation: u64,
2271    shape: RingShape,
2272    /// Composed arrays captured at pin time: pinned ops index these
2273    /// directly (native speed, no ArcSwap load per op). Producer
2274    /// growth invalidates the pin, so a re-pin picks up new rings.
2275    mpsc_rings: Arc<Vec<Arc<SpscRingCore>>>,
2276    mpmc_rings: Arc<Vec<Arc<SpscRingCore>>>,
2277    _not_sync: PhantomData<Cell<()>>,
2278}
2279
2280impl<'a> PinnedRing<'a> {
2281    /// Shape this pin was captured at.
2282    pub fn shape(&self) -> RingShape { self.shape }
2283
2284    /// Monitor-wait HINT for the consumer side of `shape`: an atom
2285    /// whose Release-store accompanies (or is) the next publish a
2286    /// pop is waiting for. Arm `crate::monitor_wait::monitor_wait_u64`
2287    /// on it instead of burning a raw spin loop - on Windows the
2288    /// scheduler deschedules and migrates pure spinners (measured
2289    /// 1.7-2.7 us one-way for a cross-process spin ping-pong that
2290    /// runs in ~100-300 ns under Linux/FreeBSD on comparable
2291    /// silicon), while a monitor-armed waiter wakes on the store
2292    /// itself.
2293    ///
2294    /// Contract: this is a HINT, not a wake guarantee - on the
2295    /// multi-line shapes (MPSC/MPMC) it covers producer line 0
2296    /// only, and on Vyukov it covers the slot at the CURRENT
2297    /// consumer position (recompute after each pop). Callers must
2298    /// keep their waits budget-bounded and re-poll, which
2299    /// `monitor_wait_u64`'s budget enforces.
2300    pub fn recv_signal(&self, shape: RingShape) -> &AtomicU64 {
2301        match shape {
2302            RingShape::Spsc => self.parent.spsc.head_signal(),
2303            RingShape::Mpsc => self.mpsc_rings[0].head_signal(),
2304            RingShape::Mpmc => self.mpmc_rings[0].head_signal(),
2305            RingShape::Vyukov => self.parent.vyukov.next_pop_signal(),
2306        }
2307    }
2308
2309    /// One Acquire load on the parent's `pin_generation`. Returns
2310    /// `true` while the pin is current; `false` if a morph has
2311    /// happened and the caller should release + re-acquire.
2312    pub fn is_still_valid(&self) -> bool {
2313        self.parent.pin_generation.load(Ordering::Acquire) == self.pinned_generation
2314    }
2315
2316    /// Native SPSC push. Caller assumes single-producer ownership
2317    /// and ensures pin validity is checked at meaningful intervals.
2318    pub fn spsc_try_push(&self, payload: &[u8]) -> Result<(), RingError> {
2319        self.parent.spsc.try_push(payload)
2320    }
2321
2322    /// Native SPSC pop.
2323    pub fn spsc_try_pop(&self, out: &mut [u8]) -> Result<usize, RingError> {
2324        self.parent.spsc.try_pop(out)
2325    }
2326
2327    /// MPSC push to a specific producer ring (captured at pin time).
2328    pub fn mpsc_try_push(&self, producer_id: usize, payload: &[u8]) -> Result<(), RingError> {
2329        let ring = self.mpsc_rings.get(producer_id)
2330            .ok_or(RingError::PayloadTooLarge)?;
2331        ring.try_push(payload)
2332    }
2333
2334    /// MPSC pop (round-robin across the producer rings captured at
2335    /// pin time; a grown producer set invalidates the pin).
2336    pub fn mpsc_try_pop(&self, out: &mut [u8]) -> Result<usize, RingError> {
2337        self.parent.mpsc_pop_in(&self.mpsc_rings, out)
2338    }
2339
2340    /// MPMC push to a specific producer ring (captured at pin time).
2341    pub fn mpmc_try_push(&self, producer_id: usize, payload: &[u8]) -> Result<(), RingError> {
2342        let ring = self.mpmc_rings.get(producer_id)
2343            .ok_or(RingError::PayloadTooLarge)?;
2344        ring.try_push(payload)
2345    }
2346
2347    /// MPMC pop for a specific consumer. Ownership is consulted live
2348    /// from the shared directory (correctness under consumer joins /
2349    /// leaves); the ring array is the pin-time capture.
2350    pub fn mpmc_try_pop(&self, consumer_id: usize, out: &mut [u8]) -> Result<usize, RingError> {
2351        self.parent.mpmc_pop_in(&self.mpmc_rings, consumer_id, out)
2352    }
2353
2354    /// Vyukov MPMC push.
2355    pub fn vyukov_try_push(&self, payload: &[u8]) -> Result<(), RingError> {
2356        self.parent.vyukov.try_push(payload)
2357    }
2358
2359    /// Vyukov MPMC pop.
2360    pub fn vyukov_try_pop(&self, out: &mut [u8]) -> Result<usize, RingError> {
2361        self.parent.vyukov.try_pop(out)
2362    }
2363
2364    /// Stamped push through the pinned shape: the producer's next
2365    /// stamp is prepended and the payload cap is
2366    /// [`STAMPED_PAYLOAD_BYTES`]. Requires a ring constructed with
2367    /// [`AdaptiveRing::with_ordering_stamps`]; returns
2368    /// [`RingError::NotStamped`] otherwise.
2369    pub fn stamped_try_push(
2370        &self,
2371        producer_id: usize,
2372        payload: &[u8],
2373    ) -> Result<(), RingError> {
2374        let ord = self.parent.ordering.as_ref().ok_or(RingError::NotStamped)?;
2375        self.parent.stamped_send_inner(ord, self.shape, producer_id, payload)
2376    }
2377
2378    /// Ordering-aware pop through the pinned shape. The pin stays
2379    /// valid across ordering-mode flips - this call reads the
2380    /// MMF-resident mode atom every time (one Acquire load, a plain
2381    /// MOV on x86 TSO) and dispatches accordingly: partition pop +
2382    /// inversion counter under `Unordered`, k-way min-stamp merge
2383    /// under `MergeByStamp` / `MergeStrict`. Returns payload bytes
2384    /// only (`Ok(56)`).
2385    pub fn ordered_try_pop(
2386        &self,
2387        consumer_id: usize,
2388        out: &mut [u8],
2389    ) -> Result<usize, RingError> {
2390        let ord = self.parent.ordering.as_ref().ok_or(RingError::NotStamped)?;
2391        self.parent
2392            .ordered_recv_inner(ord, self.shape, consumer_id, out)
2393            .map(|(n, _stamp)| n)
2394    }
2395
2396    /// As [`ordered_try_pop`](Self::ordered_try_pop), also returning
2397    /// the popped stamp so hot-loop consumers can assert the
2398    /// ordering guarantee they paid for.
2399    pub fn ordered_try_pop_with_stamp(
2400        &self,
2401        consumer_id: usize,
2402        out: &mut [u8],
2403    ) -> Result<(usize, u64), RingError> {
2404        let ord = self.parent.ordering.as_ref().ok_or(RingError::NotStamped)?;
2405        self.parent.ordered_recv_inner(ord, self.shape, consumer_id, out)
2406    }
2407}
2408
2409/// Zero-copy peek into AdaptiveRing's SPSC backing. Wraps the
2410/// underlying [`PeekedSlot`](crate::spsc_ring::PeekedSlot) so the
2411/// AdaptiveRing crate boundary owns the type. Same semantics:
2412/// derefs to `&[u8]`, call `confirm` to release the slot.
2413pub struct PeekedSpscSlot<'a> {
2414    inner: crate::spsc_ring::PeekedSlot<'a>,
2415}
2416
2417impl<'a> PeekedSpscSlot<'a> {
2418    pub fn as_slice(&self) -> &[u8] { self.inner.as_slice() }
2419    pub fn len(&self) -> usize { self.inner.len() }
2420    pub fn is_empty(&self) -> bool { self.inner.is_empty() }
2421    pub fn confirm(self) { self.inner.confirm() }
2422}
2423
2424impl<'a> std::ops::Deref for PeekedSpscSlot<'a> {
2425    type Target = [u8];
2426    fn deref(&self) -> &[u8] { &self.inner }
2427}
2428
2429/// SPSC payload size for the SPSC / MPSC / MPMC backings (Lamport
2430/// slot is 64B payload-only).
2431pub const ADAPTIVE_SPSC_PAYLOAD_BYTES: usize = SPSC_PAYLOAD_BYTES;
2432
2433/// Vyukov payload size for the Vyukov backing (56B; 8B is the
2434/// per-slot sequence atomic).
2435pub const ADAPTIVE_VYUKOV_PAYLOAD_BYTES: usize = PAYLOAD_BYTES;
2436
2437// ===================================================================
2438// Sidecar shape policy: automatic morphing based on peer-count
2439// observations.
2440// ===================================================================
2441
2442/// A snapshot of the ring's observable state passed to a policy
2443/// on every sidecar scan.
2444#[derive(Debug, Clone, Copy)]
2445pub struct PolicyObservation {
2446    pub active_producers: usize,
2447    pub active_consumers: usize,
2448    pub current_shape: RingShape,
2449    pub since_last_morph: std::time::Duration,
2450    /// Whether the ring carries ordering stamps. Shape policies
2451    /// consult this because the GlobalFifo declaration routes
2452    /// differently: unstamped rings morph to Vyukov, stamped rings
2453    /// flip the merge flag (the ordering policy's job) and must
2454    /// stay on the composed shapes.
2455    pub stamped: bool,
2456}
2457
2458/// Policy that decides when (and to what shape) the sidecar
2459/// should morph the ring.
2460///
2461/// The sidecar scanner calls `decide` on every scan tick with the
2462/// current peer counts + shape + cooldown since the last morph.
2463/// Returning `Some(new_shape)` triggers a `morph_to(new_shape)`.
2464/// Returning `None` leaves the shape alone.
2465pub trait RingShapePolicy: Send + Sync + 'static {
2466    fn decide(&self, observation: &PolicyObservation) -> Option<RingShape>;
2467}
2468
2469/// Default policy: pick the cheapest shape that fits the current
2470/// peer counts, with a fixed hysteresis interval after each morph
2471/// to prevent thrashing under rapid peer-count oscillation.
2472///
2473/// Mapping (when `since_last_morph >= hysteresis`):
2474///
2475/// | producers | consumers | shape  |
2476/// |-----------|-----------|--------|
2477/// |     1     |     1     | `Spsc` |
2478/// |    >=2    |     1     | `Mpsc` |
2479/// |     *     |    >=2    | `Mpmc` |
2480///
2481/// While `since_last_morph < hysteresis`, returns `None` even if
2482/// the target shape differs. While either peer count is 0 the
2483/// policy also returns `None` (no point morphing an empty ring).
2484pub struct DefaultRingShapePolicy {
2485    pub hysteresis: std::time::Duration,
2486}
2487
2488impl Default for DefaultRingShapePolicy {
2489    fn default() -> Self {
2490        Self { hysteresis: std::time::Duration::from_millis(100) }
2491    }
2492}
2493
2494impl DefaultRingShapePolicy {
2495    pub fn target_shape(producers: usize, consumers: usize) -> Option<RingShape> {
2496        match (producers, consumers) {
2497            (0, _) | (_, 0) => None,
2498            (1, 1) => Some(RingShape::Spsc),
2499            (_, 1) => Some(RingShape::Mpsc),
2500            (_, _) => Some(RingShape::Mpmc),
2501        }
2502    }
2503}
2504
2505impl RingShapePolicy for DefaultRingShapePolicy {
2506    fn decide(&self, obs: &PolicyObservation) -> Option<RingShape> {
2507        if obs.since_last_morph < self.hysteresis {
2508            return None;
2509        }
2510        let target = Self::target_shape(obs.active_producers, obs.active_consumers)?;
2511        if target == obs.current_shape {
2512            None
2513        } else {
2514            Some(target)
2515        }
2516    }
2517}
2518
2519/// QoS-aware shape policy: consumes the
2520/// [`Ordering`](crate::qos_policy::Ordering) declaration on a
2521/// [`QosPolicy`](crate::qos_policy::QosPolicy) alongside the peer
2522/// counts.
2523///
2524/// Decision matrix (after the hysteresis cooldown):
2525///
2526/// | declaration | ring | decision |
2527/// |---|---|---|
2528/// | `GlobalFifo` | unstamped | morph to `Vyukov` (the proven global-FIFO structure) |
2529/// | `GlobalFifo` | stamped | counts-based composed shape; the ordering axis is served by the merge flag, which the [`OrderingPolicy`] flips |
2530/// | `PerProducer` | either | counts-based default (which also walks an earlier Vyukov morph back once the declaration is withdrawn) |
2531pub struct QosRingShapePolicy {
2532    pub qos: Arc<crate::qos_policy::QosPolicy>,
2533    pub hysteresis: std::time::Duration,
2534}
2535
2536impl QosRingShapePolicy {
2537    pub fn new(qos: Arc<crate::qos_policy::QosPolicy>) -> Self {
2538        Self { qos, hysteresis: std::time::Duration::from_millis(100) }
2539    }
2540}
2541
2542impl RingShapePolicy for QosRingShapePolicy {
2543    fn decide(&self, obs: &PolicyObservation) -> Option<RingShape> {
2544        if obs.since_last_morph < self.hysteresis {
2545            return None;
2546        }
2547        let target = match self.qos.ordering() {
2548            QosOrdering::GlobalFifo if !obs.stamped => Some(RingShape::Vyukov),
2549            _ => DefaultRingShapePolicy::target_shape(
2550                obs.active_producers,
2551                obs.active_consumers,
2552            ),
2553        }?;
2554        if target == obs.current_shape {
2555            None
2556        } else {
2557            Some(target)
2558        }
2559    }
2560}
2561
2562/// A snapshot of a stamped ring's ordering-relevant state passed to
2563/// an [`OrderingPolicy`] on every sidecar scan.
2564#[derive(Debug, Clone, Copy)]
2565pub struct OrderingPolicyObservation {
2566    /// Inversions per second observed since the previous scan
2567    /// (delta of the shared inversion counter over the scan
2568    /// interval).
2569    pub inversions_per_sec: f64,
2570    /// Live ordering mode.
2571    pub current_mode: OrderingMode,
2572    /// The caller's QoS declaration.
2573    pub declared: QosOrdering,
2574    pub active_producers: usize,
2575    pub active_consumers: usize,
2576    /// Time since the last mode flip this sidecar issued.
2577    pub since_last_change: std::time::Duration,
2578}
2579
2580/// Policy that decides when (and to which mode) the sidecar flips
2581/// a stamped ring's ordering flag. Mirrors [`RingShapePolicy`]:
2582/// `Some(mode)` triggers `set_ordering_mode(mode)`, `None` leaves
2583/// the flag alone.
2584pub trait OrderingPolicy: Send + Sync + 'static {
2585    fn decide(&self, observation: &OrderingPolicyObservation) -> Option<OrderingMode>;
2586}
2587
2588/// Default ordering policy.
2589///
2590/// - Acts on the QoS declaration always: `GlobalFifo` arms
2591///   `MergeByStamp`; withdrawing to `PerProducer` disarms back to
2592///   `Unordered` (only when `auto_order_threshold` is unset - see
2593///   below).
2594/// - Acts on the inversion rate only when the caller pre-authorized
2595///   an automatic response by setting `auto_order_threshold`
2596///   (inversions/sec): under a `PerProducer` declaration, a rate
2597///   above the threshold arms `MergeByStamp`. The auto arm is
2598///   one-way - merged pops read zero inversions by construction, so
2599///   there is no symmetric signal to disarm on; disarming is the
2600///   caller's call (QoS declaration or an explicit
2601///   `set_ordering_mode`).
2602pub struct DefaultOrderingPolicy {
2603    pub hysteresis: std::time::Duration,
2604    pub auto_order_threshold: Option<f64>,
2605}
2606
2607impl Default for DefaultOrderingPolicy {
2608    fn default() -> Self {
2609        Self {
2610            hysteresis: std::time::Duration::from_millis(100),
2611            auto_order_threshold: None,
2612        }
2613    }
2614}
2615
2616impl OrderingPolicy for DefaultOrderingPolicy {
2617    fn decide(&self, obs: &OrderingPolicyObservation) -> Option<OrderingMode> {
2618        if obs.since_last_change < self.hysteresis {
2619            return None;
2620        }
2621        match obs.declared {
2622            QosOrdering::GlobalFifo => {
2623                if obs.current_mode == OrderingMode::Unordered {
2624                    Some(OrderingMode::MergeByStamp)
2625                } else {
2626                    None
2627                }
2628            }
2629            QosOrdering::PerProducer => {
2630                match self.auto_order_threshold {
2631                    Some(threshold) => {
2632                        if obs.current_mode == OrderingMode::Unordered
2633                            && obs.inversions_per_sec > threshold
2634                        {
2635                            Some(OrderingMode::MergeByStamp)
2636                        } else {
2637                            None
2638                        }
2639                    }
2640                    None => {
2641                        if obs.current_mode != OrderingMode::Unordered {
2642                            Some(OrderingMode::Unordered)
2643                        } else {
2644                            None
2645                        }
2646                    }
2647                }
2648            }
2649        }
2650    }
2651}
2652
2653/// Background scanner thread that drives shape morphs on an
2654/// [`AdaptiveRing`] from a [`RingShapePolicy`].
2655///
2656/// `spawn` starts the thread; `shutdown` stops it. The thread
2657/// scans every `scan_interval`, builds a [`PolicyObservation`],
2658/// asks the policy, and calls [`AdaptiveRing::morph_to`] on
2659/// `Some(new_shape)` responses.
2660pub struct AdaptiveRingSidecar {
2661    handle: Option<std::thread::JoinHandle<()>>,
2662    stop: Arc<std::sync::atomic::AtomicBool>,
2663    morphs_triggered: Arc<std::sync::atomic::AtomicU64>,
2664    ordering_flips: Arc<std::sync::atomic::AtomicU64>,
2665}
2666
2667impl AdaptiveRingSidecar {
2668    /// Spawn a sidecar thread that morphs `ring` according to
2669    /// `policy` decisions sampled every `scan_interval`.
2670    pub fn spawn<P: RingShapePolicy>(
2671        ring: Arc<AdaptiveRing>,
2672        policy: P,
2673        scan_interval: std::time::Duration,
2674    ) -> Self {
2675        Self::spawn_gated(
2676            ring,
2677            policy,
2678            scan_interval,
2679            crate::policy_gate::GateConfig::default(),
2680        )
2681    }
2682
2683    /// As [`spawn`](Self::spawn) with a confidence gate between
2684    /// the shape policy's recommendation and the morph. Disabled
2685    /// (the default config) reproduces `spawn` exactly.
2686    pub fn spawn_gated<P: RingShapePolicy>(
2687        ring: Arc<AdaptiveRing>,
2688        policy: P,
2689        scan_interval: std::time::Duration,
2690        gate_cfg: crate::policy_gate::GateConfig,
2691    ) -> Self {
2692        let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
2693        let morphs_triggered = Arc::new(std::sync::atomic::AtomicU64::new(0));
2694
2695        let stop_c = stop.clone();
2696        let morphs_c = morphs_triggered.clone();
2697        let handle = std::thread::spawn(move || {
2698            let mut last_morph = std::time::Instant::now();
2699            let mut gate = crate::policy_gate::ConfidenceGate::new(gate_cfg);
2700            while !stop_c.load(Ordering::Acquire) {
2701                let obs = PolicyObservation {
2702                    active_producers: ring.active_producers(),
2703                    active_consumers: ring.active_consumers(),
2704                    current_shape: ring.current_shape(),
2705                    since_last_morph: last_morph.elapsed(),
2706                    stamped: ring.is_stamped(),
2707                };
2708                // Policy-driven morphs go through the internal morph:
2709                // a sidecar IS an automatic driver, so it must not
2710                // pin the shape the way an explicit morph_to does.
2711                if let Some(new_shape) = gate
2712                    .observe(policy.decide(&obs).map(|s| ring.contract_filtered_shape(s)))
2713                    && ring.morph_shape(new_shape).is_ok()
2714                {
2715                    last_morph = std::time::Instant::now();
2716                    morphs_c.fetch_add(1, Ordering::Relaxed);
2717                }
2718                std::thread::sleep(scan_interval);
2719            }
2720        });
2721
2722        Self {
2723            handle: Some(handle),
2724            stop,
2725            morphs_triggered,
2726            ordering_flips: Arc::new(std::sync::atomic::AtomicU64::new(0)),
2727        }
2728    }
2729
2730    /// Spawn a sidecar that consults BOTH axes every scan tick: the
2731    /// shape policy (peer counts + the QoS ordering declaration, via
2732    /// [`QosRingShapePolicy`] or any custom [`RingShapePolicy`]) and
2733    /// the ordering policy (declaration + observed inversion rate).
2734    ///
2735    /// Per tick, on a stamped ring the sidecar additionally:
2736    /// - computes inversions/sec from the shared counter's delta,
2737    /// - ticks the drainer-lease epoch so a dead merge drainer
2738    ///   becomes preemptible after [`DRAINER_GRACE_EPOCHS`] scans,
2739    /// - applies the ordering policy's decision via
2740    ///   `set_ordering_mode` (counted in
2741    ///   [`ordering_flips`](Self::ordering_flips)).
2742    ///
2743    /// The shape axis is UNGATED by default: capacity-class morphs
2744    /// are cheap to reverse (the warm-backing path makes them
2745    /// microsecond-scale), so tracking load faithfully beats
2746    /// deliberating. The ordering AUTO-arm is GATED by default: the
2747    /// inversion-rate-driven `Unordered -> MergeByStamp` flip is
2748    /// one-way (merged pops read zero inversions, so there is no
2749    /// symmetric signal to walk it back), and a one-way decision
2750    /// taken on a single noisy scan is unrecoverable. The gate
2751    /// makes the auto-arm demand sustained inversions before it
2752    /// commits. Explicit caller declarations (`GlobalFifo` arm,
2753    /// declaration withdrawal) are NOT noise and fire immediately -
2754    /// only the auto-detected arm is deliberated.
2755    ///
2756    /// `spawn_with_qos_gated` overrides both axes with one explicit
2757    /// config (disabled reproduces the fully-ungated behavior).
2758    pub fn spawn_with_qos<P: RingShapePolicy, O: OrderingPolicy>(
2759        ring: Arc<AdaptiveRing>,
2760        shape_policy: P,
2761        ordering_policy: O,
2762        qos: Arc<crate::qos_policy::QosPolicy>,
2763        scan_interval: std::time::Duration,
2764    ) -> Self {
2765        Self::spawn_with_qos_core(
2766            ring,
2767            shape_policy,
2768            ordering_policy,
2769            qos,
2770            scan_interval,
2771            crate::policy_gate::GateConfig::default(),
2772            crate::policy_gate::GateConfig::enabled_with_arity(2),
2773        )
2774    }
2775
2776    /// As [`spawn_with_qos`](Self::spawn_with_qos) with confidence
2777    /// gates on BOTH axes set from one explicit config - a shape
2778    /// gate and an ordering-auto-arm gate (each accumulates its own
2779    /// conviction; a peer-count change shocks both). `GateConfig::default()`
2780    /// (disabled) reproduces the fully-ungated sidecar; an enabled
2781    /// config gates the shape morph AND the ordering auto-arm.
2782    /// Explicit ordering declarations always fire immediately
2783    /// regardless of config - the gate governs the auto-detected
2784    /// arm only.
2785    pub fn spawn_with_qos_gated<P: RingShapePolicy, O: OrderingPolicy>(
2786        ring: Arc<AdaptiveRing>,
2787        shape_policy: P,
2788        ordering_policy: O,
2789        qos: Arc<crate::qos_policy::QosPolicy>,
2790        scan_interval: std::time::Duration,
2791        gate_cfg: crate::policy_gate::GateConfig,
2792    ) -> Self {
2793        Self::spawn_with_qos_core(
2794            ring,
2795            shape_policy,
2796            ordering_policy,
2797            qos,
2798            scan_interval,
2799            gate_cfg,
2800            gate_cfg,
2801        )
2802    }
2803
2804    fn spawn_with_qos_core<P: RingShapePolicy, O: OrderingPolicy>(
2805        ring: Arc<AdaptiveRing>,
2806        shape_policy: P,
2807        ordering_policy: O,
2808        qos: Arc<crate::qos_policy::QosPolicy>,
2809        scan_interval: std::time::Duration,
2810        shape_gate_cfg: crate::policy_gate::GateConfig,
2811        order_gate_cfg: crate::policy_gate::GateConfig,
2812    ) -> Self {
2813        let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
2814        let morphs_triggered = Arc::new(std::sync::atomic::AtomicU64::new(0));
2815        let ordering_flips = Arc::new(std::sync::atomic::AtomicU64::new(0));
2816
2817        let stop_c = stop.clone();
2818        let morphs_c = morphs_triggered.clone();
2819        let flips_c = ordering_flips.clone();
2820        let handle = std::thread::spawn(move || {
2821            let mut last_morph = std::time::Instant::now();
2822            let mut last_flip = std::time::Instant::now();
2823            let mut last_inversions = ring.inversions();
2824            let mut last_scan = std::time::Instant::now();
2825            let mut shape_gate = crate::policy_gate::ConfidenceGate::new(shape_gate_cfg);
2826            let mut order_gate = crate::policy_gate::ConfidenceGate::new(order_gate_cfg);
2827            let mut last_peers = (0usize, 0usize);
2828            let mut first_scan = true;
2829            while !stop_c.load(Ordering::Acquire) {
2830                let obs = PolicyObservation {
2831                    active_producers: ring.active_producers(),
2832                    active_consumers: ring.active_consumers(),
2833                    current_shape: ring.current_shape(),
2834                    since_last_morph: last_morph.elapsed(),
2835                    stamped: ring.is_stamped(),
2836                };
2837                let peers = (obs.active_producers, obs.active_consumers);
2838                if !first_scan && peers != last_peers {
2839                    shape_gate.shock();
2840                    order_gate.shock();
2841                }
2842                last_peers = peers;
2843                first_scan = false;
2844
2845                if let Some(new_shape) = shape_gate
2846                    .observe(shape_policy.decide(&obs).map(|s| ring.contract_filtered_shape(s)))
2847                    && ring.morph_shape(new_shape).is_ok()
2848                {
2849                    last_morph = std::time::Instant::now();
2850                    morphs_c.fetch_add(1, Ordering::Relaxed);
2851                }
2852
2853                if let Some(current_mode) = ring.ordering_mode() {
2854                    ring.tick_drainer_epoch().ok();
2855
2856                    let now_inversions = ring.inversions();
2857                    let elapsed = last_scan.elapsed().as_secs_f64().max(1e-9);
2858                    let rate = now_inversions
2859                        .saturating_sub(last_inversions) as f64 / elapsed;
2860                    last_inversions = now_inversions;
2861                    last_scan = std::time::Instant::now();
2862
2863                    let declared = qos.ordering();
2864                    let ord_obs = OrderingPolicyObservation {
2865                        inversions_per_sec: rate,
2866                        current_mode,
2867                        declared,
2868                        active_producers: obs.active_producers,
2869                        active_consumers: obs.active_consumers,
2870                        since_last_change: last_flip.elapsed(),
2871                    };
2872                    let decision = ordering_policy
2873                        .decide(&ord_obs)
2874                        .filter(|m| *m != current_mode);
2875
2876                    // The auto-arm is the one-way, noise-prone
2877                    // decision: a `PerProducer` declaration (no
2878                    // global-order intent) that the inversion rate
2879                    // nonetheless pushes to `MergeByStamp`. That is
2880                    // the only ordering decision the gate governs.
2881                    // An explicit `GlobalFifo` arm and any disarm
2882                    // are caller intent, not noise - they bypass the
2883                    // gate and fire immediately.
2884                    let is_auto_arm = declared == QosOrdering::PerProducer
2885                        && decision == Some(OrderingMode::MergeByStamp);
2886                    let gated = if is_auto_arm {
2887                        order_gate.observe(decision)
2888                    } else {
2889                        decision
2890                    };
2891                    if let Some(new_mode) = gated
2892                        && ring.set_ordering_mode(new_mode).is_ok()
2893                    {
2894                        last_flip = std::time::Instant::now();
2895                        flips_c.fetch_add(1, Ordering::Relaxed);
2896                    }
2897                }
2898                std::thread::sleep(scan_interval);
2899            }
2900        });
2901
2902        Self {
2903            handle: Some(handle),
2904            stop,
2905            morphs_triggered,
2906            ordering_flips,
2907        }
2908    }
2909
2910    /// Number of successful morph_to calls the sidecar has issued
2911    /// since spawn.
2912    pub fn morphs_triggered(&self) -> u64 {
2913        self.morphs_triggered.load(Ordering::Acquire)
2914    }
2915
2916    /// Number of ordering-mode flips this sidecar has issued since
2917    /// spawn (always 0 for [`spawn`](Self::spawn)).
2918    pub fn ordering_flips(&self) -> u64 {
2919        self.ordering_flips.load(Ordering::Acquire)
2920    }
2921
2922    /// Stop the scanner thread and join it.
2923    pub fn shutdown(mut self) {
2924        self.stop.store(true, Ordering::Release);
2925        if let Some(h) = self.handle.take() {
2926            h.join().ok();
2927        }
2928    }
2929}
2930
2931impl Drop for AdaptiveRingSidecar {
2932    fn drop(&mut self) {
2933        self.stop.store(true, Ordering::Release);
2934        if let Some(h) = self.handle.take() {
2935            h.join().ok();
2936        }
2937    }
2938}
2939
2940#[cfg(test)]
2941mod tests {
2942    use super::*;
2943
2944    #[test]
2945    fn create_starts_in_spsc_shape() {
2946        let ring = AdaptiveRing::create_anon(4, 4, 64).unwrap();
2947        assert_eq!(ring.current_shape(), RingShape::Spsc);
2948        assert_eq!(ring.pin_generation(), 0);
2949    }
2950
2951    #[test]
2952    fn adaptive_dispatch_round_trip_each_shape() {
2953        for shape in [RingShape::Spsc, RingShape::Mpsc, RingShape::Mpmc, RingShape::Vyukov] {
2954            let ring = AdaptiveRing::create_anon(4, 4, 64).unwrap();
2955            ring.shape_tag.store(shape as u8, Ordering::Release);
2956            // 56 = ADAPTIVE_VYUKOV_PAYLOAD_BYTES, the smaller of the two
2957            // backings' slot sizes (Vyukov's 8B per-slot sequence eats
2958            // 8 of the 64B slot; Lamport gets the full 64B).
2959            let payload = [0xCDu8; 56];
2960            ring.try_send(0, &payload).unwrap();
2961            let mut out = [0u8; ADAPTIVE_SPSC_PAYLOAD_BYTES];
2962            let n = ring.try_recv(0, &mut out).unwrap();
2963            assert!(n > 0, "shape {:?} delivered zero bytes", shape);
2964            assert_eq!(&out[..payload.len()], &payload[..]);
2965        }
2966    }
2967
2968    #[test]
2969    fn pin_captures_shape_and_generation() {
2970        let ring = AdaptiveRing::create_anon(4, 4, 64).unwrap();
2971        let pinned = ring.pin_current_shape();
2972        assert_eq!(pinned.shape(), RingShape::Spsc);
2973        assert!(pinned.is_still_valid());
2974        // Re-pin: still valid because no morph happened.
2975        let pinned = ring.pin_current_shape();
2976        assert!(pinned.is_still_valid());
2977    }
2978
2979    #[test]
2980    fn morph_invalidates_outstanding_pin() {
2981        let ring = AdaptiveRing::create_anon(4, 4, 64).unwrap();
2982        let pinned = ring.pin_current_shape();
2983        assert!(pinned.is_still_valid());
2984
2985        ring.morph_to(RingShape::Mpsc).unwrap();
2986        assert!(!pinned.is_still_valid(),
2987                "pin must invalidate after morph_to");
2988        assert_eq!(ring.current_shape(), RingShape::Mpsc);
2989    }
2990
2991    #[test]
2992    fn morph_to_same_shape_is_no_op() {
2993        let ring = AdaptiveRing::create_anon(4, 4, 64).unwrap();
2994        let gen_before = ring.pin_generation();
2995        ring.morph_to(RingShape::Spsc).unwrap();
2996        let gen_after = ring.pin_generation();
2997        assert_eq!(gen_before, gen_after,
2998                   "morph_to(same shape) must not bump pin_generation");
2999    }
3000
3001    #[test]
3002    fn morph_preserves_in_flight_items_via_stale_walk() {
3003        let ring = AdaptiveRing::create_anon(4, 4, 64).unwrap();
3004
3005        // Push 3 items via the SPSC shape.
3006        for i in 0..3u32 {
3007            let mut buf = [0u8; ADAPTIVE_SPSC_PAYLOAD_BYTES];
3008            buf[..4].copy_from_slice(&i.to_le_bytes());
3009            ring.try_send(0, &buf).unwrap();
3010        }
3011
3012        // Morph to MPSC: no data moves; the SPSC backing becomes
3013        // the stale backing and the pop path drains it first.
3014        ring.morph_to(RingShape::Mpsc).unwrap();
3015        assert_eq!(ring.current_shape(), RingShape::Mpsc);
3016        assert_eq!(ring.approx_len(), 3,
3017                   "the stale backlog must stay visible through approx_len");
3018
3019        // New traffic lands in the new shape while the backlog is
3020        // still pending; the stale walk delivers old-before-new.
3021        let mut buf = [0u8; ADAPTIVE_SPSC_PAYLOAD_BYTES];
3022        buf[..4].copy_from_slice(&99u32.to_le_bytes());
3023        ring.try_send(0, &buf).unwrap();
3024
3025        let mut seen = Vec::new();
3026        let mut out = [0u8; ADAPTIVE_SPSC_PAYLOAD_BYTES];
3027        while ring.try_recv(0, &mut out).is_ok() {
3028            seen.push(u32::from_le_bytes(out[..4].try_into().unwrap()));
3029        }
3030        assert_eq!(seen, vec![0u32, 1, 2, 99],
3031                   "stale backlog must drain before post-morph items");
3032        assert!(ring.is_empty());
3033    }
3034
3035    #[test]
3036    fn frame_round_trip_all_shapes() {
3037        use crate::frame_ring::FrameClass;
3038        for shape in [RingShape::Spsc, RingShape::Mpsc, RingShape::Mpmc, RingShape::Vyukov] {
3039            let ring = AdaptiveRing::create_anon(4, 4, 64).unwrap();
3040            if shape != RingShape::Spsc {
3041                ring.morph_to(shape).unwrap();
3042            }
3043            let small = b"small inline payload".to_vec();
3044            let large = vec![0xABu8; 4000];
3045            assert_eq!(ring.send_frame(0, &small).unwrap(), FrameClass::Inline,
3046                       "{shape:?} small should inline");
3047            assert_eq!(ring.send_frame(0, &large).unwrap(), FrameClass::Offset,
3048                       "{shape:?} large should offset");
3049            let mut out = Vec::new();
3050            assert_eq!(ring.recv_frame(0, &mut out).unwrap(), FrameClass::Inline);
3051            assert_eq!(out, small, "{shape:?} small round-trip");
3052            assert_eq!(ring.recv_frame(0, &mut out).unwrap(), FrameClass::Offset);
3053            assert_eq!(out, large, "{shape:?} large round-trip");
3054        }
3055    }
3056
3057    #[test]
3058    fn frame_survives_morph() {
3059        let ring = AdaptiveRing::create_anon(4, 4, 64).unwrap();
3060        // SPSC: one inline, one offset.
3061        ring.send_frame(0, b"pre-morph small").unwrap();
3062        ring.send_frame(0, &vec![1u8; 3000]).unwrap();
3063        // Morph to MPSC: the SPSC backing becomes stale and drains
3064        // first; the frame descriptors and region blocks are
3065        // shape-independent so the records survive the morph intact.
3066        ring.morph_to(RingShape::Mpsc).unwrap();
3067        ring.send_frame(0, b"post-morph small").unwrap();
3068        ring.send_frame(0, &vec![2u8; 3000]).unwrap();
3069        let mut out = Vec::new();
3070        ring.recv_frame(0, &mut out).unwrap();
3071        assert_eq!(out, b"pre-morph small");
3072        ring.recv_frame(0, &mut out).unwrap();
3073        assert_eq!(out, vec![1u8; 3000]);
3074        ring.recv_frame(0, &mut out).unwrap();
3075        assert_eq!(out, b"post-morph small");
3076        ring.recv_frame(0, &mut out).unwrap();
3077        assert_eq!(out, vec![2u8; 3000]);
3078    }
3079
3080    #[test]
3081    fn frame_override_and_limits() {
3082        use crate::frame_ring::{FrameClass, LayoutHint};
3083        let ring = AdaptiveRing::create_anon(2, 2, 64).unwrap();
3084        let mut out = Vec::new();
3085        // ForceOffset spills a small payload to the region.
3086        assert_eq!(ring.send_frame_as(0, b"tiny", LayoutHint::ForceOffset).unwrap(),
3087                   FrameClass::Offset);
3088        assert_eq!(ring.recv_frame(0, &mut out).unwrap(), FrameClass::Offset);
3089        assert_eq!(out, b"tiny");
3090        // ForceInline rejects an over-budget payload.
3091        let big = vec![0u8; AdaptiveRing::FRAME_INLINE_BUDGET + 1];
3092        assert_eq!(ring.send_frame_as(0, &big, LayoutHint::ForceInline).unwrap_err(),
3093                   RingError::PayloadTooLarge);
3094        // Auto inlines exactly at the budget.
3095        let at = vec![7u8; AdaptiveRing::FRAME_INLINE_BUDGET];
3096        assert_eq!(ring.send_frame(0, &at).unwrap(), FrameClass::Inline);
3097        ring.recv_frame(0, &mut out).unwrap();
3098        assert_eq!(out, at);
3099    }
3100
3101    #[test]
3102    fn frame_rejected_on_stamped_ring() {
3103        // Frames and ordering stamps both claim the slot head, so the
3104        // frame path is refused on a stamped ring.
3105        let ring = AdaptiveRing::create_anon(2, 2, 64)
3106            .unwrap()
3107            .with_ordering_stamps()
3108            .unwrap();
3109        assert_eq!(ring.send_frame(0, b"x").unwrap_err(), RingError::LayoutMismatch);
3110        let mut out = Vec::new();
3111        assert_eq!(ring.recv_frame(0, &mut out).unwrap_err(), RingError::LayoutMismatch);
3112    }
3113
3114    #[test]
3115    fn frame_vyukov_two_thread_mixed_size() {
3116        use std::sync::Arc;
3117        use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering as AtOrd};
3118        use std::thread;
3119
3120        const PER: u32 = 5_000;
3121        const PRODUCERS: u32 = 2;
3122        let total = (PER * PRODUCERS) as usize;
3123
3124        // Vyukov is the true-MPMC shape (one SharedRing, per-slot
3125        // sequence CAS), safe for many producers AND many consumers
3126        // with no partitioning. This exercises the shared payload
3127        // region under concurrent alloc (producers) and free
3128        // (consumers) at once.
3129        let ring = Arc::new(AdaptiveRing::create_anon(2, 2, 256).unwrap());
3130        ring.morph_to(RingShape::Vyukov).unwrap();
3131        // Each item carries its global id so a consumer can verify the
3132        // record regardless of which consumer drained it.
3133        let seen: Arc<Vec<AtomicBool>> =
3134            Arc::new((0..total).map(|_| AtomicBool::new(false)).collect());
3135        let received = Arc::new(AtomicUsize::new(0));
3136
3137        let mut prods = Vec::new();
3138        for p in 0..PRODUCERS {
3139            let ring = ring.clone();
3140            prods.push(thread::spawn(move || {
3141                for i in 0..PER {
3142                    let id = p * PER + i;
3143                    let len = (id as usize % 200) + 4; // 4..203, crosses the budget
3144                    let mut payload = vec![0u8; len];
3145                    payload[0..4].copy_from_slice(&id.to_le_bytes());
3146                    for (k, b) in payload.iter_mut().enumerate().skip(4) {
3147                        *b = id.wrapping_add(k as u32) as u8;
3148                    }
3149                    while ring.send_frame(p as usize, &payload).is_err() {
3150                        std::hint::spin_loop();
3151                    }
3152                }
3153            }));
3154        }
3155
3156        let mut cons = Vec::new();
3157        for c in 0..2usize {
3158            let ring = ring.clone();
3159            let seen = seen.clone();
3160            let received = received.clone();
3161            cons.push(thread::spawn(move || {
3162                let mut out = Vec::new();
3163                while received.load(AtOrd::Acquire) < total {
3164                    if ring.recv_frame(c, &mut out).is_ok() {
3165                        let id = u32::from_le_bytes(out[0..4].try_into().unwrap());
3166                        let len = (id as usize % 200) + 4;
3167                        assert_eq!(out.len(), len, "id {id} length");
3168                        for (k, b) in out.iter().enumerate().skip(4) {
3169                            assert_eq!(*b, id.wrapping_add(k as u32) as u8,
3170                                       "id {id} byte {k}");
3171                        }
3172                        let already = seen[id as usize].swap(true, AtOrd::AcqRel);
3173                        assert!(!already, "id {id} delivered twice");
3174                        received.fetch_add(1, AtOrd::AcqRel);
3175                    } else {
3176                        std::hint::spin_loop();
3177                    }
3178                }
3179            }));
3180        }
3181
3182        for p in prods { p.join().unwrap(); }
3183        for c in cons { c.join().unwrap(); }
3184        assert_eq!(received.load(AtOrd::Acquire), total);
3185        assert!(seen.iter().all(|b| b.load(AtOrd::Acquire)),
3186                "every id delivered exactly once");
3187    }
3188
3189    #[test]
3190    fn second_morph_blocked_until_stale_backlog_drains() {
3191        let ring = AdaptiveRing::create_anon(4, 4, 64).unwrap();
3192        ring.try_send(0, &[7u8; 8]).unwrap();
3193        ring.morph_to(RingShape::Mpsc).unwrap();
3194
3195        // The SPSC backlog has not drained; another morph must wait.
3196        assert_eq!(ring.morph_to(RingShape::Mpmc).unwrap_err(),
3197                   RingError::StaleBacklog);
3198
3199        let mut out = [0u8; ADAPTIVE_SPSC_PAYLOAD_BYTES];
3200        ring.try_recv(0, &mut out).unwrap();
3201        // Drained: the next morph proceeds.
3202        ring.morph_to(RingShape::Mpmc).unwrap();
3203        assert_eq!(ring.current_shape(), RingShape::Mpmc);
3204    }
3205
3206    #[test]
3207    fn register_producer_grows_past_hint_and_recycles_slots() {
3208        let ring = AdaptiveRing::create_anon(3, 1, 64).unwrap();
3209        let id0 = ring.register_producer().unwrap();
3210        let id1 = ring.register_producer().unwrap();
3211        let id2 = ring.register_producer().unwrap();
3212        assert_eq!((id0, id1, id2), (0, 1, 2));
3213
3214        // Past the construction hint the ring GROWS instead of
3215        // erroring: a 4th producer gets slot 3 and a live backing.
3216        let id3 = ring.register_producer().unwrap();
3217        assert_eq!(id3, 3);
3218        assert_eq!(ring.published_producers(), 4);
3219        ring.try_send(id3, &7u64.to_le_bytes()).unwrap();
3220        let mut out = [0u8; 64];
3221        // 4P/0C: no consumer registered, shape stays wherever the
3222        // counts left it; the adaptive pop still drains slot 3's
3223        // backing via the current shape + stale walk.
3224        let _c = ring.register_consumer().unwrap();
3225        let n = ring.try_recv(0, &mut out).unwrap();
3226        assert!(n >= 8, "popped record too short: {n}");
3227        assert_eq!(u64::from_le_bytes(out[..8].try_into().unwrap()), 7);
3228
3229        // Unregister frees the SLOT (bitmap claim): the next register
3230        // reuses id 1 without colliding with the still-live 2 and 3.
3231        ring.unregister_producer(id1);
3232        assert_eq!(ring.register_producer().unwrap(), 1);
3233
3234        // Errors exist ONLY under a caller-declared contract pin.
3235        let pinned = AdaptiveRing::create_anon(2, 1, 64)
3236            .unwrap()
3237            .with_contract(crate::ring_contract::RingContract::from_counts(2, 1));
3238        pinned.register_producer().unwrap();
3239        pinned.register_producer().unwrap();
3240        assert_eq!(pinned.register_producer().unwrap_err(),
3241                   AdaptiveError::TooManyProducers);
3242    }
3243
3244    #[test]
3245    fn default_policy_target_shape_per_peer_count() {
3246        // Idle either side -> no target.
3247        assert_eq!(DefaultRingShapePolicy::target_shape(0, 1), None);
3248        assert_eq!(DefaultRingShapePolicy::target_shape(1, 0), None);
3249        assert_eq!(DefaultRingShapePolicy::target_shape(0, 0), None);
3250        // 1P/1C -> SPSC
3251        assert_eq!(DefaultRingShapePolicy::target_shape(1, 1), Some(RingShape::Spsc));
3252        // NP/1C -> MPSC
3253        assert_eq!(DefaultRingShapePolicy::target_shape(2, 1), Some(RingShape::Mpsc));
3254        assert_eq!(DefaultRingShapePolicy::target_shape(8, 1), Some(RingShape::Mpsc));
3255        // */NC (NC >= 2) -> MPMC
3256        assert_eq!(DefaultRingShapePolicy::target_shape(1, 2), Some(RingShape::Mpmc));
3257        assert_eq!(DefaultRingShapePolicy::target_shape(4, 4), Some(RingShape::Mpmc));
3258    }
3259
3260    #[test]
3261    fn default_policy_returns_none_during_hysteresis() {
3262        let policy = DefaultRingShapePolicy {
3263            hysteresis: std::time::Duration::from_secs(1),
3264        };
3265        let obs = PolicyObservation {
3266            active_producers: 4,
3267            active_consumers: 4,
3268            current_shape: RingShape::Spsc,
3269            since_last_morph: std::time::Duration::from_millis(50),
3270            stamped: false,
3271        };
3272        // Target would be MPMC, but hysteresis says wait.
3273        assert_eq!(policy.decide(&obs), None);
3274    }
3275
3276    #[test]
3277    fn default_policy_returns_target_after_hysteresis() {
3278        let policy = DefaultRingShapePolicy {
3279            hysteresis: std::time::Duration::from_millis(10),
3280        };
3281        let obs = PolicyObservation {
3282            active_producers: 4,
3283            active_consumers: 4,
3284            current_shape: RingShape::Spsc,
3285            since_last_morph: std::time::Duration::from_secs(1),
3286            stamped: false,
3287        };
3288        assert_eq!(policy.decide(&obs), Some(RingShape::Mpmc));
3289    }
3290
3291    #[test]
3292    fn default_policy_returns_none_when_target_equals_current() {
3293        let policy = DefaultRingShapePolicy::default();
3294        let obs = PolicyObservation {
3295            active_producers: 1,
3296            active_consumers: 1,
3297            current_shape: RingShape::Spsc,
3298            since_last_morph: std::time::Duration::from_secs(1),
3299            stamped: false,
3300        };
3301        assert_eq!(policy.decide(&obs), None);
3302    }
3303
3304    #[test]
3305    fn shape_tracks_peer_counts_and_sidecar_stays_idle() {
3306        let ring = Arc::new(AdaptiveRing::create_anon(4, 4, 64).unwrap());
3307        let policy = DefaultRingShapePolicy {
3308            hysteresis: std::time::Duration::from_millis(5),
3309        };
3310        let sidecar = AdaptiveRingSidecar::spawn(
3311            ring.clone(),
3312            policy,
3313            std::time::Duration::from_millis(10),
3314        );
3315
3316        // Register 1P+1C -> SPSC (already the initial shape).
3317        let _p0 = ring.register_producer().unwrap();
3318        let _c0 = ring.register_consumer().unwrap();
3319        assert_eq!(ring.current_shape(), RingShape::Spsc);
3320
3321        // The register path itself morphs SYNCHRONOUSLY - no scan
3322        // interval to wait out, no sidecar required.
3323        let _p1 = ring.register_producer().unwrap();
3324        assert_eq!(ring.current_shape(), RingShape::Mpsc,
3325                   "2nd producer registration must morph to MPSC immediately");
3326
3327        let _c1 = ring.register_consumer().unwrap();
3328        assert_eq!(ring.current_shape(), RingShape::Mpmc,
3329                   "2nd consumer registration must morph to MPMC immediately");
3330
3331        // The sidecar observed a ring whose shape already tracked its
3332        // counts at every scan: it never had a correction to make.
3333        std::thread::sleep(std::time::Duration::from_millis(60));
3334        assert_eq!(sidecar.morphs_triggered(), 0,
3335                   "register-path morphs left the sidecar nothing to do");
3336
3337        // Leaves shrink the shape too: back down to 1P/1C -> SPSC
3338        // (the stale walk drains the composed backings; empty here).
3339        ring.unregister_consumer(1);
3340        ring.unregister_producer(1);
3341        assert_eq!(ring.current_shape(), RingShape::Spsc,
3342                   "unregister must morph back down automatically");
3343
3344        sidecar.shutdown();
3345    }
3346
3347    #[test]
3348    fn pinned_native_paths_match_adaptive_paths() {
3349        let ring = AdaptiveRing::create_anon(4, 4, 64).unwrap();
3350        let pinned = ring.pin_current_shape();
3351        assert_eq!(pinned.shape(), RingShape::Spsc);
3352
3353        let payload = [0xAAu8; ADAPTIVE_SPSC_PAYLOAD_BYTES];
3354        pinned.spsc_try_push(&payload).unwrap();
3355        let mut out = [0u8; ADAPTIVE_SPSC_PAYLOAD_BYTES];
3356        let n = pinned.spsc_try_pop(&mut out).unwrap();
3357        assert_eq!(n, ADAPTIVE_SPSC_PAYLOAD_BYTES);
3358        assert_eq!(out, payload);
3359        assert!(pinned.is_still_valid());
3360    }
3361
3362    // ===============================================================
3363    // Ordering-axis tests
3364    // ===============================================================
3365
3366    fn stamped_anon(
3367        max_producers: usize,
3368        max_consumers: usize,
3369        kind: StampKind,
3370    ) -> AdaptiveRing {
3371        AdaptiveRing::create_anon(max_producers, max_consumers, 64)
3372            .unwrap()
3373            .with_ordering_stamps_kind(kind)
3374            .unwrap()
3375    }
3376
3377    #[test]
3378    fn stamped_round_trip_strips_stamp_and_caps_payload() {
3379        let ring = stamped_anon(2, 1, StampKind::SharedCounter);
3380        assert!(ring.is_stamped());
3381        assert_eq!(ring.stamp_kind(), Some(StampKind::SharedCounter));
3382        assert_eq!(ring.ordering_mode(), Some(OrderingMode::Unordered));
3383
3384        // 57 bytes exceed the stamped cap.
3385        let too_big = [0u8; STAMPED_PAYLOAD_BYTES + 1];
3386        assert_eq!(ring.try_send(0, &too_big).unwrap_err(),
3387                   RingError::PayloadTooLarge);
3388
3389        let payload = [0xC3u8; STAMPED_PAYLOAD_BYTES];
3390        ring.try_send(0, &payload).unwrap();
3391        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
3392        let n = ring.try_recv(0, &mut out).unwrap();
3393        assert_eq!(n, STAMPED_PAYLOAD_BYTES,
3394                   "stamped recv returns payload bytes only");
3395        assert_eq!(out, payload, "the stamp must be stripped, not leak into the payload");
3396    }
3397
3398    #[test]
3399    fn unstamped_ring_rejects_ordering_calls() {
3400        let ring = AdaptiveRing::create_anon(2, 1, 64).unwrap();
3401        assert!(!ring.is_stamped());
3402        assert_eq!(ring.inversions(), 0);
3403        assert_eq!(ring.set_ordering_mode(OrderingMode::MergeByStamp).unwrap_err(),
3404                   RingError::NotStamped);
3405        assert_eq!(ring.refresh_watermark(0).unwrap_err(), RingError::NotStamped);
3406        let pinned = ring.pin_current_shape();
3407        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
3408        assert_eq!(pinned.ordered_try_pop(0, &mut out).unwrap_err(),
3409                   RingError::NotStamped);
3410        assert_eq!(pinned.stamped_try_push(0, &[1u8; 8]).unwrap_err(),
3411                   RingError::NotStamped);
3412    }
3413
3414    #[test]
3415    fn stamped_ring_rejects_vyukov_morph_and_vyukov_ring_rejects_stamps() {
3416        let ring = stamped_anon(2, 1, StampKind::Monotonic);
3417        assert_eq!(ring.morph_to(RingShape::Vyukov).unwrap_err(),
3418                   RingError::LayoutMismatch);
3419
3420        let vyukov_first = AdaptiveRing::create_anon(2, 1, 64).unwrap();
3421        vyukov_first.morph_to(RingShape::Vyukov).unwrap();
3422        assert!(matches!(
3423            vyukov_first.with_ordering_stamps(),
3424            Err(RingError::LayoutMismatch)
3425        ));
3426    }
3427
3428    #[test]
3429    fn synthetic_interleave_fires_inversion_counter() {
3430        let ring = stamped_anon(2, 1, StampKind::SharedCounter);
3431        ring.morph_to(RingShape::Mpsc).unwrap();
3432
3433        // Producer 1 pushes FIRST (older stamp lands in ring 1),
3434        // then producer 0 (newer stamp in ring 0). The round-robin
3435        // drain starts at ring 0, so the consumer pops newer-then-
3436        // older: exactly one cross-producer inversion.
3437        ring.try_send(1, &1u64.to_le_bytes()).unwrap();
3438        ring.try_send(0, &2u64.to_le_bytes()).unwrap();
3439
3440        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
3441        ring.try_recv(0, &mut out).unwrap();
3442        assert_eq!(ring.inversions(), 0, "first pop has no predecessor");
3443        ring.try_recv(0, &mut out).unwrap();
3444        assert_eq!(ring.inversions(), 1,
3445                   "older-after-newer must count as one inversion");
3446    }
3447
3448    #[test]
3449    fn merge_mode_delivers_global_stamp_order() {
3450        let ring = stamped_anon(4, 1, StampKind::SharedCounter);
3451        ring.morph_to(RingShape::Mpsc).unwrap();
3452        ring.set_ordering_mode(OrderingMode::MergeByStamp).unwrap();
3453
3454        // Interleave 32 items across 4 producers in a single thread:
3455        // counter stamps make the push order the global order.
3456        for i in 0..32u64 {
3457            let producer = (i % 4) as usize;
3458            ring.try_send(producer, &i.to_le_bytes()).unwrap();
3459        }
3460
3461        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
3462        for expected in 0..32u64 {
3463            let n = ring.try_recv(0, &mut out).unwrap();
3464            assert_eq!(n, STAMPED_PAYLOAD_BYTES);
3465            let got = u64::from_le_bytes(out[..8].try_into().unwrap());
3466            assert_eq!(got, expected,
3467                       "merge pop must deliver global push order");
3468        }
3469        assert_eq!(ring.try_recv(0, &mut out).unwrap_err(), RingError::Empty);
3470        assert_eq!(ring.inversions(), 0,
3471                   "merged pops must observe zero inversions");
3472    }
3473
3474    #[test]
3475    fn flag_flip_orders_backlog_retroactively_without_loss() {
3476        let ring = stamped_anon(2, 1, StampKind::SharedCounter);
3477        ring.morph_to(RingShape::Mpsc).unwrap();
3478
3479        // Backlog pushed UNDER Unordered, interleaved so the
3480        // round-robin drain would invert.
3481        for i in 0..16u64 {
3482            let producer = ((i + 1) % 2) as usize;
3483            ring.try_send(producer, &i.to_le_bytes()).unwrap();
3484        }
3485
3486        // Pop two items unordered; the second is an inversion on
3487        // this interleave (ring 0 holds the odd/newer stamps).
3488        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
3489        let mut popped = Vec::new();
3490        for _ in 0..2 {
3491            ring.try_recv(0, &mut out).unwrap();
3492            popped.push(u64::from_le_bytes(out[..8].try_into().unwrap()));
3493        }
3494        let inversions_before_flip = ring.inversions();
3495        assert!(inversions_before_flip > 0,
3496                "unordered interleave must show inversions before the flip");
3497
3498        // The ordered switch: one store, no drain, retroactive over
3499        // the 14-item backlog because the stamps were already there.
3500        ring.set_ordering_mode(OrderingMode::MergeByStamp).unwrap();
3501        let mut merged = Vec::new();
3502        while let Ok(_n) = ring.try_recv(0, &mut out) {
3503            merged.push(u64::from_le_bytes(out[..8].try_into().unwrap()));
3504        }
3505
3506        // Zero loss across the transition...
3507        let mut all = popped.clone();
3508        all.extend(&merged);
3509        all.sort_unstable();
3510        assert_eq!(all, (0..16u64).collect::<Vec<_>>(),
3511                   "no item may be lost across the mode flip");
3512        // ...and the post-flip stream is globally ordered (strictly
3513        // increasing payload sequence = strictly increasing stamps).
3514        for pair in merged.windows(2) {
3515            assert!(pair[0] < pair[1],
3516                    "post-flip pops must be globally ordered: {merged:?}");
3517        }
3518        assert_eq!(ring.inversions(), inversions_before_flip,
3519                   "the flip itself and merged pops must add zero inversions");
3520    }
3521
3522    #[test]
3523    fn merge_strict_blocks_on_in_flight_stamp_then_releases() {
3524        let ring = stamped_anon(2, 1, StampKind::SharedCounter);
3525        ring.morph_to(RingShape::Mpsc).unwrap();
3526        ring.set_ordering_mode(OrderingMode::MergeStrict).unwrap();
3527        let region = ring.ordering_region().unwrap();
3528
3529        // Producer 1 stamps but stalls before pushing (the
3530        // stamp-to-publish window): issued advances, watermark
3531        // does not.
3532        let stalled_stamp = region.next_stamp(1);
3533        // Producer 0 stamps later and publishes.
3534        ring.try_send(0, &42u64.to_le_bytes()).unwrap();
3535
3536        // In-flight gate: producer 0's visible item must NOT
3537        // release while producer 1 holds a smaller in-flight stamp.
3538        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
3539        assert_eq!(ring.try_recv(0, &mut out).unwrap_err(), RingError::Empty,
3540                   "strict merge must hold the candidate while a smaller stamp is in flight");
3541
3542        // The stalled push resolves as Full-equivalent: the
3543        // watermark advances to the issued stamp ("this will never
3544        // publish"), clearing the in-flight gate. The strict
3545        // watermark gate still holds the candidate (producer 1's
3546        // empty ring has not vouched past the candidate's stamp)...
3547        region.publish_watermark(1, stalled_stamp);
3548        assert_eq!(ring.try_recv(0, &mut out).unwrap_err(), RingError::Empty,
3549                   "strict watermark gate must hold until the silent producer vouches");
3550        // ...until the idle producer heartbeats its watermark past
3551        // the candidate.
3552        ring.refresh_watermark(1).unwrap();
3553        let n = ring.try_recv(0, &mut out).unwrap();
3554        assert_eq!(n, STAMPED_PAYLOAD_BYTES);
3555        assert_eq!(u64::from_le_bytes(out[..8].try_into().unwrap()), 42);
3556    }
3557
3558    #[test]
3559    fn merge_by_stamp_in_flight_gate_blocks_descheduled_producer() {
3560        // The WSL-discovered case: a producer reserves/stamps, then
3561        // stalls (preemption) before publishing. MergeByStamp must
3562        // hold any larger candidate until the publish lands - a
3563        // fixed freshness window cannot bound a deschedule.
3564        let ring = stamped_anon(2, 1, StampKind::SharedCounter);
3565        ring.morph_to(RingShape::Mpsc).unwrap();
3566        ring.set_ordering_mode(OrderingMode::MergeByStamp).unwrap();
3567        let region = ring.ordering_region().unwrap();
3568
3569        let stalled = region.next_stamp(1); // stamped, never pushed
3570        ring.try_send(0, &9u64.to_le_bytes()).unwrap();
3571
3572        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
3573        assert_eq!(ring.try_recv(0, &mut out).unwrap_err(), RingError::Empty,
3574                   "MergeByStamp must gate on in-flight stamps too");
3575        region.publish_watermark(1, stalled); // the stall resolves
3576        let n = ring.try_recv(0, &mut out).unwrap();
3577        assert_eq!(n, STAMPED_PAYLOAD_BYTES);
3578        assert_eq!(u64::from_le_bytes(out[..8].try_into().unwrap()), 9);
3579    }
3580
3581    #[test]
3582    fn merge_strict_retired_producer_stops_gating() {
3583        let ring = stamped_anon(2, 1, StampKind::SharedCounter);
3584        ring.morph_to(RingShape::Mpsc).unwrap();
3585        ring.set_ordering_mode(OrderingMode::MergeStrict).unwrap();
3586
3587        // Producer 1 pushes once (its slot is in-use), the item is
3588        // consumed, and the producer goes silent with an old
3589        // watermark.
3590        ring.try_send(1, &1u64.to_le_bytes()).unwrap();
3591        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
3592        ring.try_recv(0, &mut out).unwrap();
3593
3594        // Producer 0's newer item is gated on producer 1's silence.
3595        ring.try_send(0, &2u64.to_le_bytes()).unwrap();
3596        assert_eq!(ring.try_recv(0, &mut out).unwrap_err(), RingError::Empty,
3597                   "strict couples release to the slowest in-use producer");
3598
3599        // Clean exit: retirement saturates the slot's watermark and
3600        // the candidate releases - permanently, no heartbeat needed.
3601        ring.retire_producer(1).unwrap();
3602        let n = ring.try_recv(0, &mut out).unwrap();
3603        assert_eq!(n, STAMPED_PAYLOAD_BYTES);
3604        assert_eq!(u64::from_le_bytes(out[..8].try_into().unwrap()), 2);
3605    }
3606
3607    #[test]
3608    fn multi_consumer_merge_enforces_single_drainer() {
3609        let ring = stamped_anon(2, 2, StampKind::SharedCounter);
3610        ring.morph_to(RingShape::Mpmc).unwrap();
3611        ring.set_ordering_mode(OrderingMode::MergeByStamp).unwrap();
3612
3613        for i in 0..4u64 {
3614            ring.try_send((i % 2) as usize, &i.to_le_bytes()).unwrap();
3615        }
3616
3617        // Consumer 0 pops first and thereby auto-acquires the lease.
3618        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
3619        ring.try_recv(0, &mut out).unwrap();
3620        // Consumer 1 is locked out while consumer 0 holds the lease.
3621        assert_eq!(ring.try_recv(1, &mut out).unwrap_err(),
3622                   RingError::NotDrainer);
3623        // Voluntary release hands the drain over.
3624        assert!(ring.release_drainer(0).unwrap());
3625        let n = ring.try_recv(1, &mut out).unwrap();
3626        assert_eq!(n, STAMPED_PAYLOAD_BYTES);
3627        assert_eq!(u64::from_le_bytes(out[..8].try_into().unwrap()), 1,
3628                   "the new drainer continues in global stamp order");
3629        // And consumer 0 is now locked out in turn.
3630        assert_eq!(ring.try_recv(0, &mut out).unwrap_err(),
3631                   RingError::NotDrainer);
3632    }
3633
3634    #[test]
3635    fn mode_flip_does_not_invalidate_pins() {
3636        let ring = stamped_anon(2, 1, StampKind::SharedCounter);
3637        ring.morph_to(RingShape::Mpsc).unwrap();
3638        let pinned = ring.pin_current_shape();
3639        assert!(pinned.is_still_valid());
3640
3641        // Interleaved stamped pushes through the pin.
3642        pinned.stamped_try_push(1, &1u64.to_le_bytes()).unwrap();
3643        pinned.stamped_try_push(0, &2u64.to_le_bytes()).unwrap();
3644
3645        // Flip the merge flag under the live pin: the pin survives
3646        // (no generation bump) and the pinned pop consults the mode
3647        // atom, so the next pops come out merged.
3648        ring.set_ordering_mode(OrderingMode::MergeByStamp).unwrap();
3649        assert!(pinned.is_still_valid(),
3650                "ordering-mode flips must not invalidate pins");
3651
3652        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
3653        pinned.ordered_try_pop(0, &mut out).unwrap();
3654        assert_eq!(u64::from_le_bytes(out[..8].try_into().unwrap()), 1,
3655                   "pinned merge pop must deliver stamp order");
3656        pinned.ordered_try_pop(0, &mut out).unwrap();
3657        assert_eq!(u64::from_le_bytes(out[..8].try_into().unwrap()), 2);
3658    }
3659
3660    #[test]
3661    fn stamped_items_survive_shape_morphs() {
3662        let ring = stamped_anon(2, 1, StampKind::SharedCounter);
3663        for i in 0..3u64 {
3664            ring.try_send(0, &i.to_le_bytes()).unwrap();
3665        }
3666        ring.morph_to(RingShape::Mpsc).unwrap();
3667        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
3668        let mut got = Vec::new();
3669        while ring.try_recv(0, &mut out).is_ok() {
3670            got.push(u64::from_le_bytes(out[..8].try_into().unwrap()));
3671        }
3672        got.sort_unstable();
3673        assert_eq!(got, vec![0, 1, 2],
3674                   "stamped slots must transfer intact across shape morphs");
3675    }
3676
3677    #[test]
3678    fn stamped_file_ring_open_adopts_creator_kind_and_shares_mode() {
3679        let mut prefix = std::env::temp_dir();
3680        prefix.push(format!(
3681            "subetha_stamped_open_{}_{}",
3682            std::process::id(),
3683            std::time::SystemTime::now()
3684                .duration_since(std::time::UNIX_EPOCH)
3685                .map(|d| d.as_nanos()).unwrap_or(0),
3686        ));
3687
3688        let creator = AdaptiveRing::create(&prefix, 2, 1, 64)
3689            .unwrap()
3690            .with_ordering_stamps_kind(StampKind::SharedCounter)
3691            .unwrap();
3692        creator.set_ordering_mode(OrderingMode::MergeByStamp).unwrap();
3693        creator.try_send(0, &7u64.to_le_bytes()).unwrap();
3694
3695        let opener = AdaptiveRing::open(&prefix, 2, 1, 64)
3696            .unwrap()
3697            .with_ordering_stamps()
3698            .unwrap();
3699        assert_eq!(opener.stamp_kind(), Some(StampKind::SharedCounter),
3700                   "opener must adopt the creator's stamp kind");
3701        assert_eq!(opener.ordering_mode(), Some(OrderingMode::MergeByStamp),
3702                   "the mode flag must be cross-process (region-resident)");
3703        // Explicit mismatched kind on open is a layout error.
3704        assert!(matches!(
3705            AdaptiveRing::open(&prefix, 2, 1, 64)
3706                .unwrap()
3707                .with_ordering_stamps_kind(StampKind::Monotonic),
3708            Err(RingError::LayoutMismatch)
3709        ));
3710
3711        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
3712        let n = opener.try_recv(0, &mut out).unwrap();
3713        assert_eq!(n, STAMPED_PAYLOAD_BYTES);
3714        assert_eq!(u64::from_le_bytes(out[..8].try_into().unwrap()), 7);
3715
3716        drop(creator);
3717        drop(opener);
3718        for suffix in [".spsc.bin", ".mpsc.0.bin", ".mpsc.1.bin",
3719                       ".mpmc.0.bin", ".mpmc.1.bin", ".vyukov.bin",
3720                       ".ordering.bin"] {
3721            let mut p = prefix.as_os_str().to_owned();
3722            p.push(suffix);
3723            std::fs::remove_file(std::path::PathBuf::from(p)).ok();
3724        }
3725    }
3726
3727    #[test]
3728    fn offset_frame_crosses_a_shared_file_backing() {
3729        // The frame payload region must live on the ring's own locale, not
3730        // a private anon mmap: a large (offset-class) frame sent through a
3731        // creator handle must be recoverable byte-exact through an opener
3732        // handle to the SAME file set. A private region would keep inline
3733        // frames working and silently drop offset ones, so this is the
3734        // case that distinguishes them.
3735        let mut prefix = std::env::temp_dir();
3736        prefix.push(format!(
3737            "subetha_offset_frame_{}_{}",
3738            std::process::id(),
3739            std::time::SystemTime::now()
3740                .duration_since(std::time::UNIX_EPOCH)
3741                .map(|d| d.as_nanos()).unwrap_or(0),
3742        ));
3743
3744        let creator = AdaptiveRing::create(&prefix, 1, 1, 64).unwrap();
3745        let opener = AdaptiveRing::open(&prefix, 1, 1, 64).unwrap();
3746
3747        let small = vec![0xABu8; 20];    // inline (under the budget)
3748        let large = vec![0xCDu8; 5000];  // offset (spills to the region)
3749        assert_eq!(creator.send_frame(0, &small).unwrap(), FrameClass::Inline);
3750        assert_eq!(creator.send_frame(0, &large).unwrap(), FrameClass::Offset);
3751
3752        let mut out = Vec::new();
3753        assert_eq!(opener.recv_frame(0, &mut out).unwrap(), FrameClass::Inline);
3754        assert_eq!(out, small, "inline frame must cross the boundary");
3755        assert_eq!(opener.recv_frame(0, &mut out).unwrap(), FrameClass::Offset,
3756                   "offset frame must cross via the shared payload region");
3757        assert_eq!(out, large,
3758                   "offset payload must be byte-exact across the boundary");
3759
3760        drop(creator);
3761        drop(opener);
3762        for suffix in [".spsc.bin", ".mpsc.0.bin", ".mpmc.0.bin",
3763                       ".vyukov.bin", ".peers.bin", ".frames.bin"] {
3764            let mut p = prefix.as_os_str().to_owned();
3765            p.push(suffix);
3766            std::fs::remove_file(std::path::PathBuf::from(p)).ok();
3767        }
3768    }
3769
3770    #[test]
3771    fn qos_shape_policy_decision_matrix() {
3772        let qos = Arc::new(crate::qos_policy::QosPolicy::default());
3773        let policy = QosRingShapePolicy {
3774            qos: qos.clone(),
3775            hysteresis: std::time::Duration::from_millis(0),
3776        };
3777        let obs = |shape, stamped| PolicyObservation {
3778            active_producers: 2,
3779            active_consumers: 1,
3780            current_shape: shape,
3781            since_last_morph: std::time::Duration::from_secs(1),
3782            stamped,
3783        };
3784
3785        // PerProducer: counts-based default (2P/1C -> MPSC).
3786        assert_eq!(policy.decide(&obs(RingShape::Spsc, false)),
3787                   Some(RingShape::Mpsc));
3788        assert_eq!(policy.decide(&obs(RingShape::Mpsc, false)), None);
3789
3790        // GlobalFifo + unstamped: Vyukov morph.
3791        qos.set_ordering(crate::qos_policy::Ordering::GlobalFifo);
3792        assert_eq!(policy.decide(&obs(RingShape::Mpsc, false)),
3793                   Some(RingShape::Vyukov));
3794        assert_eq!(policy.decide(&obs(RingShape::Vyukov, false)), None);
3795
3796        // GlobalFifo + stamped: shape stays counts-based composed
3797        // (the merge flag serves the declaration).
3798        assert_eq!(policy.decide(&obs(RingShape::Spsc, true)),
3799                   Some(RingShape::Mpsc));
3800        assert_eq!(policy.decide(&obs(RingShape::Mpsc, true)), None);
3801
3802        // Withdrawing the declaration walks Vyukov back.
3803        qos.set_ordering(crate::qos_policy::Ordering::PerProducer);
3804        assert_eq!(policy.decide(&obs(RingShape::Vyukov, false)),
3805                   Some(RingShape::Mpsc));
3806
3807        // Hysteresis suppresses everything.
3808        let cold = QosRingShapePolicy {
3809            qos: qos.clone(),
3810            hysteresis: std::time::Duration::from_secs(10),
3811        };
3812        let mut o = obs(RingShape::Spsc, false);
3813        o.since_last_morph = std::time::Duration::from_millis(1);
3814        assert_eq!(cold.decide(&o), None);
3815    }
3816
3817    #[test]
3818    fn default_ordering_policy_decision_matrix() {
3819        let obs = |mode, declared, rate, since_ms| OrderingPolicyObservation {
3820            inversions_per_sec: rate,
3821            current_mode: mode,
3822            declared,
3823            active_producers: 2,
3824            active_consumers: 1,
3825            since_last_change: std::time::Duration::from_millis(since_ms),
3826        };
3827        let declarative = DefaultOrderingPolicy {
3828            hysteresis: std::time::Duration::from_millis(0),
3829            auto_order_threshold: None,
3830        };
3831        // GlobalFifo declaration arms the merge.
3832        assert_eq!(
3833            declarative.decide(&obs(
3834                OrderingMode::Unordered, QosOrdering::GlobalFifo, 0.0, 500)),
3835            Some(OrderingMode::MergeByStamp),
3836        );
3837        assert_eq!(
3838            declarative.decide(&obs(
3839                OrderingMode::MergeByStamp, QosOrdering::GlobalFifo, 0.0, 500)),
3840            None,
3841        );
3842        // Withdrawal disarms (no auto threshold).
3843        assert_eq!(
3844            declarative.decide(&obs(
3845                OrderingMode::MergeByStamp, QosOrdering::PerProducer, 0.0, 500)),
3846            Some(OrderingMode::Unordered),
3847        );
3848
3849        let auto = DefaultOrderingPolicy {
3850            hysteresis: std::time::Duration::from_millis(0),
3851            auto_order_threshold: Some(100.0),
3852        };
3853        // Below threshold: report-only.
3854        assert_eq!(
3855            auto.decide(&obs(
3856                OrderingMode::Unordered, QosOrdering::PerProducer, 50.0, 500)),
3857            None,
3858        );
3859        // Above threshold: pre-authorized arm.
3860        assert_eq!(
3861            auto.decide(&obs(
3862                OrderingMode::Unordered, QosOrdering::PerProducer, 250.0, 500)),
3863            Some(OrderingMode::MergeByStamp),
3864        );
3865        // Auto arm is one-way: PerProducer + armed + auto -> stay.
3866        assert_eq!(
3867            auto.decide(&obs(
3868                OrderingMode::MergeByStamp, QosOrdering::PerProducer, 0.0, 500)),
3869            None,
3870        );
3871
3872        // Hysteresis suppresses both paths.
3873        let cold = DefaultOrderingPolicy {
3874            hysteresis: std::time::Duration::from_secs(10),
3875            auto_order_threshold: Some(1.0),
3876        };
3877        assert_eq!(
3878            cold.decide(&obs(
3879                OrderingMode::Unordered, QosOrdering::GlobalFifo, 1e6, 1)),
3880            None,
3881        );
3882    }
3883
3884    #[test]
3885    fn sidecar_spawn_with_qos_flips_merge_flag_on_declaration() {
3886        let ring = Arc::new(stamped_anon(2, 1, StampKind::SharedCounter));
3887        ring.morph_to(RingShape::Mpsc).unwrap();
3888        let _p0 = ring.register_producer().unwrap();
3889        let _p1 = ring.register_producer().unwrap();
3890        let _c0 = ring.register_consumer().unwrap();
3891
3892        let qos = Arc::new(crate::qos_policy::QosPolicy::default());
3893        let sidecar = AdaptiveRingSidecar::spawn_with_qos(
3894            ring.clone(),
3895            QosRingShapePolicy {
3896                qos: qos.clone(),
3897                hysteresis: std::time::Duration::from_millis(5),
3898            },
3899            DefaultOrderingPolicy {
3900                hysteresis: std::time::Duration::from_millis(5),
3901                auto_order_threshold: None,
3902            },
3903            qos.clone(),
3904            std::time::Duration::from_millis(10),
3905        );
3906
3907        // Declare GlobalFifo: on this STAMPED ring the sidecar must
3908        // flip the merge flag, never morph to Vyukov.
3909        qos.set_ordering(crate::qos_policy::Ordering::GlobalFifo);
3910        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
3911        while std::time::Instant::now() < deadline
3912            && ring.ordering_mode() != Some(OrderingMode::MergeByStamp)
3913        {
3914            std::thread::sleep(std::time::Duration::from_millis(10));
3915        }
3916        assert_eq!(ring.ordering_mode(), Some(OrderingMode::MergeByStamp),
3917                   "sidecar must arm the merge flag on the GlobalFifo declaration");
3918        assert_eq!(ring.current_shape(), RingShape::Mpsc,
3919                   "stamped ring must stay composed (no Vyukov morph)");
3920        assert!(sidecar.ordering_flips() >= 1);
3921
3922        // Withdraw the declaration: the sidecar disarms.
3923        qos.set_ordering(crate::qos_policy::Ordering::PerProducer);
3924        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
3925        while std::time::Instant::now() < deadline
3926            && ring.ordering_mode() != Some(OrderingMode::Unordered)
3927        {
3928            std::thread::sleep(std::time::Duration::from_millis(10));
3929        }
3930        assert_eq!(ring.ordering_mode(), Some(OrderingMode::Unordered),
3931                   "sidecar must disarm when the declaration is withdrawn");
3932        sidecar.shutdown();
3933    }
3934
3935    #[test]
3936    fn default_sidecar_gates_auto_arm_but_still_opens_on_sustained_inversions() {
3937        // The default `spawn_with_qos` now enables the ordering
3938        // auto-arm gate. This proves the gate OPENS under genuinely
3939        // sustained inversions (a one-way arm that never opened
3940        // would be useless): two producers race in Unordered mode,
3941        // the consumer observes cross-producer inversions, the
3942        // auto threshold pre-authorizes, and the gate commits the
3943        // single MergeByStamp flip once conviction accrues.
3944        let ring = Arc::new(stamped_anon(2, 1, StampKind::SharedCounter));
3945        ring.morph_to(RingShape::Mpsc).unwrap();
3946        ring.register_producer().unwrap();
3947        ring.register_producer().unwrap();
3948        ring.register_consumer().unwrap();
3949        ring.set_ordering_mode(OrderingMode::Unordered).unwrap();
3950
3951        let qos = Arc::new(crate::qos_policy::QosPolicy::default());
3952        qos.set_ordering(crate::qos_policy::Ordering::PerProducer);
3953        let sidecar = AdaptiveRingSidecar::spawn_with_qos(
3954            ring.clone(),
3955            DefaultRingShapePolicy::default(),
3956            DefaultOrderingPolicy {
3957                hysteresis: std::time::Duration::from_millis(0),
3958                auto_order_threshold: Some(50.0),
3959            },
3960            qos.clone(),
3961            std::time::Duration::from_millis(5),
3962        );
3963
3964        let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
3965        let stop_c = stop.clone();
3966        let r = ring.clone();
3967        let consumer = std::thread::spawn(move || {
3968            let mut out = [0u8; 64];
3969            while !stop_c.load(Ordering::Acquire) {
3970                r.try_recv(0, &mut out).ok();
3971            }
3972        });
3973
3974        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(6);
3975        let mut seq = 0u64;
3976        while std::time::Instant::now() < deadline
3977            && ring.ordering_mode() != Some(OrderingMode::MergeByStamp)
3978        {
3979            ring.try_send(0, &seq.to_le_bytes()).ok();
3980            ring.try_send(1, &seq.to_le_bytes()).ok();
3981            seq += 1;
3982        }
3983        stop.store(true, Ordering::Release);
3984        consumer.join().unwrap();
3985
3986        assert_eq!(ring.ordering_mode(), Some(OrderingMode::MergeByStamp),
3987                   "the gated auto-arm must still commit under sustained inversions");
3988        assert_eq!(sidecar.ordering_flips(), 1,
3989                   "the one-way auto-arm fires exactly once");
3990        sidecar.shutdown();
3991    }
3992}