Skip to main content

atomr_remote/
cache.rs

1//! Bounded LRU caches used by the remoting hot paths.
2//!
3//! Phase 5.H of `docs/full-port-plan.md`. Akka.NET parity:
4//! `RemoteActorRefProvider` keeps an LRU of `ActorPath ↔ RemoteRef`
5//! and `SerializerRegistry` keeps an LRU of serializer-id ↔
6//! manifest. Both speed up repeat lookups on the inbound dispatcher.
7//!
8//! We hand-roll a small LRU instead of pulling in `lru` so the
9//! crate stays dep-free.
10
11use std::collections::HashMap;
12use std::hash::Hash;
13
14/// Bounded LRU cache. Eviction is O(N) per insert in the worst case
15/// (we scan the access order), but N is the cache capacity — small
16/// in practice (≤4096 for the remoting use case).
17pub struct LruCache<K: Eq + Hash + Clone, V: Clone> {
18    capacity: usize,
19    map: HashMap<K, (V, u64)>,
20    /// Monotonically-increasing access counter.
21    tick: u64,
22}
23
24impl<K: Eq + Hash + Clone, V: Clone> LruCache<K, V> {
25    pub fn new(capacity: usize) -> Self {
26        assert!(capacity >= 1, "capacity must be >= 1");
27        Self { capacity, map: HashMap::with_capacity(capacity), tick: 0 }
28    }
29
30    pub fn capacity(&self) -> usize {
31        self.capacity
32    }
33
34    pub fn len(&self) -> usize {
35        self.map.len()
36    }
37
38    pub fn is_empty(&self) -> bool {
39        self.map.is_empty()
40    }
41
42    pub fn contains(&self, key: &K) -> bool {
43        self.map.contains_key(key)
44    }
45
46    /// Look up `key`, bumping its recency. Returns `None` on miss.
47    pub fn get(&mut self, key: &K) -> Option<V> {
48        let v = self.map.get_mut(key).map(|(v, last)| {
49            self.tick += 1;
50            *last = self.tick;
51            v.clone()
52        });
53        v
54    }
55
56    /// Insert `(key, value)`. Evicts the least-recently-used entry
57    /// when at capacity. Returns the evicted value if any.
58    pub fn put(&mut self, key: K, value: V) -> Option<V> {
59        self.tick += 1;
60        if self.map.contains_key(&key) {
61            let (slot, last) = self.map.get_mut(&key).expect("checked above");
62            *slot = value;
63            *last = self.tick;
64            return None;
65        }
66        if self.map.len() >= self.capacity {
67            // Evict the entry with the smallest `last`.
68            if let Some((evict_k, _)) =
69                self.map.iter().min_by_key(|(_, (_, last))| *last).map(|(k, _)| (k.clone(), ()))
70            {
71                let (evicted, _) = self.map.remove(&evict_k).expect("just found");
72                self.map.insert(key, (value, self.tick));
73                return Some(evicted);
74            }
75        }
76        self.map.insert(key, (value, self.tick));
77        None
78    }
79
80    pub fn remove(&mut self, key: &K) -> Option<V> {
81        self.map.remove(key).map(|(v, _)| v)
82    }
83
84    pub fn clear(&mut self) {
85        self.map.clear();
86        self.tick = 0;
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn put_and_get() {
96        let mut c = LruCache::<&'static str, i32>::new(3);
97        assert!(c.put("a", 1).is_none());
98        assert!(c.put("b", 2).is_none());
99        assert_eq!(c.get(&"a"), Some(1));
100        assert_eq!(c.get(&"b"), Some(2));
101        assert_eq!(c.get(&"c"), None);
102        assert_eq!(c.len(), 2);
103    }
104
105    #[test]
106    fn lru_eviction_drops_least_recently_used() {
107        let mut c = LruCache::<&'static str, i32>::new(2);
108        c.put("a", 1);
109        c.put("b", 2);
110        let _ = c.get(&"a"); // bump a
111        let evicted = c.put("c", 3); // b should evict
112        assert_eq!(evicted, Some(2));
113        assert!(!c.contains(&"b"));
114        assert!(c.contains(&"a"));
115        assert!(c.contains(&"c"));
116    }
117
118    #[test]
119    fn put_existing_key_updates_value_no_evict() {
120        let mut c = LruCache::<&'static str, i32>::new(2);
121        c.put("a", 1);
122        c.put("b", 2);
123        let evicted = c.put("a", 99);
124        assert!(evicted.is_none());
125        assert_eq!(c.get(&"a"), Some(99));
126        assert_eq!(c.len(), 2);
127    }
128
129    #[test]
130    fn remove_drops_entry() {
131        let mut c = LruCache::<&'static str, i32>::new(2);
132        c.put("a", 1);
133        let r = c.remove(&"a");
134        assert_eq!(r, Some(1));
135        assert!(c.is_empty());
136    }
137
138    #[test]
139    fn clear_resets_state() {
140        let mut c = LruCache::<&'static str, i32>::new(2);
141        c.put("a", 1);
142        c.put("b", 2);
143        c.clear();
144        assert!(c.is_empty());
145    }
146
147    #[test]
148    #[should_panic]
149    fn zero_capacity_panics() {
150        let _: LruCache<&'static str, i32> = LruCache::new(0);
151    }
152}