Skip to main content

reth_network/
cache.rs

1//! Network cache support
2
3use alloy_primitives::map::DefaultHashBuilder;
4use core::hash::BuildHasher;
5use derive_more::{Deref, DerefMut};
6use itertools::Itertools;
7use schnellru::{ByLength, Limiter, Unlimited};
8use std::{fmt, hash::Hash};
9
10/// A minimal LRU cache based on a [`LruMap`](schnellru::LruMap) with limited capacity.
11///
12/// If the length exceeds the set capacity, the oldest element will be removed
13/// In the limit, for each element inserted the oldest existing element will be removed.
14pub struct LruCache<T: Hash + Eq + fmt::Debug, S = DefaultHashBuilder>
15where
16    S: BuildHasher,
17{
18    limit: u32,
19    inner: LruMap<T, (), ByLength, S>,
20}
21
22impl<T: Hash + Eq + fmt::Debug> LruCache<T> {
23    /// Creates a new [`LruCache`] using the given limit
24    pub fn new(limit: u32) -> Self {
25        Self::with_hasher(limit, Default::default())
26    }
27}
28
29impl<T: Hash + Eq + fmt::Debug, S: BuildHasher> LruCache<T, S> {
30    /// Creates a new [`LruCache`] using the given limit and hash builder.
31    pub const fn with_hasher(limit: u32, hash_builder: S) -> Self {
32        // limit of lru map is one element more, so can give eviction feedback, which isn't
33        // supported by LruMap
34        Self { inner: LruMap::with_hasher(limit + 1, hash_builder), limit }
35    }
36
37    /// Insert an element into the set.
38    ///
39    /// This operation uses `get_or_insert` from the underlying `schnellru::LruMap` which:
40    /// - Automatically evicts the least recently used item if capacity is exceeded
41    ///
42    /// This method is more efficient than [`insert_and_get_evicted`](Self::insert_and_get_evicted)
43    /// as it performs fewer checks. Use this method when you don't need information about
44    /// evicted values.
45    ///
46    /// If the set did not have this value present, true is returned.
47    /// If the set did have this value present, false is returned.
48    pub fn insert(&mut self, entry: T) -> bool {
49        let mut is_new = false;
50        self.inner.get_or_insert(entry, || {
51            is_new = true;
52        });
53        is_new
54    }
55
56    /// Same as [`insert`](Self::insert) but returns a tuple, where the second index is the evicted
57    /// value, if one was evicted.
58    pub fn insert_and_get_evicted(&mut self, entry: T) -> (bool, Option<T>) {
59        let new = self.inner.peek(&entry).is_none();
60        let evicted =
61            (new && (self.limit as usize) <= self.inner.len()).then(|| self.remove_lru()).flatten();
62        _ = self.inner.get_or_insert(entry, || ());
63
64        (new, evicted)
65    }
66
67    /// Gets the given element, if exists, and promotes to lru.
68    pub fn get(&mut self, entry: &T) -> Option<&T> {
69        let _ = self.inner.get(entry)?;
70        self.iter().next()
71    }
72
73    /// Iterates through entries and returns a reference to the given entry, if exists, without
74    /// promoting to lru.
75    ///
76    /// NOTE: Use this for type that have custom impl of [`PartialEq`] and [`Eq`], that aren't
77    /// unique by all fields. If `PartialEq` and `Eq` are derived for a type, it's more efficient to
78    /// call [`contains`](Self::contains).
79    pub fn find(&self, entry: &T) -> Option<&T> {
80        self.iter().find(|key| *key == entry)
81    }
82
83    /// Remove the least recently used entry and return it.
84    ///
85    /// If the `LruCache` is empty or if the eviction feedback is
86    /// configured, this will return None.
87    #[inline]
88    fn remove_lru(&mut self) -> Option<T> {
89        self.inner.pop_oldest().map(|(k, ())| k)
90    }
91
92    /// Expels the given value. Returns true if the value existed.
93    pub fn remove(&mut self, value: &T) -> bool {
94        self.inner.remove(value).is_some()
95    }
96
97    /// Returns `true` if the set contains a value.
98    pub fn contains(&self, value: &T) -> bool {
99        self.inner.peek(value).is_some()
100    }
101
102    /// Returns an iterator over all cached entries in lru order
103    pub fn iter(&self) -> impl Iterator<Item = &T> + '_ {
104        self.inner.iter().map(|(k, ())| k)
105    }
106
107    /// Returns number of elements currently in cache.
108    pub fn len(&self) -> usize {
109        self.inner.len()
110    }
111
112    /// Returns `true` if there are currently no elements in the cache.
113    pub fn is_empty(&self) -> bool {
114        self.inner.is_empty()
115    }
116}
117
118impl<T, S> Extend<T> for LruCache<T, S>
119where
120    T: Eq + Hash + fmt::Debug,
121    S: BuildHasher,
122{
123    fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
124        for item in iter {
125            _ = self.insert(item);
126        }
127    }
128}
129
130impl<T, S> fmt::Debug for LruCache<T, S>
131where
132    T: fmt::Debug + Hash + Eq,
133    S: BuildHasher,
134{
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        let mut debug_struct = f.debug_struct("LruCache");
137
138        debug_struct.field("limit", &self.limit);
139
140        debug_struct.field(
141            "ret %iter",
142            &format_args!("Iter: {{{} }}", self.iter().map(|k| format!(" {k:?}")).format(",")),
143        );
144
145        debug_struct.finish()
146    }
147}
148
149/// Wrapper of [`schnellru::LruMap`] that implements [`fmt::Debug`] and with the common hash
150/// builder.
151#[derive(Deref, DerefMut, Default)]
152pub struct LruMap<K, V, L = ByLength, S = DefaultHashBuilder>(schnellru::LruMap<K, V, L, S>)
153where
154    K: Hash + PartialEq,
155    L: Limiter<K, V>,
156    S: BuildHasher;
157
158impl<K, V, L, S> fmt::Debug for LruMap<K, V, L, S>
159where
160    K: Hash + PartialEq + fmt::Display,
161    V: fmt::Debug,
162    L: Limiter<K, V> + fmt::Debug,
163    S: BuildHasher,
164{
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        let mut debug_struct = f.debug_struct("LruMap");
167
168        debug_struct.field("limiter", self.limiter());
169
170        debug_struct.field(
171            "ret %iter",
172            &format_args!(
173                "Iter: {{{} }}",
174                self.iter().map(|(k, v)| format!(" {k}: {v:?}")).format(",")
175            ),
176        );
177
178        debug_struct.finish()
179    }
180}
181
182impl<K, V> LruMap<K, V>
183where
184    K: Hash + PartialEq,
185{
186    /// Returns a new cache with default limiter and hash builder.
187    pub fn new(max_length: u32) -> Self {
188        Self::with_hasher(max_length, Default::default())
189    }
190}
191
192impl<K, V, S> LruMap<K, V, ByLength, S>
193where
194    K: Hash + PartialEq,
195    S: BuildHasher,
196{
197    /// Returns a new cache with default limiter and the given hash builder.
198    pub const fn with_hasher(max_length: u32, hash_builder: S) -> Self {
199        Self(schnellru::LruMap::with_hasher(ByLength::new(max_length), hash_builder))
200    }
201}
202
203impl<K, V> LruMap<K, V, Unlimited>
204where
205    K: Hash + PartialEq,
206{
207    /// Returns a new cache with [`Unlimited`] limiter and default hash builder.
208    pub fn new_unlimited() -> Self {
209        Self(schnellru::LruMap::with_hasher(Unlimited, Default::default()))
210    }
211}
212
213#[cfg(test)]
214mod test {
215    use super::*;
216    use derive_more::{Constructor, Display};
217    use std::hash::Hasher;
218
219    #[derive(Debug, Hash, PartialEq, Eq, Display, Clone, Copy)]
220    struct Key(i8);
221
222    #[derive(Debug, Eq, Constructor, Clone, Copy)]
223    struct CompoundKey {
224        // type unique for id
225        id: i8,
226        other: i8,
227    }
228
229    impl PartialEq for CompoundKey {
230        fn eq(&self, other: &Self) -> bool {
231            self.id == other.id
232        }
233    }
234
235    impl Hash for CompoundKey {
236        fn hash<H: Hasher>(&self, state: &mut H) {
237            self.id.hash(state)
238        }
239    }
240
241    #[test]
242    fn test_cache_should_insert_into_empty_set() {
243        let mut cache = LruCache::new(5);
244        let entry = "entry";
245        assert!(cache.insert(entry));
246        assert!(cache.contains(&entry));
247    }
248
249    #[test]
250    fn test_cache_should_not_insert_same_value_twice() {
251        let mut cache = LruCache::new(5);
252        let entry = "entry";
253        assert!(cache.insert(entry));
254        assert!(!cache.insert(entry));
255    }
256
257    #[test]
258    fn test_cache_should_remove_oldest_element_when_exceeding_limit() {
259        let mut cache = LruCache::new(1); // LruCache limit will be 2, check LruCache::new
260        let old_entry = "old_entry";
261        let new_entry = "new_entry";
262        cache.insert(old_entry);
263        cache.insert("entry");
264        cache.insert(new_entry);
265        assert!(cache.contains(&new_entry));
266        assert!(!cache.contains(&old_entry));
267    }
268
269    #[test]
270    fn test_cache_should_extend_an_array() {
271        let mut cache = LruCache::new(5);
272        let entries = ["some_entry", "another_entry"];
273        cache.extend(entries);
274        for e in entries {
275            assert!(cache.contains(&e));
276        }
277    }
278
279    #[test]
280    #[expect(dead_code)]
281    fn test_debug_impl_lru_map() {
282        #[derive(Debug)]
283        struct Value(i8);
284
285        let mut cache = LruMap::new(2);
286        let key_1 = Key(1);
287        let value_1 = Value(11);
288        cache.insert(key_1, value_1);
289        let key_2 = Key(2);
290        let value_2 = Value(22);
291        cache.insert(key_2, value_2);
292
293        assert_eq!(
294            "LruMap { limiter: ByLength { max_length: 2 }, ret %iter: Iter: { 2: Value(22), 1: Value(11) } }",
295            format!("{cache:?}")
296        )
297    }
298
299    #[test]
300    fn test_debug_impl_lru_cache() {
301        let mut cache = LruCache::new(2);
302        let key_1 = Key(1);
303        cache.insert(key_1);
304        let key_2 = Key(2);
305        cache.insert(key_2);
306
307        assert_eq!(
308            "LruCache { limit: 2, ret %iter: Iter: { Key(2), Key(1) } }",
309            format!("{cache:?}")
310        )
311    }
312
313    #[test]
314    fn get() {
315        let mut cache = LruCache::new(2);
316        let key_1 = Key(1);
317        cache.insert(key_1);
318        let key_2 = Key(2);
319        cache.insert(key_2);
320
321        // promotes key 1 to lru
322        _ = cache.get(&key_1);
323
324        assert_eq!(
325            "LruCache { limit: 2, ret %iter: Iter: { Key(1), Key(2) } }",
326            format!("{cache:?}")
327        )
328    }
329
330    #[test]
331    fn get_ty_custom_eq_impl() {
332        let mut cache = LruCache::new(2);
333        let key_1 = CompoundKey::new(1, 11);
334        cache.insert(key_1);
335        let key_2 = CompoundKey::new(2, 22);
336        cache.insert(key_2);
337
338        let key = cache.get(&key_1);
339
340        assert_eq!(key_1.other, key.unwrap().other)
341    }
342
343    #[test]
344    fn peek() {
345        let mut cache = LruCache::new(2);
346        let key_1 = Key(1);
347        cache.insert(key_1);
348        let key_2 = Key(2);
349        cache.insert(key_2);
350
351        // doesn't promote key 1 to lru
352        _ = cache.find(&key_1);
353
354        assert_eq!(
355            "LruCache { limit: 2, ret %iter: Iter: { Key(2), Key(1) } }",
356            format!("{cache:?}")
357        )
358    }
359
360    #[test]
361    fn peek_ty_custom_eq_impl() {
362        let mut cache = LruCache::new(2);
363        let key_1 = CompoundKey::new(1, 11);
364        cache.insert(key_1);
365        let key_2 = CompoundKey::new(2, 22);
366        cache.insert(key_2);
367
368        let key = cache.find(&key_1);
369
370        assert_eq!(key_1.other, key.unwrap().other)
371    }
372
373    #[test]
374    fn test_insert_methods() {
375        let mut cache = LruCache::new(2);
376
377        // Test basic insert
378        assert!(cache.insert("first")); // new entry
379        assert!(!cache.insert("first")); // existing entry
380        assert!(cache.insert("second")); // new entry
381
382        // Test insert_and_get_evicted
383        let (is_new, evicted) = cache.insert_and_get_evicted("third");
384        assert!(is_new); // should be new entry
385        assert_eq!(evicted, Some("first")); // should evict
386
387        assert!(cache.contains(&"second"));
388        assert!(cache.contains(&"third"));
389        assert!(!cache.contains(&"first"));
390
391        // Test insert_and_get_evicted with existing entry
392        let (is_new, evicted) = cache.insert_and_get_evicted("second");
393        assert!(!is_new); // should not be new
394        assert_eq!(evicted, None); // should not evict anything
395    }
396}