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, clamped to `>= 2` and rounded up to the next power
749    /// of two. Every terminal's backing store requires a pow2 capacity.
750    pub fn capacity(mut self, n: usize) -> Self {
751        self.capacity = n.max(2).next_power_of_two();
752        self
753    }
754
755    /// Declare the ordering requirement. `GlobalFifo` constrains
756    /// the inference to the streaming family (a work-stealing
757    /// deque's LIFO owner end cannot honor FIFO at all), and
758    /// [`build_adaptive`](Self::build_adaptive) applies the
759    /// declaration to the stamped ring's merge flag.
760    pub fn ordering(mut self, ordering: crate::qos_policy::Ordering) -> Self {
761        self.ordering = ordering;
762        self
763    }
764
765    /// Pre-authorize an automatic ordering response: when the
766    /// built endpoint observes more than `threshold` cross-producer
767    /// inversions per second, its sidecar arms global-FIFO delivery
768    /// (the stamped merge) without a further declaration. Effective
769    /// through [`build_adaptive`](Self::build_adaptive), which
770    /// constructs the stamped ring the response needs.
771    pub fn auto_order(mut self, threshold: f64) -> Self {
772        self.auto_order = Some(threshold);
773        self
774    }
775
776    /// Infer the workload shape from the declared hints.
777    pub fn inferred_shape(&self) -> MmfWorkloadShape {
778        // GlobalFifo pins the inference to the streaming family:
779        // deques cannot honor cross-producer FIFO.
780        if self.ordering == crate::qos_policy::Ordering::GlobalFifo {
781            return MmfWorkloadShape::StreamingMpmc {
782                n_producers: self.n_producers,
783                n_consumers: self.n_consumers,
784            };
785        }
786        // n_producers >= 2 OR n_consumers >= 2 with no batch +
787        // streaming intent -> streaming MPMC.
788        // single-producer + batch_size hint -> work-stealing.
789        // wait_idle -> work-stealing (URD).
790        if self.batch_size.is_some() || self.wait_idle {
791            MmfWorkloadShape::WorkStealing(
792                crate::dispatch_deque::WorkloadShape {
793                    n_thieves: self.n_consumers,
794                    batch_size: self.batch_size,
795                    wait_idle: self.wait_idle,
796                },
797            )
798        } else if self.n_producers >= 2 || self.n_consumers >= 2 {
799            MmfWorkloadShape::StreamingMpmc {
800                n_producers: self.n_producers,
801                n_consumers: self.n_consumers,
802            }
803        } else {
804            // Single producer, single consumer, no batch -> degenerate
805            // streaming case (one-to-one queue). SharedRing handles it.
806            MmfWorkloadShape::StreamingMpmc {
807                n_producers: 1,
808                n_consumers: 1,
809            }
810        }
811    }
812
813    /// Inferred family pick (informational; no construction).
814    pub fn inferred_family(&self) -> MmfFamily {
815        MmfDispatcher::pick(self.inferred_shape())
816    }
817
818    /// Build a streaming MPMC channel for `T: Marshal`. Returns
819    /// `WrongFamily` if the inferred shape resolves to something
820    /// other than `SharedRing` (e.g. you set `batch_size` and the
821    /// inference picked work-stealing).
822    pub fn build_channel<T: Marshal>(self) -> Result<Channel<T>, ApiError> {
823        let shape = self.inferred_shape();
824        Channel::<T>::create(&self.path, shape, self.capacity)
825    }
826
827    /// Build a work-stealing queue. Returns `WrongFamily` if the
828    /// inferred shape is not work-stealing (call `batch_size` to
829    /// force work-stealing inference) or if `GlobalFifo` ordering
830    /// was declared (a deque's LIFO owner end cannot honor FIFO).
831    pub fn build_work_steal_queue<T: Marshal + Copy + 'static>(
832        self,
833    ) -> Result<WorkStealQueue<T>, ApiError> {
834        if self.ordering == crate::qos_policy::Ordering::GlobalFifo {
835            return Err(ApiError::WrongFamily {
836                wanted: "SharedRing (GlobalFifo ordering declared)",
837                got: MmfFamily::SharedDeque(
838                    crate::dispatch_deque::DequeVariant::ChaseLev,
839                ),
840            });
841        }
842        // Force work-stealing inference when this method is called.
843        let shape = MmfWorkloadShape::WorkStealing(
844            crate::dispatch_deque::WorkloadShape {
845                n_thieves: self.n_consumers,
846                batch_size: self.batch_size,
847                wait_idle: self.wait_idle,
848            },
849        );
850        WorkStealQueue::<T>::create(&self.path, shape, self.capacity)
851    }
852
853    /// Build an [`AdaptiveIpc`](crate::AdaptiveIpc) endpoint with
854    /// the ordering axis wired through: the inner ring carries push
855    /// stamps, the [`ordering`](Self::ordering) declaration is
856    /// applied at construction (GlobalFifo = stamped merge ON), and
857    /// an [`auto_order`](Self::auto_order) threshold pre-authorizes
858    /// the sidecar's automatic arm on observed inversion rate.
859    pub fn build_adaptive<T: Marshal + Copy + 'static>(
860        self,
861    ) -> Result<crate::AdaptiveIpc<T>, ApiError> {
862        let shape = self.inferred_shape();
863        crate::AdaptiveIpc::<T>::create_with_ordering(
864            &self.path,
865            shape,
866            self.capacity,
867            self.n_consumers,
868            self.ordering,
869            self.auto_order,
870        )
871    }
872
873    /// Build a key-value map. The caller declares key-value intent
874    /// by calling this method (key-value access doesn't share
875    /// signature axes with streaming / work-stealing).
876    pub fn build_kv_map<K, V>(self) -> Result<KvMap<K, V>, ApiError>
877    where
878        K: Copy + Eq + std::hash::Hash + Send + Sync + 'static,
879        V: Copy + Send + Sync + 'static,
880    {
881        let shape = MmfWorkloadShape::KeyValueLookup {
882            n_readers: self.n_consumers,
883            n_writers: self.n_producers,
884        };
885        KvMap::<K, V>::create(&self.path, shape, self.capacity)
886    }
887}
888
889#[cfg(test)]
890mod tests {
891    use super::*;
892    use crate::dispatch_deque::WorkloadShape;
893
894    fn tmp(name: &str) -> std::path::PathBuf {
895        let mut p = std::env::temp_dir();
896        let pid = std::process::id();
897        let nonce = std::time::SystemTime::now()
898            .duration_since(std::time::UNIX_EPOCH)
899            .map(|d| d.as_nanos())
900            .unwrap_or(0);
901        p.push(format!("subetha_api_{pid}_{nonce}_{name}.bin"));
902        p
903    }
904
905    // A tiny Marshal type for the channel tests.
906    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
907    struct U32Item(u32);
908
909    unsafe impl Marshal for U32Item {
910        const PAYLOAD_BYTES: usize = 4;
911        fn marshal(&self, dst: &mut [u8]) {
912            dst[..4].copy_from_slice(&self.0.to_le_bytes());
913        }
914        fn unmarshal(src: &[u8]) -> Result<Self, subetha_core::MarshalError> {
915            if src.len() < 4 {
916                return Err(subetha_core::MarshalError::ShortBuffer {
917                    expected: 4,
918                    got: src.len(),
919                });
920            }
921            Ok(U32Item(u32::from_le_bytes(src[..4].try_into().unwrap())))
922        }
923    }
924
925    #[test]
926    fn channel_round_trips_via_streaming_shape() {
927        let path = tmp("channel");
928        let shape = MmfWorkloadShape::StreamingMpmc {
929            n_producers: 1,
930            n_consumers: 1,
931        };
932        let chan: Channel<U32Item> = Channel::create(&path, shape, 64).expect("create");
933        assert_eq!(chan.family(), MmfFamily::SharedRing);
934        chan.send(&U32Item(42)).expect("send");
935        let v = chan.recv().expect("recv");
936        assert_eq!(v, U32Item(42));
937        std::fs::remove_file(&path).ok();
938    }
939
940    #[test]
941    fn channel_rejects_wrong_family() {
942        let path = tmp("channel_wrong_family");
943        let bad_shape = MmfWorkloadShape::KeyValueLookup {
944            n_readers: 1,
945            n_writers: 1,
946        };
947        let result = Channel::<U32Item>::create(&path, bad_shape, 64);
948        match result {
949            Err(ApiError::WrongFamily {
950                wanted: "SharedRing",
951                got: MmfFamily::SharedHashMap,
952            }) => {}
953            Err(other) => panic!("expected WrongFamily, got {other:?}"),
954            Ok(_) => panic!("expected error, got Ok"),
955        }
956        std::fs::remove_file(&path).ok();
957    }
958
959    #[test]
960    fn work_steal_queue_round_trips_via_request_reply_shape() {
961        let path = tmp("wsq");
962        let shape = MmfWorkloadShape::WorkStealing(WorkloadShape::request_reply());
963        let q: WorkStealQueue<u64> = WorkStealQueue::create(&path, shape, 64).expect("create");
964        // request_reply -> ChaseLev (per-item).
965        assert_eq!(q.variant(), DequeVariant::ChaseLev);
966        q.push(&100).expect("push");
967        q.push(&200).expect("push");
968        // pop is LIFO end (owner) -> 200 first.
969        assert_eq!(q.pop(), Some(200));
970        // steal is FIFO end (thief) -> 100 next.
971        assert_eq!(q.steal(), Some(100));
972        std::fs::remove_file(&path).ok();
973    }
974
975    #[test]
976    fn kv_map_round_trips_via_key_value_shape() {
977        let path = tmp("kv");
978        let shape = MmfWorkloadShape::KeyValueLookup {
979            n_readers: 1,
980            n_writers: 1,
981        };
982        let map: KvMap<u32, u32> = KvMap::create(&path, shape, 64).expect("create");
983        for k in 0..10u32 {
984            map.insert(k, k * k).expect("insert");
985        }
986        for k in 0..10u32 {
987            assert_eq!(map.get(&k), Some(k * k));
988        }
989        assert_eq!(map.len(), 10);
990        std::fs::remove_file(&path).ok();
991    }
992
993    #[test]
994    fn auto_ipc_default_infers_streaming_one_to_one() {
995        let auto = AutoIpc::new("/tmp/test-default.bin");
996        let shape = auto.inferred_shape();
997        assert!(matches!(shape, MmfWorkloadShape::StreamingMpmc { .. }));
998        assert_eq!(auto.inferred_family(), MmfFamily::SharedRing);
999    }
1000
1001    #[test]
1002    fn auto_ipc_multi_producer_infers_streaming_mpmc() {
1003        let auto = AutoIpc::new("/tmp/test-mp.bin").producers(4).consumers(4);
1004        assert_eq!(auto.inferred_family(), MmfFamily::SharedRing);
1005    }
1006
1007    #[test]
1008    fn auto_ipc_batch_hint_flips_to_work_stealing() {
1009        let auto = AutoIpc::new("/tmp/test-batch.bin").batch_size(64);
1010        let shape = auto.inferred_shape();
1011        assert!(matches!(shape, MmfWorkloadShape::WorkStealing(_)));
1012        // Single-thief batched -> KHL.
1013        assert_eq!(
1014            auto.inferred_family(),
1015            MmfFamily::SharedDeque(DequeVariant::Khl)
1016        );
1017    }
1018
1019    #[test]
1020    fn auto_ipc_multi_consumer_plus_batch_infers_urd() {
1021        let auto = AutoIpc::new("/tmp/test-mt.bin")
1022            .consumers(4)
1023            .batch_size(64);
1024        assert_eq!(
1025            auto.inferred_family(),
1026            MmfFamily::SharedDeque(DequeVariant::Urd)
1027        );
1028    }
1029
1030    #[test]
1031    fn auto_ipc_idle_wait_routes_to_urd() {
1032        let auto = AutoIpc::new("/tmp/test-idle.bin").idle_wait(true);
1033        assert_eq!(
1034            auto.inferred_family(),
1035            MmfFamily::SharedDeque(DequeVariant::Urd)
1036        );
1037    }
1038
1039    #[test]
1040    fn auto_ipc_build_channel_end_to_end_round_trip() {
1041        let path = tmp("auto_ch");
1042        let auto = AutoIpc::new(&path).capacity(64);
1043        let chan: Channel<U32Item> = auto.build_channel().expect("build");
1044        chan.send(&U32Item(123)).expect("send");
1045        let v = chan.recv().expect("recv");
1046        assert_eq!(v, U32Item(123));
1047        std::fs::remove_file(&path).ok();
1048    }
1049
1050    #[test]
1051    fn auto_ipc_rounds_a_non_pow2_capacity_up_for_every_terminal() {
1052        // Every terminal's backing store requires a pow2 capacity and
1053        // refuses one differently: the ring asserts, the deque returns
1054        // InvalidCapacity. The builder rounds so a caller's arbitrary
1055        // number reaches all of them as the same legal value.
1056        let ch_path = tmp("auto_cap_ch");
1057        let chan: Channel<U32Item> = AutoIpc::new(&ch_path)
1058            .capacity(100)
1059            .build_channel()
1060            .expect("100 rounds to 128");
1061        chan.send(&U32Item(5)).expect("send");
1062        assert_eq!(chan.recv().expect("recv"), U32Item(5));
1063        std::fs::remove_file(&ch_path).ok();
1064
1065        let q_path = tmp("auto_cap_wsq");
1066        let q: WorkStealQueue<u64> = AutoIpc::new(&q_path)
1067            .batch_size(8)
1068            .capacity(100)
1069            .build_work_steal_queue()
1070            .expect("100 rounds to 128");
1071        q.push(&1).expect("push");
1072        assert_eq!(q.pop(), Some(1));
1073        std::fs::remove_file(&q_path).ok();
1074
1075        // The >= 2 clamp survives the rounding: 0 and 1 are both
1076        // illegal capacities downstream.
1077        let z_path = tmp("auto_cap_zero");
1078        let zero: Channel<U32Item> = AutoIpc::new(&z_path)
1079            .capacity(0)
1080            .build_channel()
1081            .expect("0 clamps to 2");
1082        zero.send(&U32Item(9)).expect("send");
1083        assert_eq!(zero.recv().expect("recv"), U32Item(9));
1084        std::fs::remove_file(&z_path).ok();
1085    }
1086
1087    #[test]
1088    fn auto_ipc_build_work_steal_queue_with_batch_hint() {
1089        let path = tmp("auto_wsq");
1090        let q: WorkStealQueue<u64> = AutoIpc::new(&path)
1091            .batch_size(8)
1092            .capacity(64)
1093            .build_work_steal_queue()
1094            .expect("build");
1095        q.push(&10).expect("push");
1096        q.push(&20).expect("push");
1097        assert_eq!(q.pop(), Some(20));
1098        assert_eq!(q.steal(), Some(10));
1099        std::fs::remove_file(&path).ok();
1100    }
1101
1102    #[test]
1103    fn auto_ipc_build_kv_map() {
1104        let path = tmp("auto_kv");
1105        let map: KvMap<u32, u32> = AutoIpc::new(&path)
1106            .capacity(64)
1107            .build_kv_map()
1108            .expect("build");
1109        map.insert(7, 49).expect("insert");
1110        assert_eq!(map.get(&7), Some(49));
1111        std::fs::remove_file(&path).ok();
1112    }
1113
1114    #[test]
1115    fn auto_ipc_global_fifo_forces_streaming_inference() {
1116        // A batch hint normally flips the inference to work-stealing;
1117        // the GlobalFifo declaration overrides it (deques cannot
1118        // honor cross-producer FIFO).
1119        let auto = AutoIpc::new("/tmp/test-fifo.bin")
1120            .producers(4)
1121            .batch_size(64)
1122            .ordering(crate::qos_policy::Ordering::GlobalFifo);
1123        assert!(matches!(
1124            auto.inferred_shape(),
1125            MmfWorkloadShape::StreamingMpmc { .. }
1126        ));
1127        assert_eq!(auto.inferred_family(), MmfFamily::SharedRing);
1128    }
1129
1130    #[test]
1131    fn auto_ipc_global_fifo_rejects_work_steal_queue() {
1132        let path = tmp("fifo_wsq");
1133        let result = AutoIpc::new(&path)
1134            .batch_size(8)
1135            .ordering(crate::qos_policy::Ordering::GlobalFifo)
1136            .build_work_steal_queue::<u64>();
1137        assert!(matches!(result, Err(ApiError::WrongFamily { .. })),
1138                "GlobalFifo + work-stealing must be rejected, got Ok or wrong error");
1139        std::fs::remove_file(&path).ok();
1140    }
1141
1142    #[test]
1143    fn auto_ipc_build_adaptive_with_ordering_round_trips() {
1144        let path = tmp("auto_adaptive");
1145        let ipc = AutoIpc::new(&path)
1146            .capacity(64)
1147            .ordering(crate::qos_policy::Ordering::GlobalFifo)
1148            .build_adaptive::<u64>()
1149            .expect("build");
1150        assert!(ipc.ring_handle().is_stamped(),
1151                "build_adaptive must construct the stamped ring");
1152        assert_eq!(ipc.ordering(), crate::qos_policy::Ordering::GlobalFifo);
1153        ipc.send(&31337).expect("send");
1154        assert_eq!(ipc.recv().expect("recv"), 31337);
1155    }
1156
1157    #[test]
1158    fn auto_ipc_auto_order_threshold_reaches_adaptive_endpoint() {
1159        let path = tmp("auto_threshold");
1160        let ipc = AutoIpc::new(&path)
1161            .capacity(64)
1162            .auto_order(5.0)
1163            .build_adaptive::<u64>()
1164            .expect("build");
1165        assert!(ipc.ring_handle().is_stamped(),
1166                "auto_order requires the stamped ring and build_adaptive must provide it");
1167        assert_eq!(ipc.ordering(), crate::qos_policy::Ordering::PerProducer,
1168                   "auto_order alone must not pre-arm the merge");
1169    }
1170
1171    #[test]
1172    fn kv_map_rejects_streaming_shape() {
1173        let path = tmp("kv_wrong");
1174        let bad_shape = MmfWorkloadShape::StreamingMpmc {
1175            n_producers: 1,
1176            n_consumers: 1,
1177        };
1178        let result = KvMap::<u32, u32>::create(&path, bad_shape, 64);
1179        match result {
1180            Err(ApiError::WrongFamily {
1181                wanted: "SharedHashMap",
1182                got: MmfFamily::SharedRing,
1183            }) => {}
1184            Err(other) => panic!("expected WrongFamily, got {other:?}"),
1185            Ok(_) => panic!("expected error, got Ok"),
1186        }
1187        std::fs::remove_file(&path).ok();
1188    }
1189}