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