This repository has no description
18 kB
646 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 fn by_weight<F>(max_weight: Weight, weigh: F) -> Self
369 where
370 F: Fn(&V) -> Weight + Send + Sync + 'static,
371 {
372 Self {
373 inner: moka::future::Cache::builder()
374 .max_capacity(max_weight.get())
375 .weigher(move |_key: &K, value: &V| weigh(value).get().min(u32::MAX as u64) as u32)
376 .build_with_hasher(DeterministicHasher::default()),
377 }
378 }
379
380 pub async fn get_or_fill_if<Fut, P>(&self, key: K, refill_if: P, fill: Fut) -> Filled<V>
381 where
382 Fut: Future<Output = V> + Send,
383 P: FnMut(&V) -> bool + Send,
384 {
385 let entry = self
386 .inner
387 .entry(key)
388 .or_insert_with_if(fill, refill_if)
389 .await;
390 Filled {
391 fresh: entry.is_fresh(),
392 value: entry.into_value(),
393 }
394 }
395
396 pub async fn run_pending_tasks(&self) {
397 self.inner.run_pending_tasks().await;
398 }
399}
400
401impl<K, V> AsyncCache<K, V> for MokaFuture<K, V>
402where
403 K: Hash + Eq + Send + Sync + 'static,
404 V: Clone + Send + Sync + 'static,
405{
406 fn get(&self, key: &K) -> impl Future<Output = Option<V>> + Send {
407 self.inner.get(key)
408 }
409
410 fn entry_count(&self) -> EntryCount {
411 EntryCount::new(self.inner.entry_count())
412 }
413}
414
415pub trait Reclaimable: Send + Sync {
416 fn footprint(&self) -> Weight;
417 fn reclaim(&self);
418}
419
420impl<K, V> Reclaimable for Moka<K, V>
421where
422 K: Hash + Eq + Send + Sync + 'static,
423 V: Clone + Send + Sync + 'static,
424{
425 fn footprint(&self) -> Weight {
426 self.weighted_size()
427 }
428
429 fn reclaim(&self) {
430 self.invalidate_all();
431 }
432}
433
434impl<K, V, C> Reclaimable for Lru<K, V, C>
435where
436 K: Hash + Eq + Clone + Send + Sync + 'static,
437 V: Clone + Send + Sync + 'static,
438 C: Clock,
439{
440 fn footprint(&self) -> Weight {
441 self.weighted_size()
442 }
443
444 fn reclaim(&self) {
445 self.invalidate_all();
446 }
447}
448
449#[derive(Default)]
450struct Registry {
451 caches: Mutex<Vec<Weak<dyn Reclaimable>>>,
452}
453
454impl Registry {
455 fn lock(&self) -> std::sync::MutexGuard<'_, Vec<Weak<dyn Reclaimable>>> {
456 self.caches
457 .lock()
458 .unwrap_or_else(|poisoned| poisoned.into_inner())
459 }
460
461 fn register<T: Reclaimable + 'static>(&self, cache: &Arc<T>) {
462 let erased: Arc<dyn Reclaimable> = cache.clone();
463 let weak = Arc::downgrade(&erased);
464 let mut caches = self.lock();
465 caches.retain(|entry| entry.strong_count() > 0);
466 caches.push(weak);
467 }
468
469 fn reclaim_largest(&self) -> Option<Weight> {
470 let largest = self
471 .lock()
472 .iter()
473 .filter_map(Weak::upgrade)
474 .max_by_key(|cache| cache.footprint().get())?;
475 let freed = largest.footprint();
476 (freed.get() > 0).then(|| {
477 largest.reclaim();
478 freed
479 })
480 }
481}
482
483fn global_registry() -> &'static Registry {
484 static REGISTRY: OnceLock<Registry> = OnceLock::new();
485 REGISTRY.get_or_init(Registry::default)
486}
487
488pub fn register<T: Reclaimable + 'static>(cache: &Arc<T>) {
489 global_registry().register(cache);
490}
491
492pub fn reclaim_largest() -> Option<Weight> {
493 global_registry().reclaim_largest()
494}
495
496#[cfg(test)]
497mod tests {
498 use std::sync::Arc;
499
500 use knot_runtime::ManualClock;
501
502 use super::*;
503
504 #[test]
505 fn a_no_ttl_cache_retains_until_invalidated() {
506 let cache: Moka<u32, u32> = Moka::by_count(EntryCount::new(16));
507 cache.insert(1, 9);
508 cache.insert(2, 10);
509 assert_eq!(cache.get(&1), Some(9));
510 cache.invalidate(&1);
511 assert_eq!(cache.get(&1), None);
512 assert_eq!(cache.get(&2), Some(10));
513 }
514
515 #[test]
516 fn the_same_clock_sequence_yields_the_same_hits_and_misses() {
517 let run = || {
518 let clock = Arc::new(ManualClock::new(UnixMicros::new(0)));
519 let cache: Lru<u32, u32, _> = Lru::by_count_with_ttl(
520 EntryCount::new(16),
521 Duration::from_secs(5),
522 Arc::clone(&clock),
523 );
524 cache.insert(1, 100);
525 let before = cache.get(&1);
526 clock.advance(Duration::from_secs(6));
527 let after = cache.get(&1);
528 (before, after)
529 };
530 assert_eq!(run(), run());
531 }
532
533 #[test]
534 fn get_or_try_insert_with_serves_the_first_value_without_recomputing() {
535 let cache: Moka<u32, u32> = Moka::by_count(EntryCount::new(16));
536 let first = cache.get_or_try_insert_with(1, || Ok::<u32, ()>(7));
537 assert_eq!(first.unwrap(), 7);
538 let second = cache.get_or_try_insert_with(1, || Ok::<u32, ()>(99));
539 assert_eq!(second.unwrap(), 7);
540 }
541
542 #[test]
543 fn get_or_try_insert_with_propagates_the_error_and_stores_nothing() {
544 let cache: Moka<u32, u32> = Moka::by_count(EntryCount::new(16));
545 let failed = cache.get_or_try_insert_with(1, || Err::<u32, u32>(9));
546 assert_eq!(*failed.unwrap_err(), 9);
547 assert_eq!(cache.get(&1), None);
548 }
549
550 #[test]
551 fn the_lru_evicts_the_least_recently_used_key() {
552 let cache: Lru<u32, u32> = Lru::by_count(EntryCount::new(3));
553 cache.insert(0, 0);
554 cache.insert(1, 1);
555 cache.insert(2, 2);
556 assert_eq!(cache.get(&0), Some(0));
557 cache.insert(3, 3);
558 assert_eq!(
559 cache.get(&1),
560 None,
561 "the least recently used key is evicted"
562 );
563 assert_eq!(cache.get(&0), Some(0), "the touched key survives");
564 assert_eq!(cache.get(&3), Some(3));
565 assert_eq!(cache.entry_count().get(), 3);
566 }
567
568 #[test]
569 fn the_weighted_lru_evicts_oldest_until_it_fits_the_byte_budget() {
570 let cache: Lru<u32, Vec<u8>> = Lru::by_weight(Weight::new(8), |value: &Vec<u8>| {
571 Weight::new(value.len() as u64)
572 });
573 cache.insert(0, vec![0u8; 5]);
574 cache.insert(1, vec![0u8; 5]);
575 assert_eq!(cache.get(&0), None, "the oldest entry is evicted to fit");
576 assert_eq!(cache.get(&1), Some(vec![0u8; 5]));
577 assert_eq!(cache.weighted_size().get(), 5);
578 }
579
580 #[test]
581 fn the_entry_cap_bounds_a_flood_of_zero_weight_entries() {
582 let cache: Lru<u32, Vec<u8>> = Lru::by_weight(Weight::new(1_000_000), |value: &Vec<u8>| {
583 Weight::new(value.len() as u64)
584 })
585 .with_entry_cap(EntryCount::new(4));
586 (0..64).for_each(|nonce| cache.insert(nonce, Vec::new()));
587 assert!(cache.entry_count().get() <= 4);
588 }
589
590 #[test]
591 fn the_lru_expires_entries_on_the_injected_clock() {
592 let clock = Arc::new(ManualClock::new(UnixMicros::new(0)));
593 let cache: Lru<u32, u32, _> = Lru::by_count_with_ttl(
594 EntryCount::new(8),
595 Duration::from_secs(1),
596 Arc::clone(&clock),
597 );
598 cache.insert(1, 5);
599 assert_eq!(cache.get(&1), Some(5));
600 clock.advance(Duration::from_secs(2));
601 assert_eq!(cache.get(&1), None);
602 assert_eq!(cache.entry_count().get(), 0);
603 }
604
605 #[test]
606 fn the_governor_sheds_the_largest_registered_cache() {
607 let weigh = |value: &Vec<u8>| Weight::new(value.len() as u64);
608 let small: Arc<Lru<u32, Vec<u8>>> = Arc::new(Lru::by_weight(Weight::new(1_000_000), weigh));
609 let big: Arc<Lru<u32, Vec<u8>>> = Arc::new(Lru::by_weight(Weight::new(1_000_000), weigh));
610 small.insert(0, vec![0u8; 10]);
611 big.insert(0, vec![0u8; 100]);
612 let registry = Registry::default();
613 registry.register(&small);
614 registry.register(&big);
615 let freed = registry
616 .reclaim_largest()
617 .expect("a registered cache is shed");
618 assert_eq!(freed.get(), 100, "the largest footprint is reclaimed");
619 assert_eq!(big.entry_count().get(), 0, "the largest cache is emptied");
620 assert_eq!(
621 small.entry_count().get(),
622 1,
623 "the smaller cache is untouched"
624 );
625 }
626
627 #[test]
628 fn a_registered_cache_with_nothing_to_reclaim_is_not_shed() {
629 let registry = Registry::default();
630 let cache: Arc<Lru<u32, Vec<u8>>> = Arc::new(Lru::by_count(EntryCount::new(8)));
631 registry.register(&cache);
632 assert_eq!(
633 registry.reclaim_largest(),
634 None,
635 "an empty registered cache reports nothing to shed"
636 );
637 }
638
639 #[test]
640 fn the_noop_cache_never_retains() {
641 let cache: &dyn Cache<u32, u32> = &Noop;
642 cache.insert(1, 2);
643 assert_eq!(cache.get(&1), None);
644 assert_eq!(cache.entry_count().get(), 0);
645 }
646}