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<Option<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::new(),
27			nodes: Vec::new(),
28			free: Vec::new(),
29			head: None,
30			tail: None,
31			capacity,
32		}
33	}
34
35	pub fn unbounded() -> Self {
36		Self::new(usize::MAX)
37	}
38
39	pub fn get(&mut self, key: &K) -> Option<V> {
40		if let Some(&idx) = self.map.get(key) {
41			self.move_to_front(idx);
42			Some(self.node(idx).value.clone())
43		} else {
44			None
45		}
46	}
47
48	pub fn put(&mut self, key: K, value: V) -> Option<V> {
49		if let Some(&idx) = self.map.get(&key) {
50			let old = mem::replace(&mut self.node_mut(idx).value, value);
51			self.move_to_front(idx);
52			return Some(old);
53		}
54
55		if self.map.len() >= self.capacity {
56			self.evict_tail();
57		}
58
59		let idx = self.alloc_node(key.clone(), value);
60		self.map.insert(key, idx);
61		self.push_front(idx);
62		None
63	}
64
65	pub fn remove(&mut self, key: &K) -> Option<V> {
66		if let Some(idx) = self.map.remove(key) {
67			self.unlink(idx);
68			self.free.push(idx);
69			self.nodes[idx].take().map(|node| node.value)
70		} else {
71			None
72		}
73	}
74
75	pub fn pop_tail(&mut self) -> Option<(K, V)> {
76		let idx = self.tail?;
77		self.unlink(idx);
78		let node = self.nodes[idx].take()?;
79		self.map.remove(&node.key);
80		self.free.push(idx);
81		Some((node.key, node.value))
82	}
83
84	pub fn contains_key(&self, key: &K) -> bool {
85		self.map.contains_key(key)
86	}
87
88	pub fn clear(&mut self) {
89		self.map.clear();
90		self.nodes.clear();
91		self.free.clear();
92		self.head = None;
93		self.tail = None;
94	}
95
96	pub fn len(&self) -> usize {
97		self.map.len()
98	}
99
100	pub fn is_empty(&self) -> bool {
101		self.map.is_empty()
102	}
103
104	pub fn capacity(&self) -> usize {
105		self.capacity
106	}
107
108	pub fn values(&self) -> impl Iterator<Item = &V> {
109		self.nodes.iter().filter_map(|slot| slot.as_ref().map(|node| &node.value))
110	}
111
112	pub fn keys(&self) -> impl Iterator<Item = &K> {
113		self.nodes.iter().filter_map(|slot| slot.as_ref().map(|node| &node.key))
114	}
115
116	pub const fn entry_struct_bytes() -> usize {
117		mem::size_of::<Option<SlabNode<K, V>>>() + mem::size_of::<K>() + mem::size_of::<usize>() * 2
118	}
119
120	pub fn struct_bytes(&self) -> usize {
121		self.nodes.capacity() * Self::entry_struct_bytes() + self.free.capacity() * mem::size_of::<usize>()
122	}
123
124	fn node(&self, idx: usize) -> &SlabNode<K, V> {
125		self.nodes[idx].as_ref().expect("occupied slab slot")
126	}
127
128	fn node_mut(&mut self, idx: usize) -> &mut SlabNode<K, V> {
129		self.nodes[idx].as_mut().expect("occupied slab slot")
130	}
131
132	fn alloc_node(&mut self, key: K, value: V) -> usize {
133		let node = SlabNode {
134			key,
135			value,
136			prev: None,
137			next: None,
138		};
139		if let Some(idx) = self.free.pop() {
140			self.nodes[idx] = Some(node);
141			idx
142		} else {
143			self.nodes.push(Some(node));
144			self.nodes.len() - 1
145		}
146	}
147
148	fn evict_tail(&mut self) {
149		if let Some(idx) = self.tail {
150			self.unlink(idx);
151			if let Some(node) = self.nodes[idx].take() {
152				self.map.remove(&node.key);
153			}
154			self.free.push(idx);
155		}
156	}
157
158	fn push_front(&mut self, idx: usize) {
159		let head = self.head;
160		{
161			let node = self.node_mut(idx);
162			node.prev = None;
163			node.next = head;
164		}
165		if let Some(h) = head {
166			self.node_mut(h).prev = Some(idx);
167		}
168		self.head = Some(idx);
169		if self.tail.is_none() {
170			self.tail = Some(idx);
171		}
172	}
173
174	fn unlink(&mut self, idx: usize) {
175		let (prev, next) = {
176			let node = self.node(idx);
177			(node.prev, node.next)
178		};
179		match prev {
180			Some(p) => self.node_mut(p).next = next,
181			None => self.head = next,
182		}
183		match next {
184			Some(n) => self.node_mut(n).prev = prev,
185			None => self.tail = prev,
186		}
187		let node = self.node_mut(idx);
188		node.prev = None;
189		node.next = None;
190	}
191
192	fn move_to_front(&mut self, idx: usize) {
193		if self.head == Some(idx) {
194			return;
195		}
196		self.unlink(idx);
197		self.push_front(idx);
198	}
199}
200
201#[cfg(test)]
202mod tests {
203	use super::SlabLru;
204
205	#[test]
206	fn test_basic_operations() {
207		let mut cache = SlabLru::new(2);
208
209		assert_eq!(cache.put(1, "a"), None);
210		assert_eq!(cache.put(2, "b"), None);
211		assert_eq!(cache.get(&1), Some("a"));
212		assert_eq!(cache.get(&2), Some("b"));
213		assert_eq!(cache.len(), 2);
214	}
215
216	#[test]
217	fn test_eviction_removes_lru() {
218		let mut cache = SlabLru::new(2);
219
220		cache.put(1, "a");
221		cache.put(2, "b");
222		// A new-key insertion returns None even when it evicts, matching ArcLru.
223		let evicted = cache.put(3, "c");
224
225		assert_eq!(evicted, None);
226		assert_eq!(cache.get(&1), None);
227		assert_eq!(cache.get(&2), Some("b"));
228		assert_eq!(cache.get(&3), Some("c"));
229		assert_eq!(cache.len(), 2);
230	}
231
232	#[test]
233	fn test_get_promotes_recency() {
234		let mut cache = SlabLru::new(2);
235
236		cache.put(1, "a");
237		cache.put(2, "b");
238		cache.get(&1); // 1 becomes most-recent, so 2 is now the LRU victim
239		cache.put(3, "c"); // must evict 2, not 1
240
241		assert_eq!(cache.get(&1), Some("a"));
242		assert_eq!(cache.get(&2), None);
243		assert_eq!(cache.get(&3), Some("c"));
244	}
245
246	#[test]
247	fn test_update_existing_returns_old_and_keeps_len() {
248		let mut cache = SlabLru::new(2);
249
250		cache.put(1, "a");
251		let old = cache.put(1, "b");
252
253		assert_eq!(old, Some("a"));
254		assert_eq!(cache.get(&1), Some("b"));
255		assert_eq!(cache.len(), 1);
256	}
257
258	#[test]
259	fn test_remove() {
260		let mut cache = SlabLru::new(2);
261
262		cache.put(1, "a");
263		cache.put(2, "b");
264
265		assert_eq!(cache.remove(&1), Some("a"));
266		assert_eq!(cache.get(&1), None);
267		assert_eq!(cache.len(), 1);
268		assert_eq!(cache.remove(&999), None);
269	}
270
271	#[test]
272	fn test_clear_then_reuse() {
273		let mut cache = SlabLru::new(2);
274
275		cache.put(1, "a");
276		cache.put(2, "b");
277		cache.clear();
278
279		assert_eq!(cache.len(), 0);
280		assert!(cache.is_empty());
281		// clear() must reset the free list too, or the slab is unusable afterwards.
282		assert_eq!(cache.put(5, "e"), None);
283		assert_eq!(cache.get(&5), Some("e"));
284	}
285
286	#[test]
287	fn test_contains_key_does_not_promote() {
288		let mut cache = SlabLru::new(2);
289
290		cache.put(1, "a");
291		cache.put(2, "b");
292		// A pure lookup must not bump recency, so 1 stays the next victim.
293		assert!(cache.contains_key(&1));
294		cache.put(3, "c");
295
296		assert_eq!(cache.get(&1), None);
297		assert_eq!(cache.get(&2), Some("b"));
298	}
299
300	#[test]
301	fn test_keys_yields_every_resident_key() {
302		// keys() backs prefix eviction; a key left behind for an evicted slot would leak
303		// mappings the sweep believes it removed.
304		let mut cache = SlabLru::new(2);
305		cache.put(1, "a");
306		cache.put(2, "b");
307		cache.put(3, "c"); // evicts 1
308
309		let mut keys: Vec<i32> = cache.keys().copied().collect();
310		keys.sort_unstable();
311		assert_eq!(keys, vec![2, 3], "keys() must reflect exactly the resident set after eviction");
312	}
313
314	#[test]
315	#[should_panic(expected = "capacity must be greater than 0")]
316	fn test_zero_capacity_panics() {
317		let _cache: SlabLru<i32, i32> = SlabLru::new(0);
318	}
319
320	#[test]
321	fn test_pop_tail_removes_lru_first() {
322		// pop_tail must fully unlink the entry from map and slab, or a later remove of the
323		// same key double-counts the bytes the byte-budget loop already released.
324		let mut cache = SlabLru::unbounded();
325		cache.put(1, "a");
326		cache.put(2, "b");
327		cache.put(3, "c");
328		cache.get(&1); // 1 becomes MRU; LRU order is now 2, 3, 1
329
330		assert_eq!(cache.pop_tail(), Some((2, "b")));
331		assert_eq!(cache.pop_tail(), Some((3, "c")));
332		assert_eq!(cache.len(), 1);
333		assert_eq!(cache.get(&2), None);
334		assert_eq!(cache.pop_tail(), Some((1, "a")));
335		assert_eq!(cache.pop_tail(), None);
336		assert!(cache.is_empty());
337
338		// A fully drained cache must still accept inserts, so slots have to be recycled.
339		cache.put(9, "z");
340		assert_eq!(cache.get(&9), Some("z"));
341	}
342
343	#[test]
344	fn test_unbounded_never_evicts_on_put() {
345		// An unbounded slab must retain every key so eviction stays solely the pop_tail
346		// loop's decision, not the map's.
347		let mut cache = SlabLru::unbounded();
348		for k in 0..10_000i32 {
349			cache.put(k, k);
350		}
351		assert_eq!(cache.len(), 10_000);
352		assert_eq!(cache.get(&0), Some(0));
353	}
354
355	#[test]
356	fn test_slab_recycles_slots_no_unbounded_growth() {
357		// Streaming far more distinct keys than capacity must recycle evicted slots rather
358		// than grow the node Vec, which is what keeps steady-state put/evict allocation-free.
359		let cap = 8usize;
360		let mut cache = SlabLru::new(cap);
361		for k in 0..1000i32 {
362			cache.put(k, k * 10);
363			assert!(cache.len() <= cap);
364		}
365
366		assert_eq!(cache.len(), cap);
367		assert_eq!(cache.nodes.len(), cap);
368		assert!(cache.free.is_empty());
369
370		// Eviction stays exact-LRU under churn: only the most recent `cap` keys survive.
371		for k in (1000 - cap as i32)..1000 {
372			assert_eq!(cache.get(&k), Some(k * 10));
373		}
374		assert_eq!(cache.get(&0), None);
375	}
376}