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