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