This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-cache / src / lib.rs
18 kB 634 lines
1mod expiring; 2 3pub use expiring::{Admitted, Expiring, GroupQuota, Quotas, Rejected, TotalQuota}; 4 5use std::collections::{BTreeMap, HashMap}; 6use std::future::Future; 7use std::hash::Hash; 8use std::sync::{Arc, Mutex, OnceLock, Weak}; 9use std::time::Duration; 10 11use knot_runtime::{Clock, UnixMicros}; 12 13knot_types::scalar_newtype! { 14 pub struct EntryCount(u64); 15 pub struct Weight(u64); 16} 17 18pub trait Cache<K, V>: Send + Sync { 19 fn get(&self, key: &K) -> Option<V>; 20 fn insert(&self, key: K, value: V); 21 fn invalidate(&self, key: &K); 22 fn invalidate_all(&self); 23 fn entry_count(&self) -> EntryCount; 24 fn weighted_size(&self) -> Weight; 25} 26 27pub struct Untimed; 28 29impl Clock for Untimed { 30 fn now_unix_micros(&self) -> UnixMicros { 31 UnixMicros::new(0) 32 } 33} 34 35pub struct Moka<K, V> { 36 inner: moka::sync::Cache<K, V>, 37} 38 39impl<K, V> Moka<K, V> 40where 41 K: Hash + Eq + Send + Sync + 'static, 42 V: Clone + Send + Sync + 'static, 43{ 44 pub fn by_count(max_entries: EntryCount) -> Self { 45 Self { 46 inner: moka::sync::Cache::builder() 47 .max_capacity(max_entries.get()) 48 .build(), 49 } 50 } 51 52 pub fn by_weight<F>(max_weight: Weight, weigh: F) -> Self 53 where 54 F: Fn(&V) -> Weight + Send + Sync + 'static, 55 { 56 Self { 57 inner: moka::sync::Cache::builder() 58 .max_capacity(max_weight.get()) 59 .weigher(move |_key: &K, value: &V| weigh(value).get().min(u32::MAX as u64) as u32) 60 .build(), 61 } 62 } 63 64 pub fn get_or_try_insert_with<E, F>(&self, key: K, init: F) -> Result<V, Arc<E>> 65 where 66 F: FnOnce() -> Result<V, E>, 67 E: Send + Sync + 'static, 68 { 69 self.inner.try_get_with(key, init) 70 } 71} 72 73impl<K, V> Cache<K, V> for Moka<K, V> 74where 75 K: Hash + Eq + Send + Sync + 'static, 76 V: Clone + Send + Sync + 'static, 77{ 78 fn get(&self, key: &K) -> Option<V> { 79 self.inner.get(key) 80 } 81 82 fn insert(&self, key: K, value: V) { 83 self.inner.insert(key, value); 84 } 85 86 fn invalidate(&self, key: &K) { 87 self.inner.invalidate(key); 88 } 89 90 fn invalidate_all(&self) { 91 self.inner.invalidate_all(); 92 } 93 94 fn entry_count(&self) -> EntryCount { 95 EntryCount::new(self.inner.entry_count()) 96 } 97 98 fn weighted_size(&self) -> Weight { 99 Weight::new(self.inner.weighted_size()) 100 } 101} 102 103#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 104struct Tick(u64); 105 106impl Tick { 107 fn issue(&mut self) -> Tick { 108 let issued = *self; 109 self.0 = self.0.saturating_add(1); 110 issued 111 } 112} 113 114struct Node<V> { 115 value: V, 116 tick: Tick, 117 weight: u64, 118 expires_at: Option<UnixMicros>, 119} 120 121struct LruInner<K, V> { 122 by_key: HashMap<K, Node<V>>, 123 order: BTreeMap<Tick, K>, 124 next: Tick, 125 total_weight: u64, 126} 127 128type Weigh<V> = Arc<dyn Fn(&V) -> Weight + Send + Sync>; 129 130pub struct Lru<K, V, C: Clock = Untimed> { 131 inner: Mutex<LruInner<K, V>>, 132 max_entries: Option<EntryCount>, 133 max_weight: Option<Weight>, 134 weigh: Option<Weigh<V>>, 135 ttl: Option<Duration>, 136 clock: C, 137} 138 139impl<K, V> Lru<K, V, Untimed> 140where 141 K: Hash + Eq + Clone, 142 V: Clone, 143{ 144 pub fn by_count(max_entries: EntryCount) -> Self { 145 Self::build(Some(max_entries), None, None, None, Untimed) 146 } 147 148 pub fn by_weight<F>(max_weight: Weight, weigh: F) -> Self 149 where 150 F: Fn(&V) -> Weight + Send + Sync + 'static, 151 { 152 Self::build(None, Some(max_weight), Some(Arc::new(weigh)), None, Untimed) 153 } 154} 155 156impl<K, V, C> Lru<K, V, C> 157where 158 K: Hash + Eq + Clone, 159 V: Clone, 160 C: Clock, 161{ 162 pub fn by_count_with_ttl(max_entries: EntryCount, ttl: Duration, clock: C) -> Self { 163 Self::build(Some(max_entries), None, None, Some(ttl), clock) 164 } 165 166 pub fn by_weight_with_ttl<F>(max_weight: Weight, ttl: Duration, clock: C, weigh: F) -> Self 167 where 168 F: Fn(&V) -> Weight + Send + Sync + 'static, 169 { 170 Self::build( 171 None, 172 Some(max_weight), 173 Some(Arc::new(weigh)), 174 Some(ttl), 175 clock, 176 ) 177 } 178 179 pub fn with_entry_cap(mut self, max_entries: EntryCount) -> Self { 180 self.max_entries = Some(max_entries); 181 self 182 } 183 184 fn build( 185 max_entries: Option<EntryCount>, 186 max_weight: Option<Weight>, 187 weigh: Option<Weigh<V>>, 188 ttl: Option<Duration>, 189 clock: C, 190 ) -> Self { 191 Self { 192 inner: Mutex::new(LruInner { 193 by_key: HashMap::new(), 194 order: BTreeMap::new(), 195 next: Tick(0), 196 total_weight: 0, 197 }), 198 max_entries, 199 max_weight, 200 weigh, 201 ttl, 202 clock, 203 } 204 } 205 206 fn weight_of(&self, value: &V) -> u64 { 207 self.weigh.as_ref().map_or(1, |weigh| weigh(value).get()) 208 } 209 210 fn lock(&self) -> std::sync::MutexGuard<'_, LruInner<K, V>> { 211 self.inner 212 .lock() 213 .unwrap_or_else(|poisoned| poisoned.into_inner()) 214 } 215} 216 217impl<K, V, C> Cache<K, V> for Lru<K, V, C> 218where 219 K: Hash + Eq + Clone + Send + Sync + 'static, 220 V: Clone + Send + Sync + 'static, 221 C: Clock, 222{ 223 fn get(&self, key: &K) -> Option<V> { 224 let mut guard = self.lock(); 225 let inner = &mut *guard; 226 let (stale, expires_at) = inner 227 .by_key 228 .get(key) 229 .map(|node| (node.tick, node.expires_at))?; 230 if expires_at.is_some_and(|at| at.get() <= self.clock.now_unix_micros().get()) { 231 if let Some(node) = inner.by_key.remove(key) { 232 inner.total_weight = inner.total_weight.saturating_sub(node.weight); 233 } 234 inner.order.remove(&stale); 235 return None; 236 } 237 let fresh = inner.next.issue(); 238 inner.order.remove(&stale); 239 inner.order.insert(fresh, key.clone()); 240 let node = inner.by_key.get_mut(key).expect("hit is still present"); 241 node.tick = fresh; 242 Some(node.value.clone()) 243 } 244 245 fn insert(&self, key: K, value: V) { 246 let expires_at = self.ttl.map(|ttl| { 247 UnixMicros::new( 248 self.clock 249 .now_unix_micros() 250 .get() 251 .saturating_add(ttl.as_micros() as u64), 252 ) 253 }); 254 let weight = self.weight_of(&value); 255 let mut guard = self.lock(); 256 let inner = &mut *guard; 257 if let Some(previous) = inner.by_key.remove(&key) { 258 inner.order.remove(&previous.tick); 259 inner.total_weight = inner.total_weight.saturating_sub(previous.weight); 260 } 261 let fresh = inner.next.issue(); 262 inner.order.insert(fresh, key.clone()); 263 inner.total_weight = inner.total_weight.saturating_add(weight); 264 inner.by_key.insert( 265 key, 266 Node { 267 value, 268 tick: fresh, 269 weight, 270 expires_at, 271 }, 272 ); 273 while self 274 .max_entries 275 .is_some_and(|max| inner.by_key.len() as u64 > max.get()) 276 || self 277 .max_weight 278 .is_some_and(|max| inner.total_weight > max.get()) 279 { 280 let Some((_, evicted)) = inner.order.pop_first() else { 281 break; 282 }; 283 if let Some(node) = inner.by_key.remove(&evicted) { 284 inner.total_weight = inner.total_weight.saturating_sub(node.weight); 285 } 286 } 287 } 288 289 fn invalidate(&self, key: &K) { 290 let mut guard = self.lock(); 291 if let Some(node) = guard.by_key.remove(key) { 292 guard.order.remove(&node.tick); 293 guard.total_weight = guard.total_weight.saturating_sub(node.weight); 294 } 295 } 296 297 fn invalidate_all(&self) { 298 let mut guard = self.lock(); 299 guard.by_key.clear(); 300 guard.order.clear(); 301 guard.total_weight = 0; 302 } 303 304 fn entry_count(&self) -> EntryCount { 305 EntryCount::new(self.lock().by_key.len() as u64) 306 } 307 308 fn weighted_size(&self) -> Weight { 309 Weight::new(self.lock().total_weight) 310 } 311} 312 313pub struct Noop; 314 315impl<K, V> Cache<K, V> for Noop 316where 317 K: Send + Sync + 'static, 318 V: Send + Sync + 'static, 319{ 320 fn get(&self, _key: &K) -> Option<V> { 321 None 322 } 323 324 fn insert(&self, _key: K, _value: V) {} 325 326 fn invalidate(&self, _key: &K) {} 327 328 fn invalidate_all(&self) {} 329 330 fn entry_count(&self) -> EntryCount { 331 EntryCount::new(0) 332 } 333 334 fn weighted_size(&self) -> Weight { 335 Weight::new(0) 336 } 337} 338 339pub struct Filled<V> { 340 pub value: V, 341 pub fresh: bool, 342} 343 344type DeterministicHasher = std::hash::BuildHasherDefault<std::collections::hash_map::DefaultHasher>; 345 346pub trait AsyncCache<K, V>: Send + Sync { 347 fn get(&self, key: &K) -> impl Future<Output = Option<V>> + Send; 348 fn entry_count(&self) -> EntryCount; 349} 350 351pub struct MokaFuture<K, V> { 352 inner: moka::future::Cache<K, V, DeterministicHasher>, 353} 354 355impl<K, V> MokaFuture<K, V> 356where 357 K: Hash + Eq + Send + Sync + 'static, 358 V: Clone + Send + Sync + 'static, 359{ 360 pub fn by_count(max_entries: EntryCount) -> Self { 361 Self { 362 inner: moka::future::Cache::builder() 363 .max_capacity(max_entries.get()) 364 .build_with_hasher(DeterministicHasher::default()), 365 } 366 } 367 368 pub async fn get_or_fill_if<Fut, P>(&self, key: K, refill_if: P, fill: Fut) -> Filled<V> 369 where 370 Fut: Future<Output = V> + Send, 371 P: FnMut(&V) -> bool + Send, 372 { 373 let entry = self 374 .inner 375 .entry(key) 376 .or_insert_with_if(fill, refill_if) 377 .await; 378 Filled { 379 fresh: entry.is_fresh(), 380 value: entry.into_value(), 381 } 382 } 383 384 pub async fn run_pending_tasks(&self) { 385 self.inner.run_pending_tasks().await; 386 } 387} 388 389impl<K, V> AsyncCache<K, V> for MokaFuture<K, V> 390where 391 K: Hash + Eq + Send + Sync + 'static, 392 V: Clone + Send + Sync + 'static, 393{ 394 fn get(&self, key: &K) -> impl Future<Output = Option<V>> + Send { 395 self.inner.get(key) 396 } 397 398 fn entry_count(&self) -> EntryCount { 399 EntryCount::new(self.inner.entry_count()) 400 } 401} 402 403pub trait Reclaimable: Send + Sync { 404 fn footprint(&self) -> Weight; 405 fn reclaim(&self); 406} 407 408impl<K, V> Reclaimable for Moka<K, V> 409where 410 K: Hash + Eq + Send + Sync + 'static, 411 V: Clone + Send + Sync + 'static, 412{ 413 fn footprint(&self) -> Weight { 414 self.weighted_size() 415 } 416 417 fn reclaim(&self) { 418 self.invalidate_all(); 419 } 420} 421 422impl<K, V, C> Reclaimable for Lru<K, V, C> 423where 424 K: Hash + Eq + Clone + Send + Sync + 'static, 425 V: Clone + Send + Sync + 'static, 426 C: Clock, 427{ 428 fn footprint(&self) -> Weight { 429 self.weighted_size() 430 } 431 432 fn reclaim(&self) { 433 self.invalidate_all(); 434 } 435} 436 437#[derive(Default)] 438struct Registry { 439 caches: Mutex<Vec<Weak<dyn Reclaimable>>>, 440} 441 442impl Registry { 443 fn lock(&self) -> std::sync::MutexGuard<'_, Vec<Weak<dyn Reclaimable>>> { 444 self.caches 445 .lock() 446 .unwrap_or_else(|poisoned| poisoned.into_inner()) 447 } 448 449 fn register<T: Reclaimable + 'static>(&self, cache: &Arc<T>) { 450 let erased: Arc<dyn Reclaimable> = cache.clone(); 451 let weak = Arc::downgrade(&erased); 452 let mut caches = self.lock(); 453 caches.retain(|entry| entry.strong_count() > 0); 454 caches.push(weak); 455 } 456 457 fn reclaim_largest(&self) -> Option<Weight> { 458 let largest = self 459 .lock() 460 .iter() 461 .filter_map(Weak::upgrade) 462 .max_by_key(|cache| cache.footprint().get())?; 463 let freed = largest.footprint(); 464 (freed.get() > 0).then(|| { 465 largest.reclaim(); 466 freed 467 }) 468 } 469} 470 471fn global_registry() -> &'static Registry { 472 static REGISTRY: OnceLock<Registry> = OnceLock::new(); 473 REGISTRY.get_or_init(Registry::default) 474} 475 476pub fn register<T: Reclaimable + 'static>(cache: &Arc<T>) { 477 global_registry().register(cache); 478} 479 480pub fn reclaim_largest() -> Option<Weight> { 481 global_registry().reclaim_largest() 482} 483 484#[cfg(test)] 485mod tests { 486 use std::sync::Arc; 487 488 use knot_runtime::ManualClock; 489 490 use super::*; 491 492 #[test] 493 fn a_no_ttl_cache_retains_until_invalidated() { 494 let cache: Moka<u32, u32> = Moka::by_count(EntryCount::new(16)); 495 cache.insert(1, 9); 496 cache.insert(2, 10); 497 assert_eq!(cache.get(&1), Some(9)); 498 cache.invalidate(&1); 499 assert_eq!(cache.get(&1), None); 500 assert_eq!(cache.get(&2), Some(10)); 501 } 502 503 #[test] 504 fn the_same_clock_sequence_yields_the_same_hits_and_misses() { 505 let run = || { 506 let clock = Arc::new(ManualClock::new(UnixMicros::new(0))); 507 let cache: Lru<u32, u32, _> = Lru::by_count_with_ttl( 508 EntryCount::new(16), 509 Duration::from_secs(5), 510 Arc::clone(&clock), 511 ); 512 cache.insert(1, 100); 513 let before = cache.get(&1); 514 clock.advance(Duration::from_secs(6)); 515 let after = cache.get(&1); 516 (before, after) 517 }; 518 assert_eq!(run(), run()); 519 } 520 521 #[test] 522 fn get_or_try_insert_with_serves_the_first_value_without_recomputing() { 523 let cache: Moka<u32, u32> = Moka::by_count(EntryCount::new(16)); 524 let first = cache.get_or_try_insert_with(1, || Ok::<u32, ()>(7)); 525 assert_eq!(first.unwrap(), 7); 526 let second = cache.get_or_try_insert_with(1, || Ok::<u32, ()>(99)); 527 assert_eq!(second.unwrap(), 7); 528 } 529 530 #[test] 531 fn get_or_try_insert_with_propagates_the_error_and_stores_nothing() { 532 let cache: Moka<u32, u32> = Moka::by_count(EntryCount::new(16)); 533 let failed = cache.get_or_try_insert_with(1, || Err::<u32, u32>(9)); 534 assert_eq!(*failed.unwrap_err(), 9); 535 assert_eq!(cache.get(&1), None); 536 } 537 538 #[test] 539 fn the_lru_evicts_the_least_recently_used_key() { 540 let cache: Lru<u32, u32> = Lru::by_count(EntryCount::new(3)); 541 cache.insert(0, 0); 542 cache.insert(1, 1); 543 cache.insert(2, 2); 544 assert_eq!(cache.get(&0), Some(0)); 545 cache.insert(3, 3); 546 assert_eq!( 547 cache.get(&1), 548 None, 549 "the least recently used key is evicted" 550 ); 551 assert_eq!(cache.get(&0), Some(0), "the touched key survives"); 552 assert_eq!(cache.get(&3), Some(3)); 553 assert_eq!(cache.entry_count().get(), 3); 554 } 555 556 #[test] 557 fn the_weighted_lru_evicts_oldest_until_it_fits_the_byte_budget() { 558 let cache: Lru<u32, Vec<u8>> = Lru::by_weight(Weight::new(8), |value: &Vec<u8>| { 559 Weight::new(value.len() as u64) 560 }); 561 cache.insert(0, vec![0u8; 5]); 562 cache.insert(1, vec![0u8; 5]); 563 assert_eq!(cache.get(&0), None, "the oldest entry is evicted to fit"); 564 assert_eq!(cache.get(&1), Some(vec![0u8; 5])); 565 assert_eq!(cache.weighted_size().get(), 5); 566 } 567 568 #[test] 569 fn the_entry_cap_bounds_a_flood_of_zero_weight_entries() { 570 let cache: Lru<u32, Vec<u8>> = Lru::by_weight(Weight::new(1_000_000), |value: &Vec<u8>| { 571 Weight::new(value.len() as u64) 572 }) 573 .with_entry_cap(EntryCount::new(4)); 574 (0..64).for_each(|nonce| cache.insert(nonce, Vec::new())); 575 assert!(cache.entry_count().get() <= 4); 576 } 577 578 #[test] 579 fn the_lru_expires_entries_on_the_injected_clock() { 580 let clock = Arc::new(ManualClock::new(UnixMicros::new(0))); 581 let cache: Lru<u32, u32, _> = Lru::by_count_with_ttl( 582 EntryCount::new(8), 583 Duration::from_secs(1), 584 Arc::clone(&clock), 585 ); 586 cache.insert(1, 5); 587 assert_eq!(cache.get(&1), Some(5)); 588 clock.advance(Duration::from_secs(2)); 589 assert_eq!(cache.get(&1), None); 590 assert_eq!(cache.entry_count().get(), 0); 591 } 592 593 #[test] 594 fn the_governor_sheds_the_largest_registered_cache() { 595 let weigh = |value: &Vec<u8>| Weight::new(value.len() as u64); 596 let small: Arc<Lru<u32, Vec<u8>>> = Arc::new(Lru::by_weight(Weight::new(1_000_000), weigh)); 597 let big: Arc<Lru<u32, Vec<u8>>> = Arc::new(Lru::by_weight(Weight::new(1_000_000), weigh)); 598 small.insert(0, vec![0u8; 10]); 599 big.insert(0, vec![0u8; 100]); 600 let registry = Registry::default(); 601 registry.register(&small); 602 registry.register(&big); 603 let freed = registry 604 .reclaim_largest() 605 .expect("a registered cache is shed"); 606 assert_eq!(freed.get(), 100, "the largest footprint is reclaimed"); 607 assert_eq!(big.entry_count().get(), 0, "the largest cache is emptied"); 608 assert_eq!( 609 small.entry_count().get(), 610 1, 611 "the smaller cache is untouched" 612 ); 613 } 614 615 #[test] 616 fn a_registered_cache_with_nothing_to_reclaim_is_not_shed() { 617 let registry = Registry::default(); 618 let cache: Arc<Lru<u32, Vec<u8>>> = Arc::new(Lru::by_count(EntryCount::new(8))); 619 registry.register(&cache); 620 assert_eq!( 621 registry.reclaim_largest(), 622 None, 623 "an empty registered cache reports nothing to shed" 624 ); 625 } 626 627 #[test] 628 fn the_noop_cache_never_retains() { 629 let cache: &dyn Cache<u32, u32> = &Noop; 630 cache.insert(1, 2); 631 assert_eq!(cache.get(&1), None); 632 assert_eq!(cache.entry_count().get(), 0); 633 } 634}