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