From 599cee14abb31da4e2f27cd5a4616d6433ea5ae6 Mon Sep 17 00:00:00 2001 From: Hubert Bugaj Date: Mon, 18 May 2026 15:33:09 +0200 Subject: [PATCH 1/6] feat!: `quick-cache` instead of manual impl --- Cargo.lock | 13 +++ Cargo.toml | 1 + src/beacon/drand.rs | 16 +-- src/chain_sync/bad_block_cache.rs | 2 +- src/db/blockstore_with_read_cache.rs | 4 +- src/db/car/mod.rs | 117 +++++++++++----------- src/message_pool/msgpool/msg_pool.rs | 4 +- src/message_pool/msgpool/utils.rs | 2 +- src/state_manager/cache.rs | 139 +++++++-------------------- src/utils/cache/lru.rs | 127 +++++++++++++----------- 10 files changed, 193 insertions(+), 232 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c4e2b7ac581f..cb4a1457af56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3329,6 +3329,7 @@ dependencies = [ "puruspe", "quick-protobuf", "quick-protobuf-codec", + "quick_cache", "quickcheck", "quickcheck_macros", "rand 0.8.6", @@ -7427,6 +7428,18 @@ dependencies = [ "unsigned-varint 0.8.0", ] +[[package]] +name = "quick_cache" +version = "0.6.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c821816e9b928e20e92ed59bb3ac4aab321d16ca2316871c9fe7ca739cd477" +dependencies = [ + "ahash", + "equivalent", + "hashbrown 0.16.1", + "parking_lot", +] + [[package]] name = "quickcheck" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 2cf97a9c4290..e54e958d6710 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -242,6 +242,7 @@ console-subscriber = { version = "0.5", features = ["parking_lot"], optional = t sqlx = { version = "0.8", default-features = false, features = ["sqlite", "runtime-tokio", "macros"], optional = true } tikv-jemallocator = { version = "0.6", optional = true } tracing-loki = { version = "0.2", default-features = false, features = ["compat-0-2-1", "rustls"], optional = true } +quick_cache = "0.6" [target.'cfg(unix)'.dependencies] termios = "0.3" diff --git a/src/beacon/drand.rs b/src/beacon/drand.rs index 47879532d31a..474312747c84 100644 --- a/src/beacon/drand.rs +++ b/src/beacon/drand.rs @@ -222,8 +222,10 @@ pub struct DrandBeacon { fil_gen_time: u64, fil_round_time: u64, - /// Keeps track of verified beacon entries. - verified_beacons: SizeTrackingLruCache, + /// Keeps track of verified beacon entries. Stored as `Arc` + /// so that `is_verified`'s peek-and-compare path is a refcount bump + /// instead of cloning the BLS signature `Vec` on every check. + verified_beacons: SizeTrackingLruCache>, } impl DrandBeacon { @@ -249,8 +251,7 @@ impl DrandBeacon { } fn is_verified(&self, entry: &BeaconEntry) -> bool { - let cache = self.verified_beacons.cache().read(); - cache.peek(&entry.round()) == Some(entry) + self.verified_beacons.peek_cloned(&entry.round()).as_deref() == Some(entry) } } @@ -318,7 +319,8 @@ impl Beacon for DrandBeacon { tracing::warn!(%cap, validated_len=%validated.len(), "verified_beacons.cap() is too small"); } for entry in validated { - self.verified_beacons.push(entry.round(), entry.clone()); + self.verified_beacons + .push(entry.round(), Arc::new(entry.clone())); } } @@ -326,9 +328,9 @@ impl Beacon for DrandBeacon { } async fn entry(&self, round: u64) -> anyhow::Result { - let cached: Option = self.verified_beacons.peek_cloned(&round); + let cached = self.verified_beacons.peek_cloned(&round); match cached { - Some(cached_entry) => Ok(cached_entry), + Some(cached_entry) => Ok(Arc::unwrap_or_clone(cached_entry)), None => { async fn fetch_entry_from_url( url: impl reqwest::IntoUrl, diff --git a/src/chain_sync/bad_block_cache.rs b/src/chain_sync/bad_block_cache.rs index 38f1ec63c752..8ebe510cb8a3 100644 --- a/src/chain_sync/bad_block_cache.rs +++ b/src/chain_sync/bad_block_cache.rs @@ -49,7 +49,7 @@ impl BadBlockCache { /// Returns `Some` if the block CID is in bad block cache. /// This function does not update the head position of the `Cid` key. pub fn peek(&self, c: &Cid) -> Option<()> { - self.cache.peek_cloned(&(*c).into()) + self.cache.peek_cloned(&get_size::CidWrapper::from(*c)) } pub fn clear(&self) { diff --git a/src/db/blockstore_with_read_cache.rs b/src/db/blockstore_with_read_cache.rs index df11493ed3f1..6ca6ddcf7c8c 100644 --- a/src/db/blockstore_with_read_cache.rs +++ b/src/db/blockstore_with_read_cache.rs @@ -18,11 +18,11 @@ pub type LruBlockstoreReadCache = SizeTrackingLruCache> { fn get(&self, k: &Cid) -> Option> { - self.get_cloned(&(*k).into()) + self.get_cloned(&get_size::CidWrapper::from(*k)) } fn put(&self, k: Cid, block: Vec) { - self.push(k.into(), block); + self.push(get_size::CidWrapper::from(k), block); } } diff --git a/src/db/car/mod.rs b/src/db/car/mod.rs index 593942de1f1c..18a01b392521 100644 --- a/src/db/car/mod.rs +++ b/src/db/car/mod.rs @@ -13,16 +13,12 @@ pub use plain::PlainCar; use bytes::Bytes; use positioned_io::{ReadAt, Size}; -use std::{ - num::NonZeroUsize, - sync::{ - LazyLock, - atomic::{AtomicUsize, Ordering}, - }, -}; +use std::{num::NonZeroUsize, sync::LazyLock}; use crate::prelude::*; -use crate::utils::{cache::SizeTrackingLruCache, get_size::CidWrapper}; +use crate::utils::get_size::CidWrapper; +use quick_cache::Weighter; +use quick_cache::sync::Cache as QuickCache; pub trait RandomAccessFileReader: ReadAt + Size + Send + Sync + 'static {} impl RandomAccessFileReader for X {} @@ -53,22 +49,38 @@ pub static ZSTD_FRAME_CACHE_DEFAULT_MAX_SIZE: LazyLock = LazyLock::new(|| 256 * 1024 * 1024 }); +/// One decompressed zstd frame's index, shared via [`Arc`] so cache lookups +/// don't deep-copy the inner [`hashbrown::HashMap`]. Snapshot export hits the +/// cache once per CID; a per-call HashMap clone destroys throughput. +type FrameIndex = Arc>; + +/// Weighter that bills each entry by `key.get_size() + value.get_size()`. +/// Used to make [`ZstdFrameCache`] evict by byte size. +#[derive(Clone, Copy, Debug, Default)] +struct ZstdFrameWeighter; + +impl Weighter<(FrameOffset, CacheKey), FrameIndex> for ZstdFrameWeighter { + fn weight(&self, key: &(FrameOffset, CacheKey), value: &FrameIndex) -> u64 { + // quick_cache treats weight 0 as "do not evict" — clamp to 1 so the + // cache never silently pins entries. + (key.get_size().saturating_add(value.get_size()) as u64).max(1) + } +} + +type ZstdFrameInner = QuickCache<(FrameOffset, CacheKey), FrameIndex, ZstdFrameWeighter>; + pub struct ZstdFrameCache { - /// Maximum size in bytes. Pages will be evicted if the total size of the - /// cache exceeds this amount. + /// Maximum size in bytes. Pages are evicted by the cache when the total + /// weight exceeds this amount. pub max_size: usize, - current_size: Arc, - // use `hashbrown::HashMap` here because its `GetSize` implementation is accurate - // (thanks to `hashbrown::HashMap::allocation_size`). - lru: SizeTrackingLruCache<(FrameOffset, CacheKey), hashbrown::HashMap>, + cache: Arc, } impl ShallowClone for ZstdFrameCache { fn shallow_clone(&self) -> Self { Self { max_size: self.max_size, - current_size: self.current_size.shallow_clone(), - lru: self.lru.shallow_clone(), + cache: self.cache.shallow_clone(), } } } @@ -81,24 +93,31 @@ impl Default for ZstdFrameCache { impl ZstdFrameCache { pub fn new(max_size: usize) -> Self { + // Items in this cache are decompressed zstd frame indexes — large + // hashmaps, so we don't expect many of them. The 64 estimate is a + // hint to quick_cache for initial table sizing only; the real bound + // is the weight capacity. + const ESTIMATED_ITEMS: usize = 64; ZstdFrameCache { max_size, - current_size: Arc::new(AtomicUsize::new(0)), - lru: SizeTrackingLruCache::unbounded_with_metrics("zstd_frame"), + cache: Arc::new(QuickCache::with_weighter( + ESTIMATED_ITEMS, + max_size as u64, + ZstdFrameWeighter, + )), } } /// Return a clone of the value associated with `cid`. If a value is found, - /// the cache entry is moved to the top of the queue. + /// the cache entry is touched (moved to the top of the eviction order). pub fn get(&self, offset: FrameOffset, key: CacheKey, cid: Cid) -> Option> { - self.lru - .cache() - .write() + self.cache .get(&(offset, key)) .map(|index| index.get(&CidWrapper::from(cid)).cloned()) } - /// Insert entry into lru-cache and evict pages if `max_size` has been exceeded. + /// Insert entry into the cache. Eviction happens automatically based on + /// weight (see [`ZstdFrameWeighter`]). pub fn put( &self, offset: FrameOffset, @@ -110,30 +129,12 @@ impl ZstdFrameCache { let lru_key = (offset, key); let lru_key_size = lru_key.get_size(); let entry_size = index.get_size(); - // Skip large items + // Skip individual items larger than the whole cache — they'd evict + // everything and still not fit. if entry_size.saturating_add(lru_key_size) >= self.max_size { return; } - - if let Some(prev_entry) = self.lru.push(lru_key, index) { - // keys are cancelled out - self.current_size.fetch_add(entry_size, Ordering::Relaxed); - self.current_size - .fetch_sub(prev_entry.get_size(), Ordering::Relaxed); - } else { - self.current_size - .fetch_add(entry_size.saturating_add(lru_key_size), Ordering::Relaxed); - } - while self.current_size.load(Ordering::Relaxed) > self.max_size { - if let Some((prev_key, prev_entry)) = self.lru.pop_lru() { - self.current_size.fetch_sub( - prev_key.get_size().saturating_add(prev_entry.get_size()), - Ordering::Relaxed, - ); - } else { - break; - } - } + self.cache.insert(lru_key, Arc::new(index)); } } @@ -146,23 +147,25 @@ mod tests { use rand::Rng; #[test] - fn test_zstd_frame_cache_size() { + fn test_zstd_frame_cache_stays_under_max_size() { let mut rng = forest_rng(); - let cache = ZstdFrameCache::new(10); + // Pick a non-trivial cap so a few entries fit before eviction kicks in. + let max_size: usize = 64 * 1024; + let cache = ZstdFrameCache::new(max_size); for i in 0..100 { - let index = gen_index(&mut rng); - cache.put(i, i, index); - assert_eq!( - cache.current_size.load(Ordering::Relaxed), - cache.lru.size_in_bytes() - ); - let index2 = gen_index(&mut rng); - cache.put(i, i, index2); - assert_eq!( - cache.current_size.load(Ordering::Relaxed), - cache.lru.size_in_bytes() + cache.put(i, i, gen_index(&mut rng)); + // After every insert the live weight must remain under the cap; + // quick_cache evicts synchronously to keep it that way. + assert!( + cache.cache.weight() <= max_size as u64, + "weight {} exceeds cap {}", + cache.cache.weight(), + max_size ); } + // Sanity: after stuffing 100 entries into a cap-bounded cache, at + // least one eviction must have happened. + assert!(cache.cache.len() < 100); } fn gen_index(rng: &mut impl Rng) -> hashbrown::HashMap { diff --git a/src/message_pool/msgpool/msg_pool.rs b/src/message_pool/msgpool/msg_pool.rs index 7a656d14726b..0ce0ebae70b6 100644 --- a/src/message_pool/msgpool/msg_pool.rs +++ b/src/message_pool/msgpool/msg_pool.rs @@ -313,14 +313,14 @@ where fn verify_msg_sig(&self, msg: &SignedMessage) -> Result<(), Error> { let cid = msg.cid(); - if let Some(()) = self.caches.sig_val.get_cloned(&(cid).into()) { + if let Some(()) = self.caches.sig_val.get_cloned(&CidWrapper::from(cid)) { return Ok(()); } msg.verify(self.chain_config.eth_chain_id) .map_err(|e| Error::Other(e.to_string()))?; - self.caches.sig_val.push(cid.into(), ()); + self.caches.sig_val.push(CidWrapper::from(cid), ()); Ok(()) } diff --git a/src/message_pool/msgpool/utils.rs b/src/message_pool/msgpool/utils.rs index b4dbb4243f95..e4f11ae2e080 100644 --- a/src/message_pool/msgpool/utils.rs +++ b/src/message_pool/msgpool/utils.rs @@ -50,7 +50,7 @@ pub(in crate::message_pool) fn recover_sig( msg: Message, ) -> Result { let val = bls_sig_cache - .get_cloned(&(msg.cid()).into()) + .get_cloned(&CidWrapper::from(msg.cid())) .ok_or_else(|| Error::Other("Could not recover sig".to_owned()))?; let smsg = SignedMessage::new_from_parts(msg, val)?; Ok(smsg) diff --git a/src/state_manager/cache.rs b/src/state_manager/cache.rs index 6bf9c6f12521..74fed190fe0a 100644 --- a/src/state_manager/cache.rs +++ b/src/state_manager/cache.rs @@ -4,33 +4,31 @@ use crate::prelude::*; use crate::state_manager::DEFAULT_TIPSET_CACHE_SIZE; use crate::utils::cache::{LruKeyConstraints, LruValueConstraints, SizeTrackingLruCache}; -use ahash::{HashMap, HashMapExt as _}; -use parking_lot::RwLock as SyncRwLock; +use prometheus_client::metrics::counter::Counter; use std::borrow::Cow; use std::future::Future; use std::num::NonZeroUsize; -use tokio::sync::Mutex as TokioMutex; - -struct ForestLruCacheInner { - values: SizeTrackingLruCache, - pending: HashMap>>, -} - -impl ForestLruCacheInner { - pub fn with_size( - cache_identifier: impl Into>, - cache_size: NonZeroUsize, - ) -> Self { - Self { - values: SizeTrackingLruCache::new_with_metrics(cache_identifier, cache_size), - pending: HashMap::with_capacity(8), - } - } -} - -/// A generic cache that handles concurrent access and computation for tipset-related data. +use std::sync::LazyLock; + +// Pre-resolved counter handles for the tipset cache labels. `Family::get_or_create` +// locks the inner label map on every call, so we hoist the lookup to module init. +// `Counter` is cheaply cloneable; clones share the atomic underneath. +static TIPSET_HIT: LazyLock = LazyLock::new(|| { + crate::metrics::LRU_CACHE_HIT + .get_or_create(&crate::metrics::values::STATE_MANAGER_TIPSET) + .clone() +}); +static TIPSET_MISS: LazyLock = LazyLock::new(|| { + crate::metrics::LRU_CACHE_MISS + .get_or_create(&crate::metrics::values::STATE_MANAGER_TIPSET) + .clone() +}); + +/// A cache that handles concurrent access and computation for tipset-related +/// data. Coalesces concurrent computations of the same key, so only one caller +/// actually runs the `compute` future and the rest wait on its result. pub(crate) struct ForestLruCache { - cache: Arc>>, + cache: Arc>, } impl ShallowClone for ForestLruCache { @@ -41,11 +39,6 @@ impl ShallowClone for ForestLruCac } } -enum CacheLookupStatus { - Exist(V), - Empty(Arc>), -} - impl ForestLruCache { pub fn new(cache_identifier: impl Into>) -> Self { Self::with_size(cache_identifier, DEFAULT_TIPSET_CACHE_SIZE) @@ -56,22 +49,10 @@ impl ForestLruCache { cache_size: NonZeroUsize, ) -> Self { Self { - cache: Arc::new(SyncRwLock::new(ForestLruCacheInner::with_size( + cache: Arc::new(SizeTrackingLruCache::new_with_metrics( cache_identifier, cache_size, - ))), - } - } - - fn get_or_insert(&self, get_func: F1, or_insert_func: F2) -> T - where - F1: FnOnce(&ForestLruCacheInner) -> Option, - F2: FnOnce(&mut ForestLruCacheInner) -> T, - { - if let Some(v) = get_func(&self.cache.read()) { - v - } else { - or_insert_func(&mut self.cache.write()) + )), } } @@ -81,68 +62,25 @@ impl ForestLruCache { Fut: Future> + Send, V: Send + Sync + 'static, { - let status = self.get_or_insert( - |inner| inner.values.get_cloned(key).map(CacheLookupStatus::Exist), - |inner| { - let mutex = inner - .pending - .entry(key.clone()) - .or_insert_with(|| Arc::new(TokioMutex::new(()))) - .shallow_clone(); - CacheLookupStatus::Empty(mutex) - }, - ); - match status { - CacheLookupStatus::Exist(x) => { - crate::metrics::LRU_CACHE_HIT - .get_or_create(&crate::metrics::values::STATE_MANAGER_TIPSET) - .inc(); - Ok(x) - } - CacheLookupStatus::Empty(mtx) => { - let _guard = mtx.lock().await; - match self.get(key) { - Some(v) => { - // While locking someone else computed the pending task - crate::metrics::LRU_CACHE_HIT - .get_or_create(&crate::metrics::values::STATE_MANAGER_TIPSET) - .inc(); - - Ok(v) - } - None => { - // Entry does not have state computed yet, compute value and fill the cache - crate::metrics::LRU_CACHE_MISS - .get_or_create(&crate::metrics::values::STATE_MANAGER_TIPSET) - .inc(); - let value = compute().await?; - // Write back to cache, release lock and return value - self.insert(key.clone(), value.clone()); - Ok(value) - } - } - } + let (value, hit) = self.cache.get_or_compute(key, compute).await?; + if hit { + TIPSET_HIT.inc(); + } else { + TIPSET_MISS.inc(); } + Ok(value) } - pub fn get_map(&self, key: &K, mapper: impl Fn(&V) -> T) -> Option { - self.cache.read().values.get_map(key, mapper) + pub fn get_map(&self, key: &K, mapper: impl FnOnce(&V) -> T) -> Option { + self.cache.get_map(key, mapper) } pub fn get(&self, key: &K) -> Option { - self.get_map(key, Clone::clone) - } - - pub fn insert(&self, key: K, value: V) { - let mut cache = self.cache.write(); - cache.pending.retain(|k, _| k != &key); - cache.values.push(key, value); + self.cache.get_cloned(key) } pub fn remove(&self, key: &K) { - let mut cache = self.cache.write(); - cache.pending.retain(|k, _| k != key); - cache.values.remove(key); + self.cache.remove(key); } } @@ -171,14 +109,12 @@ mod tests { let cache: ForestLruCache = ForestLruCache::new("test"); let key = create_test_tipset_key(1); - // Test cache miss and computation let result = cache .get_or_else(&key, || async { Ok("computed_value".to_string()) }) .await .unwrap(); assert_eq!(result, "computed_value"); - // Test cache hit let result = cache .get_or_else(&key, || async { Ok("should_not_compute".to_string()) }) .await @@ -192,7 +128,6 @@ mod tests { let key = create_test_tipset_key(1); let computation_count = Arc::new(AtomicU8::new(0)); - // Start multiple tasks that try to compute the same key concurrently let mut handles = vec![]; for i in 0..10 { let cache_clone = Arc::clone(&cache); @@ -204,9 +139,7 @@ mod tests { .get_or_else(&key_clone, || { let count = Arc::clone(&count_clone); async move { - // Increment computation count count.fetch_add(1, Ordering::SeqCst); - // Simulate some computation time tokio::time::sleep(Duration::from_millis(10)).await; Ok(format!("computed_value_{i}")) } @@ -222,11 +155,8 @@ mod tests { .collect::, _>>() .unwrap(); - // Computation should have been performed once assert_eq!(computation_count.load(Ordering::SeqCst), 1); - // Only one result should be returned as computation was performed once, - // and all tasks will get the same result from the cache let first_result = results[0].as_ref().unwrap(); for result in &results { assert_eq!(result.as_ref().unwrap(), first_result); @@ -238,7 +168,6 @@ mod tests { let cache: Arc> = Arc::new(ForestLruCache::new("test")); let computation_count = Arc::new(AtomicU8::new(0)); - // Start tasks that try to compute the different keys let mut handles = vec![]; for i in 0..10 { let cache_clone = Arc::clone(&cache); @@ -266,10 +195,8 @@ mod tests { .collect::, _>>() .unwrap(); - // Computation should have been performed for each key assert_eq!(computation_count.load(Ordering::SeqCst), 10); - // All results should be returned as computation was performed once for each key for (i, result) in results.iter().enumerate() { assert_eq!(result.as_ref().unwrap(), &format!("value_{i}")); } diff --git a/src/utils/cache/lru.rs b/src/utils/cache/lru.rs index c5aa8c233813..a875e163ac88 100644 --- a/src/utils/cache/lru.rs +++ b/src/utils/cache/lru.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0, MIT use std::{ - borrow::{Borrow, Cow}, + borrow::Cow, fmt::Debug, hash::Hash, num::NonZeroUsize, @@ -13,14 +13,14 @@ use std::{ }; use get_size2::GetSize; -use hashlink::LruCache; -use parking_lot::RwLock; +use quick_cache::Equivalent; use prometheus_client::{ collector::Collector, encoding::{DescriptorEncoder, EncodeMetric}, metrics::gauge::Gauge, registry::Unit, }; +use quick_cache::sync::Cache; use crate::prelude::*; @@ -38,6 +38,12 @@ pub trait LruValueConstraints: GetSize + Debug + Send + Sync + Clone + 'static { impl LruValueConstraints for T where T: GetSize + Debug + Send + Sync + Clone + 'static {} +/// A concurrent cache with Prometheus instrumentation. +/// +/// Backed by [`quick_cache::sync::Cache`] (CLOCK-PRO). The name keeps "Lru" for +/// API compatibility with historical call sites; the actual eviction policy is +/// CLOCK-PRO, which is scan-resistant and typically gives higher hit rates +/// than strict LRU on chain workloads. #[derive(Debug)] pub struct SizeTrackingLruCache where @@ -46,7 +52,8 @@ where { cache_id: usize, cache_name: Cow<'static, str>, - cache: Arc>>, + cache: Arc>, + capacity: usize, } impl ShallowClone for SizeTrackingLruCache @@ -59,6 +66,7 @@ where cache_id: self.cache_id, cache_name: self.cache_name.clone(), cache: self.cache.shallow_clone(), + capacity: self.capacity, } } } @@ -72,20 +80,14 @@ where crate::metrics::register_collector(Box::new(self.shallow_clone())); } - fn new_inner(cache_name: impl Into>, capacity: Option) -> Self { + fn new_inner(cache_name: impl Into>, capacity: NonZeroUsize) -> Self { static ID_GENERATOR: AtomicUsize = AtomicUsize::new(0); - + let capacity = capacity.get(); Self { cache_id: ID_GENERATOR.fetch_add(1, Ordering::Relaxed), cache_name: cache_name.into(), - #[allow(clippy::disallowed_methods)] - cache: Arc::new(RwLock::new( - capacity - .map(From::from) - .map(LruCache::new) - // For constructing lru cache that is bounded by memory usage instead of length - .unwrap_or_else(LruCache::new_unbounded), - )), + cache: Arc::new(Cache::new(capacity)), + capacity, } } @@ -93,7 +95,7 @@ where cache_name: impl Into>, capacity: NonZeroUsize, ) -> Self { - Self::new_inner(cache_name, Some(capacity)) + Self::new_inner(cache_name, capacity) } pub fn new_with_metrics( @@ -105,75 +107,89 @@ where c } - pub fn unbounded_without_metrics_registry(cache_name: impl Into>) -> Self { - Self::new_inner(cache_name, None) - } - - pub fn unbounded_with_metrics(cache_name: impl Into>) -> Self { - let c = Self::unbounded_without_metrics_registry(cache_name); - c.register_metrics(); - c - } - - pub fn cache(&self) -> &Arc>> { - &self.cache - } - pub fn remove(&self, k: &Q) -> Option where - K: Borrow, - Q: Hash + Eq + ?Sized, + Q: Hash + Equivalent + ?Sized, { - self.cache.write().remove(k) + self.cache.remove(k).map(|(_, v)| v) } + /// Insert `k`/`v`. If a previous entry existed for `k`, return it. + /// + /// `quick_cache::sync::Cache::insert` does not return the displaced + /// value, so this is a peek-then-insert. The two steps are not atomic; + /// concurrent inserters for the same key may both observe `None`. None of + /// the existing callers depend on atomicity here. pub fn push(&self, k: K, v: V) -> Option { - self.cache.write().insert(k, v) + let prev = self.cache.peek(&k); + self.cache.insert(k, v); + prev } - pub fn get_map(&self, k: &Q, mapper: impl Fn(&V) -> T) -> Option + pub fn get_map(&self, k: &Q, mapper: impl FnOnce(&V) -> T) -> Option where - K: Borrow, - Q: Hash + Eq + ?Sized, + Q: Hash + Equivalent + ?Sized, { - self.cache.write().get(k).map(mapper) + self.cache.get(k).map(|v| mapper(&v)) } pub fn get_cloned(&self, k: &Q) -> Option where - K: Borrow, - Q: Hash + Eq + ?Sized, + Q: Hash + Equivalent + ?Sized, { - self.get_map(k, Clone::clone) + self.cache.get(k) } + /// Read without affecting the eviction order. pub fn peek_cloned(&self, k: &Q) -> Option where - K: Borrow, - Q: Hash + Eq + ?Sized, + Q: Hash + Equivalent + ?Sized, { - self.cache.read().peek(k).cloned() - } - - pub fn pop_lru(&self) -> Option<(K, V)> { - self.cache.write().remove_lru() + self.cache.peek(k) } pub fn len(&self) -> usize { - self.cache.read().len() + self.cache.len() } pub fn cap(&self) -> usize { - self.cache.read().capacity() + self.capacity } pub fn clear(&self) { - self.cache.write().clear() + self.cache.clear() + } + + /// Get the value for `key`, computing it with `compute` on a miss. + /// + /// Concurrent callers for the same key are coalesced — only one runs + /// `compute`, the rest wait on the result. + /// + /// Returns `(value, was_hit)`; the caller can use the flag to drive + /// hit/miss metrics. If `compute` fails the placeholder is released and + /// the next caller will recompute. + pub async fn get_or_compute( + &self, + key: &K, + compute: F, + ) -> Result<(V, bool), E> + where + F: FnOnce() -> Fut, + Fut: std::future::Future>, + { + match self.cache.get_value_or_guard_async(key).await { + Ok(v) => Ok((v, true)), + Err(guard) => { + let v = compute().await?; + let _ = guard.insert(v.clone()); + Ok((v, false)) + } + } } pub(crate) fn size_in_bytes(&self) -> usize { let mut size = 0_usize; - for (k, v) in self.cache.read().iter() { + for (k, v) in self.cache.iter() { size = size .saturating_add(k.get_size()) .saturating_add(v.get_size()); @@ -183,7 +199,7 @@ where #[cfg(test)] pub(crate) fn new_mocked() -> Self { - Self::new_inner(Cow::Borrowed("mocked_cache"), NonZeroUsize::new(1)) + Self::new_inner(Cow::Borrowed("mocked_cache"), NonZeroUsize::new(1).unwrap()) } } @@ -201,7 +217,7 @@ where }; let size_metric_name = format!("cache_{}_{}_size", self.cache_name, self.cache_id); let size_metric_help = format!( - "Size of LruCache {}_{} in bytes", + "Size of cache {}_{} in bytes", self.cache_name, self.cache_id ); let size_metric_encoder = encoder.encode_descriptor( @@ -214,8 +230,7 @@ where } { let len_metric_name = format!("{}_{}_len", self.cache_name, self.cache_id); - let len_metric_help = - format!("Length of LruCache {}_{}", self.cache_name, self.cache_id); + let len_metric_help = format!("Length of cache {}_{}", self.cache_name, self.cache_id); let len: Gauge = Default::default(); len.set(self.len() as _); let len_metric_encoder = encoder.encode_descriptor( @@ -229,7 +244,7 @@ where { let cap_metric_name = format!("{}_{}_cap", self.cache_name, self.cache_id); let cap_metric_help = - format!("Capacity of LruCache {}_{}", self.cache_name, self.cache_id); + format!("Capacity of cache {}_{}", self.cache_name, self.cache_id); let cap: Gauge = Default::default(); cap.set(self.cap() as _); let cap_metric_encoder = encoder.encode_descriptor( From 09d9486934014edfa4ee6a4ef663829605fe3069 Mon Sep 17 00:00:00 2001 From: Hubert Bugaj Date: Mon, 18 May 2026 15:51:34 +0200 Subject: [PATCH 2/6] chore: use generic `cache` naming instead of `lru*` --- CHANGELOG.md | 2 + Cargo.toml | 2 +- src/beacon/drand.rs | 13 ++-- src/chain/store/chain_store.rs | 6 +- src/chain/store/index.rs | 10 ++-- src/chain_sync/bad_block_cache.rs | 12 ++-- src/db/blockstore_with_read_cache.rs | 8 +-- src/db/car/forest.rs | 2 +- src/db/car/mod.rs | 8 +-- src/message_pool/msgpool/mod.rs | 8 +-- src/message_pool/msgpool/msg_pool.rs | 63 ++++++++++---------- src/message_pool/msgpool/selection.rs | 4 +- src/message_pool/msgpool/utils.rs | 4 +- src/metrics/mod.rs | 8 +-- src/rpc/methods/eth.rs | 10 ++-- src/rpc/methods/f3.rs | 8 +-- src/state_manager/cache.rs | 22 +++---- src/state_manager/mod.rs | 12 ++-- src/state_migration/common/mod.rs | 6 +- src/utils/cache/mod.rs | 4 +- src/utils/cache/{lru.rs => size_tracking.rs} | 47 +++++++-------- 21 files changed, 124 insertions(+), 135 deletions(-) rename src/utils/cache/{lru.rs => size_tracking.rs} (86%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ee43ab91e17..ad859aac4760 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ ### Breaking +- [#7073](https://github.com/ChainSafe/forest/pull/7073): Replaced the underlying cache engine across the node. The eviction policy is no longer strict LRU — it is now CLOCK-PRO via [`quick_cache`](https://crates.io/crates/quick_cache), which is scan-resistant and typically gives higher hit rates on chain workloads. The Prometheus metric names `lru_cache_hit_total` and `lru_cache_miss_total` are renamed to `cache_hit_total` and `cache_miss_total`. **Operators must update dashboards, alert rules, and recording rules** that reference the old names. Label set (`kind="..."`) is unchanged. + ### Added - [#6012](https://github.com/ChainSafe/forest/issues/6012): Stricter validation of address arguments in `forest-wallet` subcommands. diff --git a/Cargo.toml b/Cargo.toml index e54e958d6710..38906d061262 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -239,10 +239,10 @@ zstd = "0.13" # optional dependencies console-subscriber = { version = "0.5", features = ["parking_lot"], optional = true } +quick_cache = "0.6" sqlx = { version = "0.8", default-features = false, features = ["sqlite", "runtime-tokio", "macros"], optional = true } tikv-jemallocator = { version = "0.6", optional = true } tracing-loki = { version = "0.2", default-features = false, features = ["compat-0-2-1", "rustls"], optional = true } -quick_cache = "0.6" [target.'cfg(unix)'.dependencies] termios = "0.3" diff --git a/src/beacon/drand.rs b/src/beacon/drand.rs index 474312747c84..a6ace8c850d6 100644 --- a/src/beacon/drand.rs +++ b/src/beacon/drand.rs @@ -16,7 +16,7 @@ use super::{ use crate::prelude::*; use crate::shim::clock::ChainEpoch; use crate::shim::version::NetworkVersion; -use crate::utils::cache::SizeTrackingLruCache; +use crate::utils::cache::SizeTrackingCache; use crate::utils::misc::env::is_env_truthy; use crate::utils::net::global_http_client; use ambassador::{Delegate, delegatable_trait}; @@ -222,10 +222,8 @@ pub struct DrandBeacon { fil_gen_time: u64, fil_round_time: u64, - /// Keeps track of verified beacon entries. Stored as `Arc` - /// so that `is_verified`'s peek-and-compare path is a refcount bump - /// instead of cloning the BLS signature `Vec` on every check. - verified_beacons: SizeTrackingLruCache>, + /// Keeps track of verified beacon entries. + verified_beacons: SizeTrackingCache>, } impl DrandBeacon { @@ -243,10 +241,7 @@ impl DrandBeacon { drand_gen_time: config.chain_info.genesis_time as u64, fil_round_time: interval, fil_gen_time: genesis_ts, - verified_beacons: SizeTrackingLruCache::new_with_metrics( - "verified_beacons", - CACHE_SIZE, - ), + verified_beacons: SizeTrackingCache::new_with_metrics("verified_beacons", CACHE_SIZE), } } diff --git a/src/chain/store/chain_store.rs b/src/chain/store/chain_store.rs index f9f3294549c6..6396c2fd3293 100644 --- a/src/chain/store/chain_store.rs +++ b/src/chain/store/chain_store.rs @@ -19,7 +19,7 @@ use crate::{ message::{ChainMessage, SignedMessage}, }; use crate::{db::EthMappingsStoreExt, rpc::chain::PathChange}; -use crate::{fil_cns, utils::cache::SizeTrackingLruCache}; +use crate::{fil_cns, utils::cache::SizeTrackingCache}; use crate::{ interpreter::{BlockMessages, VMTrace}, rpc::chain::PathChanges, @@ -590,13 +590,13 @@ where /// use-cases. This cache is intended to be used with a complementary function; /// [`messages_for_tipset_with_cache`]. pub struct MessagesInTipsetCache { - cache: SizeTrackingLruCache>>, + cache: SizeTrackingCache>>, } impl MessagesInTipsetCache { pub fn new(capacity: NonZeroUsize) -> Self { Self { - cache: SizeTrackingLruCache::new_with_metrics("msg_in_tipset", capacity), + cache: SizeTrackingCache::new_with_metrics("msg_in_tipset", capacity), } } diff --git a/src/chain/store/index.rs b/src/chain/store/index.rs index bfd5bfe6646a..be7c290d61b2 100644 --- a/src/chain/store/index.rs +++ b/src/chain/store/index.rs @@ -10,13 +10,13 @@ use crate::db::{DbImpl, EthMappingsStore as _}; use crate::metrics; use crate::prelude::*; use crate::shim::clock::ChainEpoch; -use crate::utils::cache::SizeTrackingLruCache; +use crate::utils::cache::SizeTrackingCache; use nonzero_ext::nonzero; use num::Integer; const DEFAULT_TIPSET_CACHE_SIZE: NonZeroUsize = nonzero!(2880_usize); -type TipsetCache = SizeTrackingLruCache; +type TipsetCache = SizeTrackingCache; type IsTipsetFinalizedFn = Arc bool + Send + Sync>; @@ -53,7 +53,7 @@ pub enum ResolveNullTipset { impl ChainIndex { pub fn new(db: impl Into) -> Self { let db = db.into(); - let ts_cache = SizeTrackingLruCache::new_with_metrics("tipset", DEFAULT_TIPSET_CACHE_SIZE); + let ts_cache = SizeTrackingCache::new_with_metrics("tipset", DEFAULT_TIPSET_CACHE_SIZE); Self { ts_cache, db, @@ -81,7 +81,7 @@ impl ChainIndex { if !cache_disabled() && let Some(ts) = self.ts_cache.get_cloned(tsk) { - metrics::LRU_CACHE_HIT + metrics::CACHE_HIT .get_or_create(&metrics::values::TIPSET) .inc(); return Ok(Some(ts)); @@ -92,7 +92,7 @@ impl ChainIndex { && let Some(ts) = &ts_opt { self.ts_cache.push(tsk.clone(), ts.clone()); - metrics::LRU_CACHE_MISS + metrics::CACHE_MISS .get_or_create(&metrics::values::TIPSET) .inc(); } diff --git a/src/chain_sync/bad_block_cache.rs b/src/chain_sync/bad_block_cache.rs index 8ebe510cb8a3..23cedca33ec8 100644 --- a/src/chain_sync/bad_block_cache.rs +++ b/src/chain_sync/bad_block_cache.rs @@ -6,7 +6,7 @@ use std::num::NonZeroUsize; use nonzero_ext::nonzero; use crate::prelude::*; -use crate::utils::{cache::SizeTrackingLruCache, get_size}; +use crate::utils::{cache::SizeTrackingCache, get_size}; /// Default capacity for CID caches (32768 entries). /// That's about 4 MiB. @@ -17,7 +17,7 @@ const DEFAULT_CID_CACHE_CAPACITY: NonZeroUsize = nonzero!(1usize << 15); /// work. #[derive(Debug)] pub struct BadBlockCache { - cache: SizeTrackingLruCache, + cache: SizeTrackingCache, } impl Default for BadBlockCache { @@ -37,7 +37,7 @@ impl ShallowClone for BadBlockCache { impl BadBlockCache { pub fn new(cap: NonZeroUsize) -> Self { Self { - cache: SizeTrackingLruCache::new_with_metrics("bad_block", cap), + cache: SizeTrackingCache::new_with_metrics("bad_block", cap), } } @@ -57,11 +57,11 @@ impl BadBlockCache { } } -/// Thread-safe LRU cache for tracking recently seen gossip block CIDs. +/// Thread-safe cache for tracking recently seen gossip block CIDs. /// Used to de-duplicate gossip blocks before expensive message fetching. #[derive(Debug)] pub struct SeenBlockCache { - cache: SizeTrackingLruCache, + cache: SizeTrackingCache, } impl ShallowClone for SeenBlockCache { @@ -81,7 +81,7 @@ impl Default for SeenBlockCache { impl SeenBlockCache { pub fn new(cap: NonZeroUsize) -> Self { Self { - cache: SizeTrackingLruCache::new_with_metrics("seen_gossip_block", cap), + cache: SizeTrackingCache::new_with_metrics("seen_gossip_block", cap), } } diff --git a/src/db/blockstore_with_read_cache.rs b/src/db/blockstore_with_read_cache.rs index 6ca6ddcf7c8c..80be98704509 100644 --- a/src/db/blockstore_with_read_cache.rs +++ b/src/db/blockstore_with_read_cache.rs @@ -5,7 +5,7 @@ use cid::Cid; use fvm_ipld_blockstore::Blockstore; use std::sync::atomic::{self, AtomicUsize}; -use crate::utils::{cache::SizeTrackingLruCache, get_size}; +use crate::utils::{cache::SizeTrackingCache, get_size}; #[auto_impl::auto_impl(&, Arc)] pub trait BlockstoreReadCache { @@ -14,9 +14,9 @@ pub trait BlockstoreReadCache { fn put(&self, k: Cid, block: Vec); } -pub type LruBlockstoreReadCache = SizeTrackingLruCache>; +pub type DefaultBlockstoreReadCache = SizeTrackingCache>; -impl BlockstoreReadCache for SizeTrackingLruCache> { +impl BlockstoreReadCache for SizeTrackingCache> { fn get(&self, k: &Cid) -> Option> { self.get_cloned(&get_size::CidWrapper::from(*k)) } @@ -126,7 +126,7 @@ mod tests { mem_db.put_keyed(&key, &record).unwrap(); records.push((key, record)); } - let cache = Arc::new(LruBlockstoreReadCache::new_without_metrics_registry( + let cache = Arc::new(DefaultBlockstoreReadCache::new_without_metrics_registry( "test_blockstore_read_cache", CACHE_SIZE.try_into().unwrap(), )); diff --git a/src/db/car/forest.rs b/src/db/car/forest.rs index e3e92144cad4..209d67b6435d 100644 --- a/src/db/car/forest.rs +++ b/src/db/car/forest.rs @@ -33,7 +33,7 @@ //! Looking up a block uses an [`index::Reader`] to find //! the right z-frame. The frame is then decoded and each block is linearly //! scanned until a match is found. Decoded (and scanned) z-frames are stored in -//! a lru-cache for faster repeat retrievals. +//! a cache for faster repeat retrievals. //! //! `forest.car.zst` files are backward compatible with Lotus (and all other //! tools that consume compressed CAR files). All Forest-specifc information is diff --git a/src/db/car/mod.rs b/src/db/car/mod.rs index 18a01b392521..3a6e7d239446 100644 --- a/src/db/car/mod.rs +++ b/src/db/car/mod.rs @@ -126,15 +126,15 @@ impl ZstdFrameCache { ) { index.shrink_to_fit(); - let lru_key = (offset, key); - let lru_key_size = lru_key.get_size(); + let cache_key = (offset, key); + let cache_key_size = cache_key.get_size(); let entry_size = index.get_size(); // Skip individual items larger than the whole cache — they'd evict // everything and still not fit. - if entry_size.saturating_add(lru_key_size) >= self.max_size { + if entry_size.saturating_add(cache_key_size) >= self.max_size { return; } - self.cache.insert(lru_key, Arc::new(index)); + self.cache.insert(cache_key, Arc::new(index)); } } diff --git a/src/message_pool/msgpool/mod.rs b/src/message_pool/msgpool/mod.rs index 6e8119d5b54b..edc8838323cd 100644 --- a/src/message_pool/msgpool/mod.rs +++ b/src/message_pool/msgpool/mod.rs @@ -25,7 +25,7 @@ use crate::networks::ChainConfig; use crate::prelude::*; use crate::shim::{address::Address, crypto::Signature}; use crate::state_manager::IdToAddressCache; -use crate::utils::cache::SizeTrackingLruCache; +use crate::utils::cache::SizeTrackingCache; use crate::utils::get_size::CidWrapper; use ahash::{HashMap, HashMapExt, HashSet}; use fvm_ipld_encoding::to_vec; @@ -208,12 +208,12 @@ where #[allow(clippy::too_many_arguments)] pub(in crate::message_pool) fn head_change( api: &T, - bls_sig_cache: &SizeTrackingLruCache, + bls_sig_cache: &SizeTrackingCache, republish: &RepublishState, pending_store: &PendingStore, cur_tipset: &SyncRwLock, key_cache: &IdToAddressCache, - state_nonce_cache: &SizeTrackingLruCache, + state_nonce_cache: &SizeTrackingCache, revert: Vec, apply: Vec, ) -> Result<(), Error> @@ -348,7 +348,7 @@ impl MpoolCtx<'_, T> { /// included in the current tipset. pub(in crate::message_pool) fn get_state_sequence( &self, - state_nonce_cache: &SizeTrackingLruCache, + state_nonce_cache: &SizeTrackingCache, addr: &Address, ) -> Result { msg_pool::get_state_sequence(self.api, self.key_cache, state_nonce_cache, addr, self.ts) diff --git a/src/message_pool/msgpool/msg_pool.rs b/src/message_pool/msgpool/msg_pool.rs index 0ce0ebae70b6..c404c87ac975 100644 --- a/src/message_pool/msgpool/msg_pool.rs +++ b/src/message_pool/msgpool/msg_pool.rs @@ -24,7 +24,7 @@ use crate::shim::{ }; use crate::state_manager::IdToAddressCache; use crate::state_manager::utils::is_valid_for_sending; -use crate::utils::cache::SizeTrackingLruCache; +use crate::utils::cache::SizeTrackingCache; use crate::utils::get_size::{CidWrapper, GetSize}; use ahash::HashSet; use anyhow::Context as _; @@ -55,7 +55,7 @@ use crate::message_pool::{ utils::get_base_fee_lower_bound, }; -// LruCache sizes have been taken from the lotus implementation +// Cache sizes have been taken from the lotus implementation const BLS_SIG_CACHE_SIZE: NonZeroUsize = nonzero!(40000usize); const SIG_VAL_CACHE_SIZE: NonZeroUsize = nonzero!(32000usize); const KEY_CACHE_SIZE: NonZeroUsize = nonzero!(1_048_576usize); @@ -83,24 +83,21 @@ pub enum TrustPolicy { pub use super::msg_set::{MsgSetLimits, StrictnessPolicy}; -/// LRU caches owned by [`MessagePool`]. +/// Caches owned by [`MessagePool`]. pub(in crate::message_pool) struct Caches { - pub bls_sig: SizeTrackingLruCache, - pub sig_val: SizeTrackingLruCache, + pub bls_sig: SizeTrackingCache, + pub sig_val: SizeTrackingCache, pub key: IdToAddressCache, - pub state_nonce: SizeTrackingLruCache, + pub state_nonce: SizeTrackingCache, } impl Caches { pub(in crate::message_pool) fn new() -> Self { Self { - bls_sig: SizeTrackingLruCache::new_with_metrics("bls_sig", BLS_SIG_CACHE_SIZE), - sig_val: SizeTrackingLruCache::new_with_metrics("sig_val", SIG_VAL_CACHE_SIZE), - key: SizeTrackingLruCache::new_with_metrics("mpool_key", KEY_CACHE_SIZE), - state_nonce: SizeTrackingLruCache::new_with_metrics( - "state_nonce", - STATE_NONCE_CACHE_SIZE, - ), + bls_sig: SizeTrackingCache::new_with_metrics("bls_sig", BLS_SIG_CACHE_SIZE), + sig_val: SizeTrackingCache::new_with_metrics("sig_val", SIG_VAL_CACHE_SIZE), + key: SizeTrackingCache::new_with_metrics("mpool_key", KEY_CACHE_SIZE), + state_nonce: SizeTrackingCache::new_with_metrics("state_nonce", STATE_NONCE_CACHE_SIZE), } } } @@ -181,7 +178,7 @@ pub(in crate::message_pool) fn resolve_to_key( pub(in crate::message_pool) fn get_state_sequence( api: &T, key_cache: &IdToAddressCache, - state_nonce_cache: &SizeTrackingLruCache, + state_nonce_cache: &SizeTrackingCache, addr: &Address, cur_ts: &Tipset, ) -> Result { @@ -670,7 +667,7 @@ where #[allow(clippy::too_many_arguments)] pub(in crate::message_pool) fn add_helper( api: &T, - bls_sig_cache: &SizeTrackingLruCache, + bls_sig_cache: &SizeTrackingCache, pending_store: &PendingStore, key_cache: &IdToAddressCache, cur_ts: &Tipset, @@ -765,8 +762,8 @@ mod tests { #[test] fn add_helper_message_gas_limit_test() { let api = TestApi::default(); - let bls_sig_cache = SizeTrackingLruCache::new_mocked(); - let key_cache = SizeTrackingLruCache::new_mocked(); + let bls_sig_cache = SizeTrackingCache::new_mocked(); + let key_cache = SizeTrackingCache::new_mocked(); let pending_store = test_pending_store(&api); let cur_ts = api.get_heaviest_tipset(); let message = ShimMessage { @@ -792,7 +789,7 @@ mod tests { #[test] fn test_resolve_to_key_returns_non_id_unchanged() { let api = TestApi::default(); - let key_cache = SizeTrackingLruCache::new_mocked(); + let key_cache = SizeTrackingCache::new_mocked(); let ts = api.get_heaviest_tipset(); let bls_addr = Address::new_bls(&[1u8; 48]).unwrap(); @@ -808,7 +805,7 @@ mod tests { #[test] fn test_resolve_to_key_resolves_id_and_caches() { let api = TestApi::default(); - let key_cache = SizeTrackingLruCache::new_mocked(); + let key_cache = SizeTrackingCache::new_mocked(); let ts = api.get_heaviest_tipset(); let id_addr = Address::new_id(100); @@ -831,8 +828,8 @@ mod tests { #[test] fn test_add_helper_keys_pending_by_resolved_address() { let api = TestApi::default(); - let bls_sig_cache = SizeTrackingLruCache::new_mocked(); - let key_cache = SizeTrackingLruCache::new_mocked(); + let bls_sig_cache = SizeTrackingCache::new_mocked(); + let key_cache = SizeTrackingCache::new_mocked(); let pending_store = test_pending_store(&api); let cur_ts = api.get_heaviest_tipset(); @@ -874,8 +871,8 @@ mod tests { #[test] fn test_get_sequence_works_with_both_address_forms() { let api = TestApi::default(); - let bls_sig_cache = SizeTrackingLruCache::new_mocked(); - let key_cache = SizeTrackingLruCache::new_mocked(); + let bls_sig_cache = SizeTrackingCache::new_mocked(); + let key_cache = SizeTrackingCache::new_mocked(); let pending_store = test_pending_store(&api); let cur_ts = api.get_heaviest_tipset(); @@ -925,8 +922,8 @@ mod tests { use crate::message_pool::test_provider::mock_block; let api = TestApi::default(); - let key_cache = SizeTrackingLruCache::new_mocked(); - let state_nonce_cache = SizeTrackingLruCache::new_mocked(); + let key_cache = SizeTrackingCache::new_mocked(); + let state_nonce_cache = SizeTrackingCache::new_mocked(); let sender = Address::new_bls(&[3u8; 48]).unwrap(); api.set_state_sequence(&sender, 5); @@ -950,8 +947,8 @@ mod tests { use crate::message_pool::test_provider::mock_block; let api = TestApi::default(); - let key_cache = SizeTrackingLruCache::new_mocked(); - let state_nonce_cache = SizeTrackingLruCache::new_mocked(); + let key_cache = SizeTrackingCache::new_mocked(); + let state_nonce_cache = SizeTrackingCache::new_mocked(); let addr_a = Address::new_bls(&[4u8; 48]).unwrap(); let addr_b = Address::new_bls(&[5u8; 48]).unwrap(); @@ -989,9 +986,9 @@ mod tests { use crate::message_pool::test_provider::mock_block; let api = TestApi::default(); - let key_cache = SizeTrackingLruCache::new_mocked(); - let state_nonce_cache: SizeTrackingLruCache = - SizeTrackingLruCache::new_mocked(); + let key_cache = SizeTrackingCache::new_mocked(); + let state_nonce_cache: SizeTrackingCache = + SizeTrackingCache::new_mocked(); let sender = Address::new_bls(&[6u8; 48]).unwrap(); api.set_state_sequence(&sender, 5); @@ -1021,9 +1018,9 @@ mod tests { use crate::message_pool::test_provider::mock_block; let api = TestApi::default(); - let key_cache = SizeTrackingLruCache::new_mocked(); - let state_nonce_cache: SizeTrackingLruCache = - SizeTrackingLruCache::new_mocked(); + let key_cache = SizeTrackingCache::new_mocked(); + let state_nonce_cache: SizeTrackingCache = + SizeTrackingCache::new_mocked(); let sender = Address::new_bls(&[7u8; 48]).unwrap(); api.set_state_sequence(&sender, 10); diff --git a/src/message_pool/msgpool/selection.rs b/src/message_pool/msgpool/selection.rs index b6d386aa314b..72379e7b65ee 100644 --- a/src/message_pool/msgpool/selection.rs +++ b/src/message_pool/msgpool/selection.rs @@ -20,7 +20,7 @@ use rand::prelude::SliceRandom; use tracing::{debug, error, warn}; use crate::shim::crypto::Signature; -use crate::utils::cache::SizeTrackingLruCache; +use crate::utils::cache::SizeTrackingCache; use crate::utils::get_size::CidWrapper; use super::{MpoolCtx, msg_pool::MessagePool, provider::Provider, utils::recover_sig}; @@ -805,7 +805,7 @@ fn merge_and_trim( // reorgs. pub(in crate::message_pool) fn run_head_change( api: &T, - bls_sig_cache: &SizeTrackingLruCache, + bls_sig_cache: &SizeTrackingCache, pending_store: &PendingStore, key_cache: &IdToAddressCache, from: Tipset, diff --git a/src/message_pool/msgpool/utils.rs b/src/message_pool/msgpool/utils.rs index e4f11ae2e080..e68329c05fe2 100644 --- a/src/message_pool/msgpool/utils.rs +++ b/src/message_pool/msgpool/utils.rs @@ -4,7 +4,7 @@ use crate::chain::MINIMUM_BASE_FEE; use crate::message::{MessageRead as _, SignedMessage}; use crate::shim::{crypto::Signature, econ::TokenAmount, message::Message}; -use crate::utils::cache::SizeTrackingLruCache; +use crate::utils::cache::SizeTrackingCache; use crate::utils::get_size::CidWrapper; use num_rational::BigRational; use num_traits::ToPrimitive; @@ -46,7 +46,7 @@ pub(in crate::message_pool) fn get_gas_perf(gas_reward: &TokenAmount, gas_limit: /// Attempt to get a signed message that corresponds to an unsigned message in /// `bls_sig_cache`. pub(in crate::message_pool) fn recover_sig( - bls_sig_cache: &SizeTrackingLruCache, + bls_sig_cache: &SizeTrackingCache, msg: Message, ) -> Result { let val = bls_sig_cache diff --git a/src/metrics/mod.rs b/src/metrics/mod.rs index e48098aa97d6..c186d164ce64 100644 --- a/src/metrics/mod.rs +++ b/src/metrics/mod.rs @@ -44,18 +44,18 @@ pub fn reset_collector_registry() { *collector_registry() = Default::default(); } -pub static LRU_CACHE_HIT: LazyLock> = LazyLock::new(|| { +pub static CACHE_HIT: LazyLock> = LazyLock::new(|| { let metric = Family::default(); DEFAULT_REGISTRY .write() - .register("lru_cache_hit", "Stats of lru cache hit", metric.clone()); + .register("cache_hit", "Cache hit count", metric.clone()); metric }); -pub static LRU_CACHE_MISS: LazyLock> = LazyLock::new(|| { +pub static CACHE_MISS: LazyLock> = LazyLock::new(|| { let metric = Family::default(); DEFAULT_REGISTRY .write() - .register("lru_cache_miss", "Stats of lru cache miss", metric.clone()); + .register("cache_miss", "Cache miss count", metric.clone()); metric }); diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 9b5a19dc50b9..7d8df05356f9 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -54,7 +54,7 @@ use crate::shim::gas::GasOutputs; use crate::shim::message::Message; use crate::shim::trace::{CallReturn, ExecutionEvent}; use crate::shim::{clock::ChainEpoch, state_tree::StateTree}; -use crate::state_manager::cache::ForestLruCache; +use crate::state_manager::cache::ForestCache; use crate::state_manager::{ExecutedMessage, ExecutedTipset, TipsetState, VMFlush}; use crate::utils::db::BlockstoreExt as _; use crate::utils::encoding::from_slice_with_fallback; @@ -478,9 +478,9 @@ impl Block { tipset: crate::blocks::Tipset, tx_info: TxInfo, ) -> Result> { - static ETH_BLOCK_HASH_TX_CACHE: LazyLock>> = + static ETH_BLOCK_HASH_TX_CACHE: LazyLock>> = LazyLock::new(|| { - ForestLruCache::with_size("eth_block_hash_tx", Block::block_cache_size()) + ForestCache::with_size("eth_block_hash_tx", Block::block_cache_size()) }); match tx_info { @@ -505,9 +505,9 @@ impl Block { ctx: Ctx, tipset: crate::blocks::Tipset, ) -> Result> { - static ETH_BLOCK_FULL_TX_CACHE: LazyLock>> = + static ETH_BLOCK_FULL_TX_CACHE: LazyLock>> = LazyLock::new(|| { - ForestLruCache::with_size("eth_block_full_tx", Block::block_cache_size()) + ForestCache::with_size("eth_block_full_tx", Block::block_cache_size()) }); let block_cid = tipset.key().cid()?; diff --git a/src/rpc/methods/f3.rs b/src/rpc/methods/f3.rs index 38e2c5313ebc..6b766b9dd80f 100644 --- a/src/rpc/methods/f3.rs +++ b/src/rpc/methods/f3.rs @@ -20,8 +20,8 @@ use crate::{ chain::index::ResolveNullTipset, chain_sync::TipsetValidator, db::{ - BlockstoreReadCacheStats as _, BlockstoreWithReadCache, DefaultBlockstoreReadCacheStats, - LruBlockstoreReadCache, + BlockstoreReadCacheStats as _, BlockstoreWithReadCache, DefaultBlockstoreReadCache, + DefaultBlockstoreReadCacheStats, }, libp2p::{NetRPCMethods, NetworkMessage}, lotus_json::{HasLotusJson as _, LotusJson}, @@ -171,8 +171,8 @@ impl GetPowerTable { async fn compute(ctx: &Ctx, ts: &Tipset) -> anyhow::Result> { // The RAM overhead on mainnet is ~14MiB const BLOCKSTORE_CACHE_CAP: NonZeroUsize = nonzero!(65536_usize); - static BLOCKSTORE_CACHE: LazyLock = LazyLock::new(|| { - LruBlockstoreReadCache::new_with_metrics("get_powertable", BLOCKSTORE_CACHE_CAP) + static BLOCKSTORE_CACHE: LazyLock = LazyLock::new(|| { + DefaultBlockstoreReadCache::new_with_metrics("get_powertable", BLOCKSTORE_CACHE_CAP) }); let db = BlockstoreWithReadCache::new( ctx.db_owned(), diff --git a/src/state_manager/cache.rs b/src/state_manager/cache.rs index 74fed190fe0a..dd18e314d3b3 100644 --- a/src/state_manager/cache.rs +++ b/src/state_manager/cache.rs @@ -3,7 +3,7 @@ use crate::prelude::*; use crate::state_manager::DEFAULT_TIPSET_CACHE_SIZE; -use crate::utils::cache::{LruKeyConstraints, LruValueConstraints, SizeTrackingLruCache}; +use crate::utils::cache::{CacheKeyConstraints, CacheValueConstraints, SizeTrackingCache}; use prometheus_client::metrics::counter::Counter; use std::borrow::Cow; use std::future::Future; @@ -14,12 +14,12 @@ use std::sync::LazyLock; // locks the inner label map on every call, so we hoist the lookup to module init. // `Counter` is cheaply cloneable; clones share the atomic underneath. static TIPSET_HIT: LazyLock = LazyLock::new(|| { - crate::metrics::LRU_CACHE_HIT + crate::metrics::CACHE_HIT .get_or_create(&crate::metrics::values::STATE_MANAGER_TIPSET) .clone() }); static TIPSET_MISS: LazyLock = LazyLock::new(|| { - crate::metrics::LRU_CACHE_MISS + crate::metrics::CACHE_MISS .get_or_create(&crate::metrics::values::STATE_MANAGER_TIPSET) .clone() }); @@ -27,11 +27,11 @@ static TIPSET_MISS: LazyLock = LazyLock::new(|| { /// A cache that handles concurrent access and computation for tipset-related /// data. Coalesces concurrent computations of the same key, so only one caller /// actually runs the `compute` future and the rest wait on its result. -pub(crate) struct ForestLruCache { - cache: Arc>, +pub(crate) struct ForestCache { + cache: Arc>, } -impl ShallowClone for ForestLruCache { +impl ShallowClone for ForestCache { fn shallow_clone(&self) -> Self { Self { cache: self.cache.shallow_clone(), @@ -39,7 +39,7 @@ impl ShallowClone for ForestLruCac } } -impl ForestLruCache { +impl ForestCache { pub fn new(cache_identifier: impl Into>) -> Self { Self::with_size(cache_identifier, DEFAULT_TIPSET_CACHE_SIZE) } @@ -49,7 +49,7 @@ impl ForestLruCache { cache_size: NonZeroUsize, ) -> Self { Self { - cache: Arc::new(SizeTrackingLruCache::new_with_metrics( + cache: Arc::new(SizeTrackingCache::new_with_metrics( cache_identifier, cache_size, )), @@ -106,7 +106,7 @@ mod tests { #[tokio::test] async fn test_tipset_cache_basic_functionality() { - let cache: ForestLruCache = ForestLruCache::new("test"); + let cache: ForestCache = ForestCache::new("test"); let key = create_test_tipset_key(1); let result = cache @@ -124,7 +124,7 @@ mod tests { #[tokio::test] async fn test_concurrent_same_key_computation() { - let cache: Arc> = Arc::new(ForestLruCache::new("test")); + let cache: Arc> = Arc::new(ForestCache::new("test")); let key = create_test_tipset_key(1); let computation_count = Arc::new(AtomicU8::new(0)); @@ -165,7 +165,7 @@ mod tests { #[tokio::test] async fn test_concurrent_different_keys() { - let cache: Arc> = Arc::new(ForestLruCache::new("test")); + let cache: Arc> = Arc::new(ForestCache::new("test")); let computation_count = Arc::new(AtomicU8::new(0)); let mut handles = vec![]; diff --git a/src/state_manager/mod.rs b/src/state_manager/mod.rs index 3f8c69d3d718..369db909de4f 100644 --- a/src/state_manager/mod.rs +++ b/src/state_manager/mod.rs @@ -48,8 +48,8 @@ use crate::shim::{ state_tree::{ActorState, StateTree}, version::NetworkVersion, }; -use crate::state_manager::cache::ForestLruCache; -use crate::utils::cache::SizeTrackingLruCache; +use crate::state_manager::cache::ForestCache; +use crate::utils::cache::SizeTrackingCache; use crate::utils::get_size::{GetSize, vec_heap_size_helper}; use anyhow::Context as _; use chain_rand::ChainRand; @@ -62,7 +62,7 @@ use tracing::warn; const DEFAULT_TIPSET_CACHE_SIZE: NonZeroUsize = nonzero!(1024usize); const DEFAULT_ID_TO_DETERMINISTIC_ADDRESS_CACHE_SIZE: NonZeroUsize = nonzero!(1024usize); pub const EVENTS_AMT_BITWIDTH: u32 = 5; -pub type IdToAddressCache = SizeTrackingLruCache; +pub type IdToAddressCache = SizeTrackingCache; /// Result of executing an individual chain message in a tipset. /// @@ -167,7 +167,7 @@ pub struct StateManager { /// Chain store cs: ChainStore, /// This is a cache which indexes tipsets to their calculated state output (state root, receipt root). - cache: ForestLruCache, + cache: ForestCache, id_to_deterministic_address_cache: IdToAddressCache, beacon: Arc, engine: Arc, @@ -209,10 +209,10 @@ impl StateManager { Ok(Self { cs, - cache: ForestLruCache::new("tipset_state_executed_tipset"), // For StateOutput + cache: ForestCache::new("tipset_state_executed_tipset"), // For StateOutput beacon, engine, - id_to_deterministic_address_cache: SizeTrackingLruCache::new_with_metrics( + id_to_deterministic_address_cache: SizeTrackingCache::new_with_metrics( "id_to_deterministic_address", DEFAULT_ID_TO_DETERMINISTIC_ADDRESS_CACHE_SIZE, ), diff --git a/src/state_migration/common/mod.rs b/src/state_migration/common/mod.rs index 1daff30e5608..cbd3ad4bee0b 100644 --- a/src/state_migration/common/mod.rs +++ b/src/state_migration/common/mod.rs @@ -8,7 +8,7 @@ use std::{num::NonZeroUsize, sync::Arc}; use crate::{ shim::{address::Address, clock::ChainEpoch, econ::TokenAmount, state_tree::StateTree}, - utils::{cache::SizeTrackingLruCache, get_size::CidWrapper}, + utils::{cache::SizeTrackingCache, get_size::CidWrapper}, }; use cid::Cid; use fvm_ipld_blockstore::Blockstore; @@ -25,13 +25,13 @@ pub(in crate::state_migration) type Migrator = Arc + /// Cache of existing CID to CID migrations for an actor. #[derive(Clone)] pub(in crate::state_migration) struct MigrationCache { - cache: Arc>, + cache: Arc>, } impl MigrationCache { pub fn new(size: NonZeroUsize) -> Self { Self { - cache: Arc::new(SizeTrackingLruCache::new_with_metrics("migration", size)), + cache: Arc::new(SizeTrackingCache::new_with_metrics("migration", size)), } } diff --git a/src/utils/cache/mod.rs b/src/utils/cache/mod.rs index a18c29b42898..eeb379db6dde 100644 --- a/src/utils/cache/mod.rs +++ b/src/utils/cache/mod.rs @@ -1,5 +1,5 @@ // Copyright 2019-2026 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -mod lru; -pub use lru::{LruKeyConstraints, LruValueConstraints, SizeTrackingLruCache}; +mod size_tracking; +pub use size_tracking::{CacheKeyConstraints, CacheValueConstraints, SizeTrackingCache}; diff --git a/src/utils/cache/lru.rs b/src/utils/cache/size_tracking.rs similarity index 86% rename from src/utils/cache/lru.rs rename to src/utils/cache/size_tracking.rs index a875e163ac88..db4645649637 100644 --- a/src/utils/cache/lru.rs +++ b/src/utils/cache/size_tracking.rs @@ -13,42 +13,41 @@ use std::{ }; use get_size2::GetSize; -use quick_cache::Equivalent; use prometheus_client::{ collector::Collector, encoding::{DescriptorEncoder, EncodeMetric}, metrics::gauge::Gauge, registry::Unit, }; +use quick_cache::Equivalent; use quick_cache::sync::Cache; use crate::prelude::*; -pub trait LruKeyConstraints: +pub trait CacheKeyConstraints: GetSize + Debug + Send + Sync + Hash + PartialEq + Eq + Clone + 'static { } -impl LruKeyConstraints for T where +impl CacheKeyConstraints for T where T: GetSize + Debug + Send + Sync + Hash + PartialEq + Eq + Clone + 'static { } -pub trait LruValueConstraints: GetSize + Debug + Send + Sync + Clone + 'static {} +pub trait CacheValueConstraints: GetSize + Debug + Send + Sync + Clone + 'static {} -impl LruValueConstraints for T where T: GetSize + Debug + Send + Sync + Clone + 'static {} +impl CacheValueConstraints for T where T: GetSize + Debug + Send + Sync + Clone + 'static {} /// A concurrent cache with Prometheus instrumentation. /// -/// Backed by [`quick_cache::sync::Cache`] (CLOCK-PRO). The name keeps "Lru" for -/// API compatibility with historical call sites; the actual eviction policy is -/// CLOCK-PRO, which is scan-resistant and typically gives higher hit rates -/// than strict LRU on chain workloads. +/// Backed by [`quick_cache::sync::Cache`], which uses the scan-resistant +/// CLOCK-PRO eviction policy. Tracks total entry size in bytes for +/// observability. #[derive(Debug)] -pub struct SizeTrackingLruCache +pub struct SizeTrackingCache where - K: LruKeyConstraints, - V: LruValueConstraints, + K: CacheKeyConstraints, + V: CacheValueConstraints, { cache_id: usize, cache_name: Cow<'static, str>, @@ -56,10 +55,10 @@ where capacity: usize, } -impl ShallowClone for SizeTrackingLruCache +impl ShallowClone for SizeTrackingCache where - K: LruKeyConstraints, - V: LruValueConstraints, + K: CacheKeyConstraints, + V: CacheValueConstraints, { fn shallow_clone(&self) -> Self { Self { @@ -71,10 +70,10 @@ where } } -impl SizeTrackingLruCache +impl SizeTrackingCache where - K: LruKeyConstraints, - V: LruValueConstraints, + K: CacheKeyConstraints, + V: CacheValueConstraints, { fn register_metrics(&self) { crate::metrics::register_collector(Box::new(self.shallow_clone())); @@ -168,11 +167,7 @@ where /// Returns `(value, was_hit)`; the caller can use the flag to drive /// hit/miss metrics. If `compute` fails the placeholder is released and /// the next caller will recompute. - pub async fn get_or_compute( - &self, - key: &K, - compute: F, - ) -> Result<(V, bool), E> + pub async fn get_or_compute(&self, key: &K, compute: F) -> Result<(V, bool), E> where F: FnOnce() -> Fut, Fut: std::future::Future>, @@ -203,10 +198,10 @@ where } } -impl Collector for SizeTrackingLruCache +impl Collector for SizeTrackingCache where - K: LruKeyConstraints, - V: LruValueConstraints, + K: CacheKeyConstraints, + V: CacheValueConstraints, { fn encode(&self, mut encoder: DescriptorEncoder) -> Result<(), std::fmt::Error> { { From fecc151b650ff51ab2c3cd59f2b5b1f55f6a4ef6 Mon Sep 17 00:00:00 2001 From: Hubert Bugaj Date: Mon, 18 May 2026 15:59:13 +0200 Subject: [PATCH 3/6] lychee ignore --- .config/lychee.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.config/lychee.toml b/.config/lychee.toml index f90f65222fde..321106c6dcd8 100644 --- a/.config/lychee.toml +++ b/.config/lychee.toml @@ -11,6 +11,8 @@ exclude = [ "github.com/ChainSafe/forest", # Maybe temporarily down with 404, but it blocks the CI "filecoin.io/slack", + # Bot protection + "crates.io", ] timeout = 30 max_retries = 6 From 04b93768b74a564f1fe9b6bda991d8dcd485a0e8 Mon Sep 17 00:00:00 2001 From: Hubert Bugaj Date: Mon, 18 May 2026 16:19:38 +0200 Subject: [PATCH 4/6] spellcheck --- src/db/car/mod.rs | 4 ++-- src/utils/cache/size_tracking.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/db/car/mod.rs b/src/db/car/mod.rs index 3a6e7d239446..1f16a7a0c61d 100644 --- a/src/db/car/mod.rs +++ b/src/db/car/mod.rs @@ -51,10 +51,10 @@ pub static ZSTD_FRAME_CACHE_DEFAULT_MAX_SIZE: LazyLock = LazyLock::new(|| /// One decompressed zstd frame's index, shared via [`Arc`] so cache lookups /// don't deep-copy the inner [`hashbrown::HashMap`]. Snapshot export hits the -/// cache once per CID; a per-call HashMap clone destroys throughput. +/// cache once per CID; a per-call `HashMap` clone destroys throughput. type FrameIndex = Arc>; -/// Weighter that bills each entry by `key.get_size() + value.get_size()`. +/// A [`Weighter`] that bills each entry by `key.get_size() + value.get_size()`. /// Used to make [`ZstdFrameCache`] evict by byte size. #[derive(Clone, Copy, Debug, Default)] struct ZstdFrameWeighter; diff --git a/src/utils/cache/size_tracking.rs b/src/utils/cache/size_tracking.rs index db4645649637..2bb1bccd80ae 100644 --- a/src/utils/cache/size_tracking.rs +++ b/src/utils/cache/size_tracking.rs @@ -117,7 +117,7 @@ where /// /// `quick_cache::sync::Cache::insert` does not return the displaced /// value, so this is a peek-then-insert. The two steps are not atomic; - /// concurrent inserters for the same key may both observe `None`. None of + /// concurrent callers for the same key may both observe `None`. None of /// the existing callers depend on atomicity here. pub fn push(&self, k: K, v: V) -> Option { let prev = self.cache.peek(&k); From 179baa35e50a11609b97b9445888e47c11f0614b Mon Sep 17 00:00:00 2001 From: Hubert Bugaj Date: Mon, 18 May 2026 16:41:26 +0200 Subject: [PATCH 5/6] fmt --- src/message_pool/msgpool/msg_pool.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/message_pool/msgpool/msg_pool.rs b/src/message_pool/msgpool/msg_pool.rs index 0aefd211eea0..433a9f8d7662 100644 --- a/src/message_pool/msgpool/msg_pool.rs +++ b/src/message_pool/msgpool/msg_pool.rs @@ -96,10 +96,7 @@ impl Caches { bls_sig: SizeTrackingCache::new_with_metrics("bls_sig", BLS_SIG_CACHE_SIZE), sig_val: SizeTrackingCache::new_with_metrics("sig_val", SIG_VAL_CACHE_SIZE), key: SizeTrackingCache::new_with_metrics("mpool_key", KEY_CACHE_SIZE), - state_nonce: SizeTrackingCache::new_with_metrics( - "state_nonce", - STATE_NONCE_CACHE_SIZE, - ), + state_nonce: SizeTrackingCache::new_with_metrics("state_nonce", STATE_NONCE_CACHE_SIZE), } } } @@ -571,10 +568,7 @@ fn validate_signature( eth_chain_id: u64, ) -> Result<(), Error> { let cid = msg.cid(); - if sig_val_cache - .get_cloned(&CidWrapper::from(cid)) - .is_some() - { + if sig_val_cache.get_cloned(&CidWrapper::from(cid)).is_some() { return Ok(()); } msg.verify(eth_chain_id) From 12627d56488b9715803115330a44ceaa84d0b9d9 Mon Sep 17 00:00:00 2001 From: Hubert Bugaj Date: Mon, 18 May 2026 16:46:26 +0200 Subject: [PATCH 6/6] fix edge case --- src/db/car/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/db/car/mod.rs b/src/db/car/mod.rs index 1f16a7a0c61d..3927fb699229 100644 --- a/src/db/car/mod.rs +++ b/src/db/car/mod.rs @@ -131,7 +131,7 @@ impl ZstdFrameCache { let entry_size = index.get_size(); // Skip individual items larger than the whole cache — they'd evict // everything and still not fit. - if entry_size.saturating_add(cache_key_size) >= self.max_size { + if entry_size.saturating_add(cache_key_size) > self.max_size { return; } self.cache.insert(cache_key, Arc::new(index));