extern crate alloc;
use crate::config::SlruCacheConfig;
use crate::entry::CacheEntry;
use crate::list::{List, ListEntry};
use crate::metrics::{CacheMetrics, SlruCacheMetrics};
use alloc::boxed::Box;
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use core::borrow::Borrow;
use core::hash::{BuildHasher, Hash};
use core::num::NonZeroUsize;
#[cfg(feature = "hashbrown")]
use hashbrown::DefaultHashBuilder;
#[cfg(feature = "hashbrown")]
use hashbrown::HashMap;
#[cfg(not(feature = "hashbrown"))]
use std::collections::hash_map::RandomState as DefaultHashBuilder;
#[cfg(not(feature = "hashbrown"))]
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Location {
#[default]
Probationary,
Protected,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct SlruMeta {
pub location: Location,
}
pub(crate) struct SlruInner<K, V, S = DefaultHashBuilder> {
config: SlruCacheConfig,
probationary: List<CacheEntry<K, V, SlruMeta>>,
protected: List<CacheEntry<K, V, SlruMeta>>,
map: HashMap<K, *mut ListEntry<CacheEntry<K, V, SlruMeta>>, S>,
metrics: SlruCacheMetrics,
current_size: u64,
max_size: u64,
}
unsafe impl<K: Send, V: Send, S: Send> Send for SlruInner<K, V, S> {}
unsafe impl<K: Send, V: Send, S: Sync> Sync for SlruInner<K, V, S> {}
impl<K: Hash + Eq, V: Clone, S: BuildHasher> SlruInner<K, V, S> {
#[allow(dead_code)] pub(crate) fn init(config: SlruCacheConfig, hasher: S) -> Self {
let capacity = config.capacity.get();
let protected = config.protected_capacity.get();
assert!(
protected < capacity,
"SlruCacheConfig invalid: protected_capacity ({}) must be strictly less than capacity ({})",
protected,
capacity
);
let probationary_max_size = NonZeroUsize::new(capacity - protected).unwrap();
SlruInner {
config,
probationary: List::new(probationary_max_size),
protected: List::new(config.protected_capacity),
map: HashMap::with_capacity_and_hasher(
config.capacity.get().next_power_of_two(),
hasher,
),
metrics: SlruCacheMetrics::new(config.max_size, config.protected_capacity.get() as u64),
current_size: 0,
max_size: config.max_size,
}
}
#[inline]
pub(crate) fn cap(&self) -> NonZeroUsize {
self.config.capacity
}
#[inline]
pub(crate) fn protected_max_size(&self) -> NonZeroUsize {
self.config.protected_capacity
}
#[inline]
pub(crate) fn len(&self) -> usize {
self.map.len()
}
#[inline]
pub(crate) fn is_empty(&self) -> bool {
self.map.is_empty()
}
#[inline]
pub(crate) fn current_size(&self) -> u64 {
self.current_size
}
#[inline]
pub(crate) fn max_size(&self) -> u64 {
self.max_size
}
#[inline]
pub(crate) fn metrics(&self) -> &SlruCacheMetrics {
&self.metrics
}
unsafe fn promote_to_protected(
&mut self,
node: *mut ListEntry<CacheEntry<K, V, SlruMeta>>,
) -> *mut ListEntry<CacheEntry<K, V, SlruMeta>> {
let boxed_entry = self
.probationary
.remove(node)
.expect("Node should exist in probationary");
if self.protected.len() >= self.protected_max_size().get() {
if self.probationary.len() >= self.probationary.cap().get() {
if let Some(old_entry) = self.probationary.remove_last() {
let old_ptr = Box::into_raw(old_entry);
let cache_entry = (*old_ptr).get_value();
let evicted_size = cache_entry.metadata.size;
self.map.remove(&cache_entry.key);
self.current_size = self.current_size.saturating_sub(evicted_size);
self.metrics.record_probationary_eviction(evicted_size);
let _ = Box::from_raw(old_ptr);
}
}
self.demote_lru_protected();
}
let entry_ptr = Box::into_raw(boxed_entry);
let cache_entry = (*entry_ptr).get_value_mut();
cache_entry.metadata.algorithm.location = Location::Protected;
cache_entry.touch();
if let Some(node_ptr) = self.map.get_mut(&cache_entry.key) {
*node_ptr = entry_ptr;
}
unsafe {
self.protected.attach_from_other_list(entry_ptr);
}
entry_ptr
}
unsafe fn demote_lru_protected(&mut self) {
if let Some(lru_protected) = self.protected.remove_last() {
let lru_ptr = Box::into_raw(lru_protected);
let cache_entry = (*lru_ptr).get_value_mut();
cache_entry.metadata.algorithm.location = Location::Probationary;
if let Some(node_ptr) = self.map.get_mut(&cache_entry.key) {
*node_ptr = lru_ptr;
}
self.probationary.attach_from_other_list(lru_ptr);
self.metrics.record_demotion();
}
}
pub(crate) fn get<Q>(&mut self, key: &Q) -> Option<&V>
where
K: Borrow<Q>,
Q: ?Sized + Hash + Eq,
{
let node = self.map.get(key).copied()?;
unsafe {
let cache_entry = (*node).get_value();
let location = cache_entry.metadata.algorithm.location;
let size = cache_entry.metadata.size;
match location {
Location::Probationary => {
self.metrics.record_probationary_hit(size);
let entry_ptr = self.promote_to_protected(node);
self.metrics.record_promotion();
self.metrics.update_segment_sizes(
self.probationary.len() as u64,
self.protected.len() as u64,
);
Some(&(*entry_ptr).get_value().value)
}
Location::Protected => {
self.metrics.record_protected_hit(size);
self.protected.move_to_front(node);
(*node).get_value_mut().touch();
Some(&(*node).get_value().value)
}
}
}
}
pub(crate) fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
where
K: Borrow<Q>,
Q: ?Sized + Hash + Eq,
{
let node = self.map.get(key).copied()?;
unsafe {
let cache_entry = (*node).get_value();
let location = cache_entry.metadata.algorithm.location;
let size = cache_entry.metadata.size;
match location {
Location::Probationary => {
self.metrics.record_probationary_hit(size);
let entry_ptr = self.promote_to_protected(node);
self.metrics.record_promotion();
self.metrics.update_segment_sizes(
self.probationary.len() as u64,
self.protected.len() as u64,
);
Some(&mut (*entry_ptr).get_value_mut().value)
}
Location::Protected => {
self.metrics.record_protected_hit(size);
self.protected.move_to_front(node);
(*node).get_value_mut().touch();
Some(&mut (*node).get_value_mut().value)
}
}
}
}
#[inline]
pub(crate) fn record_miss(&mut self, object_size: u64) {
self.metrics.core.record_miss(object_size);
}
}
impl<K: Hash + Eq + Clone, V, S: BuildHasher> SlruInner<K, V, S> {
pub(crate) fn put(&mut self, key: K, value: V, size: u64) -> Option<Vec<(K, V)>>
where
V: Clone,
{
if let Some(&node) = self.map.get(&key) {
unsafe {
let cache_entry = (*node).get_value();
let location = cache_entry.metadata.algorithm.location;
let old_size = cache_entry.metadata.size;
match location {
Location::Probationary => {
self.probationary.move_to_front(node);
let new_entry = CacheEntry::with_algorithm_metadata(
key.clone(),
value,
size,
SlruMeta {
location: Location::Probationary,
},
);
let old_entry = self.probationary.update(node, new_entry, true);
self.current_size = self.current_size.saturating_sub(old_size);
self.current_size += size;
self.metrics.core.record_size_change(old_size, size);
self.metrics.core.bytes_written_to_cache += size;
let _ = old_entry;
return None;
}
Location::Protected => {
self.protected.move_to_front(node);
let new_entry = CacheEntry::with_algorithm_metadata(
key.clone(),
value,
size,
SlruMeta {
location: Location::Protected,
},
);
let old_entry = self.protected.update(node, new_entry, true);
self.current_size = self.current_size.saturating_sub(old_size);
self.current_size += size;
self.metrics.core.record_size_change(old_size, size);
self.metrics.core.bytes_written_to_cache += size;
let _ = old_entry;
return None;
}
}
}
}
let mut evicted = Vec::new();
while self.len() >= self.cap().get()
|| (self.current_size + size > self.config.max_size && !self.map.is_empty())
{
if let Some(entry) = self.evict() {
self.metrics.core.evictions += 1;
evicted.push(entry);
} else {
break;
}
}
let cache_entry = CacheEntry::with_algorithm_metadata(
key.clone(),
value,
size,
SlruMeta {
location: Location::Probationary,
},
);
let node = self.probationary.add_unchecked(cache_entry);
self.map.insert(key, node);
self.current_size += size;
self.metrics.core.record_insertion(size);
self.metrics
.update_segment_sizes(self.probationary.len() as u64, self.protected.len() as u64);
if evicted.is_empty() {
None
} else {
Some(evicted)
}
}
pub(crate) fn remove<Q>(&mut self, key: &Q) -> Option<V>
where
K: Borrow<Q>,
Q: ?Sized + Hash + Eq,
{
let node = self.map.remove(key)?;
unsafe {
let cache_entry = (*node).get_value();
let location = cache_entry.metadata.algorithm.location;
let removed_size = cache_entry.metadata.size;
match location {
Location::Probationary => {
let boxed_entry = self.probationary.remove(node)?;
let entry_ptr = Box::into_raw(boxed_entry);
let cache_entry = (*entry_ptr).take_value();
self.current_size = self.current_size.saturating_sub(removed_size);
self.metrics.record_probationary_removal(removed_size);
let _ = Box::from_raw(entry_ptr);
Some(cache_entry.value)
}
Location::Protected => {
let boxed_entry = self.protected.remove(node)?;
let entry_ptr = Box::into_raw(boxed_entry);
let cache_entry = (*entry_ptr).take_value();
self.current_size = self.current_size.saturating_sub(removed_size);
self.metrics.record_protected_removal(removed_size);
let _ = Box::from_raw(entry_ptr);
Some(cache_entry.value)
}
}
}
}
pub(crate) fn clear(&mut self) {
self.map.clear();
self.probationary.clear();
self.protected.clear();
self.current_size = 0;
}
#[inline]
pub(crate) fn contains<Q>(&self, key: &Q) -> bool
where
K: Borrow<Q>,
Q: ?Sized + Hash + Eq,
{
self.map.contains_key(key)
}
pub(crate) fn peek<Q>(&self, key: &Q) -> Option<&V>
where
K: Borrow<Q>,
Q: ?Sized + Hash + Eq,
{
let node = self.map.get(key)?;
unsafe {
let cache_entry = (**node).get_value();
Some(&cache_entry.value)
}
}
fn evict(&mut self) -> Option<(K, V)> {
if let Some(old_entry) = self.probationary.remove_last() {
unsafe {
let entry_ptr = Box::into_raw(old_entry);
let cache_entry = (*entry_ptr).take_value();
let evicted_size = cache_entry.metadata.size;
self.map.remove(&cache_entry.key);
self.current_size = self.current_size.saturating_sub(evicted_size);
self.metrics.record_probationary_removal(evicted_size);
let _ = Box::from_raw(entry_ptr);
return Some((cache_entry.key, cache_entry.value));
}
}
if let Some(old_entry) = self.protected.remove_last() {
unsafe {
let entry_ptr = Box::into_raw(old_entry);
let cache_entry = (*entry_ptr).take_value();
let evicted_size = cache_entry.metadata.size;
self.map.remove(&cache_entry.key);
self.current_size = self.current_size.saturating_sub(evicted_size);
self.metrics.record_protected_removal(evicted_size);
let _ = Box::from_raw(entry_ptr);
return Some((cache_entry.key, cache_entry.value));
}
}
None
}
}
impl<K, V, S> core::fmt::Debug for SlruInner<K, V, S> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("SlruInner")
.field("capacity", &self.config.capacity)
.field("protected_capacity", &self.config.protected_capacity)
.field("len", &self.map.len())
.finish()
}
}
#[derive(Debug)]
pub struct SlruCache<K, V, S = DefaultHashBuilder> {
segment: SlruInner<K, V, S>,
}
impl<K: Hash + Eq, V: Clone, S: BuildHasher> SlruCache<K, V, S> {
#[inline]
pub fn cap(&self) -> NonZeroUsize {
self.segment.cap()
}
#[inline]
pub fn protected_max_size(&self) -> NonZeroUsize {
self.segment.protected_max_size()
}
#[inline]
pub fn len(&self) -> usize {
self.segment.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.segment.is_empty()
}
#[inline]
pub fn current_size(&self) -> u64 {
self.segment.current_size()
}
#[inline]
pub fn max_size(&self) -> u64 {
self.segment.max_size()
}
#[inline]
pub fn get<Q>(&mut self, key: &Q) -> Option<&V>
where
K: Borrow<Q>,
Q: ?Sized + Hash + Eq,
{
self.segment.get(key)
}
#[inline]
pub fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
where
K: Borrow<Q>,
Q: ?Sized + Hash + Eq,
{
self.segment.get_mut(key)
}
#[inline]
pub fn record_miss(&mut self, object_size: u64) {
self.segment.record_miss(object_size);
}
}
impl<K: Hash + Eq + Clone, V, S: BuildHasher> SlruCache<K, V, S> {
#[inline]
pub fn put(&mut self, key: K, value: V, size: u64) -> Option<Vec<(K, V)>>
where
V: Clone,
{
self.segment.put(key, value, size)
}
#[inline]
pub fn remove<Q>(&mut self, key: &Q) -> Option<V>
where
K: Borrow<Q>,
Q: ?Sized + Hash + Eq,
V: Clone,
{
self.segment.remove(key)
}
#[inline]
pub fn clear(&mut self) {
self.segment.clear()
}
#[inline]
pub fn contains<Q>(&self, key: &Q) -> bool
where
K: Borrow<Q>,
Q: ?Sized + Hash + Eq,
{
self.segment.contains(key)
}
#[inline]
pub fn peek<Q>(&self, key: &Q) -> Option<&V>
where
K: Borrow<Q>,
Q: ?Sized + Hash + Eq,
{
self.segment.peek(key)
}
}
impl<K: Hash + Eq, V> SlruCache<K, V>
where
V: Clone,
{
pub fn init(
config: SlruCacheConfig,
hasher: Option<DefaultHashBuilder>,
) -> SlruCache<K, V, DefaultHashBuilder> {
SlruCache {
segment: SlruInner::init(config, hasher.unwrap_or_default()),
}
}
}
impl<K: Hash + Eq, V: Clone, S: BuildHasher> CacheMetrics for SlruCache<K, V, S> {
fn metrics(&self) -> BTreeMap<String, f64> {
self.segment.metrics().metrics()
}
fn algorithm_name(&self) -> &'static str {
self.segment.metrics().algorithm_name()
}
}
#[cfg(test)]
mod tests {
extern crate std;
use std::string::ToString;
use super::*;
use crate::config::SlruCacheConfig;
use alloc::format;
use alloc::string::String;
fn make_cache<K: Hash + Eq + Clone, V: Clone>(
cap: usize,
protected_cap: usize,
) -> SlruCache<K, V> {
let config = SlruCacheConfig {
capacity: NonZeroUsize::new(cap).unwrap(),
protected_capacity: NonZeroUsize::new(protected_cap).unwrap(),
max_size: u64::MAX,
};
SlruCache::init(config, None)
}
#[test]
fn test_slru_basic() {
let mut cache = make_cache(4, 2);
assert_eq!(cache.put("a", 1, 1), None);
assert_eq!(cache.put("b", 2, 1), None);
assert_eq!(cache.put("c", 3, 1), None);
assert_eq!(cache.put("d", 4, 1), None);
assert_eq!(cache.len(), 4);
assert_eq!(cache.get(&"a"), Some(&1));
assert_eq!(cache.get(&"b"), Some(&2));
let evicted = cache.put("e", 5, 1);
assert!(evicted.is_some());
let (evicted_key, evicted_val) = evicted.unwrap()[0];
assert_eq!(evicted_key, "c");
assert_eq!(evicted_val, 3);
let evicted = cache.put("f", 6, 1);
assert!(evicted.is_some());
let (evicted_key, evicted_val) = evicted.unwrap()[0];
assert_eq!(evicted_key, "d");
assert_eq!(evicted_val, 4);
assert_eq!(cache.get(&"a"), Some(&1)); assert_eq!(cache.get(&"b"), Some(&2)); assert_eq!(cache.get(&"c"), None); assert_eq!(cache.get(&"d"), None); assert_eq!(cache.get(&"e"), Some(&5)); assert_eq!(cache.get(&"f"), Some(&6)); }
#[test]
fn test_slru_update() {
let mut cache = make_cache(4, 2);
cache.put("a", 1, 1);
cache.put("b", 2, 1);
assert_eq!(cache.get(&"a"), Some(&1));
assert!(cache.put("a", 10, 1).is_none());
assert!(cache.put("b", 20, 1).is_none());
assert_eq!(cache.get(&"a"), Some(&10));
assert_eq!(cache.get(&"b"), Some(&20));
}
#[test]
fn test_slru_remove() {
let mut cache = make_cache(4, 2);
cache.put("a", 1, 1);
cache.put("b", 2, 1);
assert_eq!(cache.get(&"a"), Some(&1));
assert_eq!(cache.remove(&"a"), Some(1)); assert_eq!(cache.remove(&"b"), Some(2));
assert_eq!(cache.get(&"a"), None);
assert_eq!(cache.get(&"b"), None);
assert_eq!(cache.remove(&"c"), None);
}
#[test]
fn test_slru_clear() {
let mut cache = make_cache(4, 2);
cache.put("a", 1, 1);
cache.put("b", 2, 1);
cache.put("c", 3, 1);
cache.put("d", 4, 1);
cache.clear();
assert_eq!(cache.len(), 0);
assert!(cache.is_empty());
assert_eq!(cache.get(&"a"), None);
assert_eq!(cache.get(&"b"), None);
assert_eq!(cache.get(&"c"), None);
assert_eq!(cache.get(&"d"), None);
}
#[test]
fn test_slru_complex_values() {
let mut cache = make_cache(4, 2);
#[derive(Debug, Clone, PartialEq)]
struct ComplexValue {
id: usize,
data: String,
}
cache.put(
"a",
ComplexValue {
id: 1,
data: "a-data".to_string(),
},
1,
);
cache.put(
"b",
ComplexValue {
id: 2,
data: "b-data".to_string(),
},
1,
);
if let Some(value) = cache.get_mut(&"a") {
value.id = 100;
value.data = "a-modified".to_string();
}
let a = cache.get(&"a").unwrap();
assert_eq!(a.id, 100);
assert_eq!(a.data, "a-modified");
}
#[test]
fn test_slru_with_ratio() {
let mut cache = make_cache(4, 2);
assert_eq!(cache.cap().get(), 4);
assert_eq!(cache.protected_max_size().get(), 2);
assert_eq!(cache.put("a", 1, 1), None);
assert_eq!(cache.put("b", 2, 1), None);
assert_eq!(cache.get(&"a"), Some(&1));
assert_eq!(cache.put("c", 3, 1), None);
assert_eq!(cache.put("d", 4, 1), None);
let result = cache.put("e", 5, 1);
assert_eq!(result.unwrap()[0].0, "b");
assert_eq!(cache.get(&"a"), Some(&1));
}
#[test]
fn test_slru_segment_directly() {
let config = SlruCacheConfig {
capacity: NonZeroUsize::new(4).unwrap(),
protected_capacity: NonZeroUsize::new(2).unwrap(),
max_size: u64::MAX,
};
let mut segment: SlruInner<&str, i32, DefaultHashBuilder> =
SlruInner::init(config, DefaultHashBuilder::default());
assert_eq!(segment.len(), 0);
assert!(segment.is_empty());
assert_eq!(segment.cap().get(), 4);
assert_eq!(segment.protected_max_size().get(), 2);
segment.put("a", 1, 1);
segment.put("b", 2, 1);
assert_eq!(segment.len(), 2);
assert_eq!(segment.get(&"a"), Some(&1));
assert_eq!(segment.get(&"b"), Some(&2));
}
#[test]
fn test_slru_concurrent_access() {
extern crate std;
use std::sync::{Arc, Mutex};
use std::thread;
use std::vec::Vec;
let cache = Arc::new(Mutex::new(make_cache::<String, i32>(100, 50)));
let num_threads = 4;
let ops_per_thread = 100;
let mut handles: Vec<std::thread::JoinHandle<()>> = Vec::new();
for t in 0..num_threads {
let cache = Arc::clone(&cache);
handles.push(thread::spawn(move || {
for i in 0..ops_per_thread {
let key = std::format!("key_{}_{}", t, i);
let mut guard = cache.lock().unwrap();
guard.put(key.clone(), i, 1);
let _ = guard.get(&key);
}
}));
}
for handle in handles {
handle.join().unwrap();
}
let mut guard = cache.lock().unwrap();
assert!(guard.len() <= 100);
guard.clear(); }
#[test]
fn test_slru_size_aware_tracking() {
let mut cache = make_cache(10, 3);
assert_eq!(cache.current_size(), 0);
assert_eq!(cache.max_size(), u64::MAX);
cache.put("a", 1, 100);
cache.put("b", 2, 200);
cache.put("c", 3, 150);
assert_eq!(cache.current_size(), 450);
assert_eq!(cache.len(), 3);
cache.clear();
assert_eq!(cache.current_size(), 0);
}
#[test]
fn test_slru_init_constructor() {
let config = SlruCacheConfig {
capacity: NonZeroUsize::new(1000).unwrap(),
protected_capacity: NonZeroUsize::new(300).unwrap(),
max_size: 1024 * 1024,
};
let cache: SlruCache<String, i32> = SlruCache::init(config, None);
assert_eq!(cache.current_size(), 0);
assert_eq!(cache.max_size(), 1024 * 1024);
}
#[test]
fn test_slru_with_limits_constructor() {
let config = SlruCacheConfig {
capacity: NonZeroUsize::new(100).unwrap(),
protected_capacity: NonZeroUsize::new(30).unwrap(),
max_size: 1024 * 1024,
};
let cache: SlruCache<String, String> = SlruCache::init(config, None);
assert_eq!(cache.current_size(), 0);
assert_eq!(cache.max_size(), 1024 * 1024);
assert_eq!(cache.cap().get(), 100);
assert_eq!(cache.protected_max_size().get(), 30);
}
#[test]
fn test_slru_record_miss() {
use crate::metrics::CacheMetrics;
let mut cache: SlruCache<String, i32> = make_cache(100, 30);
cache.record_miss(100);
cache.record_miss(200);
let metrics = cache.metrics();
assert_eq!(metrics.get("cache_misses").unwrap(), &2.0);
}
#[test]
fn test_slru_get_mut() {
let mut cache: SlruCache<String, i32> = make_cache(100, 30);
cache.put("key".to_string(), 10, 1);
assert_eq!(cache.get(&"key".to_string()), Some(&10));
if let Some(val) = cache.get_mut(&"key".to_string()) {
*val = 42;
}
assert_eq!(cache.get(&"key".to_string()), Some(&42));
assert!(cache.get_mut(&"missing".to_string()).is_none());
}
fn make_cache_with_max_size(
cap: usize,
protected: usize,
max_size: u64,
) -> SlruCache<String, i32> {
let config = SlruCacheConfig {
capacity: NonZeroUsize::new(cap).unwrap(),
protected_capacity: NonZeroUsize::new(protected).unwrap(),
max_size,
};
SlruCache::init(config, None)
}
#[test]
fn test_slru_max_size_triggers_eviction() {
let mut cache = make_cache_with_max_size(100, 30, 100);
cache.put("a".to_string(), 1, 30); cache.put("b".to_string(), 2, 30); cache.put("c".to_string(), 3, 30);
assert_eq!(cache.len(), 3, "Should have 3 items");
assert_eq!(cache.current_size(), 90, "Size should be 90");
cache.put("d".to_string(), 4, 20);
assert!(
cache.current_size() <= 100,
"current_size {} exceeds max_size 100",
cache.current_size()
);
assert!(
cache.get(&"a".to_string()).is_none() || cache.current_size() <= 100,
"Either 'a' should be evicted OR size should be within limits"
);
}
#[test]
fn test_slru_max_size_eviction_large_objects() {
let mut cache = make_cache_with_max_size(1000, 200, 500);
for i in 0..5 {
cache.put(format!("key{}", i), i, 100);
}
assert_eq!(cache.len(), 5);
assert_eq!(cache.current_size(), 500, "Should have exactly 500 bytes");
cache.put("overflow".to_string(), 99, 100);
assert!(
cache.current_size() <= 500,
"SLRU BUG: current_size {} exceeds max_size 500 after insert. \
SLRU must evict items to respect max_size limit.",
cache.current_size()
);
}
#[test]
fn test_slru_contains_non_promoting() {
let mut cache = make_cache(4, 2);
cache.put("a", 1, 1);
cache.put("b", 2, 1);
assert!(cache.contains(&"a"));
assert!(cache.contains(&"b"));
assert!(!cache.contains(&"c"));
cache.put("c", 3, 1);
cache.put("d", 4, 1);
assert_eq!(cache.len(), 4);
assert!(cache.contains(&"a"));
assert!(cache.contains(&"d"));
}
#[test]
fn test_put_returns_none_when_no_eviction() {
let mut cache = make_cache(10, 3);
assert!(cache.put("a", 1, 1).is_none());
assert!(cache.put("b", 2, 1).is_none());
}
#[test]
fn test_put_returns_single_eviction() {
let mut cache = make_cache(2, 1);
cache.put("a", 1, 1);
cache.put("b", 2, 1);
let result = cache.put("c", 3, 1);
assert!(result.is_some());
let evicted = result.unwrap();
assert_eq!(evicted.len(), 1);
}
#[test]
fn test_put_replacement_returns_none() {
let mut cache = make_cache(10, 3);
cache.put("a", 1, 1);
let result = cache.put("a", 2, 1);
assert!(result.is_none());
assert_eq!(cache.get(&"a"), Some(&2));
}
#[test]
fn test_put_returns_multiple_evictions_size_based() {
let config = SlruCacheConfig {
capacity: NonZeroUsize::new(10).unwrap(),
protected_capacity: NonZeroUsize::new(3).unwrap(),
max_size: 100,
};
let mut cache = SlruCache::init(config, None);
for i in 0..10 {
cache.put(i, i, 10);
}
let result = cache.put(99, 99, 50);
let evicted = result.unwrap();
assert_eq!(evicted.len(), 5);
}
#[test]
fn test_slru_peek_non_promoting() {
let mut cache = make_cache(4, 2);
cache.put("a", 1, 1);
cache.put("b", 2, 1);
assert_eq!(cache.peek(&"a"), Some(&1));
assert_eq!(cache.peek(&"b"), Some(&2));
assert_eq!(cache.peek(&"c"), None);
cache.get(&"a");
cache.put("c", 3, 1);
cache.put("d", 4, 1);
assert!(cache.contains(&"a"));
}
}