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 3d6f3ba..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 /// @@ -978,6 +981,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 +1002,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 +1029,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 +1382,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..4f5563a 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -513,4 +513,42 @@ 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 => _ = 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(); + assert!(total > 0); + assert_eq!(handler.hits() + handler.misses(), 1000 + 1000); + } }