Skip to main content

reth_network/
cache.rs

1//! Network cache support
2
3use alloy_primitives::map::DefaultHashBuilder;
4use core::hash::BuildHasher;
5use itertools::Itertools;
6use std::{fmt, hash::Hash};
7
8pub use reth_lru::LruMap;
9
10/// A minimal LRU cache based on a [`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    inner: LruMap<T, (), S>,
19}
20
21impl<T: Hash + Eq + fmt::Debug> LruCache<T> {
22    /// Creates a new [`LruCache`] using the given limit
23    pub fn new(limit: u32) -> Self {
24        Self::with_hasher(limit, Default::default())
25    }
26}
27
28impl<T: Hash + Eq + fmt::Debug, S: BuildHasher> LruCache<T, S> {
29    /// Creates a new [`LruCache`] using the given limit and hash builder.
30    pub const fn with_hasher(limit: u32, hash_builder: S) -> Self {
31        Self { inner: LruMap::with_hasher(limit, hash_builder) }
32    }
33
34    /// Insert an element into the set.
35    ///
36    /// Automatically evicts the least recently used item if capacity is exceeded.
37    ///
38    /// This method is more efficient than [`insert_and_get_evicted`](Self::insert_and_get_evicted)
39    /// as it performs fewer checks. Use this method when you don't need information about
40    /// evicted values.
41    ///
42    /// If the set did not have this value present, true is returned.
43    /// If the set did have this value present, false is returned.
44    pub fn insert(&mut self, entry: T) -> bool {
45        let mut is_new = false;
46        self.inner.get_or_insert(entry, || {
47            is_new = true;
48        });
49        is_new
50    }
51
52    /// Same as [`insert`](Self::insert) but returns a tuple, where the second index is the evicted
53    /// value, if one was evicted.
54    pub fn insert_and_get_evicted(&mut self, entry: T) -> (bool, Option<T>) {
55        if self.inner.get(&entry).is_some() {
56            return (false, None)
57        }
58        (true, self.inner.insert(entry, ()).map(|(evicted, ())| evicted))
59    }
60
61    /// Gets the given element, if exists, and promotes to lru.
62    pub fn get(&mut self, entry: &T) -> Option<&T> {
63        self.inner.get(entry)?;
64        self.inner.peek_newest().map(|(key, ())| key)
65    }
66
67    /// Iterates through entries and returns a reference to the given entry, if exists, without
68    /// promoting to lru.
69    ///
70    /// NOTE: Use this for type that have custom impl of [`PartialEq`] and [`Eq`], that aren't
71    /// unique by all fields. If `PartialEq` and `Eq` are derived for a type, it's more efficient to
72    /// call [`contains`](Self::contains).
73    pub fn find(&self, entry: &T) -> Option<&T> {
74        self.iter().find(|key| *key == entry)
75    }
76
77    /// Expels the given value. Returns true if the value existed.
78    pub fn remove(&mut self, value: &T) -> bool {
79        self.inner.remove(value).is_some()
80    }
81
82    /// Returns `true` if the set contains a value.
83    pub fn contains(&self, value: &T) -> bool {
84        self.inner.contains(value)
85    }
86
87    /// Returns an iterator over all cached entries in lru order
88    pub fn iter(&self) -> impl Iterator<Item = &T> + '_ {
89        self.inner.iter().map(|(k, ())| k)
90    }
91
92    /// Returns number of elements currently in cache.
93    pub const fn len(&self) -> usize {
94        self.inner.len()
95    }
96
97    /// Returns `true` if there are currently no elements in the cache.
98    pub const fn is_empty(&self) -> bool {
99        self.inner.is_empty()
100    }
101}
102
103impl<T, S> Extend<T> for LruCache<T, S>
104where
105    T: Eq + Hash + fmt::Debug,
106    S: BuildHasher,
107{
108    fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
109        for item in iter {
110            _ = self.insert(item);
111        }
112    }
113}
114
115impl<T, S> fmt::Debug for LruCache<T, S>
116where
117    T: fmt::Debug + Hash + Eq,
118    S: BuildHasher,
119{
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        let mut debug_struct = f.debug_struct("LruCache");
122
123        debug_struct.field("limit", &self.inner.max_length());
124
125        debug_struct.field(
126            "ret %iter",
127            &format_args!("Iter: {{{} }}", self.iter().map(|k| format!(" {k:?}")).format(",")),
128        );
129
130        debug_struct.finish()
131    }
132}
133
134#[cfg(test)]
135mod test {
136    use super::*;
137    use derive_more::Constructor;
138    use std::hash::Hasher;
139
140    #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
141    struct Key(i8);
142
143    #[derive(Debug, Eq, Constructor, Clone, Copy)]
144    struct CompoundKey {
145        // type unique for id
146        id: i8,
147        other: i8,
148    }
149
150    impl PartialEq for CompoundKey {
151        fn eq(&self, other: &Self) -> bool {
152            self.id == other.id
153        }
154    }
155
156    impl Hash for CompoundKey {
157        fn hash<H: Hasher>(&self, state: &mut H) {
158            self.id.hash(state)
159        }
160    }
161
162    #[test]
163    fn test_cache_should_insert_into_empty_set() {
164        let mut cache = LruCache::new(5);
165        let entry = "entry";
166        assert!(cache.insert(entry));
167        assert!(cache.contains(&entry));
168    }
169
170    #[test]
171    fn test_cache_should_not_insert_same_value_twice() {
172        let mut cache = LruCache::new(5);
173        let entry = "entry";
174        assert!(cache.insert(entry));
175        assert!(!cache.insert(entry));
176    }
177
178    #[test]
179    fn test_cache_should_remove_oldest_element_when_exceeding_limit() {
180        let mut cache = LruCache::new(1);
181        let old_entry = "old_entry";
182        let new_entry = "new_entry";
183        cache.insert(old_entry);
184        cache.insert("entry");
185        cache.insert(new_entry);
186        assert!(cache.contains(&new_entry));
187        assert!(!cache.contains(&old_entry));
188    }
189
190    #[test]
191    fn test_cache_should_extend_an_array() {
192        let mut cache = LruCache::new(5);
193        let entries = ["some_entry", "another_entry"];
194        cache.extend(entries);
195        for e in entries {
196            assert!(cache.contains(&e));
197        }
198    }
199
200    #[test]
201    #[expect(dead_code)]
202    fn test_debug_impl_lru_map() {
203        #[derive(Debug)]
204        struct Value(i8);
205
206        let mut cache = LruMap::new(2);
207        let key_1 = Key(1);
208        let value_1 = Value(11);
209        cache.insert(key_1, value_1);
210        let key_2 = Key(2);
211        let value_2 = Value(22);
212        cache.insert(key_2, value_2);
213
214        assert_eq!("LruMap { len: 2, max_length: 2, .. }", format!("{cache:?}"))
215    }
216
217    #[test]
218    fn test_debug_impl_lru_cache() {
219        let mut cache = LruCache::new(2);
220        let key_1 = Key(1);
221        cache.insert(key_1);
222        let key_2 = Key(2);
223        cache.insert(key_2);
224
225        assert_eq!(
226            "LruCache { limit: 2, ret %iter: Iter: { Key(2), Key(1) } }",
227            format!("{cache:?}")
228        )
229    }
230
231    #[test]
232    fn get() {
233        let mut cache = LruCache::new(2);
234        let key_1 = Key(1);
235        cache.insert(key_1);
236        let key_2 = Key(2);
237        cache.insert(key_2);
238
239        // promotes key 1 to lru
240        _ = cache.get(&key_1);
241
242        assert_eq!(
243            "LruCache { limit: 2, ret %iter: Iter: { Key(1), Key(2) } }",
244            format!("{cache:?}")
245        )
246    }
247
248    #[test]
249    fn get_ty_custom_eq_impl() {
250        let mut cache = LruCache::new(2);
251        let key_1 = CompoundKey::new(1, 11);
252        cache.insert(key_1);
253        let key_2 = CompoundKey::new(2, 22);
254        cache.insert(key_2);
255
256        let key = cache.get(&key_1);
257
258        assert_eq!(key_1.other, key.unwrap().other)
259    }
260
261    #[test]
262    fn peek() {
263        let mut cache = LruCache::new(2);
264        let key_1 = Key(1);
265        cache.insert(key_1);
266        let key_2 = Key(2);
267        cache.insert(key_2);
268
269        // doesn't promote key 1 to lru
270        _ = cache.find(&key_1);
271
272        assert_eq!(
273            "LruCache { limit: 2, ret %iter: Iter: { Key(2), Key(1) } }",
274            format!("{cache:?}")
275        )
276    }
277
278    #[test]
279    fn peek_ty_custom_eq_impl() {
280        let mut cache = LruCache::new(2);
281        let key_1 = CompoundKey::new(1, 11);
282        cache.insert(key_1);
283        let key_2 = CompoundKey::new(2, 22);
284        cache.insert(key_2);
285
286        let key = cache.find(&key_1);
287
288        assert_eq!(key_1.other, key.unwrap().other)
289    }
290
291    #[test]
292    fn test_insert_methods() {
293        let mut cache = LruCache::new(2);
294
295        // Test basic insert
296        assert!(cache.insert("first")); // new entry
297        assert!(!cache.insert("first")); // existing entry
298        assert!(cache.insert("second")); // new entry
299
300        // Test insert_and_get_evicted
301        let (is_new, evicted) = cache.insert_and_get_evicted("third");
302        assert!(is_new); // should be new entry
303        assert_eq!(evicted, Some("first")); // should evict
304
305        assert!(cache.contains(&"second"));
306        assert!(cache.contains(&"third"));
307        assert!(!cache.contains(&"first"));
308
309        // Test insert_and_get_evicted with existing entry
310        let (is_new, evicted) = cache.insert_and_get_evicted("second");
311        assert!(!is_new); // should not be new
312        assert_eq!(evicted, None); // should not evict anything
313    }
314}