Skip to main content

forest/utils/cache/
lru.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use std::{
5    borrow::{Borrow, Cow},
6    fmt::Debug,
7    hash::Hash,
8    num::NonZeroUsize,
9    sync::{
10        Arc,
11        atomic::{AtomicUsize, Ordering},
12    },
13};
14
15use get_size2::GetSize;
16use hashlink::LruCache;
17use parking_lot::RwLock;
18use prometheus_client::{
19    collector::Collector,
20    encoding::{DescriptorEncoder, EncodeMetric},
21    metrics::gauge::Gauge,
22    registry::Unit,
23};
24
25pub trait KeyConstraints:
26    GetSize + Debug + Send + Sync + Hash + PartialEq + Eq + Clone + 'static
27{
28}
29
30impl<T> KeyConstraints for T where
31    T: GetSize + Debug + Send + Sync + Hash + PartialEq + Eq + Clone + 'static
32{
33}
34
35pub trait LruValueConstraints: GetSize + Debug + Send + Sync + Clone + 'static {}
36
37impl<T> LruValueConstraints for T where T: GetSize + Debug + Send + Sync + Clone + 'static {}
38
39#[derive(Debug, Clone)]
40pub struct SizeTrackingLruCache<K, V>
41where
42    K: KeyConstraints,
43    V: LruValueConstraints,
44{
45    cache_id: usize,
46    cache_name: Cow<'static, str>,
47    cache: Arc<RwLock<LruCache<K, V>>>,
48}
49
50impl<K, V> SizeTrackingLruCache<K, V>
51where
52    K: KeyConstraints,
53    V: LruValueConstraints,
54{
55    fn register_metrics(&self) {
56        crate::metrics::register_collector(Box::new(self.clone()));
57    }
58
59    fn new_inner(cache_name: Cow<'static, str>, capacity: Option<NonZeroUsize>) -> Self {
60        static ID_GENERATOR: AtomicUsize = AtomicUsize::new(0);
61
62        Self {
63            cache_id: ID_GENERATOR.fetch_add(1, Ordering::Relaxed),
64            cache_name,
65            #[allow(clippy::disallowed_methods)]
66            cache: Arc::new(RwLock::new(
67                capacity
68                    .map(From::from)
69                    .map(LruCache::new)
70                    // For constructing lru cache that is bounded by memory usage instead of length
71                    .unwrap_or_else(LruCache::new_unbounded),
72            )),
73        }
74    }
75
76    pub fn new_without_metrics_registry(
77        cache_name: Cow<'static, str>,
78        capacity: NonZeroUsize,
79    ) -> Self {
80        Self::new_inner(cache_name, Some(capacity))
81    }
82
83    pub fn new_with_metrics(cache_name: Cow<'static, str>, capacity: NonZeroUsize) -> Self {
84        let c = Self::new_without_metrics_registry(cache_name, capacity);
85        c.register_metrics();
86        c
87    }
88
89    pub fn unbounded_without_metrics_registry(cache_name: Cow<'static, str>) -> Self {
90        Self::new_inner(cache_name, None)
91    }
92
93    pub fn unbounded_with_metrics(cache_name: Cow<'static, str>) -> Self {
94        let c = Self::unbounded_without_metrics_registry(cache_name);
95        c.register_metrics();
96        c
97    }
98
99    pub fn cache(&self) -> &Arc<RwLock<LruCache<K, V>>> {
100        &self.cache
101    }
102
103    pub fn push(&self, k: K, v: V) -> Option<V> {
104        self.cache.write().insert(k, v)
105    }
106
107    pub fn contains<Q>(&self, k: &Q) -> bool
108    where
109        K: Borrow<Q>,
110        Q: Hash + Eq + ?Sized,
111    {
112        self.cache.read().contains_key(k)
113    }
114
115    pub fn get_cloned<Q>(&self, k: &Q) -> Option<V>
116    where
117        K: Borrow<Q>,
118        Q: Hash + Eq + ?Sized,
119    {
120        self.cache.write().get(k).cloned()
121    }
122
123    pub fn peek_cloned<Q>(&self, k: &Q) -> Option<V>
124    where
125        K: Borrow<Q>,
126        Q: Hash + Eq + ?Sized,
127    {
128        self.cache.read().peek(k).cloned()
129    }
130
131    pub fn pop_lru(&self) -> Option<(K, V)> {
132        self.cache.write().remove_lru()
133    }
134
135    pub fn len(&self) -> usize {
136        self.cache.read().len()
137    }
138
139    pub fn cap(&self) -> usize {
140        self.cache.read().capacity()
141    }
142
143    pub(crate) fn size_in_bytes(&self) -> usize {
144        let mut size = 0_usize;
145        for (k, v) in self.cache.read().iter() {
146            size = size
147                .saturating_add(k.get_size())
148                .saturating_add(v.get_size());
149        }
150        size
151    }
152
153    #[cfg(test)]
154    pub(crate) fn new_mocked() -> Self {
155        Self::new_inner(Cow::Borrowed("mocked_cache"), NonZeroUsize::new(1))
156    }
157}
158
159impl<K, V> Collector for SizeTrackingLruCache<K, V>
160where
161    K: KeyConstraints,
162    V: LruValueConstraints,
163{
164    fn encode(&self, mut encoder: DescriptorEncoder) -> Result<(), std::fmt::Error> {
165        {
166            let size_in_bytes = {
167                let g: Gauge = Default::default();
168                g.set(self.size_in_bytes() as _);
169                g
170            };
171            let size_metric_name = format!("cache_{}_{}_size", self.cache_name, self.cache_id);
172            let size_metric_help = format!(
173                "Size of LruCache {}_{} in bytes",
174                self.cache_name, self.cache_id
175            );
176            let size_metric_encoder = encoder.encode_descriptor(
177                &size_metric_name,
178                &size_metric_help,
179                Some(&Unit::Bytes),
180                size_in_bytes.metric_type(),
181            )?;
182            size_in_bytes.encode(size_metric_encoder)?;
183        }
184        {
185            let len_metric_name = format!("{}_{}_len", self.cache_name, self.cache_id);
186            let len_metric_help =
187                format!("Length of LruCache {}_{}", self.cache_name, self.cache_id);
188            let len: Gauge = Default::default();
189            len.set(self.len() as _);
190            let len_metric_encoder = encoder.encode_descriptor(
191                &len_metric_name,
192                &len_metric_help,
193                None,
194                len.metric_type(),
195            )?;
196            len.encode(len_metric_encoder)?;
197        }
198        {
199            let cap_metric_name = format!("{}_{}_cap", self.cache_name, self.cache_id);
200            let cap_metric_help =
201                format!("Capacity of LruCache {}_{}", self.cache_name, self.cache_id);
202            let cap: Gauge = Default::default();
203            cap.set(self.cap() as _);
204            let cap_metric_encoder = encoder.encode_descriptor(
205                &cap_metric_name,
206                &cap_metric_help,
207                None,
208                cap.metric_type(),
209            )?;
210            cap.encode(cap_metric_encoder)?;
211        }
212
213        Ok(())
214    }
215}