LCOV - code coverage report
Current view: top level - proxy/src/cache - timed_lru.rs (source / functions) Coverage Total Hit
Test: a43a77853355b937a79c57b07a8f05607cf29e6c.info Lines: 56.9 % 51 29
Test Date: 2024-09-19 12:04:32 Functions: 54.5 % 11 6

            Line data    Source code
       1              : use std::{
       2              :     borrow::Borrow,
       3              :     hash::Hash,
       4              :     time::{Duration, Instant},
       5              : };
       6              : use tracing::debug;
       7              : 
       8              : // This seems to make more sense than `lru` or `cached`:
       9              : //
      10              : // * `near/nearcore` ditched `cached` in favor of `lru`
      11              : //   (https://github.com/near/nearcore/issues?q=is%3Aissue+lru+is%3Aclosed).
      12              : //
      13              : // * `lru` methods use an obscure `KeyRef` type in their contraints (which is deliberately excluded from docs).
      14              : //   This severely hinders its usage both in terms of creating wrappers and supported key types.
      15              : //
      16              : // On the other hand, `hashlink` has good download stats and appears to be maintained.
      17              : use hashlink::{linked_hash_map::RawEntryMut, LruCache};
      18              : 
      19              : use super::{common::Cached, timed_lru, Cache};
      20              : 
      21              : /// An implementation of timed LRU cache with fixed capacity.
      22              : /// Key properties:
      23              : ///
      24              : /// * Whenever a new entry is inserted, the least recently accessed one is evicted.
      25              : ///   The cache also keeps track of entry's insertion time (`created_at`) and TTL (`expires_at`).
      26              : ///
      27              : /// * If `update_ttl_on_retrieval` is `true`. When the entry is about to be retrieved, we check its expiration timestamp.
      28              : ///   If the entry has expired, we remove it from the cache; Otherwise we bump the
      29              : ///   expiration timestamp (e.g. +5mins) and change its place in LRU list to prolong
      30              : ///   its existence.
      31              : ///
      32              : /// * There's an API for immediate invalidation (removal) of a cache entry;
      33              : ///   It's useful in case we know for sure that the entry is no longer correct.
      34              : ///   See [`timed_lru::LookupInfo`] & [`timed_lru::Cached`] for more information.
      35              : ///
      36              : /// * Expired entries are kept in the cache, until they are evicted by the LRU policy,
      37              : ///   or by a successful lookup (i.e. the entry hasn't expired yet).
      38              : ///   There is no background job to reap the expired records.
      39              : ///
      40              : /// * It's possible for an entry that has not yet expired entry to be evicted
      41              : ///   before expired items. That's a bit wasteful, but probably fine in practice.
      42              : pub(crate) struct TimedLru<K, V> {
      43              :     /// Cache's name for tracing.
      44              :     name: &'static str,
      45              : 
      46              :     /// The underlying cache implementation.
      47              :     cache: parking_lot::Mutex<LruCache<K, Entry<V>>>,
      48              : 
      49              :     /// Default time-to-live of a single entry.
      50              :     ttl: Duration,
      51              : 
      52              :     update_ttl_on_retrieval: bool,
      53              : }
      54              : 
      55              : impl<K: Hash + Eq, V> Cache for TimedLru<K, V> {
      56              :     type Key = K;
      57              :     type Value = V;
      58              :     type LookupInfo<Key> = LookupInfo<Key>;
      59              : 
      60            4 :     fn invalidate(&self, info: &Self::LookupInfo<K>) {
      61            4 :         self.invalidate_raw(info);
      62            4 :     }
      63              : }
      64              : 
      65              : struct Entry<T> {
      66              :     created_at: Instant,
      67              :     expires_at: Instant,
      68              :     ttl: Duration,
      69              :     update_ttl_on_retrieval: bool,
      70              :     value: T,
      71              : }
      72              : 
      73              : impl<K: Hash + Eq, V> TimedLru<K, V> {
      74              :     /// Construct a new LRU cache with timed entries.
      75            7 :     pub(crate) fn new(
      76            7 :         name: &'static str,
      77            7 :         capacity: usize,
      78            7 :         ttl: Duration,
      79            7 :         update_ttl_on_retrieval: bool,
      80            7 :     ) -> Self {
      81            7 :         Self {
      82            7 :             name,
      83            7 :             cache: LruCache::new(capacity).into(),
      84            7 :             ttl,
      85            7 :             update_ttl_on_retrieval,
      86            7 :         }
      87            7 :     }
      88              : 
      89              :     /// Drop an entry from the cache if it's outdated.
      90            4 :     #[tracing::instrument(level = "debug", fields(cache = self.name), skip_all)]
      91              :     fn invalidate_raw(&self, info: &LookupInfo<K>) {
      92              :         let now = Instant::now();
      93              : 
      94              :         // Do costly things before taking the lock.
      95              :         let mut cache = self.cache.lock();
      96              :         let raw_entry = match cache.raw_entry_mut().from_key(&info.key) {
      97              :             RawEntryMut::Vacant(_) => return,
      98              :             RawEntryMut::Occupied(x) => x,
      99              :         };
     100              : 
     101              :         // Remove the entry if it was created prior to lookup timestamp.
     102              :         let entry = raw_entry.get();
     103              :         let (created_at, expires_at) = (entry.created_at, entry.expires_at);
     104              :         let should_remove = created_at <= info.created_at || expires_at <= now;
     105              : 
     106              :         if should_remove {
     107              :             raw_entry.remove();
     108              :         }
     109              : 
     110              :         drop(cache); // drop lock before logging
     111              :         debug!(
     112              :             created_at = format_args!("{created_at:?}"),
     113              :             expires_at = format_args!("{expires_at:?}"),
     114              :             entry_removed = should_remove,
     115              :             "processed a cache entry invalidation event"
     116              :         );
     117              :     }
     118              : 
     119              :     /// Try retrieving an entry by its key, then execute `extract` if it exists.
     120            0 :     #[tracing::instrument(level = "debug", fields(cache = self.name), skip_all)]
     121              :     fn get_raw<Q, R>(&self, key: &Q, extract: impl FnOnce(&K, &Entry<V>) -> R) -> Option<R>
     122              :     where
     123              :         K: Borrow<Q>,
     124              :         Q: Hash + Eq + ?Sized,
     125              :     {
     126              :         let now = Instant::now();
     127              : 
     128              :         // Do costly things before taking the lock.
     129              :         let mut cache = self.cache.lock();
     130              :         let mut raw_entry = match cache.raw_entry_mut().from_key(key) {
     131              :             RawEntryMut::Vacant(_) => return None,
     132              :             RawEntryMut::Occupied(x) => x,
     133              :         };
     134              : 
     135              :         // Immeditely drop the entry if it has expired.
     136              :         let entry = raw_entry.get();
     137              :         if entry.expires_at <= now {
     138              :             raw_entry.remove();
     139              :             return None;
     140              :         }
     141              : 
     142              :         let value = extract(raw_entry.key(), entry);
     143              :         let (created_at, expires_at) = (entry.created_at, entry.expires_at);
     144              : 
     145              :         // Update the deadline and the entry's position in the LRU list.
     146              :         let deadline = now.checked_add(raw_entry.get().ttl).expect("time overflow");
     147              :         if raw_entry.get().update_ttl_on_retrieval {
     148              :             raw_entry.get_mut().expires_at = deadline;
     149              :         }
     150              :         raw_entry.to_back();
     151              : 
     152              :         drop(cache); // drop lock before logging
     153              :         debug!(
     154              :             created_at = format_args!("{created_at:?}"),
     155              :             old_expires_at = format_args!("{expires_at:?}"),
     156              :             new_expires_at = format_args!("{deadline:?}"),
     157              :             "accessed a cache entry"
     158              :         );
     159              : 
     160              :         Some(value)
     161              :     }
     162              : 
     163              :     /// Insert an entry to the cache. If an entry with the same key already
     164              :     /// existed, return the previous value and its creation timestamp.
     165           10 :     #[tracing::instrument(level = "debug", fields(cache = self.name), skip_all)]
     166              :     fn insert_raw(&self, key: K, value: V) -> (Instant, Option<V>) {
     167              :         self.insert_raw_ttl(key, value, self.ttl, self.update_ttl_on_retrieval)
     168              :     }
     169              : 
     170              :     /// Insert an entry to the cache. If an entry with the same key already
     171              :     /// existed, return the previous value and its creation timestamp.
     172           10 :     #[tracing::instrument(level = "debug", fields(cache = self.name), skip_all)]
     173              :     fn insert_raw_ttl(
     174              :         &self,
     175              :         key: K,
     176              :         value: V,
     177              :         ttl: Duration,
     178              :         update: bool,
     179              :     ) -> (Instant, Option<V>) {
     180              :         let created_at = Instant::now();
     181              :         let expires_at = created_at.checked_add(ttl).expect("time overflow");
     182              : 
     183              :         let entry = Entry {
     184              :             created_at,
     185              :             expires_at,
     186              :             ttl,
     187              :             update_ttl_on_retrieval: update,
     188              :             value,
     189              :         };
     190              : 
     191              :         // Do costly things before taking the lock.
     192              :         let old = self
     193              :             .cache
     194              :             .lock()
     195              :             .insert(key, entry)
     196            0 :             .map(|entry| entry.value);
     197              : 
     198              :         debug!(
     199              :             created_at = format_args!("{created_at:?}"),
     200              :             expires_at = format_args!("{expires_at:?}"),
     201              :             replaced = old.is_some(),
     202              :             "created a cache entry"
     203              :         );
     204              : 
     205              :         (created_at, old)
     206              :     }
     207              : }
     208              : 
     209              : impl<K: Hash + Eq + Clone, V: Clone> TimedLru<K, V> {
     210            0 :     pub(crate) fn insert_ttl(&self, key: K, value: V, ttl: Duration) {
     211            0 :         self.insert_raw_ttl(key, value, ttl, false);
     212            0 :     }
     213              : 
     214           10 :     pub(crate) fn insert_unit(&self, key: K, value: V) -> (Option<V>, Cached<&Self, ()>) {
     215           10 :         let (created_at, old) = self.insert_raw(key.clone(), value);
     216           10 : 
     217           10 :         let cached = Cached {
     218           10 :             token: Some((self, LookupInfo { created_at, key })),
     219           10 :             value: (),
     220           10 :         };
     221           10 : 
     222           10 :         (old, cached)
     223           10 :     }
     224              : }
     225              : 
     226              : impl<K: Hash + Eq, V: Clone> TimedLru<K, V> {
     227              :     /// Retrieve a cached entry in convenient wrapper.
     228            0 :     pub(crate) fn get<Q>(&self, key: &Q) -> Option<timed_lru::Cached<&Self>>
     229            0 :     where
     230            0 :         K: Borrow<Q> + Clone,
     231            0 :         Q: Hash + Eq + ?Sized,
     232            0 :     {
     233            0 :         self.get_raw(key, |key, entry| {
     234            0 :             let info = LookupInfo {
     235            0 :                 created_at: entry.created_at,
     236            0 :                 key: key.clone(),
     237            0 :             };
     238            0 : 
     239            0 :             Cached {
     240            0 :                 token: Some((self, info)),
     241            0 :                 value: entry.value.clone(),
     242            0 :             }
     243            0 :         })
     244            0 :     }
     245              : }
     246              : 
     247              : /// Lookup information for key invalidation.
     248              : pub(crate) struct LookupInfo<K> {
     249              :     /// Time of creation of a cache [`Entry`].
     250              :     /// We use this during invalidation lookups to prevent eviction of a newer
     251              :     /// entry sharing the same key (it might've been inserted by a different
     252              :     /// task after we got the entry we're trying to invalidate now).
     253              :     created_at: Instant,
     254              : 
     255              :     /// Search by this key.
     256              :     key: K,
     257              : }
        

Generated by: LCOV version 2.1-beta