1use 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
61enum 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 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 pub len: AtomicU64,
108 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 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 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 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 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 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 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 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 #[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 fn write_slot(&self, i: usize, v: T) {
336 let slot = self.slot(i);
337 slot.version.fetch_add(1, Ordering::AcqRel);
339 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 slot.version.fetch_add(1, Ordering::AcqRel);
355 }
356
357 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 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); return Err(VecError::Full);
397 }
398 self.write_slot(idx, v);
399 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 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); return None;
432 }
433 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 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 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); 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 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); 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 pub fn clear(&self) {
489 if !self.mmap.is_writable() {
490 return;
491 }
492 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 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 pub fn for_each<F: FnMut(usize, &T)>(&self, f: F) {
528 self.for_each_range(0, self.len(), f);
529 }
530
531 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 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 #[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 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 first.extend_from_slice(&second);
618 assert_eq!(first, (0..40u64).collect::<Vec<_>>());
619 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); 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 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)] 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 #[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 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 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}