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