Skip to main content

orbit_stream/exchange/
arena.rs

1use std::collections::{HashMap, HashSet};
2use std::mem::size_of;
3use std::ops::Deref;
4use std::sync::atomic::{AtomicU16, 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::{
13    ChunkDescriptor, ChunkGeometry, ChunkPlan, DEFAULT_CHUNK_BYTES, ExchangeId,
14    Flow,
15};
16use crate::wake::{Doorstep, Driver};
17use crate::{Error, Incarnation, Result, lock_unpoisoned};
18
19const MAGIC: u32 = 0x4f_50_41_59; // "OPAY"
20const VERSION: u16 = 5;
21
22const SLOT_FREE: u64 = 0;
23const SLOT_RESERVED: u64 = 1;
24const SLOT_LIVE: u64 = 2;
25const SLOT_READING: u64 = 3;
26const SLOT_RECLAIMING: u64 = 4;
27const STATE_BITS: u32 = 3;
28const STATE_MASK: u64 = (1 << STATE_BITS) - 1;
29
30fn tagged(
31    generation: u64,
32    state: u64,
33) -> u64 {
34    (generation << STATE_BITS) | state
35}
36
37/// Geometry and SHM identity of one fleet-wide payload arena.
38#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
39pub struct PayloadArenaSpec {
40    pub kind: u8,
41    /// Physical allocation units available to each fleet node.
42    pub slots_per_node: usize,
43    /// Bytes in one physical allocation unit.
44    pub slot_size: usize,
45}
46
47impl PayloadArenaSpec {
48    pub const fn new(
49        kind: u8,
50        slots_per_node: usize,
51        slot_size: usize,
52    ) -> Self {
53        Self { kind, slots_per_node, slot_size }
54    }
55
56    pub const fn lane_bytes(self) -> usize {
57        self.slots_per_node * self.slot_size
58    }
59
60    pub const fn slots_for(
61        self,
62        payload_len: usize,
63    ) -> usize {
64        payload_len.div_ceil(self.slot_size)
65    }
66
67    /// Physical chunk geometry for this arena. Runtime policy chooses the
68    /// slot count independently for each request or response.
69    pub const fn chunk_geometry(self) -> Option<ChunkGeometry> {
70        ChunkGeometry::new(self.slot_size, self.slots_per_node)
71    }
72
73    /// Plan a known payload with the benchmark-backed default chunk target,
74    /// clamped so one chunk always fits in this node lane.
75    pub const fn plan_chunks(
76        self,
77        data_bytes: usize,
78    ) -> Option<ChunkPlan> {
79        let lane_bytes = self.lane_bytes();
80        let limit = if DEFAULT_CHUNK_BYTES < lane_bytes {
81            DEFAULT_CHUNK_BYTES
82        } else {
83            lane_bytes
84        };
85        ChunkPlan::for_limit(data_bytes, self.slot_size, limit)
86    }
87
88    /// Plan using a runtime-selected number of slots per publication.
89    pub const fn plan_chunks_with(
90        self,
91        data_bytes: usize,
92        slots_per_chunk: usize,
93    ) -> Option<ChunkPlan> {
94        let geometry = match self.chunk_geometry() {
95            Some(geometry) => geometry,
96            None => return None,
97        };
98        geometry.plan(data_bytes, slots_per_chunk)
99    }
100
101    fn validate(self) -> Result<()> {
102        if self.slots_per_node == 0
103            || self.slot_size == 0
104            || !self.slots_per_node.is_power_of_two()
105            || !self.slot_size.is_power_of_two()
106            || self.slots_per_node > u32::MAX as usize
107            || self.slot_size > u32::MAX as usize
108        {
109            return Err(Error::Malformed(format!(
110                "payload arena kind={} slots_per_node={} slot_size={}: both geometry values must be non-zero powers of two fitting u32",
111                self.kind, self.slots_per_node, self.slot_size
112            )));
113        }
114        Ok(())
115    }
116}
117
118#[repr(C, align(64))]
119struct Header {
120    magic: u32,
121    version: u16,
122    header_size: u16,
123    slot_meta_size: u32,
124    slot_size: u32,
125    slots_per_node: u32,
126    fleet_capacity: u16,
127    _reserved: [u8; 2],
128    next_generation: AtomicU64,
129    epoch: AtomicU64,
130    _reserved_credit: [u8; 24],
131}
132
133impl Header {
134    fn new(
135        fleet_capacity: u16,
136        spec: PayloadArenaSpec,
137    ) -> Self {
138        Self {
139            magic: MAGIC,
140            version: VERSION,
141            header_size: size_of::<Self>() as u16,
142            slot_meta_size: size_of::<SlotMeta>() as u32,
143            slot_size: spec.slot_size as u32,
144            slots_per_node: spec.slots_per_node as u32,
145            fleet_capacity,
146            _reserved: [0; 2],
147            next_generation: AtomicU64::new(1),
148            epoch: AtomicU64::new(1),
149            _reserved_credit: [0; 24],
150        }
151    }
152
153    fn compatible(
154        &self,
155        fleet_capacity: u16,
156        spec: PayloadArenaSpec,
157    ) -> bool {
158        self.magic == MAGIC
159            && self.version == VERSION
160            && self.header_size as usize == size_of::<Self>()
161            && self.slot_meta_size as usize == size_of::<SlotMeta>()
162            && self.slot_size as usize == spec.slot_size
163            && self.slots_per_node as usize == spec.slots_per_node
164            && self.fleet_capacity == fleet_capacity
165    }
166}
167
168#[repr(C, align(64))]
169struct CreditWord {
170    generation: AtomicU32,
171    waiters: AtomicU32,
172    _padding: [u8; 56],
173}
174
175#[repr(C, align(8))]
176struct SlotMeta {
177    flow: AtomicU16,
178    reader_node: AtomicU16,
179    extent_slots: AtomicU32,
180    exchange: AtomicU64,
181    allocation: AtomicU64,
182    producer_incarnation: AtomicU64,
183    reader_incarnation: AtomicU64,
184}
185
186const _: () = assert!(size_of::<Header>() == 64);
187const _: () = assert!(size_of::<CreditWord>() == 64);
188const _: () = assert!(size_of::<SlotMeta>() == 40);
189
190#[derive(Clone, Copy)]
191struct Geometry {
192    slots_per_node: usize,
193    slot_size: usize,
194    total_slots: usize,
195    credits_offset: usize,
196    metadata_offset: usize,
197    payload_offset: usize,
198    segment_size: usize,
199}
200
201impl Geometry {
202    fn new(
203        fleet_capacity: u16,
204        spec: PayloadArenaSpec,
205    ) -> Self {
206        let total_slots = usize::from(fleet_capacity) * spec.slots_per_node;
207        let credits_offset = size_of::<Header>();
208        let metadata_offset = credits_offset
209            + usize::from(fleet_capacity) * size_of::<CreditWord>();
210        let payload_offset = (metadata_offset
211            + total_slots * size_of::<SlotMeta>())
212        .next_multiple_of(64);
213        let segment_size = payload_offset + total_slots * spec.slot_size;
214        Self {
215            slots_per_node: spec.slots_per_node,
216            slot_size: spec.slot_size,
217            total_slots,
218            credits_offset,
219            metadata_offset,
220            payload_offset,
221            segment_size,
222        }
223    }
224}
225
226enum Backing {
227    Memory(AlignedBytes),
228    #[cfg(unix)]
229    Shm(ShmRegion),
230}
231
232struct AlignedBytes {
233    ptr: *mut u8,
234    layout: std::alloc::Layout,
235}
236
237impl AlignedBytes {
238    fn zeroed(size: usize) -> Self {
239        let layout = std::alloc::Layout::from_size_align(size, 64)
240            .expect("payload arena layout");
241        // SAFETY: geometry always includes the non-empty header.
242        let ptr = unsafe { std::alloc::alloc_zeroed(layout) };
243        assert!(!ptr.is_null(), "payload arena allocation failed");
244        Self { ptr, layout }
245    }
246}
247
248impl Drop for AlignedBytes {
249    fn drop(&mut self) {
250        // SAFETY: allocated by `alloc_zeroed` with this exact layout.
251        unsafe { std::alloc::dealloc(self.ptr, self.layout) }
252    }
253}
254
255// SAFETY: mapped bytes are accessed through atomics and allocation ownership.
256unsafe impl Send for AlignedBytes {}
257unsafe impl Sync for AlignedBytes {}
258
259/// A sender's credit registration: its exchange's raw id and its flow.
260pub(crate) type FlowKey = (u64, u16);
261
262/// What a parked sender is waiting for, as it registers.
263pub(crate) struct FlowWaitSpec<'a> {
264    pub(crate) key: FlowKey,
265    pub(crate) status: &'a crate::WriteStatus,
266    /// Slots the next publication needs.
267    pub(crate) demand: usize,
268    /// The flow's slot allowance.
269    pub(crate) limit: usize,
270    /// The descriptors the flow has published and may still hold.
271    pub(crate) held: &'a [ChunkDescriptor],
272}
273
274struct FlowWait {
275    waker: Waker,
276    status: crate::WriteStatus,
277    demand: usize,
278    limit: usize,
279    held: Vec<ChunkDescriptor>,
280}
281
282struct CreatorWait {
283    waker: Waker,
284    demand: usize,
285}
286
287#[derive(Default)]
288struct CreditWaits {
289    flows: HashMap<FlowKey, FlowWait>,
290    creators: Vec<CreatorWait>,
291}
292
293/// Whether a flow that has published `held` may publish `count` more slots
294/// under `limit`; a descriptor its reader has released no longer counts. The
295/// one test for the sender and for the credit driver, so they cannot differ.
296pub(crate) fn allowance_admits(
297    held: &[ChunkDescriptor],
298    limit: usize,
299    count: usize,
300    retains: impl Fn(ChunkDescriptor) -> bool,
301) -> bool {
302    let published: usize =
303        held.iter().map(|descriptor| descriptor.slot_count() as usize).sum();
304    if published + count <= limit {
305        return true;
306    }
307    let Some(mut remaining) = limit.checked_sub(count) else {
308        return false;
309    };
310    for descriptor in held {
311        if retains(*descriptor) {
312            let Some(left) =
313                remaining.checked_sub(descriptor.slot_count() as usize)
314            else {
315                return false;
316            };
317            remaining = left;
318        }
319    }
320    true
321}
322
323struct Arena {
324    _fleet: Arc<Fleet>,
325    backing: Backing,
326    geometry: Geometry,
327    kind: u8,
328    node: u16,
329    incarnation: Incarnation,
330    allocation: Mutex<usize>,
331    credit_waits: Mutex<CreditWaits>,
332    driver: Mutex<Option<Driver>>,
333}
334
335impl Arena {
336    fn base(&self) -> *mut u8 {
337        match &self.backing {
338            Backing::Memory(bytes) => bytes.ptr,
339            #[cfg(unix)]
340            Backing::Shm(region) => region.as_ptr(),
341        }
342    }
343
344    fn header(&self) -> &Header {
345        // SAFETY: every backing begins with the initialized arena header.
346        unsafe { &*self.base().cast::<Header>() }
347    }
348
349    fn metadata(&self) -> &[SlotMeta] {
350        // SAFETY: geometry reserves this aligned range for atomic metadata.
351        unsafe {
352            std::slice::from_raw_parts(
353                self.base()
354                    .add(self.geometry.metadata_offset)
355                    .cast::<SlotMeta>(),
356                self.geometry.total_slots,
357            )
358        }
359    }
360
361    fn credits(&self) -> &[CreditWord] {
362        // SAFETY: geometry reserves one cache-line-aligned word per fleet node.
363        unsafe {
364            std::slice::from_raw_parts(
365                self.base()
366                    .add(self.geometry.credits_offset)
367                    .cast::<CreditWord>(),
368                self.geometry.total_slots / self.geometry.slots_per_node,
369            )
370        }
371    }
372
373    fn local_credit(&self) -> &CreditWord {
374        &self.credits()[usize::from(self.node)]
375    }
376
377    fn payload(
378        &self,
379        absolute_slot: usize,
380    ) -> *mut u8 {
381        // SAFETY: callers validate the slot against `total_slots`.
382        unsafe {
383            self.base().add(
384                self.geometry.payload_offset
385                    + absolute_slot * self.geometry.slot_size,
386            )
387        }
388    }
389
390    fn reserve(
391        self: &Arc<Self>,
392        payload_len: usize,
393    ) -> Result<Publication> {
394        let count = self.required_slots(payload_len)?;
395
396        let mut hint = lock_unpoisoned(&self.allocation);
397        let lane_start = usize::from(self.node) * self.geometry.slots_per_node;
398        let candidate = (0..self.geometry.slots_per_node)
399            .map(|offset| (*hint + offset) & (self.geometry.slots_per_node - 1))
400            .find(|&local| {
401                local + count <= self.geometry.slots_per_node
402                    && self.metadata()
403                        [lane_start + local..lane_start + local + count]
404                        .iter()
405                        .all(|slot| {
406                            slot.allocation.load(Ordering::Acquire) & STATE_MASK
407                                == SLOT_FREE
408                        })
409            })
410            .ok_or(Error::PayloadFull { requested_slots: count })?;
411        let first = lane_start + candidate;
412        let generation =
413            self.header().next_generation.fetch_add(1, Ordering::Relaxed);
414        if generation > (u64::MAX >> STATE_BITS) {
415            return Err(Error::Malformed(
416                "payload allocation generations exhausted".to_owned(),
417            ));
418        }
419        let slots = &self.metadata()[first..first + count];
420        // Publish the extent head first so a dead producer's partial
421        // reservation can be recovered. Only an exact generation can free it.
422        for (index, slot) in slots.iter().enumerate() {
423            slot.extent_slots.store(
424                if index == 0 { count as u32 } else { 0 },
425                Ordering::Relaxed,
426            );
427            slot.exchange.store(0, Ordering::Relaxed);
428            slot.flow.store(0, Ordering::Relaxed);
429            slot.producer_incarnation
430                .store(self.incarnation.get(), Ordering::Relaxed);
431            slot.reader_node.store(u16::MAX, Ordering::Relaxed);
432            slot.reader_incarnation.store(0, Ordering::Relaxed);
433            slot.allocation
434                .store(tagged(generation, SLOT_RESERVED), Ordering::Release);
435        }
436        *hint = (candidate + count) & (self.geometry.slots_per_node - 1);
437        Ok(Publication {
438            arena: Arc::clone(self),
439            first,
440            count,
441            payload_len,
442            generation,
443            published: false,
444        })
445    }
446
447    fn release(
448        &self,
449        first: usize,
450        count: usize,
451        generation: u64,
452        state: u64,
453    ) {
454        if self.claim_reclaim(first, generation, state) {
455            self.free(first, count, generation);
456        }
457    }
458
459    fn claim_reclaim(
460        &self,
461        first: usize,
462        generation: u64,
463        state: u64,
464    ) -> bool {
465        self.metadata()[first]
466            .allocation
467            .compare_exchange(
468                tagged(generation, state),
469                tagged(generation, SLOT_RECLAIMING),
470                Ordering::AcqRel,
471                Ordering::Acquire,
472            )
473            .is_ok()
474    }
475
476    fn free(
477        &self,
478        first: usize,
479        count: usize,
480        generation: u64,
481    ) {
482        // A recovery pass may finish a release interrupted by process death.
483        // Exact generation/state CASes also make overlapping releases safe.
484        let slots = &self.metadata()[first..first + count];
485        for slot in &slots[1..] {
486            let _ = slot.allocation.compare_exchange(
487                tagged(generation, SLOT_RESERVED),
488                tagged(generation, SLOT_FREE),
489                Ordering::AcqRel,
490                Ordering::Acquire,
491            );
492        }
493        if slots[0]
494            .allocation
495            .compare_exchange(
496                tagged(generation, SLOT_RECLAIMING),
497                tagged(generation, SLOT_FREE),
498                Ordering::AcqRel,
499                Ordering::Acquire,
500            )
501            .is_ok()
502        {
503            self.notify_credit(first / self.geometry.slots_per_node);
504        }
505    }
506
507    fn notify_credit(
508        &self,
509        node: usize,
510    ) {
511        let credit = &self.credits()[node];
512        credit.generation.fetch_add(1, Ordering::SeqCst);
513        if credit.waiters.load(Ordering::SeqCst) > 0 {
514            crate::wake_on(&credit.generation);
515        }
516    }
517
518    fn required_slots(
519        &self,
520        payload_len: usize,
521    ) -> Result<usize> {
522        if payload_len == 0 {
523            return Err(Error::Malformed(
524                "an empty payload needs no arena allocation".to_owned(),
525            ));
526        }
527        let count = payload_len.div_ceil(self.geometry.slot_size);
528        if count > self.geometry.slots_per_node {
529            return Err(Error::PayloadTooLarge {
530                len: payload_len,
531                capacity: self.geometry.slots_per_node
532                    * self.geometry.slot_size,
533            });
534        }
535        Ok(count)
536    }
537
538    fn has_run(
539        &self,
540        count: usize,
541    ) -> bool {
542        let lane_start = usize::from(self.node) * self.geometry.slots_per_node;
543        (0..=self.geometry.slots_per_node - count).any(|local| {
544            self.metadata()[lane_start + local..lane_start + local + count]
545                .iter()
546                .all(|slot| {
547                    slot.allocation.load(Ordering::Acquire) & STATE_MASK
548                        == SLOT_FREE
549                })
550        })
551    }
552
553    fn wait_available(
554        &self,
555        count: usize,
556    ) -> Result<()> {
557        self.wait_credit(|| Ok(self.has_run(count)))
558    }
559
560    fn wait_credit(
561        &self,
562        ready: impl Fn() -> Result<bool>,
563    ) -> Result<()> {
564        let credit = self.local_credit();
565        loop {
566            if ready()? {
567                return Ok(());
568            }
569            credit.waiters.fetch_add(1, Ordering::SeqCst);
570            let seen = credit.generation.load(Ordering::SeqCst);
571            let outcome = ready().and_then(|ready| {
572                if ready {
573                    Ok(())
574                } else {
575                    crate::wait_on(&credit.generation, seen)
576                }
577            });
578            credit.waiters.fetch_sub(1, Ordering::SeqCst);
579            outcome?;
580        }
581    }
582
583    fn poll_available(
584        self: &Arc<Self>,
585        count: usize,
586        cx: &mut Context<'_>,
587    ) -> Poll<Result<()>> {
588        if self.has_run(count) {
589            return Poll::Ready(Ok(()));
590        }
591        {
592            let mut waits = lock_unpoisoned(&self.credit_waits);
593            match waits
594                .creators
595                .iter_mut()
596                .find(|wait| wait.waker.will_wake(cx.waker()))
597            {
598                Some(wait) => wait.demand = count,
599                None => waits.creators.push(CreatorWait {
600                    waker: cx.waker().clone(),
601                    demand: count,
602                }),
603            }
604        }
605        self.ensure_driver()?;
606        if self.has_run(count) { Poll::Ready(Ok(())) } else { Poll::Pending }
607    }
608
609    /// Register a flow's sender for credit, keyed by its exchange and flow, so
610    /// a driver pass wakes it only when it may proceed. Check, register,
611    /// check again: a return between the two checks is seen by one of them.
612    fn poll_flow(
613        self: &Arc<Self>,
614        wait: FlowWaitSpec<'_>,
615        ready: impl Fn() -> Result<bool>,
616        cx: &mut Context<'_>,
617    ) -> Poll<Result<()>> {
618        if ready()? {
619            return Poll::Ready(Ok(()));
620        }
621        {
622            let mut waits = lock_unpoisoned(&self.credit_waits);
623            let record =
624                waits.flows.entry(wait.key).or_insert_with(|| FlowWait {
625                    waker: cx.waker().clone(),
626                    status: wait.status.clone(),
627                    demand: 0,
628                    limit: 0,
629                    held: Vec::new(),
630                });
631            if !record.waker.will_wake(cx.waker()) {
632                record.waker = cx.waker().clone();
633            }
634            record.demand = wait.demand;
635            record.limit = wait.limit;
636            record.held.clear();
637            record.held.extend_from_slice(wait.held);
638        }
639        self.ensure_driver()?;
640        if ready()? {
641            self.forget_flow(wait.key);
642            Poll::Ready(Ok(()))
643        } else {
644            Poll::Pending
645        }
646    }
647
648    fn forget_flow(
649        &self,
650        key: FlowKey,
651    ) {
652        lock_unpoisoned(&self.credit_waits).flows.remove(&key);
653    }
654
655    /// Whether a parked flow may proceed now, by the sender's own test.
656    /// `longest` is this pass's longest free run, read once when first needed.
657    fn flow_may_proceed(
658        &self,
659        wait: &FlowWait,
660        longest: &mut Option<usize>,
661    ) -> bool {
662        if wait.status.is_ended() {
663            return true;
664        }
665        allowance_admits(&wait.held, wait.limit, wait.demand, |descriptor| {
666            self.holds(descriptor)
667        }) && *longest.get_or_insert_with(|| self.longest_free_run())
668            >= wait.demand
669    }
670
671    /// The longest run of free slots in this node's lane: one pass over it,
672    /// where testing each waiter's demand would scan it once per waiter.
673    fn longest_free_run(&self) -> usize {
674        let lane_start = usize::from(self.node) * self.geometry.slots_per_node;
675        let mut longest = 0;
676        let mut run = 0;
677        for slot in &self.metadata()
678            [lane_start..lane_start + self.geometry.slots_per_node]
679        {
680            if slot.allocation.load(Ordering::Acquire) & STATE_MASK == SLOT_FREE
681            {
682                run += 1;
683                longest = longest.max(run);
684            } else {
685                run = 0;
686            }
687        }
688        longest
689    }
690
691    /// Whether `descriptor`'s extent is still allocated to that publication.
692    fn holds(
693        &self,
694        descriptor: ChunkDescriptor,
695    ) -> bool {
696        let first = usize::from(descriptor.owner_node())
697            * self.geometry.slots_per_node
698            + descriptor.first_slot() as usize;
699        let allocation =
700            self.metadata()[first].allocation.load(Ordering::Acquire);
701        allocation >> STATE_BITS == descriptor.allocation_generation()
702            && allocation & STATE_MASK != SLOT_FREE
703    }
704
705    fn ensure_driver(self: &Arc<Self>) -> Result<()> {
706        let mut driver = lock_unpoisoned(&self.driver);
707        if driver.is_none() {
708            *driver = Some(Driver::start(
709                Arc::as_ptr(self),
710                format!("orbit-payload-{}-{}-driver", self.kind, self.node),
711            )?);
712        }
713        Ok(())
714    }
715
716    fn node_dead(
717        &self,
718        node: u16,
719        incarnation: Incarnation,
720        dead_flows: &HashSet<(u64, u16)>,
721    ) {
722        let metadata = self.metadata();
723        for (first, slot) in metadata.iter().enumerate() {
724            let allocation = slot.allocation.load(Ordering::Acquire);
725            let state = allocation & STATE_MASK;
726            let generation = allocation >> STATE_BITS;
727            let producer_node = first / self.geometry.slots_per_node;
728            let count = slot.extent_slots.load(Ordering::Acquire) as usize;
729            if count == 0
730                || count
731                    > self.geometry.slots_per_node
732                        - first % self.geometry.slots_per_node
733            {
734                continue;
735            }
736            let abandoned_publication =
737                matches!(state, SLOT_RESERVED | SLOT_LIVE)
738                    && producer_node == usize::from(node)
739                    && slot.producer_incarnation.load(Ordering::Acquire)
740                        == incarnation.get();
741            let abandoned_read = state == SLOT_READING
742                && slot.reader_node.load(Ordering::Acquire) == node
743                && slot.reader_incarnation.load(Ordering::Acquire)
744                    == incarnation.get();
745            // A descriptor may still be queued, or the reader may have
746            // died between claiming it and recording its exact incarnation.
747            let lost_reader = (state == SLOT_LIVE
748                || (state == SLOT_READING
749                    && slot.reader_node.load(Ordering::Acquire) == u16::MAX))
750                && dead_flows.contains(&(
751                    slot.exchange.load(Ordering::Acquire),
752                    slot.flow.load(Ordering::Acquire),
753                ));
754            if state == SLOT_RECLAIMING
755                || ((abandoned_publication || abandoned_read || lost_reader)
756                    && self.claim_reclaim(first, generation, state))
757            {
758                self.free(first, count, generation);
759            }
760        }
761        // A writer can be parked on credit held by another exchange in its
762        // lane when this peer dies. It must still observe the control close.
763        for node in 0..self.credits().len() {
764            self.notify_credit(node);
765        }
766    }
767}
768
769impl Doorstep for Arena {
770    fn generation(&self) -> &AtomicU32 {
771        &self.local_credit().generation
772    }
773
774    fn driver_waiters(&self) -> &AtomicU32 {
775        &self.local_credit().waiters
776    }
777
778    /// Wake the parked senders and creators that may proceed; the others
779    /// stay registered for the next credit change. Woken outside the lock.
780    fn drain(&self) {
781        let mut ready = Vec::new();
782        {
783            let mut waits = lock_unpoisoned(&self.credit_waits);
784            let mut longest = None;
785            ready.extend(
786                waits
787                    .flows
788                    .extract_if(|_, wait| {
789                        self.flow_may_proceed(wait, &mut longest)
790                    })
791                    .map(|(_, wait)| wait.waker),
792            );
793            if !waits.creators.is_empty() {
794                let longest =
795                    *longest.get_or_insert_with(|| self.longest_free_run());
796                ready.extend(
797                    waits
798                        .creators
799                        .extract_if(.., |wait| longest >= wait.demand)
800                        .map(|wait| wait.waker),
801                );
802            }
803        }
804        for waker in ready {
805            waker.wake();
806        }
807    }
808}
809
810impl Drop for Arena {
811    fn drop(&mut self) {
812        if let Some(mut driver) = self
813            .driver
814            .get_mut()
815            .unwrap_or_else(|error| error.into_inner())
816            .take()
817        {
818            driver.stop(&self.local_credit().generation);
819        }
820    }
821}
822
823/// One fleet-wide payload allocation table. Every producer allocates from its
824/// node's exclusive lane, independent of which exchange side it currently holds.
825#[derive(Clone)]
826pub struct PayloadArena {
827    arena: Arc<Arena>,
828}
829
830impl PayloadArena {
831    pub fn open(
832        fleet: Arc<Fleet>,
833        incarnation: Incarnation,
834        spec: PayloadArenaSpec,
835    ) -> Result<Self> {
836        spec.validate()?;
837        Ok(Self { arena: open(&fleet, incarnation, spec)? })
838    }
839
840    pub fn kind(&self) -> u8 {
841        self.arena.kind
842    }
843
844    pub fn slot_size(&self) -> usize {
845        self.arena.geometry.slot_size
846    }
847
848    pub fn slots_per_node(&self) -> usize {
849        self.arena.geometry.slots_per_node
850    }
851
852    pub fn chunk_geometry(&self) -> ChunkGeometry {
853        // PayloadArena::open validates both geometry values as non-zero.
854        ChunkGeometry::new(self.slot_size(), self.slots_per_node())
855            .expect("opened payload arena has valid chunk geometry")
856    }
857
858    pub fn plan_chunks(
859        &self,
860        data_bytes: usize,
861    ) -> Result<ChunkPlan> {
862        PayloadArenaSpec::new(
863            self.kind(),
864            self.slots_per_node(),
865            self.slot_size(),
866        )
867        .plan_chunks(data_bytes)
868        .ok_or_else(|| {
869            Error::Malformed(
870                "payload arena cannot plan zero-sized geometry".to_owned(),
871            )
872        })
873    }
874
875    pub fn plan_chunks_with(
876        &self,
877        data_bytes: usize,
878        slots_per_chunk: usize,
879    ) -> Result<ChunkPlan> {
880        self.chunk_geometry().plan(data_bytes, slots_per_chunk).ok_or_else(|| {
881            Error::Malformed(format!(
882                "chunk slot count {slots_per_chunk} exceeds payload lane capacity {}",
883                self.slots_per_node()
884            ))
885        })
886    }
887    /// Park this thread until a run large enough for `payload_len` may be
888    /// available. Allocation still decides the race after the wake.
889    pub fn wait_available(
890        &self,
891        payload_len: usize,
892    ) -> Result<()> {
893        let count = self.arena.required_slots(payload_len)?;
894        self.arena.wait_available(count)
895    }
896
897    /// Whether this producer lane currently has a contiguous run for the
898    /// payload. This is only a readiness snapshot; allocation still decides
899    /// a race between concurrent producers.
900    pub fn is_available(
901        &self,
902        payload_len: usize,
903    ) -> Result<bool> {
904        let count = self.arena.required_slots(payload_len)?;
905        Ok(self.arena.has_run(count))
906    }
907
908    /// Task readiness for this node's payload credit. Credit returns are
909    /// coalesced through one shared generation and one local driver.
910    pub fn poll_available(
911        &self,
912        payload_len: usize,
913        cx: &mut Context<'_>,
914    ) -> Poll<Result<()>> {
915        let count = match self.arena.required_slots(payload_len) {
916            Ok(count) => count,
917            Err(error) => return Poll::Ready(Err(error)),
918        };
919        self.arena.poll_available(count, cx)
920    }
921
922    /// Clear this arena only while the fleet is known quiescent. Normal
923    /// attach and replacement never call this implicitly.
924    pub fn reset_all(&self) {
925        let mut hint = lock_unpoisoned(&self.arena.allocation);
926        *hint = 0;
927        for slot in self.arena.metadata() {
928            slot.allocation.store(SLOT_FREE, Ordering::Release);
929        }
930        for credit in self.arena.credits() {
931            credit.generation.fetch_add(1, Ordering::SeqCst);
932            crate::wake_on(&credit.generation);
933        }
934    }
935
936    /// Reclaim only allocations owned by a process incarnation whose death
937    /// was confirmed by the embedder. A producer's unpublished or unread
938    /// chunks and a dead reader's held chunks return; a chunk already held by
939    /// a surviving consumer remains that consumer's.
940    pub fn node_dead(
941        &self,
942        node: orbit_core::NodeId,
943        incarnation: Incarnation,
944    ) {
945        self.node_dead_with_flows(node, incarnation, &HashSet::new());
946    }
947
948    pub(crate) fn node_dead_with_flows(
949        &self,
950        node: orbit_core::NodeId,
951        incarnation: Incarnation,
952        dead_flows: &HashSet<(u64, u16)>,
953    ) {
954        self.arena.node_dead(node.get(), incarnation, dead_flows);
955    }
956
957    /// Remove the arena's SHM name. Existing mappings remain valid.
958    #[cfg(unix)]
959    pub fn unlink(&self) -> Result<()> {
960        match &self.arena.backing {
961            Backing::Memory(_) => {
962                self.reset_all();
963                Ok(())
964            }
965            Backing::Shm(region) => region.unlink().map_err(Error::Io),
966        }
967    }
968
969    #[cfg(test)]
970    pub(crate) fn publish(
971        &self,
972        payload: &[u8],
973    ) -> Result<Publication> {
974        let mut publication = self.reserve(payload.len())?;
975        publication.as_mut_slice().copy_from_slice(payload);
976        publication.make_live();
977        Ok(publication)
978    }
979
980    pub(crate) fn reserve(
981        &self,
982        payload_len: usize,
983    ) -> Result<Publication> {
984        self.arena.reserve(payload_len)
985    }
986
987    pub(crate) fn read(
988        &self,
989        descriptor: ChunkDescriptor,
990    ) -> Result<PayloadChunk> {
991        let (first, count, generation) = self.extent(descriptor)?;
992        let len = descriptor.payload_len() as usize;
993        let slots = &self.arena.metadata()[first..first + count];
994        if slots[0].extent_slots.load(Ordering::Acquire) != count as u32
995            || slots[1..].iter().any(|slot| {
996                slot.allocation.load(Ordering::Acquire)
997                    != tagged(generation, SLOT_RESERVED)
998            })
999            || slots[0]
1000                .allocation
1001                .compare_exchange(
1002                    tagged(generation, SLOT_LIVE),
1003                    tagged(generation, SLOT_READING),
1004                    Ordering::AcqRel,
1005                    Ordering::Acquire,
1006                )
1007                .is_err()
1008        {
1009            return Err(Error::Malformed(
1010                "chunk allocation is stale or already consumed".to_owned(),
1011            ));
1012        }
1013        slots[0]
1014            .reader_incarnation
1015            .store(self.arena.incarnation.get(), Ordering::Release);
1016        slots[0].reader_node.store(self.arena.node, Ordering::Release);
1017        Ok(PayloadChunk {
1018            arena: Arc::clone(&self.arena),
1019            first,
1020            count,
1021            len,
1022            generation,
1023            descriptor,
1024        })
1025    }
1026
1027    fn extent(
1028        &self,
1029        descriptor: ChunkDescriptor,
1030    ) -> Result<(usize, usize, u64)> {
1031        if descriptor.arena_kind() != self.kind() {
1032            return Err(Error::Malformed(format!(
1033                "chunk names payload arena kind {}, opened kind is {}",
1034                descriptor.arena_kind(),
1035                self.kind()
1036            )));
1037        }
1038        let owner = usize::from(descriptor.owner_node());
1039        let first_local = descriptor.first_slot() as usize;
1040        let count = descriptor.slot_count() as usize;
1041        let len = descriptor.payload_len() as usize;
1042        let generation = descriptor.allocation_generation();
1043        if owner
1044            >= self.arena.geometry.total_slots
1045                / self.arena.geometry.slots_per_node
1046            || generation == 0
1047            || generation > (u64::MAX >> STATE_BITS)
1048            || count == 0
1049            || first_local + count > self.arena.geometry.slots_per_node
1050            || len == 0
1051            || len > count * self.arena.geometry.slot_size
1052            || (count > 1 && len <= (count - 1) * self.arena.geometry.slot_size)
1053        {
1054            return Err(Error::Malformed(
1055                "chunk descriptor is outside its payload lane".to_owned(),
1056            ));
1057        }
1058        Ok((owner * self.slots_per_node() + first_local, count, generation))
1059    }
1060
1061    pub(crate) fn discard(
1062        &self,
1063        descriptor: ChunkDescriptor,
1064    ) {
1065        if let Ok((first, count, generation)) = self.extent(descriptor)
1066            && self.arena.metadata()[first].extent_slots.load(Ordering::Acquire)
1067                == count as u32
1068        {
1069            self.arena.release(first, count, generation, SLOT_LIVE);
1070        }
1071    }
1072
1073    pub(crate) fn retains(
1074        &self,
1075        descriptor: ChunkDescriptor,
1076    ) -> bool {
1077        self.arena.holds(descriptor)
1078    }
1079
1080    pub(crate) fn notify_peer(
1081        &self,
1082        node: u16,
1083    ) {
1084        if usize::from(node) < self.arena.credits().len() {
1085            self.arena.notify_credit(usize::from(node));
1086        }
1087    }
1088
1089    pub(crate) fn wait_credit(
1090        &self,
1091        ready: impl Fn() -> Result<bool>,
1092    ) -> Result<()> {
1093        self.arena.wait_credit(ready)
1094    }
1095
1096    pub(crate) fn poll_flow(
1097        &self,
1098        wait: FlowWaitSpec<'_>,
1099        ready: impl Fn() -> Result<bool>,
1100        cx: &mut Context<'_>,
1101    ) -> Poll<Result<()>> {
1102        self.arena.poll_flow(wait, ready, cx)
1103    }
1104
1105    /// Drop a sender's credit registration, as its producer goes away.
1106    pub(crate) fn forget_flow(
1107        &self,
1108        key: FlowKey,
1109    ) {
1110        self.arena.forget_flow(key);
1111    }
1112
1113    /// Whether a sender's credit registration is still parked.
1114    #[cfg(test)]
1115    pub(crate) fn is_waiting(
1116        &self,
1117        key: FlowKey,
1118    ) -> bool {
1119        lock_unpoisoned(&self.arena.credit_waits).flows.contains_key(&key)
1120    }
1121}
1122
1123pub(crate) struct Publication {
1124    arena: Arc<Arena>,
1125    first: usize,
1126    count: usize,
1127    payload_len: usize,
1128    generation: u64,
1129    published: bool,
1130}
1131
1132impl Publication {
1133    pub(crate) fn as_slice(&self) -> &[u8] {
1134        // SAFETY: this publication owns a contiguous reserved extent and no
1135        // consumer can observe it before the control descriptor is sent.
1136        unsafe {
1137            std::slice::from_raw_parts(
1138                self.arena.payload(self.first),
1139                self.payload_len,
1140            )
1141        }
1142    }
1143
1144    pub(crate) fn as_mut_slice(&mut self) -> &mut [u8] {
1145        // SAFETY: this publication exclusively owns a contiguous reserved
1146        // extent until it is made live and its control descriptor is sent.
1147        unsafe {
1148            std::slice::from_raw_parts_mut(
1149                self.arena.payload(self.first),
1150                self.payload_len,
1151            )
1152        }
1153    }
1154
1155    pub(crate) fn coordinates(&self) -> (u64, u32, u32, u32, u8, u16) {
1156        let first_local = self.first % self.arena.geometry.slots_per_node;
1157        (
1158            self.generation,
1159            first_local as u32,
1160            self.count as u32,
1161            self.payload_len as u32,
1162            self.arena.kind,
1163            self.arena.node,
1164        )
1165    }
1166
1167    pub(crate) fn truncate(
1168        &mut self,
1169        payload_len: usize,
1170    ) -> Result<()> {
1171        if payload_len == 0 || payload_len > self.payload_len {
1172            return Err(Error::Malformed(format!(
1173                "committed payload length {payload_len} is outside reserved capacity {}",
1174                self.payload_len
1175            )));
1176        }
1177        let count = self.arena.required_slots(payload_len)?;
1178        if count < self.count {
1179            for slot in &self.arena.metadata()
1180                [self.first + count..self.first + self.count]
1181            {
1182                slot.allocation.store(
1183                    tagged(self.generation, SLOT_FREE),
1184                    Ordering::Release,
1185                );
1186            }
1187            self.arena.metadata()[self.first]
1188                .extent_slots
1189                .store(count as u32, Ordering::Release);
1190            self.count = count;
1191            self.arena.notify_credit(usize::from(self.arena.node));
1192        }
1193        self.payload_len = payload_len;
1194        Ok(())
1195    }
1196
1197    pub(crate) fn bind(
1198        &self,
1199        exchange: ExchangeId,
1200        flow: Flow,
1201    ) {
1202        let slot = &self.arena.metadata()[self.first];
1203        slot.exchange
1204            .store(exchange.stream_id().net_id().raw(), Ordering::Relaxed);
1205        slot.flow.store(u16::from(flow.wire()), Ordering::Relaxed);
1206    }
1207
1208    pub(crate) fn make_live(&self) {
1209        self.arena.metadata()[self.first]
1210            .allocation
1211            .store(tagged(self.generation, SLOT_LIVE), Ordering::Release);
1212    }
1213
1214    pub(crate) fn mark_published(mut self) {
1215        self.published = true;
1216    }
1217}
1218
1219impl Drop for Publication {
1220    fn drop(&mut self) {
1221        if !self.published {
1222            // A failed commit may already have marked the extent live.
1223            if self.arena.claim_reclaim(
1224                self.first,
1225                self.generation,
1226                SLOT_RESERVED,
1227            ) || self.arena.claim_reclaim(
1228                self.first,
1229                self.generation,
1230                SLOT_LIVE,
1231            ) {
1232                self.arena.free(self.first, self.count, self.generation);
1233            }
1234        }
1235    }
1236}
1237
1238/// An immutable chunk borrowed from a payload arena. Dropping the guard is
1239/// the consumer's credit return to its producer node's lane.
1240pub struct PayloadChunk {
1241    arena: Arc<Arena>,
1242    first: usize,
1243    count: usize,
1244    len: usize,
1245    generation: u64,
1246    descriptor: ChunkDescriptor,
1247}
1248
1249impl PayloadChunk {
1250    pub const fn descriptor(&self) -> ChunkDescriptor {
1251        self.descriptor
1252    }
1253
1254    pub fn as_slice(&self) -> &[u8] {
1255        // SAFETY: the allocation is immutable between publication and this
1256        // guard's release, and `len` was validated against the run.
1257        unsafe {
1258            std::slice::from_raw_parts(self.arena.payload(self.first), self.len)
1259        }
1260    }
1261}
1262
1263impl AsRef<[u8]> for PayloadChunk {
1264    fn as_ref(&self) -> &[u8] {
1265        self.as_slice()
1266    }
1267}
1268
1269impl Deref for PayloadChunk {
1270    type Target = [u8];
1271
1272    fn deref(&self) -> &Self::Target {
1273        self.as_slice()
1274    }
1275}
1276
1277impl Drop for PayloadChunk {
1278    fn drop(&mut self) {
1279        self.arena.release(
1280            self.first,
1281            self.count,
1282            self.generation,
1283            SLOT_READING,
1284        );
1285    }
1286}
1287
1288#[derive(Hash, Eq, PartialEq)]
1289enum Key {
1290    Memory(usize, u8),
1291    #[cfg(unix)]
1292    Shm(String, u16),
1293}
1294
1295static ARENAS: LazyLock<Mutex<HashMap<Key, Weak<Arena>>>> =
1296    LazyLock::new(|| Mutex::new(HashMap::new()));
1297
1298fn open(
1299    fleet: &Arc<Fleet>,
1300    incarnation: Incarnation,
1301    spec: PayloadArenaSpec,
1302) -> Result<Arc<Arena>> {
1303    if !crate::waits_supported() {
1304        return Err(Error::Io(std::io::Error::new(
1305            std::io::ErrorKind::Unsupported,
1306            "payload arenas need a platform that can wait on a shared word",
1307        )));
1308    }
1309    let key = if fleet.is_shm() {
1310        #[cfg(unix)]
1311        {
1312            Key::Shm(
1313                ring_segment_name(fleet.name(), spec.kind),
1314                fleet.node_id().get(),
1315            )
1316        }
1317        #[cfg(not(unix))]
1318        unreachable!("non-Unix fleets cannot use POSIX SHM")
1319    } else {
1320        Key::Memory(Arc::as_ptr(fleet) as usize, spec.kind)
1321    };
1322    let mut arenas = lock_unpoisoned(&ARENAS);
1323    arenas.retain(|_, arena| arena.strong_count() > 0);
1324    if let Some(arena) = arenas.get(&key).and_then(Weak::upgrade) {
1325        if arena.incarnation != incarnation {
1326            return Err(Error::Malformed(format!(
1327                "this process already opened payload kind {} as incarnation {}",
1328                spec.kind,
1329                arena.incarnation.get()
1330            )));
1331        }
1332        if arena.geometry.slots_per_node != spec.slots_per_node
1333            || arena.geometry.slot_size != spec.slot_size
1334        {
1335            return Err(Error::Malformed(format!(
1336                "this process already opened payload kind {} with slots_per_node={} slot_size={}",
1337                spec.kind,
1338                arena.geometry.slots_per_node,
1339                arena.geometry.slot_size
1340            )));
1341        }
1342        return Ok(arena);
1343    }
1344
1345    let geometry = Geometry::new(fleet.fleet_capacity(), spec);
1346    let backing = match &key {
1347        Key::Memory(..) => {
1348            let bytes = AlignedBytes::zeroed(geometry.segment_size);
1349            // SAFETY: fresh aligned memory is at least one header long.
1350            unsafe {
1351                std::ptr::write(
1352                    bytes.ptr.cast::<Header>(),
1353                    Header::new(fleet.fleet_capacity(), spec),
1354                );
1355            }
1356            Backing::Memory(bytes)
1357        }
1358        #[cfg(unix)]
1359        Key::Shm(name, _) => Backing::Shm(open_shm(
1360            name,
1361            fleet.fleet_capacity(),
1362            spec,
1363            geometry,
1364        )?),
1365    };
1366    let arena = Arc::new(Arena {
1367        _fleet: Arc::clone(fleet),
1368        backing,
1369        geometry,
1370        kind: spec.kind,
1371        node: fleet.node_id().get(),
1372        incarnation,
1373        allocation: Mutex::new(0),
1374        credit_waits: Mutex::new(CreditWaits::default()),
1375        driver: Mutex::new(None),
1376    });
1377    arenas.insert(key, Arc::downgrade(&arena));
1378    Ok(arena)
1379}
1380
1381#[cfg(unix)]
1382fn open_shm(
1383    name: &str,
1384    fleet_capacity: u16,
1385    spec: PayloadArenaSpec,
1386    geometry: Geometry,
1387) -> Result<ShmRegion> {
1388    use std::io;
1389
1390    let (region, _initialization_lock) =
1391        ShmRegion::open_or_create_locked(name, geometry.segment_size)?;
1392    if region.created() {
1393        // SAFETY: a fresh mapping is aligned and at least one header long.
1394        unsafe {
1395            std::ptr::write(
1396                region.as_ptr().cast::<Header>(),
1397                Header::new(fleet_capacity, spec),
1398            );
1399        }
1400    } else {
1401        // SAFETY: the open verified the minimum segment size.
1402        let header = unsafe { &*region.as_ptr().cast::<Header>() };
1403        if header.magic != MAGIC {
1404            return Err(Error::Io(io::Error::new(
1405                io::ErrorKind::InvalidData,
1406                format!("SHM segment {name} has the wrong payload-arena magic"),
1407            )));
1408        }
1409        if !header.compatible(fleet_capacity, spec) {
1410            return Err(Error::Io(io::Error::new(
1411                io::ErrorKind::InvalidData,
1412                format!(
1413                    "SHM segment {name} has an incompatible payload-arena layout"
1414                ),
1415            )));
1416        }
1417    }
1418    Ok(region)
1419}
1420
1421pub fn segment_size_for(
1422    fleet_capacity: u16,
1423    spec: PayloadArenaSpec,
1424) -> usize {
1425    Geometry::new(fleet_capacity, spec).segment_size
1426}
1427
1428#[cfg(test)]
1429mod tests {
1430    use super::*;
1431    use crate::exchange::{ExchangeId, Flow};
1432
1433    fn fleet() -> Arc<Fleet> {
1434        Arc::new(Fleet::join("payload-arena-test", 2).expect("fleet"))
1435    }
1436
1437    fn descriptor(
1438        publication: &Publication,
1439        chunk_id: u64,
1440    ) -> ChunkDescriptor {
1441        let (generation, first, count, len, kind, owner) =
1442            publication.coordinates();
1443        ChunkDescriptor::new(
1444            ExchangeId::from_stream_id(crate::StreamId::from_net_id(
1445                orbit_core::NetId64::make(240, 0, 1),
1446            )),
1447            Flow::AtoB,
1448            chunk_id,
1449            generation,
1450            first,
1451            count,
1452            len,
1453            kind,
1454            owner,
1455        )
1456    }
1457
1458    #[test]
1459    fn one_chunk_spans_the_number_of_slots_its_payload_needs() {
1460        let arena = PayloadArena::open(
1461            fleet(),
1462            Incarnation::new(1),
1463            PayloadArenaSpec::new(241, 8, 256),
1464        )
1465        .expect("arena");
1466        let bytes = vec![0x5a; 777];
1467        let publication = arena.publish(&bytes).expect("publication");
1468        let descriptor = descriptor(&publication, 1);
1469        assert_eq!(descriptor.slot_count(), 4);
1470        publication.mark_published();
1471
1472        let chunk = arena.read(descriptor).expect("chunk");
1473        assert_eq!(&*chunk, bytes);
1474    }
1475
1476    #[test]
1477    fn dropping_a_chunk_returns_the_whole_extent_as_credit() {
1478        let arena = PayloadArena::open(
1479            fleet(),
1480            Incarnation::new(1),
1481            PayloadArenaSpec::new(242, 4, 64),
1482        )
1483        .expect("arena");
1484        let first = arena.publish(&[1; 256]).expect("fills lane");
1485        let descriptor = descriptor(&first, 1);
1486        first.mark_published();
1487        assert!(matches!(arena.publish(&[2]), Err(Error::PayloadFull { .. })));
1488        drop(arena.read(descriptor).expect("consume"));
1489        assert!(arena.publish(&[3]).is_ok());
1490    }
1491
1492    #[test]
1493    fn a_cancelled_publication_never_spends_credit() {
1494        let arena = PayloadArena::open(
1495            fleet(),
1496            Incarnation::new(1),
1497            PayloadArenaSpec::new(243, 2, 64),
1498        )
1499        .expect("arena");
1500        drop(arena.publish(&[1; 128]).expect("reserved publication"));
1501        assert!(arena.publish(&[2; 128]).is_ok());
1502    }
1503
1504    #[test]
1505    fn returned_credit_wakes_a_parked_producer() {
1506        let arena = PayloadArena::open(
1507            fleet(),
1508            Incarnation::new(1),
1509            PayloadArenaSpec::new(244, 2, 64),
1510        )
1511        .expect("arena");
1512        let publication = arena.publish(&[1; 128]).expect("fills lane");
1513        let descriptor = descriptor(&publication, 1);
1514        publication.mark_published();
1515        let chunk = arena.read(descriptor).expect("held chunk");
1516
1517        let waiting = arena.clone();
1518        let waiter = std::thread::spawn(move || waiting.wait_available(1));
1519        std::thread::sleep(std::time::Duration::from_millis(20));
1520        drop(chunk);
1521        waiter.join().expect("waiter").expect("credit wake");
1522    }
1523
1524    #[cfg(unix)]
1525    #[test]
1526    fn returned_credit_wakes_only_the_owning_producer_node() {
1527        let name: &'static str =
1528            Box::leak(format!("pc{:x}", std::process::id()).into_boxed_str());
1529        let spec = PayloadArenaSpec::new(248, 1, 64);
1530        let producer = PayloadArena::open(
1531            Arc::new(
1532                Fleet::join_shm_as(name, 2, orbit_core::NodeId::ZERO)
1533                    .expect("producer fleet"),
1534            ),
1535            Incarnation::new(1),
1536            spec,
1537        )
1538        .expect("producer arena");
1539        producer.reset_all();
1540        let consumer = PayloadArena::open(
1541            Arc::new(
1542                Fleet::join_shm_as(name, 2, orbit_core::NodeId::new(1))
1543                    .expect("consumer fleet"),
1544            ),
1545            Incarnation::new(2),
1546            spec,
1547        )
1548        .expect("consumer arena");
1549
1550        let publication =
1551            producer.publish(&[7; 64]).expect("fill producer lane");
1552        let descriptor = descriptor(&publication, 1);
1553        publication.mark_published();
1554        let chunk = consumer.read(descriptor).expect("consume producer chunk");
1555
1556        let waiting = producer.clone();
1557        let waiter = std::thread::spawn(move || waiting.wait_available(64));
1558        let deadline =
1559            std::time::Instant::now() + std::time::Duration::from_secs(1);
1560        while producer.arena.local_credit().waiters.load(Ordering::SeqCst) == 0
1561        {
1562            assert!(
1563                std::time::Instant::now() < deadline,
1564                "producer did not park"
1565            );
1566            std::thread::yield_now();
1567        }
1568        assert_eq!(
1569            consumer.arena.local_credit().waiters.load(Ordering::SeqCst),
1570            0,
1571            "an unrelated node must not share the producer's credit wait word",
1572        );
1573
1574        drop(chunk);
1575        waiter.join().expect("waiter thread").expect("producer wake");
1576        producer.unlink().expect("unlink payload arena");
1577    }
1578
1579    #[cfg(feature = "tokio")]
1580    #[tokio::test]
1581    async fn returned_credit_wakes_a_pending_task() {
1582        use std::future::poll_fn;
1583
1584        let arena = PayloadArena::open(
1585            fleet(),
1586            Incarnation::new(1),
1587            PayloadArenaSpec::new(247, 2, 64),
1588        )
1589        .expect("arena");
1590        let publication = arena.publish(&[1; 128]).expect("fills lane");
1591        let descriptor = descriptor(&publication, 1);
1592        publication.mark_published();
1593        let chunk = arena.read(descriptor).expect("held chunk");
1594
1595        let waiting = arena.clone();
1596        let waiter = tokio::spawn(async move {
1597            poll_fn(|cx| waiting.poll_available(1, cx)).await
1598        });
1599        tokio::task::yield_now().await;
1600        drop(chunk);
1601        tokio::time::timeout(std::time::Duration::from_secs(2), waiter)
1602            .await
1603            .expect("credit task stayed pending")
1604            .expect("credit task")
1605            .expect("credit wake");
1606    }
1607
1608    #[test]
1609    fn death_reclaims_only_unpublished_or_held_allocations() {
1610        let arena = PayloadArena::open(
1611            fleet(),
1612            Incarnation::new(7),
1613            PayloadArenaSpec::new(245, 2, 64),
1614        )
1615        .expect("arena");
1616
1617        let abandoned = arena.arena.reserve(128).expect("reserved by producer");
1618        arena.node_dead(orbit_core::NodeId::ZERO, Incarnation::new(7));
1619        let replacement =
1620            arena.publish(&[2; 128]).expect("producer credit reclaimed");
1621        drop(abandoned);
1622        let replacement_descriptor = descriptor(&replacement, 2);
1623        replacement.mark_published();
1624        let held = arena.read(replacement_descriptor).expect("held by reader");
1625
1626        arena.node_dead(orbit_core::NodeId::ZERO, Incarnation::new(6));
1627        assert!(matches!(arena.publish(&[3]), Err(Error::PayloadFull { .. })));
1628        arena.node_dead(orbit_core::NodeId::ZERO, Incarnation::new(7));
1629        let after_reader_death =
1630            arena.publish(&[4; 128]).expect("reader credit reclaimed");
1631        drop(held);
1632        let after_descriptor = descriptor(&after_reader_death, 3);
1633        after_reader_death.mark_published();
1634        assert!(arena.read(after_descriptor).is_ok());
1635    }
1636
1637    #[test]
1638    fn committed_unread_payload_is_reclaimed_with_its_dead_producer() {
1639        let arena = PayloadArena::open(
1640            fleet(),
1641            Incarnation::new(8),
1642            PayloadArenaSpec::new(246, 2, 64),
1643        )
1644        .expect("arena");
1645        let publication = arena.publish(b"survives").expect("publication");
1646        let descriptor = descriptor(&publication, 1);
1647        publication.mark_published();
1648
1649        arena.node_dead(orbit_core::NodeId::ZERO, Incarnation::new(8));
1650        assert!(arena.read(descriptor).is_err());
1651        assert!(arena.publish(&[9; 128]).is_ok());
1652    }
1653}