Skip to main content

orbit_cell/
text.rs

1//! Fixed-width text cells: the string arena beside the scalar table.
2//!
3//! A text cell holds up to [`CELL_TEXT_MAX`] bytes of UTF-8 in shared memory.
4//! Sixty-four bits can be swapped in one instruction; a string cannot, so a
5//! cell is a seqlock: writers take the cell's own spin bit and bump a version
6//! around their write, readers copy and accept the copy only if the version
7//! held still. Readers never block writers and never see a torn string.
8
9use std::collections::HashMap;
10use std::fmt;
11use std::str::FromStr;
12use std::sync::atomic::{AtomicU8, AtomicU16, 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::{Error, Result, SLOT_EMPTY, SLOT_OCCUPIED, lock_unpoisoned};
20
21/// Reserved Orbit SHM kind for the text cell table.
22pub const CELL_TEXT_KIND: u8 = 227;
23/// Number of text cells one fleet generation can hold at once.
24///
25/// Compile-time geometry: `ORBIT_CELL_TEXT_CAPACITY`. Must be a power of two.
26pub const CELL_TEXT_CAPACITY: usize =
27    orbit_core::compile::usize_from_env(option_env!("ORBIT_CELL_TEXT_CAPACITY"), 1_024);
28/// Bytes one text cell can hold.
29///
30/// Compile-time geometry: `ORBIT_CELL_TEXT_MAX`. It sizes every slot, so it is
31/// part of the same wire contract as the capacity.
32pub const CELL_TEXT_MAX: usize =
33    orbit_core::compile::usize_from_env(option_env!("ORBIT_CELL_TEXT_MAX"), 240);
34
35#[cfg(unix)]
36const TEXT_MAGIC: u32 = 0x43_54_58_54; // "CTXT"
37#[cfg(unix)]
38const TEXT_VERSION: u16 = 2;
39
40/// The address of one text cell; prints as `text:<slot>:<generation>`.
41#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
42pub struct TextId {
43    index: u32,
44    generation: u32
45}
46
47impl TextId {
48    pub const fn index(self) -> u32 {
49        self.index
50    }
51
52    pub const fn generation(self) -> u32 {
53        self.generation
54    }
55
56    pub const fn to_bits(self) -> u64 {
57        ((self.index as u64) << 32) | self.generation as u64
58    }
59
60    pub const fn from_bits(bits: u64) -> Self {
61        Self { index: (bits >> 32) as u32, generation: bits as u32 }
62    }
63}
64
65impl fmt::Display for TextId {
66    fn fmt(
67        &self,
68        f: &mut fmt::Formatter<'_>
69    ) -> fmt::Result {
70        write!(f, "text:{}:{}", self.index, self.generation)
71    }
72}
73
74impl FromStr for TextId {
75    type Err = Error;
76
77    fn from_str(text: &str) -> Result<Self> {
78        let malformed = || Error::Malformed(text.to_owned());
79        let rest = text.strip_prefix("text:").ok_or_else(malformed)?;
80        let (index, generation) = rest.split_once(':').ok_or_else(malformed)?;
81        Ok(Self {
82            index: index.parse().map_err(|_| malformed())?,
83            generation: generation.parse().map_err(|_| malformed())?
84        })
85    }
86}
87
88/// One text cell. Cheap to clone; every clone is the same place.
89#[derive(Clone)]
90pub struct Text {
91    table: TextBackend,
92    id: TextId
93}
94
95impl Text {
96    pub fn id(&self) -> TextId {
97        self.id
98    }
99
100    /// A copy of the current text.
101    pub fn load(&self) -> Result<String> {
102        let slot = self.slot()?;
103        let mut bytes = vec![0_u8; CELL_TEXT_MAX];
104        loop {
105            let before = slot.version.load(Ordering::Acquire);
106            if before & 1 == 1 {
107                std::hint::spin_loop();
108                continue;
109            }
110            let len = usize::from(slot.len.load(Ordering::Relaxed)).min(CELL_TEXT_MAX);
111            for (target, source) in bytes[..len].iter_mut().zip(&slot.bytes[..len]) {
112                *target = source.load(Ordering::Relaxed);
113            }
114            std::sync::atomic::fence(Ordering::Acquire);
115            if slot.version.load(Ordering::Relaxed) == before {
116                bytes.truncate(len);
117                return String::from_utf8(bytes).map_err(|_| Error::Corrupt(self.id.to_string()));
118            }
119        }
120    }
121
122    pub fn len(&self) -> Result<usize> {
123        Ok(usize::from(self.slot()?.len.load(Ordering::Acquire)))
124    }
125
126    pub fn is_empty(&self) -> Result<bool> {
127        Ok(self.len()? == 0)
128    }
129
130    /// Replace the text.
131    pub fn store(
132        &self,
133        text: &str
134    ) -> Result<()> {
135        if text.len() > CELL_TEXT_MAX {
136            return Err(Error::TooLong { len: text.len(), max: CELL_TEXT_MAX });
137        }
138        let slot = self.slot()?;
139        let _write = slot.writer();
140        slot.write(0, text.as_bytes());
141        Ok(())
142    }
143
144    /// Append to the text, atomically with respect to every other writer;
145    /// refused whole, never cut, when the result would not fit.
146    pub fn append(
147        &self,
148        text: &str
149    ) -> Result<usize> {
150        let slot = self.slot()?;
151        let _write = slot.writer();
152        let current = usize::from(slot.len.load(Ordering::Relaxed));
153        let total = current + text.len();
154        if total > CELL_TEXT_MAX {
155            return Err(Error::TooLong { len: total, max: CELL_TEXT_MAX });
156        }
157        slot.write(current, text.as_bytes());
158        Ok(total)
159    }
160
161    /// Give the cell back; this and every other handle to it go stale.
162    pub fn release(self) -> Result<()> {
163        self.table.release(self.id)
164    }
165
166    /// The seqlock sequence at rest, the thing [`Self::wait_changed`] waits
167    /// on. Take it before [`Self::load`] and the wait misses nothing.
168    pub fn version(&self) -> Result<u32> {
169        let slot = self.slot()?;
170        loop {
171            let version = slot.version.load(Ordering::Acquire);
172            if version & 1 == 0 {
173                return Ok(version);
174            }
175            std::hint::spin_loop();
176        }
177    }
178
179    /// Park until the text has been written since `since`, then return the
180    /// sequence now. A released cell wakes every waiter with
181    /// [`Error::StaleText`]. Blocking; an async runtime wraps it.
182    pub fn wait_changed(
183        &self,
184        since: u32
185    ) -> Result<u32> {
186        loop {
187            let slot = self.slot()?;
188            let now = slot.version.load(Ordering::SeqCst);
189            if now != since {
190                if now & 1 == 1 {
191                    std::hint::spin_loop();
192                    continue;
193                }
194                return Ok(now);
195            }
196            slot.waiters.fetch_add(1, Ordering::SeqCst);
197            let outcome = if slot.version.load(Ordering::SeqCst) == since {
198                crate::wait_on(&slot.version, since)
199            } else {
200                Ok(())
201            };
202            slot.waiters.fetch_sub(1, Ordering::SeqCst);
203            outcome?;
204        }
205    }
206
207    /// [`Self::wait_changed`], but a second word can end it. `Ok(None)`
208    /// means the interrupt arrived, not that time ran out — nothing here
209    /// measures time. The canceller sets `interrupt`, then calls
210    /// [`Self::wake_waiters`].
211    pub fn wait_changed_until(
212        &self,
213        since: u32,
214        interrupt: &core::sync::atomic::AtomicBool
215    ) -> Result<Option<u32>> {
216        loop {
217            if interrupt.load(Ordering::SeqCst) {
218                return Ok(None);
219            }
220
221            let slot = self.slot()?;
222            let now = slot.version.load(Ordering::SeqCst);
223            if now != since {
224                if now & 1 == 1 {
225                    std::hint::spin_loop();
226                    continue;
227                }
228                return Ok(Some(now));
229            }
230
231            slot.waiters.fetch_add(1, Ordering::SeqCst);
232            let outcome = if slot.version.load(Ordering::SeqCst) == since
233                && !interrupt.load(Ordering::SeqCst)
234            {
235                crate::wait_on(&slot.version, since)
236            } else {
237                Ok(())
238            };
239            slot.waiters.fetch_sub(1, Ordering::SeqCst);
240            outcome?;
241        }
242    }
243
244    /// Wake everyone parked on this text cell without appending to it.
245    /// Only useful after the word the waiter also watches has been set.
246    pub fn wake_waiters(&self) -> Result<()> {
247        crate::wake_on(&self.slot()?.version);
248        Ok(())
249    }
250
251    fn slot(&self) -> Result<&TextSlot> {
252        self.table.slot(self.id)
253    }
254}
255
256#[derive(Clone)]
257pub(crate) enum TextBackend {
258    InMemory(Arc<MemoryTextTable>),
259    #[cfg(unix)]
260    Shm(Arc<ShmTextTable>)
261}
262
263impl TextBackend {
264    pub(crate) fn new(fleet: &Arc<Fleet>) -> Result<Self> {
265        Ok(if fleet.is_shm() {
266            #[cfg(unix)]
267            {
268                Self::Shm(Arc::new(ShmTextTable::open_or_create(&ring_segment_name(
269                    fleet.name(),
270                    CELL_TEXT_KIND
271                ))?))
272            }
273            #[cfg(not(unix))]
274            unreachable!("non-Unix fleets cannot use POSIX SHM")
275        } else {
276            Self::InMemory(memory_table(fleet))
277        })
278    }
279
280    pub(crate) fn allocate(
281        &self,
282        text: &str
283    ) -> Result<Text> {
284        if text.len() > CELL_TEXT_MAX {
285            return Err(Error::TooLong { len: text.len(), max: CELL_TEXT_MAX });
286        }
287        let id = self.with_structure(|slots, hint| {
288            let capacity = slots.len();
289            let start = hint.load(Ordering::Relaxed) as usize;
290            for offset in 0..capacity {
291                let index = (start + offset) & (capacity - 1);
292                let slot = &slots[index];
293                if slot.state.load(Ordering::Acquire) == SLOT_EMPTY {
294                    let generation = slot.install(text.as_bytes());
295                    hint.store(((index + 1) & (capacity - 1)) as u32, Ordering::Relaxed);
296                    return Ok(TextId { index: index as u32, generation });
297                }
298            }
299            Err(Error::Full { capacity })
300        })?;
301        Ok(Text { table: self.clone(), id })
302    }
303
304    pub(crate) fn open(
305        &self,
306        id: TextId
307    ) -> Result<Text> {
308        self.slot(id)?;
309        Ok(Text { table: self.clone(), id })
310    }
311
312    pub(crate) fn release(
313        &self,
314        id: TextId
315    ) -> Result<()> {
316        self.with_structure(|slots, _| {
317            let slot = slots
318                .get(id.index as usize)
319                .filter(|slot| slot.is(id))
320                .ok_or(Error::StaleText(id))?;
321            slot.state.store(SLOT_EMPTY, Ordering::Release);
322            // An even step keeps the seqlock at rest; waiters find it gone.
323            slot.version.fetch_add(2, Ordering::SeqCst);
324            slot.wake_waiters();
325            Ok(())
326        })
327    }
328
329    pub(crate) fn is_live(
330        &self,
331        id: TextId
332    ) -> bool {
333        self.slot(id).is_ok()
334    }
335
336    pub(crate) fn reset_all(&self) -> Result<()> {
337        self.with_structure(|slots, hint| {
338            for slot in slots {
339                slot.state.store(SLOT_EMPTY, Ordering::Release);
340                slot.version.fetch_add(2, Ordering::SeqCst);
341                slot.wake_waiters();
342            }
343            hint.store(0, Ordering::Relaxed);
344            Ok(())
345        })
346    }
347
348    #[cfg(unix)]
349    pub(crate) fn unlink(&self) -> Result<()> {
350        match self {
351            Self::InMemory(_) => self.reset_all(),
352            Self::Shm(table) => table.region.unlink().map_err(Error::Io)
353        }
354    }
355
356    fn slots(&self) -> &[TextSlot] {
357        match self {
358            Self::InMemory(table) => &table.slots,
359            #[cfg(unix)]
360            Self::Shm(table) => table.slots()
361        }
362    }
363
364    fn slot(
365        &self,
366        id: TextId
367    ) -> Result<&TextSlot> {
368        self.slots().get(id.index as usize).filter(|slot| slot.is(id)).ok_or(Error::StaleText(id))
369    }
370
371    fn with_structure<T>(
372        &self,
373        operation: impl FnOnce(&[TextSlot], &AtomicU32) -> Result<T>
374    ) -> Result<T> {
375        match self {
376            Self::InMemory(table) => {
377                let _local = lock_unpoisoned(&table.structural_lock);
378                operation(&table.slots, &table.hint)
379            }
380            #[cfg(unix)]
381            Self::Shm(table) => table.with_structure(operation)
382        }
383    }
384}
385
386#[repr(C, align(64))]
387struct TextStateHeader {
388    #[cfg(unix)]
389    magic: u32,
390    #[cfg(not(unix))]
391    _magic: u32,
392    #[cfg(unix)]
393    version: u16,
394    #[cfg(not(unix))]
395    _version: u16,
396    header_size: u16,
397    capacity: u32,
398    slot_size: u32,
399    hint: AtomicU32,
400    _reserved: [u8; 44]
401}
402
403impl TextStateHeader {
404    fn new() -> Self {
405        Self {
406            #[cfg(unix)]
407            magic: TEXT_MAGIC,
408            #[cfg(not(unix))]
409            _magic: 0,
410            #[cfg(unix)]
411            version: TEXT_VERSION,
412            #[cfg(not(unix))]
413            _version: 0,
414            header_size: size_of::<Self>() as u16,
415            capacity: CELL_TEXT_CAPACITY as u32,
416            slot_size: size_of::<TextSlot>() as u32,
417            hint: AtomicU32::new(0),
418            _reserved: [0; 44]
419        }
420    }
421}
422
423/// One text cell in shared memory: a seqlock version, a writer bit, a
424/// length and the bytes, padded to the cache line.
425#[repr(C, align(64))]
426struct TextSlot {
427    state: AtomicU8,
428    writer: AtomicU8,
429    len: AtomicU16,
430    generation: AtomicU32,
431    /// The seqlock's sequence: odd while a write is in progress, and the
432    /// word a waiter parks on, since every write moves it.
433    version: AtomicU32,
434    /// Waiters parked on `version`; a writer wakes only when nonzero.
435    waiters: AtomicU32,
436    bytes: [AtomicU8; CELL_TEXT_MAX]
437}
438
439impl TextSlot {
440    fn empty() -> Self {
441        Self {
442            state: AtomicU8::new(SLOT_EMPTY),
443            writer: AtomicU8::new(0),
444            len: AtomicU16::new(0),
445            generation: AtomicU32::new(0),
446            version: AtomicU32::new(0),
447            waiters: AtomicU32::new(0),
448            bytes: std::array::from_fn(|_| AtomicU8::new(0))
449        }
450    }
451
452    fn is(
453        &self,
454        id: TextId
455    ) -> bool {
456        self.state.load(Ordering::Acquire) == SLOT_OCCUPIED
457            && self.generation.load(Ordering::Relaxed) == id.generation
458    }
459
460    /// Under the structural lock.
461    fn install(
462        &self,
463        text: &[u8]
464    ) -> u32 {
465        let generation = self.generation.load(Ordering::Relaxed).wrapping_add(1).max(1);
466        for (index, byte) in self.bytes.iter().enumerate() {
467            byte.store(text.get(index).copied().unwrap_or(0), Ordering::Relaxed);
468        }
469        self.len.store(text.len() as u16, Ordering::Relaxed);
470        self.version.store(0, Ordering::Relaxed);
471        self.waiters.store(0, Ordering::Relaxed);
472        self.writer.store(0, Ordering::Relaxed);
473        self.generation.store(generation, Ordering::Relaxed);
474        self.state.store(SLOT_OCCUPIED, Ordering::Release);
475        generation
476    }
477
478    /// The cell's writer bit, held for the duration of one write.
479    fn writer(&self) -> WriterGuard<'_> {
480        while self.writer.compare_exchange_weak(0, 1, Ordering::Acquire, Ordering::Relaxed).is_err()
481        {
482            std::hint::spin_loop();
483        }
484        WriterGuard(self)
485    }
486
487    /// Write `bytes` at `offset` and publish the new length; the caller holds
488    /// the writer bit and has checked the fit.
489    fn write(
490        &self,
491        offset: usize,
492        bytes: &[u8]
493    ) {
494        debug_assert!(offset + bytes.len() <= CELL_TEXT_MAX);
495        self.version.fetch_add(1, Ordering::AcqRel);
496        for (target, source) in self.bytes[offset..offset + bytes.len()].iter().zip(bytes) {
497            target.store(*source, Ordering::Relaxed);
498        }
499        self.len.store((offset + bytes.len()) as u16, Ordering::Relaxed);
500        self.version.fetch_add(1, Ordering::SeqCst);
501        self.wake_waiters();
502    }
503
504    /// The sequence moved: wake whoever parked on it, if anyone did.
505    fn wake_waiters(&self) {
506        if self.waiters.load(Ordering::SeqCst) > 0 {
507            crate::wake_on(&self.version);
508        }
509    }
510}
511
512struct WriterGuard<'a>(&'a TextSlot);
513
514impl Drop for WriterGuard<'_> {
515    fn drop(&mut self) {
516        self.0.writer.store(0, Ordering::Release);
517    }
518}
519
520pub(crate) struct MemoryTextTable {
521    /// Held so the fleet's address, which keys the registry, cannot be
522    /// reused by another fleet while this table lives.
523    _fleet: Arc<Fleet>,
524    slots: Vec<TextSlot>,
525    hint: AtomicU32,
526    structural_lock: Mutex<()>
527}
528
529impl MemoryTextTable {
530    fn new(fleet: Arc<Fleet>) -> Self {
531        Self {
532            _fleet: fleet,
533            slots: (0..CELL_TEXT_CAPACITY).map(|_| TextSlot::empty()).collect(),
534            hint: AtomicU32::new(0),
535            structural_lock: Mutex::new(())
536        }
537    }
538}
539
540type MemoryRegistry = HashMap<usize, Weak<MemoryTextTable>>;
541
542static MEMORY_TABLES: LazyLock<Mutex<MemoryRegistry>> =
543    LazyLock::new(|| Mutex::new(HashMap::new()));
544
545fn memory_table(fleet: &Arc<Fleet>) -> Arc<MemoryTextTable> {
546    let fleet_identity = Arc::as_ptr(fleet) as usize;
547    let mut tables = lock_unpoisoned(&MEMORY_TABLES);
548    tables.retain(|_, table| table.strong_count() > 0);
549    if let Some(table) = tables.get(&fleet_identity).and_then(Weak::upgrade) {
550        return table;
551    }
552    let table = Arc::new(MemoryTextTable::new(Arc::clone(fleet)));
553    tables.insert(fleet_identity, Arc::downgrade(&table));
554    table
555}
556
557#[cfg(unix)]
558pub(crate) struct ShmTextTable {
559    region: ShmRegion,
560    structural_lock: Mutex<()>
561}
562
563#[cfg(unix)]
564impl ShmTextTable {
565    fn open_or_create(name: &str) -> Result<Self> {
566        use std::{io, ptr};
567
568        let (region, _initialization_lock) =
569            ShmRegion::open_or_create_locked(name, shm_segment_size())?;
570        if region.created() {
571            unsafe {
572                ptr::write(region.as_ptr().cast::<TextStateHeader>(), TextStateHeader::new());
573                let slots = region.as_ptr().add(size_of::<TextStateHeader>());
574                ptr::write_bytes(slots, 0, CELL_TEXT_CAPACITY * size_of::<TextSlot>());
575            }
576        } else {
577            let header = unsafe { &*region.as_ptr().cast::<TextStateHeader>() };
578            if header.magic != TEXT_MAGIC {
579                return Err(Error::Io(io::Error::new(
580                    io::ErrorKind::InvalidData,
581                    format!(
582                        "SHM segment {name} has wrong magic 0x{:08X} (expected 0x{TEXT_MAGIC:08X})",
583                        header.magic
584                    )
585                )));
586            }
587            if header.version != TEXT_VERSION
588                || usize::from(header.header_size) != size_of::<TextStateHeader>()
589                || header.capacity as usize != CELL_TEXT_CAPACITY
590                || header.slot_size as usize != size_of::<TextSlot>()
591            {
592                return Err(Error::Io(io::Error::new(
593                    io::ErrorKind::InvalidData,
594                    format!("SHM segment {name} has an incompatible text-cell layout")
595                )));
596            }
597        }
598        Ok(Self { region, structural_lock: Mutex::new(()) })
599    }
600
601    fn header(&self) -> &TextStateHeader {
602        unsafe { &*self.region.as_ptr().cast::<TextStateHeader>() }
603    }
604
605    fn slots(&self) -> &[TextSlot] {
606        unsafe {
607            std::slice::from_raw_parts(
608                self.region.as_ptr().add(size_of::<TextStateHeader>()).cast::<TextSlot>(),
609                CELL_TEXT_CAPACITY
610            )
611        }
612    }
613
614    fn with_structure<T>(
615        &self,
616        operation: impl FnOnce(&[TextSlot], &AtomicU32) -> Result<T>
617    ) -> Result<T> {
618        let _local = lock_unpoisoned(&self.structural_lock);
619        let _process = self.region.lock_exclusive()?;
620        operation(self.slots(), &self.header().hint)
621    }
622}
623
624#[cfg(unix)]
625fn shm_segment_size() -> usize {
626    size_of::<TextStateHeader>() + CELL_TEXT_CAPACITY * size_of::<TextSlot>()
627}
628
629const _: () = assert!(CELL_TEXT_CAPACITY.is_power_of_two());
630const _: () = assert!(CELL_TEXT_MAX > 0 && CELL_TEXT_MAX <= u16::MAX as usize);
631const _: () = assert!(size_of::<TextStateHeader>() == 64);
632// 16 bytes of fixed fields, then the text, padded to the cache line.
633const _: () = assert!(size_of::<TextSlot>() == (16 + CELL_TEXT_MAX).next_multiple_of(64));