Skip to main content

reifydb_runtime/cache/
slab.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{collections::HashMap, hash::Hash, mem};
5
6struct SlabNode<K, V> {
7	key: K,
8	value: V,
9	prev: Option<usize>,
10	next: Option<usize>,
11}
12
13pub struct SlabLru<K, V> {
14	map: HashMap<K, usize>,
15	nodes: Vec<SlabNode<K, V>>,
16	free: Vec<usize>,
17	head: Option<usize>,
18	tail: Option<usize>,
19	capacity: usize,
20}
21
22impl<K: Hash + Eq + Clone, V: Clone> SlabLru<K, V> {
23	pub fn new(capacity: usize) -> Self {
24		assert!(capacity > 0, "LRU cache capacity must be greater than 0");
25		Self {
26			map: HashMap::with_capacity(capacity),
27			nodes: Vec::with_capacity(capacity),
28			free: Vec::new(),
29			head: None,
30			tail: None,
31			capacity,
32		}
33	}
34
35	pub fn get(&mut self, key: &K) -> Option<V> {
36		if let Some(&idx) = self.map.get(key) {
37			self.move_to_front(idx);
38			Some(self.nodes[idx].value.clone())
39		} else {
40			None
41		}
42	}
43
44	pub fn put(&mut self, key: K, value: V) -> Option<V> {
45		if let Some(&idx) = self.map.get(&key) {
46			let old = mem::replace(&mut self.nodes[idx].value, value);
47			self.move_to_front(idx);
48			return Some(old);
49		}
50
51		if self.map.len() >= self.capacity {
52			self.evict_tail();
53		}
54
55		let idx = self.alloc_node(key.clone(), value);
56		self.map.insert(key, idx);
57		self.push_front(idx);
58		None
59	}
60
61	pub fn remove(&mut self, key: &K) -> Option<V> {
62		if let Some(idx) = self.map.remove(key) {
63			self.unlink(idx);
64			self.free.push(idx);
65			Some(self.nodes[idx].value.clone())
66		} else {
67			None
68		}
69	}
70
71	pub fn contains_key(&self, key: &K) -> bool {
72		self.map.contains_key(key)
73	}
74
75	pub fn clear(&mut self) {
76		self.map.clear();
77		self.nodes.clear();
78		self.free.clear();
79		self.head = None;
80		self.tail = None;
81	}
82
83	pub fn len(&self) -> usize {
84		self.map.len()
85	}
86
87	pub fn is_empty(&self) -> bool {
88		self.map.is_empty()
89	}
90
91	pub fn capacity(&self) -> usize {
92		self.capacity
93	}
94
95	fn alloc_node(&mut self, key: K, value: V) -> usize {
96		let node = SlabNode {
97			key,
98			value,
99			prev: None,
100			next: None,
101		};
102		if let Some(idx) = self.free.pop() {
103			self.nodes[idx] = node;
104			idx
105		} else {
106			self.nodes.push(node);
107			self.nodes.len() - 1
108		}
109	}
110
111	fn evict_tail(&mut self) {
112		if let Some(idx) = self.tail {
113			self.unlink(idx);
114			self.map.remove(&self.nodes[idx].key);
115			self.free.push(idx);
116		}
117	}
118
119	fn push_front(&mut self, idx: usize) {
120		self.nodes[idx].prev = None;
121		self.nodes[idx].next = self.head;
122		if let Some(h) = self.head {
123			self.nodes[h].prev = Some(idx);
124		}
125		self.head = Some(idx);
126		if self.tail.is_none() {
127			self.tail = Some(idx);
128		}
129	}
130
131	fn unlink(&mut self, idx: usize) {
132		let prev = self.nodes[idx].prev;
133		let next = self.nodes[idx].next;
134		match prev {
135			Some(p) => self.nodes[p].next = next,
136			None => self.head = next,
137		}
138		match next {
139			Some(n) => self.nodes[n].prev = prev,
140			None => self.tail = prev,
141		}
142		self.nodes[idx].prev = None;
143		self.nodes[idx].next = None;
144	}
145
146	fn move_to_front(&mut self, idx: usize) {
147		if self.head == Some(idx) {
148			return;
149		}
150		self.unlink(idx);
151		self.push_front(idx);
152	}
153}
154
155#[cfg(test)]
156mod tests {
157	use super::SlabLru;
158
159	#[test]
160	fn test_basic_operations() {
161		let mut cache = SlabLru::new(2);
162
163		assert_eq!(cache.put(1, "a"), None);
164		assert_eq!(cache.put(2, "b"), None);
165		assert_eq!(cache.get(&1), Some("a"));
166		assert_eq!(cache.get(&2), Some("b"));
167		assert_eq!(cache.len(), 2);
168	}
169
170	#[test]
171	fn test_eviction_removes_lru() {
172		let mut cache = SlabLru::new(2);
173
174		cache.put(1, "a");
175		cache.put(2, "b");
176		// Inserting a third key past capacity evicts the LRU entry (key 1).
177		// New-key insertion returns None, matching ArcLru.
178		let evicted = cache.put(3, "c");
179
180		assert_eq!(evicted, None);
181		assert_eq!(cache.get(&1), None);
182		assert_eq!(cache.get(&2), Some("b"));
183		assert_eq!(cache.get(&3), Some("c"));
184		assert_eq!(cache.len(), 2);
185	}
186
187	#[test]
188	fn test_get_promotes_recency() {
189		let mut cache = SlabLru::new(2);
190
191		cache.put(1, "a");
192		cache.put(2, "b");
193		cache.get(&1); // 1 becomes most-recent, so 2 is now the LRU victim
194		cache.put(3, "c"); // must evict 2, not 1
195
196		assert_eq!(cache.get(&1), Some("a"));
197		assert_eq!(cache.get(&2), None);
198		assert_eq!(cache.get(&3), Some("c"));
199	}
200
201	#[test]
202	fn test_update_existing_returns_old_and_keeps_len() {
203		let mut cache = SlabLru::new(2);
204
205		cache.put(1, "a");
206		let old = cache.put(1, "b");
207
208		assert_eq!(old, Some("a"));
209		assert_eq!(cache.get(&1), Some("b"));
210		assert_eq!(cache.len(), 1);
211	}
212
213	#[test]
214	fn test_remove() {
215		let mut cache = SlabLru::new(2);
216
217		cache.put(1, "a");
218		cache.put(2, "b");
219
220		assert_eq!(cache.remove(&1), Some("a"));
221		assert_eq!(cache.get(&1), None);
222		assert_eq!(cache.len(), 1);
223		assert_eq!(cache.remove(&999), None);
224	}
225
226	#[test]
227	fn test_clear_then_reuse() {
228		let mut cache = SlabLru::new(2);
229
230		cache.put(1, "a");
231		cache.put(2, "b");
232		cache.clear();
233
234		assert_eq!(cache.len(), 0);
235		assert!(cache.is_empty());
236		// The cache must remain usable after clear (slab/free-list reset).
237		assert_eq!(cache.put(5, "e"), None);
238		assert_eq!(cache.get(&5), Some("e"));
239	}
240
241	#[test]
242	fn test_contains_key_does_not_promote() {
243		let mut cache = SlabLru::new(2);
244
245		cache.put(1, "a");
246		cache.put(2, "b");
247		// contains_key is a pure lookup: it must NOT bump recency, so 1
248		// stays the LRU victim and is the one evicted next.
249		assert!(cache.contains_key(&1));
250		cache.put(3, "c");
251
252		assert_eq!(cache.get(&1), None);
253		assert_eq!(cache.get(&2), Some("b"));
254	}
255
256	#[test]
257	#[should_panic(expected = "capacity must be greater than 0")]
258	fn test_zero_capacity_panics() {
259		let _cache: SlabLru<i32, i32> = SlabLru::new(0);
260	}
261
262	#[test]
263	fn test_slab_recycles_slots_no_unbounded_growth() {
264		// The point of the fix: streaming far more distinct keys than
265		// capacity must recycle evicted slots, never grow the backing node
266		// Vec, and never exceed capacity. This is what makes steady-state
267		// put/evict allocation-free (vs the old per-put DashMap scan).
268		let cap = 8usize;
269		let mut cache = SlabLru::new(cap);
270		for k in 0..1000i32 {
271			cache.put(k, k * 10);
272			assert!(cache.len() <= cap);
273		}
274
275		assert_eq!(cache.len(), cap);
276		// Backing slab stays bounded at capacity - no growth over 1000 inserts.
277		assert_eq!(cache.nodes.len(), cap);
278		assert!(cache.free.is_empty());
279
280		// Exact-LRU: only the most-recent `cap` keys survive.
281		for k in (1000 - cap as i32)..1000 {
282			assert_eq!(cache.get(&k), Some(k * 10));
283		}
284		assert_eq!(cache.get(&0), None);
285	}
286}