Skip to main content

cachekit/policy/
nru.rs

1//! NRU (Not Recently Used) cache replacement policy.
2//!
3//! Implements the Not Recently Used algorithm, which uses a single reference bit
4//! per entry to approximate LRU with minimal overhead. The reference bit provides
5//! a coarse distinction between "recently used" and "not recently used" items.
6//!
7//! ## Architecture
8//!
9//! ```text
10//! ┌─────────────────────────────────────────────────────────────────────────────┐
11//! │                        NruCache<K, V> Layout                                │
12//! │                                                                             │
13//! │   ┌─────────────────────────────────────────────────────────────────────┐   │
14//! │   │  map: HashMap<K, Entry<V>>     keys: Vec<K>                         │   │
15//! │   │       key → (index, value, ref)      dense array of keys            │   │
16//! │   │                                                                     │   │
17//! │   │  ┌──────────┬─────────────────┐      ┌─────┬─────┬─────┬─────┐      │   │
18//! │   │  │   Key    │  Entry          │      │  0  │  1  │  2  │  3  │      │   │
19//! │   │  ├──────────┼─────────────────┤      ├─────┼─────┼─────┼─────┤      │   │
20//! │   │  │  "page1" │(0, v1, ref=1)   │──┐   │ p1  │ p2  │ p3  │ p4  │      │   │
21//! │   │  │  "page2" │(1, v2, ref=0)   │──┼──►└─────┴─────┴─────┴─────┘      │   │
22//! │   │  │  "page3" │(2, v3, ref=1)   │──┘                                  │   │
23//! │   │  │  "page4" │(3, v4, ref=0)   │ ← Eviction candidate (ref=0)        │   │
24//! │   │  └──────────┴─────────────────┘                                     │   │
25//! │   └─────────────────────────────────────────────────────────────────────┘   │
26//! │                                                                             │
27//! │   ┌─────────────────────────────────────────────────────────────────────┐   │
28//! │   │                    NRU Eviction (O(n) worst case)                   │   │
29//! │   │                                                                     │   │
30//! │   │   1. Scan keys vec for first entry with ref=0                       │   │
31//! │   │   2. If found: evict that entry                                     │   │
32//! │   │   3. If not found: clear all ref bits, then evict first entry       │   │
33//! │   │                                                                     │   │
34//! │   │   epoch_counter: Tracks when to do bulk ref bit clearing            │   │
35//! │   │   epoch_threshold: Number of accesses before clearing all bits      │   │
36//! │   └─────────────────────────────────────────────────────────────────────┘   │
37//! │                                                                             │
38//! └─────────────────────────────────────────────────────────────────────────────┘
39//!
40//! Access Flow
41//! ──────────────────────
42//!
43//!   get("key"):
44//!     1. Lookup entry in map
45//!     2. Set entry.referenced = true
46//!     3. Return &value
47//!
48//! Insert Flow (new key)
49//! ──────────────────────
50//!
51//!   insert("new_key", value):
52//!     1. Check map - not found
53//!     2. Evict if at capacity
54//!     3. Get next index: idx = keys.len()
55//!     4. Push key to keys vec
56//!     5. Insert Entry{idx, value, referenced: false} into map
57//!
58//! Eviction Flow
59//! ─────────────
60//!
61//!   evict_nru():
62//!     1. Scan keys for first entry with referenced=false
63//!     2. If found: remove that entry (swap-remove)
64//!     3. If not found (all referenced):
65//!        a. Clear all reference bits
66//!        b. Evict first entry (now all have ref=false)
67//! ```
68//!
69//! ## Algorithm
70//!
71//! ```text
72//! GET(key):
73//!   1. Look up entry in hash map
74//!   2. Set referenced = true
75//!   3. Return value
76//!   Cost: O(1) - just a hash lookup and bit set
77//!
78//! INSERT(key, value):
79//!   1. If key exists: update value, set referenced = true
80//!   2. If at capacity: run eviction
81//!   3. Insert entry with referenced = true
82//!
83//! EVICT():
84//!   // Phase 1: Try to find unreferenced entry
85//!   for each entry in keys:
86//!     if entry.referenced == false:
87//!       remove entry
88//!       return
89//!
90//!   // Phase 2: All referenced - clear and pick first
91//!   for each entry:
92//!     entry.referenced = false
93//!   remove first entry
94//! ```
95//!
96//! ## Performance Characteristics
97//!
98//! | Operation | Time    | Notes                             |
99//! |-----------|---------|-----------------------------------|
100//! | `get`     | O(1)    | Hash lookup + bit set             |
101//! | `insert`  | O(n)*   | *Worst case if all entries ref'd  |
102//! | `contains`| O(1)    | Hash lookup only                  |
103//! | `remove`  | O(1)    | Hash lookup + swap-remove         |
104//!
105//! ## Trade-offs
106//!
107//! | Aspect        | NRU                      | Clock                   | LRU                     |
108//! |---------------|--------------------------|-------------------------|-------------------------|
109//! | Access cost   | O(1) bit set             | O(1) bit set            | O(1) list move          |
110//! | Eviction cost | O(n) worst case          | O(n) worst case         | O(1)                    |
111//! | Granularity   | Binary (used/not used)   | Binary with hand sweep  | Full order              |
112//! | Overhead      | 1 bit per entry          | 1 bit per entry + hand  | 16 bytes per entry      |
113//!
114//! ## When to Use
115//!
116//! **Use NRU when:**
117//! - You need simple, coarse eviction tracking
118//! - Memory for full LRU list is too expensive
119//! - You can tolerate O(n) eviction in worst case
120//! - Access patterns have temporal locality
121//!
122//! **Avoid NRU when:**
123//! - You need strict LRU ordering (use LRU)
124//! - You need O(1) eviction guarantees (use Clock with hand)
125//! - You need scan resistance (use S3-FIFO, LRU-K)
126//!
127//! ## Example Usage
128//!
129//! ```
130//! use cachekit::policy::nru::NruCache;
131//! use cachekit::traits::{CoreCache, ReadOnlyCache};
132//!
133//! let mut cache = NruCache::new(100);
134//!
135//! // Insert items
136//! cache.insert("page1", "content1");
137//! cache.insert("page2", "content2");
138//!
139//! // Access sets reference bit
140//! assert_eq!(cache.get(&"page1"), Some(&"content1"));
141//!
142//! // Unreferenced items are evicted first
143//! // Referenced items protected until next epoch
144//! ```
145//!
146//! ## Implementation
147//!
148//! This implementation uses:
149//! - `HashMap<K, Entry<V>>` for O(1) lookup (stores index, value, referenced bit)
150//! - `Vec<K>` for dense key storage and eviction scanning
151//! - Swap-remove technique for O(1) removal (updates index in moved entry)
152//! - Lazy clearing of reference bits (only when needed during eviction)
153//!
154//! ## Thread Safety
155//!
156//! - [`NruCache`]: Not thread-safe, designed for single-threaded use
157//! - For concurrent access, wrap in external synchronization (e.g., `Mutex`)
158//!
159//! ## References
160//!
161//! - Wikipedia: Cache replacement policies
162
163use crate::prelude::ReadOnlyCache;
164use crate::traits::{CoreCache, MutableCache};
165use rustc_hash::FxHashMap;
166use std::fmt::{Debug, Formatter};
167use std::hash::Hash;
168
169#[cfg(feature = "metrics")]
170use crate::metrics::metrics_impl::NruMetrics;
171#[cfg(feature = "metrics")]
172use crate::metrics::snapshot::NruMetricsSnapshot;
173#[cfg(feature = "metrics")]
174use crate::metrics::traits::MetricsSnapshotProvider;
175
176/// Entry in the NRU cache containing value, index, and reference bit.
177#[derive(Debug, Clone)]
178struct Entry<V> {
179    /// Index in the keys vector
180    index: usize,
181    /// Cached value
182    value: V,
183    /// Reference bit - set on access, cleared during epoch reset
184    referenced: bool,
185}
186
187/// NRU (Not Recently Used) cache implementation.
188///
189/// Uses a single reference bit per entry to distinguish between recently used
190/// and not recently used items. Provides O(1) access but O(n) worst-case eviction.
191///
192/// # Type Parameters
193///
194/// - `K`: Key type, must be `Clone + Eq + Hash`
195/// - `V`: Value type
196///
197/// # Example
198///
199/// ```
200/// use cachekit::policy::nru::NruCache;
201/// use cachekit::traits::{CoreCache, ReadOnlyCache};
202///
203/// let mut cache = NruCache::new(100);
204///
205/// cache.insert(1, "value1");
206/// cache.insert(2, "value2");
207///
208/// // Access sets reference bit
209/// assert_eq!(cache.get(&1), Some(&"value1"));
210///
211/// // When cache is full, unreferenced items are evicted first
212/// for i in 3..=110 {
213///     cache.insert(i, "value");
214/// }
215///
216/// assert_eq!(cache.len(), 100);
217/// ```
218///
219/// # Eviction Behavior
220///
221/// When capacity is exceeded:
222/// 1. Scans for first entry with `referenced = false`
223/// 2. If all entries are referenced, clears all reference bits then evicts first entry
224///
225/// # Implementation
226///
227/// Uses HashMap + Vec for O(1) access with swap-remove for eviction.
228pub struct NruCache<K, V>
229where
230    K: Clone + Eq + Hash,
231{
232    /// HashMap for O(1) key lookup
233    map: FxHashMap<K, Entry<V>>,
234    /// Dense array of keys for eviction scanning
235    keys: Vec<K>,
236    /// Maximum capacity
237    capacity: usize,
238    #[cfg(feature = "metrics")]
239    metrics: NruMetrics,
240}
241
242impl<K, V> NruCache<K, V>
243where
244    K: Clone + Eq + Hash,
245{
246    /// Creates a new NRU cache with the specified capacity.
247    ///
248    /// # Example
249    ///
250    /// ```
251    /// use cachekit::policy::nru::NruCache;
252    /// use cachekit::traits::{CoreCache, ReadOnlyCache};
253    ///
254    /// let cache: NruCache<String, i32> = NruCache::new(100);
255    /// assert_eq!(cache.capacity(), 100);
256    /// assert!(cache.is_empty());
257    /// ```
258    #[inline]
259    pub fn new(capacity: usize) -> Self {
260        Self {
261            map: FxHashMap::default(),
262            keys: Vec::with_capacity(capacity),
263            capacity,
264            #[cfg(feature = "metrics")]
265            metrics: NruMetrics::default(),
266        }
267    }
268
269    /// Returns `true` if the cache is empty.
270    #[inline]
271    pub fn is_empty(&self) -> bool {
272        self.map.is_empty()
273    }
274
275    /// Evicts an entry using NRU policy.
276    ///
277    /// First tries to find an unreferenced entry. If all entries are referenced,
278    /// clears all reference bits and evicts the first entry.
279    ///
280    /// Returns the evicted (key, value) pair.
281    fn evict_one(&mut self) -> Option<(K, V)> {
282        if self.keys.is_empty() {
283            return None;
284        }
285
286        // Phase 1: Try to find an unreferenced entry
287        for (idx, key) in self.keys.iter().enumerate() {
288            #[cfg(feature = "metrics")]
289            {
290                self.metrics.sweep_steps += 1;
291            }
292            if let Some(entry) = self.map.get(key) {
293                if !entry.referenced {
294                    let victim_key = self.keys.swap_remove(idx);
295
296                    if idx < self.keys.len() {
297                        let swapped_key = &self.keys[idx];
298                        if let Some(swapped_entry) = self.map.get_mut(swapped_key) {
299                            swapped_entry.index = idx;
300                        }
301                    }
302
303                    let victim_entry = self.map.remove(&victim_key)?;
304                    return Some((victim_key, victim_entry.value));
305                }
306            }
307        }
308
309        // Phase 2: All entries are referenced - clear all bits and evict first
310        for key in &self.keys {
311            if let Some(entry) = self.map.get_mut(key) {
312                if entry.referenced {
313                    entry.referenced = false;
314                    #[cfg(feature = "metrics")]
315                    {
316                        self.metrics.ref_bit_resets += 1;
317                    }
318                }
319            }
320        }
321
322        if !self.keys.is_empty() {
323            let victim_key = self.keys.swap_remove(0);
324
325            if !self.keys.is_empty() {
326                let swapped_key = &self.keys[0];
327                if let Some(swapped_entry) = self.map.get_mut(swapped_key) {
328                    swapped_entry.index = 0;
329                }
330            }
331
332            let victim_entry = self.map.remove(&victim_key)?;
333            return Some((victim_key, victim_entry.value));
334        }
335
336        None
337    }
338}
339
340impl<K, V> ReadOnlyCache<K, V> for NruCache<K, V>
341where
342    K: Clone + Eq + Hash,
343{
344    /// Returns `true` if the cache contains the key.
345    ///
346    /// Does not affect the reference bit.
347    #[inline]
348    fn contains(&self, key: &K) -> bool {
349        self.map.contains_key(key)
350    }
351
352    /// Returns the number of entries in the cache.
353    #[inline]
354    fn len(&self) -> usize {
355        self.map.len()
356    }
357
358    /// Returns the maximum capacity of the cache.
359    #[inline]
360    fn capacity(&self) -> usize {
361        self.capacity
362    }
363}
364
365impl<K, V> CoreCache<K, V> for NruCache<K, V>
366where
367    K: Clone + Eq + Hash,
368{
369    /// Inserts a key-value pair into the cache.
370    ///
371    /// If the key exists, updates the value and sets the reference bit.
372    /// If at capacity, evicts using the NRU algorithm.
373    ///
374    /// # Example
375    ///
376    /// ```
377    /// use cachekit::policy::nru::NruCache;
378    /// use cachekit::traits::{CoreCache, ReadOnlyCache};
379    ///
380    /// let mut cache = NruCache::new(2);
381    /// cache.insert("a", 1);
382    /// cache.insert("b", 2);
383    ///
384    /// // Update existing
385    /// let old = cache.insert("a", 10);
386    /// assert_eq!(old, Some(1));
387    /// ```
388    #[inline]
389    fn insert(&mut self, key: K, value: V) -> Option<V> {
390        #[cfg(feature = "metrics")]
391        {
392            self.metrics.insert_calls += 1;
393        }
394
395        if self.capacity == 0 {
396            return None;
397        }
398        if let Some(entry) = self.map.get_mut(&key) {
399            #[cfg(feature = "metrics")]
400            {
401                self.metrics.insert_updates += 1;
402            }
403            let old_value = std::mem::replace(&mut entry.value, value);
404            entry.referenced = true;
405            return Some(old_value);
406        }
407
408        #[cfg(feature = "metrics")]
409        {
410            self.metrics.insert_new += 1;
411        }
412
413        if self.map.len() >= self.capacity {
414            #[cfg(feature = "metrics")]
415            {
416                self.metrics.evict_calls += 1;
417            }
418            if self.evict_one().is_some() {
419                #[cfg(feature = "metrics")]
420                {
421                    self.metrics.evicted_entries += 1;
422                }
423            }
424        }
425
426        let index = self.keys.len();
427        self.keys.push(key.clone());
428        self.map.insert(
429            key,
430            Entry {
431                index,
432                value,
433                referenced: false,
434            },
435        );
436
437        None
438    }
439
440    /// Gets a reference to the value for a key.
441    ///
442    /// Sets the reference bit on access.
443    ///
444    /// # Example
445    ///
446    /// ```
447    /// use cachekit::policy::nru::NruCache;
448    /// use cachekit::traits::{CoreCache, ReadOnlyCache};
449    ///
450    /// let mut cache = NruCache::new(10);
451    /// cache.insert("key", 42);
452    ///
453    /// // Access sets reference bit - this entry gets protection
454    /// assert_eq!(cache.get(&"key"), Some(&42));
455    /// ```
456    #[inline]
457    fn get(&mut self, key: &K) -> Option<&V> {
458        if let Some(entry) = self.map.get_mut(key) {
459            entry.referenced = true;
460            #[cfg(feature = "metrics")]
461            {
462                self.metrics.get_calls += 1;
463                self.metrics.get_hits += 1;
464            }
465            Some(&entry.value)
466        } else {
467            #[cfg(feature = "metrics")]
468            {
469                self.metrics.get_calls += 1;
470                self.metrics.get_misses += 1;
471            }
472            None
473        }
474    }
475
476    /// Clears all entries from the cache.
477    fn clear(&mut self) {
478        self.map.clear();
479        self.keys.clear();
480        #[cfg(feature = "metrics")]
481        {
482            use crate::metrics::traits::CoreMetricsRecorder;
483            self.metrics.record_clear();
484        }
485    }
486}
487
488impl<K, V> MutableCache<K, V> for NruCache<K, V>
489where
490    K: Clone + Eq + Hash,
491{
492    /// Removes a key from the cache.
493    ///
494    /// # Example
495    ///
496    /// ```
497    /// use cachekit::policy::nru::NruCache;
498    /// use cachekit::traits::{CoreCache, MutableCache, ReadOnlyCache};
499    ///
500    /// let mut cache = NruCache::new(10);
501    /// cache.insert("key", 42);
502    ///
503    /// let removed = cache.remove(&"key");
504    /// assert_eq!(removed, Some(42));
505    /// assert!(!cache.contains(&"key"));
506    /// ```
507    #[inline]
508    fn remove(&mut self, key: &K) -> Option<V> {
509        let entry = self.map.remove(key)?;
510        let idx = entry.index;
511
512        // Swap-remove from keys vec
513        self.keys.swap_remove(idx);
514
515        // Update index of swapped key if we didn't remove the last element
516        if idx < self.keys.len() {
517            let swapped_key = &self.keys[idx];
518            if let Some(swapped_entry) = self.map.get_mut(swapped_key) {
519                swapped_entry.index = idx;
520            }
521        }
522
523        Some(entry.value)
524    }
525}
526
527#[cfg(feature = "metrics")]
528impl<K, V> NruCache<K, V>
529where
530    K: Clone + Eq + Hash,
531{
532    /// Returns a snapshot of cache metrics.
533    pub fn metrics_snapshot(&self) -> NruMetricsSnapshot {
534        NruMetricsSnapshot {
535            get_calls: self.metrics.get_calls,
536            get_hits: self.metrics.get_hits,
537            get_misses: self.metrics.get_misses,
538            insert_calls: self.metrics.insert_calls,
539            insert_updates: self.metrics.insert_updates,
540            insert_new: self.metrics.insert_new,
541            evict_calls: self.metrics.evict_calls,
542            evicted_entries: self.metrics.evicted_entries,
543            sweep_steps: self.metrics.sweep_steps,
544            ref_bit_resets: self.metrics.ref_bit_resets,
545            cache_len: self.map.len(),
546            capacity: self.capacity,
547        }
548    }
549}
550
551#[cfg(feature = "metrics")]
552impl<K, V> MetricsSnapshotProvider<NruMetricsSnapshot> for NruCache<K, V>
553where
554    K: Clone + Eq + Hash,
555{
556    fn snapshot(&self) -> NruMetricsSnapshot {
557        self.metrics_snapshot()
558    }
559}
560
561impl<K, V> Debug for NruCache<K, V>
562where
563    K: Clone + Eq + Hash + std::fmt::Debug,
564    V: std::fmt::Debug,
565{
566    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
567        f.debug_struct("NruCache")
568            .field("capacity", &self.capacity)
569            .field("len", &self.map.len())
570            .field("keys", &self.keys)
571            .finish()
572    }
573}
574
575#[cfg(test)]
576mod tests {
577    use super::*;
578    use crate::traits::{CoreCache, MutableCache};
579
580    #[test]
581    fn test_new() {
582        let cache: NruCache<i32, i32> = NruCache::new(10);
583        assert_eq!(cache.capacity(), 10);
584        assert_eq!(cache.len(), 0);
585        assert!(cache.is_empty());
586    }
587
588    #[test]
589    fn test_insert_and_get() {
590        let mut cache = NruCache::new(3);
591
592        cache.insert(1, 100);
593        cache.insert(2, 200);
594        cache.insert(3, 300);
595
596        assert_eq!(cache.get(&1), Some(&100));
597        assert_eq!(cache.get(&2), Some(&200));
598        assert_eq!(cache.get(&3), Some(&300));
599        assert_eq!(cache.len(), 3);
600    }
601
602    #[test]
603    fn test_update_existing() {
604        let mut cache = NruCache::new(3);
605
606        cache.insert(1, 100);
607        let old = cache.insert(1, 999);
608
609        assert_eq!(old, Some(100));
610        assert_eq!(cache.get(&1), Some(&999));
611        assert_eq!(cache.len(), 1);
612    }
613
614    #[test]
615    fn test_eviction_unreferenced() {
616        let mut cache = NruCache::new(3);
617
618        // Insert 3 items
619        cache.insert(1, 100);
620        cache.insert(2, 200);
621        cache.insert(3, 300);
622
623        // Access only 1 and 3
624        let _ = cache.get(&1);
625        let _ = cache.get(&3);
626
627        // Insert 4th item - should evict 2 (unreferenced)
628        cache.insert(4, 400);
629
630        assert_eq!(cache.len(), 3);
631        assert!(cache.contains(&1));
632        assert!(!cache.contains(&2)); // 2 was evicted
633        assert!(cache.contains(&3));
634        assert!(cache.contains(&4));
635    }
636
637    #[test]
638    fn test_eviction_all_referenced() {
639        let mut cache = NruCache::new(3);
640
641        // Insert and access all 3 items
642        cache.insert(1, 100);
643        cache.insert(2, 200);
644        cache.insert(3, 300);
645        let _ = cache.get(&1);
646        let _ = cache.get(&2);
647        let _ = cache.get(&3);
648
649        // Insert 4th item - all are referenced, so clears bits and evicts one
650        cache.insert(4, 400);
651
652        assert_eq!(cache.len(), 3);
653        assert!(cache.contains(&4));
654    }
655
656    #[test]
657    fn test_remove() {
658        let mut cache = NruCache::new(3);
659
660        cache.insert(1, 100);
661        cache.insert(2, 200);
662        cache.insert(3, 300);
663
664        let removed = cache.remove(&2);
665        assert_eq!(removed, Some(200));
666        assert_eq!(cache.len(), 2);
667        assert!(!cache.contains(&2));
668        assert!(cache.contains(&1));
669        assert!(cache.contains(&3));
670    }
671
672    #[test]
673    fn test_clear() {
674        let mut cache = NruCache::new(3);
675
676        cache.insert(1, 100);
677        cache.insert(2, 200);
678        cache.insert(3, 300);
679
680        cache.clear();
681
682        assert_eq!(cache.len(), 0);
683        assert!(cache.is_empty());
684        assert!(!cache.contains(&1));
685    }
686
687    #[test]
688    fn test_contains_does_not_set_reference() {
689        let mut cache = NruCache::new(2);
690
691        cache.insert(1, 100);
692        cache.insert(2, 200);
693
694        // contains() doesn't set reference bit
695        assert!(cache.contains(&1));
696
697        // Manually clear reference bit by checking internals
698        // (In real usage, this would happen during eviction phase)
699        if let Some(entry) = cache.map.get_mut(&1) {
700            entry.referenced = false;
701        }
702
703        // Now 1 should be evictable
704        cache.insert(3, 300);
705
706        assert!(!cache.contains(&1)); // 1 was evicted
707        assert!(cache.contains(&2));
708        assert!(cache.contains(&3));
709    }
710
711    #[test]
712    fn test_zero_capacity() {
713        let mut cache = NruCache::new(0);
714        assert_eq!(cache.capacity(), 0);
715
716        cache.insert(1, 100);
717        assert!(!cache.contains(&1));
718    }
719}