lychee_lib/cache.rs
1//! Facility for cached asynchronous operations, with operations keyed by a
2//! key type and ensuring mutual exclusion of operations with the same key.
3
4use std::hash::Hash;
5use std::sync::Arc;
6use std::sync::atomic::AtomicUsize;
7use std::sync::atomic::Ordering;
8
9use dashmap::DashMap;
10use dashmap::mapref::entry::Entry;
11use tokio::sync::SetOnce;
12
13/// Cache for asynchronous operations. Each operation is associated with a key,
14/// and operations are cached, deduplicated and mutually exclusive with other
15/// operations on the same key, including in-progress operations.
16pub struct Cache<K, V> {
17 /// Internal map of keys to set-once values.
18 data: DashMap<K, Arc<SetOnce<V>>>,
19 /// Number of cache hits (including hits to in-progress values).
20 pub num_hits: AtomicUsize,
21 /// Number of cache misses.
22 pub num_misses: AtomicUsize,
23}
24
25/// A future returned on cache hits. [`CacheFut::wait`] returns a future which
26/// resolves when the cache value has been computed by another task.
27#[derive(Debug)]
28pub struct CacheFut<V>(Arc<SetOnce<V>>);
29
30impl<T> CacheFut<T> {
31 /// Returns a future which resolves when the cache value is computed
32 /// (by another task). If the value has already been computed and stored,
33 /// the future will be ready immediately.
34 pub async fn wait(&self) -> &T {
35 self.0.wait().await
36 }
37}
38
39/// A value returned on cache misses. The owner of this struct should compute
40/// the value, then call [`CacheSetter::set`] to write the value into the cache.
41///
42/// If this struct is dropped before being written to (including due to panic),
43/// the value will remain empty and associated [`CacheFut`]s will *never resolve*.
44/// This can be avoided by calling [`CacheSetter::with_fallback`] which will
45/// specify a fallback closure in case it is prematurely dropped.
46#[derive(Debug)]
47pub struct CacheSetter<T, Fn: FnOnce() -> T = fn() -> T>(Arc<SetOnce<T>>, Option<Fn>);
48
49impl<T, Fn: FnOnce() -> T> CacheSetter<T, Fn> {
50 /// Constructs a new [`CacheSetter`] writing into the given [`SetOnce`].
51 ///
52 /// By default, no fallback is configured.
53 #[must_use]
54 pub const fn new(arc: Arc<SetOnce<T>>) -> Self {
55 Self(arc, None)
56 }
57
58 /// Returns a new [`CacheSetter`] with the configured fallback closure and
59 /// writing into the same [`SetOnce`].
60 pub fn with_fallback<F: FnOnce() -> T>(mut self, default: F) -> CacheSetter<T, F> {
61 let arc = std::mem::take(&mut self.0);
62 self.1 = None;
63 CacheSetter(arc, Some(default))
64 }
65
66 /// Writes the given value into the cache, consuming this [`CacheSetter`].
67 pub fn set(self, value: T) {
68 let _ = self.0.set(value);
69 }
70
71 /// Returns a new dissociated [`CacheSetter`]. That is, a setter which is
72 /// not backed by any value within the cache. This can be useful to let
73 /// uncacheable entities use the same cache-handling logic.
74 pub fn dissociated() -> Self {
75 Self(Arc::default(), None)
76 }
77}
78
79/// Drop implementation that calls the stored [`CacheSetter::with_fallback`]
80/// closure, if it is configured and no value has been manually stored.
81impl<T, Fn: FnOnce() -> T> Drop for CacheSetter<T, Fn> {
82 fn drop(&mut self) {
83 if let Some(f) = self.1.take()
84 && !self.0.initialized()
85 {
86 let _ = self.0.set(f());
87 }
88 }
89}
90
91impl<K, V> Cache<K, V>
92where
93 K: Hash + Eq,
94{
95 /// Constructs a new empty [`Cache`].
96 #[must_use]
97 pub fn new() -> Self {
98 Self {
99 data: DashMap::new(),
100 num_hits: 0.into(),
101 num_misses: 0.into(),
102 }
103 }
104
105 /// Locks the cache entry with the given key, adding it to the cache if
106 /// it does not already exist. This function returns values which can be
107 /// used to write into or read from the cache.
108 ///
109 /// If this is the first task to lock this entry, [`Ok`] of [`CacheSetter`]
110 /// is returned so the call can compute and store the value. If the value is
111 /// already cached or another task is currently computing the value, [`Err`]
112 /// of [`CacheFut`] is returned which can be used to wait and retrieve the value
113 /// from the cache.
114 ///
115 /// The given key will only be cloned if the cache does not currently have
116 /// an entry for this key.
117 ///
118 /// # Errors
119 /// An [`Err`] means the cache key is already completed or in-progress, as
120 /// described above.
121 pub fn lock_entry(&self, key: &K) -> Result<CacheSetter<V>, CacheFut<V>>
122 where
123 K: Clone,
124 {
125 if let Some(entry) = self.data.get(key) {
126 return Err(CacheFut(entry.value().clone()));
127 }
128
129 match self.data.entry(key.clone()) {
130 Entry::Vacant(vac) => {
131 self.num_misses.fetch_add(1, Ordering::Relaxed);
132 let arc = vac.insert(Arc::default()).value().clone();
133 Ok(CacheSetter::new(arc))
134 }
135 Entry::Occupied(occ) => {
136 self.num_hits.fetch_add(1, Ordering::Relaxed);
137 Err(CacheFut(occ.get().clone()))
138 }
139 }
140 }
141
142 /// Consumes the cache and returns an iterator over the completed key
143 /// and value pairs.
144 ///
145 /// # Panics
146 ///
147 /// This function will panic if there are leftover [`CacheFut`] or [`CacheSetter`]
148 /// references pointing to the current cache.
149 pub fn into_completed_entries(self) -> impl Iterator<Item = (K, V)> {
150 self.data.into_iter().filter_map(|(k, v)| {
151 let cell = Arc::into_inner(v).expect("unresolved CacheFut or CacheSetter values exist");
152 cell.into_inner().map(|x| (k, x))
153 })
154 }
155}
156impl<K, V> Default for Cache<K, V>
157where
158 K: Hash + Eq,
159{
160 fn default() -> Self {
161 Self::new()
162 }
163}
164
165impl<K, V> FromIterator<(K, V)> for Cache<K, V>
166where
167 K: Hash + Eq,
168{
169 fn from_iter<It: IntoIterator<Item = (K, V)>>(iter: It) -> Self {
170 let cache = Self::new();
171 for (k, v) in iter {
172 cache.data.insert(k, Arc::new(v.into()));
173 }
174 cache
175 }
176}
177
178impl<K, V> std::fmt::Debug for Cache<K, V> {
179 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180 f.debug_struct("Cache").finish_non_exhaustive()
181 }
182}