reifydb_runtime/cache/sync/
mod.rs1use 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 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 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 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 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}