Skip to main content

subetha_cxc/
api.rs

1//! Top-level user-facing IPC API.
2//!
3//! Wraps the [`MmfDispatcher`] family pick behind one type per
4//! access pattern, so callers express WHAT they want (streaming,
5//! work-stealing, key-value) and the dispatcher decides which
6//! MMF-backed primitive to use under the hood. The shape of the
7//! API mirrors `std::sync::mpsc::channel` but adds:
8//!
9//! - cross-process visibility via memory-mapped file backing
10//! - kernel-bypass data path (atomic protocol layer in user space)
11//! - per-workload routing to the empirically-best primitive
12//!
13//! Three intent types are exposed:
14//!
15//! - [`Channel<T>`]: streaming MPMC. Backed by [`SharedRing`].
16//! - [`WorkStealQueue<T>`]: single-owner, multi-thief work-stealing.
17//!   Backed by [`MmfDispatcher`]'s within-family pick (Chase-Lev /
18//!   KHPD / LOH / URD / KHL / Fcl) for the deque family. Currently
19//!   exposes the Chase-Lev `T: Marshal` surface; batched-fast deque
20//!   variants are routed through internally for byte-slice payloads.
21//! - [`KvMap<K, V>`]: key-value lookup. Backed by [`SharedHashMap`].
22//!
23//! ## Example
24//!
25//! ```no_run
26//! use subetha_cxc::api::Channel;
27//! use subetha_cxc::MmfWorkloadShape;
28//!
29//! let chan: Channel<u64> = Channel::create(
30//!     "/tmp/my-channel.bin",
31//!     MmfWorkloadShape::StreamingMpmc { n_producers: 4, n_consumers: 4 },
32//!     1024,
33//! ).expect("create channel");
34//! chan.send(&42).expect("send");
35//! let v = chan.recv().expect("recv");
36//! assert_eq!(v, 42);
37//! ```
38
39#![allow(clippy::missing_errors_doc)]
40
41use std::future::Future;
42use std::marker::PhantomData;
43use std::path::Path;
44use std::pin::Pin;
45use std::sync::{Arc, OnceLock};
46use std::sync::atomic::{AtomicBool, Ordering};
47use std::task::{Context, Poll, Waker};
48use std::time::{Duration, Instant};
49
50use parking_lot::Mutex;
51use subetha_core::Marshal;
52
53use crate::cross_process_waker::{CrossProcessWaker, WakerError, MAX_WAITERS_DEFAULT};
54use crate::dispatch_deque::DequeVariant;
55use crate::message_transport::TransportError;
56use crate::mmf_dispatcher::{MmfDispatcher, MmfFamily, MmfWorkloadShape};
57use crate::reactor::{spawn_seq_reactor, SeqReactor};
58use crate::shared_deque::SharedDeque;
59use crate::shared_hash_map::{InsertOutcome, MapError, SharedHashMap};
60use crate::shared_ring::{RingError, SharedRing, PAYLOAD_BYTES};
61
62/// Heal tick for a blocking wait with no caller deadline: a wake (the
63/// common path) returns far sooner, the tick only backstops a wake lost
64/// to the register/visibility race.
65pub(crate) const BLOCKING_HEAL: Duration = Duration::from_millis(1);
66
67/// Errors returned by the user-facing IPC types.
68#[derive(Debug)]
69pub enum ApiError {
70    /// Underlying transport error (full / empty / etc.).
71    Transport(TransportError),
72    /// Marshal codec error.
73    Marshal(subetha_core::MarshalError),
74    /// I/O error during MMF setup.
75    Io(std::io::Error),
76    /// Key-value map error.
77    Map(MapError),
78    /// The workload shape resolved to a family this type cannot wrap.
79    WrongFamily { wanted: &'static str, got: MmfFamily },
80    /// Payload too large for the chosen transport's wire format.
81    PayloadTooLarge,
82    /// A blocking send / recv hit its deadline before the ring made
83    /// progress.
84    Timeout,
85}
86
87impl std::fmt::Display for ApiError {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        match self {
90            Self::Transport(e) => write!(f, "transport: {e:?}"),
91            Self::Marshal(e) => write!(f, "marshal: {e:?}"),
92            Self::Io(e) => write!(f, "io: {e}"),
93            Self::Map(e) => write!(f, "map: {e:?}"),
94            Self::WrongFamily { wanted, got } => {
95                write!(f, "wrong family: wanted {wanted}, got {got:?}")
96            }
97            Self::PayloadTooLarge => write!(f, "payload too large for transport"),
98            Self::Timeout => write!(f, "blocking op timed out"),
99        }
100    }
101}
102
103impl std::error::Error for ApiError {}
104
105impl From<std::io::Error> for ApiError {
106    fn from(e: std::io::Error) -> Self {
107        Self::Io(e)
108    }
109}
110impl From<subetha_core::MarshalError> for ApiError {
111    fn from(e: subetha_core::MarshalError) -> Self {
112        Self::Marshal(e)
113    }
114}
115impl From<TransportError> for ApiError {
116    fn from(e: TransportError) -> Self {
117        Self::Transport(e)
118    }
119}
120impl From<MapError> for ApiError {
121    fn from(e: MapError) -> Self {
122        Self::Map(e)
123    }
124}
125impl From<crate::shared_ring::RingError> for ApiError {
126    fn from(e: crate::shared_ring::RingError) -> Self {
127        match e {
128            crate::shared_ring::RingError::Full => {
129                ApiError::Transport(TransportError::Full)
130            }
131            crate::shared_ring::RingError::Empty => {
132                ApiError::Transport(TransportError::Empty)
133            }
134            crate::shared_ring::RingError::PayloadTooLarge => {
135                ApiError::Transport(TransportError::PayloadTooLarge)
136            }
137            _ => ApiError::Transport(TransportError::Other),
138        }
139    }
140}
141
142/// Streaming MPMC channel, backed by [`SharedRing`]. Use this for
143/// arrival-order queues with multiple producers and multiple
144/// consumers (the canonical request-fanout / result-fanin shape).
145///
146/// The dispatcher confirms `SharedRing` is the right family for the
147/// caller's workload shape; if a different family is picked, the
148/// constructor returns [`ApiError::WrongFamily`].
149pub struct Channel<T: Marshal> {
150    ring: Arc<SharedRing>,
151    /// Producer fires on push; a blocking / awaiting recv waits on it.
152    consumer_waker: Arc<CrossProcessWaker>,
153    /// Consumer fires on pop; a blocking / awaiting send waits on it.
154    producer_waker: Arc<CrossProcessWaker>,
155    /// The awaiting consumer's `Waker` (fired directly in-process, or by
156    /// the recv reactor cross-process).
157    recv_slot: Arc<Mutex<Option<Waker>>>,
158    /// The awaiting producer's `Waker`.
159    send_slot: Arc<Mutex<Option<Waker>>>,
160    /// Reactors spawned on first async use; bridge the MMF waker to the
161    /// local slot when the peer is in another process.
162    recv_reactor: OnceLock<SeqReactor>,
163    send_reactor: OnceLock<SeqReactor>,
164    /// Set once a recv / send blocks or awaits. Gates the wake signal so
165    /// a pure-sync channel pays nothing for the async machinery.
166    has_recv_waiter: AtomicBool,
167    has_send_waiter: AtomicBool,
168    family: MmfFamily,
169    _phantom: PhantomData<T>,
170}
171
172fn waker_paths(base: &Path) -> (std::path::PathBuf, std::path::PathBuf) {
173    let mut cw = base.as_os_str().to_owned();
174    cw.push(".cw");
175    let mut pw = base.as_os_str().to_owned();
176    pw.push(".pw");
177    (std::path::PathBuf::from(cw), std::path::PathBuf::from(pw))
178}
179
180fn ring_err(e: RingError) -> ApiError {
181    match e {
182        RingError::Full => ApiError::Transport(TransportError::Full),
183        RingError::Empty => ApiError::Transport(TransportError::Empty),
184        RingError::PayloadTooLarge => ApiError::PayloadTooLarge,
185        RingError::IoError(k) => ApiError::Io(std::io::Error::from(k)),
186        _ => ApiError::Transport(TransportError::Other),
187    }
188}
189
190impl<T: Marshal> Channel<T> {
191    fn assemble(
192        ring: SharedRing,
193        consumer_waker: CrossProcessWaker,
194        producer_waker: CrossProcessWaker,
195        family: MmfFamily,
196    ) -> Self {
197        Self {
198            ring: Arc::new(ring),
199            consumer_waker: Arc::new(consumer_waker),
200            producer_waker: Arc::new(producer_waker),
201            recv_slot: Arc::new(Mutex::new(None)),
202            send_slot: Arc::new(Mutex::new(None)),
203            recv_reactor: OnceLock::new(),
204            send_reactor: OnceLock::new(),
205            has_recv_waiter: AtomicBool::new(false),
206            has_send_waiter: AtomicBool::new(false),
207            family,
208            _phantom: PhantomData,
209        }
210    }
211
212    // UFCS onto the concrete ring: `Arc<SharedRing>` also implements
213    // `MessageTransport`, so `self.ring.try_push` would resolve to the
214    // trait method (TransportError); these force the inherent
215    // `SharedRing` methods (RingError) the blocking paths match on.
216    #[inline]
217    fn ring_push(&self, payload: &[u8]) -> Result<(), RingError> {
218        SharedRing::try_push(&self.ring, payload)
219    }
220
221    #[inline]
222    fn ring_pop(&self, out: &mut [u8]) -> Result<usize, RingError> {
223        SharedRing::try_pop(&self.ring, out)
224    }
225
226    /// Create a channel at `path` with the given workload shape.
227    /// `capacity` is the ring slot count (rounded up to next pow2).
228    /// Two small adjacent waker files (`.cw` / `.pw`) carry the
229    /// blocking + async wakeups across processes.
230    pub fn create(
231        path: impl AsRef<Path>,
232        shape: MmfWorkloadShape,
233        capacity: usize,
234    ) -> Result<Self, ApiError> {
235        let family = MmfDispatcher::pick(shape);
236        if family != MmfFamily::SharedRing {
237            return Err(ApiError::WrongFamily {
238                wanted: "SharedRing",
239                got: family,
240            });
241        }
242        if T::PAYLOAD_BYTES > PAYLOAD_BYTES {
243            return Err(ApiError::PayloadTooLarge);
244        }
245        let (cw, pw) = waker_paths(path.as_ref());
246        let ring = SharedRing::create(path.as_ref(), capacity)?;
247        let consumer_waker = CrossProcessWaker::create(cw, MAX_WAITERS_DEFAULT)
248            .map_err(map_waker)?;
249        let producer_waker = CrossProcessWaker::create(pw, MAX_WAITERS_DEFAULT)
250            .map_err(map_waker)?;
251        Ok(Self::assemble(ring, consumer_waker, producer_waker, family))
252    }
253
254    /// Open an existing channel at `path`.
255    pub fn open(path: impl AsRef<Path>, capacity: usize) -> Result<Self, ApiError> {
256        if T::PAYLOAD_BYTES > PAYLOAD_BYTES {
257            return Err(ApiError::PayloadTooLarge);
258        }
259        let (cw, pw) = waker_paths(path.as_ref());
260        let ring = SharedRing::open(path.as_ref(), capacity)?;
261        let consumer_waker = CrossProcessWaker::open(cw, MAX_WAITERS_DEFAULT)
262            .map_err(map_waker)?;
263        let producer_waker = CrossProcessWaker::open(pw, MAX_WAITERS_DEFAULT)
264            .map_err(map_waker)?;
265        Ok(Self::assemble(
266            ring, consumer_waker, producer_waker, MmfFamily::SharedRing,
267        ))
268    }
269
270    /// Wake whoever waits to RECEIVE: the awaiting task's `Waker` and
271    /// any thread parked in `recv_blocking`. A pure-sync channel never
272    /// trips `has_recv_waiter`, so this returns on one relaxed load.
273    fn signal_consumer(&self) {
274        if !self.has_recv_waiter.load(Ordering::Relaxed) {
275            return;
276        }
277        if let Some(w) = self.recv_slot.lock().take() {
278            w.wake();
279        }
280        self.consumer_waker.wake_up_to(self.ring.producer_seq());
281    }
282
283    /// Wake whoever waits to SEND.
284    fn signal_producer(&self) {
285        if !self.has_send_waiter.load(Ordering::Relaxed) {
286            return;
287        }
288        if let Some(w) = self.send_slot.lock().take() {
289            w.wake();
290        }
291        self.producer_waker.wake_up_to(self.ring.consumer_seq());
292    }
293
294    fn ensure_recv_reactor(&self) {
295        self.recv_reactor.get_or_init(|| {
296            let ring = Arc::clone(&self.ring);
297            spawn_seq_reactor(
298                Arc::new(move || ring.producer_seq()),
299                Arc::clone(&self.consumer_waker),
300                Arc::clone(&self.recv_slot),
301            )
302        });
303    }
304
305    fn ensure_send_reactor(&self) {
306        self.send_reactor.get_or_init(|| {
307            let ring = Arc::clone(&self.ring);
308            spawn_seq_reactor(
309                Arc::new(move || ring.consumer_seq()),
310                Arc::clone(&self.producer_waker),
311                Arc::clone(&self.send_slot),
312            )
313        });
314    }
315
316    fn marshal_buf(item: &T) -> ([u8; PAYLOAD_BYTES], usize) {
317        let mut buf = [0u8; PAYLOAD_BYTES];
318        item.marshal(&mut buf[..T::PAYLOAD_BYTES]);
319        (buf, T::PAYLOAD_BYTES)
320    }
321
322    fn unmarshal_buf(buf: &[u8], n: usize) -> Result<T, ApiError> {
323        Ok(T::unmarshal(&buf[..n.min(T::PAYLOAD_BYTES.max(1))])?)
324    }
325
326    /// Non-blocking send. `Err(Transport(Full))` when the ring is full.
327    pub fn send(&self, item: &T) -> Result<(), ApiError> {
328        let (buf, len) = Self::marshal_buf(item);
329        self.ring_push(&buf[..len]).map_err(ring_err)?;
330        self.signal_consumer();
331        Ok(())
332    }
333
334    /// Non-blocking recv. `Err(Transport(Empty))` when the ring is empty.
335    pub fn recv(&self) -> Result<T, ApiError> {
336        let mut buf = [0u8; PAYLOAD_BYTES];
337        let n = self.ring_pop(&mut buf).map_err(ring_err)?;
338        self.signal_producer();
339        Self::unmarshal_buf(&buf, n)
340    }
341
342    /// Blocking send: parks the calling thread until space frees up (or
343    /// `timeout` elapses). `None` waits indefinitely.
344    pub fn send_blocking(
345        &self,
346        item: &T,
347        timeout: Option<Duration>,
348    ) -> Result<(), ApiError> {
349        self.has_send_waiter.store(true, Ordering::Relaxed);
350        let (buf, len) = Self::marshal_buf(item);
351        let deadline = timeout.map(|d| Instant::now() + d);
352        loop {
353            match self.ring_push(&buf[..len]) {
354                Ok(()) => {
355                    self.signal_consumer();
356                    return Ok(());
357                }
358                Err(RingError::Full) => {}
359                Err(e) => return Err(ring_err(e)),
360            }
361            let seen = self.ring.consumer_seq();
362            let token = self.producer_waker.try_park(seen + 1).map_err(map_waker)?;
363            // Re-attempt after registering: take space freed during the
364            // park instead of sleeping over it.
365            match self.ring_push(&buf[..len]) {
366                Ok(()) => {
367                    self.producer_waker.release(token);
368                    self.signal_consumer();
369                    return Ok(());
370                }
371                Err(RingError::Full) => {}
372                Err(e) => {
373                    self.producer_waker.release(token);
374                    return Err(ring_err(e));
375                }
376            }
377            match wait_heal(&self.producer_waker, token, deadline) {
378                Ok(()) => continue,
379                Err(e) => {
380                    return Err(e);
381                }
382            }
383        }
384    }
385
386    /// Blocking recv: parks the calling thread until an item arrives (or
387    /// `timeout` elapses). `None` waits indefinitely.
388    pub fn recv_blocking(&self, timeout: Option<Duration>) -> Result<T, ApiError> {
389        self.has_recv_waiter.store(true, Ordering::Relaxed);
390        let deadline = timeout.map(|d| Instant::now() + d);
391        let mut buf = [0u8; PAYLOAD_BYTES];
392        loop {
393            match self.ring_pop(&mut buf) {
394                Ok(n) => {
395                    self.signal_producer();
396                    return Self::unmarshal_buf(&buf, n);
397                }
398                Err(RingError::Empty) => {}
399                Err(e) => return Err(ring_err(e)),
400            }
401            let seen = self.ring.producer_seq();
402            let token = self.consumer_waker.try_park(seen + 1).map_err(map_waker)?;
403            match self.ring_pop(&mut buf) {
404                Ok(n) => {
405                    self.consumer_waker.release(token);
406                    self.signal_producer();
407                    return Self::unmarshal_buf(&buf, n);
408                }
409                Err(RingError::Empty) => {}
410                Err(e) => {
411                    self.consumer_waker.release(token);
412                    return Err(ring_err(e));
413                }
414            }
415            match wait_heal(&self.consumer_waker, token, deadline) {
416                Ok(()) => continue,
417                Err(e) => return Err(e),
418            }
419        }
420    }
421
422    /// Async recv. Resolves to the next item, suspending the task while
423    /// the ring is empty. Spawns a recv reactor on first call so the
424    /// wake bridges across processes.
425    pub fn recv_async(&self) -> RecvFut<'_, T> {
426        self.has_recv_waiter.store(true, Ordering::Relaxed);
427        self.ensure_recv_reactor();
428        RecvFut { chan: self }
429    }
430
431    /// Async send. Resolves once the item is in the ring, suspending the
432    /// task while it is full.
433    pub fn send_async(&self, item: &T) -> SendFut<'_, T> {
434        self.has_send_waiter.store(true, Ordering::Relaxed);
435        self.ensure_send_reactor();
436        let (buf, len) = Self::marshal_buf(item);
437        SendFut { chan: self, buf, len }
438    }
439
440    /// The family the dispatcher picked at construction.
441    pub fn family(&self) -> MmfFamily {
442        self.family
443    }
444}
445
446pub(crate) fn map_waker(e: WakerError) -> ApiError {
447    match e {
448        WakerError::Timeout => ApiError::Timeout,
449        WakerError::IoError(k) => ApiError::Io(std::io::Error::from(k)),
450        _ => ApiError::Transport(TransportError::Other),
451    }
452}
453
454/// Heal-bounded wait: a real wake (the common path) ends it fast; an
455/// unbounded caller still re-checks the ring on each tick, so a lost
456/// wake self-heals instead of hanging.
457pub(crate) fn wait_heal(
458    waker: &CrossProcessWaker,
459    token: crate::cross_process_waker::WakerToken,
460    deadline: Option<Instant>,
461) -> Result<(), ApiError> {
462    let wait_for = match deadline {
463        None => BLOCKING_HEAL,
464        Some(d) => {
465            let now = Instant::now();
466            if now >= d {
467                waker.release(token);
468                return Err(ApiError::Timeout);
469            }
470            (d - now).min(BLOCKING_HEAL)
471        }
472    };
473    match waker.wait(token, Some(wait_for)) {
474        Ok(()) | Err(WakerError::Timeout) => Ok(()),
475        Err(e) => Err(map_waker(e)),
476    }
477}
478
479/// Future from [`Channel::recv_async`].
480pub struct RecvFut<'a, T: Marshal> {
481    chan: &'a Channel<T>,
482}
483
484impl<'a, T: Marshal> Future for RecvFut<'a, T> {
485    type Output = Result<T, ApiError>;
486
487    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
488        let c = self.chan;
489        let mut buf = [0u8; PAYLOAD_BYTES];
490        if let Ok(n) = c.ring_pop(&mut buf) {
491            c.signal_producer();
492            return Poll::Ready(Channel::<T>::unmarshal_buf(&buf, n));
493        }
494        *c.recv_slot.lock() = Some(cx.waker().clone());
495        if let Ok(n) = c.ring_pop(&mut buf) {
496            c.signal_producer();
497            return Poll::Ready(Channel::<T>::unmarshal_buf(&buf, n));
498        }
499        Poll::Pending
500    }
501}
502
503/// Future from [`Channel::send_async`].
504pub struct SendFut<'a, T: Marshal> {
505    chan: &'a Channel<T>,
506    buf: [u8; PAYLOAD_BYTES],
507    len: usize,
508}
509
510impl<'a, T: Marshal> Future for SendFut<'a, T> {
511    type Output = Result<(), ApiError>;
512
513    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
514        let this = self.get_mut();
515        if this.chan.ring_push(&this.buf[..this.len]).is_ok() {
516            this.chan.signal_consumer();
517            return Poll::Ready(Ok(()));
518        }
519        *this.chan.send_slot.lock() = Some(cx.waker().clone());
520        if this.chan.ring_push(&this.buf[..this.len]).is_ok() {
521            this.chan.signal_consumer();
522            return Poll::Ready(Ok(()));
523        }
524        Poll::Pending
525    }
526}
527
528/// Single-owner, multi-thief work-stealing queue, backed by the
529/// [`SharedDeque<T>`](crate::SharedDeque) family. The owner pushes
530/// via [`push`](Self::push); thieves drain via [`steal`](Self::steal).
531///
532/// The dispatcher's variant pick for the caller's workload shape is
533/// reported via [`variant`](Self::variant); the generic-`T: Marshal`
534/// surface uses Chase-Lev as the backing because it is the only
535/// variant generic over arbitrary `T: Marshal`. Byte-slice
536/// workloads (e.g. scheduler `PassSlot`) can ride the higher-
537/// throughput KHL/KHPD/LOH/URD variants directly via the
538/// [`DequeDispatcher`](crate::DequeDispatcher) API.
539pub struct WorkStealQueue<T: Marshal + Copy + 'static> {
540    owner: Arc<SharedDeque<T>>,
541    variant: DequeVariant,
542}
543
544impl<T: Marshal + Copy + 'static> WorkStealQueue<T> {
545    /// Create the queue at `path` with the given workload shape.
546    /// `capacity` is rounded up to next pow2.
547    pub fn create(
548        path: impl AsRef<Path>,
549        shape: MmfWorkloadShape,
550        capacity: usize,
551    ) -> Result<Self, ApiError> {
552        let family = MmfDispatcher::pick(shape);
553        let variant = match family {
554            MmfFamily::SharedDeque(v) => v,
555            other => {
556                return Err(ApiError::WrongFamily {
557                    wanted: "SharedDeque",
558                    got: other,
559                });
560            }
561        };
562        let owner = SharedDeque::<T>::create(path.as_ref(), capacity)?;
563        Ok(Self {
564            owner: Arc::new(owner),
565            variant,
566        })
567    }
568
569    /// Open an existing queue at `path` as a thief.
570    pub fn open_as_thief(path: impl AsRef<Path>) -> Result<Self, ApiError> {
571        let thief = SharedDeque::<T>::open_as_thief(path.as_ref())?;
572        Ok(Self {
573            owner: Arc::new(thief),
574            variant: DequeVariant::ChaseLev,
575        })
576    }
577
578    /// Owner-side push.
579    pub fn push(&self, item: &T) -> Result<(), ApiError> {
580        self.owner.push(item)?;
581        Ok(())
582    }
583
584    /// Owner-side pop (LIFO end).
585    pub fn pop(&self) -> Option<T> {
586        self.owner.pop()
587    }
588
589    /// Thief-side steal (FIFO end).
590    pub fn steal(&self) -> Option<T> {
591        self.owner.steal()
592    }
593
594    /// The variant the dispatcher picked for this workload.
595    pub fn variant(&self) -> DequeVariant {
596        self.variant
597    }
598}
599
600impl From<crate::shared_deque::DequeError> for ApiError {
601    fn from(e: crate::shared_deque::DequeError) -> Self {
602        match e {
603            crate::shared_deque::DequeError::Full => {
604                ApiError::Transport(TransportError::Full)
605            }
606            _ => ApiError::Transport(TransportError::Other),
607        }
608    }
609}
610
611/// Key-value lookup map, backed by [`SharedHashMap`]. Multiple
612/// processes can insert + look up concurrently via the shared MMF.
613///
614/// The dispatcher confirms `SharedHashMap` is the right family;
615/// if a different family is picked, the constructor returns
616/// [`ApiError::WrongFamily`].
617pub struct KvMap<K: Copy + Eq + Send + Sync + 'static, V: Copy + Send + Sync + 'static> {
618    map: Arc<SharedHashMap<K, V>>,
619}
620
621impl<K, V> KvMap<K, V>
622where
623    K: Copy + Eq + std::hash::Hash + Send + Sync + 'static,
624    V: Copy + Send + Sync + 'static,
625{
626    /// Create the map at `path` with the given workload shape.
627    /// `capacity` is rounded up to next pow2.
628    pub fn create(
629        path: impl AsRef<Path>,
630        shape: MmfWorkloadShape,
631        capacity: usize,
632    ) -> Result<Self, ApiError> {
633        let family = MmfDispatcher::pick(shape);
634        if family != MmfFamily::SharedHashMap {
635            return Err(ApiError::WrongFamily {
636                wanted: "SharedHashMap",
637                got: family,
638            });
639        }
640        let map = SharedHashMap::<K, V>::create(path.as_ref(), capacity)?;
641        Ok(Self { map: Arc::new(map) })
642    }
643
644    /// Insert a key-value pair.
645    pub fn insert(&self, key: K, value: V) -> Result<InsertOutcome, ApiError> {
646        let outcome = self.map.insert(key, value)?;
647        Ok(outcome)
648    }
649
650    /// Look up a value by key.
651    pub fn get(&self, key: &K) -> Option<V> {
652        self.map.get(key)
653    }
654
655    /// Current number of occupied slots.
656    pub fn len(&self) -> usize {
657        self.map.len()
658    }
659
660    /// `true` if the map has no occupied slots.
661    pub fn is_empty(&self) -> bool {
662        self.map.len() == 0
663    }
664}
665
666/// Builder for an auto-inferred IPC endpoint. The caller describes
667/// the workload with declarative hints; the builder infers the
668/// `MmfWorkloadShape`, asks [`MmfDispatcher`] for the family, and
669/// constructs the right typed-intent wrapper.
670///
671/// No workload shape, no family enum, no primitive choice ever
672/// touches the user. They write:
673///
674/// ```no_run
675/// use subetha_cxc::AutoIpc;
676///
677/// let auto = AutoIpc::new("/tmp/auto-ipc.bin")
678///     .producers(4)
679///     .consumers(4)
680///     .batch_size(64)
681///     .capacity(1024)
682///     .build_channel::<u64>()
683///     .expect("create");
684/// auto.send(&42).expect("send");
685/// ```
686///
687/// The builder picks streaming MPMC when there are multiple
688/// producers / consumers without a single-owner constraint;
689/// work-stealing when there is one producer and multiple consumers
690/// with a batch hint; key-value when the caller selects
691/// `build_kv_map`. The inference is zero-cost: it runs once at
692/// `build_*`, never per-op.
693pub struct AutoIpc {
694    path: std::path::PathBuf,
695    n_producers: usize,
696    n_consumers: usize,
697    batch_size: Option<usize>,
698    wait_idle: bool,
699    capacity: usize,
700    ordering: crate::qos_policy::Ordering,
701    auto_order: Option<f64>,
702}
703
704impl AutoIpc {
705    /// Start a new auto-inferred IPC endpoint at `path`.
706    /// Defaults: 1 producer, 1 consumer, no batch, capacity 64,
707    /// per-producer ordering, no auto-order threshold.
708    pub fn new(path: impl Into<std::path::PathBuf>) -> Self {
709        Self {
710            path: path.into(),
711            n_producers: 1,
712            n_consumers: 1,
713            batch_size: None,
714            wait_idle: false,
715            capacity: 64,
716            ordering: crate::qos_policy::Ordering::PerProducer,
717            auto_order: None,
718        }
719    }
720
721    /// Number of producers expected to push concurrently.
722    pub fn producers(mut self, n: usize) -> Self {
723        self.n_producers = n.max(1);
724        self
725    }
726
727    /// Number of consumers expected to drain concurrently.
728    pub fn consumers(mut self, n: usize) -> Self {
729        self.n_consumers = n.max(1);
730        self
731    }
732
733    /// Hint that the producer will publish batches of `k` items.
734    /// Setting this is what flips a single-producer streaming
735    /// workload into work-stealing routing.
736    pub fn batch_size(mut self, k: usize) -> Self {
737        self.batch_size = Some(k);
738        self
739    }
740
741    /// Hint that consumers should idle-wait between batches
742    /// (WAITPKG on capable silicon; PAUSE-spin otherwise).
743    pub fn idle_wait(mut self, on: bool) -> Self {
744        self.wait_idle = on;
745        self
746    }
747
748    /// Ring slot capacity (rounded to next pow2).
749    pub fn capacity(mut self, n: usize) -> Self {
750        self.capacity = n.max(2);
751        self
752    }
753
754    /// Declare the ordering requirement. `GlobalFifo` constrains
755    /// the inference to the streaming family (a work-stealing
756    /// deque's LIFO owner end cannot honor FIFO at all), and
757    /// [`build_adaptive`](Self::build_adaptive) applies the
758    /// declaration to the stamped ring's merge flag.
759    pub fn ordering(mut self, ordering: crate::qos_policy::Ordering) -> Self {
760        self.ordering = ordering;
761        self
762    }
763
764    /// Pre-authorize an automatic ordering response: when the
765    /// built endpoint observes more than `threshold` cross-producer
766    /// inversions per second, its sidecar arms global-FIFO delivery
767    /// (the stamped merge) without a further declaration. Effective
768    /// through [`build_adaptive`](Self::build_adaptive), which
769    /// constructs the stamped ring the response needs.
770    pub fn auto_order(mut self, threshold: f64) -> Self {
771        self.auto_order = Some(threshold);
772        self
773    }
774
775    /// Infer the workload shape from the declared hints.
776    pub fn inferred_shape(&self) -> MmfWorkloadShape {
777        // GlobalFifo pins the inference to the streaming family:
778        // deques cannot honor cross-producer FIFO.
779        if self.ordering == crate::qos_policy::Ordering::GlobalFifo {
780            return MmfWorkloadShape::StreamingMpmc {
781                n_producers: self.n_producers,
782                n_consumers: self.n_consumers,
783            };
784        }
785        // n_producers >= 2 OR n_consumers >= 2 with no batch +
786        // streaming intent -> streaming MPMC.
787        // single-producer + batch_size hint -> work-stealing.
788        // wait_idle -> work-stealing (URD).
789        if self.batch_size.is_some() || self.wait_idle {
790            MmfWorkloadShape::WorkStealing(
791                crate::dispatch_deque::WorkloadShape {
792                    n_thieves: self.n_consumers,
793                    batch_size: self.batch_size,
794                    wait_idle: self.wait_idle,
795                },
796            )
797        } else if self.n_producers >= 2 || self.n_consumers >= 2 {
798            MmfWorkloadShape::StreamingMpmc {
799                n_producers: self.n_producers,
800                n_consumers: self.n_consumers,
801            }
802        } else {
803            // Single producer, single consumer, no batch -> degenerate
804            // streaming case (one-to-one queue). SharedRing handles it.
805            MmfWorkloadShape::StreamingMpmc {
806                n_producers: 1,
807                n_consumers: 1,
808            }
809        }
810    }
811
812    /// Inferred family pick (informational; no construction).
813    pub fn inferred_family(&self) -> MmfFamily {
814        MmfDispatcher::pick(self.inferred_shape())
815    }
816
817    /// Build a streaming MPMC channel for `T: Marshal`. Returns
818    /// `WrongFamily` if the inferred shape resolves to something
819    /// other than `SharedRing` (e.g. you set `batch_size` and the
820    /// inference picked work-stealing).
821    pub fn build_channel<T: Marshal>(self) -> Result<Channel<T>, ApiError> {
822        let shape = self.inferred_shape();
823        Channel::<T>::create(&self.path, shape, self.capacity)
824    }
825
826    /// Build a work-stealing queue. Returns `WrongFamily` if the
827    /// inferred shape is not work-stealing (call `batch_size` to
828    /// force work-stealing inference) or if `GlobalFifo` ordering
829    /// was declared (a deque's LIFO owner end cannot honor FIFO).
830    pub fn build_work_steal_queue<T: Marshal + Copy + 'static>(
831        self,
832    ) -> Result<WorkStealQueue<T>, ApiError> {
833        if self.ordering == crate::qos_policy::Ordering::GlobalFifo {
834            return Err(ApiError::WrongFamily {
835                wanted: "SharedRing (GlobalFifo ordering declared)",
836                got: MmfFamily::SharedDeque(
837                    crate::dispatch_deque::DequeVariant::ChaseLev,
838                ),
839            });
840        }
841        // Force work-stealing inference when this method is called.
842        let shape = MmfWorkloadShape::WorkStealing(
843            crate::dispatch_deque::WorkloadShape {
844                n_thieves: self.n_consumers,
845                batch_size: self.batch_size,
846                wait_idle: self.wait_idle,
847            },
848        );
849        WorkStealQueue::<T>::create(&self.path, shape, self.capacity)
850    }
851
852    /// Build an [`AdaptiveIpc`](crate::AdaptiveIpc) endpoint with
853    /// the ordering axis wired through: the inner ring carries push
854    /// stamps, the [`ordering`](Self::ordering) declaration is
855    /// applied at construction (GlobalFifo = stamped merge ON), and
856    /// an [`auto_order`](Self::auto_order) threshold pre-authorizes
857    /// the sidecar's automatic arm on observed inversion rate.
858    pub fn build_adaptive<T: Marshal + Copy + 'static>(
859        self,
860    ) -> Result<crate::AdaptiveIpc<T>, ApiError> {
861        let shape = self.inferred_shape();
862        crate::AdaptiveIpc::<T>::create_with_ordering(
863            &self.path,
864            shape,
865            self.capacity,
866            self.n_consumers,
867            self.ordering,
868            self.auto_order,
869        )
870    }
871
872    /// Build a key-value map. The caller declares key-value intent
873    /// by calling this method (key-value access doesn't share
874    /// signature axes with streaming / work-stealing).
875    pub fn build_kv_map<K, V>(self) -> Result<KvMap<K, V>, ApiError>
876    where
877        K: Copy + Eq + std::hash::Hash + Send + Sync + 'static,
878        V: Copy + Send + Sync + 'static,
879    {
880        let shape = MmfWorkloadShape::KeyValueLookup {
881            n_readers: self.n_consumers,
882            n_writers: self.n_producers,
883        };
884        KvMap::<K, V>::create(&self.path, shape, self.capacity)
885    }
886}
887
888#[cfg(test)]
889mod tests {
890    use super::*;
891    use crate::dispatch_deque::WorkloadShape;
892
893    fn tmp(name: &str) -> std::path::PathBuf {
894        let mut p = std::env::temp_dir();
895        let pid = std::process::id();
896        let nonce = std::time::SystemTime::now()
897            .duration_since(std::time::UNIX_EPOCH)
898            .map(|d| d.as_nanos())
899            .unwrap_or(0);
900        p.push(format!("subetha_api_{pid}_{nonce}_{name}.bin"));
901        p
902    }
903
904    // A tiny Marshal type for the channel tests.
905    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
906    struct U32Item(u32);
907
908    unsafe impl Marshal for U32Item {
909        const PAYLOAD_BYTES: usize = 4;
910        fn marshal(&self, dst: &mut [u8]) {
911            dst[..4].copy_from_slice(&self.0.to_le_bytes());
912        }
913        fn unmarshal(src: &[u8]) -> Result<Self, subetha_core::MarshalError> {
914            if src.len() < 4 {
915                return Err(subetha_core::MarshalError::ShortBuffer {
916                    expected: 4,
917                    got: src.len(),
918                });
919            }
920            Ok(U32Item(u32::from_le_bytes(src[..4].try_into().unwrap())))
921        }
922    }
923
924    #[test]
925    fn channel_round_trips_via_streaming_shape() {
926        let path = tmp("channel");
927        let shape = MmfWorkloadShape::StreamingMpmc {
928            n_producers: 1,
929            n_consumers: 1,
930        };
931        let chan: Channel<U32Item> = Channel::create(&path, shape, 64).expect("create");
932        assert_eq!(chan.family(), MmfFamily::SharedRing);
933        chan.send(&U32Item(42)).expect("send");
934        let v = chan.recv().expect("recv");
935        assert_eq!(v, U32Item(42));
936        std::fs::remove_file(&path).ok();
937    }
938
939    #[test]
940    fn channel_rejects_wrong_family() {
941        let path = tmp("channel_wrong_family");
942        let bad_shape = MmfWorkloadShape::KeyValueLookup {
943            n_readers: 1,
944            n_writers: 1,
945        };
946        let result = Channel::<U32Item>::create(&path, bad_shape, 64);
947        match result {
948            Err(ApiError::WrongFamily {
949                wanted: "SharedRing",
950                got: MmfFamily::SharedHashMap,
951            }) => {}
952            Err(other) => panic!("expected WrongFamily, got {other:?}"),
953            Ok(_) => panic!("expected error, got Ok"),
954        }
955        std::fs::remove_file(&path).ok();
956    }
957
958    #[test]
959    fn work_steal_queue_round_trips_via_request_reply_shape() {
960        let path = tmp("wsq");
961        let shape = MmfWorkloadShape::WorkStealing(WorkloadShape::request_reply());
962        let q: WorkStealQueue<u64> = WorkStealQueue::create(&path, shape, 64).expect("create");
963        // request_reply -> ChaseLev (per-item).
964        assert_eq!(q.variant(), DequeVariant::ChaseLev);
965        q.push(&100).expect("push");
966        q.push(&200).expect("push");
967        // pop is LIFO end (owner) -> 200 first.
968        assert_eq!(q.pop(), Some(200));
969        // steal is FIFO end (thief) -> 100 next.
970        assert_eq!(q.steal(), Some(100));
971        std::fs::remove_file(&path).ok();
972    }
973
974    #[test]
975    fn kv_map_round_trips_via_key_value_shape() {
976        let path = tmp("kv");
977        let shape = MmfWorkloadShape::KeyValueLookup {
978            n_readers: 1,
979            n_writers: 1,
980        };
981        let map: KvMap<u32, u32> = KvMap::create(&path, shape, 64).expect("create");
982        for k in 0..10u32 {
983            map.insert(k, k * k).expect("insert");
984        }
985        for k in 0..10u32 {
986            assert_eq!(map.get(&k), Some(k * k));
987        }
988        assert_eq!(map.len(), 10);
989        std::fs::remove_file(&path).ok();
990    }
991
992    #[test]
993    fn auto_ipc_default_infers_streaming_one_to_one() {
994        let auto = AutoIpc::new("/tmp/test-default.bin");
995        let shape = auto.inferred_shape();
996        assert!(matches!(shape, MmfWorkloadShape::StreamingMpmc { .. }));
997        assert_eq!(auto.inferred_family(), MmfFamily::SharedRing);
998    }
999
1000    #[test]
1001    fn auto_ipc_multi_producer_infers_streaming_mpmc() {
1002        let auto = AutoIpc::new("/tmp/test-mp.bin").producers(4).consumers(4);
1003        assert_eq!(auto.inferred_family(), MmfFamily::SharedRing);
1004    }
1005
1006    #[test]
1007    fn auto_ipc_batch_hint_flips_to_work_stealing() {
1008        let auto = AutoIpc::new("/tmp/test-batch.bin").batch_size(64);
1009        let shape = auto.inferred_shape();
1010        assert!(matches!(shape, MmfWorkloadShape::WorkStealing(_)));
1011        // Single-thief batched -> KHL.
1012        assert_eq!(
1013            auto.inferred_family(),
1014            MmfFamily::SharedDeque(DequeVariant::Khl)
1015        );
1016    }
1017
1018    #[test]
1019    fn auto_ipc_multi_consumer_plus_batch_infers_urd() {
1020        let auto = AutoIpc::new("/tmp/test-mt.bin")
1021            .consumers(4)
1022            .batch_size(64);
1023        assert_eq!(
1024            auto.inferred_family(),
1025            MmfFamily::SharedDeque(DequeVariant::Urd)
1026        );
1027    }
1028
1029    #[test]
1030    fn auto_ipc_idle_wait_routes_to_urd() {
1031        let auto = AutoIpc::new("/tmp/test-idle.bin").idle_wait(true);
1032        assert_eq!(
1033            auto.inferred_family(),
1034            MmfFamily::SharedDeque(DequeVariant::Urd)
1035        );
1036    }
1037
1038    #[test]
1039    fn auto_ipc_build_channel_end_to_end_round_trip() {
1040        let path = tmp("auto_ch");
1041        let auto = AutoIpc::new(&path).capacity(64);
1042        let chan: Channel<U32Item> = auto.build_channel().expect("build");
1043        chan.send(&U32Item(123)).expect("send");
1044        let v = chan.recv().expect("recv");
1045        assert_eq!(v, U32Item(123));
1046        std::fs::remove_file(&path).ok();
1047    }
1048
1049    #[test]
1050    fn auto_ipc_build_work_steal_queue_with_batch_hint() {
1051        let path = tmp("auto_wsq");
1052        let q: WorkStealQueue<u64> = AutoIpc::new(&path)
1053            .batch_size(8)
1054            .capacity(64)
1055            .build_work_steal_queue()
1056            .expect("build");
1057        q.push(&10).expect("push");
1058        q.push(&20).expect("push");
1059        assert_eq!(q.pop(), Some(20));
1060        assert_eq!(q.steal(), Some(10));
1061        std::fs::remove_file(&path).ok();
1062    }
1063
1064    #[test]
1065    fn auto_ipc_build_kv_map() {
1066        let path = tmp("auto_kv");
1067        let map: KvMap<u32, u32> = AutoIpc::new(&path)
1068            .capacity(64)
1069            .build_kv_map()
1070            .expect("build");
1071        map.insert(7, 49).expect("insert");
1072        assert_eq!(map.get(&7), Some(49));
1073        std::fs::remove_file(&path).ok();
1074    }
1075
1076    #[test]
1077    fn auto_ipc_global_fifo_forces_streaming_inference() {
1078        // A batch hint normally flips the inference to work-stealing;
1079        // the GlobalFifo declaration overrides it (deques cannot
1080        // honor cross-producer FIFO).
1081        let auto = AutoIpc::new("/tmp/test-fifo.bin")
1082            .producers(4)
1083            .batch_size(64)
1084            .ordering(crate::qos_policy::Ordering::GlobalFifo);
1085        assert!(matches!(
1086            auto.inferred_shape(),
1087            MmfWorkloadShape::StreamingMpmc { .. }
1088        ));
1089        assert_eq!(auto.inferred_family(), MmfFamily::SharedRing);
1090    }
1091
1092    #[test]
1093    fn auto_ipc_global_fifo_rejects_work_steal_queue() {
1094        let path = tmp("fifo_wsq");
1095        let result = AutoIpc::new(&path)
1096            .batch_size(8)
1097            .ordering(crate::qos_policy::Ordering::GlobalFifo)
1098            .build_work_steal_queue::<u64>();
1099        assert!(matches!(result, Err(ApiError::WrongFamily { .. })),
1100                "GlobalFifo + work-stealing must be rejected, got Ok or wrong error");
1101        std::fs::remove_file(&path).ok();
1102    }
1103
1104    #[test]
1105    fn auto_ipc_build_adaptive_with_ordering_round_trips() {
1106        let path = tmp("auto_adaptive");
1107        let ipc = AutoIpc::new(&path)
1108            .capacity(64)
1109            .ordering(crate::qos_policy::Ordering::GlobalFifo)
1110            .build_adaptive::<u64>()
1111            .expect("build");
1112        assert!(ipc.ring_handle().is_stamped(),
1113                "build_adaptive must construct the stamped ring");
1114        assert_eq!(ipc.ordering(), crate::qos_policy::Ordering::GlobalFifo);
1115        ipc.send(&31337).expect("send");
1116        assert_eq!(ipc.recv().expect("recv"), 31337);
1117    }
1118
1119    #[test]
1120    fn auto_ipc_auto_order_threshold_reaches_adaptive_endpoint() {
1121        let path = tmp("auto_threshold");
1122        let ipc = AutoIpc::new(&path)
1123            .capacity(64)
1124            .auto_order(5.0)
1125            .build_adaptive::<u64>()
1126            .expect("build");
1127        assert!(ipc.ring_handle().is_stamped(),
1128                "auto_order requires the stamped ring and build_adaptive must provide it");
1129        assert_eq!(ipc.ordering(), crate::qos_policy::Ordering::PerProducer,
1130                   "auto_order alone must not pre-arm the merge");
1131    }
1132
1133    #[test]
1134    fn kv_map_rejects_streaming_shape() {
1135        let path = tmp("kv_wrong");
1136        let bad_shape = MmfWorkloadShape::StreamingMpmc {
1137            n_producers: 1,
1138            n_consumers: 1,
1139        };
1140        let result = KvMap::<u32, u32>::create(&path, bad_shape, 64);
1141        match result {
1142            Err(ApiError::WrongFamily {
1143                wanted: "SharedHashMap",
1144                got: MmfFamily::SharedRing,
1145            }) => {}
1146            Err(other) => panic!("expected WrongFamily, got {other:?}"),
1147            Ok(_) => panic!("expected error, got Ok"),
1148        }
1149        std::fs::remove_file(&path).ok();
1150    }
1151}