Skip to main content

velesdb_core/cache/
lru.rs

1//! LRU Cache implementation for `VelesDB`.
2//!
3//! Thread-safe LRU cache with O(1) operations using `IndexMap`.
4//! Based on arXiv:2310.11703v2 recommendations.
5//!
6//! # Performance (US-CORE-003-14)
7//!
8//! | Operation | Complexity | Notes |
9//! |-----------|------------|-------|
10//! | insert | O(1) | Amortized |
11//! | get | O(n) | With recency update (shift_remove) |
12//! | remove | O(1) | swap_remove |
13//! | eviction | O(1) | shift_remove from front |
14
15#![allow(clippy::cast_precision_loss)] // Precision loss acceptable for hit rate calculation
16
17use indexmap::IndexMap;
18use parking_lot::RwLock;
19use std::hash::Hash;
20use std::sync::atomic::{AtomicU64, Ordering};
21
22/// Cache statistics for monitoring.
23#[derive(Debug, Clone, Default)]
24pub struct CacheStats {
25    /// Number of cache hits.
26    pub hits: u64,
27    /// Number of cache misses.
28    pub misses: u64,
29    /// Number of evictions.
30    pub evictions: u64,
31}
32
33impl CacheStats {
34    /// Calculate hit rate (0.0 to 1.0).
35    #[must_use]
36    pub fn hit_rate(&self) -> f64 {
37        let total = self.hits + self.misses;
38        if total == 0 {
39            0.0
40        } else {
41            self.hits as f64 / total as f64
42        }
43    }
44}
45
46/// Thread-safe LRU cache with O(1) operations.
47///
48/// Uses `IndexMap` internally which preserves insertion order
49/// and provides O(1) access, making move-to-back operations efficient.
50pub struct LruCache<K, V>
51where
52    K: Hash + Eq + Clone,
53    V: Clone,
54{
55    /// Maximum capacity.
56    capacity: usize,
57    /// Internal data protected by `RwLock`.
58    /// `IndexMap` preserves insertion order (front = LRU, back = MRU).
59    inner: RwLock<IndexMap<K, V>>,
60    /// Statistics (atomic for lock-free reads).
61    hits: AtomicU64,
62    misses: AtomicU64,
63    evictions: AtomicU64,
64}
65
66impl<K, V> LruCache<K, V>
67where
68    K: Hash + Eq + Clone,
69    V: Clone,
70{
71    /// Create a new LRU cache with the given capacity.
72    #[must_use]
73    pub fn new(capacity: usize) -> Self {
74        Self {
75            capacity,
76            inner: RwLock::new(IndexMap::with_capacity(capacity)),
77            hits: AtomicU64::new(0),
78            misses: AtomicU64::new(0),
79            evictions: AtomicU64::new(0),
80        }
81    }
82
83    /// Get the capacity of the cache.
84    #[must_use]
85    pub fn capacity(&self) -> usize {
86        self.capacity
87    }
88
89    /// Get the current number of entries.
90    #[must_use]
91    pub fn len(&self) -> usize {
92        self.inner.read().len()
93    }
94
95    /// Check if the cache is empty.
96    #[must_use]
97    pub fn is_empty(&self) -> bool {
98        self.inner.read().is_empty()
99    }
100
101    /// Insert a key-value pair, evicting LRU entry if at capacity.
102    ///
103    /// O(1) amortized complexity.
104    pub fn insert(&self, key: K, value: V) {
105        let mut inner = self.inner.write();
106
107        // Check if key already exists - if so, remove and re-insert to move to back
108        if inner.shift_remove(&key).is_some() {
109            // Key existed, just re-insert at back
110            inner.insert(key, value);
111            return;
112        }
113
114        // Evict LRU (front) if at capacity
115        if inner.len() >= self.capacity {
116            // shift_remove(0) removes the first element (LRU)
117            if inner.shift_remove_index(0).is_some() {
118                self.evictions.fetch_add(1, Ordering::Relaxed);
119            }
120        }
121
122        // Insert new entry at back (MRU)
123        inner.insert(key, value);
124    }
125
126    /// Get a value by key, updating recency.
127    ///
128    /// F-18: Uses a single write lock for lookup + move-to-back in one operation,
129    /// eliminating the previous read-lock → clone → write-lock → re-clone pattern
130    /// (2 locks + 2 clones → 1 lock + 1 clone).
131    #[must_use]
132    pub fn get(&self, key: &K) -> Option<V> {
133        let mut inner = self.inner.write();
134        if let Some((_idx, owned_key, value)) = inner.shift_remove_full(key) {
135            let cloned = value.clone();
136            // Re-insert at back (MRU position)
137            inner.insert(owned_key, value);
138            drop(inner);
139            self.hits.fetch_add(1, Ordering::Relaxed);
140            Some(cloned)
141        } else {
142            drop(inner);
143            self.misses.fetch_add(1, Ordering::Relaxed);
144            None
145        }
146    }
147
148    /// Get a value without updating recency (peek).
149    ///
150    /// O(1) complexity with only read lock.
151    #[must_use]
152    pub fn peek(&self, key: &K) -> Option<V> {
153        let inner = self.inner.read();
154        inner.get(key).cloned()
155    }
156
157    /// Remove a key from the cache.
158    ///
159    /// O(1) complexity using `swap_remove` (doesn't preserve order of other elements).
160    pub fn remove(&self, key: &K) {
161        let mut inner = self.inner.write();
162        inner.swap_remove(key);
163    }
164
165    /// Clear all entries.
166    pub fn clear(&self) {
167        let mut inner = self.inner.write();
168        inner.clear();
169    }
170
171    /// Get cache statistics.
172    #[must_use]
173    pub fn stats(&self) -> CacheStats {
174        CacheStats {
175            hits: self.hits.load(Ordering::Relaxed),
176            misses: self.misses.load(Ordering::Relaxed),
177            evictions: self.evictions.load(Ordering::Relaxed),
178        }
179    }
180}
181
182impl<K, V> Default for LruCache<K, V>
183where
184    K: Hash + Eq + Clone,
185    V: Clone,
186{
187    fn default() -> Self {
188        Self::new(10_000) // Default 10K entries
189    }
190}