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> {
214 Self::check_layout()?;
215 assert!(capacity >= 2);
216 let total = map_file_size(capacity);
217 let file = OpenOptions::new()
218 .read(true).write(true).create(true).truncate(true)
219 .open(path.as_ref())?;
220 file.set_len(total as u64)?;
221 let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
222 let hdr_ptr = mmap.as_mut_ptr() as *mut MapHeader;
223 unsafe {
224 std::ptr::write_bytes(hdr_ptr as *mut u8, 0, size_of::<MapHeader>());
225 (*hdr_ptr).magic = MAP_MAGIC;
226 (*hdr_ptr).capacity = capacity as u32;
227 (*hdr_ptr).key_size = size_of::<K>() as u32;
228 (*hdr_ptr).value_size = size_of::<V>() as u32;
229 }
232 for i in 0..capacity {
233 let slot_ptr = unsafe {
234 mmap.as_mut_ptr()
235 .add(size_of::<MapHeader>())
236 .add(i * size_of::<MapSlot>())
237 } as *mut MapSlot;
238 unsafe {
239 std::ptr::write(slot_ptr, MapSlot {
240 state: AtomicU8::new(SLOT_EMPTY),
241 _pad1: [0; 3],
242 version: AtomicU32::new(0),
243 hash: AtomicU64::new(0),
244 payload: [0u8; MAP_PAYLOAD_BYTES],
245 });
246 }
247 }
248 Ok(Self {
249 _file: file, mmap, capacity,
250 cap_mask: capacity.wrapping_sub(1),
251 cap_is_pow2: capacity.is_power_of_two(),
252 _phantom: PhantomData,
253 header_sidecar: subetha_core::HandshakeHeader::new(),
254 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
255 })
256 }
257
258 pub fn open(path: impl AsRef<Path>, expected_capacity: usize) -> Result<Self, MapError> {
259 Self::check_layout()?;
260 let total = map_file_size(expected_capacity);
261 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
262 if file.metadata()?.len() < total as u64 {
263 return Err(MapError::LayoutMismatch);
264 }
265 let mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
266 let hdr = unsafe { &*(mmap.as_ptr() as *const MapHeader) };
267 if hdr.magic != MAP_MAGIC
268 || hdr.capacity != expected_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: expected_capacity,
276 cap_mask: expected_capacity.wrapping_sub(1),
277 cap_is_pow2: expected_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 #[inline]
292 fn wrap(&self, i: usize) -> usize {
293 if self.cap_is_pow2 { i & self.cap_mask } else { i % self.capacity }
294 }
295
296 #[inline]
297 pub fn capacity(&self) -> usize { self.capacity }
298
299 #[inline]
300 pub fn len(&self) -> usize {
301 self.header().count.load(Ordering::Acquire) as usize
302 }
303
304 #[inline]
305 pub fn is_empty(&self) -> bool { self.len() == 0 }
306
307 fn header(&self) -> &MapHeader {
308 unsafe { &*(self.mmap.as_ptr() as *const MapHeader) }
309 }
310
311 fn slot(&self, idx: usize) -> &MapSlot {
312 let base = unsafe { self.mmap.as_ptr().add(size_of::<MapHeader>()) };
313 unsafe { &*(base.add(idx * size_of::<MapSlot>()) as *const MapSlot) }
314 }
315
316 fn hash_key(k: &K) -> u64 {
317 let bytes = unsafe {
318 std::slice::from_raw_parts(k as *const K as *const u8, size_of::<K>())
319 };
320 fnv1a_64(bytes)
321 }
322
323 fn write_payload(&self, slot_idx: usize, k: &K, v: &V) {
325 let slot = self.slot(slot_idx);
326 slot.version.fetch_add(1, Ordering::AcqRel); let base = unsafe {
328 self.mmap.as_ptr()
329 .add(size_of::<MapHeader>())
330 .add(slot_idx * size_of::<MapSlot>())
331 .add(std::mem::offset_of!(MapSlot, payload))
332 as *mut u8
333 };
334 unsafe {
335 std::ptr::copy_nonoverlapping(
337 k as *const K as *const u8, base, size_of::<K>(),
338 );
339 std::ptr::copy_nonoverlapping(
340 v as *const V as *const u8,
341 base.add(size_of::<K>()),
342 size_of::<V>(),
343 );
344 }
345 slot.version.fetch_add(1, Ordering::AcqRel); }
347
348 fn read_payload(&self, slot_idx: usize) -> (K, V) {
350 let slot = self.slot(slot_idx);
351 loop {
352 let v1 = slot.version.load(Ordering::Acquire);
353 if v1 & 1 != 0 {
354 std::hint::spin_loop();
355 continue;
356 }
357 let mut k = std::mem::MaybeUninit::<K>::uninit();
358 let mut v = std::mem::MaybeUninit::<V>::uninit();
359 let src = unsafe {
360 self.mmap.as_ptr()
361 .add(size_of::<MapHeader>())
362 .add(slot_idx * size_of::<MapSlot>())
363 .add(std::mem::offset_of!(MapSlot, payload))
364 };
365 unsafe {
366 std::ptr::copy_nonoverlapping(
367 src, k.as_mut_ptr() as *mut u8, size_of::<K>(),
368 );
369 std::ptr::copy_nonoverlapping(
370 src.add(size_of::<K>()),
371 v.as_mut_ptr() as *mut u8,
372 size_of::<V>(),
373 );
374 }
375 let v2 = slot.version.load(Ordering::Acquire);
376 if v1 == v2 {
377 return unsafe { (k.assume_init(), v.assume_init()) };
378 }
379 }
380 }
381
382 pub fn insert(&self, key: K, value: V) -> Result<InsertOutcome, MapError> {
399 let r = self.insert_inner(key, value);
400 self.ring_sidecar.push_op(
401 crate::sidecar_ops::hash_map::OP_INSERT,
402 if matches!(r, Err(MapError::Full)) { 1 } else { 0 },
403 );
404 r
405 }
406
407 fn insert_inner(&self, key: K, value: V) -> Result<InsertOutcome, MapError> {
408 let h = Self::hash_key(&key);
409 let start = self.wrap(h as usize);
410 let mut first_tombstone: Option<usize> = None;
414 for i in 0..self.capacity {
415 let idx = self.wrap(start + i);
416 let slot = self.slot(idx);
417 let state = slot.state.load(Ordering::Acquire);
418 if state == SLOT_EMPTY {
419 if let Some(tomb_idx) = first_tombstone
423 && self.try_claim_tombstone(tomb_idx, h, &key, &value) {
424 return Ok(InsertOutcome::Inserted);
425 }
426 if slot.state.compare_exchange(
429 SLOT_EMPTY, SLOT_OCCUPIED,
430 Ordering::AcqRel, Ordering::Acquire,
431 ).is_ok() {
432 slot.hash.store(h, Ordering::Release);
433 self.write_payload(idx, &key, &value);
434 self.header().count.fetch_add(1, Ordering::AcqRel);
435 return Ok(InsertOutcome::Inserted);
436 }
437 let now = slot.state.load(Ordering::Acquire);
440 if now == SLOT_OCCUPIED {
441 let cached = slot.hash.load(Ordering::Acquire);
442 if cached == h {
443 let (k, _) = self.read_payload(idx);
444 if k == key {
445 self.write_payload(idx, &key, &value);
446 return Ok(InsertOutcome::Updated);
447 }
448 }
449 } else if now == SLOT_TOMBSTONE && first_tombstone.is_none() {
450 first_tombstone = Some(idx);
451 }
452 continue;
453 }
454 if state == SLOT_OCCUPIED {
455 let cached = slot.hash.load(Ordering::Acquire);
456 if cached == h {
457 let (k, _) = self.read_payload(idx);
458 if k == key {
459 self.write_payload(idx, &key, &value);
460 return Ok(InsertOutcome::Updated);
461 }
462 }
463 continue;
464 }
465 if first_tombstone.is_none() {
468 first_tombstone = Some(idx);
469 }
470 }
471 if let Some(tomb_idx) = first_tombstone
474 && self.try_claim_tombstone(tomb_idx, h, &key, &value) {
475 return Ok(InsertOutcome::Inserted);
476 }
477 Err(MapError::Full)
478 }
479
480 #[inline]
488 fn try_claim_tombstone(&self, tomb_idx: usize, h: u64, key: &K, value: &V) -> bool {
489 let tomb_slot = self.slot(tomb_idx);
490 if tomb_slot.state.compare_exchange(
491 SLOT_TOMBSTONE, SLOT_OCCUPIED,
492 Ordering::AcqRel, Ordering::Acquire,
493 ).is_err() {
494 return false;
495 }
496 tomb_slot.hash.store(h, Ordering::Release);
497 self.write_payload(tomb_idx, key, value);
498 self.header().count.fetch_add(1, Ordering::AcqRel);
499 loop {
505 let cur = self.header().tombstones.load(Ordering::Acquire);
506 if cur == 0 { break; }
507 if self.header().tombstones.compare_exchange(
508 cur, cur - 1, Ordering::AcqRel, Ordering::Acquire,
509 ).is_ok() {
510 break;
511 }
512 }
513 true
514 }
515
516 pub fn get(&self, key: &K) -> Option<V> {
518 let r = self.get_inner(key);
519 self.ring_sidecar.push_op(
520 crate::sidecar_ops::hash_map::OP_GET,
521 if r.is_none() { 2 } else { 0 },
522 );
523 r
524 }
525
526 fn get_inner(&self, key: &K) -> Option<V> {
530 let h = Self::hash_key(key);
531 let start = self.wrap(h as usize);
532 for i in 0..self.capacity {
533 let idx = self.wrap(start + i);
534 let slot = self.slot(idx);
535 let state = slot.state.load(Ordering::Acquire);
536 if state == SLOT_EMPTY {
537 return None;
538 }
539 if state == SLOT_OCCUPIED {
540 let cached = slot.hash.load(Ordering::Acquire);
541 if cached == h {
542 let (k, v) = self.read_payload(idx);
543 if k == *key {
544 return Some(v);
545 }
546 }
547 }
548 }
550 None
551 }
552
553 pub fn contains_key(&self, key: &K) -> bool {
555 let r = self.get_inner(key);
556 self.ring_sidecar.push_op(
557 crate::sidecar_ops::hash_map::OP_CONTAINS,
558 if r.is_none() { 2 } else { 0 },
559 );
560 r.is_some()
561 }
562
563 pub fn remove(&self, key: &K) -> Option<V> {
565 let r = self.remove_inner(key);
566 self.ring_sidecar.push_op(
567 crate::sidecar_ops::hash_map::OP_REMOVE,
568 if r.is_none() { 2 } else { 0 },
569 );
570 r
571 }
572
573 fn remove_inner(&self, key: &K) -> Option<V> {
574 let h = Self::hash_key(key);
575 let start = self.wrap(h as usize);
576 for i in 0..self.capacity {
577 let idx = self.wrap(start + i);
578 let slot = self.slot(idx);
579 let state = slot.state.load(Ordering::Acquire);
580 if state == SLOT_EMPTY { return None; }
581 if state == SLOT_OCCUPIED {
582 let cached = slot.hash.load(Ordering::Acquire);
583 if cached == h {
584 let (k, v) = self.read_payload(idx);
585 if k == *key {
586 if slot.state.compare_exchange(
587 SLOT_OCCUPIED, SLOT_TOMBSTONE,
588 Ordering::AcqRel, Ordering::Acquire,
589 ).is_ok() {
590 self.header().count.fetch_sub(1, Ordering::AcqRel);
591 self.header().tombstones.fetch_add(1, Ordering::AcqRel);
592 return Some(v);
593 }
594 return None;
596 }
597 }
598 }
599 }
600 None
601 }
602
603 pub fn clear(&self) {
608 for i in 0..self.capacity {
609 let slot = self.slot(i);
610 slot.state.store(SLOT_EMPTY, Ordering::Release);
611 }
612 self.header().count.store(0, Ordering::Release);
613 self.header().tombstones.store(0, Ordering::Release);
614 self.ring_sidecar
615 .push_op(crate::sidecar_ops::hash_map::OP_CLEAR, 0);
616 }
617
618 #[inline]
621 pub fn tombstone_count(&self) -> usize {
622 self.header().tombstones.load(Ordering::Acquire) as usize
623 }
624
625 pub fn should_compact(&self, threshold_fraction: f64) -> bool {
630 debug_assert!(
631 (0.0..=1.0).contains(&threshold_fraction),
632 "threshold_fraction must be in [0, 1]; got {threshold_fraction}",
633 );
634 let tombs = self.tombstone_count() as f64;
635 tombs / self.capacity as f64 >= threshold_fraction
636 }
637
638 pub fn compact(&self) -> Result<usize, MapError> {
667 self.ring_sidecar
668 .push_op(crate::sidecar_ops::hash_map::OP_COMPACT, 0);
669 let mut live: Vec<(K, V)> = Vec::with_capacity(self.len());
670 let mut reclaimed = 0usize;
671 for i in 0..self.capacity {
672 let slot = self.slot(i);
673 let s = slot.state.load(Ordering::Acquire);
674 if s == SLOT_OCCUPIED {
675 live.push(self.read_payload(i));
676 } else if s == SLOT_TOMBSTONE {
677 reclaimed += 1;
678 }
679 }
680 for i in 0..self.capacity {
681 let slot = self.slot(i);
682 slot.state.store(SLOT_EMPTY, Ordering::Release);
683 }
684 self.header().count.store(0, Ordering::Release);
685 self.header().tombstones.store(0, Ordering::Release);
686 for (k, v) in live {
687 self.insert(k, v)?;
691 }
692 Ok(reclaimed)
693 }
694
695 pub fn snapshot(&self) -> Vec<(K, V)> {
698 let mut out = Vec::with_capacity(self.len());
699 for i in 0..self.capacity {
700 let slot = self.slot(i);
701 if slot.state.load(Ordering::Acquire) == SLOT_OCCUPIED {
702 out.push(self.read_payload(i));
703 }
704 }
705 out
706 }
707
708 pub fn load_factor(&self) -> f64 {
710 self.len() as f64 / self.capacity as f64
711 }
712
713 pub fn flush(&self) -> Result<(), MapError> {
714 self.mmap.flush()?;
715 Ok(())
716 }
717
718 pub fn flush_async(&self) -> Result<(), MapError> {
722 self.mmap.flush_async()?;
723 Ok(())
724 }
725}
726
727#[cfg(test)]
728mod tests {
729 use super::*;
730 use std::sync::Arc;
731 use std::thread;
732
733 fn tmp(name: &str) -> std::path::PathBuf {
734 let mut p = std::env::temp_dir();
735 let pid = std::process::id();
736 p.push(format!("subetha-hashmap-{name}-{pid}.bin"));
737 p
738 }
739
740 #[test]
741 fn create_initial_empty() {
742 let p = tmp("init");
743 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 32).unwrap();
744 assert_eq!(m.capacity(), 32);
745 assert_eq!(m.len(), 0);
746 assert!(m.is_empty());
747 assert_eq!(m.get(&42), None);
748 std::fs::remove_file(&p).ok();
749 }
750
751 #[test]
752 fn insert_and_get_round_trip() {
753 let p = tmp("rt");
754 let m: SharedHashMap<u32, u64> = SharedHashMap::create(&p, 32).unwrap();
755 assert_eq!(m.insert(1, 100).unwrap(), InsertOutcome::Inserted);
756 assert_eq!(m.insert(2, 200).unwrap(), InsertOutcome::Inserted);
757 assert_eq!(m.insert(3, 300).unwrap(), InsertOutcome::Inserted);
758 assert_eq!(m.len(), 3);
759 assert_eq!(m.get(&1), Some(100));
760 assert_eq!(m.get(&2), Some(200));
761 assert_eq!(m.get(&3), Some(300));
762 assert_eq!(m.get(&999), None);
763 std::fs::remove_file(&p).ok();
764 }
765
766 #[test]
767 fn duplicate_insert_updates_value() {
768 let p = tmp("dup");
769 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 16).unwrap();
770 assert_eq!(m.insert(7, 100).unwrap(), InsertOutcome::Inserted);
771 assert_eq!(m.insert(7, 200).unwrap(), InsertOutcome::Updated);
772 assert_eq!(m.len(), 1);
773 assert_eq!(m.get(&7), Some(200));
774 std::fs::remove_file(&p).ok();
775 }
776
777 #[test]
778 fn remove_returns_value_and_decrements_count() {
779 let p = tmp("rm");
780 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 16).unwrap();
781 m.insert(1, 10).unwrap();
782 m.insert(2, 20).unwrap();
783 assert_eq!(m.remove(&1), Some(10));
784 assert_eq!(m.len(), 1);
785 assert_eq!(m.get(&1), None);
786 assert_eq!(m.get(&2), Some(20));
787 assert_eq!(m.remove(&999), None);
788 std::fs::remove_file(&p).ok();
789 }
790
791 #[test]
792 fn tombstone_does_not_break_probing() {
793 let p = tmp("tomb");
794 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 8).unwrap();
795 for k in 0..6u32 { m.insert(k, k * 10).unwrap(); }
799 m.remove(&2);
801 for k in [0u32, 1, 3, 4, 5] {
802 assert_eq!(m.get(&k), Some(k * 10),
803 "key {k} should still be findable past the tombstone");
804 }
805 assert_eq!(m.get(&2), None);
806 std::fs::remove_file(&p).ok();
807 }
808
809 #[test]
810 fn full_map_returns_error_on_new_key() {
811 let p = tmp("full");
812 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 4).unwrap();
813 m.insert(1, 1).unwrap();
814 m.insert(2, 2).unwrap();
815 m.insert(3, 3).unwrap();
816 m.insert(4, 4).unwrap();
817 assert_eq!(m.insert(5, 5).err(), Some(MapError::Full));
818 assert_eq!(m.insert(1, 100).unwrap(), InsertOutcome::Updated);
820 std::fs::remove_file(&p).ok();
821 }
822
823 #[test]
824 fn clear_resets_to_empty() {
825 let p = tmp("clear");
826 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 16).unwrap();
827 for k in 0..5u32 { m.insert(k, k).unwrap(); }
828 assert_eq!(m.len(), 5);
829 m.clear();
830 assert_eq!(m.len(), 0);
831 assert_eq!(m.get(&0), None);
832 m.insert(99, 99).unwrap();
833 assert_eq!(m.get(&99), Some(99));
834 std::fs::remove_file(&p).ok();
835 }
836
837 #[test]
838 fn snapshot_collects_all_present_pairs() {
839 let p = tmp("snap");
840 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 16).unwrap();
841 m.insert(1, 10).unwrap();
842 m.insert(2, 20).unwrap();
843 m.insert(3, 30).unwrap();
844 m.remove(&2);
845 let mut snap = m.snapshot();
846 snap.sort();
847 assert_eq!(snap, vec![(1, 10), (3, 30)]);
848 std::fs::remove_file(&p).ok();
849 }
850
851 #[test]
852 fn cross_handle_visibility() {
853 let p = tmp("cross-handle");
854 let writer: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 16).unwrap();
855 let reader: SharedHashMap<u32, u32> = SharedHashMap::open(&p, 16).unwrap();
856 writer.insert(42, 4242).unwrap();
857 assert_eq!(reader.get(&42), Some(4242));
858 reader.insert(7, 77).unwrap();
859 assert_eq!(writer.get(&7), Some(77));
860 writer.remove(&42);
861 assert_eq!(reader.get(&42), None);
862 std::fs::remove_file(&p).ok();
863 }
864
865 #[test]
866 fn concurrent_inserters_all_keys_present() {
867 let p = tmp("concurrent");
868 let m: Arc<SharedHashMap<u32, u32>> = Arc::new(SharedHashMap::create(&p, 1024).unwrap());
869 let n_threads = 4;
870 let per_thread = 100u32;
871 let mut handles = vec![];
872 for t in 0..n_threads {
873 let m = m.clone();
874 handles.push(thread::spawn(move || {
875 for i in 0..per_thread {
876 let key = (t as u32) * per_thread + i;
877 m.insert(key, key * 10).unwrap();
878 }
879 }));
880 }
881 for h in handles { h.join().unwrap(); }
882 assert_eq!(m.len(), n_threads * per_thread as usize);
883 for t in 0..n_threads as u32 {
884 for i in 0..per_thread {
885 let key = t * per_thread + i;
886 assert_eq!(m.get(&key), Some(key * 10),
887 "key {key} should be present with value {}", key * 10);
888 }
889 }
890 std::fs::remove_file(&p).ok();
891 }
892
893 #[test]
894 fn struct_key_and_value_round_trip() {
895 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
896 #[repr(C)]
897 struct UserId { realm: u32, user: u32 }
898 #[derive(Clone, Copy, Debug, PartialEq)]
899 #[repr(C)]
900 struct Session { token: u64, expires_us: u64 }
901 let p = tmp("struct");
902 let m: SharedHashMap<UserId, Session> = SharedHashMap::create(&p, 32).unwrap();
903 let k = UserId { realm: 1, user: 42 };
904 let v = Session { token: 0xDEAD_BEEF, expires_us: 9_999_999_999 };
905 m.insert(k, v).unwrap();
906 assert_eq!(m.get(&k), Some(v));
907 std::fs::remove_file(&p).ok();
908 }
909
910 #[test]
911 fn payload_too_large_at_create() {
912 #[allow(dead_code)] struct BigKey([u8; 64]);
914 impl Copy for BigKey {}
915 impl Clone for BigKey { fn clone(&self) -> Self { *self } }
916 impl PartialEq for BigKey { fn eq(&self, _: &Self) -> bool { true } }
917 impl Eq for BigKey {}
918 let p = tmp("too-large");
919 let r = SharedHashMap::<BigKey, u32>::create(&p, 4);
920 assert_eq!(r.err(), Some(MapError::PayloadTooLarge));
921 std::fs::remove_file(&p).ok();
922 }
923
924 #[test]
925 fn disk_persistence_survives_reopen() {
926 let p = tmp("disk");
927 {
928 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 16).unwrap();
929 for k in 0..5u32 { m.insert(k, k * 100).unwrap(); }
930 m.remove(&2);
931 m.flush().unwrap();
932 }
933 let m2: SharedHashMap<u32, u32> = SharedHashMap::open(&p, 16).unwrap();
934 assert_eq!(m2.len(), 4);
935 assert_eq!(m2.get(&0), Some(0));
936 assert_eq!(m2.get(&1), Some(100));
937 assert_eq!(m2.get(&2), None);
938 assert_eq!(m2.get(&3), Some(300));
939 assert_eq!(m2.get(&4), Some(400));
940 std::fs::remove_file(&p).ok();
941 }
942
943 #[test]
944 fn fnv1a_64_is_deterministic() {
945 let h1 = fnv1a_64(b"adaptive-prims");
947 let h2 = fnv1a_64(b"adaptive-prims");
948 assert_eq!(h1, h2);
949 let h3 = fnv1a_64(b"ADAPTIVE-PRIMS");
951 assert_ne!(h1, h3);
952 }
953
954 #[test]
955 fn load_factor_reports_correctly() {
956 let p = tmp("load");
957 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 10).unwrap();
958 for k in 0..3u32 { m.insert(k, k).unwrap(); }
959 assert_eq!(m.load_factor(), 0.3);
960 std::fs::remove_file(&p).ok();
961 }
962
963 #[test]
964 fn remove_bumps_tombstone_counter() {
965 let p = tmp("tomb-counter");
966 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 16).unwrap();
967 assert_eq!(m.tombstone_count(), 0);
968 for k in 0..5u32 { m.insert(k, k).unwrap(); }
969 assert_eq!(m.tombstone_count(), 0);
970 m.remove(&1);
971 m.remove(&3);
972 assert_eq!(m.tombstone_count(), 2);
973 m.remove(&999);
975 assert_eq!(m.tombstone_count(), 2);
976 std::fs::remove_file(&p).ok();
977 }
978
979 #[test]
980 fn compact_on_empty_map_is_noop() {
981 let p = tmp("compact-empty");
982 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 16).unwrap();
983 assert_eq!(m.compact().unwrap(), 0);
984 assert_eq!(m.len(), 0);
985 assert_eq!(m.tombstone_count(), 0);
986 m.insert(7, 70).unwrap();
988 assert_eq!(m.get(&7), Some(70));
989 std::fs::remove_file(&p).ok();
990 }
991
992 #[test]
993 fn compact_reclaims_tombstones() {
994 let p = tmp("compact-reclaim");
995 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 16).unwrap();
996 for k in 0..10u32 { m.insert(k, k * 10).unwrap(); }
997 for k in [0u32, 2, 4, 6, 8] { m.remove(&k); }
998 assert_eq!(m.tombstone_count(), 5);
999 let reclaimed = m.compact().unwrap();
1000 assert_eq!(reclaimed, 5);
1001 assert_eq!(m.tombstone_count(), 0);
1002 assert_eq!(m.len(), 5);
1003 std::fs::remove_file(&p).ok();
1004 }
1005
1006 #[test]
1007 fn compact_preserves_all_live_pairs() {
1008 let p = tmp("compact-preserve");
1009 let m: SharedHashMap<u32, u64> = SharedHashMap::create(&p, 32).unwrap();
1010 for k in 0..20u32 { m.insert(k, (k as u64) * 1000).unwrap(); }
1013 for k in (0..20u32).filter(|k| k % 2 == 0) { m.remove(&k); }
1014 let pre: Vec<(u32, u64)> = {
1015 let mut s = m.snapshot();
1016 s.sort();
1017 s
1018 };
1019 m.compact().unwrap();
1020 let post: Vec<(u32, u64)> = {
1021 let mut s = m.snapshot();
1022 s.sort();
1023 s
1024 };
1025 assert_eq!(pre, post,
1026 "compact must preserve every live (K, V) pair exactly");
1027 for (k, v) in &post {
1029 assert_eq!(m.get(k), Some(*v));
1030 }
1031 std::fs::remove_file(&p).ok();
1032 }
1033
1034 #[test]
1035 fn compact_reclaims_after_heavy_churn() {
1036 let p = tmp("compact-churn");
1041 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 64).unwrap();
1042 for k in 0..32u32 { m.insert(k, k).unwrap(); }
1043 for round in 0..16u32 {
1044 m.remove(&round);
1045 let new_key = 100 + round;
1046 m.insert(new_key, new_key).unwrap();
1047 }
1048 assert_eq!(m.tombstone_count(), 16);
1049 let live_before = m.len();
1050 let reclaimed = m.compact().unwrap();
1051 assert_eq!(reclaimed, 16);
1052 assert_eq!(m.tombstone_count(), 0);
1053 assert_eq!(m.len(), live_before);
1054 for round in 0..16u32 {
1055 assert_eq!(m.get(&round), None);
1056 let new_key = 100 + round;
1057 assert_eq!(m.get(&new_key), Some(new_key));
1058 }
1059 std::fs::remove_file(&p).ok();
1060 }
1061
1062 #[test]
1063 fn tombstone_reuse_avoids_full_after_remove() {
1064 let p = tmp("reuse-avoids-full");
1069 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 8).unwrap();
1070 for k in 0..8u32 { m.insert(k, k).unwrap(); }
1071 m.remove(&3);
1072 assert_eq!(m.tombstone_count(), 1);
1073 assert!(m.insert(99, 99).is_ok(),
1077 "tombstone reuse must let insert succeed");
1078 assert_eq!(m.get(&99), Some(99));
1079 assert_eq!(m.tombstone_count(), 0,
1080 "successful tombstone reuse must decrement the counter");
1081 std::fs::remove_file(&p).ok();
1082 }
1083
1084 #[test]
1085 fn compact_still_useful_for_remove_heavy_workload() {
1086 let p = tmp("compact-still-useful");
1092 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 32).unwrap();
1093 for k in 0..20u32 { m.insert(k, k).unwrap(); }
1094 for k in 0..15u32 { m.remove(&k); }
1095 assert_eq!(m.tombstone_count(), 15);
1096 let reclaimed = m.compact().unwrap();
1097 assert_eq!(reclaimed, 15);
1098 assert_eq!(m.tombstone_count(), 0);
1099 assert_eq!(m.len(), 5);
1100 std::fs::remove_file(&p).ok();
1101 }
1102
1103 #[test]
1104 fn should_compact_threshold_logic() {
1105 let p = tmp("should-compact");
1106 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 100).unwrap();
1107 assert!(!m.should_compact(0.01));
1109 for k in 0..50u32 { m.insert(k, k).unwrap(); }
1111 for k in 0..30u32 { m.remove(&k); }
1112 assert_eq!(m.tombstone_count(), 30);
1113 assert!(m.should_compact(0.30));
1114 assert!(m.should_compact(0.29));
1115 assert!(!m.should_compact(0.31));
1116 std::fs::remove_file(&p).ok();
1117 }
1118
1119 #[test]
1120 fn compact_persists_across_reopen() {
1121 let p = tmp("compact-disk");
1122 {
1123 let m: SharedHashMap<u32, u32> = SharedHashMap::create(&p, 16).unwrap();
1124 for k in 0..6u32 { m.insert(k, k * 10).unwrap(); }
1125 for k in [0u32, 2, 4] { m.remove(&k); }
1126 m.compact().unwrap();
1127 m.flush().unwrap();
1128 }
1129 let m2: SharedHashMap<u32, u32> = SharedHashMap::open(&p, 16).unwrap();
1130 assert_eq!(m2.len(), 3);
1131 assert_eq!(m2.tombstone_count(), 0);
1132 for k in [1u32, 3, 5] {
1133 assert_eq!(m2.get(&k), Some(k * 10));
1134 }
1135 for k in [0u32, 2, 4] {
1136 assert_eq!(m2.get(&k), None);
1137 }
1138 std::fs::remove_file(&p).ok();
1139 }
1140}