Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 15 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand Down Expand Up @@ -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<u64, u64> = new_cache(256);
let n = iters(1000);

Expand All @@ -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;
}
Expand All @@ -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<u64, u64> = new_cache(1024);
let n = iters(100);

Expand Down Expand Up @@ -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;
}
Expand Down
38 changes: 38 additions & 0 deletions src/stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u64, u64, BH> =
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);
}
}