1#[cfg(with_metrics)]
14use std::any::type_name;
15use std::{
16 borrow::Cow,
17 hash::Hash,
18 sync::{Arc, Weak},
19};
20
21use linera_base::{crypto::CryptoHash, hashed::Hashed};
22use papaya::{Compute, Operation};
23use quick_cache::sync::Cache;
24
25pub const DEFAULT_CLEANUP_INTERVAL_SECS: u64 = 30;
27
28pub struct ValueCache<K, V> {
37 cache: Cache<K, Arc<V>>,
38 weak_index: Arc<papaya::HashMap<K, Weak<V>>>,
39}
40
41impl<K, V> ValueCache<K, V>
42where
43 K: Hash + Eq + Clone + Send + Sync + 'static,
44 V: Send + Sync + 'static,
45{
46 #[cfg(not(web))]
49 pub fn new(size: usize, cleanup_interval_secs: u64) -> Self {
50 let weak_index = Arc::new(papaya::HashMap::new());
51 Self::spawn_cleanup_task(
52 Arc::clone(&weak_index),
53 std::time::Duration::from_secs(cleanup_interval_secs),
54 );
55 ValueCache {
56 cache: Cache::new(size),
57 weak_index,
58 }
59 }
60
61 #[cfg(web)]
63 pub fn new(size: usize, _cleanup_interval_secs: u64) -> Self {
64 ValueCache {
65 cache: Cache::new(size),
66 weak_index: Arc::new(papaya::HashMap::new()),
67 }
68 }
69
70 pub fn insert(&self, key: &K, value: V) -> Arc<V> {
76 self.dedup_insert(key, Arc::new(value))
77 }
78
79 #[cfg(with_testing)]
81 pub fn insert_arc(&self, key: &K, value: Arc<V>) -> Arc<V> {
82 self.dedup_insert(key, value)
83 }
84
85 pub fn remove(&self, key: &K) -> Option<Arc<V>> {
92 let value = self.cache.peek(key);
93 if value.is_some() {
94 self.cache.remove(key);
95 }
96 Self::track_cache_usage(value)
97 }
98
99 pub fn get(&self, key: &K) -> Option<Arc<V>> {
102 if let Some(arc) = self.cache.get(key) {
104 return Self::track_cache_usage(Some(arc));
105 }
106
107 let guard = self.weak_index.guard();
109 if let Some(weak) = self.weak_index.get(key, &guard) {
110 if let Some(arc) = weak.upgrade() {
111 self.cache.insert(key.clone(), arc.clone());
113 return Self::track_cache_usage(Some(arc));
114 }
115 }
116
117 Self::track_cache_usage(None)
118 }
119
120 pub fn contains(&self, key: &K) -> bool {
123 if self.cache.peek(key).is_some() {
124 return true;
125 }
126 let guard = self.weak_index.guard();
127 self.weak_index
128 .get(key, &guard)
129 .is_some_and(|weak| weak.strong_count() > 0)
130 }
131
132 #[cfg(with_testing)]
134 pub fn cleanup_dead_entries(&self) {
135 let guard = self.weak_index.guard();
136 self.weak_index
137 .retain(|_, weak| weak.strong_count() > 0, &guard);
138 }
139
140 #[cfg(not(web))]
144 fn spawn_cleanup_task(
145 weak_index: Arc<papaya::HashMap<K, Weak<V>>>,
146 cleanup_interval: std::time::Duration,
147 ) {
148 if tokio::runtime::Handle::try_current().is_err() {
149 return;
150 }
151 tokio::spawn(async move {
152 let mut interval = tokio::time::interval(cleanup_interval);
153 loop {
154 interval.tick().await;
155 let guard = weak_index.guard();
156 weak_index.retain(|_, weak| weak.strong_count() > 0, &guard);
157 }
158 });
159 }
160
161 fn dedup_insert(&self, key: &K, new_arc: Arc<V>) -> Arc<V> {
165 let guard = self.weak_index.guard();
166 let weak = Arc::downgrade(&new_arc);
167
168 let result = self.weak_index.compute(
169 key.clone(),
170 |entry| match entry {
171 Some((_k, existing_weak)) => match existing_weak.upgrade() {
172 Some(existing_arc) => Operation::Abort(existing_arc),
173 None => Operation::Insert(weak.clone()),
174 },
175 None => Operation::Insert(weak.clone()),
176 },
177 &guard,
178 );
179
180 let canonical_arc = match result {
181 Compute::Inserted(..) | Compute::Updated { .. } => new_arc,
182 Compute::Aborted(existing_arc) => existing_arc,
183 _ => unreachable!(),
184 };
185
186 self.cache.insert(key.clone(), canonical_arc.clone());
187 canonical_arc
188 }
189
190 fn track_cache_usage(maybe_value: Option<Arc<V>>) -> Option<Arc<V>> {
191 #[cfg(with_metrics)]
192 {
193 let metric = if maybe_value.is_some() {
194 &metrics::CACHE_HIT_COUNT
195 } else {
196 &metrics::CACHE_MISS_COUNT
197 };
198
199 metric
200 .with_label_values(&[type_name::<K>(), type_name::<V>()])
201 .inc();
202 }
203 maybe_value
204 }
205}
206
207impl<T: Send + Sync + 'static> ValueCache<CryptoHash, Hashed<T>> {
208 pub fn insert_hashed(&self, value: Cow<Hashed<T>>) -> Arc<Hashed<T>>
213 where
214 T: Clone,
215 {
216 let hash = (*value).hash();
217 if let Some(arc) = self.cache.peek(&hash) {
219 return arc;
220 }
221 let guard = self.weak_index.guard();
223 if let Some(weak) = self.weak_index.get(&hash, &guard) {
224 if let Some(arc) = weak.upgrade() {
225 self.cache.insert(hash, arc.clone());
226 return arc;
227 }
228 }
229 drop(guard);
230 self.dedup_insert(&hash, Arc::new(value.into_owned()))
231 }
232
233 #[cfg(with_testing)]
235 pub fn insert_all_hashed<'a>(&self, values: impl IntoIterator<Item = Cow<'a, Hashed<T>>>)
236 where
237 T: Clone + 'a,
238 {
239 for value in values {
240 self.insert_hashed(value);
241 }
242 }
243}
244
245#[cfg(with_metrics)]
246mod metrics {
247 use std::sync::LazyLock;
248
249 use linera_base::prometheus_util::register_int_counter_vec;
250 use prometheus::IntCounterVec;
251
252 pub static CACHE_HIT_COUNT: LazyLock<IntCounterVec> = LazyLock::new(|| {
253 register_int_counter_vec(
254 "value_cache_hit",
255 "Cache hits in `ValueCache`",
256 &["key_type", "value_type"],
257 )
258 });
259
260 pub static CACHE_MISS_COUNT: LazyLock<IntCounterVec> = LazyLock::new(|| {
261 register_int_counter_vec(
262 "value_cache_miss",
263 "Cache misses in `ValueCache`",
264 &["key_type", "value_type"],
265 )
266 });
267}
268
269#[cfg(test)]
270mod tests {
271 use std::{borrow::Cow, sync::Arc};
272
273 use linera_base::{crypto::CryptoHash, hashed::Hashed};
274 use serde::{Deserialize, Serialize};
275
276 use super::{ValueCache, DEFAULT_CLEANUP_INTERVAL_SECS};
277
278 const TEST_CACHE_SIZE: usize = 10;
280
281 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
283 struct TestValue(u64);
284
285 impl linera_base::crypto::BcsHashable<'_> for TestValue {}
286
287 fn create_test_value(n: u64) -> Hashed<TestValue> {
288 Hashed::new(TestValue(n))
289 }
290
291 fn create_test_values(iter: impl IntoIterator<Item = u64>) -> Vec<Hashed<TestValue>> {
292 iter.into_iter().map(create_test_value).collect()
293 }
294
295 fn new_hashed_cache(size: usize) -> ValueCache<CryptoHash, Hashed<TestValue>> {
296 ValueCache::new(size, DEFAULT_CLEANUP_INTERVAL_SECS)
297 }
298
299 fn new_string_cache(size: usize) -> ValueCache<u64, String> {
300 ValueCache::new(size, DEFAULT_CLEANUP_INTERVAL_SECS)
301 }
302
303 #[test]
304 fn test_retrieve_missing_value() {
305 let cache = new_hashed_cache(TEST_CACHE_SIZE);
306 let hash = CryptoHash::test_hash("Missing value");
307
308 assert!(cache.get(&hash).is_none());
309 assert!(!cache.contains(&hash));
310 }
311
312 #[test]
313 fn test_insert_and_get() {
314 let cache = new_hashed_cache(TEST_CACHE_SIZE);
315 let value = create_test_value(0);
316 let hash = value.hash();
317
318 cache.insert_hashed(Cow::Borrowed(&value));
319 assert!(cache.contains(&hash));
320 assert_eq!(cache.get(&hash).as_deref(), Some(&value));
321 }
322
323 #[test]
324 fn test_insert_many_values() {
325 let cache = new_hashed_cache(TEST_CACHE_SIZE);
326 let values = create_test_values(0..TEST_CACHE_SIZE as u64);
327
328 for value in &values {
329 cache.insert_hashed(Cow::Borrowed(value));
330 }
331
332 for value in &values {
333 assert!(cache.contains(&value.hash()));
334 assert_eq!(cache.get(&value.hash()).as_deref(), Some(value));
335 }
336
337 let cache2 = new_hashed_cache(TEST_CACHE_SIZE);
339 cache2.insert_all_hashed(values.iter().map(Cow::Borrowed));
340 for value in &values {
341 assert_eq!(cache2.get(&value.hash()).as_deref(), Some(value));
342 }
343 }
344
345 #[test]
346 fn test_reinsertion_dedup() {
347 let cache = new_hashed_cache(TEST_CACHE_SIZE);
348 let values = create_test_values(0..TEST_CACHE_SIZE as u64);
349
350 let first_arcs: Vec<_> = values
352 .iter()
353 .map(|v| cache.insert_hashed(Cow::Borrowed(v)))
354 .collect();
355
356 for (value, first_arc) in values.iter().zip(&first_arcs) {
358 let second_arc = cache.insert_hashed(Cow::Borrowed(value));
359 assert!(Arc::ptr_eq(&second_arc, first_arc));
360 }
361 }
362
363 #[test]
364 fn test_eviction() {
365 let cache = new_hashed_cache(TEST_CACHE_SIZE);
366 let total = TEST_CACHE_SIZE * 3;
367 let values = create_test_values(0..total as u64);
368
369 for value in &values {
370 cache.insert_hashed(Cow::Borrowed(value));
371 }
372
373 let present_count = values.iter().filter(|v| cache.contains(&v.hash())).count();
374 assert!(
375 present_count <= TEST_CACHE_SIZE + 1,
376 "cache should not hold significantly more than its capacity, \
377 but has {present_count} entries for capacity {TEST_CACHE_SIZE}"
378 );
379 assert!(present_count > 0, "cache should still hold some entries");
380 }
381
382 #[test]
383 fn test_accessed_entry_survives_eviction() {
384 let cache = new_hashed_cache(TEST_CACHE_SIZE);
385 let promoted = create_test_value(0);
386 let promoted_hash = promoted.hash();
387
388 cache.insert_hashed(Cow::Borrowed(&promoted));
389 cache.get(&promoted_hash); let extras = create_test_values(1..=TEST_CACHE_SIZE as u64 * 2);
392 for value in &extras {
393 cache.insert_hashed(Cow::Borrowed(value));
394 }
395
396 assert!(
397 cache.contains(&promoted_hash),
398 "recently accessed entry should survive eviction"
399 );
400 }
401
402 #[test]
403 fn test_promotion_of_reinsertion() {
404 let cache = new_hashed_cache(TEST_CACHE_SIZE);
405 let promoted = create_test_value(0);
406 let promoted_hash = promoted.hash();
407
408 let first = cache.insert_hashed(Cow::Borrowed(&promoted));
409 let second = cache.insert_hashed(Cow::Borrowed(&promoted));
410 assert!(Arc::ptr_eq(&first, &second));
411
412 let extras = create_test_values(1..=TEST_CACHE_SIZE as u64 * 2);
413 for value in &extras {
414 cache.insert_hashed(Cow::Borrowed(value));
415 }
416
417 assert!(
418 cache.contains(&promoted_hash),
419 "re-inserted entry should survive eviction"
420 );
421 }
422
423 #[test]
424 fn test_weak_index_dedup_after_eviction() {
425 let cache = new_string_cache(2);
426
427 let held = cache.insert(&1, "hello".to_string());
429
430 cache.insert(&2, "world".to_string());
432 cache.insert(&3, "foo".to_string());
433 cache.insert(&4, "bar".to_string());
434
435 let retrieved = cache
437 .get(&1)
438 .expect("held Arc should keep entry findable via weak index");
439 assert!(
440 Arc::ptr_eq(&retrieved, &held),
441 "must return same allocation, not a duplicate"
442 );
443
444 let reinserted = cache.insert(&1, "replacement".to_string());
446 assert!(Arc::ptr_eq(&reinserted, &held));
447 assert_eq!(&*reinserted, "hello");
448 }
449
450 #[test]
451 fn test_remove_preserves_weak_for_held_arcs() {
452 let cache = new_string_cache(TEST_CACHE_SIZE);
453
454 let held = cache.insert(&1, "hello".to_string());
455
456 cache.remove(&1);
458
459 let retrieved = cache.get(&1).expect("weak index should find held Arc");
461 assert!(Arc::ptr_eq(&retrieved, &held));
462 }
463
464 #[test]
465 fn test_remove_without_holder() {
466 let cache = new_string_cache(TEST_CACHE_SIZE);
467
468 cache.insert(&1, "hello".to_string());
469
470 cache.remove(&1);
472 assert!(!cache.contains(&1));
473 assert!(cache.get(&1).is_none());
474 }
475
476 #[test]
477 fn test_cleanup_dead_entries() {
478 let cache = new_string_cache(2);
479
480 cache.insert(&1, "alive".to_string());
481 let _held = cache.get(&1).expect("just inserted"); cache.insert(&2, "dead".to_string());
484 cache.insert(&3, "a".to_string());
488 cache.insert(&4, "b".to_string());
489 cache.insert(&5, "c".to_string());
490
491 cache.cleanup_dead_entries();
492
493 assert!(cache.contains(&1));
495 }
496
497 #[test]
498 fn test_insert_arc_dedup() {
499 let cache = new_string_cache(TEST_CACHE_SIZE);
500 let value = Arc::new("hello".to_string());
501
502 let first = cache.insert_arc(&1, value.clone());
503 assert!(Arc::ptr_eq(&first, &value));
504
505 let second = cache.insert_arc(&1, Arc::new("other".to_string()));
506 assert!(Arc::ptr_eq(&second, &value));
507
508 assert_eq!(&*cache.get(&1).expect("just inserted"), "hello");
509 }
510}