Skip to main content

veilid_tools/async_locks/
async_keyed_cache.rs

1use super::*;
2
3use core::fmt::Debug;
4use core::hash::Hash;
5use hashlink::LruCache;
6
7/// A bounded keyed cache where each key has its own async lock.
8///
9/// [`lookup`](AsyncKeyedCache::lookup) acquires the key's lock and returns an
10/// [`AsyncKeyedCacheEntry`] that holds it for the entry's lifetime. While the entry is
11/// held, `get`/`insert`/`remove`/`take` operate on that key's slot atomically with
12/// respect to other lookups of the *same* key; other keys are never blocked.
13///
14/// This encapsulates the common "lock the key, check the cache, maybe fill it, all
15/// without racing another filler" pattern in one place, so the lock and the cache can't
16/// drift out of sync.
17#[derive(Clone, Debug)]
18pub struct AsyncKeyedCache<K, V>
19where
20    K: Hash + Eq + Clone + Debug,
21    V: Clone,
22{
23    lock_table: AsyncTagLockTable<K>,
24    cache: Arc<Mutex<LruCache<K, V>>>,
25}
26
27impl<K, V> AsyncKeyedCache<K, V>
28where
29    K: Hash + Eq + Clone + Debug,
30    V: Clone,
31{
32    /// Create a new keyed cache holding at most `capacity` entries (LRU eviction).
33    #[must_use]
34    pub fn new(capacity: usize) -> Self {
35        Self {
36            lock_table: AsyncTagLockTable::new(),
37            cache: Arc::new(Mutex::new(LruCache::new(capacity))),
38        }
39    }
40
41    /// Lock `key` and return an entry for operating on its cache slot. The key stays
42    /// locked until the returned entry is dropped; other keys are not blocked.
43    pub async fn lookup(&self, key: K) -> AsyncKeyedCacheEntry<K, V> {
44        let guard = self.lock_table.lock_tag(key.clone()).await;
45        AsyncKeyedCacheEntry {
46            _guard: guard,
47            cache: self.cache.clone(),
48            key,
49        }
50    }
51
52    /// Clear the cache entries if the cache is not in use
53    /// Returns true if the cache was cleared
54    /// Returns false if the cache was not cleared because it is in use
55    #[must_use]
56    pub fn purge(&self) -> bool {
57        let mut cache = self.cache.lock();
58        if !self.lock_table.is_empty() {
59            return false;
60        }
61        cache.clear();
62        true
63    }
64}
65
66/// A locked handle to one key's slot in an [`AsyncKeyedCache`].
67///
68/// Holds the key's lock for its lifetime (released on drop). All accessors operate on
69/// the slot for [`key`](AsyncKeyedCacheEntry::key).
70pub struct AsyncKeyedCacheEntry<K, V>
71where
72    K: Hash + Eq + Clone + Debug,
73    V: Clone,
74{
75    // Held for its lifetime to keep the key locked; released on drop.
76    _guard: AsyncTagLockGuard<K>,
77    cache: Arc<Mutex<LruCache<K, V>>>,
78    key: K,
79}
80
81impl<K, V> AsyncKeyedCacheEntry<K, V>
82where
83    K: Hash + Eq + Clone + Debug,
84    V: Clone,
85{
86    /// The key this entry is locked on.
87    #[must_use]
88    pub fn key(&self) -> &K {
89        &self.key
90    }
91
92    /// Get a clone of the cached value for this key, if present.
93    #[must_use]
94    pub fn get(&self) -> Option<V> {
95        self.cache.lock().get(&self.key).cloned()
96    }
97
98    /// Insert (or replace) the cached value for this key, returning the previous value if any.
99    pub fn insert(&self, value: V) -> Option<V> {
100        self.cache.lock().insert(self.key.clone(), value)
101    }
102
103    /// Remove and return the cached value for this key, if present.
104    pub fn remove(&self) -> Option<V> {
105        self.cache.lock().remove(&self.key)
106    }
107}