From 00ea88a81362a4b06a21ac8a6aa36e135434cd3c Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sat, 7 Feb 2026 19:38:20 +0100 Subject: [PATCH 1/3] test: add some more tests --- src/lib.rs | 12 ++++++++++++ src/stats.rs | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 3d6f3ba..5cb4d68 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -978,6 +978,11 @@ mod tests { #[test] fn concurrent_read_write() { + // Miri flags the seqlock's speculative read as a data race with concurrent writers. + if cfg!(miri) { + return; + } + let cache: Cache = new_cache(256); let n = iters(1000); @@ -994,6 +999,7 @@ mod tests { #[test] fn seqlock_aba() { + // Miri flags the seqlock's speculative read as a data race with concurrent writers. if cfg!(miri) { return; } @@ -1020,6 +1026,11 @@ mod tests { #[test] fn concurrent_get_or_insert() { + // Miri flags the seqlock's speculative read as a data race with concurrent writers. + if cfg!(miri) { + return; + } + let cache: Cache = new_cache(1024); let n = iters(100); @@ -1368,6 +1379,7 @@ mod tests { #[test] fn epoch_concurrent_seqlock() { + // Miri flags the seqlock's speculative read as a data race with concurrent writers. if cfg!(miri) { return; } diff --git a/src/stats.rs b/src/stats.rs index 1cc781d..8e30b35 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -513,4 +513,37 @@ mod tests { cache.remove(&CollidingKey(99, 99)); assert_eq!(handler.removes(), 1); } + + #[test] + fn concurrent_stats() { + // Miri flags the seqlock's speculative read as a data race with concurrent writers. + if cfg!(miri) { + return; + } + + let handler = Arc::new(CountingStatsHandler::new()); + let stats = Stats::new(Arc::clone(&handler)); + let cache: Cache = Cache::new(1024, Default::default()).with_stats(Some(stats)); + + std::thread::scope(|s| { + for t in 0..4 { + let cache = &cache; + s.spawn(move || { + for i in 0..1000u64 { + match t { + 0 => { cache.insert(i % 100, i); } + 1 => { let _ = cache.get(&(i % 100)); } + 2 => { cache.get_or_insert_with(i % 100, |&k| k * 2); } + _ => { let _ = cache.remove(&(i % 100)); } + } + } + }); + } + }); + + let total = handler.hits() + handler.misses() + handler.inserts() + + handler.updates() + handler.removes() + handler.collisions(); + assert!(total > 0); + assert_eq!(handler.hits() + handler.misses(), 1000 + 1000); + } } From ed957321c3a4bc5a44fb4d52c7b5ea1a5b29f59c Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sat, 7 Feb 2026 19:51:47 +0100 Subject: [PATCH 2/3] Clone + !Drop limitation --- README.md | 7 +++++-- src/lib.rs | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0d120d2..6900316 100644 --- a/README.md +++ b/README.md @@ -79,8 +79,11 @@ This design is ideal for memoization caches where: ## Limitations -- **No iteration**: You cannot iterate over cached entries -- **Eviction on collision**: Hash collisions cause immediate eviction +- **Eviction on collision**: When two keys hash to the same bucket, the older entry is evicted. +- **No iteration**: Individual entries cannot be enumerated. +- **`Clone + !Drop` types**: The lock-free read path uses `!needs_drop` as a proxy for `Copy`, + so types that implement `Clone` with non-trivial logic but don't implement `Drop` will be + bitwise-copied instead of cloned on reads. Avoid using such types as keys or values. ## Feature Flags diff --git a/src/lib.rs b/src/lib.rs index 5cb4d68..70a7283 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -106,6 +106,9 @@ impl CacheConfig for DefaultCacheConfig {} /// /// - **Eviction on collision**: When two keys hash to the same bucket, the older entry is evicted. /// - **No iteration**: Individual entries cannot be enumerated. +/// - **`Clone + !Drop` types**: The lock-free read path uses `!needs_drop` as a proxy for `Copy`, +/// so types that implement `Clone` with non-trivial logic but don't implement `Drop` will be +/// bitwise-copied instead of cloned on reads. Avoid using such types as keys or values. /// /// # Type Parameters /// From 455b5ebfdc19004e99417350cfd5acbfcef0b525 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sat, 7 Feb 2026 19:52:56 +0100 Subject: [PATCH 3/3] fmt --- src/stats.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/stats.rs b/src/stats.rs index 8e30b35..4f5563a 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -523,7 +523,8 @@ mod tests { let handler = Arc::new(CountingStatsHandler::new()); let stats = Stats::new(Arc::clone(&handler)); - let cache: Cache = Cache::new(1024, Default::default()).with_stats(Some(stats)); + let cache: Cache = + Cache::new(1024, Default::default()).with_stats(Some(stats)); std::thread::scope(|s| { for t in 0..4 { @@ -531,18 +532,22 @@ mod tests { s.spawn(move || { for i in 0..1000u64 { match t { - 0 => { cache.insert(i % 100, i); } - 1 => { let _ = cache.get(&(i % 100)); } - 2 => { cache.get_or_insert_with(i % 100, |&k| k * 2); } - _ => { let _ = cache.remove(&(i % 100)); } + 0 => cache.insert(i % 100, i), + 1 => _ = cache.get(&(i % 100)), + 2 => _ = cache.get_or_insert_with(i % 100, |&k| k * 2), + _ => _ = cache.remove(&(i % 100)), } } }); } }); - let total = handler.hits() + handler.misses() + handler.inserts() - + handler.updates() + handler.removes() + handler.collisions(); + let total = handler.hits() + + handler.misses() + + handler.inserts() + + handler.updates() + + handler.removes() + + handler.collisions(); assert!(total > 0); assert_eq!(handler.hits() + handler.misses(), 1000 + 1000); }