diff --git a/src/lru_lockmap.rs b/src/lru_lockmap.rs index ddf6c94..e5239a8 100644 --- a/src/lru_lockmap.rs +++ b/src/lru_lockmap.rs @@ -121,6 +121,7 @@ struct LruShardInner { head: *mut State, tail: *mut State, max_size: usize, + max_evict: usize, } // SAFETY: The raw pointers (head, tail, prev, next) are only accessed while @@ -134,6 +135,7 @@ impl LruShardInner { head: std::ptr::null_mut(), tail: std::ptr::null_mut(), max_size, + max_evict: usize::MAX, } } @@ -208,7 +210,12 @@ impl LruShardInner { /// be evicted even though it is at the head of the list. fn try_evict(&mut self, current: *mut State) { let mut cursor = self.tail; - while self.table.len() > self.max_size && !cursor.is_null() && cursor != current { + let mut evicted = 0; + while self.table.len() > self.max_size + && !cursor.is_null() + && cursor != current + && evicted < self.max_evict + { let prev = unsafe { *(*cursor).prev.get() }; let state = unsafe { &*cursor }; @@ -224,6 +231,7 @@ impl LruShardInner { let _ = entry.remove(); } + evicted += 1; cursor = prev; } } @@ -259,6 +267,10 @@ impl LruShardMap { fn set_max_size(&self, max_size: usize) { self.inner.lock().unwrap().max_size = max_size; } + + fn set_max_evict(&self, max_evict: usize) { + self.inner.lock().unwrap().max_evict = max_evict.max(1); + } } // --------------------------------------------------------------------------- @@ -390,6 +402,27 @@ impl LruLockMap { } } + /// Sets the maximum number of entries that can be evicted in a single `try_evict` call. + /// + /// The limit is applied **per shard**. The default is `usize::MAX`, meaning + /// no limit is enforced and eviction continues until the shard is within + /// capacity or all candidates are exhausted. + /// + /// A value of `0` is treated as `1`. + /// + /// # Examples + /// + /// ``` + /// # use lockmap::LruLockMap; + /// let cache = LruLockMap::::with_options(10, 10, 1); + /// cache.set_max_evict(3); + /// ``` + pub fn set_max_evict(&self, max_evict: usize) { + for shard in &self.shards { + shard.set_max_evict(max_evict); + } + } + // --- shard routing --- #[inline(always)] @@ -1629,4 +1662,98 @@ mod tests { THREADS as u32 * OPS_PER_THREAD as u32 ); } + + // --- max_evict --- + + #[test] + fn test_max_evict_default_unlimited() { + let cache = LruLockMap::::with_options(10, 10, 1); + for i in 0..5u32 { + cache.insert(i, i * 10); + } + assert_eq!(cache.len(), 5); + + // Shrink max_size to 1 — now 4 entries over capacity + cache.set_max_size(1); + + // Insert triggers eviction. Default max_evict=usize::MAX should evict all + // excess entries until within capacity (only the new entry remains). + cache.insert(5, 50); + assert_eq!(cache.len(), 1); + assert_eq!(cache.get(&5), Some(50)); + assert!(cache.get(&0).is_none()); + assert!(cache.get(&1).is_none()); + assert!(cache.get(&2).is_none()); + assert!(cache.get(&3).is_none()); + assert!(cache.get(&4).is_none()); + } + + #[test] + fn test_max_evict_limited() { + let cache = LruLockMap::::with_options(2, 2, 1); + cache.set_max_evict(1); + cache.insert(1, 10); + cache.insert(2, 20); + + // Insert key 3, max_size=2 so we need to evict. max_evict=1 means only 1 can be evicted. + cache.insert(3, 30); + // Only key 1 (LRU) should be evicted, key 2 and 3 remain + assert_eq!(cache.get(&1), None); + assert_eq!(cache.get(&2), Some(20)); + assert_eq!(cache.get(&3), Some(30)); + } + + #[test] + fn test_max_evict_zero_treated_as_one() { + let cache = LruLockMap::::with_options(2, 2, 1); + cache.set_max_evict(0); // should be treated as 1 + cache.insert(1, 10); + cache.insert(2, 20); + cache.insert(3, 30); + assert_eq!(cache.get(&1), None); // key 1 evicted + assert_eq!(cache.get(&2), Some(20)); + assert_eq!(cache.get(&3), Some(30)); + } + + #[test] + fn test_max_evict_still_respects_in_use() { + let cache = LruLockMap::::with_options(1, 1, 1); + cache.set_max_evict(1); + cache.insert(1, 10); + + let _entry = cache.entry(1); // refcnt > 0, cannot be evicted + + cache.insert(2, 20); // need to evict key 1 but it's in use, and max_evict=1 + assert_eq!(*_entry.get(), Some(10)); // key 1 still present (in use) + assert_eq!(cache.get(&2), Some(20)); + } + + #[test] + fn test_max_evict_after_shrinking_capacity() { + let cache = LruLockMap::::with_options(10, 10, 1); + for i in 0..5u32 { + cache.insert(i, i * 10); + } + assert_eq!(cache.len(), 5); + + // Shrink max_size: 5 entries but max_size=2 → 3 slots over capacity + cache.set_max_size(2); + cache.set_max_evict(2); + + // Insert triggers eviction, but only up to max_evict=2 + cache.insert(5, 50); + + // 5 initial + 1 new - 2 evicted = 4 remaining + assert_eq!(cache.len(), 4); + + // LRU entries 0 and 1 should be evicted + assert_eq!(cache.get(&0), None); + assert_eq!(cache.get(&1), None); + + // Remaining entries should still be present + assert!(cache.get(&2).is_some()); + assert!(cache.get(&3).is_some()); + assert!(cache.get(&4).is_some()); + assert_eq!(cache.get(&5), Some(50)); + } }