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.get_or_init(|| {
1564            Arc::new(
1565                FrameRegion::create_anon(block_size, block_count)
1566                    .expect("frame region create"),
1567            )
1568        });
1569        self
1570    }
1571
1572    fn frame_region(&self) -> &FrameRegion {
1573        self.frame_region.get_or_init(|| {
1574            let blocks = self.spsc.capacity().max(16);
1575            Arc::new(
1576                FrameRegion::create_anon(Self::FRAME_DEFAULT_BLOCK_SIZE, blocks)
1577                    .expect("frame region create"),
1578            )
1579        })
1580    }
1581
1582    /// Frame-path send: carries any payload size on whatever shape the
1583    /// ring is in. Records up to
1584    /// [`FRAME_INLINE_BUDGET`](Self::FRAME_INLINE_BUDGET) go inline in
1585    /// the ring slot; larger ones spill to the shared payload region
1586    /// and the slot carries the block index. Returns which path the
1587    /// record took. `producer_id` selects the backing ring for MPSC /
1588    /// MPMC exactly as [`try_send`](Self::try_send). The same call
1589    /// works at every shape because the descriptor rides the slot and
1590    /// the region is multi-producer / multi-consumer safe.
1591    ///
1592    /// Not available on stamped (ordering) rings - frames and stamps
1593    /// both claim the slot head, so they are mutually exclusive;
1594    /// returns [`RingError::LayoutMismatch`] there.
1595    pub fn send_frame(&self, producer_id: usize, payload: &[u8])
1596        -> Result<FrameClass, RingError>
1597    {
1598        self.send_frame_as(producer_id, payload, LayoutHint::Auto)
1599    }
1600
1601    /// [`send_frame`](Self::send_frame) with an explicit layout
1602    /// override ([`LayoutHint::ForceInline`] / [`LayoutHint::ForceOffset`]).
1603    pub fn send_frame_as(&self, producer_id: usize, payload: &[u8], hint: LayoutHint)
1604        -> Result<FrameClass, RingError>
1605    {
1606        if self.ordering.is_some() {
1607            return Err(RingError::LayoutMismatch);
1608        }
1609        let inline = match hint {
1610            LayoutHint::ForceInline => {
1611                if payload.len() > Self::FRAME_INLINE_BUDGET {
1612                    return Err(RingError::PayloadTooLarge);
1613                }
1614                true
1615            }
1616            LayoutHint::ForceOffset => false,
1617            LayoutHint::Auto => payload.len() <= Self::FRAME_INLINE_BUDGET,
1618        };
1619        let len = payload.len() as u32;
1620        if inline {
1621            // [class:u8][len:u32][payload bytes]; fits the 56-byte
1622            // Vyukov slot, so it fits every shape's slot.
1623            let mut buf = [0u8; PAYLOAD_BYTES];
1624            buf[0] = FrameClass::Inline as u8;
1625            buf[1..5].copy_from_slice(&len.to_le_bytes());
1626            buf[5..5 + payload.len()].copy_from_slice(payload);
1627            self.try_send(producer_id, &buf[..5 + payload.len()])?;
1628            Ok(FrameClass::Inline)
1629        } else {
1630            let region = self.frame_region();
1631            if payload.len() > region.block_size() {
1632                return Err(RingError::PayloadTooLarge);
1633            }
1634            let idx = region.alloc().ok_or(RingError::Full)?;
1635            region.write_block(idx, payload);
1636            // [class:u8][len:u32][block_idx:u32]
1637            let mut buf = [0u8; 9];
1638            buf[0] = FrameClass::Offset as u8;
1639            buf[1..5].copy_from_slice(&len.to_le_bytes());
1640            buf[5..9].copy_from_slice(&idx.to_le_bytes());
1641            match self.try_send(producer_id, &buf) {
1642                Ok(()) => Ok(FrameClass::Offset),
1643                Err(e) => {
1644                    // Descriptor push failed (ring full): return the
1645                    // block so it is not leaked.
1646                    region.free(idx);
1647                    Err(e)
1648                }
1649            }
1650        }
1651    }
1652
1653    /// Frame-path recv: counterpart to [`send_frame`](Self::send_frame).
1654    /// Clears `out` and
1655    /// fills it with the record's payload, transparently reading the
1656    /// payload region and freeing its block for offset records. Returns
1657    /// which path the record took. `consumer_id` selects the consumer
1658    /// partition for MPMC as [`try_recv`](Self::try_recv). Not available
1659    /// on stamped rings.
1660    pub fn recv_frame(&self, consumer_id: usize, out: &mut Vec<u8>)
1661        -> Result<FrameClass, RingError>
1662    {
1663        if self.ordering.is_some() {
1664            return Err(RingError::LayoutMismatch);
1665        }
1666        // SPSC_PAYLOAD_BYTES (64) holds any shape's slot.
1667        let mut slot = [0u8; SPSC_PAYLOAD_BYTES];
1668        self.try_recv(consumer_id, &mut slot)?;
1669        let len = u32::from_le_bytes([slot[1], slot[2], slot[3], slot[4]]) as usize;
1670        out.clear();
1671        if slot[0] == FrameClass::Inline as u8 {
1672            out.extend_from_slice(&slot[5..5 + len]);
1673            Ok(FrameClass::Inline)
1674        } else {
1675            let idx = u32::from_le_bytes([slot[5], slot[6], slot[7], slot[8]]);
1676            let region = self.frame_region();
1677            region.read_block_into(idx, len, out);
1678            region.free(idx);
1679            Ok(FrameClass::Offset)
1680        }
1681    }
1682
1683    /// As [`try_recv`](Self::try_recv) on a stamped ring, also
1684    /// returning the popped item's stamp. This is how consumers
1685    /// assert the ordering guarantee they paid for (monotone stamps
1686    /// under the merge modes) instead of trusting it. Returns
1687    /// [`RingError::NotStamped`] on unstamped rings.
1688    pub fn try_recv_with_stamp(
1689        &self,
1690        consumer_id: usize,
1691        out: &mut [u8],
1692    ) -> Result<(usize, u64), RingError> {
1693        let ord = self.ordering.as_ref().ok_or(RingError::NotStamped)?;
1694        let shape = RingShape::from_u8(self.shape_tag.load(Ordering::Acquire));
1695        self.ordered_recv_inner(ord, shape, consumer_id, out)
1696    }
1697
1698    /// Stamped push: issue the producer's next stamp, lay out
1699    /// `[stamp; 8][payload; <=56]` and push to the active Lamport
1700    /// backing. The watermark advances whether the push lands or
1701    /// returns `Full` - a stamp that failed to publish will never
1702    /// appear, so advancing keeps the MergeStrict in-flight gate
1703    /// live.
1704    fn stamped_send_inner(
1705        &self,
1706        ord: &OrderingState,
1707        shape: RingShape,
1708        producer_id: usize,
1709        payload: &[u8],
1710    ) -> Result<(), RingError> {
1711        if payload.len() > STAMPED_PAYLOAD_BYTES {
1712            return Err(RingError::PayloadTooLarge);
1713        }
1714        if producer_id >= ord.region.max_producers() {
1715            return Err(RingError::PayloadTooLarge);
1716        }
1717        let mpsc_guard;
1718        let mpmc_guard;
1719        let ring: &SpscRingCore = match shape {
1720            RingShape::Spsc => &self.spsc,
1721            RingShape::Mpsc => {
1722                mpsc_guard = self.mpsc.rings.load();
1723                mpsc_guard.get(producer_id).ok_or(RingError::PayloadTooLarge)?
1724            }
1725            RingShape::Mpmc => {
1726                mpmc_guard = self.mpmc.rings.load();
1727                mpmc_guard.get(producer_id).ok_or(RingError::PayloadTooLarge)?
1728            }
1729            // Stamped rings never run the Vyukov backing: the
1730            // stamped 64-byte slot layout does not fit its 56-byte
1731            // slots. morph_to rejects the transition, so this arm is
1732            // a defensive layout error, not a reachable path.
1733            RingShape::Vyukov => return Err(RingError::LayoutMismatch),
1734        };
1735        let stamp = ord.region.next_stamp(producer_id);
1736        let mut buf = [0u8; SPSC_PAYLOAD_BYTES];
1737        buf[..STAMP_BYTES].copy_from_slice(&stamp.to_le_bytes());
1738        buf[STAMP_BYTES..STAMP_BYTES + payload.len()].copy_from_slice(payload);
1739        let result = ring.try_push(&buf[..STAMP_BYTES + payload.len()]);
1740        ord.region.publish_watermark(producer_id, stamp);
1741        result
1742    }
1743
1744    /// Stamped pop: strip the stamp, run the inversion counter, and
1745    /// dispatch per the live ordering mode. Returns the payload
1746    /// length and the popped stamp.
1747    fn ordered_recv_inner(
1748        &self,
1749        ord: &OrderingState,
1750        shape: RingShape,
1751        consumer_id: usize,
1752        out: &mut [u8],
1753    ) -> Result<(usize, u64), RingError> {
1754        if consumer_id >= CONSUMER_SLOT_CEILING {
1755            return Err(RingError::PayloadTooLarge);
1756        }
1757        if out.len() < STAMPED_PAYLOAD_BYTES {
1758            return Err(RingError::PayloadTooLarge);
1759        }
1760        let mode = ord.region.mode();
1761        match mode {
1762            OrderingMode::Unordered => {
1763                let mut buf = [0u8; SPSC_PAYLOAD_BYTES];
1764                // Stale walk first (a stamped ring's backings are
1765                // all Lamport shapes, so the stale pop is the same
1766                // stamped slot layout).
1767                let popped = self.stale_shape()
1768                    .filter(|stale| *stale != shape)
1769                    .filter(|stale| Self::may_walk_stale(*stale, consumer_id))
1770                    .and_then(|stale| {
1771                        self.shape_pop(stale, consumer_id, &mut buf).ok()
1772                    });
1773                if popped.is_none() {
1774                    match shape {
1775                        RingShape::Spsc => self.spsc.try_pop(&mut buf),
1776                        RingShape::Mpsc => self.mpsc_pop(&mut buf),
1777                        RingShape::Mpmc => self.mpmc_pop(consumer_id, &mut buf),
1778                        RingShape::Vyukov => Err(RingError::LayoutMismatch),
1779                    }?;
1780                }
1781                let stamp = u64::from_le_bytes(
1782                    buf[..STAMP_BYTES].try_into().unwrap(),
1783                );
1784                self.note_stamp(ord, consumer_id, mode, stamp);
1785                out[..STAMPED_PAYLOAD_BYTES]
1786                    .copy_from_slice(&buf[STAMP_BYTES..]);
1787                Ok((STAMPED_PAYLOAD_BYTES, stamp))
1788            }
1789            OrderingMode::MergeByStamp | OrderingMode::MergeStrict => {
1790                // Always hold the single-drainer lease: consumers can
1791                // JOIN at runtime, so a static 1-consumer bypass would
1792                // leave a leaseless drainer racing the new joiner's
1793                // leased one. Per-pop verification must stay OFF the
1794                // stamp-hot header line (producers fetch_add it every
1795                // push; each extra consumer load of it costs a cache
1796                // transfer): one load of the quiet lease-generation
1797                // line, compared to a consumer-local cache, and only a
1798                // change (claim / takeover / release / epoch tick)
1799                // runs the full lease handshake.
1800                let seen = &ord.seen[consumer_id];
1801                let lease_gen_now = ord.region.lease_generation();
1802                if seen.lease_gen.load(Ordering::Relaxed) != lease_gen_now {
1803                    if !ord.region.try_acquire_drainer(
1804                        drainer_token(consumer_id),
1805                        DRAINER_GRACE_EPOCHS,
1806                    ) {
1807                        return Err(RingError::NotDrainer);
1808                    }
1809                    seen.lease_gen.store(lease_gen_now, Ordering::Relaxed);
1810                }
1811                // Stale walk: merge within the stale shape's rings
1812                // until that backlog drains, then merge the active
1813                // shape. Stale items predate active items (producers
1814                // switched at the tag flip), so stale-first keeps
1815                // global stamp order across the morph boundary.
1816                if let Some(stale) = self.stale_shape()
1817                    && stale != shape
1818                {
1819                    match self.merge_pop(ord, stale, consumer_id, mode, out) {
1820                        Ok(result) => return Ok(result),
1821                        Err(RingError::Empty) => {}
1822                        Err(e) => return Err(e),
1823                    }
1824                }
1825                self.merge_pop(ord, shape, consumer_id, mode, out)
1826            }
1827        }
1828    }
1829
1830
1831    /// K-way min-stamp merge over the active shape's ring heads:
1832    /// peek every non-empty ring, pick the minimum stamp, confirm
1833    /// exactly that slot, leave every other head unconsumed.
1834    ///
1835    /// Three release gates sit between the scan and the confirm:
1836    ///
1837    /// - **In-flight gate** (both merge modes): a producer whose
1838    ///   `issued` stamp (or reservation floor) is ahead of its
1839    ///   `watermark` holds exactly one stamp in its
1840    ///   reserve-stamp-push window; if it undercuts the candidate,
1841    ///   the pop returns `Empty` until the publish lands (or the
1842    ///   push's `Full` failure advances the watermark). This is the
1843    ///   only bound that survives producer descheduling - the
1844    ///   window stretches to scheduler quanta under preemption or
1845    ///   virtualization, far past any fixed time guard.
1846    /// - **Freshness guard** (time-based stamps, both merge modes,
1847    ///   only when at least one ring is empty): a candidate younger
1848    ///   than the guard window may be raced by a stamp a producer
1849    ///   has not even RESERVED yet (cross-core clock skew); the
1850    ///   merge re-peeks until the candidate ages out (bounded by
1851    ///   the guard, ~2us).
1852    /// - **Watermark gate** (`MergeStrict` only): every EMPTY
1853    ///   in-use ring's watermark must have reached the candidate,
1854    ///   closing the not-yet-reserved case with zero time-semantics
1855    ///   assumptions. This couples release latency to the slowest
1856    ///   producer: idle producers heartbeat via
1857    ///   [`refresh_watermark`](OrderingRegion::refresh_watermark)
1858    ///   and exiting producers call
1859    ///   [`retire_producer`](OrderingRegion::retire_producer), or
1860    ///   the strict consumer stalls on their silence by design.
1861    fn merge_pop(
1862        &self,
1863        ord: &OrderingState,
1864        shape: RingShape,
1865        consumer_id: usize,
1866        mode: OrderingMode,
1867        out: &mut [u8],
1868    ) -> Result<(usize, u64), RingError> {
1869        // Snapshot the composed arrays (they live behind an ArcSwap
1870        // for producer growth); the SPSC arm borrows directly.
1871        let mpsc_guard;
1872        let mpmc_guard;
1873        let rings: &[Arc<SpscRingCore>] = match shape {
1874            RingShape::Spsc => std::slice::from_ref(&self.spsc),
1875            RingShape::Mpsc => {
1876                mpsc_guard = self.mpsc.rings.load();
1877                mpsc_guard.as_slice()
1878            }
1879            RingShape::Mpmc => {
1880                mpmc_guard = self.mpmc.rings.load();
1881                mpmc_guard.as_slice()
1882            }
1883            RingShape::Vyukov => &[],
1884        };
1885        if rings.is_empty() {
1886            return Err(RingError::LayoutMismatch);
1887        }
1888        // The release gates cover every producer slot that has ever
1889        // stamped: the PUBLISHED slot count, not the region's
1890        // ceiling-sized line array (whose untouched tail would cost
1891        // thousands of loads per pop).
1892        let gate_lines = self.directory.published()
1893            .min(ord.region.max_producers());
1894        let kind = ord.region.stamp_kind();
1895        loop {
1896            // Scalar min scan: the per-ring peek atomics dominate
1897            // the cost of each pass, so the comparison work is not
1898            // the bottleneck at realistic producer counts.
1899            let mut best: Option<(usize, u64)> = None;
1900            let mut any_empty = false;
1901            for (i, ring) in rings.iter().enumerate() {
1902                match ring.peek_slot() {
1903                    Some(peek) => {
1904                        let s = u64::from_le_bytes(
1905                            peek[..STAMP_BYTES].try_into().unwrap(),
1906                        );
1907                        if best.is_none_or(|(_, bs)| s < bs) {
1908                            best = Some((i, s));
1909                        }
1910                    }
1911                    None => any_empty = true,
1912                }
1913            }
1914            let Some((idx, stamp)) = best else {
1915                return Err(RingError::Empty);
1916            };
1917
1918            for line in 0..gate_lines {
1919                if ord.region.in_flight_below(line, stamp) {
1920                    return Err(RingError::Empty);
1921                }
1922            }
1923            if mode == OrderingMode::MergeStrict {
1924                for line in 0..gate_lines {
1925                    // In-use slot (has ever stamped; retirement
1926                    // saturates the watermark so retired slots
1927                    // always pass) whose ring is empty: its
1928                    // watermark must have reached the candidate.
1929                    if ord.region.issued(line) != 0
1930                        && rings[line.min(rings.len() - 1)].approx_len() == 0
1931                        && ord.region.watermark(line) < stamp
1932                    {
1933                        return Err(RingError::Empty);
1934                    }
1935                }
1936            }
1937            if any_empty
1938                && let Some(guard) = kind.freshness_guard()
1939                && stamp_now(kind).wrapping_sub(stamp) < guard
1940            {
1941                std::hint::spin_loop();
1942                continue;
1943            }
1944
1945            let peek = rings[idx].peek_slot().expect(
1946                "single drainer holds the lease; a peeked head cannot vanish",
1947            );
1948            let confirmed_stamp = u64::from_le_bytes(
1949                peek[..STAMP_BYTES].try_into().unwrap(),
1950            );
1951            out[..STAMPED_PAYLOAD_BYTES].copy_from_slice(&peek[STAMP_BYTES..]);
1952            peek.confirm();
1953            self.note_stamp(ord, consumer_id, mode, confirmed_stamp);
1954            return Ok((STAMPED_PAYLOAD_BYTES, confirmed_stamp));
1955        }
1956    }
1957
1958    /// Per-consumer inversion accounting. A pop whose stamp
1959    /// undercuts the previous pop's stamp is one cross-producer
1960    /// inversion: the counter bumps in the shared header and an
1961    /// observation rides the sidecar ring. Mode transitions reset
1962    /// the baseline so the retroactive reordering of the backlog at
1963    /// an Off->On flip is not miscounted.
1964    fn note_stamp(
1965        &self,
1966        ord: &OrderingState,
1967        consumer_id: usize,
1968        mode: OrderingMode,
1969        stamp: u64,
1970    ) {
1971        let line = &ord.seen[consumer_id];
1972        if line.mode_tag.swap(mode as u32, Ordering::Relaxed) != mode as u32 {
1973            line.stamp.store(0, Ordering::Relaxed);
1974        }
1975        let last = line.stamp.load(Ordering::Relaxed);
1976        if stamp < last {
1977            ord.region.record_inversion();
1978            self.ring_sidecar
1979                .push_op(crate::sidecar_ops::ordering::OP_ORDER_INVERSION, 0);
1980        }
1981        line.stamp.store(stamp, Ordering::Relaxed);
1982    }
1983
1984    fn mpsc_pop(&self, out: &mut [u8]) -> Result<usize, RingError> {
1985        let rings = self.mpsc.rings.load();
1986        self.mpsc_pop_in(&rings, out)
1987    }
1988
1989    fn mpsc_pop_in(
1990        &self,
1991        rings: &[Arc<SpscRingCore>],
1992        out: &mut [u8],
1993    ) -> Result<usize, RingError> {
1994        let n = rings.len();
1995        if n == 0 {
1996            return Err(RingError::Empty);
1997        }
1998        let start = self.mpsc.next_drain.load(Ordering::Relaxed);
1999        for i in 0..n {
2000            let idx = (start + i) % n;
2001            if let Ok(bytes) = rings[idx].try_pop(out) {
2002                self.mpsc.next_drain.store((idx + 1) % n, Ordering::Relaxed);
2003                return Ok(bytes);
2004            }
2005        }
2006        Err(RingError::Empty)
2007    }
2008
2009    /// MPMC pop through the shared ownership table: this consumer
2010    /// drains exactly the rings whose owner entry names its slot
2011    /// (single-reader invariant), CAS-claims unowned rings on sight
2012    /// (so unregistered-consumer flows keep working), applies
2013    /// pending rebalance handoffs from its own scan (single-writer
2014    /// transfer), and - rate-limited - takes over rings whose owner
2015    /// process died.
2016    fn mpmc_pop(&self, consumer_id: usize, out: &mut [u8]) -> Result<usize, RingError> {
2017        let rings = self.mpmc.rings.load();
2018        self.mpmc_pop_in(&rings, consumer_id, out)
2019    }
2020
2021    fn mpmc_pop_in(
2022        &self,
2023        rings: &[Arc<SpscRingCore>],
2024        consumer_id: usize,
2025        out: &mut [u8],
2026    ) -> Result<usize, RingError> {
2027        let cursor_line = self.mpmc.consumer_cursors.get(consumer_id)
2028            .ok_or(RingError::PayloadTooLarge)?;
2029        let me = consumer_id as u16;
2030        let n = rings.len();
2031        if n == 0 {
2032            return Err(RingError::Empty);
2033        }
2034        let start = cursor_line.0.load(Ordering::Relaxed) % n;
2035        // First stuck ring (owned elsewhere, has items): the crash-
2036        // takeover candidate when the whole scan comes up empty.
2037        let mut stuck: Option<(usize, u16)> = None;
2038        for i in 0..n {
2039            let idx = (start + i) % n;
2040            let (owner, pending) = self.directory.ring_owner(idx);
2041            if owner == me {
2042                if pending != OWNER_NONE
2043                    && self.directory.apply_handoff(idx, me).is_some()
2044                {
2045                    continue; // handed off; not ours to drain anymore
2046                }
2047                if let Ok(bytes) = rings[idx].try_pop(out) {
2048                    cursor_line.0.store((idx + 1) % n, Ordering::Relaxed);
2049                    return Ok(bytes);
2050                }
2051            } else if owner == OWNER_NONE {
2052                if self.directory.try_claim_ring(idx, me)
2053                    && let Ok(bytes) = rings[idx].try_pop(out)
2054                {
2055                    cursor_line.0.store((idx + 1) % n, Ordering::Relaxed);
2056                    return Ok(bytes);
2057                }
2058            } else if stuck.is_none() && rings[idx].approx_len() > 0 {
2059                stuck = Some((idx, owner));
2060            }
2061        }
2062        // Crash takeover, rate-limited: the pid probe is a syscall,
2063        // so only every 1024th empty scan per consumer attempts it.
2064        if let Some((idx, owner)) = stuck {
2065            let probes = cursor_line.1.fetch_add(1, Ordering::Relaxed);
2066            if probes % 1024 == 1023
2067                && self.directory.try_takeover(idx, owner, me)
2068                && let Ok(bytes) = rings[idx].try_pop(out)
2069            {
2070                cursor_line.0.store((idx + 1) % n, Ordering::Relaxed);
2071                return Ok(bytes);
2072            }
2073        }
2074        Err(RingError::Empty)
2075    }
2076
2077    /// Pin the current shape and return a [`PinnedRing`] that
2078    /// exposes the matching backend at native speed. The composed
2079    /// arrays are captured at pin time (zero per-op indirection);
2080    /// producer growth bumps the pin generation, so pin holders see
2081    /// [`PinnedRing::is_still_valid`] `== false` and re-pin to pick
2082    /// up new backings.
2083    pub fn pin_current_shape(&self) -> PinnedRing<'_> {
2084        self.ensure_synced();
2085        let captured_gen = self.pin_generation.load(Ordering::Acquire);
2086        let shape = RingShape::from_u8(self.shape_tag.load(Ordering::Acquire));
2087        PinnedRing {
2088            parent: self,
2089            pinned_generation: captured_gen,
2090            shape,
2091            mpsc_rings: self.mpsc.rings.load_full(),
2092            mpmc_rings: self.mpmc.rings.load_full(),
2093            _not_sync: PhantomData,
2094        }
2095    }
2096
2097    /// Trigger a shape morph. NO data moves: the old shape's
2098    /// backing becomes the STALE backing, producers follow the new
2099    /// `shape_tag` immediately, and the consumer's pop path drains
2100    /// the stale backlog first (the stale walk) before reading from
2101    /// the new shape. This is what makes morphing safe under
2102    /// saturating traffic - there is no transfer to overflow the
2103    /// target's capacity and no second drainer racing the live
2104    /// consumer (each backing keeps exactly one reader).
2105    ///
2106    /// The stale marker stays set until the NEXT morph, which
2107    /// requires the backlog drained ([`RingError::StaleBacklog`]
2108    /// otherwise - the sidecar's scan loop simply retries). Keeping
2109    /// it set gives a producer whose push straddled the tag flip a
2110    /// wide grace window: its item lands in the old backing, which
2111    /// the consumer still walks.
2112    ///
2113    /// Bumps `pin_generation` so outstanding pins see
2114    /// `is_still_valid() == false` and re-acquire. Pinned NATIVE
2115    /// pops (`spsc_try_pop` etc.) are shape-direct and do not walk
2116    /// the stale backing; consumers that pop through pins across
2117    /// morphs use [`AdaptiveRing::try_recv`] or
2118    /// [`PinnedRing::ordered_try_pop`], which do.
2119    ///
2120    /// An explicit `morph_to` is a USER shape decision, so it pins
2121    /// the shape (suppresses the automatic count-driven reshape)
2122    /// until [`resume_auto_shape`](Self::resume_auto_shape).
2123    pub fn morph_to(&self, new_shape: RingShape) -> Result<(), RingError> {
2124        self.shape_auto.store(false, Ordering::Relaxed);
2125        self.morph_shape(new_shape)
2126    }
2127
2128    /// The morph mechanism, shared by the public (pinning)
2129    /// [`morph_to`](Self::morph_to), the automatic count-driven
2130    /// reshape, and policy sidecars.
2131    pub(crate) fn morph_shape(&self, new_shape: RingShape) -> Result<(), RingError> {
2132        let old_shape = RingShape::from_u8(self.shape_tag.load(Ordering::Acquire));
2133        if old_shape == new_shape {
2134            return Ok(());
2135        }
2136
2137        // A stamped ring never runs the Vyukov backing: the stamped
2138        // 64-byte slot layout ([stamp; 8][payload; 56]) does not fit
2139        // Vyukov's 56-byte slots. The GlobalFifo declaration on a
2140        // stamped ring is served by the merge flag
2141        // (set_ordering_mode) instead of this morph.
2142        if self.ordering.is_some() && new_shape == RingShape::Vyukov {
2143            return Err(RingError::LayoutMismatch);
2144        }
2145
2146        // One stale backing at a time: the prior morph's backlog
2147        // must be drained before another shape change.
2148        let prior_stale = self.stale_shape_tag.load(Ordering::Acquire);
2149        if prior_stale != STALE_NONE
2150            && !self.backing_is_empty(RingShape::from_u8(prior_stale))
2151        {
2152            return Err(RingError::StaleBacklog);
2153        }
2154
2155        // Bump the pin generation so existing pins see
2156        // is_still_valid() == false on their next check; then
2157        // publish old-as-stale before the new tag so a pop that
2158        // observes the new shape also sees the stale marker.
2159        self.pin_generation.fetch_add(1, Ordering::AcqRel);
2160        self.stale_shape_tag.store(old_shape as u8, Ordering::Release);
2161        self.shape_tag.store(new_shape as u8, Ordering::Release);
2162        Ok(())
2163    }
2164
2165    /// Whether one shape's backing holds no items right now.
2166    fn backing_is_empty(&self, shape: RingShape) -> bool {
2167        match shape {
2168            RingShape::Spsc => self.spsc.approx_len() == 0,
2169            RingShape::Mpsc => self.mpsc.rings.load().iter().all(|r| r.approx_len() == 0),
2170            RingShape::Mpmc => self.mpmc.rings.load().iter().all(|r| r.approx_len() == 0),
2171            RingShape::Vyukov => self.vyukov.approx_len() == 0,
2172        }
2173    }
2174
2175    /// The stale shape still draining after the last morph, if any.
2176    fn stale_shape(&self) -> Option<RingShape> {
2177        let tag = self.stale_shape_tag.load(Ordering::Acquire);
2178        if tag == STALE_NONE {
2179            None
2180        } else {
2181            Some(RingShape::from_u8(tag))
2182        }
2183    }
2184
2185    /// Whether `consumer_id` may drain a stale backing of `shape`.
2186    /// Single-reader backings (SPSC, MPSC) are walked by consumer 0
2187    /// only; the MPMC grid partitions per consumer and Vyukov pops
2188    /// are CAS-safe for any consumer.
2189    fn may_walk_stale(shape: RingShape, consumer_id: usize) -> bool {
2190        match shape {
2191            RingShape::Spsc | RingShape::Mpsc => consumer_id == 0,
2192            RingShape::Mpmc | RingShape::Vyukov => true,
2193        }
2194    }
2195
2196    /// Unstamped pop from one shape's backing.
2197    fn shape_pop(
2198        &self,
2199        shape: RingShape,
2200        consumer_id: usize,
2201        out: &mut [u8],
2202    ) -> Result<usize, RingError> {
2203        match shape {
2204            RingShape::Spsc => self.spsc.try_pop(out),
2205            RingShape::Mpsc => self.mpsc_pop(out),
2206            RingShape::Mpmc => self.mpmc_pop(consumer_id, out),
2207            RingShape::Vyukov => self.vyukov.try_pop(out),
2208        }
2209    }
2210}
2211
2212fn with_suffix(base: &std::path::Path, suffix: &str) -> std::path::PathBuf {
2213    let mut s = base.as_os_str().to_owned();
2214    s.push(suffix);
2215    std::path::PathBuf::from(s)
2216}
2217
2218/// Error type for AdaptiveRing registration / morph operations.
2219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2220pub enum AdaptiveError {
2221    /// `register_producer` refused: a caller-declared contract
2222    /// ceiling ([`AdaptiveRing::with_contract`]) or the substrate
2223    /// slot ceiling. Never returned by the unpinned default below
2224    /// the substrate ceiling - registration grows the ring instead.
2225    TooManyProducers,
2226    /// `register_consumer` refused: same two cases on the consumer
2227    /// axis.
2228    TooManyConsumers,
2229    /// Producer registration claimed a slot but creating / opening
2230    /// the grown backing failed (I/O); the slot was released.
2231    GrowthFailed,
2232}
2233
2234/// Handle pinned to one shape of the parent [`AdaptiveRing`].
2235/// Hot-path ops bypass the adaptive dispatch and call the native
2236/// backend directly. The pin holder periodically calls
2237/// [`is_still_valid`](Self::is_still_valid) to check whether a
2238/// morph has invalidated this pin; on `false` the caller releases
2239/// and re-acquires via [`AdaptiveRing::pin_current_shape`].
2240pub struct PinnedRing<'a> {
2241    parent: &'a AdaptiveRing,
2242    pinned_generation: u64,
2243    shape: RingShape,
2244    /// Composed arrays captured at pin time: pinned ops index these
2245    /// directly (native speed, no ArcSwap load per op). Producer
2246    /// growth invalidates the pin, so a re-pin picks up new rings.
2247    mpsc_rings: Arc<Vec<Arc<SpscRingCore>>>,
2248    mpmc_rings: Arc<Vec<Arc<SpscRingCore>>>,
2249    _not_sync: PhantomData<Cell<()>>,
2250}
2251
2252impl<'a> PinnedRing<'a> {
2253    /// Shape this pin was captured at.
2254    pub fn shape(&self) -> RingShape { self.shape }
2255
2256    /// Monitor-wait HINT for the consumer side of `shape`: an atom
2257    /// whose Release-store accompanies (or is) the next publish a
2258    /// pop is waiting for. Arm `crate::monitor_wait::monitor_wait_u64`
2259    /// on it instead of burning a raw spin loop - on Windows the
2260    /// scheduler deschedules and migrates pure spinners (measured
2261    /// 1.7-2.7 us one-way for a cross-process spin ping-pong that
2262    /// runs in ~100-300 ns under Linux/FreeBSD on comparable
2263    /// silicon), while a monitor-armed waiter wakes on the store
2264    /// itself.
2265    ///
2266    /// Contract: this is a HINT, not a wake guarantee - on the
2267    /// multi-line shapes (MPSC/MPMC) it covers producer line 0
2268    /// only, and on Vyukov it covers the slot at the CURRENT
2269    /// consumer position (recompute after each pop). Callers must
2270    /// keep their waits budget-bounded and re-poll, which
2271    /// `monitor_wait_u64`'s budget enforces.
2272    pub fn recv_signal(&self, shape: RingShape) -> &AtomicU64 {
2273        match shape {
2274            RingShape::Spsc => self.parent.spsc.head_signal(),
2275            RingShape::Mpsc => self.mpsc_rings[0].head_signal(),
2276            RingShape::Mpmc => self.mpmc_rings[0].head_signal(),
2277            RingShape::Vyukov => self.parent.vyukov.next_pop_signal(),
2278        }
2279    }
2280
2281    /// One Acquire load on the parent's `pin_generation`. Returns
2282    /// `true` while the pin is current; `false` if a morph has
2283    /// happened and the caller should release + re-acquire.
2284    pub fn is_still_valid(&self) -> bool {
2285        self.parent.pin_generation.load(Ordering::Acquire) == self.pinned_generation
2286    }
2287
2288    /// Native SPSC push. Caller assumes single-producer ownership
2289    /// and ensures pin validity is checked at meaningful intervals.
2290    pub fn spsc_try_push(&self, payload: &[u8]) -> Result<(), RingError> {
2291        self.parent.spsc.try_push(payload)
2292    }
2293
2294    /// Native SPSC pop.
2295    pub fn spsc_try_pop(&self, out: &mut [u8]) -> Result<usize, RingError> {
2296        self.parent.spsc.try_pop(out)
2297    }
2298
2299    /// MPSC push to a specific producer ring (captured at pin time).
2300    pub fn mpsc_try_push(&self, producer_id: usize, payload: &[u8]) -> Result<(), RingError> {
2301        let ring = self.mpsc_rings.get(producer_id)
2302            .ok_or(RingError::PayloadTooLarge)?;
2303        ring.try_push(payload)
2304    }
2305
2306    /// MPSC pop (round-robin across the producer rings captured at
2307    /// pin time; a grown producer set invalidates the pin).
2308    pub fn mpsc_try_pop(&self, out: &mut [u8]) -> Result<usize, RingError> {
2309        self.parent.mpsc_pop_in(&self.mpsc_rings, out)
2310    }
2311
2312    /// MPMC push to a specific producer ring (captured at pin time).
2313    pub fn mpmc_try_push(&self, producer_id: usize, payload: &[u8]) -> Result<(), RingError> {
2314        let ring = self.mpmc_rings.get(producer_id)
2315            .ok_or(RingError::PayloadTooLarge)?;
2316        ring.try_push(payload)
2317    }
2318
2319    /// MPMC pop for a specific consumer. Ownership is consulted live
2320    /// from the shared directory (correctness under consumer joins /
2321    /// leaves); the ring array is the pin-time capture.
2322    pub fn mpmc_try_pop(&self, consumer_id: usize, out: &mut [u8]) -> Result<usize, RingError> {
2323        self.parent.mpmc_pop_in(&self.mpmc_rings, consumer_id, out)
2324    }
2325
2326    /// Vyukov MPMC push.
2327    pub fn vyukov_try_push(&self, payload: &[u8]) -> Result<(), RingError> {
2328        self.parent.vyukov.try_push(payload)
2329    }
2330
2331    /// Vyukov MPMC pop.
2332    pub fn vyukov_try_pop(&self, out: &mut [u8]) -> Result<usize, RingError> {
2333        self.parent.vyukov.try_pop(out)
2334    }
2335
2336    /// Stamped push through the pinned shape: the producer's next
2337    /// stamp is prepended and the payload cap is
2338    /// [`STAMPED_PAYLOAD_BYTES`]. Requires a ring constructed with
2339    /// [`AdaptiveRing::with_ordering_stamps`]; returns
2340    /// [`RingError::NotStamped`] otherwise.
2341    pub fn stamped_try_push(
2342        &self,
2343        producer_id: usize,
2344        payload: &[u8],
2345    ) -> Result<(), RingError> {
2346        let ord = self.parent.ordering.as_ref().ok_or(RingError::NotStamped)?;
2347        self.parent.stamped_send_inner(ord, self.shape, producer_id, payload)
2348    }
2349
2350    /// Ordering-aware pop through the pinned shape. The pin stays
2351    /// valid across ordering-mode flips - this call reads the
2352    /// MMF-resident mode atom every time (one Acquire load, a plain
2353    /// MOV on x86 TSO) and dispatches accordingly: partition pop +
2354    /// inversion counter under `Unordered`, k-way min-stamp merge
2355    /// under `MergeByStamp` / `MergeStrict`. Returns payload bytes
2356    /// only (`Ok(56)`).
2357    pub fn ordered_try_pop(
2358        &self,
2359        consumer_id: usize,
2360        out: &mut [u8],
2361    ) -> Result<usize, RingError> {
2362        let ord = self.parent.ordering.as_ref().ok_or(RingError::NotStamped)?;
2363        self.parent
2364            .ordered_recv_inner(ord, self.shape, consumer_id, out)
2365            .map(|(n, _stamp)| n)
2366    }
2367
2368    /// As [`ordered_try_pop`](Self::ordered_try_pop), also returning
2369    /// the popped stamp so hot-loop consumers can assert the
2370    /// ordering guarantee they paid for.
2371    pub fn ordered_try_pop_with_stamp(
2372        &self,
2373        consumer_id: usize,
2374        out: &mut [u8],
2375    ) -> Result<(usize, u64), RingError> {
2376        let ord = self.parent.ordering.as_ref().ok_or(RingError::NotStamped)?;
2377        self.parent.ordered_recv_inner(ord, self.shape, consumer_id, out)
2378    }
2379}
2380
2381/// Zero-copy peek into AdaptiveRing's SPSC backing. Wraps the
2382/// underlying [`PeekedSlot`](crate::spsc_ring::PeekedSlot) so the
2383/// AdaptiveRing crate boundary owns the type. Same semantics:
2384/// derefs to `&[u8]`, call `confirm` to release the slot.
2385pub struct PeekedSpscSlot<'a> {
2386    inner: crate::spsc_ring::PeekedSlot<'a>,
2387}
2388
2389impl<'a> PeekedSpscSlot<'a> {
2390    pub fn as_slice(&self) -> &[u8] { self.inner.as_slice() }
2391    pub fn len(&self) -> usize { self.inner.len() }
2392    pub fn is_empty(&self) -> bool { self.inner.is_empty() }
2393    pub fn confirm(self) { self.inner.confirm() }
2394}
2395
2396impl<'a> std::ops::Deref for PeekedSpscSlot<'a> {
2397    type Target = [u8];
2398    fn deref(&self) -> &[u8] { &self.inner }
2399}
2400
2401/// SPSC payload size for the SPSC / MPSC / MPMC backings (Lamport
2402/// slot is 64B payload-only).
2403pub const ADAPTIVE_SPSC_PAYLOAD_BYTES: usize = SPSC_PAYLOAD_BYTES;
2404
2405/// Vyukov payload size for the Vyukov backing (56B; 8B is the
2406/// per-slot sequence atomic).
2407pub const ADAPTIVE_VYUKOV_PAYLOAD_BYTES: usize = PAYLOAD_BYTES;
2408
2409// ===================================================================
2410// Sidecar shape policy: automatic morphing based on peer-count
2411// observations.
2412// ===================================================================
2413
2414/// A snapshot of the ring's observable state passed to a policy
2415/// on every sidecar scan.
2416#[derive(Debug, Clone, Copy)]
2417pub struct PolicyObservation {
2418    pub active_producers: usize,
2419    pub active_consumers: usize,
2420    pub current_shape: RingShape,
2421    pub since_last_morph: std::time::Duration,
2422    /// Whether the ring carries ordering stamps. Shape policies
2423    /// consult this because the GlobalFifo declaration routes
2424    /// differently: unstamped rings morph to Vyukov, stamped rings
2425    /// flip the merge flag (the ordering policy's job) and must
2426    /// stay on the composed shapes.
2427    pub stamped: bool,
2428}
2429
2430/// Policy that decides when (and to what shape) the sidecar
2431/// should morph the ring.
2432///
2433/// The sidecar scanner calls `decide` on every scan tick with the
2434/// current peer counts + shape + cooldown since the last morph.
2435/// Returning `Some(new_shape)` triggers a `morph_to(new_shape)`.
2436/// Returning `None` leaves the shape alone.
2437pub trait RingShapePolicy: Send + Sync + 'static {
2438    fn decide(&self, observation: &PolicyObservation) -> Option<RingShape>;
2439}
2440
2441/// Default policy: pick the cheapest shape that fits the current
2442/// peer counts, with a fixed hysteresis interval after each morph
2443/// to prevent thrashing under rapid peer-count oscillation.
2444///
2445/// Mapping (when `since_last_morph >= hysteresis`):
2446///
2447/// | producers | consumers | shape  |
2448/// |-----------|-----------|--------|
2449/// |     1     |     1     | `Spsc` |
2450/// |    >=2    |     1     | `Mpsc` |
2451/// |     *     |    >=2    | `Mpmc` |
2452///
2453/// While `since_last_morph < hysteresis`, returns `None` even if
2454/// the target shape differs. While either peer count is 0 the
2455/// policy also returns `None` (no point morphing an empty ring).
2456pub struct DefaultRingShapePolicy {
2457    pub hysteresis: std::time::Duration,
2458}
2459
2460impl Default for DefaultRingShapePolicy {
2461    fn default() -> Self {
2462        Self { hysteresis: std::time::Duration::from_millis(100) }
2463    }
2464}
2465
2466impl DefaultRingShapePolicy {
2467    pub fn target_shape(producers: usize, consumers: usize) -> Option<RingShape> {
2468        match (producers, consumers) {
2469            (0, _) | (_, 0) => None,
2470            (1, 1) => Some(RingShape::Spsc),
2471            (_, 1) => Some(RingShape::Mpsc),
2472            (_, _) => Some(RingShape::Mpmc),
2473        }
2474    }
2475}
2476
2477impl RingShapePolicy for DefaultRingShapePolicy {
2478    fn decide(&self, obs: &PolicyObservation) -> Option<RingShape> {
2479        if obs.since_last_morph < self.hysteresis {
2480            return None;
2481        }
2482        let target = Self::target_shape(obs.active_producers, obs.active_consumers)?;
2483        if target == obs.current_shape {
2484            None
2485        } else {
2486            Some(target)
2487        }
2488    }
2489}
2490
2491/// QoS-aware shape policy: consumes the
2492/// [`Ordering`](crate::qos_policy::Ordering) declaration on a
2493/// [`QosPolicy`](crate::qos_policy::QosPolicy) alongside the peer
2494/// counts.
2495///
2496/// Decision matrix (after the hysteresis cooldown):
2497///
2498/// | declaration | ring | decision |
2499/// |---|---|---|
2500/// | `GlobalFifo` | unstamped | morph to `Vyukov` (the proven global-FIFO structure) |
2501/// | `GlobalFifo` | stamped | counts-based composed shape; the ordering axis is served by the merge flag, which the [`OrderingPolicy`] flips |
2502/// | `PerProducer` | either | counts-based default (which also walks an earlier Vyukov morph back once the declaration is withdrawn) |
2503pub struct QosRingShapePolicy {
2504    pub qos: Arc<crate::qos_policy::QosPolicy>,
2505    pub hysteresis: std::time::Duration,
2506}
2507
2508impl QosRingShapePolicy {
2509    pub fn new(qos: Arc<crate::qos_policy::QosPolicy>) -> Self {
2510        Self { qos, hysteresis: std::time::Duration::from_millis(100) }
2511    }
2512}
2513
2514impl RingShapePolicy for QosRingShapePolicy {
2515    fn decide(&self, obs: &PolicyObservation) -> Option<RingShape> {
2516        if obs.since_last_morph < self.hysteresis {
2517            return None;
2518        }
2519        let target = match self.qos.ordering() {
2520            QosOrdering::GlobalFifo if !obs.stamped => Some(RingShape::Vyukov),
2521            _ => DefaultRingShapePolicy::target_shape(
2522                obs.active_producers,
2523                obs.active_consumers,
2524            ),
2525        }?;
2526        if target == obs.current_shape {
2527            None
2528        } else {
2529            Some(target)
2530        }
2531    }
2532}
2533
2534/// A snapshot of a stamped ring's ordering-relevant state passed to
2535/// an [`OrderingPolicy`] on every sidecar scan.
2536#[derive(Debug, Clone, Copy)]
2537pub struct OrderingPolicyObservation {
2538    /// Inversions per second observed since the previous scan
2539    /// (delta of the shared inversion counter over the scan
2540    /// interval).
2541    pub inversions_per_sec: f64,
2542    /// Live ordering mode.
2543    pub current_mode: OrderingMode,
2544    /// The caller's QoS declaration.
2545    pub declared: QosOrdering,
2546    pub active_producers: usize,
2547    pub active_consumers: usize,
2548    /// Time since the last mode flip this sidecar issued.
2549    pub since_last_change: std::time::Duration,
2550}
2551
2552/// Policy that decides when (and to which mode) the sidecar flips
2553/// a stamped ring's ordering flag. Mirrors [`RingShapePolicy`]:
2554/// `Some(mode)` triggers `set_ordering_mode(mode)`, `None` leaves
2555/// the flag alone.
2556pub trait OrderingPolicy: Send + Sync + 'static {
2557    fn decide(&self, observation: &OrderingPolicyObservation) -> Option<OrderingMode>;
2558}
2559
2560/// Default ordering policy.
2561///
2562/// - Acts on the QoS declaration always: `GlobalFifo` arms
2563///   `MergeByStamp`; withdrawing to `PerProducer` disarms back to
2564///   `Unordered` (only when `auto_order_threshold` is unset - see
2565///   below).
2566/// - Acts on the inversion rate only when the caller pre-authorized
2567///   an automatic response by setting `auto_order_threshold`
2568///   (inversions/sec): under a `PerProducer` declaration, a rate
2569///   above the threshold arms `MergeByStamp`. The auto arm is
2570///   one-way - merged pops read zero inversions by construction, so
2571///   there is no symmetric signal to disarm on; disarming is the
2572///   caller's call (QoS declaration or an explicit
2573///   `set_ordering_mode`).
2574pub struct DefaultOrderingPolicy {
2575    pub hysteresis: std::time::Duration,
2576    pub auto_order_threshold: Option<f64>,
2577}
2578
2579impl Default for DefaultOrderingPolicy {
2580    fn default() -> Self {
2581        Self {
2582            hysteresis: std::time::Duration::from_millis(100),
2583            auto_order_threshold: None,
2584        }
2585    }
2586}
2587
2588impl OrderingPolicy for DefaultOrderingPolicy {
2589    fn decide(&self, obs: &OrderingPolicyObservation) -> Option<OrderingMode> {
2590        if obs.since_last_change < self.hysteresis {
2591            return None;
2592        }
2593        match obs.declared {
2594            QosOrdering::GlobalFifo => {
2595                if obs.current_mode == OrderingMode::Unordered {
2596                    Some(OrderingMode::MergeByStamp)
2597                } else {
2598                    None
2599                }
2600            }
2601            QosOrdering::PerProducer => {
2602                match self.auto_order_threshold {
2603                    Some(threshold) => {
2604                        if obs.current_mode == OrderingMode::Unordered
2605                            && obs.inversions_per_sec > threshold
2606                        {
2607                            Some(OrderingMode::MergeByStamp)
2608                        } else {
2609                            None
2610                        }
2611                    }
2612                    None => {
2613                        if obs.current_mode != OrderingMode::Unordered {
2614                            Some(OrderingMode::Unordered)
2615                        } else {
2616                            None
2617                        }
2618                    }
2619                }
2620            }
2621        }
2622    }
2623}
2624
2625/// Background scanner thread that drives shape morphs on an
2626/// [`AdaptiveRing`] from a [`RingShapePolicy`].
2627///
2628/// `spawn` starts the thread; `shutdown` stops it. The thread
2629/// scans every `scan_interval`, builds a [`PolicyObservation`],
2630/// asks the policy, and calls [`AdaptiveRing::morph_to`] on
2631/// `Some(new_shape)` responses.
2632pub struct AdaptiveRingSidecar {
2633    handle: Option<std::thread::JoinHandle<()>>,
2634    stop: Arc<std::sync::atomic::AtomicBool>,
2635    morphs_triggered: Arc<std::sync::atomic::AtomicU64>,
2636    ordering_flips: Arc<std::sync::atomic::AtomicU64>,
2637}
2638
2639impl AdaptiveRingSidecar {
2640    /// Spawn a sidecar thread that morphs `ring` according to
2641    /// `policy` decisions sampled every `scan_interval`.
2642    pub fn spawn<P: RingShapePolicy>(
2643        ring: Arc<AdaptiveRing>,
2644        policy: P,
2645        scan_interval: std::time::Duration,
2646    ) -> Self {
2647        Self::spawn_gated(
2648            ring,
2649            policy,
2650            scan_interval,
2651            crate::policy_gate::GateConfig::default(),
2652        )
2653    }
2654
2655    /// As [`spawn`](Self::spawn) with a confidence gate between
2656    /// the shape policy's recommendation and the morph. Disabled
2657    /// (the default config) reproduces `spawn` exactly.
2658    pub fn spawn_gated<P: RingShapePolicy>(
2659        ring: Arc<AdaptiveRing>,
2660        policy: P,
2661        scan_interval: std::time::Duration,
2662        gate_cfg: crate::policy_gate::GateConfig,
2663    ) -> Self {
2664        let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
2665        let morphs_triggered = Arc::new(std::sync::atomic::AtomicU64::new(0));
2666
2667        let stop_c = stop.clone();
2668        let morphs_c = morphs_triggered.clone();
2669        let handle = std::thread::spawn(move || {
2670            let mut last_morph = std::time::Instant::now();
2671            let mut gate = crate::policy_gate::ConfidenceGate::new(gate_cfg);
2672            while !stop_c.load(Ordering::Acquire) {
2673                let obs = PolicyObservation {
2674                    active_producers: ring.active_producers(),
2675                    active_consumers: ring.active_consumers(),
2676                    current_shape: ring.current_shape(),
2677                    since_last_morph: last_morph.elapsed(),
2678                    stamped: ring.is_stamped(),
2679                };
2680                // Policy-driven morphs go through the internal morph:
2681                // a sidecar IS an automatic driver, so it must not
2682                // pin the shape the way an explicit morph_to does.
2683                if let Some(new_shape) = gate
2684                    .observe(policy.decide(&obs).map(|s| ring.contract_filtered_shape(s)))
2685                    && ring.morph_shape(new_shape).is_ok()
2686                {
2687                    last_morph = std::time::Instant::now();
2688                    morphs_c.fetch_add(1, Ordering::Relaxed);
2689                }
2690                std::thread::sleep(scan_interval);
2691            }
2692        });
2693
2694        Self {
2695            handle: Some(handle),
2696            stop,
2697            morphs_triggered,
2698            ordering_flips: Arc::new(std::sync::atomic::AtomicU64::new(0)),
2699        }
2700    }
2701
2702    /// Spawn a sidecar that consults BOTH axes every scan tick: the
2703    /// shape policy (peer counts + the QoS ordering declaration, via
2704    /// [`QosRingShapePolicy`] or any custom [`RingShapePolicy`]) and
2705    /// the ordering policy (declaration + observed inversion rate).
2706    ///
2707    /// Per tick, on a stamped ring the sidecar additionally:
2708    /// - computes inversions/sec from the shared counter's delta,
2709    /// - ticks the drainer-lease epoch so a dead merge drainer
2710    ///   becomes preemptible after [`DRAINER_GRACE_EPOCHS`] scans,
2711    /// - applies the ordering policy's decision via
2712    ///   `set_ordering_mode` (counted in
2713    ///   [`ordering_flips`](Self::ordering_flips)).
2714    ///
2715    /// The shape axis is UNGATED by default: capacity-class morphs
2716    /// are cheap to reverse (the warm-backing path makes them
2717    /// microsecond-scale), so tracking load faithfully beats
2718    /// deliberating. The ordering AUTO-arm is GATED by default: the
2719    /// inversion-rate-driven `Unordered -> MergeByStamp` flip is
2720    /// one-way (merged pops read zero inversions, so there is no
2721    /// symmetric signal to walk it back), and a one-way decision
2722    /// taken on a single noisy scan is unrecoverable. The gate
2723    /// makes the auto-arm demand sustained inversions before it
2724    /// commits. Explicit caller declarations (`GlobalFifo` arm,
2725    /// declaration withdrawal) are NOT noise and fire immediately -
2726    /// only the auto-detected arm is deliberated.
2727    ///
2728    /// `spawn_with_qos_gated` overrides both axes with one explicit
2729    /// config (disabled reproduces the fully-ungated behavior).
2730    pub fn spawn_with_qos<P: RingShapePolicy, O: OrderingPolicy>(
2731        ring: Arc<AdaptiveRing>,
2732        shape_policy: P,
2733        ordering_policy: O,
2734        qos: Arc<crate::qos_policy::QosPolicy>,
2735        scan_interval: std::time::Duration,
2736    ) -> Self {
2737        Self::spawn_with_qos_core(
2738            ring,
2739            shape_policy,
2740            ordering_policy,
2741            qos,
2742            scan_interval,
2743            crate::policy_gate::GateConfig::default(),
2744            crate::policy_gate::GateConfig::enabled_with_arity(2),
2745        )
2746    }
2747
2748    /// As [`spawn_with_qos`](Self::spawn_with_qos) with confidence
2749    /// gates on BOTH axes set from one explicit config - a shape
2750    /// gate and an ordering-auto-arm gate (each accumulates its own
2751    /// conviction; a peer-count change shocks both). `GateConfig::default()`
2752    /// (disabled) reproduces the fully-ungated sidecar; an enabled
2753    /// config gates the shape morph AND the ordering auto-arm.
2754    /// Explicit ordering declarations always fire immediately
2755    /// regardless of config - the gate governs the auto-detected
2756    /// arm only.
2757    pub fn spawn_with_qos_gated<P: RingShapePolicy, O: OrderingPolicy>(
2758        ring: Arc<AdaptiveRing>,
2759        shape_policy: P,
2760        ordering_policy: O,
2761        qos: Arc<crate::qos_policy::QosPolicy>,
2762        scan_interval: std::time::Duration,
2763        gate_cfg: crate::policy_gate::GateConfig,
2764    ) -> Self {
2765        Self::spawn_with_qos_core(
2766            ring,
2767            shape_policy,
2768            ordering_policy,
2769            qos,
2770            scan_interval,
2771            gate_cfg,
2772            gate_cfg,
2773        )
2774    }
2775
2776    fn spawn_with_qos_core<P: RingShapePolicy, O: OrderingPolicy>(
2777        ring: Arc<AdaptiveRing>,
2778        shape_policy: P,
2779        ordering_policy: O,
2780        qos: Arc<crate::qos_policy::QosPolicy>,
2781        scan_interval: std::time::Duration,
2782        shape_gate_cfg: crate::policy_gate::GateConfig,
2783        order_gate_cfg: crate::policy_gate::GateConfig,
2784    ) -> Self {
2785        let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
2786        let morphs_triggered = Arc::new(std::sync::atomic::AtomicU64::new(0));
2787        let ordering_flips = Arc::new(std::sync::atomic::AtomicU64::new(0));
2788
2789        let stop_c = stop.clone();
2790        let morphs_c = morphs_triggered.clone();
2791        let flips_c = ordering_flips.clone();
2792        let handle = std::thread::spawn(move || {
2793            let mut last_morph = std::time::Instant::now();
2794            let mut last_flip = std::time::Instant::now();
2795            let mut last_inversions = ring.inversions();
2796            let mut last_scan = std::time::Instant::now();
2797            let mut shape_gate = crate::policy_gate::ConfidenceGate::new(shape_gate_cfg);
2798            let mut order_gate = crate::policy_gate::ConfidenceGate::new(order_gate_cfg);
2799            let mut last_peers = (0usize, 0usize);
2800            let mut first_scan = true;
2801            while !stop_c.load(Ordering::Acquire) {
2802                let obs = PolicyObservation {
2803                    active_producers: ring.active_producers(),
2804                    active_consumers: ring.active_consumers(),
2805                    current_shape: ring.current_shape(),
2806                    since_last_morph: last_morph.elapsed(),
2807                    stamped: ring.is_stamped(),
2808                };
2809                let peers = (obs.active_producers, obs.active_consumers);
2810                if !first_scan && peers != last_peers {
2811                    shape_gate.shock();
2812                    order_gate.shock();
2813                }
2814                last_peers = peers;
2815                first_scan = false;
2816
2817                if let Some(new_shape) = shape_gate
2818                    .observe(shape_policy.decide(&obs).map(|s| ring.contract_filtered_shape(s)))
2819                    && ring.morph_shape(new_shape).is_ok()
2820                {
2821                    last_morph = std::time::Instant::now();
2822                    morphs_c.fetch_add(1, Ordering::Relaxed);
2823                }
2824
2825                if let Some(current_mode) = ring.ordering_mode() {
2826                    ring.tick_drainer_epoch().ok();
2827
2828                    let now_inversions = ring.inversions();
2829                    let elapsed = last_scan.elapsed().as_secs_f64().max(1e-9);
2830                    let rate = now_inversions
2831                        .saturating_sub(last_inversions) as f64 / elapsed;
2832                    last_inversions = now_inversions;
2833                    last_scan = std::time::Instant::now();
2834
2835                    let declared = qos.ordering();
2836                    let ord_obs = OrderingPolicyObservation {
2837                        inversions_per_sec: rate,
2838                        current_mode,
2839                        declared,
2840                        active_producers: obs.active_producers,
2841                        active_consumers: obs.active_consumers,
2842                        since_last_change: last_flip.elapsed(),
2843                    };
2844                    let decision = ordering_policy
2845                        .decide(&ord_obs)
2846                        .filter(|m| *m != current_mode);
2847
2848                    // The auto-arm is the one-way, noise-prone
2849                    // decision: a `PerProducer` declaration (no
2850                    // global-order intent) that the inversion rate
2851                    // nonetheless pushes to `MergeByStamp`. That is
2852                    // the only ordering decision the gate governs.
2853                    // An explicit `GlobalFifo` arm and any disarm
2854                    // are caller intent, not noise - they bypass the
2855                    // gate and fire immediately.
2856                    let is_auto_arm = declared == QosOrdering::PerProducer
2857                        && decision == Some(OrderingMode::MergeByStamp);
2858                    let gated = if is_auto_arm {
2859                        order_gate.observe(decision)
2860                    } else {
2861                        decision
2862                    };
2863                    if let Some(new_mode) = gated
2864                        && ring.set_ordering_mode(new_mode).is_ok()
2865                    {
2866                        last_flip = std::time::Instant::now();
2867                        flips_c.fetch_add(1, Ordering::Relaxed);
2868                    }
2869                }
2870                std::thread::sleep(scan_interval);
2871            }
2872        });
2873
2874        Self {
2875            handle: Some(handle),
2876            stop,
2877            morphs_triggered,
2878            ordering_flips,
2879        }
2880    }
2881
2882    /// Number of successful morph_to calls the sidecar has issued
2883    /// since spawn.
2884    pub fn morphs_triggered(&self) -> u64 {
2885        self.morphs_triggered.load(Ordering::Acquire)
2886    }
2887
2888    /// Number of ordering-mode flips this sidecar has issued since
2889    /// spawn (always 0 for [`spawn`](Self::spawn)).
2890    pub fn ordering_flips(&self) -> u64 {
2891        self.ordering_flips.load(Ordering::Acquire)
2892    }
2893
2894    /// Stop the scanner thread and join it.
2895    pub fn shutdown(mut self) {
2896        self.stop.store(true, Ordering::Release);
2897        if let Some(h) = self.handle.take() {
2898            h.join().ok();
2899        }
2900    }
2901}
2902
2903impl Drop for AdaptiveRingSidecar {
2904    fn drop(&mut self) {
2905        self.stop.store(true, Ordering::Release);
2906        if let Some(h) = self.handle.take() {
2907            h.join().ok();
2908        }
2909    }
2910}
2911
2912#[cfg(test)]
2913mod tests {
2914    use super::*;
2915
2916    #[test]
2917    fn create_starts_in_spsc_shape() {
2918        let ring = AdaptiveRing::create_anon(4, 4, 64).unwrap();
2919        assert_eq!(ring.current_shape(), RingShape::Spsc);
2920        assert_eq!(ring.pin_generation(), 0);
2921    }
2922
2923    #[test]
2924    fn adaptive_dispatch_round_trip_each_shape() {
2925        for shape in [RingShape::Spsc, RingShape::Mpsc, RingShape::Mpmc, RingShape::Vyukov] {
2926            let ring = AdaptiveRing::create_anon(4, 4, 64).unwrap();
2927            ring.shape_tag.store(shape as u8, Ordering::Release);
2928            // 56 = ADAPTIVE_VYUKOV_PAYLOAD_BYTES, the smaller of the two
2929            // backings' slot sizes (Vyukov's 8B per-slot sequence eats
2930            // 8 of the 64B slot; Lamport gets the full 64B).
2931            let payload = [0xCDu8; 56];
2932            ring.try_send(0, &payload).unwrap();
2933            let mut out = [0u8; ADAPTIVE_SPSC_PAYLOAD_BYTES];
2934            let n = ring.try_recv(0, &mut out).unwrap();
2935            assert!(n > 0, "shape {:?} delivered zero bytes", shape);
2936            assert_eq!(&out[..payload.len()], &payload[..]);
2937        }
2938    }
2939
2940    #[test]
2941    fn pin_captures_shape_and_generation() {
2942        let ring = AdaptiveRing::create_anon(4, 4, 64).unwrap();
2943        let pinned = ring.pin_current_shape();
2944        assert_eq!(pinned.shape(), RingShape::Spsc);
2945        assert!(pinned.is_still_valid());
2946        // Re-pin: still valid because no morph happened.
2947        let pinned = ring.pin_current_shape();
2948        assert!(pinned.is_still_valid());
2949    }
2950
2951    #[test]
2952    fn morph_invalidates_outstanding_pin() {
2953        let ring = AdaptiveRing::create_anon(4, 4, 64).unwrap();
2954        let pinned = ring.pin_current_shape();
2955        assert!(pinned.is_still_valid());
2956
2957        ring.morph_to(RingShape::Mpsc).unwrap();
2958        assert!(!pinned.is_still_valid(),
2959                "pin must invalidate after morph_to");
2960        assert_eq!(ring.current_shape(), RingShape::Mpsc);
2961    }
2962
2963    #[test]
2964    fn morph_to_same_shape_is_no_op() {
2965        let ring = AdaptiveRing::create_anon(4, 4, 64).unwrap();
2966        let gen_before = ring.pin_generation();
2967        ring.morph_to(RingShape::Spsc).unwrap();
2968        let gen_after = ring.pin_generation();
2969        assert_eq!(gen_before, gen_after,
2970                   "morph_to(same shape) must not bump pin_generation");
2971    }
2972
2973    #[test]
2974    fn morph_preserves_in_flight_items_via_stale_walk() {
2975        let ring = AdaptiveRing::create_anon(4, 4, 64).unwrap();
2976
2977        // Push 3 items via the SPSC shape.
2978        for i in 0..3u32 {
2979            let mut buf = [0u8; ADAPTIVE_SPSC_PAYLOAD_BYTES];
2980            buf[..4].copy_from_slice(&i.to_le_bytes());
2981            ring.try_send(0, &buf).unwrap();
2982        }
2983
2984        // Morph to MPSC: no data moves; the SPSC backing becomes
2985        // the stale backing and the pop path drains it first.
2986        ring.morph_to(RingShape::Mpsc).unwrap();
2987        assert_eq!(ring.current_shape(), RingShape::Mpsc);
2988        assert_eq!(ring.approx_len(), 3,
2989                   "the stale backlog must stay visible through approx_len");
2990
2991        // New traffic lands in the new shape while the backlog is
2992        // still pending; the stale walk delivers old-before-new.
2993        let mut buf = [0u8; ADAPTIVE_SPSC_PAYLOAD_BYTES];
2994        buf[..4].copy_from_slice(&99u32.to_le_bytes());
2995        ring.try_send(0, &buf).unwrap();
2996
2997        let mut seen = Vec::new();
2998        let mut out = [0u8; ADAPTIVE_SPSC_PAYLOAD_BYTES];
2999        while ring.try_recv(0, &mut out).is_ok() {
3000            seen.push(u32::from_le_bytes(out[..4].try_into().unwrap()));
3001        }
3002        assert_eq!(seen, vec![0u32, 1, 2, 99],
3003                   "stale backlog must drain before post-morph items");
3004        assert!(ring.is_empty());
3005    }
3006
3007    #[test]
3008    fn frame_round_trip_all_shapes() {
3009        use crate::frame_ring::FrameClass;
3010        for shape in [RingShape::Spsc, RingShape::Mpsc, RingShape::Mpmc, RingShape::Vyukov] {
3011            let ring = AdaptiveRing::create_anon(4, 4, 64).unwrap();
3012            if shape != RingShape::Spsc {
3013                ring.morph_to(shape).unwrap();
3014            }
3015            let small = b"small inline payload".to_vec();
3016            let large = vec![0xABu8; 4000];
3017            assert_eq!(ring.send_frame(0, &small).unwrap(), FrameClass::Inline,
3018                       "{shape:?} small should inline");
3019            assert_eq!(ring.send_frame(0, &large).unwrap(), FrameClass::Offset,
3020                       "{shape:?} large should offset");
3021            let mut out = Vec::new();
3022            assert_eq!(ring.recv_frame(0, &mut out).unwrap(), FrameClass::Inline);
3023            assert_eq!(out, small, "{shape:?} small round-trip");
3024            assert_eq!(ring.recv_frame(0, &mut out).unwrap(), FrameClass::Offset);
3025            assert_eq!(out, large, "{shape:?} large round-trip");
3026        }
3027    }
3028
3029    #[test]
3030    fn frame_survives_morph() {
3031        let ring = AdaptiveRing::create_anon(4, 4, 64).unwrap();
3032        // SPSC: one inline, one offset.
3033        ring.send_frame(0, b"pre-morph small").unwrap();
3034        ring.send_frame(0, &vec![1u8; 3000]).unwrap();
3035        // Morph to MPSC: the SPSC backing becomes stale and drains
3036        // first; the frame descriptors and region blocks are
3037        // shape-independent so the records survive the morph intact.
3038        ring.morph_to(RingShape::Mpsc).unwrap();
3039        ring.send_frame(0, b"post-morph small").unwrap();
3040        ring.send_frame(0, &vec![2u8; 3000]).unwrap();
3041        let mut out = Vec::new();
3042        ring.recv_frame(0, &mut out).unwrap();
3043        assert_eq!(out, b"pre-morph small");
3044        ring.recv_frame(0, &mut out).unwrap();
3045        assert_eq!(out, vec![1u8; 3000]);
3046        ring.recv_frame(0, &mut out).unwrap();
3047        assert_eq!(out, b"post-morph small");
3048        ring.recv_frame(0, &mut out).unwrap();
3049        assert_eq!(out, vec![2u8; 3000]);
3050    }
3051
3052    #[test]
3053    fn frame_override_and_limits() {
3054        use crate::frame_ring::{FrameClass, LayoutHint};
3055        let ring = AdaptiveRing::create_anon(2, 2, 64).unwrap();
3056        let mut out = Vec::new();
3057        // ForceOffset spills a small payload to the region.
3058        assert_eq!(ring.send_frame_as(0, b"tiny", LayoutHint::ForceOffset).unwrap(),
3059                   FrameClass::Offset);
3060        assert_eq!(ring.recv_frame(0, &mut out).unwrap(), FrameClass::Offset);
3061        assert_eq!(out, b"tiny");
3062        // ForceInline rejects an over-budget payload.
3063        let big = vec![0u8; AdaptiveRing::FRAME_INLINE_BUDGET + 1];
3064        assert_eq!(ring.send_frame_as(0, &big, LayoutHint::ForceInline).unwrap_err(),
3065                   RingError::PayloadTooLarge);
3066        // Auto inlines exactly at the budget.
3067        let at = vec![7u8; AdaptiveRing::FRAME_INLINE_BUDGET];
3068        assert_eq!(ring.send_frame(0, &at).unwrap(), FrameClass::Inline);
3069        ring.recv_frame(0, &mut out).unwrap();
3070        assert_eq!(out, at);
3071    }
3072
3073    #[test]
3074    fn frame_rejected_on_stamped_ring() {
3075        // Frames and ordering stamps both claim the slot head, so the
3076        // frame path is refused on a stamped ring.
3077        let ring = AdaptiveRing::create_anon(2, 2, 64)
3078            .unwrap()
3079            .with_ordering_stamps()
3080            .unwrap();
3081        assert_eq!(ring.send_frame(0, b"x").unwrap_err(), RingError::LayoutMismatch);
3082        let mut out = Vec::new();
3083        assert_eq!(ring.recv_frame(0, &mut out).unwrap_err(), RingError::LayoutMismatch);
3084    }
3085
3086    #[test]
3087    fn frame_vyukov_two_thread_mixed_size() {
3088        use std::sync::Arc;
3089        use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering as AtOrd};
3090        use std::thread;
3091
3092        const PER: u32 = 5_000;
3093        const PRODUCERS: u32 = 2;
3094        let total = (PER * PRODUCERS) as usize;
3095
3096        // Vyukov is the true-MPMC shape (one SharedRing, per-slot
3097        // sequence CAS), safe for many producers AND many consumers
3098        // with no partitioning. This exercises the shared payload
3099        // region under concurrent alloc (producers) and free
3100        // (consumers) at once.
3101        let ring = Arc::new(AdaptiveRing::create_anon(2, 2, 256).unwrap());
3102        ring.morph_to(RingShape::Vyukov).unwrap();
3103        // Each item carries its global id so a consumer can verify the
3104        // record regardless of which consumer drained it.
3105        let seen: Arc<Vec<AtomicBool>> =
3106            Arc::new((0..total).map(|_| AtomicBool::new(false)).collect());
3107        let received = Arc::new(AtomicUsize::new(0));
3108
3109        let mut prods = Vec::new();
3110        for p in 0..PRODUCERS {
3111            let ring = ring.clone();
3112            prods.push(thread::spawn(move || {
3113                for i in 0..PER {
3114                    let id = p * PER + i;
3115                    let len = (id as usize % 200) + 4; // 4..203, crosses the budget
3116                    let mut payload = vec![0u8; len];
3117                    payload[0..4].copy_from_slice(&id.to_le_bytes());
3118                    for k in 4..len {
3119                        payload[k] = id.wrapping_add(k as u32) as u8;
3120                    }
3121                    while ring.send_frame(p as usize, &payload).is_err() {
3122                        std::hint::spin_loop();
3123                    }
3124                }
3125            }));
3126        }
3127
3128        let mut cons = Vec::new();
3129        for c in 0..2usize {
3130            let ring = ring.clone();
3131            let seen = seen.clone();
3132            let received = received.clone();
3133            cons.push(thread::spawn(move || {
3134                let mut out = Vec::new();
3135                while received.load(AtOrd::Acquire) < total {
3136                    if ring.recv_frame(c, &mut out).is_ok() {
3137                        let id = u32::from_le_bytes(out[0..4].try_into().unwrap());
3138                        let len = (id as usize % 200) + 4;
3139                        assert_eq!(out.len(), len, "id {id} length");
3140                        for k in 4..len {
3141                            assert_eq!(out[k], id.wrapping_add(k as u32) as u8,
3142                                       "id {id} byte {k}");
3143                        }
3144                        let already = seen[id as usize].swap(true, AtOrd::AcqRel);
3145                        assert!(!already, "id {id} delivered twice");
3146                        received.fetch_add(1, AtOrd::AcqRel);
3147                    } else {
3148                        std::hint::spin_loop();
3149                    }
3150                }
3151            }));
3152        }
3153
3154        for p in prods { p.join().unwrap(); }
3155        for c in cons { c.join().unwrap(); }
3156        assert_eq!(received.load(AtOrd::Acquire), total);
3157        assert!(seen.iter().all(|b| b.load(AtOrd::Acquire)),
3158                "every id delivered exactly once");
3159    }
3160
3161    #[test]
3162    fn second_morph_blocked_until_stale_backlog_drains() {
3163        let ring = AdaptiveRing::create_anon(4, 4, 64).unwrap();
3164        ring.try_send(0, &[7u8; 8]).unwrap();
3165        ring.morph_to(RingShape::Mpsc).unwrap();
3166
3167        // The SPSC backlog has not drained; another morph must wait.
3168        assert_eq!(ring.morph_to(RingShape::Mpmc).unwrap_err(),
3169                   RingError::StaleBacklog);
3170
3171        let mut out = [0u8; ADAPTIVE_SPSC_PAYLOAD_BYTES];
3172        ring.try_recv(0, &mut out).unwrap();
3173        // Drained: the next morph proceeds.
3174        ring.morph_to(RingShape::Mpmc).unwrap();
3175        assert_eq!(ring.current_shape(), RingShape::Mpmc);
3176    }
3177
3178    #[test]
3179    fn register_producer_grows_past_hint_and_recycles_slots() {
3180        let ring = AdaptiveRing::create_anon(3, 1, 64).unwrap();
3181        let id0 = ring.register_producer().unwrap();
3182        let id1 = ring.register_producer().unwrap();
3183        let id2 = ring.register_producer().unwrap();
3184        assert_eq!((id0, id1, id2), (0, 1, 2));
3185
3186        // Past the construction hint the ring GROWS instead of
3187        // erroring: a 4th producer gets slot 3 and a live backing.
3188        let id3 = ring.register_producer().unwrap();
3189        assert_eq!(id3, 3);
3190        assert_eq!(ring.published_producers(), 4);
3191        ring.try_send(id3, &7u64.to_le_bytes()).unwrap();
3192        let mut out = [0u8; 64];
3193        // 4P/0C: no consumer registered, shape stays wherever the
3194        // counts left it; the adaptive pop still drains slot 3's
3195        // backing via the current shape + stale walk.
3196        let _c = ring.register_consumer().unwrap();
3197        let n = ring.try_recv(0, &mut out).unwrap();
3198        assert!(n >= 8, "popped record too short: {n}");
3199        assert_eq!(u64::from_le_bytes(out[..8].try_into().unwrap()), 7);
3200
3201        // Unregister frees the SLOT (bitmap claim): the next register
3202        // reuses id 1 without colliding with the still-live 2 and 3.
3203        ring.unregister_producer(id1);
3204        assert_eq!(ring.register_producer().unwrap(), 1);
3205
3206        // Errors exist ONLY under a caller-declared contract pin.
3207        let pinned = AdaptiveRing::create_anon(2, 1, 64)
3208            .unwrap()
3209            .with_contract(crate::ring_contract::RingContract::from_counts(2, 1));
3210        pinned.register_producer().unwrap();
3211        pinned.register_producer().unwrap();
3212        assert_eq!(pinned.register_producer().unwrap_err(),
3213                   AdaptiveError::TooManyProducers);
3214    }
3215
3216    #[test]
3217    fn default_policy_target_shape_per_peer_count() {
3218        // Idle either side -> no target.
3219        assert_eq!(DefaultRingShapePolicy::target_shape(0, 1), None);
3220        assert_eq!(DefaultRingShapePolicy::target_shape(1, 0), None);
3221        assert_eq!(DefaultRingShapePolicy::target_shape(0, 0), None);
3222        // 1P/1C -> SPSC
3223        assert_eq!(DefaultRingShapePolicy::target_shape(1, 1), Some(RingShape::Spsc));
3224        // NP/1C -> MPSC
3225        assert_eq!(DefaultRingShapePolicy::target_shape(2, 1), Some(RingShape::Mpsc));
3226        assert_eq!(DefaultRingShapePolicy::target_shape(8, 1), Some(RingShape::Mpsc));
3227        // */NC (NC >= 2) -> MPMC
3228        assert_eq!(DefaultRingShapePolicy::target_shape(1, 2), Some(RingShape::Mpmc));
3229        assert_eq!(DefaultRingShapePolicy::target_shape(4, 4), Some(RingShape::Mpmc));
3230    }
3231
3232    #[test]
3233    fn default_policy_returns_none_during_hysteresis() {
3234        let policy = DefaultRingShapePolicy {
3235            hysteresis: std::time::Duration::from_secs(1),
3236        };
3237        let obs = PolicyObservation {
3238            active_producers: 4,
3239            active_consumers: 4,
3240            current_shape: RingShape::Spsc,
3241            since_last_morph: std::time::Duration::from_millis(50),
3242            stamped: false,
3243        };
3244        // Target would be MPMC, but hysteresis says wait.
3245        assert_eq!(policy.decide(&obs), None);
3246    }
3247
3248    #[test]
3249    fn default_policy_returns_target_after_hysteresis() {
3250        let policy = DefaultRingShapePolicy {
3251            hysteresis: std::time::Duration::from_millis(10),
3252        };
3253        let obs = PolicyObservation {
3254            active_producers: 4,
3255            active_consumers: 4,
3256            current_shape: RingShape::Spsc,
3257            since_last_morph: std::time::Duration::from_secs(1),
3258            stamped: false,
3259        };
3260        assert_eq!(policy.decide(&obs), Some(RingShape::Mpmc));
3261    }
3262
3263    #[test]
3264    fn default_policy_returns_none_when_target_equals_current() {
3265        let policy = DefaultRingShapePolicy::default();
3266        let obs = PolicyObservation {
3267            active_producers: 1,
3268            active_consumers: 1,
3269            current_shape: RingShape::Spsc,
3270            since_last_morph: std::time::Duration::from_secs(1),
3271            stamped: false,
3272        };
3273        assert_eq!(policy.decide(&obs), None);
3274    }
3275
3276    #[test]
3277    fn shape_tracks_peer_counts_and_sidecar_stays_idle() {
3278        let ring = Arc::new(AdaptiveRing::create_anon(4, 4, 64).unwrap());
3279        let policy = DefaultRingShapePolicy {
3280            hysteresis: std::time::Duration::from_millis(5),
3281        };
3282        let sidecar = AdaptiveRingSidecar::spawn(
3283            ring.clone(),
3284            policy,
3285            std::time::Duration::from_millis(10),
3286        );
3287
3288        // Register 1P+1C -> SPSC (already the initial shape).
3289        let _p0 = ring.register_producer().unwrap();
3290        let _c0 = ring.register_consumer().unwrap();
3291        assert_eq!(ring.current_shape(), RingShape::Spsc);
3292
3293        // The register path itself morphs SYNCHRONOUSLY - no scan
3294        // interval to wait out, no sidecar required.
3295        let _p1 = ring.register_producer().unwrap();
3296        assert_eq!(ring.current_shape(), RingShape::Mpsc,
3297                   "2nd producer registration must morph to MPSC immediately");
3298
3299        let _c1 = ring.register_consumer().unwrap();
3300        assert_eq!(ring.current_shape(), RingShape::Mpmc,
3301                   "2nd consumer registration must morph to MPMC immediately");
3302
3303        // The sidecar observed a ring whose shape already tracked its
3304        // counts at every scan: it never had a correction to make.
3305        std::thread::sleep(std::time::Duration::from_millis(60));
3306        assert_eq!(sidecar.morphs_triggered(), 0,
3307                   "register-path morphs left the sidecar nothing to do");
3308
3309        // Leaves shrink the shape too: back down to 1P/1C -> SPSC
3310        // (the stale walk drains the composed backings; empty here).
3311        ring.unregister_consumer(1);
3312        ring.unregister_producer(1);
3313        assert_eq!(ring.current_shape(), RingShape::Spsc,
3314                   "unregister must morph back down automatically");
3315
3316        sidecar.shutdown();
3317    }
3318
3319    #[test]
3320    fn pinned_native_paths_match_adaptive_paths() {
3321        let ring = AdaptiveRing::create_anon(4, 4, 64).unwrap();
3322        let pinned = ring.pin_current_shape();
3323        assert_eq!(pinned.shape(), RingShape::Spsc);
3324
3325        let payload = [0xAAu8; ADAPTIVE_SPSC_PAYLOAD_BYTES];
3326        pinned.spsc_try_push(&payload).unwrap();
3327        let mut out = [0u8; ADAPTIVE_SPSC_PAYLOAD_BYTES];
3328        let n = pinned.spsc_try_pop(&mut out).unwrap();
3329        assert_eq!(n, ADAPTIVE_SPSC_PAYLOAD_BYTES);
3330        assert_eq!(out, payload);
3331        assert!(pinned.is_still_valid());
3332    }
3333
3334    // ===============================================================
3335    // Ordering-axis tests
3336    // ===============================================================
3337
3338    fn stamped_anon(
3339        max_producers: usize,
3340        max_consumers: usize,
3341        kind: StampKind,
3342    ) -> AdaptiveRing {
3343        AdaptiveRing::create_anon(max_producers, max_consumers, 64)
3344            .unwrap()
3345            .with_ordering_stamps_kind(kind)
3346            .unwrap()
3347    }
3348
3349    #[test]
3350    fn stamped_round_trip_strips_stamp_and_caps_payload() {
3351        let ring = stamped_anon(2, 1, StampKind::SharedCounter);
3352        assert!(ring.is_stamped());
3353        assert_eq!(ring.stamp_kind(), Some(StampKind::SharedCounter));
3354        assert_eq!(ring.ordering_mode(), Some(OrderingMode::Unordered));
3355
3356        // 57 bytes exceed the stamped cap.
3357        let too_big = [0u8; STAMPED_PAYLOAD_BYTES + 1];
3358        assert_eq!(ring.try_send(0, &too_big).unwrap_err(),
3359                   RingError::PayloadTooLarge);
3360
3361        let payload = [0xC3u8; STAMPED_PAYLOAD_BYTES];
3362        ring.try_send(0, &payload).unwrap();
3363        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
3364        let n = ring.try_recv(0, &mut out).unwrap();
3365        assert_eq!(n, STAMPED_PAYLOAD_BYTES,
3366                   "stamped recv returns payload bytes only");
3367        assert_eq!(out, payload, "the stamp must be stripped, not leak into the payload");
3368    }
3369
3370    #[test]
3371    fn unstamped_ring_rejects_ordering_calls() {
3372        let ring = AdaptiveRing::create_anon(2, 1, 64).unwrap();
3373        assert!(!ring.is_stamped());
3374        assert_eq!(ring.inversions(), 0);
3375        assert_eq!(ring.set_ordering_mode(OrderingMode::MergeByStamp).unwrap_err(),
3376                   RingError::NotStamped);
3377        assert_eq!(ring.refresh_watermark(0).unwrap_err(), RingError::NotStamped);
3378        let pinned = ring.pin_current_shape();
3379        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
3380        assert_eq!(pinned.ordered_try_pop(0, &mut out).unwrap_err(),
3381                   RingError::NotStamped);
3382        assert_eq!(pinned.stamped_try_push(0, &[1u8; 8]).unwrap_err(),
3383                   RingError::NotStamped);
3384    }
3385
3386    #[test]
3387    fn stamped_ring_rejects_vyukov_morph_and_vyukov_ring_rejects_stamps() {
3388        let ring = stamped_anon(2, 1, StampKind::Monotonic);
3389        assert_eq!(ring.morph_to(RingShape::Vyukov).unwrap_err(),
3390                   RingError::LayoutMismatch);
3391
3392        let vyukov_first = AdaptiveRing::create_anon(2, 1, 64).unwrap();
3393        vyukov_first.morph_to(RingShape::Vyukov).unwrap();
3394        assert!(matches!(
3395            vyukov_first.with_ordering_stamps(),
3396            Err(RingError::LayoutMismatch)
3397        ));
3398    }
3399
3400    #[test]
3401    fn synthetic_interleave_fires_inversion_counter() {
3402        let ring = stamped_anon(2, 1, StampKind::SharedCounter);
3403        ring.morph_to(RingShape::Mpsc).unwrap();
3404
3405        // Producer 1 pushes FIRST (older stamp lands in ring 1),
3406        // then producer 0 (newer stamp in ring 0). The round-robin
3407        // drain starts at ring 0, so the consumer pops newer-then-
3408        // older: exactly one cross-producer inversion.
3409        ring.try_send(1, &1u64.to_le_bytes()).unwrap();
3410        ring.try_send(0, &2u64.to_le_bytes()).unwrap();
3411
3412        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
3413        ring.try_recv(0, &mut out).unwrap();
3414        assert_eq!(ring.inversions(), 0, "first pop has no predecessor");
3415        ring.try_recv(0, &mut out).unwrap();
3416        assert_eq!(ring.inversions(), 1,
3417                   "older-after-newer must count as one inversion");
3418    }
3419
3420    #[test]
3421    fn merge_mode_delivers_global_stamp_order() {
3422        let ring = stamped_anon(4, 1, StampKind::SharedCounter);
3423        ring.morph_to(RingShape::Mpsc).unwrap();
3424        ring.set_ordering_mode(OrderingMode::MergeByStamp).unwrap();
3425
3426        // Interleave 32 items across 4 producers in a single thread:
3427        // counter stamps make the push order the global order.
3428        for i in 0..32u64 {
3429            let producer = (i % 4) as usize;
3430            ring.try_send(producer, &i.to_le_bytes()).unwrap();
3431        }
3432
3433        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
3434        for expected in 0..32u64 {
3435            let n = ring.try_recv(0, &mut out).unwrap();
3436            assert_eq!(n, STAMPED_PAYLOAD_BYTES);
3437            let got = u64::from_le_bytes(out[..8].try_into().unwrap());
3438            assert_eq!(got, expected,
3439                       "merge pop must deliver global push order");
3440        }
3441        assert_eq!(ring.try_recv(0, &mut out).unwrap_err(), RingError::Empty);
3442        assert_eq!(ring.inversions(), 0,
3443                   "merged pops must observe zero inversions");
3444    }
3445
3446    #[test]
3447    fn flag_flip_orders_backlog_retroactively_without_loss() {
3448        let ring = stamped_anon(2, 1, StampKind::SharedCounter);
3449        ring.morph_to(RingShape::Mpsc).unwrap();
3450
3451        // Backlog pushed UNDER Unordered, interleaved so the
3452        // round-robin drain would invert.
3453        for i in 0..16u64 {
3454            let producer = ((i + 1) % 2) as usize;
3455            ring.try_send(producer, &i.to_le_bytes()).unwrap();
3456        }
3457
3458        // Pop two items unordered; the second is an inversion on
3459        // this interleave (ring 0 holds the odd/newer stamps).
3460        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
3461        let mut popped = Vec::new();
3462        for _ in 0..2 {
3463            ring.try_recv(0, &mut out).unwrap();
3464            popped.push(u64::from_le_bytes(out[..8].try_into().unwrap()));
3465        }
3466        let inversions_before_flip = ring.inversions();
3467        assert!(inversions_before_flip > 0,
3468                "unordered interleave must show inversions before the flip");
3469
3470        // The ordered switch: one store, no drain, retroactive over
3471        // the 14-item backlog because the stamps were already there.
3472        ring.set_ordering_mode(OrderingMode::MergeByStamp).unwrap();
3473        let mut merged = Vec::new();
3474        while let Ok(_n) = ring.try_recv(0, &mut out) {
3475            merged.push(u64::from_le_bytes(out[..8].try_into().unwrap()));
3476        }
3477
3478        // Zero loss across the transition...
3479        let mut all = popped.clone();
3480        all.extend(&merged);
3481        all.sort_unstable();
3482        assert_eq!(all, (0..16u64).collect::<Vec<_>>(),
3483                   "no item may be lost across the mode flip");
3484        // ...and the post-flip stream is globally ordered (strictly
3485        // increasing payload sequence = strictly increasing stamps).
3486        for pair in merged.windows(2) {
3487            assert!(pair[0] < pair[1],
3488                    "post-flip pops must be globally ordered: {merged:?}");
3489        }
3490        assert_eq!(ring.inversions(), inversions_before_flip,
3491                   "the flip itself and merged pops must add zero inversions");
3492    }
3493
3494    #[test]
3495    fn merge_strict_blocks_on_in_flight_stamp_then_releases() {
3496        let ring = stamped_anon(2, 1, StampKind::SharedCounter);
3497        ring.morph_to(RingShape::Mpsc).unwrap();
3498        ring.set_ordering_mode(OrderingMode::MergeStrict).unwrap();
3499        let region = ring.ordering_region().unwrap();
3500
3501        // Producer 1 stamps but stalls before pushing (the
3502        // stamp-to-publish window): issued advances, watermark
3503        // does not.
3504        let stalled_stamp = region.next_stamp(1);
3505        // Producer 0 stamps later and publishes.
3506        ring.try_send(0, &42u64.to_le_bytes()).unwrap();
3507
3508        // In-flight gate: producer 0's visible item must NOT
3509        // release while producer 1 holds a smaller in-flight stamp.
3510        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
3511        assert_eq!(ring.try_recv(0, &mut out).unwrap_err(), RingError::Empty,
3512                   "strict merge must hold the candidate while a smaller stamp is in flight");
3513
3514        // The stalled push resolves as Full-equivalent: the
3515        // watermark advances to the issued stamp ("this will never
3516        // publish"), clearing the in-flight gate. The strict
3517        // watermark gate still holds the candidate (producer 1's
3518        // empty ring has not vouched past the candidate's stamp)...
3519        region.publish_watermark(1, stalled_stamp);
3520        assert_eq!(ring.try_recv(0, &mut out).unwrap_err(), RingError::Empty,
3521                   "strict watermark gate must hold until the silent producer vouches");
3522        // ...until the idle producer heartbeats its watermark past
3523        // the candidate.
3524        ring.refresh_watermark(1).unwrap();
3525        let n = ring.try_recv(0, &mut out).unwrap();
3526        assert_eq!(n, STAMPED_PAYLOAD_BYTES);
3527        assert_eq!(u64::from_le_bytes(out[..8].try_into().unwrap()), 42);
3528    }
3529
3530    #[test]
3531    fn merge_by_stamp_in_flight_gate_blocks_descheduled_producer() {
3532        // The WSL-discovered case: a producer reserves/stamps, then
3533        // stalls (preemption) before publishing. MergeByStamp must
3534        // hold any larger candidate until the publish lands - a
3535        // fixed freshness window cannot bound a deschedule.
3536        let ring = stamped_anon(2, 1, StampKind::SharedCounter);
3537        ring.morph_to(RingShape::Mpsc).unwrap();
3538        ring.set_ordering_mode(OrderingMode::MergeByStamp).unwrap();
3539        let region = ring.ordering_region().unwrap();
3540
3541        let stalled = region.next_stamp(1); // stamped, never pushed
3542        ring.try_send(0, &9u64.to_le_bytes()).unwrap();
3543
3544        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
3545        assert_eq!(ring.try_recv(0, &mut out).unwrap_err(), RingError::Empty,
3546                   "MergeByStamp must gate on in-flight stamps too");
3547        region.publish_watermark(1, stalled); // the stall resolves
3548        let n = ring.try_recv(0, &mut out).unwrap();
3549        assert_eq!(n, STAMPED_PAYLOAD_BYTES);
3550        assert_eq!(u64::from_le_bytes(out[..8].try_into().unwrap()), 9);
3551    }
3552
3553    #[test]
3554    fn merge_strict_retired_producer_stops_gating() {
3555        let ring = stamped_anon(2, 1, StampKind::SharedCounter);
3556        ring.morph_to(RingShape::Mpsc).unwrap();
3557        ring.set_ordering_mode(OrderingMode::MergeStrict).unwrap();
3558
3559        // Producer 1 pushes once (its slot is in-use), the item is
3560        // consumed, and the producer goes silent with an old
3561        // watermark.
3562        ring.try_send(1, &1u64.to_le_bytes()).unwrap();
3563        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
3564        ring.try_recv(0, &mut out).unwrap();
3565
3566        // Producer 0's newer item is gated on producer 1's silence.
3567        ring.try_send(0, &2u64.to_le_bytes()).unwrap();
3568        assert_eq!(ring.try_recv(0, &mut out).unwrap_err(), RingError::Empty,
3569                   "strict couples release to the slowest in-use producer");
3570
3571        // Clean exit: retirement saturates the slot's watermark and
3572        // the candidate releases - permanently, no heartbeat needed.
3573        ring.retire_producer(1).unwrap();
3574        let n = ring.try_recv(0, &mut out).unwrap();
3575        assert_eq!(n, STAMPED_PAYLOAD_BYTES);
3576        assert_eq!(u64::from_le_bytes(out[..8].try_into().unwrap()), 2);
3577    }
3578
3579    #[test]
3580    fn multi_consumer_merge_enforces_single_drainer() {
3581        let ring = stamped_anon(2, 2, StampKind::SharedCounter);
3582        ring.morph_to(RingShape::Mpmc).unwrap();
3583        ring.set_ordering_mode(OrderingMode::MergeByStamp).unwrap();
3584
3585        for i in 0..4u64 {
3586            ring.try_send((i % 2) as usize, &i.to_le_bytes()).unwrap();
3587        }
3588
3589        // Consumer 0 pops first and thereby auto-acquires the lease.
3590        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
3591        ring.try_recv(0, &mut out).unwrap();
3592        // Consumer 1 is locked out while consumer 0 holds the lease.
3593        assert_eq!(ring.try_recv(1, &mut out).unwrap_err(),
3594                   RingError::NotDrainer);
3595        // Voluntary release hands the drain over.
3596        assert!(ring.release_drainer(0).unwrap());
3597        let n = ring.try_recv(1, &mut out).unwrap();
3598        assert_eq!(n, STAMPED_PAYLOAD_BYTES);
3599        assert_eq!(u64::from_le_bytes(out[..8].try_into().unwrap()), 1,
3600                   "the new drainer continues in global stamp order");
3601        // And consumer 0 is now locked out in turn.
3602        assert_eq!(ring.try_recv(0, &mut out).unwrap_err(),
3603                   RingError::NotDrainer);
3604    }
3605
3606    #[test]
3607    fn mode_flip_does_not_invalidate_pins() {
3608        let ring = stamped_anon(2, 1, StampKind::SharedCounter);
3609        ring.morph_to(RingShape::Mpsc).unwrap();
3610        let pinned = ring.pin_current_shape();
3611        assert!(pinned.is_still_valid());
3612
3613        // Interleaved stamped pushes through the pin.
3614        pinned.stamped_try_push(1, &1u64.to_le_bytes()).unwrap();
3615        pinned.stamped_try_push(0, &2u64.to_le_bytes()).unwrap();
3616
3617        // Flip the merge flag under the live pin: the pin survives
3618        // (no generation bump) and the pinned pop consults the mode
3619        // atom, so the next pops come out merged.
3620        ring.set_ordering_mode(OrderingMode::MergeByStamp).unwrap();
3621        assert!(pinned.is_still_valid(),
3622                "ordering-mode flips must not invalidate pins");
3623
3624        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
3625        pinned.ordered_try_pop(0, &mut out).unwrap();
3626        assert_eq!(u64::from_le_bytes(out[..8].try_into().unwrap()), 1,
3627                   "pinned merge pop must deliver stamp order");
3628        pinned.ordered_try_pop(0, &mut out).unwrap();
3629        assert_eq!(u64::from_le_bytes(out[..8].try_into().unwrap()), 2);
3630    }
3631
3632    #[test]
3633    fn stamped_items_survive_shape_morphs() {
3634        let ring = stamped_anon(2, 1, StampKind::SharedCounter);
3635        for i in 0..3u64 {
3636            ring.try_send(0, &i.to_le_bytes()).unwrap();
3637        }
3638        ring.morph_to(RingShape::Mpsc).unwrap();
3639        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
3640        let mut got = Vec::new();
3641        while ring.try_recv(0, &mut out).is_ok() {
3642            got.push(u64::from_le_bytes(out[..8].try_into().unwrap()));
3643        }
3644        got.sort_unstable();
3645        assert_eq!(got, vec![0, 1, 2],
3646                   "stamped slots must transfer intact across shape morphs");
3647    }
3648
3649    #[test]
3650    fn stamped_file_ring_open_adopts_creator_kind_and_shares_mode() {
3651        let mut prefix = std::env::temp_dir();
3652        prefix.push(format!(
3653            "subetha_stamped_open_{}_{}",
3654            std::process::id(),
3655            std::time::SystemTime::now()
3656                .duration_since(std::time::UNIX_EPOCH)
3657                .map(|d| d.as_nanos()).unwrap_or(0),
3658        ));
3659
3660        let creator = AdaptiveRing::create(&prefix, 2, 1, 64)
3661            .unwrap()
3662            .with_ordering_stamps_kind(StampKind::SharedCounter)
3663            .unwrap();
3664        creator.set_ordering_mode(OrderingMode::MergeByStamp).unwrap();
3665        creator.try_send(0, &7u64.to_le_bytes()).unwrap();
3666
3667        let opener = AdaptiveRing::open(&prefix, 2, 1, 64)
3668            .unwrap()
3669            .with_ordering_stamps()
3670            .unwrap();
3671        assert_eq!(opener.stamp_kind(), Some(StampKind::SharedCounter),
3672                   "opener must adopt the creator's stamp kind");
3673        assert_eq!(opener.ordering_mode(), Some(OrderingMode::MergeByStamp),
3674                   "the mode flag must be cross-process (region-resident)");
3675        // Explicit mismatched kind on open is a layout error.
3676        assert!(matches!(
3677            AdaptiveRing::open(&prefix, 2, 1, 64)
3678                .unwrap()
3679                .with_ordering_stamps_kind(StampKind::Monotonic),
3680            Err(RingError::LayoutMismatch)
3681        ));
3682
3683        let mut out = [0u8; STAMPED_PAYLOAD_BYTES];
3684        let n = opener.try_recv(0, &mut out).unwrap();
3685        assert_eq!(n, STAMPED_PAYLOAD_BYTES);
3686        assert_eq!(u64::from_le_bytes(out[..8].try_into().unwrap()), 7);
3687
3688        drop(creator);
3689        drop(opener);
3690        for suffix in [".spsc.bin", ".mpsc.0.bin", ".mpsc.1.bin",
3691                       ".mpmc.0.bin", ".mpmc.1.bin", ".vyukov.bin",
3692                       ".ordering.bin"] {
3693            let mut p = prefix.as_os_str().to_owned();
3694            p.push(suffix);
3695            std::fs::remove_file(std::path::PathBuf::from(p)).ok();
3696        }
3697    }
3698
3699    #[test]
3700    fn qos_shape_policy_decision_matrix() {
3701        let qos = Arc::new(crate::qos_policy::QosPolicy::default());
3702        let policy = QosRingShapePolicy {
3703            qos: qos.clone(),
3704            hysteresis: std::time::Duration::from_millis(0),
3705        };
3706        let obs = |shape, stamped| PolicyObservation {
3707            active_producers: 2,
3708            active_consumers: 1,
3709            current_shape: shape,
3710            since_last_morph: std::time::Duration::from_secs(1),
3711            stamped,
3712        };
3713
3714        // PerProducer: counts-based default (2P/1C -> MPSC).
3715        assert_eq!(policy.decide(&obs(RingShape::Spsc, false)),
3716                   Some(RingShape::Mpsc));
3717        assert_eq!(policy.decide(&obs(RingShape::Mpsc, false)), None);
3718
3719        // GlobalFifo + unstamped: Vyukov morph.
3720        qos.set_ordering(crate::qos_policy::Ordering::GlobalFifo);
3721        assert_eq!(policy.decide(&obs(RingShape::Mpsc, false)),
3722                   Some(RingShape::Vyukov));
3723        assert_eq!(policy.decide(&obs(RingShape::Vyukov, false)), None);
3724
3725        // GlobalFifo + stamped: shape stays counts-based composed
3726        // (the merge flag serves the declaration).
3727        assert_eq!(policy.decide(&obs(RingShape::Spsc, true)),
3728                   Some(RingShape::Mpsc));
3729        assert_eq!(policy.decide(&obs(RingShape::Mpsc, true)), None);
3730
3731        // Withdrawing the declaration walks Vyukov back.
3732        qos.set_ordering(crate::qos_policy::Ordering::PerProducer);
3733        assert_eq!(policy.decide(&obs(RingShape::Vyukov, false)),
3734                   Some(RingShape::Mpsc));
3735
3736        // Hysteresis suppresses everything.
3737        let cold = QosRingShapePolicy {
3738            qos: qos.clone(),
3739            hysteresis: std::time::Duration::from_secs(10),
3740        };
3741        let mut o = obs(RingShape::Spsc, false);
3742        o.since_last_morph = std::time::Duration::from_millis(1);
3743        assert_eq!(cold.decide(&o), None);
3744    }
3745
3746    #[test]
3747    fn default_ordering_policy_decision_matrix() {
3748        let obs = |mode, declared, rate, since_ms| OrderingPolicyObservation {
3749            inversions_per_sec: rate,
3750            current_mode: mode,
3751            declared,
3752            active_producers: 2,
3753            active_consumers: 1,
3754            since_last_change: std::time::Duration::from_millis(since_ms),
3755        };
3756        let declarative = DefaultOrderingPolicy {
3757            hysteresis: std::time::Duration::from_millis(0),
3758            auto_order_threshold: None,
3759        };
3760        // GlobalFifo declaration arms the merge.
3761        assert_eq!(
3762            declarative.decide(&obs(
3763                OrderingMode::Unordered, QosOrdering::GlobalFifo, 0.0, 500)),
3764            Some(OrderingMode::MergeByStamp),
3765        );
3766        assert_eq!(
3767            declarative.decide(&obs(
3768                OrderingMode::MergeByStamp, QosOrdering::GlobalFifo, 0.0, 500)),
3769            None,
3770        );
3771        // Withdrawal disarms (no auto threshold).
3772        assert_eq!(
3773            declarative.decide(&obs(
3774                OrderingMode::MergeByStamp, QosOrdering::PerProducer, 0.0, 500)),
3775            Some(OrderingMode::Unordered),
3776        );
3777
3778        let auto = DefaultOrderingPolicy {
3779            hysteresis: std::time::Duration::from_millis(0),
3780            auto_order_threshold: Some(100.0),
3781        };
3782        // Below threshold: report-only.
3783        assert_eq!(
3784            auto.decide(&obs(
3785                OrderingMode::Unordered, QosOrdering::PerProducer, 50.0, 500)),
3786            None,
3787        );
3788        // Above threshold: pre-authorized arm.
3789        assert_eq!(
3790            auto.decide(&obs(
3791                OrderingMode::Unordered, QosOrdering::PerProducer, 250.0, 500)),
3792            Some(OrderingMode::MergeByStamp),
3793        );
3794        // Auto arm is one-way: PerProducer + armed + auto -> stay.
3795        assert_eq!(
3796            auto.decide(&obs(
3797                OrderingMode::MergeByStamp, QosOrdering::PerProducer, 0.0, 500)),
3798            None,
3799        );
3800
3801        // Hysteresis suppresses both paths.
3802        let cold = DefaultOrderingPolicy {
3803            hysteresis: std::time::Duration::from_secs(10),
3804            auto_order_threshold: Some(1.0),
3805        };
3806        assert_eq!(
3807            cold.decide(&obs(
3808                OrderingMode::Unordered, QosOrdering::GlobalFifo, 1e6, 1)),
3809            None,
3810        );
3811    }
3812
3813    #[test]
3814    fn sidecar_spawn_with_qos_flips_merge_flag_on_declaration() {
3815        let ring = Arc::new(stamped_anon(2, 1, StampKind::SharedCounter));
3816        ring.morph_to(RingShape::Mpsc).unwrap();
3817        let _p0 = ring.register_producer().unwrap();
3818        let _p1 = ring.register_producer().unwrap();
3819        let _c0 = ring.register_consumer().unwrap();
3820
3821        let qos = Arc::new(crate::qos_policy::QosPolicy::default());
3822        let sidecar = AdaptiveRingSidecar::spawn_with_qos(
3823            ring.clone(),
3824            QosRingShapePolicy {
3825                qos: qos.clone(),
3826                hysteresis: std::time::Duration::from_millis(5),
3827            },
3828            DefaultOrderingPolicy {
3829                hysteresis: std::time::Duration::from_millis(5),
3830                auto_order_threshold: None,
3831            },
3832            qos.clone(),
3833            std::time::Duration::from_millis(10),
3834        );
3835
3836        // Declare GlobalFifo: on this STAMPED ring the sidecar must
3837        // flip the merge flag, never morph to Vyukov.
3838        qos.set_ordering(crate::qos_policy::Ordering::GlobalFifo);
3839        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
3840        while std::time::Instant::now() < deadline
3841            && ring.ordering_mode() != Some(OrderingMode::MergeByStamp)
3842        {
3843            std::thread::sleep(std::time::Duration::from_millis(10));
3844        }
3845        assert_eq!(ring.ordering_mode(), Some(OrderingMode::MergeByStamp),
3846                   "sidecar must arm the merge flag on the GlobalFifo declaration");
3847        assert_eq!(ring.current_shape(), RingShape::Mpsc,
3848                   "stamped ring must stay composed (no Vyukov morph)");
3849        assert!(sidecar.ordering_flips() >= 1);
3850
3851        // Withdraw the declaration: the sidecar disarms.
3852        qos.set_ordering(crate::qos_policy::Ordering::PerProducer);
3853        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
3854        while std::time::Instant::now() < deadline
3855            && ring.ordering_mode() != Some(OrderingMode::Unordered)
3856        {
3857            std::thread::sleep(std::time::Duration::from_millis(10));
3858        }
3859        assert_eq!(ring.ordering_mode(), Some(OrderingMode::Unordered),
3860                   "sidecar must disarm when the declaration is withdrawn");
3861        sidecar.shutdown();
3862    }
3863
3864    #[test]
3865    fn default_sidecar_gates_auto_arm_but_still_opens_on_sustained_inversions() {
3866        // The default `spawn_with_qos` now enables the ordering
3867        // auto-arm gate. This proves the gate OPENS under genuinely
3868        // sustained inversions (a one-way arm that never opened
3869        // would be useless): two producers race in Unordered mode,
3870        // the consumer observes cross-producer inversions, the
3871        // auto threshold pre-authorizes, and the gate commits the
3872        // single MergeByStamp flip once conviction accrues.
3873        let ring = Arc::new(stamped_anon(2, 1, StampKind::SharedCounter));
3874        ring.morph_to(RingShape::Mpsc).unwrap();
3875        ring.register_producer().unwrap();
3876        ring.register_producer().unwrap();
3877        ring.register_consumer().unwrap();
3878        ring.set_ordering_mode(OrderingMode::Unordered).unwrap();
3879
3880        let qos = Arc::new(crate::qos_policy::QosPolicy::default());
3881        qos.set_ordering(crate::qos_policy::Ordering::PerProducer);
3882        let sidecar = AdaptiveRingSidecar::spawn_with_qos(
3883            ring.clone(),
3884            DefaultRingShapePolicy::default(),
3885            DefaultOrderingPolicy {
3886                hysteresis: std::time::Duration::from_millis(0),
3887                auto_order_threshold: Some(50.0),
3888            },
3889            qos.clone(),
3890            std::time::Duration::from_millis(5),
3891        );
3892
3893        let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
3894        let stop_c = stop.clone();
3895        let r = ring.clone();
3896        let consumer = std::thread::spawn(move || {
3897            let mut out = [0u8; 64];
3898            while !stop_c.load(Ordering::Acquire) {
3899                r.try_recv(0, &mut out).ok();
3900            }
3901        });
3902
3903        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(6);
3904        let mut seq = 0u64;
3905        while std::time::Instant::now() < deadline
3906            && ring.ordering_mode() != Some(OrderingMode::MergeByStamp)
3907        {
3908            ring.try_send(0, &seq.to_le_bytes()).ok();
3909            ring.try_send(1, &seq.to_le_bytes()).ok();
3910            seq += 1;
3911        }
3912        stop.store(true, Ordering::Release);
3913        consumer.join().unwrap();
3914
3915        assert_eq!(ring.ordering_mode(), Some(OrderingMode::MergeByStamp),
3916                   "the gated auto-arm must still commit under sustained inversions");
3917        assert_eq!(sidecar.ordering_flips(), 1,
3918                   "the one-way auto-arm fires exactly once");
3919        sidecar.shutdown();
3920    }
3921}