Skip to main content

orbit_stream/exchange/
arena.rs

1use std::collections::HashMap;
2use std::mem::size_of;
3use std::ops::Deref;
4use std::sync::atomic::{AtomicU8, AtomicU32, AtomicU64, Ordering};
5use std::sync::{Arc, LazyLock, Mutex, Weak};
6use std::task::{Context, Poll, Waker};
7
8use orbit_core::Fleet;
9#[cfg(unix)]
10use orbit_core::shm::{ShmRegion, ring_segment_name};
11
12use super::{ChunkDescriptor, ChunkGeometry, ChunkPlan, DEFAULT_CHUNK_BYTES};
13use crate::wake::{Doorstep, Driver};
14use crate::{Error, Incarnation, Result, lock_unpoisoned};
15
16const MAGIC: u32 = 0x4f_50_41_59; // "OPAY"
17const VERSION: u16 = 4;
18
19const SLOT_FREE: u8 = 0;
20const SLOT_RESERVED: u8 = 1;
21const SLOT_LIVE: u8 = 2;
22const SLOT_READING: u8 = 3;
23const SLOT_RECLAIMING: u8 = 4;
24
25/// Geometry and SHM identity of one fleet-wide payload arena.
26#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
27pub struct PayloadArenaSpec {
28    pub kind: u8,
29    /// Physical allocation units available to each fleet node.
30    pub slots_per_node: usize,
31    /// Bytes in one physical allocation unit.
32    pub slot_size: usize,
33}
34
35impl PayloadArenaSpec {
36    pub const fn new(
37        kind: u8,
38        slots_per_node: usize,
39        slot_size: usize,
40    ) -> Self {
41        Self { kind, slots_per_node, slot_size }
42    }
43
44    pub const fn lane_bytes(self) -> usize {
45        self.slots_per_node * self.slot_size
46    }
47
48    pub const fn slots_for(
49        self,
50        payload_len: usize,
51    ) -> usize {
52        payload_len.div_ceil(self.slot_size)
53    }
54
55    /// Physical chunk geometry for this arena. Runtime policy chooses the
56    /// slot count independently for each request or response.
57    pub const fn chunk_geometry(self) -> Option<ChunkGeometry> {
58        ChunkGeometry::new(self.slot_size, self.slots_per_node)
59    }
60
61    /// Plan a known payload with the benchmark-backed default chunk target,
62    /// clamped so one chunk always fits in this node lane.
63    pub const fn plan_chunks(
64        self,
65        data_bytes: usize,
66    ) -> Option<ChunkPlan> {
67        let lane_bytes = self.lane_bytes();
68        let limit = if DEFAULT_CHUNK_BYTES < lane_bytes {
69            DEFAULT_CHUNK_BYTES
70        } else {
71            lane_bytes
72        };
73        ChunkPlan::for_limit(data_bytes, self.slot_size, limit)
74    }
75
76    /// Plan using a runtime-selected number of slots per publication.
77    pub const fn plan_chunks_with(
78        self,
79        data_bytes: usize,
80        slots_per_chunk: usize,
81    ) -> Option<ChunkPlan> {
82        let geometry = match self.chunk_geometry() {
83            Some(geometry) => geometry,
84            None => return None,
85        };
86        geometry.plan(data_bytes, slots_per_chunk)
87    }
88
89    fn validate(self) -> Result<()> {
90        if self.slots_per_node == 0
91            || self.slot_size == 0
92            || !self.slots_per_node.is_power_of_two()
93            || !self.slot_size.is_power_of_two()
94            || self.slots_per_node > u32::MAX as usize
95            || self.slot_size > u32::MAX as usize
96        {
97            return Err(Error::Malformed(format!(
98                "payload arena kind={} slots_per_node={} slot_size={}: both geometry values must be non-zero powers of two fitting u32",
99                self.kind, self.slots_per_node, self.slot_size
100            )));
101        }
102        Ok(())
103    }
104}
105
106#[repr(C, align(64))]
107struct Header {
108    magic: u32,
109    version: u16,
110    header_size: u16,
111    slot_meta_size: u32,
112    slot_size: u32,
113    slots_per_node: u32,
114    fleet_capacity: u16,
115    _reserved: [u8; 2],
116    next_generation: AtomicU64,
117    epoch: AtomicU64,
118    _reserved_credit: [u8; 24],
119}
120
121impl Header {
122    fn new(
123        fleet_capacity: u16,
124        spec: PayloadArenaSpec,
125    ) -> Self {
126        Self {
127            magic: MAGIC,
128            version: VERSION,
129            header_size: size_of::<Self>() as u16,
130            slot_meta_size: size_of::<SlotMeta>() as u32,
131            slot_size: spec.slot_size as u32,
132            slots_per_node: spec.slots_per_node as u32,
133            fleet_capacity,
134            _reserved: [0; 2],
135            next_generation: AtomicU64::new(1),
136            epoch: AtomicU64::new(1),
137            _reserved_credit: [0; 24],
138        }
139    }
140
141    fn compatible(
142        &self,
143        fleet_capacity: u16,
144        spec: PayloadArenaSpec,
145    ) -> bool {
146        self.magic == MAGIC
147            && self.version == VERSION
148            && self.header_size as usize == size_of::<Self>()
149            && self.slot_meta_size as usize == size_of::<SlotMeta>()
150            && self.slot_size as usize == spec.slot_size
151            && self.slots_per_node as usize == spec.slots_per_node
152            && self.fleet_capacity == fleet_capacity
153    }
154}
155
156#[repr(C, align(64))]
157struct CreditWord {
158    generation: AtomicU32,
159    waiters: AtomicU32,
160    _padding: [u8; 56],
161}
162
163#[repr(C, align(8))]
164struct SlotMeta {
165    state: AtomicU8,
166    _reserved: AtomicU8,
167    reader_node: std::sync::atomic::AtomicU16,
168    allocation_first: AtomicU64,
169    generation: AtomicU64,
170    producer_incarnation: AtomicU64,
171    reader_incarnation: AtomicU64,
172}
173
174const _: () = assert!(size_of::<Header>() == 64);
175const _: () = assert!(size_of::<CreditWord>() == 64);
176const _: () = assert!(size_of::<SlotMeta>() == 40);
177
178#[derive(Clone, Copy)]
179struct Geometry {
180    slots_per_node: usize,
181    slot_size: usize,
182    total_slots: usize,
183    credits_offset: usize,
184    metadata_offset: usize,
185    payload_offset: usize,
186    segment_size: usize,
187}
188
189impl Geometry {
190    fn new(
191        fleet_capacity: u16,
192        spec: PayloadArenaSpec,
193    ) -> Self {
194        let total_slots = usize::from(fleet_capacity) * spec.slots_per_node;
195        let credits_offset = size_of::<Header>();
196        let metadata_offset = credits_offset
197            + usize::from(fleet_capacity) * size_of::<CreditWord>();
198        let payload_offset = (metadata_offset
199            + total_slots * size_of::<SlotMeta>())
200        .next_multiple_of(64);
201        let segment_size = payload_offset + total_slots * spec.slot_size;
202        Self {
203            slots_per_node: spec.slots_per_node,
204            slot_size: spec.slot_size,
205            total_slots,
206            credits_offset,
207            metadata_offset,
208            payload_offset,
209            segment_size,
210        }
211    }
212}
213
214enum Backing {
215    Memory(AlignedBytes),
216    #[cfg(unix)]
217    Shm(ShmRegion),
218}
219
220struct AlignedBytes {
221    ptr: *mut u8,
222    layout: std::alloc::Layout,
223}
224
225impl AlignedBytes {
226    fn zeroed(size: usize) -> Self {
227        let layout = std::alloc::Layout::from_size_align(size, 64)
228            .expect("payload arena layout");
229        // SAFETY: geometry always includes the non-empty header.
230        let ptr = unsafe { std::alloc::alloc_zeroed(layout) };
231        assert!(!ptr.is_null(), "payload arena allocation failed");
232        Self { ptr, layout }
233    }
234}
235
236impl Drop for AlignedBytes {
237    fn drop(&mut self) {
238        // SAFETY: allocated by `alloc_zeroed` with this exact layout.
239        unsafe { std::alloc::dealloc(self.ptr, self.layout) }
240    }
241}
242
243// SAFETY: mapped bytes are accessed through atomics and allocation ownership.
244unsafe impl Send for AlignedBytes {}
245unsafe impl Sync for AlignedBytes {}
246
247struct Arena {
248    _fleet: Arc<Fleet>,
249    backing: Backing,
250    geometry: Geometry,
251    kind: u8,
252    node: u16,
253    incarnation: Incarnation,
254    allocation: Mutex<usize>,
255    credit_wakers: Mutex<Vec<Waker>>,
256    driver: Mutex<Option<Driver>>,
257}
258
259impl Arena {
260    fn base(&self) -> *mut u8 {
261        match &self.backing {
262            Backing::Memory(bytes) => bytes.ptr,
263            #[cfg(unix)]
264            Backing::Shm(region) => region.as_ptr(),
265        }
266    }
267
268    fn header(&self) -> &Header {
269        // SAFETY: every backing begins with the initialized arena header.
270        unsafe { &*self.base().cast::<Header>() }
271    }
272
273    fn metadata(&self) -> &[SlotMeta] {
274        // SAFETY: geometry reserves this aligned range for atomic metadata.
275        unsafe {
276            std::slice::from_raw_parts(
277                self.base()
278                    .add(self.geometry.metadata_offset)
279                    .cast::<SlotMeta>(),
280                self.geometry.total_slots,
281            )
282        }
283    }
284
285    fn credits(&self) -> &[CreditWord] {
286        // SAFETY: geometry reserves one cache-line-aligned word per fleet node.
287        unsafe {
288            std::slice::from_raw_parts(
289                self.base()
290                    .add(self.geometry.credits_offset)
291                    .cast::<CreditWord>(),
292                self.geometry.total_slots / self.geometry.slots_per_node,
293            )
294        }
295    }
296
297    fn local_credit(&self) -> &CreditWord {
298        &self.credits()[usize::from(self.node)]
299    }
300
301    fn payload(
302        &self,
303        absolute_slot: usize,
304    ) -> *mut u8 {
305        // SAFETY: callers validate the slot against `total_slots`.
306        unsafe {
307            self.base().add(
308                self.geometry.payload_offset
309                    + absolute_slot * self.geometry.slot_size,
310            )
311        }
312    }
313
314    fn reserve(
315        self: &Arc<Self>,
316        payload_len: usize,
317    ) -> Result<Publication> {
318        let count = self.required_slots(payload_len)?;
319
320        let mut hint = lock_unpoisoned(&self.allocation);
321        let lane_start = usize::from(self.node) * self.geometry.slots_per_node;
322        let candidate = (0..self.geometry.slots_per_node)
323            .map(|offset| (*hint + offset) & (self.geometry.slots_per_node - 1))
324            .find(|&local| {
325                local + count <= self.geometry.slots_per_node
326                    && self.metadata()
327                        [lane_start + local..lane_start + local + count]
328                        .iter()
329                        .all(|slot| {
330                            slot.state.load(Ordering::Acquire) == SLOT_FREE
331                        })
332            })
333            .ok_or(Error::PayloadFull { requested_slots: count })?;
334        let first = lane_start + candidate;
335        for slot in &self.metadata()[first..first + count] {
336            slot.state.store(SLOT_RESERVED, Ordering::Relaxed);
337        }
338        let mut generation =
339            self.header().next_generation.fetch_add(1, Ordering::Relaxed);
340        if generation == 0 {
341            generation =
342                self.header().next_generation.fetch_add(1, Ordering::Relaxed);
343        }
344        for slot in &self.metadata()[first..first + count] {
345            slot.allocation_first.store(first as u64, Ordering::Relaxed);
346            slot.generation.store(generation, Ordering::Relaxed);
347            slot.producer_incarnation
348                .store(self.incarnation.get(), Ordering::Relaxed);
349            slot.reader_node.store(u16::MAX, Ordering::Relaxed);
350            slot.reader_incarnation.store(0, Ordering::Relaxed);
351        }
352        self.metadata()[first].state.store(SLOT_RESERVED, Ordering::Release);
353        *hint = (candidate + count) & (self.geometry.slots_per_node - 1);
354        Ok(Publication {
355            arena: Arc::clone(self),
356            first,
357            count,
358            payload_len,
359            generation,
360            published: false,
361        })
362    }
363
364    fn release(
365        &self,
366        first: usize,
367        count: usize,
368        generation: u64,
369    ) {
370        let slots = &self.metadata()[first..first + count];
371        if slots
372            .iter()
373            .any(|slot| slot.generation.load(Ordering::Acquire) != generation)
374        {
375            return;
376        }
377        for slot in slots.iter().skip(1) {
378            slot.state.store(SLOT_FREE, Ordering::Relaxed);
379        }
380        slots[0].state.store(SLOT_FREE, Ordering::Release);
381        let owner = first / self.geometry.slots_per_node;
382        let credit = &self.credits()[owner];
383        credit.generation.fetch_add(1, Ordering::SeqCst);
384        if credit.waiters.load(Ordering::SeqCst) > 0 {
385            crate::wake_on(&credit.generation);
386        }
387    }
388
389    fn required_slots(
390        &self,
391        payload_len: usize,
392    ) -> Result<usize> {
393        if payload_len == 0 {
394            return Err(Error::Malformed(
395                "an empty payload needs no arena allocation".to_owned(),
396            ));
397        }
398        let count = payload_len.div_ceil(self.geometry.slot_size);
399        if count > self.geometry.slots_per_node {
400            return Err(Error::PayloadTooLarge {
401                len: payload_len,
402                capacity: self.geometry.slots_per_node
403                    * self.geometry.slot_size,
404            });
405        }
406        Ok(count)
407    }
408
409    fn has_run(
410        &self,
411        count: usize,
412    ) -> bool {
413        let lane_start = usize::from(self.node) * self.geometry.slots_per_node;
414        (0..=self.geometry.slots_per_node - count).any(|local| {
415            self.metadata()[lane_start + local..lane_start + local + count]
416                .iter()
417                .all(|slot| slot.state.load(Ordering::Acquire) == SLOT_FREE)
418        })
419    }
420
421    fn wait_available(
422        &self,
423        count: usize,
424    ) -> Result<()> {
425        let credit = self.local_credit();
426        loop {
427            if self.has_run(count) {
428                return Ok(());
429            }
430            credit.waiters.fetch_add(1, Ordering::SeqCst);
431            let seen = credit.generation.load(Ordering::SeqCst);
432            let outcome = if self.has_run(count) {
433                Ok(())
434            } else {
435                crate::wait_on(&credit.generation, seen)
436            };
437            credit.waiters.fetch_sub(1, Ordering::SeqCst);
438            outcome?;
439        }
440    }
441
442    fn poll_available(
443        self: &Arc<Self>,
444        count: usize,
445        cx: &mut Context<'_>,
446    ) -> Poll<Result<()>> {
447        if self.has_run(count) {
448            return Poll::Ready(Ok(()));
449        }
450        {
451            let mut wakers = lock_unpoisoned(&self.credit_wakers);
452            if !wakers.iter().any(|waker| waker.will_wake(cx.waker())) {
453                wakers.push(cx.waker().clone());
454            }
455        }
456        if let Err(error) = self.ensure_driver() {
457            return Poll::Ready(Err(error));
458        }
459        if self.has_run(count) { Poll::Ready(Ok(())) } else { Poll::Pending }
460    }
461
462    fn ensure_driver(self: &Arc<Self>) -> Result<()> {
463        let mut driver = lock_unpoisoned(&self.driver);
464        if driver.is_none() {
465            *driver = Some(Driver::start(
466                Arc::as_ptr(self),
467                format!("orbit-payload-{}-{}-driver", self.kind, self.node),
468            )?);
469        }
470        Ok(())
471    }
472
473    fn extent_count(
474        &self,
475        first: usize,
476        generation: u64,
477    ) -> usize {
478        let lane_end = (first / self.geometry.slots_per_node + 1)
479            * self.geometry.slots_per_node;
480        self.metadata()[first..lane_end]
481            .iter()
482            .take_while(|slot| {
483                slot.allocation_first.load(Ordering::Acquire) as usize == first
484                    && slot.generation.load(Ordering::Acquire) == generation
485            })
486            .count()
487    }
488
489    fn node_dead(
490        &self,
491        node: u16,
492        incarnation: Incarnation,
493    ) {
494        let metadata = self.metadata();
495        for (first, slot) in metadata.iter().enumerate() {
496            let state = slot.state.load(Ordering::Acquire);
497            let producer_node = first / self.geometry.slots_per_node;
498            let allocation_start =
499                slot.allocation_first.load(Ordering::Acquire) as usize == first;
500            let abandoned_publication = allocation_start
501                && matches!(state, SLOT_RESERVED | SLOT_LIVE)
502                && producer_node == usize::from(node)
503                && slot.producer_incarnation.load(Ordering::Acquire)
504                    == incarnation.get();
505            let abandoned_read = state == SLOT_READING
506                && slot.reader_node.load(Ordering::Acquire) == node
507                && slot.reader_incarnation.load(Ordering::Acquire)
508                    == incarnation.get();
509            if !abandoned_publication && !abandoned_read {
510                continue;
511            }
512            if slot
513                .state
514                .compare_exchange(
515                    state,
516                    SLOT_RECLAIMING,
517                    Ordering::SeqCst,
518                    Ordering::Acquire,
519                )
520                .is_err()
521            {
522                continue;
523            }
524            let generation = slot.generation.load(Ordering::Acquire);
525            let count = self.extent_count(first, generation);
526            if count != 0 {
527                self.release(first, count, generation);
528            }
529        }
530    }
531}
532
533impl Doorstep for Arena {
534    fn generation(&self) -> &AtomicU32 {
535        &self.local_credit().generation
536    }
537
538    fn listening(
539        &self,
540        delta: i32,
541    ) {
542        if delta > 0 {
543            self.local_credit().waiters.fetch_add(1, Ordering::SeqCst);
544        } else {
545            self.local_credit().waiters.fetch_sub(1, Ordering::SeqCst);
546        }
547    }
548
549    fn drain(&self) {
550        let wakers = std::mem::take(&mut *lock_unpoisoned(&self.credit_wakers));
551        for waker in wakers {
552            waker.wake();
553        }
554    }
555}
556
557impl Drop for Arena {
558    fn drop(&mut self) {
559        if let Some(mut driver) = lock_unpoisoned(&self.driver).take() {
560            driver.stop(&self.local_credit().generation);
561        }
562    }
563}
564
565/// One fleet-wide payload allocation table. Every producer allocates from its
566/// node's exclusive lane, independent of which exchange side it currently holds.
567#[derive(Clone)]
568pub struct PayloadArena {
569    arena: Arc<Arena>,
570}
571
572impl PayloadArena {
573    pub fn open(
574        fleet: Arc<Fleet>,
575        incarnation: Incarnation,
576        spec: PayloadArenaSpec,
577    ) -> Result<Self> {
578        spec.validate()?;
579        Ok(Self { arena: open(&fleet, incarnation, spec)? })
580    }
581
582    pub fn kind(&self) -> u8 {
583        self.arena.kind
584    }
585
586    pub fn slot_size(&self) -> usize {
587        self.arena.geometry.slot_size
588    }
589
590    pub fn slots_per_node(&self) -> usize {
591        self.arena.geometry.slots_per_node
592    }
593
594    pub fn chunk_geometry(&self) -> ChunkGeometry {
595        // PayloadArena::open validates both geometry values as non-zero.
596        ChunkGeometry::new(self.slot_size(), self.slots_per_node())
597            .expect("opened payload arena has valid chunk geometry")
598    }
599
600    pub fn plan_chunks(
601        &self,
602        data_bytes: usize,
603    ) -> Result<ChunkPlan> {
604        PayloadArenaSpec::new(
605            self.kind(),
606            self.slots_per_node(),
607            self.slot_size(),
608        )
609        .plan_chunks(data_bytes)
610        .ok_or_else(|| {
611            Error::Malformed(
612                "payload arena cannot plan zero-sized geometry".to_owned(),
613            )
614        })
615    }
616
617    pub fn plan_chunks_with(
618        &self,
619        data_bytes: usize,
620        slots_per_chunk: usize,
621    ) -> Result<ChunkPlan> {
622        self.chunk_geometry().plan(data_bytes, slots_per_chunk).ok_or_else(|| {
623            Error::Malformed(format!(
624                "chunk slot count {slots_per_chunk} exceeds payload lane capacity {}",
625                self.slots_per_node()
626            ))
627        })
628    }
629    /// Park this thread until a run large enough for `payload_len` may be
630    /// available. Allocation still decides the race after the wake.
631    pub fn wait_available(
632        &self,
633        payload_len: usize,
634    ) -> Result<()> {
635        let count = self.arena.required_slots(payload_len)?;
636        self.arena.wait_available(count)
637    }
638
639    /// Whether this producer lane currently has a contiguous run for the
640    /// payload. This is only a readiness snapshot; allocation still decides
641    /// a race between concurrent producers.
642    pub fn is_available(
643        &self,
644        payload_len: usize,
645    ) -> Result<bool> {
646        let count = self.arena.required_slots(payload_len)?;
647        Ok(self.arena.has_run(count))
648    }
649
650    /// Task readiness for this node's payload credit. Credit returns are
651    /// coalesced through one shared generation and one local driver.
652    pub fn poll_available(
653        &self,
654        payload_len: usize,
655        cx: &mut Context<'_>,
656    ) -> Poll<Result<()>> {
657        let count = match self.arena.required_slots(payload_len) {
658            Ok(count) => count,
659            Err(error) => return Poll::Ready(Err(error)),
660        };
661        self.arena.poll_available(count, cx)
662    }
663
664    /// Clear this arena only while the fleet is known quiescent. Normal
665    /// attach and replacement never call this implicitly.
666    pub fn reset_all(&self) {
667        let mut hint = lock_unpoisoned(&self.arena.allocation);
668        *hint = 0;
669        for slot in self.arena.metadata() {
670            slot.state.store(SLOT_FREE, Ordering::Release);
671        }
672        for credit in self.arena.credits() {
673            credit.generation.fetch_add(1, Ordering::SeqCst);
674            crate::wake_on(&credit.generation);
675        }
676    }
677
678    /// Reclaim only allocations owned by a process incarnation whose death
679    /// was confirmed by the embedder. A producer's unpublished or unread
680    /// chunks and a dead reader's held chunks return; a chunk already held by
681    /// a surviving consumer remains that consumer's.
682    pub fn node_dead(
683        &self,
684        node: orbit_core::NodeId,
685        incarnation: Incarnation,
686    ) {
687        self.arena.node_dead(node.get(), incarnation);
688    }
689
690    /// Remove the arena's SHM name. Existing mappings remain valid.
691    #[cfg(unix)]
692    pub fn unlink(&self) -> Result<()> {
693        match &self.arena.backing {
694            Backing::Memory(_) => {
695                self.reset_all();
696                Ok(())
697            }
698            Backing::Shm(region) => region.unlink().map_err(Error::Io),
699        }
700    }
701
702    #[cfg(test)]
703    pub(crate) fn publish(
704        &self,
705        payload: &[u8],
706    ) -> Result<Publication> {
707        let mut publication = self.reserve(payload.len())?;
708        publication.as_mut_slice().copy_from_slice(payload);
709        publication.make_live();
710        Ok(publication)
711    }
712
713    pub(crate) fn reserve(
714        &self,
715        payload_len: usize,
716    ) -> Result<Publication> {
717        self.arena.reserve(payload_len)
718    }
719
720    pub(crate) fn read(
721        &self,
722        descriptor: ChunkDescriptor,
723    ) -> Result<PayloadChunk> {
724        if descriptor.arena_kind() != self.kind() {
725            return Err(Error::Malformed(format!(
726                "chunk names payload arena kind {}, opened kind is {}",
727                descriptor.arena_kind(),
728                self.kind()
729            )));
730        }
731        let owner = usize::from(descriptor.owner_node());
732        let first_local = descriptor.first_slot() as usize;
733        let count = descriptor.slot_count() as usize;
734        let len = descriptor.payload_len() as usize;
735        if owner
736            >= self.arena.geometry.total_slots
737                / self.arena.geometry.slots_per_node
738            || count == 0
739            || first_local + count > self.arena.geometry.slots_per_node
740            || len == 0
741            || len > count * self.arena.geometry.slot_size
742            || (count > 1 && len <= (count - 1) * self.arena.geometry.slot_size)
743        {
744            return Err(Error::Malformed(
745                "chunk descriptor is outside its payload lane".to_owned(),
746            ));
747        }
748        let first = owner * self.arena.geometry.slots_per_node + first_local;
749        let slots = &self.arena.metadata()[first..first + count];
750        let generation = descriptor.allocation_generation();
751        if slots
752            .iter()
753            .any(|slot| slot.generation.load(Ordering::Acquire) != generation)
754            || slots
755                .iter()
756                .skip(1)
757                .any(|slot| slot.state.load(Ordering::Acquire) != SLOT_RESERVED)
758            || slots[0]
759                .state
760                .compare_exchange(
761                    SLOT_LIVE,
762                    SLOT_READING,
763                    Ordering::Acquire,
764                    Ordering::Acquire,
765                )
766                .is_err()
767        {
768            return Err(Error::Malformed(
769                "chunk allocation is stale or already consumed".to_owned(),
770            ));
771        }
772        slots[0]
773            .reader_incarnation
774            .store(self.arena.incarnation.get(), Ordering::Release);
775        slots[0].reader_node.store(self.arena.node, Ordering::Release);
776        Ok(PayloadChunk {
777            arena: Arc::clone(&self.arena),
778            first,
779            count,
780            len,
781            generation,
782            descriptor,
783        })
784    }
785}
786
787pub(crate) struct Publication {
788    arena: Arc<Arena>,
789    first: usize,
790    count: usize,
791    payload_len: usize,
792    generation: u64,
793    published: bool,
794}
795
796impl Publication {
797    pub(crate) fn as_slice(&self) -> &[u8] {
798        // SAFETY: this publication owns a contiguous reserved extent and no
799        // consumer can observe it before the control descriptor is sent.
800        unsafe {
801            std::slice::from_raw_parts(
802                self.arena.payload(self.first),
803                self.payload_len,
804            )
805        }
806    }
807
808    pub(crate) fn as_mut_slice(&mut self) -> &mut [u8] {
809        // SAFETY: this publication exclusively owns a contiguous reserved
810        // extent until it is made live and its control descriptor is sent.
811        unsafe {
812            std::slice::from_raw_parts_mut(
813                self.arena.payload(self.first),
814                self.payload_len,
815            )
816        }
817    }
818
819    pub(crate) fn coordinates(&self) -> (u64, u32, u32, u32, u8, u16) {
820        let first_local = self.first % self.arena.geometry.slots_per_node;
821        (
822            self.generation,
823            first_local as u32,
824            self.count as u32,
825            self.payload_len as u32,
826            self.arena.kind,
827            self.arena.node,
828        )
829    }
830
831    pub(crate) fn truncate(
832        &mut self,
833        payload_len: usize,
834    ) -> Result<()> {
835        if payload_len == 0 || payload_len > self.payload_len {
836            return Err(Error::Malformed(format!(
837                "committed payload length {payload_len} is outside reserved capacity {}",
838                self.payload_len
839            )));
840        }
841        let count = self.arena.required_slots(payload_len)?;
842        if count < self.count {
843            for slot in &self.arena.metadata()
844                [self.first + count..self.first + self.count]
845            {
846                slot.state.store(SLOT_FREE, Ordering::Release);
847            }
848            self.count = count;
849            let credit = self.arena.local_credit();
850            credit.generation.fetch_add(1, Ordering::SeqCst);
851            if credit.waiters.load(Ordering::SeqCst) > 0 {
852                crate::wake_on(&credit.generation);
853            }
854        }
855        self.payload_len = payload_len;
856        Ok(())
857    }
858
859    pub(crate) fn make_live(&self) {
860        self.arena.metadata()[self.first]
861            .state
862            .store(SLOT_LIVE, Ordering::Release);
863    }
864
865    pub(crate) fn mark_published(mut self) {
866        self.published = true;
867    }
868}
869
870impl Drop for Publication {
871    fn drop(&mut self) {
872        if !self.published {
873            self.arena.release(self.first, self.count, self.generation);
874        }
875    }
876}
877
878/// An immutable chunk borrowed from a payload arena. Dropping the guard is
879/// the consumer's credit return to its producer node's lane.
880pub struct PayloadChunk {
881    arena: Arc<Arena>,
882    first: usize,
883    count: usize,
884    len: usize,
885    generation: u64,
886    descriptor: ChunkDescriptor,
887}
888
889impl PayloadChunk {
890    pub const fn descriptor(&self) -> ChunkDescriptor {
891        self.descriptor
892    }
893
894    pub fn as_slice(&self) -> &[u8] {
895        // SAFETY: the allocation is immutable between publication and this
896        // guard's release, and `len` was validated against the run.
897        unsafe {
898            std::slice::from_raw_parts(self.arena.payload(self.first), self.len)
899        }
900    }
901}
902
903impl Deref for PayloadChunk {
904    type Target = [u8];
905
906    fn deref(&self) -> &Self::Target {
907        self.as_slice()
908    }
909}
910
911impl Drop for PayloadChunk {
912    fn drop(&mut self) {
913        self.arena.release(self.first, self.count, self.generation);
914    }
915}
916
917#[derive(Hash, Eq, PartialEq)]
918enum Key {
919    Memory(usize, u8),
920    #[cfg(unix)]
921    Shm(String, u16),
922}
923
924static ARENAS: LazyLock<Mutex<HashMap<Key, Weak<Arena>>>> =
925    LazyLock::new(|| Mutex::new(HashMap::new()));
926
927fn open(
928    fleet: &Arc<Fleet>,
929    incarnation: Incarnation,
930    spec: PayloadArenaSpec,
931) -> Result<Arc<Arena>> {
932    if !crate::waits_supported() {
933        return Err(Error::Io(std::io::Error::new(
934            std::io::ErrorKind::Unsupported,
935            "payload arenas need a platform that can wait on a shared word",
936        )));
937    }
938    let key = if fleet.is_shm() {
939        #[cfg(unix)]
940        {
941            Key::Shm(
942                ring_segment_name(fleet.name(), spec.kind),
943                fleet.node_id().get(),
944            )
945        }
946        #[cfg(not(unix))]
947        unreachable!("non-Unix fleets cannot use POSIX SHM")
948    } else {
949        Key::Memory(Arc::as_ptr(fleet) as usize, spec.kind)
950    };
951    let mut arenas = lock_unpoisoned(&ARENAS);
952    arenas.retain(|_, arena| arena.strong_count() > 0);
953    if let Some(arena) = arenas.get(&key).and_then(Weak::upgrade) {
954        if arena.incarnation != incarnation {
955            return Err(Error::Malformed(format!(
956                "this process already opened payload kind {} as incarnation {}",
957                spec.kind,
958                arena.incarnation.get()
959            )));
960        }
961        if arena.geometry.slots_per_node != spec.slots_per_node
962            || arena.geometry.slot_size != spec.slot_size
963        {
964            return Err(Error::Malformed(format!(
965                "this process already opened payload kind {} with slots_per_node={} slot_size={}",
966                spec.kind,
967                arena.geometry.slots_per_node,
968                arena.geometry.slot_size
969            )));
970        }
971        return Ok(arena);
972    }
973
974    let geometry = Geometry::new(fleet.fleet_capacity(), spec);
975    let backing = match &key {
976        Key::Memory(..) => {
977            let bytes = AlignedBytes::zeroed(geometry.segment_size);
978            // SAFETY: fresh aligned memory is at least one header long.
979            unsafe {
980                std::ptr::write(
981                    bytes.ptr.cast::<Header>(),
982                    Header::new(fleet.fleet_capacity(), spec),
983                );
984            }
985            Backing::Memory(bytes)
986        }
987        #[cfg(unix)]
988        Key::Shm(name, _) => Backing::Shm(open_shm(
989            name,
990            fleet.fleet_capacity(),
991            spec,
992            geometry,
993        )?),
994    };
995    let arena = Arc::new(Arena {
996        _fleet: Arc::clone(fleet),
997        backing,
998        geometry,
999        kind: spec.kind,
1000        node: fleet.node_id().get(),
1001        incarnation,
1002        allocation: Mutex::new(0),
1003        credit_wakers: Mutex::new(Vec::new()),
1004        driver: Mutex::new(None),
1005    });
1006    arenas.insert(key, Arc::downgrade(&arena));
1007    Ok(arena)
1008}
1009
1010#[cfg(unix)]
1011fn open_shm(
1012    name: &str,
1013    fleet_capacity: u16,
1014    spec: PayloadArenaSpec,
1015    geometry: Geometry,
1016) -> Result<ShmRegion> {
1017    use std::io;
1018
1019    let (region, _initialization_lock) =
1020        ShmRegion::open_or_create_locked(name, geometry.segment_size)?;
1021    if region.created() {
1022        // SAFETY: a fresh mapping is aligned and at least one header long.
1023        unsafe {
1024            std::ptr::write(
1025                region.as_ptr().cast::<Header>(),
1026                Header::new(fleet_capacity, spec),
1027            );
1028        }
1029    } else {
1030        // SAFETY: the open verified the minimum segment size.
1031        let header = unsafe { &*region.as_ptr().cast::<Header>() };
1032        if header.magic != MAGIC {
1033            return Err(Error::Io(io::Error::new(
1034                io::ErrorKind::InvalidData,
1035                format!("SHM segment {name} has the wrong payload-arena magic"),
1036            )));
1037        }
1038        if !header.compatible(fleet_capacity, spec) {
1039            return Err(Error::Io(io::Error::new(
1040                io::ErrorKind::InvalidData,
1041                format!(
1042                    "SHM segment {name} has an incompatible payload-arena layout"
1043                ),
1044            )));
1045        }
1046    }
1047    Ok(region)
1048}
1049
1050pub fn segment_size_for(
1051    fleet_capacity: u16,
1052    spec: PayloadArenaSpec,
1053) -> usize {
1054    Geometry::new(fleet_capacity, spec).segment_size
1055}
1056
1057#[cfg(test)]
1058mod tests {
1059    use super::*;
1060    use crate::exchange::{ExchangeId, Flow};
1061
1062    fn fleet() -> Arc<Fleet> {
1063        Arc::new(Fleet::join("payload-arena-test", 2).expect("fleet"))
1064    }
1065
1066    fn descriptor(
1067        publication: &Publication,
1068        chunk_id: u64,
1069    ) -> ChunkDescriptor {
1070        let (generation, first, count, len, kind, owner) =
1071            publication.coordinates();
1072        ChunkDescriptor::new(
1073            ExchangeId::from_stream_id(crate::StreamId::from_net_id(
1074                orbit_core::NetId64::make(240, 0, 1),
1075            )),
1076            Flow::AtoB,
1077            chunk_id,
1078            generation,
1079            first,
1080            count,
1081            len,
1082            kind,
1083            owner,
1084        )
1085    }
1086
1087    #[test]
1088    fn one_chunk_spans_the_number_of_slots_its_payload_needs() {
1089        let arena = PayloadArena::open(
1090            fleet(),
1091            Incarnation::new(1),
1092            PayloadArenaSpec::new(241, 8, 256),
1093        )
1094        .expect("arena");
1095        let bytes = vec![0x5a; 777];
1096        let publication = arena.publish(&bytes).expect("publication");
1097        let descriptor = descriptor(&publication, 1);
1098        assert_eq!(descriptor.slot_count(), 4);
1099        publication.mark_published();
1100
1101        let chunk = arena.read(descriptor).expect("chunk");
1102        assert_eq!(&*chunk, bytes);
1103    }
1104
1105    #[test]
1106    fn dropping_a_chunk_returns_the_whole_extent_as_credit() {
1107        let arena = PayloadArena::open(
1108            fleet(),
1109            Incarnation::new(1),
1110            PayloadArenaSpec::new(242, 4, 64),
1111        )
1112        .expect("arena");
1113        let first = arena.publish(&[1; 256]).expect("fills lane");
1114        let descriptor = descriptor(&first, 1);
1115        first.mark_published();
1116        assert!(matches!(arena.publish(&[2]), Err(Error::PayloadFull { .. })));
1117        drop(arena.read(descriptor).expect("consume"));
1118        assert!(arena.publish(&[3]).is_ok());
1119    }
1120
1121    #[test]
1122    fn a_cancelled_publication_never_spends_credit() {
1123        let arena = PayloadArena::open(
1124            fleet(),
1125            Incarnation::new(1),
1126            PayloadArenaSpec::new(243, 2, 64),
1127        )
1128        .expect("arena");
1129        drop(arena.publish(&[1; 128]).expect("reserved publication"));
1130        assert!(arena.publish(&[2; 128]).is_ok());
1131    }
1132
1133    #[test]
1134    fn returned_credit_wakes_a_parked_producer() {
1135        let arena = PayloadArena::open(
1136            fleet(),
1137            Incarnation::new(1),
1138            PayloadArenaSpec::new(244, 2, 64),
1139        )
1140        .expect("arena");
1141        let publication = arena.publish(&[1; 128]).expect("fills lane");
1142        let descriptor = descriptor(&publication, 1);
1143        publication.mark_published();
1144        let chunk = arena.read(descriptor).expect("held chunk");
1145
1146        let waiting = arena.clone();
1147        let waiter = std::thread::spawn(move || waiting.wait_available(1));
1148        std::thread::sleep(std::time::Duration::from_millis(20));
1149        drop(chunk);
1150        waiter.join().expect("waiter").expect("credit wake");
1151    }
1152
1153    #[cfg(unix)]
1154    #[test]
1155    fn returned_credit_wakes_only_the_owning_producer_node() {
1156        let name: &'static str =
1157            Box::leak(format!("pc{:x}", std::process::id()).into_boxed_str());
1158        let spec = PayloadArenaSpec::new(248, 1, 64);
1159        let producer = PayloadArena::open(
1160            Arc::new(
1161                Fleet::join_shm_as(name, 2, orbit_core::NodeId::ZERO)
1162                    .expect("producer fleet"),
1163            ),
1164            Incarnation::new(1),
1165            spec,
1166        )
1167        .expect("producer arena");
1168        producer.reset_all();
1169        let consumer = PayloadArena::open(
1170            Arc::new(
1171                Fleet::join_shm_as(name, 2, orbit_core::NodeId::new(1))
1172                    .expect("consumer fleet"),
1173            ),
1174            Incarnation::new(2),
1175            spec,
1176        )
1177        .expect("consumer arena");
1178
1179        let publication =
1180            producer.publish(&[7; 64]).expect("fill producer lane");
1181        let descriptor = descriptor(&publication, 1);
1182        publication.mark_published();
1183        let chunk = consumer.read(descriptor).expect("consume producer chunk");
1184
1185        let waiting = producer.clone();
1186        let waiter = std::thread::spawn(move || waiting.wait_available(64));
1187        let deadline =
1188            std::time::Instant::now() + std::time::Duration::from_secs(1);
1189        while producer.arena.local_credit().waiters.load(Ordering::SeqCst) == 0
1190        {
1191            assert!(
1192                std::time::Instant::now() < deadline,
1193                "producer did not park"
1194            );
1195            std::thread::yield_now();
1196        }
1197        assert_eq!(
1198            consumer.arena.local_credit().waiters.load(Ordering::SeqCst),
1199            0,
1200            "an unrelated node must not share the producer's credit wait word",
1201        );
1202
1203        drop(chunk);
1204        waiter.join().expect("waiter thread").expect("producer wake");
1205        producer.unlink().expect("unlink payload arena");
1206    }
1207
1208    #[cfg(feature = "tokio")]
1209    #[tokio::test]
1210    async fn returned_credit_wakes_a_pending_task() {
1211        use std::future::poll_fn;
1212
1213        let arena = PayloadArena::open(
1214            fleet(),
1215            Incarnation::new(1),
1216            PayloadArenaSpec::new(247, 2, 64),
1217        )
1218        .expect("arena");
1219        let publication = arena.publish(&[1; 128]).expect("fills lane");
1220        let descriptor = descriptor(&publication, 1);
1221        publication.mark_published();
1222        let chunk = arena.read(descriptor).expect("held chunk");
1223
1224        let waiting = arena.clone();
1225        let waiter = tokio::spawn(async move {
1226            poll_fn(|cx| waiting.poll_available(1, cx)).await
1227        });
1228        tokio::task::yield_now().await;
1229        drop(chunk);
1230        tokio::time::timeout(std::time::Duration::from_secs(2), waiter)
1231            .await
1232            .expect("credit task stayed pending")
1233            .expect("credit task")
1234            .expect("credit wake");
1235    }
1236
1237    #[test]
1238    fn death_reclaims_only_unpublished_or_held_allocations() {
1239        let arena = PayloadArena::open(
1240            fleet(),
1241            Incarnation::new(7),
1242            PayloadArenaSpec::new(245, 2, 64),
1243        )
1244        .expect("arena");
1245
1246        let abandoned = arena.arena.reserve(128).expect("reserved by producer");
1247        arena.node_dead(orbit_core::NodeId::ZERO, Incarnation::new(7));
1248        let replacement =
1249            arena.publish(&[2; 128]).expect("producer credit reclaimed");
1250        drop(abandoned);
1251        let replacement_descriptor = descriptor(&replacement, 2);
1252        replacement.mark_published();
1253        let held = arena.read(replacement_descriptor).expect("held by reader");
1254
1255        arena.node_dead(orbit_core::NodeId::ZERO, Incarnation::new(6));
1256        assert!(matches!(arena.publish(&[3]), Err(Error::PayloadFull { .. })));
1257        arena.node_dead(orbit_core::NodeId::ZERO, Incarnation::new(7));
1258        let after_reader_death =
1259            arena.publish(&[4; 128]).expect("reader credit reclaimed");
1260        drop(held);
1261        let after_descriptor = descriptor(&after_reader_death, 3);
1262        after_reader_death.mark_published();
1263        assert!(arena.read(after_descriptor).is_ok());
1264    }
1265
1266    #[test]
1267    fn committed_unread_payload_is_reclaimed_with_its_dead_producer() {
1268        let arena = PayloadArena::open(
1269            fleet(),
1270            Incarnation::new(8),
1271            PayloadArenaSpec::new(246, 2, 64),
1272        )
1273        .expect("arena");
1274        let publication = arena.publish(b"survives").expect("publication");
1275        let descriptor = descriptor(&publication, 1);
1276        publication.mark_published();
1277
1278        arena.node_dead(orbit_core::NodeId::ZERO, Incarnation::new(8));
1279        assert!(arena.read(descriptor).is_err());
1280        assert!(arena.publish(&[9; 128]).is_ok());
1281    }
1282}