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