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}