1use std::marker::PhantomData;
52use std::path::{Path, PathBuf};
53
54use crate::shared_hash_map::{MapError, SharedHashMap};
55use crate::shared_linked_list::{LinkedListError, NodeHandle, SharedLinkedList};
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum LRUError {
59 Map(MapError),
60 LinkedList(LinkedListError),
61 LayoutMismatch,
62 IoError(std::io::ErrorKind),
63}
64
65impl From<MapError> for LRUError {
66 fn from(e: MapError) -> Self { Self::Map(e) }
67}
68impl From<LinkedListError> for LRUError {
69 fn from(e: LinkedListError) -> Self { Self::LinkedList(e) }
70}
71impl From<std::io::Error> for LRUError {
72 fn from(e: std::io::Error) -> Self { Self::IoError(e.kind()) }
73}
74
75fn map_path(base: &Path) -> PathBuf {
76 let mut p = base.to_path_buf();
77 let stem = p.file_name().unwrap().to_string_lossy().to_string();
78 p.set_file_name(format!("{stem}.map.bin"));
79 p
80}
81fn list_path(base: &Path) -> PathBuf {
82 let mut p = base.to_path_buf();
83 let stem = p.file_name().unwrap().to_string_lossy().to_string();
84 p.set_file_name(format!("{stem}.list.bin"));
85 p
86}
87
88pub struct SharedLRUCache<
89 K: Copy + Eq + Default + 'static,
90 V: Copy + Default + 'static,
91> {
92 map: SharedHashMap<K, u32>,
93 list: SharedLinkedList<(K, V)>,
94 capacity: u32,
95 _phantom: PhantomData<(K, V)>,
96 header_sidecar: subetha_core::HandshakeHeader,
97 ring_sidecar: Box<subetha_core::ObservationRing>,
98}
99
100impl<
101 K: Copy + Eq + Default + Send + Sync + 'static,
102 V: Copy + Default + Send + Sync + 'static,
103> subetha_sidecar::AdaptiveInstance for SharedLRUCache<K, V> {
104 fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
105 fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
106 fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
107 Box::new(subetha_sidecar::NoMigrationPolicy)
108 }
109}
110
111impl<
112 K: Copy + Eq + Default + 'static,
113 V: Copy + Default + 'static,
114> SharedLRUCache<K, V> {
115 pub fn create(
124 base_path: impl AsRef<Path>, capacity: u32,
125 ) -> Result<Self, LRUError> {
126 assert!(capacity >= 1);
127 let base = base_path.as_ref();
128 let map = SharedHashMap::<K, u32>::create(
129 map_path(base), (capacity as usize * 8).max(32),
130 )?;
131 let list = SharedLinkedList::<(K, V)>::create(
132 list_path(base), capacity as usize + 2,
133 )?;
134 Ok(Self {
135 map, list, capacity,
136 _phantom: PhantomData,
137 header_sidecar: subetha_core::HandshakeHeader::new(),
138 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
139 })
140 }
141
142 pub fn open(
143 base_path: impl AsRef<Path>, capacity: u32,
144 ) -> Result<Self, LRUError> {
145 let base = base_path.as_ref();
146 let map = SharedHashMap::<K, u32>::open(
147 map_path(base), (capacity as usize * 8).max(32),
148 )?;
149 let list = SharedLinkedList::<(K, V)>::open(
150 list_path(base), capacity as usize + 2,
151 )?;
152 Ok(Self {
153 map, list, capacity,
154 _phantom: PhantomData,
155 header_sidecar: subetha_core::HandshakeHeader::new(),
156 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
157 })
158 }
159
160 #[inline]
161 pub fn capacity(&self) -> u32 { self.capacity }
162
163 pub fn len(&self) -> usize {
165 self.list.len()
166 }
167
168 pub fn is_empty(&self) -> bool { self.len() == 0 }
169
170 pub fn get(&self, key: &K) -> Option<V> {
174 let r = (|| {
175 let idx = self.map.get(key)?;
176 let (k, v) = self.list.get(NodeHandle::new(idx))?;
177 if k != *key { return None; }
182 Some(v)
183 })();
184 self.ring_sidecar.push_op(
185 crate::sidecar_ops::lru_cache::OP_GET,
186 if r.is_none() { 2 } else { 0 },
187 );
188 r
189 }
190
191 pub fn contains_key(&self, key: &K) -> bool {
193 self.map.contains_key(key)
194 }
195
196 pub fn touch(&self, key: &K) -> bool {
199 let promoted = (|| {
200 let idx = self.map.get(key)?;
201 let (k, v) = self.list.remove(NodeHandle::new(idx))?;
202 match self.list.push_front((k, v)) {
204 Ok(new_handle) => {
205 self.map.insert(k, new_handle.index).ok();
209 Some(true)
210 }
211 Err(_) => {
212 self.list.push_back((k, v)).ok();
215 Some(false)
216 }
217 }
218 })().unwrap_or(false);
219 self.ring_sidecar.push_op(
220 crate::sidecar_ops::lru_cache::OP_TOUCH,
221 if promoted { 0 } else { 2 }, );
223 promoted
224 }
225
226 pub fn get_and_touch(&self, key: &K) -> Option<V> {
228 let v = self.get(key)?;
229 self.touch(key);
230 Some(v)
231 }
232
233 pub fn put(&self, key: K, value: V) -> Result<Option<V>, LRUError> {
237 let r = self.put_inner(key, value);
238 self.ring_sidecar.push_op(
239 crate::sidecar_ops::lru_cache::OP_PUT,
240 if r.is_err() { 1 } else { 0 },
241 );
242 r
243 }
244
245 fn put_inner(&self, key: K, value: V) -> Result<Option<V>, LRUError> {
246 if let Some(idx) = self.map.get(&key) {
248 let old = self.list.remove(NodeHandle::new(idx))
249 .map(|(_, v)| v);
250 let new_handle = self.list.push_front((key, value))?;
251 self.map.insert(key, new_handle.index)?;
252 return Ok(old);
253 }
254 if self.len() >= self.capacity as usize {
256 self.evict_oldest();
257 }
258 let new_handle = self.list.push_front((key, value))?;
259 self.map.insert(key, new_handle.index)?;
260 Ok(None)
261 }
262
263 pub fn remove(&self, key: &K) -> Option<V> {
265 let r = (|| {
266 let idx = self.map.remove(key)?;
267 let (_, v) = self.list.remove(NodeHandle::new(idx))?;
268 Some(v)
269 })();
270 self.ring_sidecar.push_op(
271 crate::sidecar_ops::lru_cache::OP_REMOVE,
272 if r.is_none() { 2 } else { 0 },
273 );
274 r
275 }
276
277 pub fn evict_oldest(&self) -> Option<(K, V)> {
280 let r = (|| {
282 let (k, v) = self.list.pop_back()?;
283 let _removed = self.map.remove(&k);
286 Some((k, v))
287 })();
288 self.ring_sidecar.push_op(
289 crate::sidecar_ops::lru_cache::OP_EVICT,
290 if r.is_none() { 2 } else { 0 },
291 );
292 r
293 }
294
295 pub fn snapshot_mru_first(&self) -> Vec<(K, V)> {
298 self.list.iter_forward()
299 }
300
301 pub fn snapshot_lru_first(&self) -> Vec<(K, V)> {
303 self.list.iter_backward()
304 }
305
306 pub fn flush(&self) -> Result<(), LRUError> {
307 self.map.flush()?;
308 self.list.flush()?;
309 Ok(())
310 }
311
312 pub fn flush_async(&self) -> Result<(), LRUError> {
313 self.map.flush_async()?;
314 self.list.flush_async()?;
315 Ok(())
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 fn tmp_base(name: &str) -> PathBuf {
324 let mut p = std::env::temp_dir();
325 let pid = std::process::id();
326 p.push(format!("subetha-lru-{name}-{pid}"));
327 p
328 }
329
330 fn cleanup(base: &Path) {
331 std::fs::remove_file(map_path(base)).ok();
332 std::fs::remove_file(list_path(base)).ok();
333 }
334
335 #[test]
336 fn create_initial_state_is_empty() {
337 let base = tmp_base("init");
338 let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 16).unwrap();
339 assert_eq!(c.capacity(), 16);
340 assert_eq!(c.len(), 0);
341 assert!(c.is_empty());
342 assert_eq!(c.get(&1), None);
343 cleanup(&base);
344 }
345
346 #[test]
347 fn put_get_round_trip() {
348 let base = tmp_base("rt");
349 let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 16).unwrap();
350 c.put(1, 100).unwrap();
351 c.put(2, 200).unwrap();
352 c.put(3, 300).unwrap();
353 assert_eq!(c.get(&1), Some(100));
354 assert_eq!(c.get(&2), Some(200));
355 assert_eq!(c.get(&3), Some(300));
356 assert_eq!(c.get(&999), None);
357 assert_eq!(c.len(), 3);
358 cleanup(&base);
359 }
360
361 #[test]
362 fn put_existing_key_updates_and_promotes() {
363 let base = tmp_base("update");
364 let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 16).unwrap();
365 c.put(1, 100).unwrap();
366 c.put(2, 200).unwrap();
367 c.put(3, 300).unwrap();
368 let prev = c.put(1, 111).unwrap();
370 assert_eq!(prev, Some(100));
371 assert_eq!(c.get(&1), Some(111));
372 assert_eq!(c.len(), 3);
373 let snap = c.snapshot_mru_first();
375 assert_eq!(snap[0], (1, 111));
376 cleanup(&base);
377 }
378
379 #[test]
380 fn get_does_not_promote() {
381 let base = tmp_base("get-no-promote");
382 let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 16).unwrap();
383 c.put(1, 100).unwrap();
384 c.put(2, 200).unwrap();
385 c.put(3, 300).unwrap();
386 let before = c.snapshot_mru_first();
388 assert_eq!(before, vec![(3, 300), (2, 200), (1, 100)]);
389 c.get(&1).unwrap();
391 let after = c.snapshot_mru_first();
392 assert_eq!(after, vec![(3, 300), (2, 200), (1, 100)],
393 "plain get must not change order");
394 cleanup(&base);
395 }
396
397 #[test]
398 fn touch_promotes_to_front() {
399 let base = tmp_base("touch");
400 let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 16).unwrap();
401 c.put(1, 100).unwrap();
402 c.put(2, 200).unwrap();
403 c.put(3, 300).unwrap();
404 assert!(c.touch(&1));
406 let snap = c.snapshot_mru_first();
407 assert_eq!(snap, vec![(1, 100), (3, 300), (2, 200)]);
408 cleanup(&base);
409 }
410
411 #[test]
412 fn touch_nonexistent_returns_false() {
413 let base = tmp_base("touch-none");
414 let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 8).unwrap();
415 c.put(1, 100).unwrap();
416 assert!(!c.touch(&999));
417 cleanup(&base);
418 }
419
420 #[test]
421 fn get_and_touch_combines_both() {
422 let base = tmp_base("get-touch");
423 let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 8).unwrap();
424 c.put(1, 100).unwrap();
425 c.put(2, 200).unwrap();
426 c.put(3, 300).unwrap();
427 let v = c.get_and_touch(&1).unwrap();
428 assert_eq!(v, 100);
429 let snap = c.snapshot_mru_first();
430 assert_eq!(snap[0], (1, 100));
431 cleanup(&base);
432 }
433
434 #[test]
435 fn eviction_at_capacity_drops_oldest() {
436 let base = tmp_base("evict");
437 let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 3).unwrap();
438 c.put(1, 10).unwrap();
439 c.put(2, 20).unwrap();
440 c.put(3, 30).unwrap();
441 let prev = c.put(4, 40).unwrap();
443 assert_eq!(prev, None);
444 assert_eq!(c.len(), 3);
445 assert_eq!(c.get(&1), None, "key 1 should have been evicted");
446 assert_eq!(c.get(&2), Some(20));
447 assert_eq!(c.get(&3), Some(30));
448 assert_eq!(c.get(&4), Some(40));
449 cleanup(&base);
450 }
451
452 #[test]
453 fn touch_prevents_eviction_of_recently_used() {
454 let base = tmp_base("touch-evict");
455 let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 3).unwrap();
456 c.put(1, 10).unwrap();
457 c.put(2, 20).unwrap();
458 c.put(3, 30).unwrap();
459 c.touch(&1);
461 c.put(4, 40).unwrap();
463 assert_eq!(c.get(&1), Some(10), "touched key 1 should survive");
464 assert_eq!(c.get(&2), None, "untouched key 2 should be evicted");
465 assert_eq!(c.get(&3), Some(30));
466 assert_eq!(c.get(&4), Some(40));
467 cleanup(&base);
468 }
469
470 #[test]
471 fn remove_cleans_both_map_and_list() {
472 let base = tmp_base("rm");
473 let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 8).unwrap();
474 c.put(1, 10).unwrap();
475 c.put(2, 20).unwrap();
476 c.put(3, 30).unwrap();
477 let v = c.remove(&2).unwrap();
478 assert_eq!(v, 20);
479 assert_eq!(c.len(), 2);
480 assert_eq!(c.get(&2), None);
481 assert_eq!(c.get(&1), Some(10));
483 assert_eq!(c.get(&3), Some(30));
484 let snap = c.snapshot_mru_first();
486 let keys: Vec<u32> = snap.iter().map(|(k, _)| *k).collect();
487 assert!(!keys.contains(&2));
488 cleanup(&base);
489 }
490
491 #[test]
492 fn evict_oldest_returns_lru() {
493 let base = tmp_base("evict-oldest");
494 let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 8).unwrap();
495 c.put(1, 10).unwrap();
496 c.put(2, 20).unwrap();
497 c.put(3, 30).unwrap();
498 let evicted = c.evict_oldest().unwrap();
499 assert_eq!(evicted, (1, 10));
500 assert_eq!(c.len(), 2);
501 assert_eq!(c.get(&1), None);
502 cleanup(&base);
503 }
504
505 #[test]
506 fn evict_oldest_on_empty_returns_none() {
507 let base = tmp_base("evict-empty");
508 let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 4).unwrap();
509 assert_eq!(c.evict_oldest(), None);
510 cleanup(&base);
511 }
512
513 #[test]
514 fn snapshot_mru_and_lru_first_are_reverses() {
515 let base = tmp_base("snap");
516 let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 8).unwrap();
517 for (k, v) in [(1, 10), (2, 20), (3, 30)] {
518 c.put(k, v).unwrap();
519 }
520 let mru = c.snapshot_mru_first();
521 let mut lru = c.snapshot_lru_first();
522 lru.reverse();
523 assert_eq!(mru, lru);
524 cleanup(&base);
525 }
526
527 #[test]
528 fn cross_handle_visibility() {
529 let base = tmp_base("cross-handle");
530 let writer: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 8).unwrap();
531 let reader: SharedLRUCache<u32, u32> = SharedLRUCache::open(&base, 8).unwrap();
532 writer.put(42, 4242).unwrap();
533 writer.put(7, 77).unwrap();
534 assert_eq!(reader.get(&42), Some(4242));
535 assert_eq!(reader.get(&7), Some(77));
536 writer.evict_oldest();
539 assert_eq!(reader.get(&42), None); cleanup(&base);
541 }
542
543 #[test]
544 fn struct_key_value_round_trip() {
545 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
546 #[repr(C)]
547 struct UserKey { realm: u32, user: u32 }
548 #[derive(Clone, Copy, Debug, PartialEq, Default)]
549 #[repr(C)]
550 struct Session { token: u64, expires_us: u64 }
551 let base = tmp_base("struct");
552 let c: SharedLRUCache<UserKey, Session> = SharedLRUCache::create(&base, 8).unwrap();
553 let k = UserKey { realm: 1, user: 42 };
554 let v = Session { token: 0xDEAD_BEEF, expires_us: 9_999_999_999 };
555 c.put(k, v).unwrap();
556 assert_eq!(c.get(&k), Some(v));
557 cleanup(&base);
558 }
559
560 #[test]
561 fn disk_persistence_survives_reopen() {
562 let base = tmp_base("disk");
563 {
564 let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 8).unwrap();
565 c.put(1, 100).unwrap();
566 c.put(2, 200).unwrap();
567 c.put(3, 300).unwrap();
568 c.flush().unwrap();
569 }
570 let c2: SharedLRUCache<u32, u32> = SharedLRUCache::open(&base, 8).unwrap();
571 assert_eq!(c2.len(), 3);
572 assert_eq!(c2.get(&1), Some(100));
573 assert_eq!(c2.get(&2), Some(200));
574 assert_eq!(c2.get(&3), Some(300));
575 cleanup(&base);
576 }
577
578 #[test]
579 fn many_evictions_maintain_mru_correctness() {
580 let base = tmp_base("many-evict");
584 let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 10).unwrap();
585 for k in 0..50u32 {
586 c.put(k, k * 10).unwrap();
587 }
588 assert_eq!(c.len(), 10);
591 for k in 0..40u32 {
592 assert_eq!(c.get(&k), None, "key {k} should have been evicted");
593 }
594 for k in 40..50u32 {
595 assert_eq!(c.get(&k), Some(k * 10), "key {k} should be present");
596 }
597 cleanup(&base);
598 }
599
600 #[test]
601 fn touched_keys_survive_subsequent_evictions() {
602 let base = tmp_base("touch-survival");
606 let c: SharedLRUCache<u32, u32> = SharedLRUCache::create(&base, 5).unwrap();
607 for k in 0..5u32 { c.put(k, k * 10).unwrap(); }
609 c.touch(&0);
612 c.touch(&1);
613 c.put(100, 1000).unwrap();
615 c.put(101, 1010).unwrap();
616 c.put(102, 1020).unwrap();
617 assert_eq!(c.get(&0), Some(0));
619 assert_eq!(c.get(&1), Some(10));
620 assert_eq!(c.get(&100), Some(1000));
622 assert_eq!(c.get(&101), Some(1010));
623 assert_eq!(c.get(&102), Some(1020));
624 cleanup(&base);
625 }
626}