1#![allow(clippy::missing_errors_doc)]
32
33use std::cell::Cell;
34use std::future::Future;
35use std::marker::PhantomData;
36use std::path::{Path, PathBuf};
37use std::pin::Pin;
38use std::sync::Arc;
39use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
40use std::task::{Context, Poll, Waker};
41use std::time::Duration;
42
43use parking_lot::Mutex;
44use subetha_core::Marshal;
45
46use crate::adaptive_ring::{AdaptiveRing, RingShape};
47use crate::api::{map_waker, wait_heal, ApiError};
48use crate::cross_process_waker::{CrossProcessWaker, MAX_WAITERS_DEFAULT};
49use crate::message_transport::{PassSlot, TransportError};
50use crate::mmf_dispatcher::{MmfDispatcher, MmfFamily, MmfWorkloadShape};
51use crate::ordering::{monotonic_nanos, OrderingMode};
52use crate::qos_policy::Ordering as QosOrdering;
53use crate::shared_atomic::{SharedAtomicU32, SharedAtomicU64};
54use crate::shared_deque::SharedDeque;
55use crate::shared_deque_khl::{SharedDequeKhl, Steal as KhlSteal};
56use crate::shared_deque_khpd::{LineItem, KHPD_ITEM_BYTES};
57use crate::shared_ring::PAYLOAD_BYTES;
58
59const TAG_RING: u32 = 0;
61const TAG_DEQUE: u32 = 1;
62
63#[derive(Debug, Default, Clone, Copy)]
66pub struct ProfileSnapshot {
67 pub total_sends: u64,
69 pub batch_sends: u64,
71 pub batch_size_sum: u64,
73 pub max_batch_size: u64,
75}
76
77impl ProfileSnapshot {
78 pub fn avg_batch_size(&self) -> u64 {
80 self.batch_size_sum.checked_div(self.batch_sends).unwrap_or(1)
81 }
82
83 pub fn batch_ratio(&self) -> f64 {
85 let total = self.total_sends + self.batch_sends;
86 if total == 0 {
87 0.0
88 } else {
89 self.batch_sends as f64 / total as f64
90 }
91 }
92
93 pub fn inferred_shape(&self, n_consumers: usize) -> MmfWorkloadShape {
95 if self.batch_ratio() >= 0.5 || self.max_batch_size >= 8 {
96 MmfWorkloadShape::WorkStealing(
97 crate::dispatch_deque::WorkloadShape {
98 n_thieves: n_consumers,
99 batch_size: Some(self.avg_batch_size().max(2) as usize),
100 wait_idle: false,
101 },
102 )
103 } else {
104 MmfWorkloadShape::StreamingMpmc {
105 n_producers: 1,
106 n_consumers,
107 }
108 }
109 }
110}
111
112pub struct AdaptiveIpc<T: Marshal + Copy + 'static> {
118 control: Arc<SharedAtomicU32>,
122 pin_generation: Arc<SharedAtomicU64>,
129 ring: AdaptiveRing,
137 deque: SharedDeque<PassSlot>,
141 khl: Option<Arc<SharedDequeKhl>>,
150 khl_surplus: Mutex<Vec<LineItem>>,
155 base_path: PathBuf,
159 total_sends_atom: AtomicU64,
162 batch_sends_atom: AtomicU64,
163 batch_size_sum_atom: AtomicU64,
164 max_batch_size_atom: AtomicU64,
165 shape_bloom_atom: AtomicU64,
171 n_consumers: usize,
174 auto_order_threshold: Option<f64>,
180 last_inversions_atom: AtomicU64,
182 last_inversion_check_nanos: AtomicU64,
183 consumer_waker: Arc<CrossProcessWaker>,
185 producer_waker: Arc<CrossProcessWaker>,
187 recv_slot: Arc<Mutex<Option<Waker>>>,
189 send_slot: Arc<Mutex<Option<Waker>>>,
191 published: AtomicU64,
194 consumed: AtomicU64,
195 has_recv_waiter: AtomicBool,
198 has_send_waiter: AtomicBool,
199 _phantom: PhantomData<T>,
200}
201
202impl<T: Marshal + Copy + 'static> AdaptiveIpc<T> {
203 pub fn create(
208 base_path: impl Into<PathBuf>,
209 initial_shape: MmfWorkloadShape,
210 capacity: usize,
211 n_consumers: usize,
212 ) -> Result<Self, ApiError> {
213 if T::PAYLOAD_BYTES > PAYLOAD_BYTES {
214 return Err(ApiError::PayloadTooLarge);
215 }
216 let base_path: PathBuf = base_path.into();
217 let ctl_path = control_path_for(&base_path);
218 let deque_path = deque_path_for(&base_path);
219 let pingen_path = pingen_path_for(&base_path);
220
221 let control = Arc::new(
222 SharedAtomicU32::create(&ctl_path, 0)
223 .map_err(|e| ApiError::Io(std::io::Error::other(format!("control: {e:?}"))))?,
224 );
225 let pin_generation = Arc::new(
226 SharedAtomicU64::create(&pingen_path, 0)
227 .map_err(|e| ApiError::Io(std::io::Error::other(format!("pingen: {e:?}"))))?,
228 );
229 let max_producers = n_consumers.max(1);
236 let ring_prefix = ring_path_prefix_for(&base_path);
237 let ring = AdaptiveRing::create(
238 &ring_prefix,
239 max_producers,
240 n_consumers.max(1),
241 capacity,
242 )
243 .map_err(|e| ApiError::Io(std::io::Error::other(format!("ring: {e:?}"))))?;
244 ring.register_producer()
247 .map_err(|e| ApiError::Io(std::io::Error::other(format!("ring register_producer: {e:?}"))))?;
248 for _ in 0..n_consumers.max(1) {
249 ring.register_consumer()
250 .map_err(|e| ApiError::Io(std::io::Error::other(format!("ring register_consumer: {e:?}"))))?;
251 }
252 let deque = SharedDeque::<PassSlot>::create(&deque_path, capacity)?;
253 let khl = if T::PAYLOAD_BYTES <= KHPD_ITEM_BYTES {
256 Some(Arc::new(
257 SharedDequeKhl::create(khl_path_for(&base_path), capacity)
258 .map_err(|e| ApiError::Io(std::io::Error::other(format!("khl: {e:?}"))))?,
259 ))
260 } else {
261 None
262 };
263
264 let initial_family = MmfDispatcher::pick(initial_shape);
265 let initial_tag = match initial_family {
266 MmfFamily::SharedRing => TAG_RING,
267 MmfFamily::SharedDeque(_) => TAG_DEQUE,
268 MmfFamily::SharedHashMap => {
269 return Err(ApiError::WrongFamily {
270 wanted: "SharedRing or SharedDeque",
271 got: initial_family,
272 });
273 }
274 };
275 control.store(initial_tag, Ordering::Release);
276
277 let consumer_waker = Arc::new(
278 CrossProcessWaker::create_anon(MAX_WAITERS_DEFAULT).map_err(|e| {
279 ApiError::Io(std::io::Error::other(format!("consumer waker: {e:?}")))
280 })?,
281 );
282 let producer_waker = Arc::new(
283 CrossProcessWaker::create_anon(MAX_WAITERS_DEFAULT).map_err(|e| {
284 ApiError::Io(std::io::Error::other(format!("producer waker: {e:?}")))
285 })?,
286 );
287
288 Ok(Self {
289 control,
290 pin_generation,
291 ring,
292 deque,
293 khl,
294 khl_surplus: Mutex::new(Vec::new()),
295 base_path,
296 total_sends_atom: AtomicU64::new(0),
297 batch_sends_atom: AtomicU64::new(0),
298 batch_size_sum_atom: AtomicU64::new(0),
299 max_batch_size_atom: AtomicU64::new(0),
300 shape_bloom_atom: AtomicU64::new(0),
301 n_consumers,
302 auto_order_threshold: None,
303 last_inversions_atom: AtomicU64::new(0),
304 last_inversion_check_nanos: AtomicU64::new(monotonic_nanos()),
305 consumer_waker,
306 producer_waker,
307 recv_slot: Arc::new(Mutex::new(None)),
308 send_slot: Arc::new(Mutex::new(None)),
309 published: AtomicU64::new(0),
310 consumed: AtomicU64::new(0),
311 has_recv_waiter: AtomicBool::new(false),
312 has_send_waiter: AtomicBool::new(false),
313 _phantom: PhantomData,
314 })
315 }
316
317 pub fn create_with_ordering(
329 base_path: impl Into<PathBuf>,
330 initial_shape: MmfWorkloadShape,
331 capacity: usize,
332 n_consumers: usize,
333 ordering: QosOrdering,
334 auto_order: Option<f64>,
335 ) -> Result<Self, ApiError> {
336 if T::PAYLOAD_BYTES > PAYLOAD_BYTES {
337 return Err(ApiError::PayloadTooLarge);
338 }
339 let base_path: PathBuf = base_path.into();
340 let ctl_path = control_path_for(&base_path);
341 let deque_path = deque_path_for(&base_path);
342 let pingen_path = pingen_path_for(&base_path);
343
344 let control = Arc::new(
345 SharedAtomicU32::create(&ctl_path, 0)
346 .map_err(|e| ApiError::Io(std::io::Error::other(format!("control: {e:?}"))))?,
347 );
348 let pin_generation = Arc::new(
349 SharedAtomicU64::create(&pingen_path, 0)
350 .map_err(|e| ApiError::Io(std::io::Error::other(format!("pingen: {e:?}"))))?,
351 );
352 let max_producers = n_consumers.max(1);
353 let ring_prefix = ring_path_prefix_for(&base_path);
354 let ring = AdaptiveRing::create(
355 &ring_prefix,
356 max_producers,
357 n_consumers.max(1),
358 capacity,
359 )
360 .map_err(|e| ApiError::Io(std::io::Error::other(format!("ring: {e:?}"))))?
361 .with_ordering_stamps()
362 .map_err(|e| ApiError::Io(std::io::Error::other(format!("ordering: {e:?}"))))?;
363 ring.register_producer()
364 .map_err(|e| ApiError::Io(std::io::Error::other(format!("ring register_producer: {e:?}"))))?;
365 for _ in 0..n_consumers.max(1) {
366 ring.register_consumer()
367 .map_err(|e| ApiError::Io(std::io::Error::other(format!("ring register_consumer: {e:?}"))))?;
368 }
369 let deque = SharedDeque::<PassSlot>::create(&deque_path, capacity)?;
370 let khl = if T::PAYLOAD_BYTES <= KHPD_ITEM_BYTES {
373 Some(Arc::new(
374 SharedDequeKhl::create(khl_path_for(&base_path), capacity)
375 .map_err(|e| ApiError::Io(std::io::Error::other(format!("khl: {e:?}"))))?,
376 ))
377 } else {
378 None
379 };
380
381 let initial_family = MmfDispatcher::pick(initial_shape);
382 let initial_tag = match initial_family {
383 MmfFamily::SharedRing => TAG_RING,
384 MmfFamily::SharedDeque(_) => TAG_DEQUE,
385 MmfFamily::SharedHashMap => {
386 return Err(ApiError::WrongFamily {
387 wanted: "SharedRing or SharedDeque",
388 got: initial_family,
389 });
390 }
391 };
392 control.store(initial_tag, Ordering::Release);
393
394 let consumer_waker = Arc::new(
395 CrossProcessWaker::create_anon(MAX_WAITERS_DEFAULT).map_err(|e| {
396 ApiError::Io(std::io::Error::other(format!("consumer waker: {e:?}")))
397 })?,
398 );
399 let producer_waker = Arc::new(
400 CrossProcessWaker::create_anon(MAX_WAITERS_DEFAULT).map_err(|e| {
401 ApiError::Io(std::io::Error::other(format!("producer waker: {e:?}")))
402 })?,
403 );
404
405 let ipc = Self {
406 control,
407 pin_generation,
408 ring,
409 deque,
410 khl,
411 khl_surplus: Mutex::new(Vec::new()),
412 base_path,
413 total_sends_atom: AtomicU64::new(0),
414 batch_sends_atom: AtomicU64::new(0),
415 batch_size_sum_atom: AtomicU64::new(0),
416 max_batch_size_atom: AtomicU64::new(0),
417 shape_bloom_atom: AtomicU64::new(0),
418 n_consumers,
419 auto_order_threshold: auto_order,
420 last_inversions_atom: AtomicU64::new(0),
421 last_inversion_check_nanos: AtomicU64::new(monotonic_nanos()),
422 consumer_waker,
423 producer_waker,
424 recv_slot: Arc::new(Mutex::new(None)),
425 send_slot: Arc::new(Mutex::new(None)),
426 published: AtomicU64::new(0),
427 consumed: AtomicU64::new(0),
428 has_recv_waiter: AtomicBool::new(false),
429 has_send_waiter: AtomicBool::new(false),
430 _phantom: PhantomData,
431 };
432 ipc.set_ordering(ordering)?;
433 Ok(ipc)
434 }
435
436 pub fn set_ordering(&self, ordering: QosOrdering) -> Result<(), ApiError> {
449 if self.ring.is_stamped() {
450 let mode = match ordering {
451 QosOrdering::GlobalFifo => OrderingMode::MergeByStamp,
452 QosOrdering::PerProducer => OrderingMode::Unordered,
453 };
454 self.ring.set_ordering_mode(mode).map_err(map_ring_err)?;
455 return Ok(());
456 }
457 match ordering {
458 QosOrdering::GlobalFifo => {
459 if self.ring.current_shape() != RingShape::Vyukov {
460 self.ring.morph_to(RingShape::Vyukov).map_err(map_ring_err)?;
461 }
462 }
463 QosOrdering::PerProducer => {
464 self.ring.resume_auto_shape();
468 }
469 }
470 Ok(())
471 }
472
473 pub fn ordering(&self) -> QosOrdering {
477 if self.ring.is_stamped() {
478 match self.ring.ordering_mode() {
479 Some(OrderingMode::Unordered) | None => QosOrdering::PerProducer,
480 Some(_) => QosOrdering::GlobalFifo,
481 }
482 } else if self.ring.current_shape() == RingShape::Vyukov {
483 QosOrdering::GlobalFifo
484 } else {
485 QosOrdering::PerProducer
486 }
487 }
488
489 pub fn inversions(&self) -> u64 {
492 self.ring.inversions()
493 }
494
495 #[inline]
515 pub fn send(&self, item: &T) -> Result<(), ApiError> {
516 if core::any::TypeId::of::<T>() == core::any::TypeId::of::<u64>() {
520 let val: u64 = unsafe { *(item as *const T as *const u64) };
524 return self.send_u64(val);
525 }
526 let tag = self.control.load(Ordering::Acquire);
527 let mut buf = [0u8; PAYLOAD_BYTES];
528 item.marshal(&mut buf[..T::PAYLOAD_BYTES]);
529 match tag {
530 TAG_RING => {
531 self.ring.try_send(0, &buf[..T::PAYLOAD_BYTES])
532 .map_err(map_ring_err)?;
533 }
534 TAG_DEQUE => {
535 let mut slot = PassSlot([0u8; PAYLOAD_BYTES]);
536 slot.0[..T::PAYLOAD_BYTES]
537 .copy_from_slice(&buf[..T::PAYLOAD_BYTES]);
538 self.deque.push(&slot)?;
539 }
540 _ => return Err(ApiError::Transport(TransportError::Other)),
541 }
542 self.total_sends_atom.fetch_add(1, Ordering::Relaxed);
543 self.signal_consumer();
544 Ok(())
545 }
546
547 #[inline]
560 pub fn send_u64(&self, item: u64) -> Result<(), ApiError> {
561 let tag = self.control.load(Ordering::Acquire);
562 let buf = item.to_le_bytes();
563 match tag {
564 TAG_RING => {
565 self.ring.try_send(0, &buf)
566 .map_err(map_ring_err)?;
567 }
568 TAG_DEQUE => {
569 let mut slot = PassSlot([0u8; PAYLOAD_BYTES]);
570 slot.0[..8].copy_from_slice(&buf);
571 self.deque.push(&slot)?;
572 }
573 _ => return Err(ApiError::Transport(TransportError::Other)),
574 }
575 self.total_sends_atom.fetch_add(1, Ordering::Relaxed);
576 self.signal_consumer();
577 Ok(())
578 }
579
580 pub fn send_batch(&self, items: &[T]) -> Result<(), ApiError> {
594 if items.is_empty() {
595 return Ok(());
596 }
597 if items.len() >= 2
603 && let Some(khl) = self.khl.as_ref()
604 {
605 self.publish_batch_khl(khl, items)?;
606 self.record_batch_profile(items.len() as u64);
607 return Ok(());
608 }
609 let mut buf = [0u8; PAYLOAD_BYTES];
610 let mut sent = 0usize;
611 while sent < items.len() {
612 items[sent].marshal(&mut buf[..T::PAYLOAD_BYTES]);
613 let tag = self.control.load(Ordering::Acquire);
614 let result: Result<(), ApiError> = match tag {
615 TAG_RING => self
616 .ring
617 .try_send(0, &buf[..T::PAYLOAD_BYTES])
618 .map_err(map_ring_err),
619 TAG_DEQUE => {
620 let mut slot = PassSlot([0u8; PAYLOAD_BYTES]);
621 slot.0[..T::PAYLOAD_BYTES]
622 .copy_from_slice(&buf[..T::PAYLOAD_BYTES]);
623 self.deque.push(&slot).map_err(ApiError::from)
624 }
625 _ => return Err(ApiError::Transport(TransportError::Other)),
626 };
627 match result {
628 Ok(()) => sent += 1,
629 Err(ApiError::Transport(TransportError::Full)) => {
630 std::hint::spin_loop();
631 }
632 Err(e) => return Err(e),
633 }
634 }
635 let len = items.len() as u64;
636 self.batch_sends_atom.fetch_add(1, Ordering::Relaxed);
637 self.batch_size_sum_atom.fetch_add(len, Ordering::Relaxed);
638 let mut cur = self.max_batch_size_atom.load(Ordering::Relaxed);
639 while len > cur {
640 match self.max_batch_size_atom.compare_exchange_weak(
641 cur,
642 len,
643 Ordering::Relaxed,
644 Ordering::Relaxed,
645 ) {
646 Ok(_) => break,
647 Err(observed) => cur = observed,
648 }
649 }
650 let log2_bucket = (64u32 - len.leading_zeros()).saturating_sub(1);
652 let mut bloom = subetha_pointers::bloom_pointer::Bloom64(
653 self.shape_bloom_atom.load(Ordering::Relaxed),
654 );
655 bloom.insert(&(1u32, log2_bucket));
656 self.shape_bloom_atom.store(bloom.0, Ordering::Relaxed);
657 Ok(())
658 }
659
660 fn publish_batch_khl(&self, khl: &SharedDequeKhl, items: &[T]) -> Result<(), ApiError> {
667 use crate::shared_deque_khl::KHL_ITEMS_PER_SLOT;
668 let mut chunk = [LineItem::default(); KHL_ITEMS_PER_SLOT];
669 let mut i = 0;
670 while i < items.len() {
671 let n = (items.len() - i).min(KHL_ITEMS_PER_SLOT);
672 for j in 0..n {
673 let mut lb = [0u8; KHPD_ITEM_BYTES];
674 items[i + j].marshal(&mut lb[..T::PAYLOAD_BYTES]);
675 chunk[j] = LineItem::new(&lb).map_err(|_| ApiError::PayloadTooLarge)?;
676 }
677 let mut done = 0;
678 while done < n {
679 match khl.publish_batch(&chunk[done..n]) {
680 Ok(c) => done += c,
681 Err(_) => return Err(ApiError::Transport(TransportError::Other)),
682 }
683 if done < n {
684 std::hint::spin_loop();
685 }
686 }
687 i += n;
688 }
689 Ok(())
690 }
691
692 fn record_batch_profile(&self, len: u64) {
696 self.batch_sends_atom.fetch_add(1, Ordering::Relaxed);
697 self.batch_size_sum_atom.fetch_add(len, Ordering::Relaxed);
698 let mut cur = self.max_batch_size_atom.load(Ordering::Relaxed);
699 while len > cur {
700 match self.max_batch_size_atom.compare_exchange_weak(
701 cur,
702 len,
703 Ordering::Relaxed,
704 Ordering::Relaxed,
705 ) {
706 Ok(_) => break,
707 Err(observed) => cur = observed,
708 }
709 }
710 let log2_bucket = (64u32 - len.leading_zeros()).saturating_sub(1);
711 let mut bloom = subetha_pointers::bloom_pointer::Bloom64(
712 self.shape_bloom_atom.load(Ordering::Relaxed),
713 );
714 bloom.insert(&(1u32, log2_bucket));
715 self.shape_bloom_atom.store(bloom.0, Ordering::Relaxed);
716 }
717
718 fn recv_from_khl(&self) -> Result<Option<T>, ApiError> {
725 let Some(khl) = self.khl.as_ref() else {
726 return Ok(None);
727 };
728 {
729 let mut surplus = self.khl_surplus.lock();
730 if let Some(item) = surplus.pop() {
731 return Ok(Some(Self::unmarshal_line(&item)?));
732 }
733 }
734 for _ in 0..4 {
735 match khl.steal_slot() {
736 KhlSteal::Success(res) => {
737 let n = res.n_items;
738 if n == 0 {
739 return Ok(None);
740 }
741 if n > 1 {
742 let mut surplus = self.khl_surplus.lock();
743 for k in (1..n).rev() {
746 surplus.push(res.items[k]);
747 }
748 }
749 return Ok(Some(Self::unmarshal_line(&res.items[0])?));
750 }
751 KhlSteal::Empty => return Ok(None),
752 KhlSteal::Retry => continue,
753 }
754 }
755 Ok(None)
756 }
757
758 #[inline]
759 fn unmarshal_line(item: &LineItem) -> Result<T, ApiError> {
760 let bytes = item.bytes();
761 Ok(T::unmarshal(&bytes[..T::PAYLOAD_BYTES])?)
762 }
763
764 #[inline]
768 pub fn recv(&self) -> Result<T, ApiError> {
769 if let Some(v) = self.recv_from_khl()? {
770 self.signal_producer();
771 return Ok(v);
772 }
773 let active = self.control.load(Ordering::Acquire);
774 let stale = if active == TAG_RING { TAG_DEQUE } else { TAG_RING };
775 if let Some(v) = self.try_recv_from(stale)? {
776 self.signal_producer();
777 return Ok(v);
778 }
779 match self.try_recv_from(active)? {
780 Some(v) => {
781 self.signal_producer();
782 Ok(v)
783 }
784 None => Err(ApiError::Transport(TransportError::Empty)),
785 }
786 }
787
788 #[inline]
789 fn try_recv_from(&self, tag: u32) -> Result<Option<T>, ApiError> {
790 let mut out = [0u8; crate::adaptive_ring::ADAPTIVE_SPSC_PAYLOAD_BYTES];
797 match tag {
798 TAG_RING => match self.ring.try_recv(0, &mut out) {
799 Ok(n) => Ok(Some(T::unmarshal(&out[..n.min(out.len())])?)),
800 Err(_) => Ok(None),
801 },
802 TAG_DEQUE => match self.deque.steal() {
803 Some(slot) => Ok(Some(T::unmarshal(
804 &slot.0[..PAYLOAD_BYTES.min(T::PAYLOAD_BYTES.max(1))],
805 )?)),
806 None => Ok(None),
807 },
808 _ => Err(ApiError::Transport(TransportError::Other)),
809 }
810 }
811
812 fn signal_consumer(&self) {
816 if !self.has_recv_waiter.load(Ordering::Relaxed) {
817 return;
818 }
819 let n = self.published.fetch_add(1, Ordering::AcqRel) + 1;
820 if let Some(w) = self.recv_slot.lock().take() {
821 w.wake();
822 }
823 self.consumer_waker.wake_up_to(n);
824 }
825
826 fn signal_producer(&self) {
828 if !self.has_send_waiter.load(Ordering::Relaxed) {
829 return;
830 }
831 let n = self.consumed.fetch_add(1, Ordering::AcqRel) + 1;
832 if let Some(w) = self.send_slot.lock().take() {
833 w.wake();
834 }
835 self.producer_waker.wake_up_to(n);
836 }
837
838 pub fn send_blocking(
841 &self,
842 item: &T,
843 timeout: Option<Duration>,
844 ) -> Result<(), ApiError> {
845 self.has_send_waiter.store(true, Ordering::Relaxed);
846 let deadline = timeout.map(|d| std::time::Instant::now() + d);
847 loop {
848 match self.send(item) {
849 Ok(()) => return Ok(()),
850 Err(ApiError::Transport(TransportError::Full)) => {}
851 Err(e) => return Err(e),
852 }
853 let seen = self.consumed.load(Ordering::Acquire);
854 let token = self.producer_waker.try_park(seen + 1).map_err(map_waker)?;
855 match self.send(item) {
856 Ok(()) => {
857 self.producer_waker.release(token);
858 return Ok(());
859 }
860 Err(ApiError::Transport(TransportError::Full)) => {}
861 Err(e) => {
862 self.producer_waker.release(token);
863 return Err(e);
864 }
865 }
866 wait_heal(&self.producer_waker, token, deadline)?;
867 }
868 }
869
870 pub fn recv_blocking(&self, timeout: Option<Duration>) -> Result<T, ApiError> {
873 self.has_recv_waiter.store(true, Ordering::Relaxed);
874 let deadline = timeout.map(|d| std::time::Instant::now() + d);
875 loop {
876 match self.recv() {
877 Ok(v) => return Ok(v),
878 Err(ApiError::Transport(TransportError::Empty)) => {}
879 Err(e) => return Err(e),
880 }
881 let seen = self.published.load(Ordering::Acquire);
882 let token = self.consumer_waker.try_park(seen + 1).map_err(map_waker)?;
883 match self.recv() {
884 Ok(v) => {
885 self.consumer_waker.release(token);
886 return Ok(v);
887 }
888 Err(ApiError::Transport(TransportError::Empty)) => {}
889 Err(e) => {
890 self.consumer_waker.release(token);
891 return Err(e);
892 }
893 }
894 wait_heal(&self.consumer_waker, token, deadline)?;
895 }
896 }
897
898 pub fn send_async(&self, item: &T) -> AdaptiveSendFut<'_, T> {
901 self.has_send_waiter.store(true, Ordering::Relaxed);
902 AdaptiveSendFut { ipc: self, item: *item }
903 }
904
905 pub fn recv_async(&self) -> AdaptiveRecvFut<'_, T> {
907 self.has_recv_waiter.store(true, Ordering::Relaxed);
908 AdaptiveRecvFut { ipc: self }
909 }
910
911 pub fn profile_snapshot(&self) -> ProfileSnapshot {
913 ProfileSnapshot {
914 total_sends: self.total_sends_atom.load(Ordering::Relaxed),
915 batch_sends: self.batch_sends_atom.load(Ordering::Relaxed),
916 batch_size_sum: self.batch_size_sum_atom.load(Ordering::Relaxed),
917 max_batch_size: self.max_batch_size_atom.load(Ordering::Relaxed),
918 }
919 }
920
921 pub fn active_family(&self) -> MmfFamily {
923 match self.control.load(Ordering::Acquire) {
924 TAG_RING => MmfFamily::SharedRing,
925 TAG_DEQUE => MmfFamily::SharedDeque(
926 crate::dispatch_deque::DequeVariant::Khl,
927 ),
928 _ => MmfFamily::SharedRing,
929 }
930 }
931
932 pub fn migrate_to(&self, target_family: MmfFamily) -> Result<(), ApiError> {
940 let new_tag = match target_family {
941 MmfFamily::SharedRing => TAG_RING,
942 MmfFamily::SharedDeque(_) => TAG_DEQUE,
943 MmfFamily::SharedHashMap => {
944 return Err(ApiError::WrongFamily {
945 wanted: "SharedRing or SharedDeque",
946 got: target_family,
947 });
948 }
949 };
950 let current_tag = self.control.load(Ordering::Acquire);
951 if current_tag == new_tag {
952 return Ok(());
953 }
954 self.pin_generation.fetch_add(1, Ordering::AcqRel);
955 self.control.store(new_tag, Ordering::Release);
956 Ok(())
957 }
958
959 pub fn pin_generation(&self) -> u64 {
964 self.pin_generation.load(Ordering::Acquire)
965 }
966
967 pub fn ring_handle(&self) -> &AdaptiveRing {
976 &self.ring
977 }
978
979 pub fn pin_current_family(&self) -> PinnedIpc<'_, T> {
987 let captured_gen = self.pin_generation.load(Ordering::Acquire);
988 let tag = self.control.load(Ordering::Acquire);
989 let family = match tag {
990 TAG_RING => MmfFamily::SharedRing,
991 TAG_DEQUE => MmfFamily::SharedDeque(
992 crate::dispatch_deque::DequeVariant::Khl,
993 ),
994 _ => MmfFamily::SharedRing,
995 };
996 PinnedIpc {
997 parent: self,
998 pinned_generation: captured_gen,
999 family,
1000 _not_sync: PhantomData,
1001 }
1002 }
1003
1004 pub fn maybe_promote(&self) -> Result<Option<MmfFamily>, ApiError> {
1020 self.maybe_auto_order();
1021 let snap = self.profile_snapshot();
1022 let total_events = snap.total_sends + snap.batch_sends;
1023 if total_events < 8 {
1024 return Ok(None);
1025 }
1026 let bloom = subetha_pointers::bloom_pointer::Bloom64(
1029 self.shape_bloom_atom.load(Ordering::Relaxed),
1030 );
1031 let any_batched = (1..=10).any(|b| {
1033 bloom.might_contain(&(1u32, b))
1034 });
1035 if !any_batched && self.active_family() == MmfFamily::SharedRing {
1036 return Ok(None);
1039 }
1040 let target_shape = snap.inferred_shape(self.n_consumers);
1041 let target_family = MmfDispatcher::pick(target_shape);
1042 let active = self.active_family();
1043 if target_family != active {
1044 self.migrate_to(target_family)?;
1045 return Ok(Some(target_family));
1046 }
1047 Ok(None)
1048 }
1049
1050 fn maybe_auto_order(&self) {
1058 let Some(threshold) = self.auto_order_threshold else { return };
1059 if self.ring.ordering_mode() != Some(OrderingMode::Unordered) {
1060 return;
1061 }
1062 let now = monotonic_nanos();
1063 let then = self.last_inversion_check_nanos.swap(now, Ordering::AcqRel);
1064 let inversions = self.ring.inversions();
1065 let last = self.last_inversions_atom.swap(inversions, Ordering::AcqRel);
1066 let elapsed_secs = (now.saturating_sub(then) as f64 / 1e9).max(1e-9);
1067 let rate = inversions.saturating_sub(last) as f64 / elapsed_secs;
1068 if rate > threshold {
1069 self.ring.set_ordering_mode(OrderingMode::MergeByStamp).ok();
1070 }
1071 }
1072}
1073
1074impl<T: Marshal + Copy + 'static> Drop for AdaptiveIpc<T> {
1075 fn drop(&mut self) {
1076 let deque_p = deque_path_for(&self.base_path);
1077 let ctl_p = control_path_for(&self.base_path);
1078 let pingen_p = pingen_path_for(&self.base_path);
1079 std::fs::remove_file(&deque_p).ok();
1080 std::fs::remove_file(&ctl_p).ok();
1081 std::fs::remove_file(&pingen_p).ok();
1082 if self.khl.is_some() {
1083 std::fs::remove_file(khl_path_for(&self.base_path)).ok();
1084 }
1085
1086 let ring_prefix = ring_path_prefix_for(&self.base_path);
1091 let max_producers = self.ring.max_producers();
1092 std::fs::remove_file(with_suffix(&ring_prefix, ".spsc.bin")).ok();
1093 std::fs::remove_file(with_suffix(&ring_prefix, ".vyukov.bin")).ok();
1094 std::fs::remove_file(with_suffix(&ring_prefix, ".ordering.bin")).ok();
1095 for i in 0..max_producers {
1096 std::fs::remove_file(
1097 with_suffix(&ring_prefix, &format!(".mpsc.{i}.bin")),
1098 ).ok();
1099 std::fs::remove_file(
1100 with_suffix(&ring_prefix, &format!(".mpmc.{i}.bin")),
1101 ).ok();
1102 }
1103 }
1104}
1105
1106fn with_suffix(base: &Path, suffix: &str) -> PathBuf {
1107 let mut s = base.as_os_str().to_owned();
1108 s.push(suffix);
1109 PathBuf::from(s)
1110}
1111
1112fn map_ring_err(e: crate::shared_ring::RingError) -> ApiError {
1113 match e {
1114 crate::shared_ring::RingError::Full => {
1115 ApiError::Transport(TransportError::Full)
1116 }
1117 crate::shared_ring::RingError::Empty => {
1118 ApiError::Transport(TransportError::Empty)
1119 }
1120 crate::shared_ring::RingError::PayloadTooLarge => {
1121 ApiError::Transport(TransportError::PayloadTooLarge)
1122 }
1123 _ => ApiError::Transport(TransportError::Other),
1124 }
1125}
1126
1127pub struct AdaptiveRecvFut<'a, T: Marshal + Copy + 'static> {
1129 ipc: &'a AdaptiveIpc<T>,
1130}
1131
1132impl<'a, T: Marshal + Copy + 'static> Future for AdaptiveRecvFut<'a, T> {
1133 type Output = Result<T, ApiError>;
1134
1135 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1136 let ipc = self.ipc;
1137 match ipc.recv() {
1138 Ok(v) => return Poll::Ready(Ok(v)),
1139 Err(ApiError::Transport(TransportError::Empty)) => {}
1140 Err(e) => return Poll::Ready(Err(e)),
1141 }
1142 *ipc.recv_slot.lock() = Some(cx.waker().clone());
1143 match ipc.recv() {
1144 Ok(v) => Poll::Ready(Ok(v)),
1145 Err(ApiError::Transport(TransportError::Empty)) => Poll::Pending,
1146 Err(e) => Poll::Ready(Err(e)),
1147 }
1148 }
1149}
1150
1151pub struct AdaptiveSendFut<'a, T: Marshal + Copy + 'static> {
1153 ipc: &'a AdaptiveIpc<T>,
1154 item: T,
1155}
1156
1157impl<'a, T: Marshal + Copy + 'static> Future for AdaptiveSendFut<'a, T> {
1158 type Output = Result<(), ApiError>;
1159
1160 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1161 match self.ipc.send(&self.item) {
1164 Ok(()) => return Poll::Ready(Ok(())),
1165 Err(ApiError::Transport(TransportError::Full)) => {}
1166 Err(e) => return Poll::Ready(Err(e)),
1167 }
1168 *self.ipc.send_slot.lock() = Some(cx.waker().clone());
1169 match self.ipc.send(&self.item) {
1170 Ok(()) => Poll::Ready(Ok(())),
1171 Err(ApiError::Transport(TransportError::Full)) => Poll::Pending,
1172 Err(e) => Poll::Ready(Err(e)),
1173 }
1174 }
1175}
1176
1177fn control_path_for(base: &Path) -> PathBuf {
1178 let mut p = base.to_path_buf();
1179 let stem = p.file_stem().map(|s| s.to_owned()).unwrap_or_default();
1180 p.set_file_name(format!("{}.ctl.bin", stem.to_string_lossy()));
1181 p
1182}
1183
1184fn ring_path_prefix_for(base: &Path) -> PathBuf {
1185 let mut p = base.to_path_buf();
1190 let stem = p.file_stem().map(|s| s.to_owned()).unwrap_or_default();
1191 p.set_file_name(format!("{}.ring", stem.to_string_lossy()));
1192 p
1193}
1194
1195fn khl_path_for(base: &Path) -> PathBuf {
1196 let stem = base.file_stem().map(|s| s.to_owned()).unwrap_or_default();
1197 base.with_file_name(format!("{}.khl.bin", stem.to_string_lossy()))
1198}
1199
1200fn deque_path_for(base: &Path) -> PathBuf {
1201 let mut p = base.to_path_buf();
1202 let stem = p.file_stem().map(|s| s.to_owned()).unwrap_or_default();
1203 p.set_file_name(format!("{}.deque.bin", stem.to_string_lossy()));
1204 p
1205}
1206
1207fn pingen_path_for(base: &Path) -> PathBuf {
1208 let mut p = base.to_path_buf();
1209 let stem = p.file_stem().map(|s| s.to_owned()).unwrap_or_default();
1210 p.set_file_name(format!("{}.pingen.bin", stem.to_string_lossy()));
1211 p
1212}
1213
1214pub struct PinnedIpc<'a, T: Marshal + Copy + 'static> {
1235 parent: &'a AdaptiveIpc<T>,
1236 pinned_generation: u64,
1237 family: MmfFamily,
1238 _not_sync: PhantomData<Cell<()>>,
1239}
1240
1241impl<'a, T: Marshal + Copy + 'static> PinnedIpc<'a, T> {
1242 pub fn family(&self) -> MmfFamily { self.family }
1244
1245 pub fn pinned_generation(&self) -> u64 { self.pinned_generation }
1247
1248 pub fn is_still_valid(&self) -> bool {
1252 self.parent.pin_generation.load(Ordering::Acquire)
1253 == self.pinned_generation
1254 }
1255
1256 pub fn as_ring(&self) -> Option<&AdaptiveRing> {
1267 match self.family {
1268 MmfFamily::SharedRing => Some(&self.parent.ring),
1269 _ => None,
1270 }
1271 }
1272
1273 pub fn as_deque(&self) -> Option<&SharedDeque<PassSlot>> {
1275 match self.family {
1276 MmfFamily::SharedDeque(_) => Some(&self.parent.deque),
1277 _ => None,
1278 }
1279 }
1280}
1281
1282pub struct AdaptiveIpcSidecar {
1295 handle: Option<std::thread::JoinHandle<()>>,
1296 stop: Arc<std::sync::atomic::AtomicBool>,
1297 promotions_triggered: Arc<AtomicU64>,
1298}
1299
1300impl AdaptiveIpcSidecar {
1301 pub fn spawn<T: Marshal + Copy + Send + Sync + 'static>(
1305 ipc: Arc<AdaptiveIpc<T>>,
1306 scan_interval: std::time::Duration,
1307 ) -> Self {
1308 let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
1309 let promotions_triggered = Arc::new(AtomicU64::new(0));
1310
1311 let stop_c = stop.clone();
1312 let promotions_c = promotions_triggered.clone();
1313 let handle = std::thread::spawn(move || {
1314 while !stop_c.load(Ordering::Acquire) {
1315 if let Ok(Some(_)) = ipc.maybe_promote() {
1316 promotions_c.fetch_add(1, Ordering::Relaxed);
1317 }
1318 std::thread::sleep(scan_interval);
1319 }
1320 });
1321
1322 Self {
1323 handle: Some(handle),
1324 stop,
1325 promotions_triggered,
1326 }
1327 }
1328
1329 pub fn promotions_triggered(&self) -> u64 {
1332 self.promotions_triggered.load(Ordering::Acquire)
1333 }
1334
1335 pub fn shutdown(mut self) {
1337 self.stop.store(true, Ordering::Release);
1338 if let Some(h) = self.handle.take() {
1339 h.join().expect("sidecar thread panicked");
1340 }
1341 }
1342}
1343
1344impl Drop for AdaptiveIpcSidecar {
1345 fn drop(&mut self) {
1346 self.stop.store(true, Ordering::Release);
1347 if let Some(h) = self.handle.take() {
1348 h.join().ok();
1349 }
1350 }
1351}
1352
1353
1354#[cfg(test)]
1355mod tests {
1356 use super::*;
1357 use crate::dispatch_deque::DequeVariant;
1358
1359 fn tmp(name: &str) -> PathBuf {
1360 let mut p = std::env::temp_dir();
1361 let pid = std::process::id();
1362 let nonce = std::time::SystemTime::now()
1363 .duration_since(std::time::UNIX_EPOCH)
1364 .map(|d| d.as_nanos())
1365 .unwrap_or(0);
1366 p.push(format!("subetha_adaptive_{pid}_{nonce}_{name}"));
1367 p
1368 }
1369
1370 #[test]
1371 fn create_and_send_round_trip_in_initial_family() {
1372 let path = tmp("init");
1373 let shape = MmfWorkloadShape::StreamingMpmc {
1374 n_producers: 1,
1375 n_consumers: 1,
1376 };
1377 let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 64, 1)
1378 .expect("create");
1379 assert_eq!(ipc.active_family(), MmfFamily::SharedRing);
1380 ipc.send(&111).expect("send");
1381 ipc.send(&222).expect("send");
1382 let a = ipc.recv().expect("recv");
1383 let b = ipc.recv().expect("recv");
1384 assert_eq!(a, 111);
1385 assert_eq!(b, 222);
1386 }
1387
1388 #[test]
1389 fn migrate_to_changes_active_family_kernel_bypass() {
1390 let path = tmp("migrate");
1391 let shape = MmfWorkloadShape::StreamingMpmc {
1392 n_producers: 1,
1393 n_consumers: 1,
1394 };
1395 let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 64, 1)
1396 .expect("create");
1397 assert_eq!(ipc.active_family(), MmfFamily::SharedRing);
1398 ipc.migrate_to(MmfFamily::SharedDeque(DequeVariant::Khl))
1399 .expect("migrate");
1400 assert_eq!(
1401 ipc.active_family(),
1402 MmfFamily::SharedDeque(DequeVariant::Khl)
1403 );
1404 ipc.send(&333).expect("send post-migrate");
1405 let v = ipc.recv().expect("recv post-migrate");
1406 assert_eq!(v, 333);
1407 }
1408
1409 #[test]
1410 fn maybe_promote_observes_batches_and_migrates_to_work_stealing() {
1411 let path = tmp("auto_promote");
1412 let shape = MmfWorkloadShape::StreamingMpmc {
1413 n_producers: 1,
1414 n_consumers: 1,
1415 };
1416 let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 256, 1)
1417 .expect("create");
1418 assert_eq!(ipc.active_family(), MmfFamily::SharedRing);
1419 for _ in 0..10 {
1420 let batch: Vec<u64> = (0..16).collect();
1421 ipc.send_batch(&batch).expect("batch");
1422 }
1423 let snap = ipc.profile_snapshot();
1424 assert!(snap.batch_ratio() > 0.5);
1425 let promoted = ipc.maybe_promote().expect("promote");
1426 assert!(promoted.is_some());
1427 assert!(matches!(
1428 ipc.active_family(),
1429 MmfFamily::SharedDeque(_)
1430 ));
1431 }
1432
1433 #[test]
1434 fn drain_after_migration_reads_from_both_backings() {
1435 let path = tmp("drain");
1436 let shape = MmfWorkloadShape::StreamingMpmc {
1437 n_producers: 1,
1438 n_consumers: 1,
1439 };
1440 let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 64, 1)
1441 .expect("create");
1442 for i in 0..3u64 {
1443 ipc.send(&i).expect("send pre");
1444 }
1445 ipc.migrate_to(MmfFamily::SharedDeque(DequeVariant::Khl))
1446 .expect("migrate");
1447 for i in 100..103u64 {
1448 ipc.send(&i).expect("send post");
1449 }
1450 let mut seen = Vec::new();
1451 for _ in 0..6 {
1452 let v = ipc.recv().expect("recv");
1453 seen.push(v);
1454 }
1455 assert_eq!(seen.iter().sum::<u64>(), 306);
1456 }
1457
1458 #[test]
1459 fn profile_snapshot_tracks_single_and_batch_sends() {
1460 let path = tmp("profile");
1461 let shape = MmfWorkloadShape::StreamingMpmc {
1462 n_producers: 1,
1463 n_consumers: 1,
1464 };
1465 let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 256, 1)
1466 .expect("create");
1467 ipc.send(&1).expect("send");
1468 ipc.send(&2).expect("send");
1469 let batch: Vec<u64> = (0..8).collect();
1470 ipc.send_batch(&batch).expect("batch");
1471 let snap = ipc.profile_snapshot();
1472 assert_eq!(snap.total_sends, 2);
1473 assert_eq!(snap.batch_sends, 1);
1474 assert_eq!(snap.batch_size_sum, 8);
1475 assert_eq!(snap.max_batch_size, 8);
1476 }
1477
1478 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1481 struct Big32([u8; 32]);
1482 unsafe impl Marshal for Big32 {
1483 const PAYLOAD_BYTES: usize = 32;
1484 fn marshal(&self, dst: &mut [u8]) {
1485 dst[..32].copy_from_slice(&self.0);
1486 }
1487 fn unmarshal(src: &[u8]) -> Result<Self, subetha_core::MarshalError> {
1488 if src.len() < 32 {
1489 return Err(subetha_core::MarshalError::ShortBuffer {
1490 expected: 32,
1491 got: src.len(),
1492 });
1493 }
1494 let mut b = [0u8; 32];
1495 b.copy_from_slice(&src[..32]);
1496 Ok(Big32(b))
1497 }
1498 }
1499
1500 fn drain_all_u64(ipc: &AdaptiveIpc<u64>, n: usize) -> Vec<u64> {
1501 let mut got = Vec::with_capacity(n);
1502 let mut spins = 0u64;
1503 while got.len() < n {
1504 match ipc.recv() {
1505 Ok(v) => got.push(v),
1506 Err(_) => {
1507 spins += 1;
1508 assert!(spins < 200_000_000, "recv stalled before draining all items");
1509 std::hint::spin_loop();
1510 }
1511 }
1512 }
1513 got
1514 }
1515
1516 #[test]
1517 fn khl_batch_send_round_trips_through_side_backing() {
1518 let path = tmp("khl_rt");
1519 let shape = MmfWorkloadShape::StreamingMpmc { n_producers: 1, n_consumers: 1 };
1520 let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 256, 1).expect("create");
1521 assert!(ipc.khl.is_some(), "u64 (8 bytes) fits KHL's 16-byte slot");
1522 let batch: Vec<u64> = (0..6).collect();
1523 ipc.send_batch(&batch).expect("batch");
1524 let mut got = drain_all_u64(&ipc, 6);
1525 got.sort_unstable();
1526 assert_eq!(got, batch, "every batched item received exactly once");
1527 }
1528
1529 #[test]
1530 fn khl_surplus_buffers_partial_slots() {
1531 let path = tmp("khl_surplus");
1534 let shape = MmfWorkloadShape::StreamingMpmc { n_producers: 1, n_consumers: 1 };
1535 let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 64, 1).expect("create");
1536 let batch: Vec<u64> = (10..17).collect();
1537 ipc.send_batch(&batch).expect("batch");
1538 let mut got = drain_all_u64(&ipc, 7);
1539 got.sort_unstable();
1540 assert_eq!(got, batch);
1541 }
1542
1543 #[test]
1544 fn khl_mixed_single_and_batch_all_received() {
1545 let path = tmp("khl_mixed");
1548 let shape = MmfWorkloadShape::StreamingMpmc { n_producers: 1, n_consumers: 1 };
1549 let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 256, 1).expect("create");
1550 ipc.send(&1).expect("single");
1551 ipc.send(&2).expect("single");
1552 let batch: Vec<u64> = (100..108).collect();
1553 ipc.send_batch(&batch).expect("batch");
1554 let mut expected: Vec<u64> = vec![1, 2];
1555 expected.extend(&batch);
1556 expected.sort_unstable();
1557 let mut got = drain_all_u64(&ipc, expected.len());
1558 got.sort_unstable();
1559 assert_eq!(got, expected);
1560 }
1561
1562 #[test]
1563 fn khl_large_batch_integrity() {
1564 let path = tmp("khl_large");
1565 let shape = MmfWorkloadShape::StreamingMpmc { n_producers: 1, n_consumers: 1 };
1566 let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 512, 1).expect("create");
1567 let batch: Vec<u64> = (0..300).collect();
1568 ipc.send_batch(&batch).expect("batch");
1569 let mut got = drain_all_u64(&ipc, 300);
1570 got.sort_unstable();
1571 assert_eq!(got, batch);
1572 }
1573
1574 #[test]
1575 fn khl_payload_gate_large_type_uses_no_khl() {
1576 let path = tmp("khl_gate");
1577 let shape = MmfWorkloadShape::StreamingMpmc { n_producers: 1, n_consumers: 1 };
1578 let ipc: AdaptiveIpc<Big32> = AdaptiveIpc::create(&path, shape, 64, 1).expect("create");
1579 assert!(ipc.khl.is_none(), "32-byte payload exceeds KHL's 16-byte slot");
1580 let batch: Vec<Big32> = (0..5u8).map(|i| Big32([i; 32])).collect();
1581 ipc.send_batch(&batch).expect("batch via existing per-item path");
1582 let mut got = Vec::new();
1583 let mut spins = 0u64;
1584 while got.len() < 5 {
1585 match ipc.recv() {
1586 Ok(v) => got.push(v),
1587 Err(_) => {
1588 spins += 1;
1589 assert!(spins < 200_000_000, "recv stalled");
1590 std::hint::spin_loop();
1591 }
1592 }
1593 }
1594 got.sort_by_key(|b| b.0[0]);
1595 assert_eq!(got, batch, ">16-byte batch round-trips via the per-item path");
1596 }
1597
1598 #[test]
1599 fn khl_multi_consumer_no_loss_or_dup() {
1600 use std::sync::atomic::{AtomicU64, Ordering as AOrd};
1601 let path = tmp("khl_multi");
1602 let shape = MmfWorkloadShape::StreamingMpmc { n_producers: 1, n_consumers: 4 };
1603 let ipc = Arc::new(AdaptiveIpc::<u64>::create(&path, shape, 512, 4).expect("create"));
1604 const N: u64 = 600;
1605 let batch: Vec<u64> = (0..N).collect();
1606 ipc.send_batch(&batch).expect("batch");
1607 let received = Arc::new(AtomicU64::new(0));
1608 let checksum = Arc::new(AtomicU64::new(0));
1609 let mut handles = Vec::new();
1610 for _ in 0..4 {
1611 let ipc = Arc::clone(&ipc);
1612 let received = Arc::clone(&received);
1613 let checksum = Arc::clone(&checksum);
1614 handles.push(std::thread::spawn(move || loop {
1615 if received.load(AOrd::Acquire) >= N {
1616 break;
1617 }
1618 match ipc.recv() {
1619 Ok(v) => {
1620 checksum.fetch_add(v, AOrd::AcqRel);
1621 received.fetch_add(1, AOrd::AcqRel);
1622 }
1623 Err(_) => std::hint::spin_loop(),
1624 }
1625 }));
1626 }
1627 for h in handles {
1628 h.join().unwrap();
1629 }
1630 assert_eq!(received.load(AOrd::Acquire), N, "exactly N items received");
1631 assert_eq!(
1632 checksum.load(AOrd::Acquire),
1633 (0..N).sum::<u64>(),
1634 "every item exactly once, none lost or duplicated"
1635 );
1636 }
1637
1638 #[test]
1639 fn rejects_kv_map_family_at_construction() {
1640 let path = tmp("reject_kv");
1641 let shape = MmfWorkloadShape::KeyValueLookup {
1642 n_readers: 1,
1643 n_writers: 1,
1644 };
1645 let result = AdaptiveIpc::<u64>::create(&path, shape, 64, 1);
1646 match result {
1647 Err(ApiError::WrongFamily { .. }) => {}
1648 Err(other) => panic!("expected WrongFamily, got {other:?}"),
1649 Ok(_) => panic!("expected error, got Ok"),
1650 }
1651 }
1652
1653 #[test]
1654 fn pin_captures_family_and_generation() {
1655 let path = tmp("pin_capture");
1656 let shape = MmfWorkloadShape::StreamingMpmc {
1657 n_producers: 1,
1658 n_consumers: 1,
1659 };
1660 let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 64, 1)
1661 .expect("create");
1662 let gen_before = ipc.pin_generation();
1663 let pin = ipc.pin_current_family();
1664 assert_eq!(pin.family(), MmfFamily::SharedRing);
1665 assert_eq!(pin.pinned_generation(), gen_before);
1666 assert!(pin.is_still_valid());
1667 }
1668
1669 #[test]
1670 fn migration_invalidates_outstanding_pin() {
1671 let path = tmp("pin_invalidate");
1672 let shape = MmfWorkloadShape::StreamingMpmc {
1673 n_producers: 1,
1674 n_consumers: 1,
1675 };
1676 let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 64, 1)
1677 .expect("create");
1678 let pin = ipc.pin_current_family();
1679 assert!(pin.is_still_valid());
1680 ipc.migrate_to(MmfFamily::SharedDeque(DequeVariant::Khl))
1681 .expect("migrate");
1682 assert!(!pin.is_still_valid(),
1683 "pin must invalidate on migration");
1684 let pin2 = ipc.pin_current_family();
1685 assert_eq!(pin2.family(), MmfFamily::SharedDeque(DequeVariant::Khl));
1686 assert!(pin2.is_still_valid());
1687 }
1688
1689 #[test]
1690 fn migrate_to_same_family_does_not_bump_generation() {
1691 let path = tmp("pin_noop");
1692 let shape = MmfWorkloadShape::StreamingMpmc {
1693 n_producers: 1,
1694 n_consumers: 1,
1695 };
1696 let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 64, 1)
1697 .expect("create");
1698 let pin = ipc.pin_current_family();
1699 let gen_before = pin.pinned_generation();
1700 ipc.migrate_to(MmfFamily::SharedRing).expect("noop migrate");
1701 assert_eq!(ipc.pin_generation(), gen_before,
1702 "no-op migrate must not bump generation");
1703 assert!(pin.is_still_valid(),
1704 "no-op migrate must not invalidate pin");
1705 }
1706
1707 #[test]
1708 fn pinned_as_ring_round_trip() {
1709 let path = tmp("pin_ring_rt");
1710 let shape = MmfWorkloadShape::StreamingMpmc {
1711 n_producers: 1,
1712 n_consumers: 1,
1713 };
1714 let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 64, 1)
1715 .expect("create");
1716 let pin = ipc.pin_current_family();
1717 let ring = pin.as_ring().expect("pinned at ring family");
1718 assert!(pin.as_deque().is_none(),
1719 "as_deque must return None when pinned at ring family");
1720
1721 let shape_pin = ring.pin_current_shape();
1727 assert_eq!(shape_pin.shape(), crate::RingShape::Spsc);
1728 assert!(shape_pin.is_still_valid());
1729
1730 let payload = 0xDEADBEEFu64.to_le_bytes();
1731 shape_pin.spsc_try_push(&payload).expect("native SPSC push");
1732 let mut buf = [0u8; crate::adaptive_ring::ADAPTIVE_SPSC_PAYLOAD_BYTES];
1733 let n = shape_pin.spsc_try_pop(&mut buf).expect("native SPSC pop");
1734 assert!(n >= 8);
1735 assert_eq!(&buf[..8], &payload);
1736 }
1737
1738 #[test]
1739 fn pinned_as_deque_round_trip() {
1740 let path = tmp("pin_deque_rt");
1741 let shape = MmfWorkloadShape::WorkStealing(
1742 crate::dispatch_deque::WorkloadShape {
1743 n_thieves: 1,
1744 batch_size: Some(4),
1745 wait_idle: false,
1746 },
1747 );
1748 let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 64, 1)
1749 .expect("create");
1750 let pin = ipc.pin_current_family();
1751 let deque = pin.as_deque().expect("pinned at deque family");
1752 assert!(pin.as_ring().is_none(),
1753 "as_ring must return None when pinned at deque family");
1754
1755 let mut slot = PassSlot([0u8; PAYLOAD_BYTES]);
1756 slot.0[..8].copy_from_slice(&7777u64.to_le_bytes());
1757 deque.push(&slot).expect("native push");
1758 let popped = deque.steal().expect("native steal");
1759 let val = u64::from_le_bytes(popped.0[..8].try_into().unwrap());
1760 assert_eq!(val, 7777);
1761 }
1762
1763 #[test]
1764 fn create_with_ordering_applies_declaration_and_round_trips() {
1765 let path = tmp("ordering_create");
1766 let shape = MmfWorkloadShape::StreamingMpmc {
1767 n_producers: 1,
1768 n_consumers: 1,
1769 };
1770 let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create_with_ordering(
1771 &path, shape, 64, 1, QosOrdering::GlobalFifo, None,
1772 ).expect("create");
1773 assert!(ipc.ring_handle().is_stamped());
1774 assert_eq!(ipc.ordering(), QosOrdering::GlobalFifo);
1775 assert_eq!(ipc.ring_handle().ordering_mode(),
1776 Some(OrderingMode::MergeByStamp));
1777
1778 ipc.send(&777).expect("send");
1780 ipc.send(&888).expect("send");
1781 assert_eq!(ipc.recv().expect("recv"), 777);
1782 assert_eq!(ipc.recv().expect("recv"), 888);
1783
1784 ipc.set_ordering(QosOrdering::PerProducer).expect("withdraw");
1786 assert_eq!(ipc.ordering(), QosOrdering::PerProducer);
1787 assert_eq!(ipc.ring_handle().ordering_mode(),
1788 Some(OrderingMode::Unordered));
1789 }
1790
1791 #[test]
1792 fn set_ordering_on_unstamped_ring_routes_through_vyukov_morph() {
1793 let path = tmp("ordering_unstamped");
1794 let shape = MmfWorkloadShape::StreamingMpmc {
1795 n_producers: 1,
1796 n_consumers: 1,
1797 };
1798 let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create(&path, shape, 64, 1)
1799 .expect("create");
1800 assert!(!ipc.ring_handle().is_stamped());
1801 assert_eq!(ipc.ordering(), QosOrdering::PerProducer);
1802
1803 ipc.set_ordering(QosOrdering::GlobalFifo).expect("declare");
1804 assert_eq!(ipc.ring_handle().current_shape(), RingShape::Vyukov,
1805 "unstamped GlobalFifo declaration must morph to Vyukov");
1806 assert_eq!(ipc.ordering(), QosOrdering::GlobalFifo);
1807 ipc.send(&5).expect("send through Vyukov");
1808 assert_eq!(ipc.recv().expect("recv"), 5);
1809
1810 ipc.set_ordering(QosOrdering::PerProducer).expect("withdraw");
1811 assert_ne!(ipc.ring_handle().current_shape(), RingShape::Vyukov,
1812 "withdrawal must walk the Vyukov morph back");
1813 }
1814
1815 #[test]
1816 fn auto_order_arms_merge_on_observed_inversion_rate() {
1817 let path = tmp("auto_order");
1818 let shape = MmfWorkloadShape::StreamingMpmc {
1819 n_producers: 1,
1820 n_consumers: 2,
1821 };
1822 let ipc: AdaptiveIpc<u64> = AdaptiveIpc::create_with_ordering(
1823 &path, shape, 64, 2, QosOrdering::PerProducer, Some(1.0),
1824 ).expect("create");
1825 let ring = ipc.ring_handle();
1826 assert_eq!(ring.ordering_mode(), Some(OrderingMode::Unordered));
1827
1828 ring.morph_to(crate::RingShape::Mpsc).expect("morph");
1832 ring.register_producer().expect("p1");
1833 ring.try_send(1, &1u64.to_le_bytes()).expect("send p1");
1834 ring.try_send(0, &2u64.to_le_bytes()).expect("send p0");
1835 let mut out = [0u8; crate::ordering::STAMPED_PAYLOAD_BYTES];
1836 ring.try_recv(0, &mut out).expect("pop 1");
1837 ring.try_recv(0, &mut out).expect("pop 2");
1838 assert!(ring.inversions() >= 1, "interleave must register an inversion");
1839
1840 ipc.maybe_promote().expect("promote poll");
1843 assert_eq!(ring.ordering_mode(), Some(OrderingMode::MergeByStamp),
1844 "auto_order threshold crossing must arm MergeByStamp");
1845 assert_eq!(ipc.ordering(), QosOrdering::GlobalFifo);
1846 }
1847
1848 #[test]
1849 fn sidecar_auto_promotes_on_observed_batches() {
1850 let path = tmp("sidecar_promote");
1851 let shape = MmfWorkloadShape::StreamingMpmc {
1852 n_producers: 1,
1853 n_consumers: 1,
1854 };
1855 let ipc = Arc::new(
1856 AdaptiveIpc::<u64>::create(&path, shape, 256, 1)
1857 .expect("create"),
1858 );
1859 assert_eq!(ipc.active_family(), MmfFamily::SharedRing);
1860 let sidecar = AdaptiveIpcSidecar::spawn(
1861 ipc.clone(),
1862 std::time::Duration::from_millis(5),
1863 );
1864
1865 for _ in 0..10 {
1867 let batch: Vec<u64> = (0..16).collect();
1868 ipc.send_batch(&batch).expect("batch");
1869 }
1870 for _ in 0..16 { ipc.recv().ok(); }
1872
1873 let deadline = std::time::Instant::now()
1875 + std::time::Duration::from_secs(2);
1876 while std::time::Instant::now() < deadline
1877 && !matches!(ipc.active_family(), MmfFamily::SharedDeque(_))
1878 {
1879 std::thread::sleep(std::time::Duration::from_millis(10));
1880 }
1881
1882 assert!(matches!(ipc.active_family(), MmfFamily::SharedDeque(_)),
1883 "sidecar should have promoted to SharedDeque");
1884 assert!(sidecar.promotions_triggered() >= 1,
1885 "sidecar should have recorded at least one promotion");
1886 sidecar.shutdown();
1887 }
1888}