1use std::fs::OpenOptions;
57use std::marker::PhantomData;
58use std::mem::size_of;
59use std::path::{Path, PathBuf};
60use std::sync::atomic::{AtomicU64, Ordering};
61
62use memmap2::{MmapMut, MmapOptions};
63use parking_lot::RwLock;
64
65use crate::shared_hash_map::{MapError, SharedHashMap};
66use crate::shared_vec::SharedVec;
67
68pub const UNIVERSAL_MAGIC: u32 = 0x4150_5556;
69
70#[repr(u8)]
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum Strategy {
74 Vec = 0,
75 Map = 1,
76}
77
78impl Strategy {
79 fn from_u8(b: u8) -> Option<Self> {
80 match b {
81 0 => Some(Self::Vec),
82 1 => Some(Self::Map),
83 _ => None,
84 }
85 }
86
87 fn file_suffix(self) -> &'static str {
88 match self {
89 Self::Vec => "vec",
90 Self::Map => "map",
91 }
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum UniversalError {
97 InvalidStrategy,
98 IoError(std::io::ErrorKind),
99 LayoutMismatch,
100 VecError,
101 MapError(MapError),
102 Full,
103 VersionExhausted,
108}
109
110impl From<std::io::Error> for UniversalError {
111 fn from(e: std::io::Error) -> Self { Self::IoError(e.kind()) }
112}
113impl From<MapError> for UniversalError {
114 fn from(e: MapError) -> Self {
115 match e {
116 MapError::Full => Self::Full,
117 other => Self::MapError(other),
118 }
119 }
120}
121
122#[repr(C, align(64))]
123pub struct UniversalHeader {
124 pub magic: u32,
125 pub capacity: u32,
126 pub state: AtomicU64,
140 pub insert_count: AtomicU64,
143 pub contains_count: AtomicU64,
145 _pad: [u8; 32],
146}
147
148const _: () = {
149 assert!(size_of::<UniversalHeader>() == 64);
150};
151
152#[inline]
153fn pack(version: u32, generation: u16, strategy: u8) -> u64 {
154 ((version as u64) << 32) | ((generation as u64) << 16) | (strategy as u64)
155}
156#[inline]
157fn unpack(v: u64) -> (u32, u16, u8) {
158 let version = (v >> 32) as u32;
159 let generation = ((v >> 16) & 0xFFFF) as u16;
160 let strategy = (v & 0xFF) as u8;
161 (version, generation, strategy)
162}
163
164enum Backing<T: Copy + Eq + 'static> {
165 Vec(SharedVec<T>),
166 Map(SharedHashMap<T, ()>),
167}
168
169pub struct SharedUniversal<T: Copy + Eq + 'static> {
170 base: PathBuf,
171 capacity: usize,
172 _state_file: std::fs::File,
173 state_mmap: MmapMut,
174 backing: RwLock<(u32, u16, Backing<T>)>,
178 _phantom: PhantomData<T>,
179 header_sidecar: subetha_core::HandshakeHeader,
180 ring_sidecar: Box<subetha_core::ObservationRing>,
181}
182
183unsafe impl<T: Copy + Eq + Send + 'static> Send for SharedUniversal<T> {}
184unsafe impl<T: Copy + Eq + Sync + 'static> Sync for SharedUniversal<T> {}
185
186impl<T: Copy + Eq + Send + Sync + 'static> subetha_sidecar::AdaptiveInstance for SharedUniversal<T> {
187 fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
188 fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
189 fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
190 Box::new(subetha_sidecar::NoMigrationPolicy)
191 }
192}
193
194impl<T: Copy + Eq + 'static> SharedUniversal<T> {
195 fn state_path(base: &Path) -> PathBuf {
196 let mut p = base.to_path_buf();
197 let stem = p.file_name().map(|s| s.to_string_lossy().to_string()).unwrap_or_default();
198 p.set_file_name(format!("{stem}.state.bin"));
199 p
200 }
201
202 fn backing_path(base: &Path, generation: u16, version: u32, strategy: Strategy) -> PathBuf {
203 let mut p = base.to_path_buf();
204 let stem = p.file_name().map(|s| s.to_string_lossy().to_string()).unwrap_or_default();
205 p.set_file_name(format!(
206 "{stem}-g{generation}-v{version}-{}.bin",
207 strategy.file_suffix(),
208 ));
209 p
210 }
211
212 pub fn create(base: impl AsRef<Path>, capacity: usize) -> Result<Self, UniversalError> {
215 let base = base.as_ref().to_path_buf();
216 assert!(capacity >= 1);
217 let state_p = Self::state_path(&base);
218 let state_file = OpenOptions::new()
219 .read(true).write(true).create(true).truncate(true)
220 .open(&state_p)?;
221 state_file.set_len(size_of::<UniversalHeader>() as u64)?;
222 let mut mmap = unsafe { MmapOptions::new().len(size_of::<UniversalHeader>()).map_mut(&state_file)? };
223 let hdr = mmap.as_mut_ptr() as *mut UniversalHeader;
224 unsafe {
225 std::ptr::write_bytes(hdr as *mut u8, 0, size_of::<UniversalHeader>());
226 (*hdr).magic = UNIVERSAL_MAGIC;
227 (*hdr).capacity = capacity as u32;
228 (*hdr).state.store(pack(0, 0, Strategy::Vec as u8), Ordering::Release);
229 }
230 let backing_p = Self::backing_path(&base, 0, 0, Strategy::Vec);
231 let vec: SharedVec<T> = SharedVec::create(&backing_p, capacity)
232 .map_err(|_| UniversalError::VecError)?;
233 Ok(Self {
234 base, capacity,
235 _state_file: state_file,
236 state_mmap: mmap,
237 backing: RwLock::new((0, 0, Backing::Vec(vec))),
238 _phantom: PhantomData,
239 header_sidecar: subetha_core::HandshakeHeader::new(),
240 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
241 })
242 }
243
244 pub fn open(base: impl AsRef<Path>, capacity: usize) -> Result<Self, UniversalError> {
248 let base = base.as_ref().to_path_buf();
249 let state_p = Self::state_path(&base);
250 let state_file = OpenOptions::new().read(true).write(true).open(&state_p)?;
251 if state_file.metadata()?.len() < size_of::<UniversalHeader>() as u64 {
252 return Err(UniversalError::LayoutMismatch);
253 }
254 let mmap = unsafe { MmapOptions::new().len(size_of::<UniversalHeader>()).map_mut(&state_file)? };
255 let hdr = unsafe { &*(mmap.as_ptr() as *const UniversalHeader) };
256 if hdr.magic != UNIVERSAL_MAGIC || hdr.capacity != capacity as u32 {
257 return Err(UniversalError::LayoutMismatch);
258 }
259 let (version, generation, strategy_byte) = unpack(hdr.state.load(Ordering::Acquire));
260 let strategy = Strategy::from_u8(strategy_byte).ok_or(UniversalError::InvalidStrategy)?;
261 let backing = Self::open_backing(&base, generation, version, strategy, capacity)?;
262 Ok(Self {
263 base, capacity,
264 _state_file: state_file,
265 state_mmap: mmap,
266 backing: RwLock::new((version, generation, backing)),
267 _phantom: PhantomData,
268 header_sidecar: subetha_core::HandshakeHeader::new(),
269 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
270 })
271 }
272
273 fn open_backing(
274 base: &Path, generation: u16, version: u32, strategy: Strategy, capacity: usize,
275 ) -> Result<Backing<T>, UniversalError> {
276 let p = Self::backing_path(base, generation, version, strategy);
277 match strategy {
278 Strategy::Vec => {
279 let v: SharedVec<T> = SharedVec::open(&p, capacity)
280 .map_err(|_| UniversalError::VecError)?;
281 Ok(Backing::Vec(v))
282 }
283 Strategy::Map => {
284 let m: SharedHashMap<T, ()> = SharedHashMap::open(&p, capacity)?;
285 Ok(Backing::Map(m))
286 }
287 }
288 }
289
290 fn header(&self) -> &UniversalHeader {
291 unsafe { &*(self.state_mmap.as_ptr() as *const UniversalHeader) }
292 }
293
294 pub fn strategy(&self) -> Strategy {
298 let (_, _, s) = unpack(self.header().state.load(Ordering::Acquire));
299 Strategy::from_u8(s).expect("invalid strategy byte in state header")
300 }
301
302 pub fn strategy_version(&self) -> u32 {
305 unpack(self.header().state.load(Ordering::Acquire)).0
306 }
307
308 pub fn strategy_generation(&self) -> u16 {
312 unpack(self.header().state.load(Ordering::Acquire)).1
313 }
314
315 fn refresh_backing_if_stale(&self) -> Result<(), UniversalError> {
323 let (shared_v, shared_g, _) = unpack(self.header().state.load(Ordering::Acquire));
324 {
325 let g = self.backing.read();
326 if g.0 == shared_v && g.1 == shared_g { return Ok(()); }
327 }
328 let mut g = self.backing.write();
329 let (shared_v2, shared_g2, shared_s_byte2) =
330 unpack(self.header().state.load(Ordering::Acquire));
331 if g.0 == shared_v2 && g.1 == shared_g2 { return Ok(()); }
332 let strategy = Strategy::from_u8(shared_s_byte2).ok_or(UniversalError::InvalidStrategy)?;
333 let new_backing = Self::open_backing(
334 &self.base, shared_g2, shared_v2, strategy, self.capacity,
335 )?;
336 *g = (shared_v2, shared_g2, new_backing);
337 Ok(())
338 }
339
340 pub fn insert(&self, value: T) -> Result<(), UniversalError>
343 where T: std::hash::Hash,
344 {
345 self.refresh_backing_if_stale()?;
346 let g = self.backing.read();
347 let r: Result<(), UniversalError> = match &g.2 {
348 Backing::Vec(v) => {
349 v.push_back(value).map_err(|_| UniversalError::Full).map(|_| ())
350 }
351 Backing::Map(m) => {
352 m.insert(value, ()).map(|_| ()).map_err(Into::into)
353 }
354 };
355 self.header().insert_count.fetch_add(1, Ordering::Relaxed);
356 self.ring_sidecar.push_op(
357 crate::sidecar_ops::universal::OP_INSERT,
358 if r.is_err() { 1 } else { 0 },
359 );
360 r
361 }
362
363 pub fn contains(&self, value: &T) -> Result<bool, UniversalError>
366 where T: std::hash::Hash,
367 {
368 self.refresh_backing_if_stale()?;
369 let g = self.backing.read();
370 let hit = match &g.2 {
371 Backing::Vec(v) => v.snapshot().iter().any(|x| x == value),
372 Backing::Map(m) => m.contains_key(value),
373 };
374 self.header().contains_count.fetch_add(1, Ordering::Relaxed);
375 self.ring_sidecar.push_op(
376 crate::sidecar_ops::universal::OP_CONTAINS,
377 if hit { 0 } else { 2 },
378 );
379 Ok(hit)
380 }
381
382 pub fn len(&self) -> Result<usize, UniversalError> {
384 self.refresh_backing_if_stale()?;
385 let g = self.backing.read();
386 Ok(match &g.2 {
387 Backing::Vec(v) => v.len(),
388 Backing::Map(m) => m.len(),
389 })
390 }
391
392 pub fn is_empty(&self) -> Result<bool, UniversalError> {
393 Ok(self.len()? == 0)
394 }
395
396 pub fn clear(&self) -> Result<(), UniversalError> {
402 self.refresh_backing_if_stale()?;
403 let g = self.backing.read();
404 match &g.2 {
405 Backing::Vec(v) => v.clear(),
406 Backing::Map(m) => m.clear(),
407 }
408 Ok(())
409 }
410
411 pub fn snapshot(&self) -> Result<Vec<T>, UniversalError> {
414 self.refresh_backing_if_stale()?;
415 let g = self.backing.read();
416 Ok(match &g.2 {
417 Backing::Vec(v) => v.snapshot(),
418 Backing::Map(m) => m.snapshot().into_iter().map(|(k, _)| k).collect(),
419 })
420 }
421
422 pub fn op_histogram(&self) -> (u64, u64) {
425 let hdr = self.header();
426 (
427 hdr.insert_count.load(Ordering::Acquire),
428 hdr.contains_count.load(Ordering::Acquire),
429 )
430 }
431
432 pub fn migrate_to(&self, target: Strategy) -> Result<(), UniversalError>
443 where T: std::hash::Hash,
444 {
445 let r = self.migrate_to_inner(target);
446 self.ring_sidecar.push_op(
447 crate::sidecar_ops::universal::OP_MIGRATE,
448 if r.is_err() { 1 } else { 0 },
449 );
450 r
451 }
452
453 fn migrate_to_inner(&self, target: Strategy) -> Result<(), UniversalError>
454 where T: std::hash::Hash,
455 {
456 let current_state = self.header().state.load(Ordering::Acquire);
457 let (current_v, current_g, current_s) = unpack(current_state);
458 let current = Strategy::from_u8(current_s).ok_or(UniversalError::InvalidStrategy)?;
459 if current == target { return Ok(()); }
460 let (new_v, new_g) = match current_v.checked_add(1) {
465 Some(v) => (v, current_g),
466 None => {
467 let next_g = current_g.checked_add(1)
468 .ok_or(UniversalError::VersionExhausted)?;
469 (0, next_g)
470 }
471 };
472 let snap = self.snapshot()?;
473 let mut g = self.backing.write();
474 let new_p = Self::backing_path(&self.base, new_g, new_v, target);
475 let build_result: Result<Backing<T>, UniversalError> = (|| {
478 match target {
479 Strategy::Vec => {
480 let v: SharedVec<T> = SharedVec::create(&new_p, self.capacity)
481 .map_err(|_| UniversalError::VecError)?;
482 for x in &snap {
483 v.push_back(*x).map_err(|_| UniversalError::Full)?;
484 }
485 Ok(Backing::Vec(v))
486 }
487 Strategy::Map => {
488 let m: SharedHashMap<T, ()> = SharedHashMap::create(&new_p, self.capacity)?;
489 for x in &snap { m.insert(*x, ())?; }
490 Ok(Backing::Map(m))
491 }
492 }
493 })();
494 let new_backing = match build_result {
495 Ok(b) => b,
496 Err(e) => {
497 std::fs::remove_file(&new_p).ok();
498 return Err(e);
499 }
500 };
501 let new_state = pack(new_v, new_g, target as u8);
502 match self.header().state.compare_exchange(
503 current_state, new_state, Ordering::AcqRel, Ordering::Acquire,
504 ) {
505 Ok(_) => {
506 *g = (new_v, new_g, new_backing);
507 Ok(())
508 }
509 Err(_) => {
510 drop(new_backing);
511 std::fs::remove_file(&new_p).ok();
512 Err(UniversalError::VecError)
513 }
514 }
515 }
516
517 pub fn maybe_migrate_by_policy(
525 &self,
526 contains_to_insert_ratio: f64,
527 min_total_ops: u64,
528 ) -> Result<Option<Strategy>, UniversalError>
529 where T: std::hash::Hash,
530 {
531 let (ins, cnt) = self.op_histogram();
532 if ins + cnt < min_total_ops { return Ok(None); }
533 let current = self.strategy();
534 let ratio = cnt as f64 / (ins.max(1)) as f64;
535 let want = if ratio >= contains_to_insert_ratio { Strategy::Map } else { Strategy::Vec };
536 if want == current { return Ok(None); }
537 self.migrate_to(want)?;
538 Ok(Some(want))
539 }
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545
546 fn tmp_base(name: &str) -> PathBuf {
547 let mut p = std::env::temp_dir();
548 let pid = std::process::id();
549 p.push(format!("subetha-universal-{name}-{pid}"));
550 p
551 }
552
553 fn cleanup(base: &Path) {
554 let stem = base.file_name().unwrap().to_string_lossy().to_string();
555 let parent = base.parent().unwrap_or_else(|| Path::new(""));
556 if let Ok(entries) = std::fs::read_dir(parent) {
557 for e in entries.flatten() {
558 let name = e.file_name().to_string_lossy().to_string();
559 if name.starts_with(&stem) {
560 std::fs::remove_file(e.path()).ok();
561 }
562 }
563 }
564 }
565
566 #[test]
567 fn create_starts_in_vec_strategy() {
568 let base = tmp_base("starts-vec");
569 let u: SharedUniversal<u64> = SharedUniversal::create(&base, 64).unwrap();
570 assert_eq!(u.strategy(), Strategy::Vec);
571 assert_eq!(u.strategy_version(), 0);
572 assert_eq!(u.len().unwrap(), 0);
573 drop(u);
574 cleanup(&base);
575 }
576
577 #[test]
578 fn insert_and_contains_round_trip_in_vec_mode() {
579 let base = tmp_base("vec-roundtrip");
580 let u: SharedUniversal<u64> = SharedUniversal::create(&base, 64).unwrap();
581 for k in 0..10u64 { u.insert(k).unwrap(); }
582 for k in 0..10u64 { assert!(u.contains(&k).unwrap()); }
583 assert!(!u.contains(&999).unwrap());
584 assert_eq!(u.len().unwrap(), 10);
585 drop(u);
586 cleanup(&base);
587 }
588
589 #[test]
590 fn explicit_migrate_to_map_preserves_contents() {
591 let base = tmp_base("explicit-map");
592 let u: SharedUniversal<u64> = SharedUniversal::create(&base, 64).unwrap();
593 for k in 0..10u64 { u.insert(k).unwrap(); }
594 u.migrate_to(Strategy::Map).unwrap();
595 assert_eq!(u.strategy(), Strategy::Map);
596 assert_eq!(u.strategy_version(), 1);
597 for k in 0..10u64 { assert!(u.contains(&k).unwrap()); }
598 assert_eq!(u.len().unwrap(), 10);
599 drop(u);
600 cleanup(&base);
601 }
602
603 #[test]
604 fn migrate_back_and_forth_preserves_contents() {
605 let base = tmp_base("round-trip");
606 let u: SharedUniversal<u64> = SharedUniversal::create(&base, 64).unwrap();
607 for k in 0..5u64 { u.insert(k).unwrap(); }
608 u.migrate_to(Strategy::Map).unwrap();
609 u.migrate_to(Strategy::Vec).unwrap();
610 u.migrate_to(Strategy::Map).unwrap();
611 assert_eq!(u.strategy(), Strategy::Map);
612 assert_eq!(u.strategy_version(), 3);
613 let mut snap = u.snapshot().unwrap();
614 snap.sort();
615 assert_eq!(snap, vec![0, 1, 2, 3, 4]);
616 drop(u);
617 cleanup(&base);
618 }
619
620 #[test]
621 fn migrate_to_same_strategy_is_noop() {
622 let base = tmp_base("same");
623 let u: SharedUniversal<u64> = SharedUniversal::create(&base, 16).unwrap();
624 u.migrate_to(Strategy::Vec).unwrap();
625 assert_eq!(u.strategy_version(), 0);
626 u.migrate_to(Strategy::Map).unwrap();
627 u.migrate_to(Strategy::Map).unwrap();
628 assert_eq!(u.strategy_version(), 1);
629 drop(u);
630 cleanup(&base);
631 }
632
633 #[test]
634 fn local_policy_migrates_to_map_under_contains_load() {
635 let base = tmp_base("policy-map");
636 let u: SharedUniversal<u64> = SharedUniversal::create(&base, 64).unwrap();
637 for k in 0..5u64 { u.insert(k).unwrap(); }
638 for _ in 0..100 { u.contains(&3).unwrap(); }
640 let migrated = u.maybe_migrate_by_policy(0.5, 100).unwrap();
641 assert_eq!(migrated, Some(Strategy::Map));
642 assert_eq!(u.strategy(), Strategy::Map);
643 drop(u);
644 cleanup(&base);
645 }
646
647 #[test]
648 fn local_policy_keeps_vec_under_insert_load() {
649 let base = tmp_base("policy-vec");
650 let u: SharedUniversal<u64> = SharedUniversal::create(&base, 256).unwrap();
651 for k in 0..200u64 { u.insert(k).unwrap(); }
652 for _ in 0..10 { u.contains(&3).unwrap(); }
653 let migrated = u.maybe_migrate_by_policy(0.5, 100).unwrap();
654 assert_eq!(migrated, None);
655 assert_eq!(u.strategy(), Strategy::Vec);
656 drop(u);
657 cleanup(&base);
658 }
659
660 #[test]
661 fn reader_handle_observes_migration_via_version_bump() {
662 let base = tmp_base("cross-handle");
667 let writer: SharedUniversal<u64> = SharedUniversal::create(&base, 32).unwrap();
668 let reader: SharedUniversal<u64> = SharedUniversal::open(&base, 32).unwrap();
669 for k in 0..5u64 { writer.insert(k).unwrap(); }
670 assert_eq!(reader.strategy(), Strategy::Vec);
672 assert_eq!(reader.len().unwrap(), 5);
673 writer.migrate_to(Strategy::Map).unwrap();
675 assert_eq!(reader.strategy(), Strategy::Map);
677 assert_eq!(reader.strategy_version(), 1);
678 assert_eq!(reader.len().unwrap(), 5);
679 for k in 0..5u64 { assert!(reader.contains(&k).unwrap()); }
680 drop(writer);
681 drop(reader);
682 cleanup(&base);
683 }
684
685 #[test]
686 fn snapshot_preserves_through_migration() {
687 let base = tmp_base("snap");
688 let u: SharedUniversal<u32> = SharedUniversal::create(&base, 32).unwrap();
689 for k in [10u32, 5, 7, 1, 99] { u.insert(k).unwrap(); }
690 let mut pre = u.snapshot().unwrap();
691 pre.sort();
692 u.migrate_to(Strategy::Map).unwrap();
693 let mut post = u.snapshot().unwrap();
694 post.sort();
695 assert_eq!(pre, post);
696 drop(u);
697 cleanup(&base);
698 }
699
700 #[test]
701 fn op_histogram_tracks_real_ops() {
702 let base = tmp_base("hist");
703 let u: SharedUniversal<u64> = SharedUniversal::create(&base, 32).unwrap();
704 u.insert(1).unwrap();
705 u.insert(2).unwrap();
706 u.insert(3).unwrap();
707 u.contains(&2).unwrap();
708 u.contains(&2).unwrap();
709 let (ins, cnt) = u.op_histogram();
710 assert_eq!(ins, 3);
711 assert_eq!(cnt, 2);
712 drop(u);
713 cleanup(&base);
714 }
715
716 #[test]
717 fn pack_unpack_round_trips_all_three_fields() {
718 let v: u32 = 0xDEAD_BEEF;
721 let g: u16 = 0xCAFE;
722 let s: u8 = Strategy::Map as u8;
723 let packed = pack(v, g, s);
724 let (rv, rg, rs) = unpack(packed);
725 assert_eq!(rv, v);
726 assert_eq!(rg, g);
727 assert_eq!(rs, s);
728 }
729
730 #[test]
731 fn starts_at_generation_zero() {
732 let base = tmp_base("gen-zero");
733 let u: SharedUniversal<u64> = SharedUniversal::create(&base, 16).unwrap();
734 assert_eq!(u.strategy_version(), 0);
735 assert_eq!(u.strategy_generation(), 0);
736 drop(u);
737 cleanup(&base);
738 }
739
740 #[test]
741 fn migration_within_generation_keeps_generation_zero() {
742 let base = tmp_base("same-gen");
743 let u: SharedUniversal<u64> = SharedUniversal::create(&base, 16).unwrap();
744 u.migrate_to(Strategy::Map).unwrap();
745 u.migrate_to(Strategy::Vec).unwrap();
746 u.migrate_to(Strategy::Map).unwrap();
747 assert_eq!(u.strategy_version(), 3);
748 assert_eq!(u.strategy_generation(), 0);
749 drop(u);
750 cleanup(&base);
751 }
752
753 #[test]
754 fn version_wrap_bumps_generation_and_resets_version() {
755 let base = tmp_base("wrap-gen");
761 let u: SharedUniversal<u64> = SharedUniversal::create(&base, 16).unwrap();
762 let synth_p = SharedUniversal::<u64>::backing_path(
763 u.base.as_path(), 0, u32::MAX, Strategy::Vec,
764 );
765 let synth: SharedVec<u64> = SharedVec::create(&synth_p, 16).unwrap();
768 drop(synth);
769 u.header().state.store(
770 pack(u32::MAX, 0, Strategy::Vec as u8),
771 Ordering::Release,
772 );
773 u.refresh_backing_if_stale().unwrap();
774 u.migrate_to(Strategy::Map).unwrap();
776 assert_eq!(u.strategy_version(), 0);
777 assert_eq!(u.strategy_generation(), 1);
778 assert_eq!(u.strategy(), Strategy::Map);
779 drop(u);
780 cleanup(&base);
781 }
782
783 #[test]
784 fn true_exhaustion_returns_version_exhausted() {
785 let base = tmp_base("exhausted");
788 let u: SharedUniversal<u64> = SharedUniversal::create(&base, 16).unwrap();
789 u.header().state.store(
790 pack(u32::MAX, u16::MAX, Strategy::Vec as u8),
791 Ordering::Release,
792 );
793 u.refresh_backing_if_stale().unwrap_or(());
794 let r = u.migrate_to(Strategy::Map);
795 assert_eq!(r.err(), Some(UniversalError::VersionExhausted));
796 drop(u);
797 cleanup(&base);
798 }
799
800 #[test]
801 fn reader_re_opens_on_generation_change_even_at_same_version() {
802 let base = tmp_base("reader-gen");
807 let writer: SharedUniversal<u64> = SharedUniversal::create(&base, 16).unwrap();
808 let reader: SharedUniversal<u64> = SharedUniversal::open(&base, 16).unwrap();
809 writer.insert(11).unwrap();
812 writer.insert(22).unwrap();
813 let new_p = SharedUniversal::<u64>::backing_path(
818 writer.base.as_path(), 1, 0, Strategy::Map,
819 );
820 let m: SharedHashMap<u64, ()> = SharedHashMap::create(&new_p, 16).unwrap();
821 m.insert(99u64, ()).unwrap();
822 drop(m);
823 writer.header().state.store(
824 pack(0, 1, Strategy::Map as u8),
825 Ordering::Release,
826 );
827 assert!(reader.contains(&99u64).unwrap());
832 assert!(!reader.contains(&11u64).unwrap());
835 assert_eq!(reader.strategy(), Strategy::Map);
836 assert_eq!(reader.strategy_version(), 0);
837 assert_eq!(reader.strategy_generation(), 1);
838 drop(writer);
839 drop(reader);
840 cleanup(&base);
841 }
842}