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}