1use std::fs::{File, OpenOptions};
99use std::marker::PhantomData;
100use std::mem::size_of;
101use std::path::Path;
102use std::sync::atomic::{AtomicU32, AtomicU64, AtomicU8, Ordering};
103
104use memmap2::{MmapMut, MmapOptions};
105
106pub const MAP_MAGIC: u32 = 0x4150_484D;
107pub const MAP_PAYLOAD_BYTES: usize = 48;
108
109pub const SLOT_EMPTY: u8 = 0;
110pub const SLOT_OCCUPIED: u8 = 1;
111pub const SLOT_TOMBSTONE: u8 = 2;
112
113#[repr(C, align(64))]
114pub struct MapHeader {
115 pub magic: u32,
116 pub capacity: u32,
117 pub count: AtomicU64,
118 pub key_size: u32,
119 pub value_size: u32,
120 pub tombstones: AtomicU64,
124 _pad: [u8; 32],
125}
126
127#[repr(C, align(64))]
128pub struct MapSlot {
129 pub state: AtomicU8,
130 _pad1: [u8; 3],
131 pub version: AtomicU32,
132 pub hash: AtomicU64,
133 pub payload: [u8; MAP_PAYLOAD_BYTES],
134}
135
136const _: () = {
137 assert!(size_of::<MapHeader>() == 64);
138 assert!(size_of::<MapSlot>() == 64);
139};
140
141pub const fn map_file_size(capacity: usize) -> usize {
142 size_of::<MapHeader>() + capacity * size_of::<MapSlot>()
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum MapError {
147 Full,
148 PayloadTooLarge,
149 LayoutMismatch,
150 IoError(std::io::ErrorKind),
151}
152
153impl From<std::io::Error> for MapError {
154 fn from(e: std::io::Error) -> Self { Self::IoError(e.kind()) }
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum InsertOutcome {
159 Inserted,
160 Updated,
161}
162
163#[inline]
167pub fn fnv1a_64(bytes: &[u8]) -> u64 {
168 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
169 for &b in bytes {
170 h ^= b as u64;
171 h = h.wrapping_mul(0x100_0000_01b3);
172 }
173 h
174}
175
176pub struct SharedHashMap<K: Copy + Eq + 'static, V: Copy + 'static> {
177 _file: File,
178 mmap: MmapMut,
179 capacity: usize,
180 cap_mask: usize,
182 cap_is_pow2: bool,
187 _phantom: PhantomData<(K, V)>,
188 header_sidecar: subetha_core::HandshakeHeader,
189 ring_sidecar: Box<subetha_core::ObservationRing>,
190}
191
192unsafe impl<K: Copy + Eq + Send + 'static, V: Copy + Send + 'static> Send for SharedHashMap<K, V> {}
193unsafe impl<K: Copy + Eq + Sync + 'static, V: Copy + Sync + 'static> Sync for SharedHashMap<K, V> {}
194
195impl<K: Copy + Eq + Send + Sync + 'static, V: Copy + Send + Sync + 'static>
196 subetha_sidecar::AdaptiveInstance for SharedHashMap<K, V>
197{
198 fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
199 fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
200 fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
201 Box::new(subetha_sidecar::NoMigrationPolicy)
202 }
203}
204
205impl<K: Copy + Eq + 'static, V: Copy + 'static> SharedHashMap<K, V> {
206 fn check_layout() -> Result<(), MapError> {
207 if size_of::<K>() + size_of::<V>() > MAP_PAYLOAD_BYTES {
208 return Err(MapError::PayloadTooLarge);
209 }
210 Ok(())
211 }
212
213 pub fn create(path: impl AsRef<Path>, capacity: usize) -> Result<Self, MapError> {
219 Self::check_layout()?;
220 assert!(capacity >= 2);
221 let total = map_file_size(capacity);
222 let (file, mmap) = crate::mmf_attach::create_or_attach(
223 path.as_ref(),
224 total,
225 |ptr| unsafe { Self::init_region(ptr, capacity) },
226 |ptr| unsafe { (*(ptr as *const MapHeader)).magic == MAP_MAGIC },
227 )?;
228 Self::from_region(file, mmap, capacity)
229 }
230
231 pub fn reset(path: impl AsRef<Path>, capacity: usize) -> Result<Self, MapError> {
235 Self::check_layout()?;
236 assert!(capacity >= 2);
237 let total = map_file_size(capacity);
238 let (file, mmap) = crate::mmf_attach::reset(path.as_ref(), total, |ptr| unsafe {
239 Self::init_region(ptr, capacity)
240 })?;
241 Self::from_region(file, mmap, capacity)
242 }
243
244 unsafe fn init_region(ptr: *mut u8, capacity: usize) {
254 let hdr = ptr as *mut MapHeader;
255 unsafe {
256 (*hdr).capacity = capacity as u32;
257 (*hdr).key_size = size_of::<K>() as u32;
258 (*hdr).value_size = size_of::<V>() as u32;
259 std::ptr::write_volatile(&raw mut (*hdr).magic, MAP_MAGIC);
260 }
261 }
262
263 fn from_region(file: File, mmap: MmapMut, capacity: usize) -> Result<Self, MapError> {
266 let hdr = unsafe { &*(mmap.as_ptr() as *const MapHeader) };
267 if hdr.magic != MAP_MAGIC
268 || hdr.capacity != capacity as u32
269 || hdr.key_size != size_of::<K>() as u32
270 || hdr.value_size != size_of::<V>() as u32
271 {
272 return Err(MapError::LayoutMismatch);
273 }
274 Ok(Self {
275 _file: file, mmap, capacity,
276 cap_mask: capacity.wrapping_sub(1),
277 cap_is_pow2: capacity.is_power_of_two(),
278 _phantom: PhantomData,
279 header_sidecar: subetha_core::HandshakeHeader::new(),
280 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
281 })
282 }
283
284 pub fn open(path: impl AsRef<Path>, expected_capacity: usize) -> Result<Self, MapError> {
285 Self::check_layout()?;
286 let total = map_file_size(expected_capacity);
287 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
288 if file.metadata()?.len() < total as u64 {
289 return Err(MapError::LayoutMismatch);
290 }
291 let mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
292 Self::from_region(file, mmap, expected_capacity)
293 }
294
295 #[inline]
303 fn wrap(&self, i: usize) -> usize {
304 if self.cap_is_pow2 { i & self.cap_mask } else { i % self.capacity }
305 }
306
307 #[inline]
308 pub fn capacity(&self) -> usize { self.capacity }
309
310 #[inline]
311 pub fn len(&self) -> usize {
312 self.header().count.load(Ordering::Acquire) as usize
313 }
314
315 #[inline]
316 pub fn is_empty(&self) -> bool { self.len() == 0 }
317
318 fn header(&self) -> &MapHeader {
319 unsafe { &*(self.mmap.as_ptr() as *const MapHeader) }
320 }
321
322 fn slot(&self, idx: usize) -> &MapSlot {
323 let base = unsafe { self.mmap.as_ptr().add(size_of::<MapHeader>()) };
324 unsafe { &*(base.add(idx * size_of::<MapSlot>()) as *const MapSlot) }
325 }
326
327 fn hash_key(k: &K) -> u64 {
328 let bytes = unsafe {
329 std::slice::from_raw_parts(k as *const K as *const u8, size_of::<K>())
330 };
331 fnv1a_64(bytes)
332 }
333
334 fn write_payload(&self, slot_idx: usize, k: &K, v: &V) {
336 let slot = self.slot(slot_idx);
337 slot.version.fetch_add(1, Ordering::AcqRel); let base = unsafe {
339 self.mmap.as_ptr()
340 .add(size_of::<MapHeader>())
341 .add(slot_idx * size_of::<MapSlot>())
342 .add(std::mem::offset_of!(MapSlot, payload))
343 as *mut u8
344 };
345 unsafe {
346 std::ptr::copy_nonoverlapping(
348 k as *const K as *const u8, base, size_of::<K>(),
349 );
350 std::ptr::copy_nonoverlapping(
351 v as *const V as *const u8,
352 base.add(size_of::<K>()),
353 size_of::<V>(),
354 );
355 }
356 slot.version.fetch_add(1, Ordering::AcqRel); }
358
359 fn read_payload(&self, slot_idx: usize) -> (K, V) {
361 let slot = self.slot(slot_idx);
362 loop {
363 let v1 = slot.version.load(Ordering::Acquire);
364 if v1 & 1 != 0 {
365 std::hint::spin_loop();
366 continue;
367 }
368 let mut k = std::mem::MaybeUninit::<K>::uninit();
369 let mut v = std::mem::MaybeUninit::<V>::uninit();
370 let src = unsafe {
371 self.mmap.as_ptr()
372 .add(size_of::<MapHeader>())
373 .add(slot_idx * size_of::<MapSlot>())
374 .add(std::mem::offset_of!(MapSlot, payload))
375 };
376 unsafe {
377 std::ptr::copy_nonoverlapping(
378 src, k.as_mut_ptr() as *mut u8, size_of::<K>(),
379 );
380 std::ptr::copy_nonoverlapping(
381 src.add(size_of::<K>()),
382 v.as_mut_ptr() as *mut u8,
383 size_of::<V>(),
384 );
385 }
386 let v2 = slot.version.load(Ordering::Acquire);
387 if v1 == v2 {
388 return unsafe { (k.assume_init(), v.assume_init()) };
389 }
390 }
391 }
392
393 pub fn insert(&self, key: K, value: V) -> Result<InsertOutcome, MapError> {
410 let r = self.insert_inner(key, value);
411 self.ring_sidecar.push_op(
412 crate::sidecar_ops::hash_map::OP_INSERT,
413 if matches!(r, Err(MapError::Full)) { 1 } else { 0 },
414 );
415 r
416 }
417
418 fn insert_inner(&self, key: K, value: V) -> Result<InsertOutcome, MapError> {
419 let h = Self::hash_key(&key);
420 let start = self.wrap(h as usize);
421 let mut first_tombstone: Option<usize> = None;
425 for i in 0..self.capacity {
426 let idx = self.wrap(start + i);
427 let slot = self.slot(idx);
428 let state = slot.state.load(Ordering::Acquire);
429 if state == SLOT_EMPTY {
430 if let Some(tomb_idx) = first_tombstone
434 && self.try_claim_tombstone(tomb_idx, h, &key, &value) {
435 return Ok(InsertOutcome::Inserted);
436 }
437 if slot.state.compare_exchange(
440 SLOT_EMPTY, SLOT_OCCUPIED,
441 Ordering::AcqRel, Ordering::Acquire,
442 ).is_ok() {
443 slot.hash.store(h, Ordering::Release);
444 self.write_payload(idx, &key, &value);
445 self.header().count.fetch_add(1, Ordering::AcqRel);
446 return Ok(InsertOutcome::Inserted);
447 }
448 let now = slot.state.load(Ordering::Acquire);
451 if now == SLOT_OCCUPIED {
452 let cached = slot.hash.load(Ordering::Acquire);
453 if cached == h {
454 let (k, _) = self.read_payload(idx);
455 if k == key {
456 self.write_payload(idx, &key, &value);
457 return Ok(InsertOutcome::Updated);
458 }
459 }
460 } else if now == SLOT_TOMBSTONE && first_tombstone.is_none() {
461 first_tombstone = Some(idx);
462 }
463 continue;
464 }
465 if state == SLOT_OCCUPIED {
466 let cached = slot.hash.load(Ordering::Acquire);
467 if cached == h {
468 let (k, _) = self.read_payload(idx);
469 if k == key {
470 self.write_payload(idx, &key, &value);
471 return Ok(InsertOutcome::Updated);
472 }
473 }
474 continue;
475 }
476 if first_tombstone.is_none() {
479 first_tombstone = Some(idx);
480 }
481 }
482 if let Some(tomb_idx) = first_tombstone
485 && self.try_claim_tombstone(tomb_idx, h, &key, &value) {
486 return Ok(InsertOutcome::Inserted);
487 }
488 Err(MapError::Full)
489 }
490
491 #[inline]
499 fn try_claim_tombstone(&self, tomb_idx: usize, h: u64, key: &K, value: &V) -> bool {
500 let tomb_slot = self.slot(tomb_idx);
501 if tomb_slot.state.compare_exchange(
502 SLOT_TOMBSTONE, SLOT_OCCUPIED,
503 Ordering::AcqRel, Ordering::Acquire,
504 ).is_err() {
505 return false;
506 }
507 tomb_slot.hash.store(h, Ordering::Release);
508 self.write_payload(tomb_idx, key, value);
509 self.header().count.fetch_add(1, Ordering::AcqRel);
510 loop {
516 let cur = self.header().tombstones.load(Ordering::Acquire);
517 if cur == 0 { break; }
518 if self.header().tombstones.compare_exchange(
519 cur, cur - 1, Ordering::AcqRel, Ordering::Acquire,
520 ).is_ok() {
521 break;
522 }
523 }
524 true
525 }
526
527 pub fn get(&self, key: &K) -> Option<V> {
529 let r = self.get_inner(key);
530 self.ring_sidecar.push_op(
531 crate::sidecar_ops::hash_map::OP_GET,
532 if r.is_none() { 2 } else { 0 },
533 );
534 r
535 }
536
537 fn get_inner(&self, key: &K) -> Option<V> {
541 let h = Self::hash_key(key);
542 let start = self.wrap(h as usize);
543 for i in 0..self.capacity {
544 let idx = self.wrap(start + i);
545 let slot = self.slot(idx);
546 let state = slot.state.load(Ordering::Acquire);
547 if state == SLOT_EMPTY {
548 return None;
549 }
550 if state == SLOT_OCCUPIED {
551 let cached = slot.hash.load(Ordering::Acquire);
552 if cached == h {
553 let (k, v) = self.read_payload(idx);
554 if k == *key {
555 return Some(v);
556 }
557 }
558 }
559 }
561 None
562 }
563
564 pub fn contains_key(&self, key: &K) -> bool {
566 let r = self.get_inner(key);
567 self.ring_sidecar.push_op(
568 crate::sidecar_ops::hash_map::OP_CONTAINS,
569 if r.is_none() { 2 } else { 0 },
570 );
571 r.is_some()
572 }
573
574 pub fn remove(&self, key: &K) -> Option<V> {
576 let r = self.remove_inner(key);
577 self.ring_sidecar.push_op(
578 crate::sidecar_ops::hash_map::OP_REMOVE,
579 if r.is_none() { 2 } else { 0 },
580 );
581 r
582 }
583
584 fn remove_inner(&self, key: &K) -> Option<V> {
585 let h = Self::hash_key(key);
586 let start = self.wrap(h as usize);
587 for i in 0..self.capacity {
588 let idx = self.wrap(start + i);
589 let slot = self.slot(idx);
590 let state = slot.state.load(Ordering::Acquire);
591 if state == SLOT_EMPTY { return None; }
592 if state == SLOT_OCCUPIED {
593 let cached = slot.hash.load(Ordering::Acquire);
594 if cached == h {
595 let (k, v) = self.read_payload(idx);
596 if k == *key {
597 if slot.state.compare_exchange(
598 SLOT_OCCUPIED, SLOT_TOMBSTONE,
599 Ordering::AcqRel, Ordering::Acquire,
600 ).is_ok() {
601 self.header().count.fetch_sub(1, Ordering::AcqRel);
602 self.header().tombstones.fetch_add(1, Ordering::AcqRel);
603 return Some(v);
604 }
605 return None;
607 }
608 }
609 }
610 }
611 None
612 }
613
614 pub fn clear(&self) {
619 for i in 0..self.capacity {
620 let slot = self.slot(i);
621 slot.state.store(SLOT_EMPTY, Ordering::Release);
622 }
623 self.header().count.store(0, Ordering::Release);
624 self.header().tombstones.store(0, Ordering::Release);
625 self.ring_sidecar
626 .push_op(crate::sidecar_ops::hash_map::OP_CLEAR, 0);
627 }
628
629 #[inline]
632 pub fn tombstone_count(&self) -> usize {
633 self.header().tombstones.load(Ordering::Acquire) as usize
634 }
635
636 pub fn should_compact(&self, threshold_fraction: f64) -> bool {
641 debug_assert!(
642 (0.0..=1.0).contains(&threshold_fraction),
643 "threshold_fraction must be in [0, 1]; got {threshold_fraction}",
644 );
645 let tombs = self.tombstone_count() as f64;
646 tombs / self.capacity as f64 >= threshold_fraction
647 }
648
649 pub fn compact(&self) -> Result<usize, MapError> {
678 self.ring_sidecar
679 .push_op(crate::sidecar_ops::hash_map::OP_COMPACT, 0);
680 let mut live: Vec<(K, V)> = Vec::with_capacity(self.len());
681 let mut reclaimed = 0usize;
682 for i in 0..self.capacity {
683 let slot = self.slot(i);
684 let s = slot.state.load(Ordering::Acquire);
685 if s == SLOT_OCCUPIED {
686 live.push(self.read_payload(i));
687 } else if s == SLOT_TOMBSTONE {
688 reclaimed += 1;
689 }
690 }
691 for i in 0..self.capacity {
692 let slot = self.slot(i);
693 slot.state.store(SLOT_EMPTY, Ordering::Release);
694 }
695 self.header().count.store(0, Ordering::Release);
696 self.header().tombstones.store(0, Ordering::Release);
697 for (k, v) in live {
698 self.insert(k, v)?;
702 }
703 Ok(reclaimed)
704 }
705
706 pub fn snapshot(&self) -> Vec<(K, V)> {
709 let mut out = Vec::with_capacity(self.len());
710 for i in 0..self.capacity {
711 let slot = self.slot(i);
712 if slot.state.load(Ordering::Acquire) == SLOT_OCCUPIED {
713 out.push(self.read_payload(i));
714 }
715 }
716 out
717 }
718
719 pub fn load_factor(&self) -> f64 {
721 self.len() as f64 / self.capacity as f64
722 }
723
724 pub fn flush(&self) -> Result<(), MapError> {
725 self.mmap.flush()?;
726 Ok(())
727 }
728
729 pub fn flush_async(&self) -> Result<(), MapError> {
733 self.mmap.flush_async()?;
734 Ok(())
735 }
736}
737
738#[cfg(test)]
739mod tests {
740 use super::*;
741 use std::sync::Arc;
742 use std::thread;
743
744 fn tmp(name: &str) -> std::path::PathBuf {
745 let mut p = std::env::temp_dir();
746 let pid = std::process::id();
747 p.push(format!("subetha-hashmap-{name}-{pid}.bin"));
748 p
749 }
750
751 #[test]
752 fn create_initial_empty() {
753 let p = tmp("init");
754 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 32).unwrap();
755 assert_eq!(m.capacity(), 32);
756 assert_eq!(m.len(), 0);
757 assert!(m.is_empty());
758 assert_eq!(m.get(&42), None);
759 std::fs::remove_file(&p).ok();
760 }
761
762 #[test]
763 fn insert_and_get_round_trip() {
764 let p = tmp("rt");
765 let m: SharedHashMap<u32, u64> = SharedHashMap::create(&p, 32).unwrap();
766 assert_eq!(m.insert(1, 100).unwrap(), InsertOutcome::Inserted);
767 assert_eq!(m.insert(2, 200).unwrap(), InsertOutcome::Inserted);
768 assert_eq!(m.insert(3, 300).unwrap(), InsertOutcome::Inserted);
769 assert_eq!(m.len(), 3);
770 assert_eq!(m.get(&1), Some(100));
771 assert_eq!(m.get(&2), Some(200));
772 assert_eq!(m.get(&3), Some(300));
773 assert_eq!(m.get(&999), None);
774 std::fs::remove_file(&p).ok();
775 }
776
777 #[test]
780 fn second_create_attaches_and_keeps_entries() {
781 let p = tmp("attach");
782 std::fs::remove_file(&p).ok();
783 let m: SharedHashMap<u32, u64> = SharedHashMap::create(&p, 32).unwrap();
784 m.insert(7, 777).unwrap();
785
786 let m2: SharedHashMap<u32, u64> = SharedHashMap::create(&p, 32).unwrap();
787 assert_eq!(m2.get(&7), Some(777), "attach lost a live entry");
788 assert_eq!(m2.len(), 1);
789
790 drop(m);
793 drop(m2);
794 let fresh: SharedHashMap<u32, u64> = SharedHashMap::reset(&p, 32).unwrap();
795 assert_eq!(fresh.len(), 0);
796 assert_eq!(fresh.get(&7), None, "reset left an entry behind");
797 drop(fresh);
798 std::fs::remove_file(&p).ok();
799 }
800
801 #[test]
803 fn create_refuses_a_mismatched_region() {
804 let p = tmp("mismatch");
805 std::fs::remove_file(&p).ok();
806 let m: SharedHashMap<u32, u64> = SharedHashMap::create(&p, 32).unwrap();
807 assert!(matches!(
808 SharedHashMap::<u32, u64>::create(&p, 16),
809 Err(MapError::LayoutMismatch),
810 ));
811 assert!(matches!(
812 SharedHashMap::<u64, u64>::create(&p, 32),
813 Err(MapError::LayoutMismatch),
814 ));
815 drop(m);
816 std::fs::remove_file(&p).ok();
817 }
818
819 #[test]
820 fn duplicate_insert_updates_value() {
821 let p = tmp("dup");
822 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 16).unwrap();
823 assert_eq!(m.insert(7, 100).unwrap(), InsertOutcome::Inserted);
824 assert_eq!(m.insert(7, 200).unwrap(), InsertOutcome::Updated);
825 assert_eq!(m.len(), 1);
826 assert_eq!(m.get(&7), Some(200));
827 std::fs::remove_file(&p).ok();
828 }
829
830 #[test]
831 fn remove_returns_value_and_decrements_count() {
832 let p = tmp("rm");
833 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 16).unwrap();
834 m.insert(1, 10).unwrap();
835 m.insert(2, 20).unwrap();
836 assert_eq!(m.remove(&1), Some(10));
837 assert_eq!(m.len(), 1);
838 assert_eq!(m.get(&1), None);
839 assert_eq!(m.get(&2), Some(20));
840 assert_eq!(m.remove(&999), None);
841 std::fs::remove_file(&p).ok();
842 }
843
844 #[test]
845 fn tombstone_does_not_break_probing() {
846 let p = tmp("tomb");
847 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 8).unwrap();
848 for k in 0..6u32 { m.insert(k, k * 10).unwrap(); }
852 m.remove(&2);
854 for k in [0u32, 1, 3, 4, 5] {
855 assert_eq!(m.get(&k), Some(k * 10),
856 "key {k} should still be findable past the tombstone");
857 }
858 assert_eq!(m.get(&2), None);
859 std::fs::remove_file(&p).ok();
860 }
861
862 #[test]
863 fn full_map_returns_error_on_new_key() {
864 let p = tmp("full");
865 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 4).unwrap();
866 m.insert(1, 1).unwrap();
867 m.insert(2, 2).unwrap();
868 m.insert(3, 3).unwrap();
869 m.insert(4, 4).unwrap();
870 assert_eq!(m.insert(5, 5).err(), Some(MapError::Full));
871 assert_eq!(m.insert(1, 100).unwrap(), InsertOutcome::Updated);
873 std::fs::remove_file(&p).ok();
874 }
875
876 #[test]
877 fn clear_resets_to_empty() {
878 let p = tmp("clear");
879 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 16).unwrap();
880 for k in 0..5u32 { m.insert(k, k).unwrap(); }
881 assert_eq!(m.len(), 5);
882 m.clear();
883 assert_eq!(m.len(), 0);
884 assert_eq!(m.get(&0), None);
885 m.insert(99, 99).unwrap();
886 assert_eq!(m.get(&99), Some(99));
887 std::fs::remove_file(&p).ok();
888 }
889
890 #[test]
891 fn snapshot_collects_all_present_pairs() {
892 let p = tmp("snap");
893 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 16).unwrap();
894 m.insert(1, 10).unwrap();
895 m.insert(2, 20).unwrap();
896 m.insert(3, 30).unwrap();
897 m.remove(&2);
898 let mut snap = m.snapshot();
899 snap.sort();
900 assert_eq!(snap, vec![(1, 10), (3, 30)]);
901 std::fs::remove_file(&p).ok();
902 }
903
904 #[test]
905 fn cross_handle_visibility() {
906 let p = tmp("cross-handle");
907 let writer: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 16).unwrap();
908 let reader: SharedHashMap<u32, u32> = SharedHashMap::open(&p, 16).unwrap();
909 writer.insert(42, 4242).unwrap();
910 assert_eq!(reader.get(&42), Some(4242));
911 reader.insert(7, 77).unwrap();
912 assert_eq!(writer.get(&7), Some(77));
913 writer.remove(&42);
914 assert_eq!(reader.get(&42), None);
915 std::fs::remove_file(&p).ok();
916 }
917
918 #[test]
919 fn concurrent_inserters_all_keys_present() {
920 let p = tmp("concurrent");
921 let m: Arc<SharedHashMap<u32, u32>> = Arc::new(SharedHashMap::create(&p, 1024).unwrap());
922 let n_threads = 4;
923 let per_thread = 100u32;
924 let mut handles = vec![];
925 for t in 0..n_threads {
926 let m = m.clone();
927 handles.push(thread::spawn(move || {
928 for i in 0..per_thread {
929 let key = (t as u32) * per_thread + i;
930 m.insert(key, key * 10).unwrap();
931 }
932 }));
933 }
934 for h in handles { h.join().unwrap(); }
935 assert_eq!(m.len(), n_threads * per_thread as usize);
936 for t in 0..n_threads as u32 {
937 for i in 0..per_thread {
938 let key = t * per_thread + i;
939 assert_eq!(m.get(&key), Some(key * 10),
940 "key {key} should be present with value {}", key * 10);
941 }
942 }
943 std::fs::remove_file(&p).ok();
944 }
945
946 #[test]
947 fn struct_key_and_value_round_trip() {
948 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
949 #[repr(C)]
950 struct UserId { realm: u32, user: u32 }
951 #[derive(Clone, Copy, Debug, PartialEq)]
952 #[repr(C)]
953 struct Session { token: u64, expires_us: u64 }
954 let p = tmp("struct");
955 let m: SharedHashMap<UserId, Session> = SharedHashMap::create(&p, 32).unwrap();
956 let k = UserId { realm: 1, user: 42 };
957 let v = Session { token: 0xDEAD_BEEF, expires_us: 9_999_999_999 };
958 m.insert(k, v).unwrap();
959 assert_eq!(m.get(&k), Some(v));
960 std::fs::remove_file(&p).ok();
961 }
962
963 #[test]
964 fn payload_too_large_at_create() {
965 #[allow(dead_code)] struct BigKey([u8; 64]);
967 impl Copy for BigKey {}
968 impl Clone for BigKey { fn clone(&self) -> Self { *self } }
969 impl PartialEq for BigKey { fn eq(&self, _: &Self) -> bool { true } }
970 impl Eq for BigKey {}
971 let p = tmp("too-large");
972 let r = SharedHashMap::<BigKey, u32>::create(&p, 4);
973 assert_eq!(r.err(), Some(MapError::PayloadTooLarge));
974 std::fs::remove_file(&p).ok();
975 }
976
977 #[test]
978 fn disk_persistence_survives_reopen() {
979 let p = tmp("disk");
980 {
981 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 16).unwrap();
982 for k in 0..5u32 { m.insert(k, k * 100).unwrap(); }
983 m.remove(&2);
984 m.flush().unwrap();
985 }
986 let m2: SharedHashMap<u32, u32> = SharedHashMap::open(&p, 16).unwrap();
987 assert_eq!(m2.len(), 4);
988 assert_eq!(m2.get(&0), Some(0));
989 assert_eq!(m2.get(&1), Some(100));
990 assert_eq!(m2.get(&2), None);
991 assert_eq!(m2.get(&3), Some(300));
992 assert_eq!(m2.get(&4), Some(400));
993 std::fs::remove_file(&p).ok();
994 }
995
996 #[test]
997 fn fnv1a_64_is_deterministic() {
998 let h1 = fnv1a_64(b"adaptive-prims");
1000 let h2 = fnv1a_64(b"adaptive-prims");
1001 assert_eq!(h1, h2);
1002 let h3 = fnv1a_64(b"ADAPTIVE-PRIMS");
1004 assert_ne!(h1, h3);
1005 }
1006
1007 #[test]
1008 fn load_factor_reports_correctly() {
1009 let p = tmp("load");
1010 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 10).unwrap();
1011 for k in 0..3u32 { m.insert(k, k).unwrap(); }
1012 assert_eq!(m.load_factor(), 0.3);
1013 std::fs::remove_file(&p).ok();
1014 }
1015
1016 #[test]
1017 fn remove_bumps_tombstone_counter() {
1018 let p = tmp("tomb-counter");
1019 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 16).unwrap();
1020 assert_eq!(m.tombstone_count(), 0);
1021 for k in 0..5u32 { m.insert(k, k).unwrap(); }
1022 assert_eq!(m.tombstone_count(), 0);
1023 m.remove(&1);
1024 m.remove(&3);
1025 assert_eq!(m.tombstone_count(), 2);
1026 m.remove(&999);
1028 assert_eq!(m.tombstone_count(), 2);
1029 std::fs::remove_file(&p).ok();
1030 }
1031
1032 #[test]
1033 fn compact_on_empty_map_is_noop() {
1034 let p = tmp("compact-empty");
1035 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 16).unwrap();
1036 assert_eq!(m.compact().unwrap(), 0);
1037 assert_eq!(m.len(), 0);
1038 assert_eq!(m.tombstone_count(), 0);
1039 m.insert(7, 70).unwrap();
1041 assert_eq!(m.get(&7), Some(70));
1042 std::fs::remove_file(&p).ok();
1043 }
1044
1045 #[test]
1046 fn compact_reclaims_tombstones() {
1047 let p = tmp("compact-reclaim");
1048 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 16).unwrap();
1049 for k in 0..10u32 { m.insert(k, k * 10).unwrap(); }
1050 for k in [0u32, 2, 4, 6, 8] { m.remove(&k); }
1051 assert_eq!(m.tombstone_count(), 5);
1052 let reclaimed = m.compact().unwrap();
1053 assert_eq!(reclaimed, 5);
1054 assert_eq!(m.tombstone_count(), 0);
1055 assert_eq!(m.len(), 5);
1056 std::fs::remove_file(&p).ok();
1057 }
1058
1059 #[test]
1060 fn compact_preserves_all_live_pairs() {
1061 let p = tmp("compact-preserve");
1062 let m: SharedHashMap<u32, u64> = SharedHashMap::create(&p, 32).unwrap();
1063 for k in 0..20u32 { m.insert(k, (k as u64) * 1000).unwrap(); }
1066 for k in (0..20u32).filter(|k| k % 2 == 0) { m.remove(&k); }
1067 let pre: Vec<(u32, u64)> = {
1068 let mut s = m.snapshot();
1069 s.sort();
1070 s
1071 };
1072 m.compact().unwrap();
1073 let post: Vec<(u32, u64)> = {
1074 let mut s = m.snapshot();
1075 s.sort();
1076 s
1077 };
1078 assert_eq!(pre, post,
1079 "compact must preserve every live (K, V) pair exactly");
1080 for (k, v) in &post {
1082 assert_eq!(m.get(k), Some(*v));
1083 }
1084 std::fs::remove_file(&p).ok();
1085 }
1086
1087 #[test]
1088 fn compact_reclaims_after_heavy_churn() {
1089 let p = tmp("compact-churn");
1094 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 64).unwrap();
1095 for k in 0..32u32 { m.insert(k, k).unwrap(); }
1096 for round in 0..16u32 {
1097 m.remove(&round);
1098 let new_key = 100 + round;
1099 m.insert(new_key, new_key).unwrap();
1100 }
1101 assert_eq!(m.tombstone_count(), 16);
1102 let live_before = m.len();
1103 let reclaimed = m.compact().unwrap();
1104 assert_eq!(reclaimed, 16);
1105 assert_eq!(m.tombstone_count(), 0);
1106 assert_eq!(m.len(), live_before);
1107 for round in 0..16u32 {
1108 assert_eq!(m.get(&round), None);
1109 let new_key = 100 + round;
1110 assert_eq!(m.get(&new_key), Some(new_key));
1111 }
1112 std::fs::remove_file(&p).ok();
1113 }
1114
1115 #[test]
1116 fn tombstone_reuse_avoids_full_after_remove() {
1117 let p = tmp("reuse-avoids-full");
1122 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 8).unwrap();
1123 for k in 0..8u32 { m.insert(k, k).unwrap(); }
1124 m.remove(&3);
1125 assert_eq!(m.tombstone_count(), 1);
1126 assert!(m.insert(99, 99).is_ok(),
1130 "tombstone reuse must let insert succeed");
1131 assert_eq!(m.get(&99), Some(99));
1132 assert_eq!(m.tombstone_count(), 0,
1133 "successful tombstone reuse must decrement the counter");
1134 std::fs::remove_file(&p).ok();
1135 }
1136
1137 #[test]
1138 fn compact_still_useful_for_remove_heavy_workload() {
1139 let p = tmp("compact-still-useful");
1145 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 32).unwrap();
1146 for k in 0..20u32 { m.insert(k, k).unwrap(); }
1147 for k in 0..15u32 { m.remove(&k); }
1148 assert_eq!(m.tombstone_count(), 15);
1149 let reclaimed = m.compact().unwrap();
1150 assert_eq!(reclaimed, 15);
1151 assert_eq!(m.tombstone_count(), 0);
1152 assert_eq!(m.len(), 5);
1153 std::fs::remove_file(&p).ok();
1154 }
1155
1156 #[test]
1157 fn should_compact_threshold_logic() {
1158 let p = tmp("should-compact");
1159 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 100).unwrap();
1160 assert!(!m.should_compact(0.01));
1162 for k in 0..50u32 { m.insert(k, k).unwrap(); }
1164 for k in 0..30u32 { m.remove(&k); }
1165 assert_eq!(m.tombstone_count(), 30);
1166 assert!(m.should_compact(0.30));
1167 assert!(m.should_compact(0.29));
1168 assert!(!m.should_compact(0.31));
1169 std::fs::remove_file(&p).ok();
1170 }
1171
1172 #[test]
1173 fn compact_persists_across_reopen() {
1174 let p = tmp("compact-disk");
1175 {
1176 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 16).unwrap();
1177 for k in 0..6u32 { m.insert(k, k * 10).unwrap(); }
1178 for k in [0u32, 2, 4] { m.remove(&k); }
1179 m.compact().unwrap();
1180 m.flush().unwrap();
1181 }
1182 let m2: SharedHashMap<u32, u32> = SharedHashMap::open(&p, 16).unwrap();
1183 assert_eq!(m2.len(), 3);
1184 assert_eq!(m2.tombstone_count(), 0);
1185 for k in [1u32, 3, 5] {
1186 assert_eq!(m2.get(&k), Some(k * 10));
1187 }
1188 for k in [0u32, 2, 4] {
1189 assert_eq!(m2.get(&k), None);
1190 }
1191 std::fs::remove_file(&p).ok();
1192 }
1193}