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 put(&self, key: K, value: V) -> Option<V> {
73		self.inner.put(key, value)
74	}
75
76	pub fn remove(&self, key: &K) -> Option<V> {
77		self.inner.remove(key)
78	}
79
80	pub fn contains_key(&self, key: &K) -> bool {
81		self.inner.contains_key(key)
82	}
83
84	pub fn clear(&self) {
85		self.inner.clear();
86	}
87
88	pub fn len(&self) -> usize {
89		self.inner.len()
90	}
91
92	pub fn is_empty(&self) -> bool {
93		self.len() == 0
94	}
95
96	pub fn capacity(&self) -> usize {
97		self.inner.capacity()
98	}
99
100	pub fn run_pending_tasks(&self) {
101		self.inner.run_pending_tasks();
102	}
103}
104
105#[cfg(all(test, not(reifydb_single_threaded)))]
106mod tests {
107	use std::{mem::size_of, sync::Arc};
108
109	use reifydb_value::{byte_size::ByteSize, count::Count};
110
111	use super::{CacheFootprint, SyncLru};
112
113	fn footprint(_key: &u64, value: &String) -> CacheFootprint {
114		// Mirrors how a real consumer derives a footprint from HeapSize.
115		CacheFootprint {
116			heap: value.capacity(),
117			payload: size_of::<u64>() + value.len(),
118		}
119	}
120
121	#[test]
122	fn test_basic_operations() {
123		let cache = SyncLru::new(2);
124
125		assert_eq!(cache.put(1, "a"), None);
126		assert_eq!(cache.put(2, "b"), None);
127		assert_eq!(cache.get(&1), Some("a"));
128		assert_eq!(cache.get(&2), Some("b"));
129		cache.run_pending_tasks();
130		assert_eq!(cache.len(), 2);
131	}
132
133	#[test]
134	fn test_eviction() {
135		let cache = SyncLru::new(2);
136
137		cache.put(1, "a");
138		cache.put(2, "b");
139		let evicted = cache.put(3, "c");
140		cache.run_pending_tasks();
141
142		assert_eq!(evicted, None);
143		assert_eq!(cache.get(&1), None);
144		assert_eq!(cache.get(&2), Some("b"));
145		assert_eq!(cache.get(&3), Some("c"));
146	}
147
148	#[test]
149	fn test_lru_order() {
150		let cache = SyncLru::new(2);
151
152		cache.put(1, "a");
153		cache.put(2, "b");
154		cache.run_pending_tasks();
155		// moka applies recency lazily, so each step needs its pending tasks drained before
156		// the next one observes it.
157		cache.get(&1);
158		cache.run_pending_tasks();
159		cache.put(3, "c");
160		cache.run_pending_tasks();
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 = SyncLru::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		cache.run_pending_tasks();
177		assert_eq!(cache.len(), 1);
178	}
179
180	#[test]
181	fn test_remove() {
182		let cache = SyncLru::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		cache.run_pending_tasks();
191		assert_eq!(cache.len(), 1);
192	}
193
194	#[test]
195	fn test_clear() {
196		let cache = SyncLru::new(2);
197
198		cache.put(1, "a");
199		cache.put(2, "b");
200		cache.clear();
201		cache.run_pending_tasks();
202
203		assert_eq!(cache.len(), 0);
204		assert!(cache.is_empty());
205	}
206
207	#[test]
208	fn test_contains_key() {
209		let cache = SyncLru::new(2);
210
211		cache.put(1, "a");
212		assert!(cache.contains_key(&1));
213		assert!(!cache.contains_key(&2));
214	}
215
216	#[test]
217	fn unmeasured_cache_reports_no_memory_usage() {
218		let cache: SyncLru<u64, String> = SyncLru::new(2);
219		cache.put(1, "a".to_string());
220		assert_eq!(cache.memory_usage(), None);
221	}
222
223	#[test]
224	fn measured_cache_counts_entries_heap_and_payload() {
225		let cache: SyncLru<u64, String> = SyncLru::measured(8, footprint);
226		let a = String::with_capacity(16) + "aaaa";
227		let b = String::with_capacity(32) + "bbbbbbbb";
228		let heap = a.capacity() + b.capacity();
229		let payload = (8 + a.len()) + (8 + b.len());
230
231		cache.put(1, a);
232		cache.put(2, b);
233		cache.run_pending_tasks();
234
235		let usage = cache.memory_usage().expect("measured cache must report usage");
236		assert_eq!(usage.entries, Count::new(2));
237		assert_eq!(usage.payload, ByteSize::from_bytes(payload as u64));
238		// Equality with heap alone would mean the cache's own per-entry bookkeeping is
239		// unaccounted for.
240		assert!(usage.resident.as_bytes() > heap as u64);
241	}
242
243	#[test]
244	fn replacing_a_key_keeps_single_entry_accounting() {
245		let cache: SyncLru<u64, String> = SyncLru::measured(8, footprint);
246		cache.put(1, "aaaa".to_string());
247		cache.put(1, "bbbbbbbb".to_string());
248		cache.run_pending_tasks();
249
250		let usage = cache.memory_usage().expect("measured cache must report usage");
251		assert_eq!(usage.entries, Count::new(1), "replacement must not leak the old entry's count");
252		assert_eq!(
253			usage.payload,
254			ByteSize::from_bytes(8 + 8),
255			"payload must reflect only the replacement value"
256		);
257	}
258
259	#[test]
260	fn removal_and_clear_release_accounted_memory() {
261		let cache: SyncLru<u64, String> = SyncLru::measured(8, footprint);
262		cache.put(1, "aaaa".to_string());
263		cache.put(2, "bbbb".to_string());
264		cache.remove(&1);
265		cache.run_pending_tasks();
266
267		let usage = cache.memory_usage().expect("measured cache must report usage");
268		assert_eq!(usage.entries, Count::new(1));
269		assert_eq!(usage.payload, ByteSize::from_bytes(8 + 4));
270
271		cache.clear();
272		cache.run_pending_tasks();
273
274		let usage = cache.memory_usage().expect("measured cache must report usage");
275		assert_eq!(usage.entries, Count::ZERO, "clear must release every accounted entry");
276		assert_eq!(usage.payload, ByteSize::ZERO);
277		assert_eq!(usage.resident, ByteSize::ZERO);
278	}
279
280	#[test]
281	fn eviction_at_capacity_releases_the_victims_memory() {
282		let cache: SyncLru<u64, String> = SyncLru::measured(2, footprint);
283		cache.put(1, "aaaa".to_string());
284		cache.put(2, "bbbb".to_string());
285		cache.run_pending_tasks();
286		cache.put(3, "cccc".to_string());
287		cache.run_pending_tasks();
288
289		let usage = cache.memory_usage().expect("measured cache must report usage");
290		assert_eq!(usage.entries, Count::new(2), "eviction must decrement the entry count");
291		assert_eq!(usage.payload, ByteSize::from_bytes(2 * (8 + 4)));
292	}
293
294	#[test]
295	fn measured_values_shared_via_arc_count_their_heap_once_per_slot() {
296		fn arc_footprint(_key: &u64, value: &Arc<str>) -> CacheFootprint {
297			CacheFootprint {
298				heap: 2 * size_of::<usize>() + value.len(),
299				payload: size_of::<u64>() + value.len(),
300			}
301		}
302		let cache: SyncLru<u64, Arc<str>> = SyncLru::measured(8, arc_footprint);
303		let shared: Arc<str> = Arc::from("shared-value");
304		cache.put(1, shared.clone());
305		cache.put(2, shared);
306		cache.run_pending_tasks();
307
308		let usage = cache.memory_usage().expect("measured cache must report usage");
309		assert_eq!(usage.entries, Count::new(2));
310		assert_eq!(usage.payload, ByteSize::from_bytes(2 * (8 + 12)));
311	}
312}