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; const 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#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
39pub struct PayloadArenaSpec {
40 pub kind: u8,
41 pub slots_per_node: usize,
43 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 pub const fn chunk_geometry(self) -> Option<ChunkGeometry> {
70 ChunkGeometry::new(self.slot_size, self.slots_per_node)
71 }
72
73 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 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 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 unsafe { std::alloc::dealloc(self.ptr, self.layout) }
252 }
253}
254
255unsafe impl Send for AlignedBytes {}
257unsafe impl Sync for AlignedBytes {}
258
259pub(crate) type FlowKey = (u64, u16);
261
262pub(crate) struct FlowWaitSpec<'a> {
264 pub(crate) key: FlowKey,
265 pub(crate) status: &'a crate::WriteStatus,
266 pub(crate) demand: usize,
268 pub(crate) limit: usize,
270 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
293pub(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 unsafe { &*self.base().cast::<Header>() }
347 }
348
349 fn metadata(&self) -> &[SlotMeta] {
350 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 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 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 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 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 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 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 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 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 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 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 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#[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 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 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 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 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 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 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 #[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 pub(crate) fn forget_flow(
1107 &self,
1108 key: FlowKey,
1109 ) {
1110 self.arena.forget_flow(key);
1111 }
1112
1113 #[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 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 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 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
1238pub 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 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 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 unsafe {
1395 std::ptr::write(
1396 region.as_ptr().cast::<Header>(),
1397 Header::new(fleet_capacity, spec),
1398 );
1399 }
1400 } else {
1401 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}