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 len(&self) -> Result<usize> {
126 self.slot()?.copy(self.id, &mut [])
127 }
128
129 pub fn is_empty(&self) -> Result<bool> {
130 Ok(self.len()? == 0)
131 }
132
133 pub fn store(
135 &self,
136 text: &str,
137 ) -> Result<()> {
138 if text.len() > CELL_TEXT_MAX {
139 return Err(Error::TooLong { len: text.len(), max: CELL_TEXT_MAX });
140 }
141 let slot = self.slot()?;
142 slot.writer(self.table.writer_token())?
143 .write(text.as_bytes(), false)?;
144 Ok(())
145 }
146
147 pub fn append(
150 &self,
151 text: &str,
152 ) -> Result<usize> {
153 self.slot()?
154 .writer(self.table.writer_token())?
155 .write(text.as_bytes(), true)
156 }
157
158 pub fn release(self) -> Result<()> {
161 self.table.release(self.id)
162 }
163
164 pub fn version(&self) -> Result<u64> {
167 Ok(self.slot()?.committed.load(Ordering::Acquire))
168 }
169
170 pub fn wait_changed(
174 &self,
175 since: u64,
176 ) -> Result<u64> {
177 loop {
178 let slot = self.slot()?;
179 let now = slot.committed.load(Ordering::SeqCst);
180 if now != since {
181 return Ok(now);
182 }
183 slot.wake.wait_if(|| {
184 slot.state.load(Ordering::SeqCst) == SLOT_OCCUPIED
185 && slot.committed.load(Ordering::SeqCst) == since
186 })?;
187 }
188 }
189
190 pub fn wait_changed_until(
196 &self,
197 since: u64,
198 interrupt: &core::sync::atomic::AtomicBool,
199 ) -> Result<Option<u64>> {
200 loop {
201 if interrupt.load(Ordering::SeqCst) {
202 return Ok(None);
203 }
204
205 let slot = self.slot()?;
206 let now = slot.committed.load(Ordering::SeqCst);
207 if now != since {
208 return Ok(Some(now));
209 }
210
211 slot.wake.wait_if(|| {
212 slot.state.load(Ordering::SeqCst) == SLOT_OCCUPIED
213 && slot.committed.load(Ordering::SeqCst) == since
214 && !interrupt.load(Ordering::SeqCst)
215 })?;
216 }
217 }
218
219 pub fn wake_waiters(&self) -> Result<()> {
222 self.slot()?.wake.notify();
223 Ok(())
224 }
225
226 fn slot(&self) -> Result<&TextSlot> {
227 self.table.slot(self.id)
228 }
229}
230
231#[derive(Clone)]
232pub(crate) enum TextBackend {
233 InMemory(Arc<MemoryTextTable>),
234 #[cfg(unix)]
235 Shm(Arc<ShmTextTable>),
236}
237
238impl TextBackend {
239 pub(crate) fn new(fleet: &Arc<Fleet>) -> Result<Self> {
240 Ok(if fleet.is_shm() {
241 #[cfg(unix)]
242 {
243 Self::Shm(shm_table(
244 &ring_segment_name(fleet.name(), CELL_TEXT_KIND),
245 lifetime::owner_capacity(fleet),
246 )?)
247 }
248 #[cfg(not(unix))]
249 unreachable!("non-Unix fleets cannot use POSIX SHM")
250 } else {
251 Self::InMemory(memory_table(fleet))
252 })
253 }
254
255 pub(crate) fn allocate(
256 &self,
257 text: &str,
258 ) -> Result<Text> {
259 if text.len() > CELL_TEXT_MAX {
260 return Err(Error::TooLong { len: text.len(), max: CELL_TEXT_MAX });
261 }
262 let (id, lease) = self.with_structure(|slots, hint, owners| {
263 let capacity = slots.len();
264 let start = hint.load(Ordering::Relaxed) as usize;
265 let allocate = || -> Result<Option<(TextId, Arc<Lease>)>> {
266 for offset in 0..capacity {
267 let index = (start + offset) & (capacity - 1);
268 let slot = &slots[index];
269 if matches!(
270 slot.state.load(Ordering::Acquire),
271 SLOT_EMPTY | SLOT_RETIRED
272 ) && slot.generation.load(Ordering::Relaxed) != u32::MAX
273 && slot.writer.load(Ordering::Acquire) == 0
274 && !owners.pinned(index)
275 {
276 let lease = owners.lease(index)?;
277 let generation = slot.install(text.as_bytes());
278 hint.store(
279 ((index + 1) & (capacity - 1)) as u32,
280 Ordering::Relaxed,
281 );
282 return Ok(Some((
283 TextId { index: index as u32, generation },
284 lease,
285 )));
286 }
287 }
288 Ok(None)
289 };
290 if let Some(allocated) = allocate()? {
291 return Ok(allocated);
292 }
293 if self.reclaim_dead(owners)?
294 && let Some(allocated) = allocate()?
295 {
296 return Ok(allocated);
297 }
298 Err(Error::Full { capacity })
299 })?;
300 Ok(Text { table: self.clone(), id, _lease: lease })
301 }
302
303 pub(crate) fn open(
304 &self,
305 id: TextId,
306 ) -> Result<Text> {
307 let lease = self.with_structure(|slots, _, owners| {
308 slots
309 .get(id.index as usize)
310 .filter(|slot| slot.is(id))
311 .ok_or(Error::StaleText(id))?;
312 owners.lease(id.index as usize)
313 })?;
314 Ok(Text { table: self.clone(), id, _lease: lease })
315 }
316
317 pub(crate) fn release(
318 &self,
319 id: TextId,
320 ) -> Result<()> {
321 self.with_structure(|slots, _, _| {
322 let slot = slots
323 .get(id.index as usize)
324 .filter(|slot| slot.is(id))
325 .ok_or(Error::StaleText(id))?;
326 slot.state.store(SLOT_RETIRED, Ordering::SeqCst);
327 slot.wake.changed();
328 Ok(())
329 })
330 }
331
332 pub(crate) fn is_live(
333 &self,
334 id: TextId,
335 ) -> bool {
336 self.slot(id).is_ok()
337 }
338
339 pub(crate) fn reset_all(&self) -> Result<()> {
340 self.with_structure(|slots, hint, _| {
341 for slot in slots {
342 slot.state.store(SLOT_RETIRED, Ordering::SeqCst);
343 slot.wake.changed();
344 }
345 hint.store(0, Ordering::Relaxed);
346 Ok(())
347 })
348 }
349
350 #[cfg(unix)]
351 pub(crate) fn unlink(&self) -> Result<()> {
352 match self {
353 Self::InMemory(_) => self.reset_all(),
354 Self::Shm(table) => table.shared.region.unlink().map_err(Error::Io),
355 }
356 }
357
358 fn writer_token(&self) -> u64 {
359 match self {
360 Self::InMemory(_) => 1,
361 #[cfg(unix)]
362 Self::Shm(table) => table.token,
363 }
364 }
365
366 fn reclaim_dead(
367 &self,
368 owners: &Owners,
369 ) -> Result<bool> {
370 owners.reclaim_dead_with(|owner| {
371 #[cfg(unix)]
372 if let Self::Shm(table) = self {
373 owners::recover(&table.shared.region, owner)?;
374 }
375 #[cfg(not(unix))]
376 let _ = owner;
377 Ok(())
378 })
379 }
380
381 pub(crate) fn recover_writers(&self) -> Result<usize> {
382 let mut recovered = 0;
383 self.with_structure(|_, _, pins| {
384 pins.reclaim_dead_with(|owner| {
385 #[cfg(unix)]
386 if let Self::Shm(table) = self {
387 recovered += owners::recover(&table.shared.region, owner)?;
388 }
389 #[cfg(not(unix))]
390 let _ = owner;
391 Ok(())
392 })?;
393 Ok(())
394 })?;
395 Ok(recovered)
396 }
397
398 fn slots(&self) -> &[TextSlot] {
399 match self {
400 Self::InMemory(table) => &table.slots,
401 #[cfg(unix)]
402 Self::Shm(table) => table.slots(),
403 }
404 }
405
406 fn slot(
407 &self,
408 id: TextId,
409 ) -> Result<&TextSlot> {
410 self.slots()
411 .get(id.index as usize)
412 .filter(|slot| slot.is(id))
413 .ok_or(Error::StaleText(id))
414 }
415
416 fn with_structure<T>(
417 &self,
418 operation: impl FnOnce(&[TextSlot], &AtomicU32, &Owners) -> Result<T>,
419 ) -> Result<T> {
420 match self {
421 Self::InMemory(table) => {
422 let _local = lock_unpoisoned(&table.structural_lock);
423 operation(&table.slots, &table.hint, &table.owners)
424 }
425 #[cfg(unix)]
426 Self::Shm(table) => table.with_structure(operation),
427 }
428 }
429}
430
431#[repr(C, align(64))]
432struct TextStateHeader {
433 #[cfg(unix)]
434 magic: u32,
435 #[cfg(not(unix))]
436 _magic: u32,
437 #[cfg(unix)]
438 version: u16,
439 #[cfg(not(unix))]
440 _version: u16,
441 header_size: u16,
442 capacity: u32,
443 slot_size: u32,
444 hint: AtomicU32,
445 owners: u32,
446 _reserved: [u8; 40],
447}
448
449impl TextStateHeader {
450 fn new(owners: usize) -> Self {
451 Self {
452 #[cfg(unix)]
453 magic: TEXT_MAGIC,
454 #[cfg(not(unix))]
455 _magic: 0,
456 #[cfg(unix)]
457 version: TEXT_VERSION,
458 #[cfg(not(unix))]
459 _version: 0,
460 header_size: size_of::<Self>() as u16,
461 capacity: CELL_TEXT_CAPACITY as u32,
462 slot_size: size_of::<TextSlot>() as u32,
463 hint: AtomicU32::new(0),
464 owners: owners as u32,
465 _reserved: [0; 40],
466 }
467 }
468}
469
470pub(crate) struct MemoryTextTable {
471 _fleet: Arc<Fleet>,
474 slots: Vec<TextSlot>,
475 hint: AtomicU32,
476 structural_lock: Mutex<()>,
477 owners: Owners,
478}
479
480impl MemoryTextTable {
481 fn new(fleet: Arc<Fleet>) -> Self {
482 Self {
483 _fleet: fleet,
484 slots: (0..CELL_TEXT_CAPACITY).map(|_| TextSlot::empty()).collect(),
485 hint: AtomicU32::new(0),
486 structural_lock: Mutex::new(()),
487 owners: Owners::memory(CELL_TEXT_CAPACITY),
488 }
489 }
490}
491
492type MemoryRegistry = HashMap<usize, Weak<MemoryTextTable>>;
493
494static MEMORY_TABLES: LazyLock<Mutex<MemoryRegistry>> =
495 LazyLock::new(|| Mutex::new(HashMap::new()));
496
497fn memory_table(fleet: &Arc<Fleet>) -> Arc<MemoryTextTable> {
498 let fleet_identity = Arc::as_ptr(fleet) as usize;
499 let mut tables = lock_unpoisoned(&MEMORY_TABLES);
500 tables.retain(|_, table| table.strong_count() > 0);
501 if let Some(table) = tables.get(&fleet_identity).and_then(Weak::upgrade) {
502 return table;
503 }
504 let table = Arc::new(MemoryTextTable::new(Arc::clone(fleet)));
505 tables.insert(fleet_identity, Arc::downgrade(&table));
506 table
507}
508
509#[cfg(unix)]
510pub(crate) struct ShmTextTable {
511 shared: Arc<lifetime::Shared>,
512 token: u64,
513 structural_lock: Mutex<()>,
514}
515
516#[cfg(unix)]
517impl ShmTextTable {
518 fn open_or_create(
519 name: &str,
520 owners: usize,
521 ) -> Result<Self> {
522 use std::{io, ptr};
523
524 let (region, _initialization_lock) =
525 ShmRegion::open_or_create_locked(name, shm_segment_size(owners)?)?;
526 if region.created() {
527 unsafe {
528 ptr::write(
529 region.as_ptr().cast::<TextStateHeader>(),
530 TextStateHeader::new(owners),
531 );
532 let slots = region.as_ptr().add(size_of::<TextStateHeader>());
533 ptr::write_bytes(
534 slots,
535 0,
536 shm_segment_size(owners)? - size_of::<TextStateHeader>(),
537 );
538 }
539 } else {
540 let header = unsafe { &*region.as_ptr().cast::<TextStateHeader>() };
541 if header.magic != TEXT_MAGIC {
542 return Err(Error::Io(io::Error::new(
543 io::ErrorKind::InvalidData,
544 format!(
545 "SHM segment {name} has wrong magic 0x{:08X} (expected 0x{TEXT_MAGIC:08X})",
546 header.magic
547 ),
548 )));
549 }
550 if header.version != TEXT_VERSION
551 || usize::from(header.header_size)
552 != size_of::<TextStateHeader>()
553 || header.capacity as usize != CELL_TEXT_CAPACITY
554 || header.slot_size as usize != size_of::<TextSlot>()
555 || header.owners as usize != owners
556 {
557 return Err(Error::Io(io::Error::new(
558 io::ErrorKind::InvalidData,
559 format!(
560 "SHM segment {name} has an incompatible text-cell layout"
561 ),
562 )));
563 }
564 }
565 let shared = lifetime::Shared::claim_locked_with(
566 region,
567 name,
568 owners,
569 CELL_TEXT_CAPACITY,
570 owners::pins_offset(owners)?,
571 |owner, region| owners::claim(region, owner),
572 )?;
573 let token = owners::token(&shared.region, shared.owner());
574 Ok(Self { shared, token, structural_lock: Mutex::new(()) })
575 }
576
577 fn header(&self) -> &TextStateHeader {
578 unsafe { &*self.shared.region.as_ptr().cast::<TextStateHeader>() }
579 }
580
581 fn slots(&self) -> &[TextSlot] {
582 owners::slots(&self.shared.region)
583 }
584
585 fn with_structure<T>(
586 &self,
587 operation: impl FnOnce(&[TextSlot], &AtomicU32, &Owners) -> Result<T>,
588 ) -> Result<T> {
589 let owners = Owners::Shared(self.shared.clone());
590 owners.check_process()?;
591 let _local = lock_unpoisoned(&self.structural_lock);
592 let _process = self.shared.region.lock_exclusive()?;
593 operation(self.slots(), &self.header().hint, &owners)
594 }
595}
596
597#[cfg(unix)]
598fn shm_segment_size(owners: usize) -> Result<usize> {
599 lifetime::segment_size(
600 owners::pins_offset(owners)?,
601 CELL_TEXT_CAPACITY,
602 owners,
603 )
604}
605
606#[cfg(unix)]
607fn shm_table(
608 name: &str,
609 owners: usize,
610) -> Result<Arc<ShmTextTable>> {
611 static TABLES: LazyLock<Mutex<HashMap<String, Weak<ShmTextTable>>>> =
612 LazyLock::new(|| Mutex::new(HashMap::new()));
613 let mut tables = lock_unpoisoned(&TABLES);
614 tables.retain(|_, table| table.strong_count() != 0);
615 if let Some(table) = tables.get(name).and_then(Weak::upgrade)
616 && table.shared.is_current(owners)
617 {
618 return Ok(table);
619 }
620 let table = Arc::new(ShmTextTable::open_or_create(name, owners)?);
621 tables.insert(name.to_owned(), Arc::downgrade(&table));
622 Ok(table)
623}
624
625const _: () = assert!(CELL_TEXT_CAPACITY.is_power_of_two());
626const _: () = assert!(CELL_TEXT_MAX > 0 && CELL_TEXT_MAX <= u16::MAX as usize);
627const _: () = assert!(size_of::<TextStateHeader>() == 64);