1use std::collections::HashMap;
10use std::fmt;
11use std::str::FromStr;
12use std::sync::atomic::{AtomicU32, Ordering};
13use std::sync::{Arc, LazyLock, Mutex, Weak};
14
15use orbit_core::Fleet;
16#[cfg(unix)]
17use orbit_core::shm::{ShmRegion, ring_segment_name};
18
19use crate::lifetime::{self, Lease, Owners};
20use crate::{
21 Error, Result, SLOT_EMPTY, SLOT_OCCUPIED, SLOT_RETIRED, lock_unpoisoned,
22};
23
24mod slot;
25use slot::TextSlot;
26#[cfg(unix)]
27mod owners;
28#[cfg(test)]
29pub(crate) mod tests;
30
31pub const CELL_TEXT_KIND: u8 = 186;
33pub const CELL_TEXT_CAPACITY: usize = orbit_core::compile::usize_from_env(
37 option_env!("ORBIT_CELL_TEXT_CAPACITY"),
38 1_024,
39);
40pub const CELL_TEXT_MAX: usize = orbit_core::compile::usize_from_env(
45 option_env!("ORBIT_CELL_TEXT_MAX"),
46 240,
47);
48
49#[cfg(unix)]
50const TEXT_MAGIC: u32 = 0x43_54_58_54; #[cfg(unix)]
52const TEXT_VERSION: u16 = 5;
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
56pub struct TextId {
57 index: u32,
58 generation: u32,
59}
60
61impl TextId {
62 pub const fn index(self) -> u32 {
63 self.index
64 }
65
66 pub const fn generation(self) -> u32 {
67 self.generation
68 }
69
70 pub const fn to_bits(self) -> u64 {
71 ((self.index as u64) << 32) | self.generation as u64
72 }
73
74 pub const fn from_bits(bits: u64) -> Self {
75 Self { index: (bits >> 32) as u32, generation: bits as u32 }
76 }
77}
78
79impl fmt::Display for TextId {
80 fn fmt(
81 &self,
82 f: &mut fmt::Formatter<'_>,
83 ) -> fmt::Result {
84 write!(f, "text:{}:{}", self.index, self.generation)
85 }
86}
87
88impl FromStr for TextId {
89 type Err = Error;
90
91 fn from_str(text: &str) -> Result<Self> {
92 let malformed = || Error::Malformed(text.to_owned());
93 let rest = text.strip_prefix("text:").ok_or_else(malformed)?;
94 let (index, generation) = rest.split_once(':').ok_or_else(malformed)?;
95 Ok(Self {
96 index: index.parse().map_err(|_| malformed())?,
97 generation: generation.parse().map_err(|_| malformed())?,
98 })
99 }
100}
101
102#[derive(Clone)]
104pub struct Text {
105 table: TextBackend,
106 id: TextId,
107 _lease: Arc<Lease>,
108}
109
110impl Text {
111 pub fn id(&self) -> TextId {
112 self.id
113 }
114
115 pub fn load(&self) -> Result<String> {
118 let mut bytes = vec![0_u8; CELL_TEXT_MAX];
119 let len = self.slot()?.copy(self.id, &mut bytes)?;
120 bytes.truncate(len);
121 String::from_utf8(bytes)
122 .map_err(|_| Error::Corrupt(self.id.to_string()))
123 }
124
125 pub fn load_into<'a>(
135 &self,
136 buffer: &'a mut [u8],
137 ) -> Result<&'a str> {
138 let len = self.slot()?.copy(self.id, buffer)?;
139 if len > buffer.len() {
140 return Err(Error::BufferTooSmall {
141 required: len,
142 available: buffer.len(),
143 });
144 }
145 std::str::from_utf8(&buffer[..len])
146 .map_err(|_| Error::Corrupt(self.id.to_string()))
147 }
148
149 pub fn len(&self) -> Result<usize> {
150 self.slot()?.copy(self.id, &mut [])
151 }
152
153 pub fn is_empty(&self) -> Result<bool> {
154 Ok(self.len()? == 0)
155 }
156
157 pub fn store(
159 &self,
160 text: &str,
161 ) -> Result<()> {
162 if text.len() > CELL_TEXT_MAX {
163 return Err(Error::TooLong { len: text.len(), max: CELL_TEXT_MAX });
164 }
165 let slot = self.slot()?;
166 slot.writer(self.table.writer_token())?
167 .write(text.as_bytes(), false)?;
168 Ok(())
169 }
170
171 pub fn append(
174 &self,
175 text: &str,
176 ) -> Result<usize> {
177 self.slot()?
178 .writer(self.table.writer_token())?
179 .write(text.as_bytes(), true)
180 }
181
182 pub fn release(self) -> Result<()> {
185 self.table.release(self.id)
186 }
187
188 pub fn version(&self) -> Result<u64> {
191 Ok(self.slot()?.committed.load(Ordering::Acquire))
192 }
193
194 pub fn wait_changed(
198 &self,
199 since: u64,
200 ) -> Result<u64> {
201 loop {
202 let slot = self.slot()?;
203 let now = slot.committed.load(Ordering::SeqCst);
204 if now != since {
205 return Ok(now);
206 }
207 slot.wake.wait_if(|| {
208 slot.state.load(Ordering::SeqCst) == SLOT_OCCUPIED
209 && slot.committed.load(Ordering::SeqCst) == since
210 })?;
211 }
212 }
213
214 pub fn wait_changed_until(
220 &self,
221 since: u64,
222 interrupt: &core::sync::atomic::AtomicBool,
223 ) -> Result<Option<u64>> {
224 loop {
225 if interrupt.load(Ordering::SeqCst) {
226 return Ok(None);
227 }
228
229 let slot = self.slot()?;
230 let now = slot.committed.load(Ordering::SeqCst);
231 if now != since {
232 return Ok(Some(now));
233 }
234
235 slot.wake.wait_if(|| {
236 slot.state.load(Ordering::SeqCst) == SLOT_OCCUPIED
237 && slot.committed.load(Ordering::SeqCst) == since
238 && !interrupt.load(Ordering::SeqCst)
239 })?;
240 }
241 }
242
243 pub fn wake_waiters(&self) -> Result<()> {
246 self.slot()?.wake.notify();
247 Ok(())
248 }
249
250 fn slot(&self) -> Result<&TextSlot> {
251 self.table.slot(self.id)
252 }
253}
254
255#[derive(Clone)]
256pub(crate) enum TextBackend {
257 InMemory(Arc<MemoryTextTable>),
258 #[cfg(unix)]
259 Shm(Arc<ShmTextTable>),
260}
261
262impl TextBackend {
263 pub(crate) fn new(fleet: &Arc<Fleet>) -> Result<Self> {
264 Ok(if fleet.is_shm() {
265 #[cfg(unix)]
266 {
267 Self::Shm(shm_table(
268 &ring_segment_name(fleet.name(), CELL_TEXT_KIND),
269 lifetime::owner_capacity(fleet),
270 )?)
271 }
272 #[cfg(not(unix))]
273 unreachable!("non-Unix fleets cannot use POSIX SHM")
274 } else {
275 Self::InMemory(memory_table(fleet))
276 })
277 }
278
279 pub(crate) fn allocate(
280 &self,
281 text: &str,
282 ) -> Result<Text> {
283 if text.len() > CELL_TEXT_MAX {
284 return Err(Error::TooLong { len: text.len(), max: CELL_TEXT_MAX });
285 }
286 let (id, lease) = self.with_structure(|slots, hint, owners| {
287 let capacity = slots.len();
288 let start = hint.load(Ordering::Relaxed) as usize;
289 let allocate = || -> Result<Option<(TextId, Arc<Lease>)>> {
290 for offset in 0..capacity {
291 let index = (start + offset) & (capacity - 1);
292 let slot = &slots[index];
293 if matches!(
294 slot.state.load(Ordering::Acquire),
295 SLOT_EMPTY | SLOT_RETIRED
296 ) && slot.generation.load(Ordering::Relaxed) != u32::MAX
297 && slot.writer.load(Ordering::Acquire) == 0
298 && !owners.pinned(index)
299 {
300 let lease = owners.lease(index)?;
301 let generation = slot.install(text.as_bytes());
302 hint.store(
303 ((index + 1) & (capacity - 1)) as u32,
304 Ordering::Relaxed,
305 );
306 return Ok(Some((
307 TextId { index: index as u32, generation },
308 lease,
309 )));
310 }
311 }
312 Ok(None)
313 };
314 if let Some(allocated) = allocate()? {
315 return Ok(allocated);
316 }
317 if self.reclaim_dead(owners)?
318 && let Some(allocated) = allocate()?
319 {
320 return Ok(allocated);
321 }
322 Err(Error::Full { capacity })
323 })?;
324 Ok(Text { table: self.clone(), id, _lease: lease })
325 }
326
327 pub(crate) fn open(
328 &self,
329 id: TextId,
330 ) -> Result<Text> {
331 let lease = self.with_structure(|slots, _, owners| {
332 slots
333 .get(id.index as usize)
334 .filter(|slot| slot.is(id))
335 .ok_or(Error::StaleText(id))?;
336 owners.lease(id.index as usize)
337 })?;
338 Ok(Text { table: self.clone(), id, _lease: lease })
339 }
340
341 pub(crate) fn release(
342 &self,
343 id: TextId,
344 ) -> Result<()> {
345 self.with_structure(|slots, _, _| {
346 let slot = slots
347 .get(id.index as usize)
348 .filter(|slot| slot.is(id))
349 .ok_or(Error::StaleText(id))?;
350 slot.state.store(SLOT_RETIRED, Ordering::SeqCst);
351 slot.wake.changed();
352 Ok(())
353 })
354 }
355
356 pub(crate) fn is_live(
357 &self,
358 id: TextId,
359 ) -> bool {
360 self.slot(id).is_ok()
361 }
362
363 pub(crate) fn reset_all(&self) -> Result<()> {
364 self.with_structure(|slots, hint, _| {
365 for slot in slots {
366 slot.state.store(SLOT_RETIRED, Ordering::SeqCst);
367 slot.wake.changed();
368 }
369 hint.store(0, Ordering::Relaxed);
370 Ok(())
371 })
372 }
373
374 #[cfg(unix)]
375 pub(crate) fn unlink(&self) -> Result<()> {
376 match self {
377 Self::InMemory(_) => self.reset_all(),
378 Self::Shm(table) => table.shared.region.unlink().map_err(Error::Io),
379 }
380 }
381
382 fn writer_token(&self) -> u64 {
383 match self {
384 Self::InMemory(_) => 1,
385 #[cfg(unix)]
386 Self::Shm(table) => table.token,
387 }
388 }
389
390 fn reclaim_dead(
391 &self,
392 owners: &Owners,
393 ) -> Result<bool> {
394 owners.reclaim_dead_with(|owner| {
395 #[cfg(unix)]
396 if let Self::Shm(table) = self {
397 owners::recover(&table.shared.region, owner)?;
398 }
399 #[cfg(not(unix))]
400 let _ = owner;
401 Ok(())
402 })
403 }
404
405 pub(crate) fn recover_writers(&self) -> Result<usize> {
406 let mut recovered = 0;
407 self.with_structure(|_, _, pins| {
408 pins.reclaim_dead_with(|owner| {
409 #[cfg(unix)]
410 if let Self::Shm(table) = self {
411 recovered += owners::recover(&table.shared.region, owner)?;
412 }
413 #[cfg(not(unix))]
414 let _ = owner;
415 Ok(())
416 })?;
417 Ok(())
418 })?;
419 Ok(recovered)
420 }
421
422 fn slots(&self) -> &[TextSlot] {
423 match self {
424 Self::InMemory(table) => &table.slots,
425 #[cfg(unix)]
426 Self::Shm(table) => table.slots(),
427 }
428 }
429
430 fn slot(
431 &self,
432 id: TextId,
433 ) -> Result<&TextSlot> {
434 self.slots()
435 .get(id.index as usize)
436 .filter(|slot| slot.is(id))
437 .ok_or(Error::StaleText(id))
438 }
439
440 fn with_structure<T>(
441 &self,
442 operation: impl FnOnce(&[TextSlot], &AtomicU32, &Owners) -> Result<T>,
443 ) -> Result<T> {
444 match self {
445 Self::InMemory(table) => {
446 let _local = lock_unpoisoned(&table.structural_lock);
447 operation(&table.slots, &table.hint, &table.owners)
448 }
449 #[cfg(unix)]
450 Self::Shm(table) => table.with_structure(operation),
451 }
452 }
453}
454
455#[repr(C, align(64))]
456struct TextStateHeader {
457 #[cfg(unix)]
458 magic: u32,
459 #[cfg(not(unix))]
460 _magic: u32,
461 #[cfg(unix)]
462 version: u16,
463 #[cfg(not(unix))]
464 _version: u16,
465 header_size: u16,
466 capacity: u32,
467 slot_size: u32,
468 hint: AtomicU32,
469 owners: u32,
470 _reserved: [u8; 40],
471}
472
473impl TextStateHeader {
474 fn new(owners: usize) -> Self {
475 Self {
476 #[cfg(unix)]
477 magic: TEXT_MAGIC,
478 #[cfg(not(unix))]
479 _magic: 0,
480 #[cfg(unix)]
481 version: TEXT_VERSION,
482 #[cfg(not(unix))]
483 _version: 0,
484 header_size: size_of::<Self>() as u16,
485 capacity: CELL_TEXT_CAPACITY as u32,
486 slot_size: size_of::<TextSlot>() as u32,
487 hint: AtomicU32::new(0),
488 owners: owners as u32,
489 _reserved: [0; 40],
490 }
491 }
492}
493
494pub(crate) struct MemoryTextTable {
495 _fleet: Arc<Fleet>,
498 slots: Vec<TextSlot>,
499 hint: AtomicU32,
500 structural_lock: Mutex<()>,
501 owners: Owners,
502}
503
504impl MemoryTextTable {
505 fn new(fleet: Arc<Fleet>) -> Self {
506 Self {
507 _fleet: fleet,
508 slots: (0..CELL_TEXT_CAPACITY).map(|_| TextSlot::empty()).collect(),
509 hint: AtomicU32::new(0),
510 structural_lock: Mutex::new(()),
511 owners: Owners::memory(CELL_TEXT_CAPACITY),
512 }
513 }
514}
515
516type MemoryRegistry = HashMap<usize, Weak<MemoryTextTable>>;
517
518static MEMORY_TABLES: LazyLock<Mutex<MemoryRegistry>> =
519 LazyLock::new(|| Mutex::new(HashMap::new()));
520
521fn memory_table(fleet: &Arc<Fleet>) -> Arc<MemoryTextTable> {
522 let fleet_identity = Arc::as_ptr(fleet) as usize;
523 let mut tables = lock_unpoisoned(&MEMORY_TABLES);
524 tables.retain(|_, table| table.strong_count() > 0);
525 if let Some(table) = tables.get(&fleet_identity).and_then(Weak::upgrade) {
526 return table;
527 }
528 let table = Arc::new(MemoryTextTable::new(Arc::clone(fleet)));
529 tables.insert(fleet_identity, Arc::downgrade(&table));
530 table
531}
532
533#[cfg(unix)]
534pub(crate) struct ShmTextTable {
535 shared: Arc<lifetime::Shared>,
536 token: u64,
537 structural_lock: Mutex<()>,
538}
539
540#[cfg(unix)]
541impl ShmTextTable {
542 fn open_or_create(
543 name: &str,
544 owners: usize,
545 ) -> Result<Self> {
546 use std::{io, ptr};
547
548 let (region, _initialization_lock) =
549 ShmRegion::open_or_create_locked(name, shm_segment_size(owners)?)?;
550 if region.created() {
551 unsafe {
552 ptr::write(
553 region.as_ptr().cast::<TextStateHeader>(),
554 TextStateHeader::new(owners),
555 );
556 let slots = region.as_ptr().add(size_of::<TextStateHeader>());
557 ptr::write_bytes(
558 slots,
559 0,
560 shm_segment_size(owners)? - size_of::<TextStateHeader>(),
561 );
562 }
563 } else {
564 let header = unsafe { &*region.as_ptr().cast::<TextStateHeader>() };
565 if header.magic != TEXT_MAGIC {
566 return Err(Error::Io(io::Error::new(
567 io::ErrorKind::InvalidData,
568 format!(
569 "SHM segment {name} has wrong magic 0x{:08X} (expected 0x{TEXT_MAGIC:08X})",
570 header.magic
571 ),
572 )));
573 }
574 if header.version != TEXT_VERSION
575 || usize::from(header.header_size)
576 != size_of::<TextStateHeader>()
577 || header.capacity as usize != CELL_TEXT_CAPACITY
578 || header.slot_size as usize != size_of::<TextSlot>()
579 || header.owners as usize != owners
580 {
581 return Err(Error::Io(io::Error::new(
582 io::ErrorKind::InvalidData,
583 format!(
584 "SHM segment {name} has an incompatible text-cell layout"
585 ),
586 )));
587 }
588 }
589 let shared = lifetime::Shared::claim_locked_with(
590 region,
591 name,
592 owners,
593 CELL_TEXT_CAPACITY,
594 owners::pins_offset(owners)?,
595 |owner, region| owners::claim(region, owner),
596 )?;
597 let token = owners::token(&shared.region, shared.owner());
598 Ok(Self { shared, token, structural_lock: Mutex::new(()) })
599 }
600
601 fn header(&self) -> &TextStateHeader {
602 unsafe { &*self.shared.region.as_ptr().cast::<TextStateHeader>() }
603 }
604
605 fn slots(&self) -> &[TextSlot] {
606 owners::slots(&self.shared.region)
607 }
608
609 fn with_structure<T>(
610 &self,
611 operation: impl FnOnce(&[TextSlot], &AtomicU32, &Owners) -> Result<T>,
612 ) -> Result<T> {
613 let owners = Owners::Shared(self.shared.clone());
614 owners.check_process()?;
615 let _local = lock_unpoisoned(&self.structural_lock);
616 let _process = self.shared.region.lock_exclusive()?;
617 operation(self.slots(), &self.header().hint, &owners)
618 }
619}
620
621#[cfg(unix)]
622fn shm_segment_size(owners: usize) -> Result<usize> {
623 lifetime::segment_size(
624 owners::pins_offset(owners)?,
625 CELL_TEXT_CAPACITY,
626 owners,
627 )
628}
629
630#[cfg(unix)]
631fn shm_table(
632 name: &str,
633 owners: usize,
634) -> Result<Arc<ShmTextTable>> {
635 static TABLES: LazyLock<Mutex<HashMap<String, Weak<ShmTextTable>>>> =
636 LazyLock::new(|| Mutex::new(HashMap::new()));
637 let mut tables = lock_unpoisoned(&TABLES);
638 tables.retain(|_, table| table.strong_count() != 0);
639 if let Some(table) = tables.get(name).and_then(Weak::upgrade)
640 && table.shared.is_current(owners)
641 {
642 return Ok(table);
643 }
644 let table = Arc::new(ShmTextTable::open_or_create(name, owners)?);
645 tables.insert(name.to_owned(), Arc::downgrade(&table));
646 Ok(table)
647}
648
649const _: () = assert!(CELL_TEXT_CAPACITY.is_power_of_two());
650const _: () = assert!(CELL_TEXT_MAX > 0 && CELL_TEXT_MAX <= u16::MAX as usize);
651const _: () = assert!(size_of::<TextStateHeader>() == 64);