Skip to main content

subetha_cxc/
shared_vec.rs

1//! `SharedVec<T>` - cross-process bounded indexable sequence.
2//!
3//! Distinct from [`SharedRing`](crate::SharedRing) (FIFO; drain
4//! semantics): SharedVec is RANDOM-ACCESS, accumulates monotonically
5//! up to capacity, and supports `get(i)` for any prior index.
6//!
7//! # Layout
8//!
9//! Single MMF file:
10//!
11//! ```text
12//! +---------------------------+
13//! | VecHeader  (64B aligned)  |  magic, capacity, len, slot_size
14//! +---------------------------+
15//! | Slot[0]    (64B = cache)  |  version + payload[VEC_PAYLOAD_BYTES]
16//! | Slot[1]                   |
17//! | ...                       |
18//! | Slot[capacity - 1]        |
19//! +---------------------------+
20//! ```
21//!
22//! Each slot is its own SeqLock cell (same shape as SharedCell);
23//! per-slot writes never false-share because each is its own cache
24//! line.
25//!
26//! # Concurrency
27//!
28//! - `push_back`: atomic `len.fetch_add(1)` claims a slot index; if
29//!   it exceeds capacity, rollback with `fetch_sub(1)` and return
30//!   `Full`. On success, write the payload under the slot's SeqLock
31//!   (version bump odd → write → bump even).
32//! - `get(i)`: load `len` (Acquire). If `i >= len`, return None.
33//!   Otherwise SeqLock-read `slot[i]`: spin if version is odd
34//!   (writer in progress), reread on version change.
35//! - `pop_back`: `compare_exchange` on `len` to decrement; if
36//!   successful, read the now-popped slot's payload at the old
37//!   index. The slot bytes remain in place but are no longer
38//!   addressable via `len`-bounded access.
39//! - `set(i, v)`: bounds-check against `len`, then SeqLock-write.
40//! - `clear`: store `len = 0` (Release). Previously-pushed slot
41//!   payloads remain on disk but become unreachable through the
42//!   bounded indexing.
43//!
44//! # Capacity
45//!
46//! Fixed at create time. The MMF is pre-allocated to the full size;
47//! no resize-on-grow protocol. The unbounded variant (with
48//! coordinator-mediated MMF resize) is a separate primitive.
49
50use std::fs::{File, OpenOptions};
51use std::marker::PhantomData;
52use std::mem::size_of;
53use std::path::Path;
54use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
55
56use memmap2::{Mmap, MmapMut, MmapOptions};
57
58pub const VEC_MAGIC: u32 = 0x4150_5656;
59pub const VEC_PAYLOAD_BYTES: usize = 52;
60
61/// How this process mapped the file. `MmapMut` demands a read+write
62/// file handle, which a consumer holding read access alone cannot get.
63enum Mapping {
64    Writable(MmapMut),
65    ReadOnly(Mmap),
66}
67
68impl Mapping {
69    #[inline]
70    fn as_ptr(&self) -> *const u8 {
71        match self {
72            Mapping::Writable(m) => m.as_ptr(),
73            Mapping::ReadOnly(m) => m.as_ptr(),
74        }
75    }
76
77    #[inline]
78    fn is_writable(&self) -> bool {
79        matches!(self, Mapping::Writable(_))
80    }
81
82    /// A no-op on a read-only mapping, so a caller flushing on a timer
83    /// need not know which kind it holds.
84    fn flush(&self) -> Result<(), std::io::Error> {
85        match self {
86            Mapping::Writable(m) => m.flush(),
87            Mapping::ReadOnly(_) => Ok(()),
88        }
89    }
90
91    fn flush_async(&self) -> Result<(), std::io::Error> {
92        match self {
93            Mapping::Writable(m) => m.flush_async(),
94            Mapping::ReadOnly(_) => Ok(()),
95        }
96    }
97}
98
99#[repr(C, align(64))]
100pub struct VecHeader {
101    pub magic: u32,
102    pub slot_payload_size: u32,
103    pub capacity: u64,
104    /// Slots a reader may read: every index below this holds a fully
105    /// written payload. A pusher advances it only after its own slot is
106    /// written and every earlier reservation has committed.
107    pub len: AtomicU64,
108    /// Slots handed out to pushers, committed or still being written.
109    /// Ahead of `len` exactly while a push is in flight.
110    pub reserved: AtomicU64,
111    _pad: [u8; 32],
112}
113
114#[repr(C, align(64))]
115pub struct VecSlot {
116    pub version: AtomicU32,
117    _pad: [u8; 4],
118    pub payload: [u8; VEC_PAYLOAD_BYTES],
119}
120
121const _: () = {
122    assert!(size_of::<VecHeader>() == 64);
123    assert!(size_of::<VecSlot>() == 64);
124};
125
126pub const fn vec_file_size(capacity: usize) -> usize {
127    size_of::<VecHeader>() + capacity * size_of::<VecSlot>()
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum VecError {
132    Full,
133    OutOfBounds,
134    LayoutMismatch,
135    PayloadTooLarge,
136    /// The vec was opened read-only and something tried to write it.
137    ReadOnly,
138    IoError(std::io::ErrorKind),
139}
140
141impl From<std::io::Error> for VecError {
142    fn from(e: std::io::Error) -> Self { Self::IoError(e.kind()) }
143}
144
145pub struct SharedVec<T: Copy + 'static> {
146    _file: File,
147    mmap: Mapping,
148    capacity: usize,
149    _phantom: PhantomData<T>,
150    header_sidecar: subetha_core::HandshakeHeader,
151    ring_sidecar: Box<subetha_core::ObservationRing>,
152}
153
154unsafe impl<T: Copy + Send + 'static> Send for SharedVec<T> {}
155unsafe impl<T: Copy + Sync + 'static> Sync for SharedVec<T> {}
156
157impl<T: Copy + Send + Sync + 'static> subetha_sidecar::AdaptiveInstance for SharedVec<T> {
158    fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
159    fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
160    fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
161        Box::new(subetha_sidecar::NoMigrationPolicy)
162    }
163}
164
165impl<T: Copy + 'static> SharedVec<T> {
166    /// Obtain the vec at `path`, initializing an empty one if the path
167    /// does not yet exist and attaching to it if it does. Attaching
168    /// leaves live elements and `len` in place; a region built with a
169    /// different capacity is a `LayoutMismatch`.
170    /// [`reset`](Self::reset) reinitializes.
171    pub fn create(
172        path: impl AsRef<Path>, capacity: usize,
173    ) -> Result<Self, VecError> {
174        if size_of::<T>() > VEC_PAYLOAD_BYTES {
175            return Err(VecError::PayloadTooLarge);
176        }
177        assert!(capacity >= 1);
178        let (file, mmap) = crate::mmf_attach::create_or_attach(
179            path.as_ref(),
180            vec_file_size(capacity),
181            |ptr| unsafe { Self::init_region(ptr, capacity) },
182            |ptr| unsafe { (*(ptr as *const VecHeader)).magic == VEC_MAGIC },
183        )?;
184        let this = Self {
185            _file: file, mmap: Mapping::Writable(mmap), capacity, _phantom: PhantomData,
186            header_sidecar: subetha_core::HandshakeHeader::new(),
187            ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
188        };
189        this.validate(capacity)?;
190        Ok(this)
191    }
192
193    /// Truncate the vec at `path` and initialize an empty one,
194    /// discarding every element live peers share. For a caller that
195    /// knows it owns the path.
196    pub fn reset(
197        path: impl AsRef<Path>, capacity: usize,
198    ) -> Result<Self, VecError> {
199        if size_of::<T>() > VEC_PAYLOAD_BYTES {
200            return Err(VecError::PayloadTooLarge);
201        }
202        assert!(capacity >= 1);
203        let (file, mmap) = crate::mmf_attach::reset(path.as_ref(), vec_file_size(capacity), |ptr| unsafe {
204            Self::init_region(ptr, capacity)
205        })?;
206        Ok(Self {
207            _file: file, mmap: Mapping::Writable(mmap), capacity, _phantom: PhantomData,
208            header_sidecar: subetha_core::HandshakeHeader::new(),
209            ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
210        })
211    }
212
213    /// Lay out an empty vec: sizes first, magic last, because attachers
214    /// spin on it. The zeroed region is already the empty slot array
215    /// (version 0) and `len` 0.
216    ///
217    /// # Safety
218    /// `ptr` addresses at least `vec_file_size(capacity)` writable
219    /// zeroed bytes.
220    unsafe fn init_region(ptr: *mut u8, capacity: usize) {
221        let hdr = ptr as *mut VecHeader;
222        unsafe {
223            (*hdr).slot_payload_size = VEC_PAYLOAD_BYTES as u32;
224            (*hdr).capacity = capacity as u64;
225            std::ptr::write_volatile(&raw mut (*hdr).magic, VEC_MAGIC);
226        }
227    }
228
229    pub fn open(
230        path: impl AsRef<Path>, expected_capacity: usize,
231    ) -> Result<Self, VecError> {
232        if size_of::<T>() > VEC_PAYLOAD_BYTES {
233            return Err(VecError::PayloadTooLarge);
234        }
235        let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
236        let total = vec_file_size(expected_capacity);
237        if file.metadata()?.len() < total as u64 {
238            return Err(VecError::LayoutMismatch);
239        }
240        let mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
241        let this = Self {
242            _file: file, mmap: Mapping::Writable(mmap), capacity: expected_capacity,
243            _phantom: PhantomData,
244            header_sidecar: subetha_core::HandshakeHeader::new(),
245            ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
246        };
247        this.validate(expected_capacity)?;
248        Ok(this)
249    }
250
251    /// Open a vec this process may only read.
252    ///
253    /// [`open`](Self::open) needs a read+write file handle, which a
254    /// consumer of a privileged producer's vec does not have - granting
255    /// it would let any reader corrupt the state for all of them.
256    /// Reads behave identically, under the same per-slot SeqLock;
257    /// writes return [`VecError::ReadOnly`].
258    pub fn open_read_only(
259        path: impl AsRef<Path>, expected_capacity: usize,
260    ) -> Result<Self, VecError> {
261        if size_of::<T>() > VEC_PAYLOAD_BYTES {
262            return Err(VecError::PayloadTooLarge);
263        }
264        let file = OpenOptions::new().read(true).open(path.as_ref())?;
265        let total = vec_file_size(expected_capacity);
266        if file.metadata()?.len() < total as u64 {
267            return Err(VecError::LayoutMismatch);
268        }
269        let mmap = unsafe { MmapOptions::new().len(total).map(&file)? };
270        let this = Self {
271            _file: file, mmap: Mapping::ReadOnly(mmap), capacity: expected_capacity,
272            _phantom: PhantomData,
273            header_sidecar: subetha_core::HandshakeHeader::new(),
274            ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
275        };
276        this.validate(expected_capacity)?;
277        Ok(this)
278    }
279
280    /// Whether the header on disk is the one this mapping expects.
281    fn validate(&self, expected_capacity: usize) -> Result<(), VecError> {
282        let hdr = self.header();
283        if hdr.magic != VEC_MAGIC || hdr.capacity != expected_capacity as u64 {
284            return Err(VecError::LayoutMismatch);
285        }
286        // A region written before `reserved` existed carries zero there
287        // while holding elements, and the allocator must never sit below
288        // the published length or a pusher would hand out a live slot and
289        // then wait to publish an index already behind it. Raising it is
290        // safe on a healthy region too, where the two are equal at rest.
291        if self.mmap.is_writable() {
292            let len = hdr.len.load(Ordering::Acquire);
293            let mut cur = hdr.reserved.load(Ordering::Acquire);
294            while cur < len {
295                match hdr.reserved.compare_exchange_weak(
296                    cur, len, Ordering::AcqRel, Ordering::Acquire,
297                ) {
298                    Ok(_) => break,
299                    Err(seen) => cur = seen,
300                }
301            }
302        }
303        Ok(())
304    }
305
306    /// Whether this mapping may be written.
307    #[inline]
308    pub fn is_writable(&self) -> bool {
309        self.mmap.is_writable()
310    }
311
312    #[inline]
313    pub fn capacity(&self) -> usize { self.capacity }
314
315    #[inline]
316    pub fn len(&self) -> usize {
317        self.header().len.load(Ordering::Acquire) as usize
318    }
319
320    #[inline]
321    pub fn is_empty(&self) -> bool { self.len() == 0 }
322
323    fn header(&self) -> &VecHeader {
324        unsafe { &*(self.mmap.as_ptr() as *const VecHeader) }
325    }
326
327    fn slot(&self, i: usize) -> &VecSlot {
328        assert!(i < self.capacity, "slot index {i} out of bounds for cap {}", self.capacity);
329        let base = unsafe { self.mmap.as_ptr().add(size_of::<VecHeader>()) };
330        unsafe { &*(base.add(i * size_of::<VecSlot>()) as *const VecSlot) }
331    }
332
333    /// SeqLock write of a payload into a slot. Caller is responsible
334    /// for ensuring `i < capacity`.
335    fn write_slot(&self, i: usize, v: T) {
336        let slot = self.slot(i);
337        // Bump version odd; subsequent readers will spin.
338        slot.version.fetch_add(1, Ordering::AcqRel);
339        // Memcpy the value.
340        let dst = unsafe {
341            let base = self.mmap.as_ptr().add(size_of::<VecHeader>())
342                .add(i * size_of::<VecSlot>())
343                .add(std::mem::offset_of!(VecSlot, payload));
344            base as *mut u8
345        };
346        unsafe {
347            std::ptr::copy_nonoverlapping(
348                &v as *const T as *const u8,
349                dst,
350                size_of::<T>(),
351            );
352        }
353        // Bump version even; readers resume.
354        slot.version.fetch_add(1, Ordering::AcqRel);
355    }
356
357    /// SeqLock read of a slot. Spins if version is odd; rereads on
358    /// version change.
359    fn read_slot(&self, i: usize) -> T {
360        let slot = self.slot(i);
361        loop {
362            let v1 = slot.version.load(Ordering::Acquire);
363            if v1 & 1 != 0 {
364                std::hint::spin_loop();
365                continue;
366            }
367            let mut out = std::mem::MaybeUninit::<T>::uninit();
368            let src = unsafe {
369                self.mmap.as_ptr().add(size_of::<VecHeader>())
370                    .add(i * size_of::<VecSlot>())
371                    .add(std::mem::offset_of!(VecSlot, payload))
372            };
373            unsafe {
374                std::ptr::copy_nonoverlapping(
375                    src, out.as_mut_ptr() as *mut u8, size_of::<T>(),
376                );
377            }
378            let v2 = slot.version.load(Ordering::Acquire);
379            if v1 == v2 {
380                return unsafe { out.assume_init() };
381            }
382        }
383    }
384
385    /// Append a value. Returns the index it landed at.
386    /// Returns `Err(Full)` when the vec is at capacity.
387    pub fn push_back(&self, v: T) -> Result<usize, VecError> {
388        if !self.mmap.is_writable() {
389            return Err(VecError::ReadOnly);
390        }
391        let idx = self.header().reserved.fetch_add(1, Ordering::AcqRel) as usize;
392        if idx >= self.capacity {
393            self.header().reserved.fetch_sub(1, Ordering::AcqRel);
394            self.ring_sidecar
395                .push_op(crate::sidecar_ops::ordered::OP_INSERT, 1); // full
396            return Err(VecError::Full);
397        }
398        self.write_slot(idx, v);
399        // Publish only once this slot is written AND every earlier
400        // reservation has published, so `len` never covers a slot a
401        // reader would find half-written. Concurrent pushers spin here
402        // for as long as a predecessor's write takes.
403        while self
404            .header()
405            .len
406            .compare_exchange_weak(
407                idx as u64,
408                idx as u64 + 1,
409                Ordering::Release,
410                Ordering::Relaxed,
411            )
412            .is_err()
413        {
414            std::hint::spin_loop();
415        }
416        self.ring_sidecar
417            .push_op(crate::sidecar_ops::ordered::OP_INSERT, 0);
418        Ok(idx)
419    }
420
421    /// Remove and return the last element. Returns None when empty.
422    pub fn pop_back(&self) -> Option<T> {
423        if !self.mmap.is_writable() {
424            return None;
425        }
426        loop {
427            let cur = self.header().len.load(Ordering::Acquire);
428            if cur == 0 {
429                self.ring_sidecar
430                    .push_op(crate::sidecar_ops::ordered::OP_POP, 2); // empty
431                return None;
432            }
433            // Claim from the allocator, and only while it agrees with the
434            // published length: a reservation in flight owns the slot
435            // above `len`, and taking that slot would leave the pusher
436            // waiting to publish an index that no longer exists.
437            if self.header().reserved.load(Ordering::Acquire) != cur {
438                std::hint::spin_loop();
439                continue;
440            }
441            let new = cur - 1;
442            if self.header().reserved.compare_exchange(
443                cur, new, Ordering::AcqRel, Ordering::Acquire,
444            ).is_ok() {
445                let v = self.read_slot(new as usize);
446                // Unpublish after the read, so a reader never sees a
447                // length covering a slot this pop is still reading.
448                self.header().len.store(new, Ordering::Release);
449                self.ring_sidecar
450                    .push_op(crate::sidecar_ops::ordered::OP_POP, 0);
451                return Some(v);
452            }
453        }
454    }
455
456    /// Read the value at index `i`. Returns None when `i >= len`.
457    pub fn get(&self, i: usize) -> Option<T> {
458        if i >= self.len() {
459            self.ring_sidecar
460                .push_op(crate::sidecar_ops::ordered::OP_GET, 2); // out of bounds / absent
461            return None;
462        }
463        let v = self.read_slot(i);
464        self.ring_sidecar
465            .push_op(crate::sidecar_ops::ordered::OP_GET, 0);
466        Some(v)
467    }
468
469    /// Overwrite the value at index `i`. Returns `Err(OutOfBounds)`
470    /// when `i >= len`.
471    pub fn set(&self, i: usize, v: T) -> Result<(), VecError> {
472        if !self.mmap.is_writable() {
473            return Err(VecError::ReadOnly);
474        }
475        if i >= self.len() {
476            self.ring_sidecar
477                .push_op(crate::sidecar_ops::ordered::OP_INSERT, 1); // out of bounds (positional write rejected)
478            return Err(VecError::OutOfBounds);
479        }
480        self.write_slot(i, v);
481        self.ring_sidecar
482            .push_op(crate::sidecar_ops::ordered::OP_INSERT, 0);
483        Ok(())
484    }
485
486    /// Clear the vec by resetting len to 0. Slot payloads are not
487    /// zeroed; they become unreachable through bounded indexing.
488    pub fn clear(&self) {
489        if !self.mmap.is_writable() {
490            return;
491        }
492        // Length first, so no reader is left addressing a slot the
493        // allocator has already handed back out.
494        self.header().len.store(0, Ordering::Release);
495        self.header().reserved.store(0, Ordering::Release);
496        self.ring_sidecar
497            .push_op(crate::sidecar_ops::ordered::OP_REMOVE, 0);
498    }
499
500    /// Snapshot all current values into a Vec. Best-effort: under
501    /// concurrent writers, the snapshot is a consistent prefix at
502    /// the moment of the `len` load, with each slot read under its
503    /// own SeqLock.
504    pub fn snapshot(&self) -> Vec<T> {
505        let n = self.len();
506        let mut out = Vec::with_capacity(n);
507        for i in 0..n {
508            out.push(self.read_slot(i));
509        }
510        self.ring_sidecar
511            .push_op(crate::sidecar_ops::ordered::OP_ITER, 0);
512        out
513    }
514
515    /// Read every live slot in order and hand each to `f`, without
516    /// building a `Vec`.
517    ///
518    /// For a consumer that looks at every element rather than keeping
519    /// them. [`snapshot`](Self::snapshot) costs an allocation the size
520    /// of the data plus a pass to fill it; this costs neither.
521    ///
522    /// Each slot is read through its own SeqLock into a local, as `get`
523    /// does, so `f` never sees a torn value. A reference into the
524    /// mapping would let a writer change the bytes under the reader.
525    ///
526    /// `f` is called in index order, over `len` as of entry.
527    pub fn for_each<F: FnMut(usize, &T)>(&self, f: F) {
528        self.for_each_range(0, self.len(), f);
529    }
530
531    /// [`for_each`](Self::for_each) over `start..end`, clamped to the
532    /// live length. The range form lets workers take disjoint spans
533    /// without materializing their share first.
534    pub fn for_each_range<F: FnMut(usize, &T)>(&self, start: usize, end: usize, mut f: F) {
535        let end = end.min(self.len());
536        for i in start..end {
537            let v = self.read_slot(i);
538            f(i, &v);
539        }
540        self.ring_sidecar
541            .push_op(crate::sidecar_ops::ordered::OP_ITER, 0);
542    }
543
544    pub fn flush(&self) -> Result<(), VecError> {
545        self.mmap.flush()?;
546        Ok(())
547    }
548
549    /// Non-blocking flush: schedules a writeback via the OS.
550    /// Note: Windows is only partially async (sync to page cache,
551    /// not to disk).
552    pub fn flush_async(&self) -> Result<(), VecError> {
553        self.mmap.flush_async()?;
554        Ok(())
555    }
556}
557
558#[cfg(test)]
559mod tests {
560    use super::*;
561    use std::sync::Arc;
562    use std::thread;
563
564    /// A second create attaches with live elements in place; reset is
565    /// what strips them.
566    #[test]
567    fn second_create_attaches_and_keeps_elements() {
568        let p = tmp("attach");
569        std::fs::remove_file(&p).ok();
570        let v = SharedVec::<u64>::create(&p, 16).unwrap();
571        v.push_back(777).unwrap();
572
573        let v2 = SharedVec::<u64>::create(&p, 16).unwrap();
574        assert_eq!(v2.len(), 1, "attach emptied a live vec");
575        assert_eq!(v2.get(0), Some(777));
576        assert!(matches!(
577            SharedVec::<u64>::create(&p, 8),
578            Err(VecError::LayoutMismatch),
579        ));
580
581        // Windows refuses to truncate a mapped file, so every handle goes
582        // before the reset.
583        drop(v);
584        drop(v2);
585        let fresh = SharedVec::<u64>::reset(&p, 16).unwrap();
586        assert_eq!(fresh.len(), 0, "reset kept an element");
587        drop(fresh);
588        std::fs::remove_file(&p).ok();
589    }
590
591    #[test]
592    fn for_each_visits_every_live_slot_in_order() {
593        let p = tmp("foreach");
594        let v = SharedVec::<u64>::create(&p, 256).unwrap();
595        for i in 0..100u64 {
596            v.push_back(i * 3).unwrap();
597        }
598        let mut seen = Vec::new();
599        v.for_each(|i, x| seen.push((i, *x)));
600        assert_eq!(seen.len(), 100, "capacity beyond len is not visited");
601        assert!(seen.iter().enumerate().all(|(n, (i, x))| *i == n && *x == n as u64 * 3));
602        std::fs::remove_file(&p).ok();
603    }
604
605    #[test]
606    fn for_each_range_is_clamped_and_disjoint() {
607        let p = tmp("foreach-range");
608        let v = SharedVec::<u64>::create(&p, 64).unwrap();
609        for i in 0..40u64 {
610            v.push_back(i).unwrap();
611        }
612        let mut first = Vec::new();
613        let mut second = Vec::new();
614        v.for_each_range(0, 20, |_, x| first.push(*x));
615        v.for_each_range(20, 40, |_, x| second.push(*x));
616        // Together the spans are the whole vec, once each.
617        first.extend_from_slice(&second);
618        assert_eq!(first, (0..40u64).collect::<Vec<_>>());
619        // An end past len is clamped, not out of bounds.
620        let mut over = Vec::new();
621        v.for_each_range(30, 9999, |_, x| over.push(*x));
622        assert_eq!(over.len(), 10);
623        std::fs::remove_file(&p).ok();
624    }
625
626    #[test]
627    fn for_each_agrees_with_snapshot() {
628        let p = tmp("foreach-vs-snapshot");
629        let v = SharedVec::<u64>::create(&p, 512).unwrap();
630        for i in 0..300u64 {
631            v.push_back(i ^ 0xa5a5).unwrap();
632        }
633        let mut walked = Vec::new();
634        v.for_each(|_, x| walked.push(*x));
635        assert_eq!(walked, v.snapshot());
636        std::fs::remove_file(&p).ok();
637    }
638
639    #[test]
640    fn a_read_only_vec_reads_the_same_and_refuses_writes() {
641        let p = tmp("readonly");
642        {
643            let w = SharedVec::<u64>::create(&p, 64).unwrap();
644            for i in 0..10u64 {
645                w.push_back(i * 7).unwrap();
646            }
647            w.flush().unwrap();
648        }
649        let r = SharedVec::<u64>::open_read_only(&p, 64).unwrap();
650        assert!(!r.is_writable());
651        assert_eq!(r.len(), 10);
652        assert_eq!(r.get(3), Some(21));
653        assert_eq!(r.snapshot(), (0..10u64).map(|i| i * 7).collect::<Vec<_>>());
654        assert_eq!(r.push_back(1), Err(VecError::ReadOnly));
655        assert_eq!(r.set(0, 1), Err(VecError::ReadOnly));
656        assert_eq!(r.pop_back(), None);
657        r.clear();
658        assert_eq!(r.len(), 10, "clear on a read-only vec changes nothing");
659        r.flush().unwrap();
660        std::fs::remove_file(&p).ok();
661    }
662
663    #[test]
664    fn a_read_only_open_still_validates_the_header() {
665        let p = tmp("readonly-mismatch");
666        {
667            let w = SharedVec::<u64>::create(&p, 64).unwrap();
668            w.push_back(1).unwrap();
669            w.flush().unwrap();
670        }
671        assert_eq!(
672            SharedVec::<u64>::open_read_only(&p, 32).err(),
673            Some(VecError::LayoutMismatch)
674        );
675        std::fs::remove_file(&p).ok();
676    }
677
678    fn tmp(name: &str) -> std::path::PathBuf {
679        let mut p = std::env::temp_dir();
680        let pid = std::process::id();
681        p.push(format!("subetha-vec-{name}-{pid}.bin"));
682        p
683    }
684
685    #[test]
686    fn create_initial_state_is_empty() {
687        let p = tmp("init");
688        let v: SharedVec<u32> = SharedVec::create(&p, 16).unwrap();
689        assert_eq!(v.capacity(), 16);
690        assert_eq!(v.len(), 0);
691        assert!(v.is_empty());
692        assert_eq!(v.get(0), None);
693        std::fs::remove_file(&p).ok();
694    }
695
696    #[test]
697    fn push_back_advances_len_and_get_round_trip() {
698        let p = tmp("push");
699        let v: SharedVec<u32> = SharedVec::create(&p, 8).unwrap();
700        for i in 0..5u32 {
701            let idx = v.push_back(i * 10).unwrap();
702            assert_eq!(idx, i as usize);
703        }
704        assert_eq!(v.len(), 5);
705        for i in 0..5 {
706            assert_eq!(v.get(i), Some((i as u32) * 10));
707        }
708        assert_eq!(v.get(5), None);
709        std::fs::remove_file(&p).ok();
710    }
711
712    #[test]
713    fn full_capacity_returns_error() {
714        let p = tmp("full");
715        let v: SharedVec<u32> = SharedVec::create(&p, 4).unwrap();
716        for i in 0..4u32 { v.push_back(i).unwrap(); }
717        assert_eq!(v.push_back(99).err(), Some(VecError::Full));
718        assert_eq!(v.len(), 4);  // rolled back
719        std::fs::remove_file(&p).ok();
720    }
721
722    #[test]
723    fn pop_back_returns_last_then_none() {
724        let p = tmp("pop");
725        let v: SharedVec<u32> = SharedVec::create(&p, 8).unwrap();
726        v.push_back(10).unwrap();
727        v.push_back(20).unwrap();
728        v.push_back(30).unwrap();
729        assert_eq!(v.pop_back(), Some(30));
730        assert_eq!(v.pop_back(), Some(20));
731        assert_eq!(v.len(), 1);
732        assert_eq!(v.pop_back(), Some(10));
733        assert_eq!(v.pop_back(), None);
734        std::fs::remove_file(&p).ok();
735    }
736
737    #[test]
738    fn set_replaces_value_at_index() {
739        let p = tmp("set");
740        let v: SharedVec<u32> = SharedVec::create(&p, 8).unwrap();
741        v.push_back(1).unwrap();
742        v.push_back(2).unwrap();
743        v.set(0, 100).unwrap();
744        assert_eq!(v.get(0), Some(100));
745        assert_eq!(v.get(1), Some(2));
746        assert_eq!(v.set(2, 200).err(), Some(VecError::OutOfBounds));
747        std::fs::remove_file(&p).ok();
748    }
749
750    #[test]
751    fn clear_resets_len_to_zero() {
752        let p = tmp("clear");
753        let v: SharedVec<u32> = SharedVec::create(&p, 8).unwrap();
754        for i in 0..5u32 { v.push_back(i).unwrap(); }
755        assert_eq!(v.len(), 5);
756        v.clear();
757        assert_eq!(v.len(), 0);
758        assert_eq!(v.get(0), None);
759        // After clear, push works fresh.
760        v.push_back(42).unwrap();
761        assert_eq!(v.get(0), Some(42));
762        std::fs::remove_file(&p).ok();
763    }
764
765    #[test]
766    fn snapshot_returns_consistent_prefix() {
767        let p = tmp("snapshot");
768        let v: SharedVec<u32> = SharedVec::create(&p, 16).unwrap();
769        for i in 0..7u32 { v.push_back(i + 100).unwrap(); }
770        let snap = v.snapshot();
771        assert_eq!(snap, vec![100, 101, 102, 103, 104, 105, 106]);
772        std::fs::remove_file(&p).ok();
773    }
774
775    #[test]
776    fn cross_handle_visibility() {
777        let p = tmp("cross-handle");
778        let writer: SharedVec<u32> = SharedVec::create(&p, 8).unwrap();
779        let reader: SharedVec<u32> = SharedVec::open(&p, 8).unwrap();
780        writer.push_back(777).unwrap();
781        assert_eq!(reader.get(0), Some(777));
782        reader.push_back(888).unwrap();
783        assert_eq!(writer.get(1), Some(888));
784        assert_eq!(writer.len(), 2);
785        std::fs::remove_file(&p).ok();
786    }
787
788    #[test]
789    fn concurrent_pushers_all_land_at_distinct_indices() {
790        let p = tmp("concurrent");
791        let v: Arc<SharedVec<u32>> = Arc::new(SharedVec::create(&p, 1024).unwrap());
792        let n_threads = 4;
793        let per_thread = 50u32;
794        let mut handles = vec![];
795        for t in 0..n_threads {
796            let v = v.clone();
797            handles.push(thread::spawn(move || {
798                let mut indices = vec![];
799                for i in 0..per_thread {
800                    let val = (t as u32) * per_thread + i;
801                    let idx = v.push_back(val).unwrap();
802                    indices.push(idx);
803                }
804                indices
805            }));
806        }
807        let mut all_indices: Vec<usize> = handles.into_iter()
808            .flat_map(|h| h.join().unwrap())
809            .collect();
810        all_indices.sort();
811        for (expected, actual) in all_indices.iter().enumerate() {
812            assert_eq!(*actual, expected,
813                "indices must form a contiguous 0..N sequence");
814        }
815        assert_eq!(v.len(), (n_threads * per_thread as usize));
816        std::fs::remove_file(&p).ok();
817    }
818
819    #[test]
820    fn payload_too_large_at_create() {
821        #[allow(dead_code)] // size_of<Big> is the test signal, not the field
822        struct Big([u8; VEC_PAYLOAD_BYTES + 1]);
823        impl Copy for Big {}
824        impl Clone for Big { fn clone(&self) -> Self { *self } }
825        let p = tmp("too-large");
826        let r = SharedVec::<Big>::create(&p, 4);
827        assert_eq!(r.err(), Some(VecError::PayloadTooLarge));
828        std::fs::remove_file(&p).ok();
829    }
830
831    #[test]
832    fn struct_payload_round_trip() {
833        #[derive(Clone, Copy, Debug, PartialEq)]
834        #[repr(C)]
835        struct Point { x: f64, y: f64, z: f64 }
836        let p = tmp("struct");
837        let v: SharedVec<Point> = SharedVec::create(&p, 8).unwrap();
838        v.push_back(Point { x: 1.0, y: 2.0, z: 3.0 }).unwrap();
839        v.push_back(Point { x: -1.5, y: 0.0, z: 7.25 }).unwrap();
840        assert_eq!(v.get(0), Some(Point { x: 1.0, y: 2.0, z: 3.0 }));
841        assert_eq!(v.get(1), Some(Point { x: -1.5, y: 0.0, z: 7.25 }));
842        std::fs::remove_file(&p).ok();
843    }
844
845    #[test]
846    fn disk_persistence_data_survives_reopen() {
847        let p = tmp("disk");
848        {
849            let v: SharedVec<u32> = SharedVec::create(&p, 8).unwrap();
850            for i in 0..4u32 { v.push_back(i * 100).unwrap(); }
851            v.flush().unwrap();
852        }
853        let v2: SharedVec<u32> = SharedVec::open(&p, 8).unwrap();
854        assert_eq!(v2.len(), 4);
855        for i in 0..4 {
856            assert_eq!(v2.get(i), Some((i as u32) * 100));
857        }
858        std::fs::remove_file(&p).ok();
859    }
860
861    /// `len` covers only committed slots. Many pushers race readers
862    /// that read every visible slot on every pass, so a length
863    /// published ahead of its payload shows up as a zero where the
864    /// sentinel belongs. The window between reserving an index and
865    /// writing it is only a few instructions wide, so both sides are
866    /// hammered.
867    #[test]
868    fn length_never_covers_a_slot_still_being_written() {
869        let p = tmp("commit-order");
870        std::fs::remove_file(&p).ok();
871        let v: Arc<SharedVec<u32>> = Arc::new(SharedVec::create(&p, 4096).unwrap());
872        let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
873
874        // Every pusher writes the same sentinel. A slot the length covers
875        // before its payload landed still reads as the zeroed region, so
876        // any zero a reader sees is the race.
877        const WRITTEN: u32 = 0xABCD_EF01;
878
879        let readers: Vec<_> = (0..3)
880            .map(|_| {
881                let v = v.clone();
882                let stop = stop.clone();
883                thread::spawn(move || {
884                    while !stop.load(Ordering::Acquire) {
885                        let n = v.len();
886                        for i in 0..n {
887                            assert_eq!(
888                                v.get(i),
889                                Some(WRITTEN),
890                                "slot {i} was visible before it was written",
891                            );
892                        }
893                    }
894                })
895            })
896            .collect();
897
898        let writers: Vec<_> = (0..4)
899            .map(|_| {
900                let v = v.clone();
901                thread::spawn(move || {
902                    while let Ok(idx) = v.push_back(WRITTEN) {
903                        if idx >= 4000 {
904                            break;
905                        }
906                    }
907                })
908            })
909            .collect();
910
911        for w in writers {
912            w.join().unwrap();
913        }
914        stop.store(true, Ordering::Release);
915        for r in readers {
916            r.join().unwrap();
917        }
918        std::fs::remove_file(&p).ok();
919    }
920
921    #[test]
922    fn concurrent_reader_during_writes_sees_consistent_data() {
923        let p = tmp("read-during-write");
924        let v: Arc<SharedVec<u32>> = Arc::new(SharedVec::create(&p, 256).unwrap());
925        let v_w = v.clone();
926        let writer = thread::spawn(move || {
927            for i in 0..100u32 {
928                v_w.push_back(i).unwrap();
929            }
930        });
931        let v_r = v.clone();
932        let reader = thread::spawn(move || {
933            let mut last_len = 0;
934            loop {
935                let n = v_r.len();
936                if n == 100 { break; }
937                // Read every visible slot; values must equal index.
938                for i in last_len..n {
939                    let got = v_r.get(i);
940                    assert_eq!(got, Some(i as u32),
941                        "slot {i} should hold {i}, got {got:?}");
942                }
943                last_len = n;
944                std::thread::yield_now();
945            }
946        });
947        writer.join().unwrap();
948        reader.join().unwrap();
949        std::fs::remove_file(&p).ok();
950    }
951}