Skip to main content

reifydb_runtime/cache/sync/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::hash::Hash;
5
6use cfg_if::cfg_if;
7use reifydb_value::{byte_size::ByteSize, count::Count};
8
9#[cfg(not(reifydb_single_threaded))]
10pub(crate) mod host;
11#[cfg(reifydb_single_threaded)]
12pub(crate) mod wasm;
13
14cfg_if! {
15	if #[cfg(not(reifydb_single_threaded))] {
16		type LruImpl<K, V> = host::HostLru<K, V>;
17	} else {
18		type LruImpl<K, V> = wasm::WasmLru<K, V>;
19	}
20}
21
22#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
23pub struct CacheFootprint {
24	pub heap: usize,
25	pub payload: usize,
26}
27
28pub type FootprintFn<K, V> = fn(&K, &V) -> CacheFootprint;
29
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
31pub struct CacheMemory {
32	pub entries: Count,
33	pub resident: ByteSize,
34	pub payload: ByteSize,
35}
36
37pub struct SyncLru<K, V>
38where
39	K: Hash + Eq + Clone + Send + Sync + 'static,
40	V: Clone + Send + Sync + 'static,
41{
42	inner: LruImpl<K, V>,
43}
44
45impl<K, V> SyncLru<K, V>
46where
47	K: Hash + Eq + Clone + Send + Sync + 'static,
48	V: Clone + Send + Sync + 'static,
49{
50	pub fn new(capacity: usize) -> Self {
51		assert!(capacity > 0, "LRU cache capacity must be greater than 0");
52		Self {
53			inner: LruImpl::new(capacity),
54		}
55	}
56
57	pub fn measured(capacity: usize, footprint: FootprintFn<K, V>) -> Self {
58		assert!(capacity > 0, "LRU cache capacity must be greater than 0");
59		Self {
60			inner: LruImpl::measured(capacity, footprint),
61		}
62	}
63
64	pub fn memory_usage(&self) -> Option<CacheMemory> {
65		self.inner.memory_usage()
66	}
67
68	pub fn get(&self, key: &K) -> Option<V> {
69		self.inner.get(key)
70	}
71
72	pub fn get_with(&self, key: K, init: impl FnOnce() -> V) -> V {
73		self.inner.get_with(key, init)
74	}
75
76	pub fn put(&self, key: K, value: V) -> Option<V> {
77		self.inner.put(key, value)
78	}
79
80	pub fn remove(&self, key: &K) -> Option<V> {
81		self.inner.remove(key)
82	}
83
84	pub fn contains_key(&self, key: &K) -> bool {
85		self.inner.contains_key(key)
86	}
87
88	pub fn clear(&self) {
89		self.inner.clear();
90	}
91
92	pub fn len(&self) -> usize {
93		self.inner.len()
94	}
95
96	pub fn is_empty(&self) -> bool {
97		self.len() == 0
98	}
99
100	pub fn capacity(&self) -> usize {
101		self.inner.capacity()
102	}
103
104	pub fn run_pending_tasks(&self) {
105		self.inner.run_pending_tasks();
106	}
107}
108
109#[cfg(all(test, not(reifydb_single_threaded)))]
110mod tests {
111	use std::{
112		mem::size_of,
113		sync::{
114			Arc, Barrier,
115			atomic::{AtomicUsize, Ordering},
116		},
117		thread::spawn,
118	};
119
120	use reifydb_value::{byte_size::ByteSize, count::Count};
121
122	use super::{CacheFootprint, SyncLru};
123
124	fn footprint(_key: &u64, value: &String) -> CacheFootprint {
125		// Mirrors how a real consumer derives a footprint from HeapSize.
126		CacheFootprint {
127			heap: value.capacity(),
128			payload: size_of::<u64>() + value.len(),
129		}
130	}
131
132	#[test]
133	fn test_basic_operations() {
134		let cache = SyncLru::new(2);
135
136		assert_eq!(cache.put(1, "a"), None);
137		assert_eq!(cache.put(2, "b"), None);
138		assert_eq!(cache.get(&1), Some("a"));
139		assert_eq!(cache.get(&2), Some("b"));
140		cache.run_pending_tasks();
141		assert_eq!(cache.len(), 2);
142	}
143
144	#[test]
145	fn test_eviction() {
146		let cache = SyncLru::new(2);
147
148		cache.put(1, "a");
149		cache.put(2, "b");
150		let evicted = cache.put(3, "c");
151		cache.run_pending_tasks();
152
153		assert_eq!(evicted, None);
154		assert_eq!(cache.get(&1), None);
155		assert_eq!(cache.get(&2), Some("b"));
156		assert_eq!(cache.get(&3), Some("c"));
157	}
158
159	#[test]
160	fn test_lru_order() {
161		let cache = SyncLru::new(2);
162
163		cache.put(1, "a");
164		cache.put(2, "b");
165		cache.run_pending_tasks();
166		// moka applies recency lazily, so each step needs its pending tasks drained before
167		// the next one observes it.
168		cache.get(&1);
169		cache.run_pending_tasks();
170		cache.put(3, "c");
171		cache.run_pending_tasks();
172
173		assert_eq!(cache.get(&1), Some("a"));
174		assert_eq!(cache.get(&2), None);
175		assert_eq!(cache.get(&3), Some("c"));
176	}
177
178	#[test]
179	fn test_update_existing() {
180		let cache = SyncLru::new(2);
181
182		cache.put(1, "a");
183		let old = cache.put(1, "b");
184
185		assert_eq!(old, Some("a"));
186		assert_eq!(cache.get(&1), Some("b"));
187		cache.run_pending_tasks();
188		assert_eq!(cache.len(), 1);
189	}
190
191	#[test]
192	fn test_remove() {
193		let cache = SyncLru::new(2);
194
195		cache.put(1, "a");
196		cache.put(2, "b");
197		let removed = cache.remove(&1);
198
199		assert_eq!(removed, Some("a"));
200		assert_eq!(cache.get(&1), None);
201		cache.run_pending_tasks();
202		assert_eq!(cache.len(), 1);
203	}
204
205	#[test]
206	fn test_clear() {
207		let cache = SyncLru::new(2);
208
209		cache.put(1, "a");
210		cache.put(2, "b");
211		cache.clear();
212		cache.run_pending_tasks();
213
214		assert_eq!(cache.len(), 0);
215		assert!(cache.is_empty());
216	}
217
218	#[test]
219	fn test_contains_key() {
220		let cache = SyncLru::new(2);
221
222		cache.put(1, "a");
223		assert!(cache.contains_key(&1));
224		assert!(!cache.contains_key(&2));
225	}
226
227	#[test]
228	fn unmeasured_cache_reports_no_memory_usage() {
229		let cache: SyncLru<u64, String> = SyncLru::new(2);
230		cache.put(1, "a".to_string());
231		assert_eq!(cache.memory_usage(), None);
232	}
233
234	#[test]
235	fn measured_cache_counts_entries_heap_and_payload() {
236		let cache: SyncLru<u64, String> = SyncLru::measured(8, footprint);
237		let a = String::with_capacity(16) + "aaaa";
238		let b = String::with_capacity(32) + "bbbbbbbb";
239		let heap = a.capacity() + b.capacity();
240		let payload = (8 + a.len()) + (8 + b.len());
241
242		cache.put(1, a);
243		cache.put(2, b);
244		cache.run_pending_tasks();
245
246		let usage = cache.memory_usage().expect("measured cache must report usage");
247		assert_eq!(usage.entries, Count::new(2));
248		assert_eq!(usage.payload, ByteSize::from_bytes(payload as u64));
249		// Equality with heap alone would mean the cache's own per-entry bookkeeping is
250		// unaccounted for.
251		assert!(usage.resident.as_bytes() > heap as u64);
252	}
253
254	#[test]
255	fn replacing_a_key_keeps_single_entry_accounting() {
256		let cache: SyncLru<u64, String> = SyncLru::measured(8, footprint);
257		cache.put(1, "aaaa".to_string());
258		cache.put(1, "bbbbbbbb".to_string());
259		cache.run_pending_tasks();
260
261		let usage = cache.memory_usage().expect("measured cache must report usage");
262		assert_eq!(usage.entries, Count::new(1), "replacement must not leak the old entry's count");
263		assert_eq!(
264			usage.payload,
265			ByteSize::from_bytes(8 + 8),
266			"payload must reflect only the replacement value"
267		);
268	}
269
270	#[test]
271	fn removal_and_clear_release_accounted_memory() {
272		let cache: SyncLru<u64, String> = SyncLru::measured(8, footprint);
273		cache.put(1, "aaaa".to_string());
274		cache.put(2, "bbbb".to_string());
275		cache.remove(&1);
276		cache.run_pending_tasks();
277
278		let usage = cache.memory_usage().expect("measured cache must report usage");
279		assert_eq!(usage.entries, Count::new(1));
280		assert_eq!(usage.payload, ByteSize::from_bytes(8 + 4));
281
282		cache.clear();
283		cache.run_pending_tasks();
284
285		let usage = cache.memory_usage().expect("measured cache must report usage");
286		assert_eq!(usage.entries, Count::ZERO, "clear must release every accounted entry");
287		assert_eq!(usage.payload, ByteSize::ZERO);
288		assert_eq!(usage.resident, ByteSize::ZERO);
289	}
290
291	#[test]
292	fn eviction_at_capacity_releases_the_victims_memory() {
293		let cache: SyncLru<u64, String> = SyncLru::measured(2, footprint);
294		cache.put(1, "aaaa".to_string());
295		cache.put(2, "bbbb".to_string());
296		cache.run_pending_tasks();
297		cache.put(3, "cccc".to_string());
298		cache.run_pending_tasks();
299
300		let usage = cache.memory_usage().expect("measured cache must report usage");
301		assert_eq!(usage.entries, Count::new(2), "eviction must decrement the entry count");
302		assert_eq!(usage.payload, ByteSize::from_bytes(2 * (8 + 4)));
303	}
304
305	#[test]
306	fn get_with_runs_the_initializer_once_under_concurrent_misses() {
307		// A get-then-put lets every racer insert its own value; the losers are dropped from the
308		// cache while their callers still hold them, so callers disagree on the cached identity.
309		fn arc_footprint(_key: &u64, value: &Arc<str>) -> CacheFootprint {
310			CacheFootprint {
311				heap: 2 * size_of::<usize>() + value.len(),
312				payload: size_of::<u64>() + value.len(),
313			}
314		}
315		let cache: Arc<SyncLru<u64, Arc<str>>> = Arc::new(SyncLru::measured(8, arc_footprint));
316		let runs = Arc::new(AtomicUsize::new(0));
317		let barrier = Arc::new(Barrier::new(8));
318		let handles: Vec<_> = (0..8)
319			.map(|_| {
320				let cache = Arc::clone(&cache);
321				let runs = Arc::clone(&runs);
322				let barrier = Arc::clone(&barrier);
323				spawn(move || {
324					barrier.wait();
325					cache.get_with(1, || {
326						runs.fetch_add(1, Ordering::Relaxed);
327						Arc::from("value")
328					})
329				})
330			})
331			.collect();
332		let values: Vec<Arc<str>> = handles.into_iter().map(|h| h.join().unwrap()).collect();
333
334		assert_eq!(
335			runs.load(Ordering::Relaxed),
336			1,
337			"the initializer must run once per key, not once per racing caller"
338		);
339		for value in &values {
340			assert!(Arc::ptr_eq(value, &values[0]), "every racer must observe one allocation");
341		}
342		cache.run_pending_tasks();
343		let usage = cache.memory_usage().expect("measured cache must report usage");
344		assert_eq!(usage.entries, Count::new(1), "a single-flight insert must be counted exactly once");
345	}
346
347	#[test]
348	fn get_with_on_a_present_key_neither_reruns_the_initializer_nor_double_counts() {
349		let cache: SyncLru<u64, String> = SyncLru::measured(8, footprint);
350		cache.put(1, "aaaa".to_string());
351
352		let value = cache.get_with(1, || panic!("the initializer must not run for a cached key"));
353		cache.run_pending_tasks();
354
355		let usage = cache.memory_usage().expect("measured cache must report usage");
356		assert_eq!(value, "aaaa");
357		assert_eq!(usage.entries, Count::new(1), "a hit must not add a second entry");
358		assert_eq!(usage.payload, ByteSize::from_bytes(8 + 4), "a hit must not re-add the value's payload");
359	}
360
361	#[test]
362	fn measured_values_shared_via_arc_count_their_heap_once_per_slot() {
363		fn arc_footprint(_key: &u64, value: &Arc<str>) -> CacheFootprint {
364			CacheFootprint {
365				heap: 2 * size_of::<usize>() + value.len(),
366				payload: size_of::<u64>() + value.len(),
367			}
368		}
369		let cache: SyncLru<u64, Arc<str>> = SyncLru::measured(8, arc_footprint);
370		let shared: Arc<str> = Arc::from("shared-value");
371		cache.put(1, shared.clone());
372		cache.put(2, shared);
373		cache.run_pending_tasks();
374
375		let usage = cache.memory_usage().expect("measured cache must report usage");
376		assert_eq!(usage.entries, Count::new(2));
377		assert_eq!(usage.payload, ByteSize::from_bytes(2 * (8 + 12)));
378	}
379}