Skip to main content

reifydb_core/util/
lru.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright (c) 2025 ReifyDB
3
4use std::{
5	hash::Hash,
6	mem,
7	sync::atomic::{AtomicU64, Ordering},
8};
9
10use dashmap::DashMap;
11
12/// Thread-safe LRU cache using DashMap with approximate LRU eviction.
13///
14/// Uses an atomic counter for access ordering instead of timestamps,
15/// providing better performance while maintaining approximate LRU semantics.
16pub struct LruCache<K, V> {
17	map: DashMap<K, Entry<V>>,
18	capacity: usize,
19	access_counter: AtomicU64,
20}
21
22struct Entry<V> {
23	value: V,
24	last_access: u64,
25}
26
27impl<K: Hash + Eq + Clone, V: Clone> LruCache<K, V> {
28	pub fn new(capacity: usize) -> Self {
29		assert!(capacity > 0, "LRU cache capacity must be greater than 0");
30		Self {
31			map: DashMap::with_capacity(capacity),
32			capacity,
33			access_counter: AtomicU64::new(0),
34		}
35	}
36
37	/// Get a value and update LRU order. Returns cloned value.
38	pub fn get(&self, key: &K) -> Option<V> {
39		if let Some(mut entry) = self.map.get_mut(key) {
40			entry.last_access = self.access_counter.fetch_add(1, Ordering::Relaxed);
41			Some(entry.value.clone())
42		} else {
43			None
44		}
45	}
46
47	/// Put a value into the cache. Returns old value if key existed.
48	pub fn put(&self, key: K, value: V) -> Option<V> {
49		let access = self.access_counter.fetch_add(1, Ordering::Relaxed);
50
51		// Check if key already exists
52		if let Some(mut entry) = self.map.get_mut(&key) {
53			let old_value = mem::replace(&mut entry.value, value);
54			entry.last_access = access;
55			return Some(old_value);
56		}
57
58		// Evict if at capacity before inserting
59		if self.map.len() >= self.capacity {
60			self.evict_lru();
61		}
62
63		self.map.insert(
64			key,
65			Entry {
66				value,
67				last_access: access,
68			},
69		);
70		None
71	}
72
73	/// Remove a value from the cache.
74	pub fn remove(&self, key: &K) -> Option<V> {
75		self.map.remove(key).map(|(_, entry)| entry.value)
76	}
77
78	/// Check if cache contains a key (read-only, doesn't update LRU order).
79	pub fn contains_key(&self, key: &K) -> bool {
80		self.map.contains_key(key)
81	}
82
83	/// Clear all entries from the cache.
84	pub fn clear(&self) {
85		self.map.clear();
86	}
87
88	/// Get the current number of entries.
89	pub fn len(&self) -> usize {
90		self.map.len()
91	}
92
93	/// Check if the cache is empty.
94	pub fn is_empty(&self) -> bool {
95		self.map.is_empty()
96	}
97
98	/// Get the capacity of the cache.
99	pub fn capacity(&self) -> usize {
100		self.capacity
101	}
102
103	/// Evict the least recently used entry (approximate).
104	fn evict_lru(&self) {
105		let mut oldest_key: Option<K> = None;
106		let mut oldest_access = u64::MAX;
107
108		for entry in self.map.iter() {
109			if entry.last_access < oldest_access {
110				oldest_access = entry.last_access;
111				oldest_key = Some(entry.key().clone());
112			}
113		}
114
115		if let Some(key) = oldest_key {
116			self.map.remove(&key);
117		}
118	}
119}
120
121#[cfg(test)]
122pub mod tests {
123
124	mod lru {
125		use crate::util::lru::LruCache;
126
127		#[test]
128		fn test_basic_operations() {
129			let cache = LruCache::new(2);
130
131			assert_eq!(cache.put(1, "a"), None);
132			assert_eq!(cache.put(2, "b"), None);
133			assert_eq!(cache.get(&1), Some("a"));
134			assert_eq!(cache.get(&2), Some("b"));
135			assert_eq!(cache.len(), 2);
136		}
137
138		#[test]
139		fn test_eviction() {
140			let cache = LruCache::new(2);
141
142			cache.put(1, "a");
143			cache.put(2, "b");
144			let evicted = cache.put(3, "c");
145
146			// Eviction returns None because it's a new key insertion
147			// The evicted value is the LRU entry (key 1)
148			assert_eq!(evicted, None);
149			assert_eq!(cache.get(&1), None);
150			assert_eq!(cache.get(&2), Some("b"));
151			assert_eq!(cache.get(&3), Some("c"));
152		}
153
154		#[test]
155		fn test_lru_order() {
156			let cache = LruCache::new(2);
157
158			cache.put(1, "a");
159			cache.put(2, "b");
160			cache.get(&1); // Access 1, making it more recent than 2
161			cache.put(3, "c"); // Should evict 2 (least recently used)
162
163			assert_eq!(cache.get(&1), Some("a"));
164			assert_eq!(cache.get(&2), None);
165			assert_eq!(cache.get(&3), Some("c"));
166		}
167
168		#[test]
169		fn test_update_existing() {
170			let cache = LruCache::new(2);
171
172			cache.put(1, "a");
173			let old = cache.put(1, "b");
174
175			assert_eq!(old, Some("a"));
176			assert_eq!(cache.get(&1), Some("b"));
177			assert_eq!(cache.len(), 1);
178		}
179
180		#[test]
181		fn test_remove() {
182			let cache = LruCache::new(2);
183
184			cache.put(1, "a");
185			cache.put(2, "b");
186			let removed = cache.remove(&1);
187
188			assert_eq!(removed, Some("a"));
189			assert_eq!(cache.get(&1), None);
190			assert_eq!(cache.len(), 1);
191		}
192
193		#[test]
194		fn test_clear() {
195			let cache = LruCache::new(2);
196
197			cache.put(1, "a");
198			cache.put(2, "b");
199			cache.clear();
200
201			assert_eq!(cache.len(), 0);
202			assert!(cache.is_empty());
203		}
204
205		#[test]
206		fn test_contains_key() {
207			let cache = LruCache::new(2);
208
209			cache.put(1, "a");
210			assert!(cache.contains_key(&1));
211			assert!(!cache.contains_key(&2));
212		}
213	}
214}