cache_rs/
lru.rs

1//! Least Recently Used (LRU) Cache Implementation
2//!
3//! This module provides a memory-efficient LRU cache implementation with O(1) operations
4//! for all common cache operations. LRU is one of the most widely used cache eviction
5//! algorithms due to its simplicity and good performance for workloads with temporal locality.
6//!
7//! # Algorithm
8//!
9//! The LRU cache maintains items in order of recency of use, evicting the least recently
10//! used item when capacity is reached. This works on the principle of temporal locality:
11//! items that have been accessed recently are likely to be accessed again soon.
12//!
13//! # Performance Characteristics
14//!
15//! - **Time Complexity**:
16//!   - Get: O(1)
17//!   - Put: O(1)
18//!   - Remove: O(1)
19//!
20//! - **Space Complexity**:
21//!   - O(n) where n is the capacity of the cache
22//!   - Memory overhead is approximately 48 bytes per entry plus the size of keys and values
23//!
24//! # When to Use
25//!
26//! LRU caches are ideal for:
27//! - General-purpose caching where access patterns exhibit temporal locality
28//! - Simple implementation with predictable performance
29//! - Caching with a fixed memory budget
30//!
31//! They are less suitable for:
32//! - Workloads where frequency of access is more important than recency
33//! - Scanning patterns where a large set of items is accessed once in sequence
34//!
35//! # Thread Safety
36//!
37//! This implementation is not thread-safe. For concurrent access, you should
38//! wrap the cache with a synchronization primitive such as `Mutex` or `RwLock`.
39
40extern crate alloc;
41
42use crate::config::LruCacheConfig;
43use crate::list::{Entry, List};
44use crate::metrics::{CacheMetrics, LruCacheMetrics};
45use alloc::boxed::Box;
46use alloc::collections::BTreeMap;
47use alloc::string::String;
48use core::borrow::Borrow;
49use core::hash::{BuildHasher, Hash};
50use core::mem;
51use core::num::NonZeroUsize;
52
53#[cfg(feature = "hashbrown")]
54use hashbrown::hash_map::DefaultHashBuilder;
55#[cfg(feature = "hashbrown")]
56use hashbrown::HashMap;
57
58#[cfg(not(feature = "hashbrown"))]
59use std::collections::hash_map::RandomState as DefaultHashBuilder;
60#[cfg(not(feature = "hashbrown"))]
61use std::collections::HashMap;
62
63/// Internal LRU segment containing the actual cache algorithm.
64///
65/// This is shared between `LruCache` (single-threaded) and
66/// `ConcurrentLruCache` (multi-threaded). All algorithm logic is
67/// implemented here to avoid code duplication.
68///
69/// # Safety
70///
71/// This struct contains raw pointers in the `map` field.
72/// These pointers are always valid as long as:
73/// - The pointer was obtained from a `list` entry's `add()` call
74/// - The node has not been removed from the list
75/// - The segment has not been dropped
76pub(crate) struct LruSegment<K, V, S = DefaultHashBuilder> {
77    config: LruCacheConfig,
78    list: List<(K, V)>,
79    map: HashMap<K, *mut Entry<(K, V)>, S>,
80    metrics: LruCacheMetrics,
81}
82
83// SAFETY: LruSegment owns all data and raw pointers point only to nodes owned by `list`.
84// Concurrent access is safe when wrapped in proper synchronization primitives.
85unsafe impl<K: Send, V: Send, S: Send> Send for LruSegment<K, V, S> {}
86
87// SAFETY: All mutation requires &mut self; shared references cannot cause data races.
88unsafe impl<K: Send, V: Send, S: Sync> Sync for LruSegment<K, V, S> {}
89
90impl<K: Hash + Eq, V: Clone, S: BuildHasher> LruSegment<K, V, S> {
91    pub(crate) fn with_hasher(cap: NonZeroUsize, hash_builder: S) -> Self {
92        Self::with_hasher_and_size(cap, hash_builder, cap.get() as u64 * 1024)
93    }
94
95    pub(crate) fn with_hasher_and_size(
96        cap: NonZeroUsize,
97        hash_builder: S,
98        max_size_bytes: u64,
99    ) -> Self {
100        let map_capacity = cap.get().next_power_of_two();
101        let config = LruCacheConfig::new(cap);
102        LruSegment {
103            config,
104            list: List::new(cap),
105            map: HashMap::with_capacity_and_hasher(map_capacity, hash_builder),
106            metrics: LruCacheMetrics::new(max_size_bytes),
107        }
108    }
109
110    #[inline]
111    pub(crate) fn cap(&self) -> NonZeroUsize {
112        self.config.capacity()
113    }
114
115    #[inline]
116    pub(crate) fn len(&self) -> usize {
117        self.map.len()
118    }
119
120    #[inline]
121    pub(crate) fn is_empty(&self) -> bool {
122        self.map.is_empty()
123    }
124
125    #[inline]
126    pub(crate) fn metrics(&self) -> &LruCacheMetrics {
127        &self.metrics
128    }
129
130    fn estimate_object_size(&self, _key: &K, _value: &V) -> u64 {
131        mem::size_of::<K>() as u64 + mem::size_of::<V>() as u64 + 64
132    }
133
134    pub(crate) fn get<Q>(&mut self, key: &Q) -> Option<&V>
135    where
136        K: Borrow<Q>,
137        Q: ?Sized + Hash + Eq,
138    {
139        if let Some(node) = self.map.get(key).copied() {
140            unsafe {
141                // SAFETY: node comes from our map
142                self.list.move_to_front(node);
143                let (k, v) = (*node).get_value();
144                let object_size = self.estimate_object_size(k, v);
145                self.metrics.core.record_hit(object_size);
146                Some(v)
147            }
148        } else {
149            None
150        }
151    }
152
153    #[inline]
154    pub(crate) fn record_miss(&mut self, object_size: u64) {
155        self.metrics.core.record_miss(object_size);
156    }
157
158    pub(crate) fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
159    where
160        K: Borrow<Q>,
161        Q: ?Sized + Hash + Eq,
162    {
163        let node = self.map.get(key).copied()?;
164        unsafe {
165            // SAFETY: node comes from our map
166            self.list.move_to_front(node);
167            let (k, v) = (*node).get_value_mut();
168            let object_size = self.estimate_object_size(k, v);
169            self.metrics.core.record_hit(object_size);
170            Some(v)
171        }
172    }
173
174    pub(crate) fn put(&mut self, key: K, value: V) -> Option<(K, V)>
175    where
176        K: Clone,
177    {
178        let mut evicted = None;
179        let object_size = self.estimate_object_size(&key, &value);
180
181        if let Some(&node) = self.map.get(&key) {
182            unsafe {
183                // SAFETY: node comes from our map
184                self.list.move_to_front(node);
185                let (k, old_value) = self.list.update(node, (key, value), true).0?;
186                return Some((k, old_value));
187            }
188        }
189
190        if self.map.len() >= self.cap().get() {
191            if let Some(old_entry) = self.list.remove_last() {
192                unsafe {
193                    let entry_ptr = Box::into_raw(old_entry);
194                    let key_ref = &(*entry_ptr).get_value().0;
195                    self.map.remove(key_ref);
196                    let key = (*entry_ptr).get_value().0.clone();
197                    let value = (*entry_ptr).get_value().1.clone();
198                    let evicted_size = self.estimate_object_size(&key, &value);
199                    self.metrics.core.record_eviction(evicted_size);
200                    evicted = Some((key, value));
201                    let _ = Box::from_raw(entry_ptr);
202                }
203            }
204        }
205
206        if let Some(node) = self.list.add((key.clone(), value)) {
207            self.map.insert(key, node);
208            self.metrics.core.record_insertion(object_size);
209        }
210
211        evicted
212    }
213
214    pub(crate) fn remove<Q>(&mut self, key: &Q) -> Option<V>
215    where
216        K: Borrow<Q>,
217        Q: ?Sized + Hash + Eq,
218        V: Clone,
219    {
220        let node = self.map.remove(key)?;
221        unsafe {
222            // SAFETY: node comes from our map
223            let (k, v) = (*node).get_value();
224            let object_size = self.estimate_object_size(k, v);
225            let value = v.clone();
226            self.list.remove(node);
227            self.metrics.core.record_eviction(object_size);
228            Some(value)
229        }
230    }
231
232    pub(crate) fn clear(&mut self) {
233        self.metrics.core.cache_size_bytes = 0;
234        self.map.clear();
235        self.list.clear();
236    }
237}
238
239impl<K, V, S> core::fmt::Debug for LruSegment<K, V, S> {
240    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
241        f.debug_struct("LruSegment")
242            .field("capacity", &self.config.capacity())
243            .field("len", &self.map.len())
244            .finish()
245    }
246}
247
248/// An implementation of a Least Recently Used (LRU) cache.
249///
250/// The cache has a fixed capacity and supports O(1) operations for
251/// inserting, retrieving, and updating entries. When the cache reaches capacity,
252/// the least recently used entry will be evicted to make room for new entries.
253///
254/// # Examples
255///
256/// ```
257/// use cache_rs::LruCache;
258/// use core::num::NonZeroUsize;
259///
260/// let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
261///
262/// // Add items to the cache
263/// cache.put("apple", 1);
264/// cache.put("banana", 2);
265///
266/// // Accessing items updates their recency
267/// assert_eq!(cache.get(&"apple"), Some(&1));
268///
269/// // Adding beyond capacity evicts the least recently used item
270/// cache.put("cherry", 3);
271/// assert_eq!(cache.get(&"banana"), None);
272/// assert_eq!(cache.get(&"apple"), Some(&1));
273/// assert_eq!(cache.get(&"cherry"), Some(&3));
274/// ```
275#[derive(Debug)]
276pub struct LruCache<K, V, S = DefaultHashBuilder> {
277    segment: LruSegment<K, V, S>,
278}
279
280impl<K: Hash + Eq, V: Clone, S: BuildHasher> LruCache<K, V, S> {
281    /// Creates a new LRU cache with the specified capacity and hash builder.
282    pub fn with_hasher(cap: NonZeroUsize, hash_builder: S) -> Self {
283        Self {
284            segment: LruSegment::with_hasher(cap, hash_builder),
285        }
286    }
287
288    /// Creates a new LRU cache with specified capacity, hash builder, and size limit.
289    pub fn with_hasher_and_size(cap: NonZeroUsize, hash_builder: S, max_size_bytes: u64) -> Self {
290        Self {
291            segment: LruSegment::with_hasher_and_size(cap, hash_builder, max_size_bytes),
292        }
293    }
294
295    #[inline]
296    pub fn cap(&self) -> NonZeroUsize {
297        self.segment.cap()
298    }
299
300    #[inline]
301    pub fn len(&self) -> usize {
302        self.segment.len()
303    }
304
305    #[inline]
306    pub fn is_empty(&self) -> bool {
307        self.segment.is_empty()
308    }
309
310    #[inline]
311    pub fn get<Q>(&mut self, key: &Q) -> Option<&V>
312    where
313        K: Borrow<Q>,
314        Q: ?Sized + Hash + Eq,
315    {
316        self.segment.get(key)
317    }
318
319    #[inline]
320    pub fn record_miss(&mut self, object_size: u64) {
321        self.segment.record_miss(object_size);
322    }
323
324    #[inline]
325    pub fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
326    where
327        K: Borrow<Q>,
328        Q: ?Sized + Hash + Eq,
329    {
330        self.segment.get_mut(key)
331    }
332}
333
334impl<K: Hash + Eq + Clone, V: Clone, S: BuildHasher> LruCache<K, V, S> {
335    #[inline]
336    pub fn put(&mut self, key: K, value: V) -> Option<(K, V)> {
337        self.segment.put(key, value)
338    }
339
340    #[inline]
341    pub fn remove<Q>(&mut self, key: &Q) -> Option<V>
342    where
343        K: Borrow<Q>,
344        Q: ?Sized + Hash + Eq,
345    {
346        self.segment.remove(key)
347    }
348
349    #[inline]
350    pub fn clear(&mut self) {
351        self.segment.clear()
352    }
353
354    pub fn iter(&self) -> Iter<'_, K, V> {
355        unimplemented!("Iteration not yet implemented")
356    }
357
358    pub fn iter_mut(&mut self) -> IterMut<'_, K, V> {
359        unimplemented!("Mutable iteration not yet implemented")
360    }
361}
362
363impl<K: Hash + Eq, V> LruCache<K, V>
364where
365    V: Clone,
366{
367    pub fn new(cap: NonZeroUsize) -> LruCache<K, V, DefaultHashBuilder> {
368        LruCache::with_hasher(cap, DefaultHashBuilder::default())
369    }
370}
371
372impl<K: Hash + Eq, V: Clone, S: BuildHasher> CacheMetrics for LruCache<K, V, S> {
373    fn metrics(&self) -> BTreeMap<String, f64> {
374        self.segment.metrics().metrics()
375    }
376
377    fn algorithm_name(&self) -> &'static str {
378        self.segment.metrics().algorithm_name()
379    }
380}
381
382pub struct Iter<'a, K, V> {
383    _marker: core::marker::PhantomData<&'a (K, V)>,
384}
385
386pub struct IterMut<'a, K, V> {
387    _marker: core::marker::PhantomData<&'a mut (K, V)>,
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393    use alloc::string::String;
394
395    #[test]
396    fn test_lru_get_put() {
397        let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
398        assert_eq!(cache.put("apple", 1), None);
399        assert_eq!(cache.put("banana", 2), None);
400        assert_eq!(cache.get(&"apple"), Some(&1));
401        assert_eq!(cache.get(&"banana"), Some(&2));
402        assert_eq!(cache.get(&"cherry"), None);
403        assert_eq!(cache.put("apple", 3).unwrap().1, 1);
404        assert_eq!(cache.get(&"apple"), Some(&3));
405        assert_eq!(cache.put("cherry", 4).unwrap().1, 2);
406        assert_eq!(cache.get(&"banana"), None);
407        assert_eq!(cache.get(&"apple"), Some(&3));
408        assert_eq!(cache.get(&"cherry"), Some(&4));
409    }
410
411    #[test]
412    fn test_lru_get_mut() {
413        let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
414        cache.put("apple", 1);
415        cache.put("banana", 2);
416        if let Some(v) = cache.get_mut(&"apple") {
417            *v = 3;
418        }
419        assert_eq!(cache.get(&"apple"), Some(&3));
420        cache.put("cherry", 4);
421        assert_eq!(cache.get(&"banana"), None);
422        assert_eq!(cache.get(&"apple"), Some(&3));
423        assert_eq!(cache.get(&"cherry"), Some(&4));
424    }
425
426    #[test]
427    fn test_lru_remove() {
428        let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
429        cache.put("apple", 1);
430        cache.put("banana", 2);
431        assert_eq!(cache.get(&"apple"), Some(&1));
432        assert_eq!(cache.get(&"banana"), Some(&2));
433        assert_eq!(cache.get(&"cherry"), None);
434        assert_eq!(cache.remove(&"apple"), Some(1));
435        assert_eq!(cache.get(&"apple"), None);
436        assert_eq!(cache.len(), 1);
437        assert_eq!(cache.remove(&"cherry"), None);
438        let evicted = cache.put("cherry", 3);
439        assert_eq!(evicted, None);
440        assert_eq!(cache.get(&"banana"), Some(&2));
441        assert_eq!(cache.get(&"cherry"), Some(&3));
442    }
443
444    #[test]
445    fn test_lru_clear() {
446        let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
447        cache.put("apple", 1);
448        cache.put("banana", 2);
449        assert_eq!(cache.len(), 2);
450        cache.clear();
451        assert_eq!(cache.len(), 0);
452        assert!(cache.is_empty());
453        cache.put("cherry", 3);
454        assert_eq!(cache.get(&"cherry"), Some(&3));
455    }
456
457    #[test]
458    fn test_lru_capacity_limits() {
459        let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
460        cache.put("apple", 1);
461        cache.put("banana", 2);
462        cache.put("cherry", 3);
463        assert_eq!(cache.len(), 2);
464        assert_eq!(cache.get(&"apple"), None);
465        assert_eq!(cache.get(&"banana"), Some(&2));
466        assert_eq!(cache.get(&"cherry"), Some(&3));
467    }
468
469    #[test]
470    fn test_lru_string_keys() {
471        let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
472        let key1 = String::from("apple");
473        let key2 = String::from("banana");
474        cache.put(key1.clone(), 1);
475        cache.put(key2.clone(), 2);
476        assert_eq!(cache.get(&key1), Some(&1));
477        assert_eq!(cache.get(&key2), Some(&2));
478        assert_eq!(cache.get("apple"), Some(&1));
479        assert_eq!(cache.get("banana"), Some(&2));
480        drop(cache);
481    }
482
483    #[derive(Debug, Clone, Eq, PartialEq)]
484    struct ComplexValue {
485        val: i32,
486        description: String,
487    }
488
489    #[test]
490    fn test_lru_complex_values() {
491        let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
492        let key1 = String::from("apple");
493        let key2 = String::from("banana");
494        let fruit1 = ComplexValue {
495            val: 1,
496            description: String::from("First fruit"),
497        };
498        let fruit2 = ComplexValue {
499            val: 2,
500            description: String::from("Second fruit"),
501        };
502        let fruit3 = ComplexValue {
503            val: 3,
504            description: String::from("Third fruit"),
505        };
506        cache.put(key1.clone(), fruit1.clone());
507        cache.put(key2.clone(), fruit2.clone());
508        assert_eq!(cache.get(&key1).unwrap().val, fruit1.val);
509        assert_eq!(cache.get(&key2).unwrap().val, fruit2.val);
510        let evicted = cache.put(String::from("cherry"), fruit3.clone());
511        let evicted_fruit = evicted.unwrap();
512        assert_eq!(evicted_fruit.1, fruit1);
513        let removed = cache.remove(&key1);
514        assert_eq!(removed, None);
515    }
516
517    #[test]
518    fn test_lru_metrics() {
519        use crate::metrics::CacheMetrics;
520        let mut cache = LruCache::new(NonZeroUsize::new(2).unwrap());
521        let metrics = cache.metrics();
522        assert_eq!(metrics.get("requests").unwrap(), &0.0);
523        assert_eq!(metrics.get("cache_hits").unwrap(), &0.0);
524        assert_eq!(metrics.get("cache_misses").unwrap(), &0.0);
525        cache.put("apple", 1);
526        cache.put("banana", 2);
527        cache.get(&"apple");
528        cache.get(&"banana");
529        let metrics = cache.metrics();
530        assert_eq!(metrics.get("cache_hits").unwrap(), &2.0);
531        cache.record_miss(64);
532        let metrics = cache.metrics();
533        assert_eq!(metrics.get("cache_misses").unwrap(), &1.0);
534        assert_eq!(metrics.get("requests").unwrap(), &3.0);
535        cache.put("cherry", 3);
536        let metrics = cache.metrics();
537        assert_eq!(metrics.get("evictions").unwrap(), &1.0);
538        assert!(metrics.get("bytes_written_to_cache").unwrap() > &0.0);
539        assert_eq!(cache.algorithm_name(), "LRU");
540    }
541
542    #[test]
543    fn test_lru_segment_directly() {
544        let mut segment: LruSegment<&str, i32, DefaultHashBuilder> =
545            LruSegment::with_hasher(NonZeroUsize::new(2).unwrap(), DefaultHashBuilder::default());
546        assert_eq!(segment.len(), 0);
547        assert!(segment.is_empty());
548        assert_eq!(segment.cap().get(), 2);
549        segment.put("a", 1);
550        segment.put("b", 2);
551        assert_eq!(segment.len(), 2);
552        assert_eq!(segment.get(&"a"), Some(&1));
553        assert_eq!(segment.get(&"b"), Some(&2));
554    }
555
556    #[test]
557    fn test_lru_concurrent_access() {
558        extern crate std;
559        use std::sync::{Arc, Mutex};
560        use std::thread;
561        use std::vec::Vec;
562
563        let cache = Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(100).unwrap())));
564        let num_threads = 4;
565        let ops_per_thread = 100;
566
567        let mut handles: Vec<std::thread::JoinHandle<()>> = Vec::new();
568
569        // Spawn writer threads
570        for t in 0..num_threads {
571            let cache = Arc::clone(&cache);
572            handles.push(thread::spawn(move || {
573                for i in 0..ops_per_thread {
574                    let key = std::format!("thread_{}_key_{}", t, i);
575                    let mut guard = cache.lock().unwrap();
576                    guard.put(key, t * 1000 + i);
577                }
578            }));
579        }
580
581        // Spawn reader threads
582        for t in 0..num_threads {
583            let cache = Arc::clone(&cache);
584            handles.push(thread::spawn(move || {
585                for i in 0..ops_per_thread {
586                    let key = std::format!("thread_{}_key_{}", t, i);
587                    let mut guard = cache.lock().unwrap();
588                    let _ = guard.get(&key);
589                }
590            }));
591        }
592
593        for handle in handles {
594            handle.join().unwrap();
595        }
596
597        let mut guard = cache.lock().unwrap();
598        assert!(guard.len() <= 100);
599        assert!(!guard.is_empty());
600        guard.clear(); // Clean up for MIRI
601    }
602
603    #[test]
604    fn test_lru_high_contention() {
605        extern crate std;
606        use std::sync::{Arc, Mutex};
607        use std::thread;
608        use std::vec::Vec;
609
610        let cache = Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(50).unwrap())));
611        let num_threads = 8;
612        let ops_per_thread = 500;
613
614        let mut handles: Vec<std::thread::JoinHandle<()>> = Vec::new();
615
616        for t in 0..num_threads {
617            let cache = Arc::clone(&cache);
618            handles.push(thread::spawn(move || {
619                for i in 0..ops_per_thread {
620                    let key = std::format!("key_{}", i % 100); // Overlapping keys
621                    let mut guard = cache.lock().unwrap();
622                    if i % 2 == 0 {
623                        guard.put(key, t * 1000 + i);
624                    } else {
625                        let _ = guard.get(&key);
626                    }
627                }
628            }));
629        }
630
631        for handle in handles {
632            handle.join().unwrap();
633        }
634
635        let mut guard = cache.lock().unwrap();
636        assert!(guard.len() <= 50);
637        guard.clear(); // Clean up for MIRI
638    }
639
640    #[test]
641    fn test_lru_concurrent_mixed_operations() {
642        extern crate std;
643        use std::sync::{Arc, Mutex};
644        use std::thread;
645        use std::vec::Vec;
646
647        let cache = Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(100).unwrap())));
648        let num_threads = 8;
649        let ops_per_thread = 1000;
650
651        let mut handles: Vec<std::thread::JoinHandle<()>> = Vec::new();
652
653        for t in 0..num_threads {
654            let cache = Arc::clone(&cache);
655            handles.push(thread::spawn(move || {
656                for i in 0..ops_per_thread {
657                    let key = std::format!("key_{}", i % 200);
658                    let mut guard = cache.lock().unwrap();
659
660                    match i % 4 {
661                        0 => {
662                            guard.put(key, i);
663                        }
664                        1 => {
665                            let _ = guard.get(&key);
666                        }
667                        2 => {
668                            let _ = guard.get_mut(&key);
669                        }
670                        3 => {
671                            let _ = guard.remove(&key);
672                        }
673                        _ => unreachable!(),
674                    }
675
676                    if i == 500 && t == 0 {
677                        guard.clear();
678                    }
679                }
680            }));
681        }
682
683        for handle in handles {
684            handle.join().unwrap();
685        }
686
687        let mut guard = cache.lock().unwrap();
688        assert!(guard.len() <= 100);
689        guard.clear(); // Clean up for MIRI
690    }
691}