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

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

Generated by: LCOV version 2.1-beta