1use std::borrow::Borrow;
16use std::hash::Hash;
17use std::sync::Arc;
18
19use crate::metrics::{CacheMetrics, CacheSizeMetric, CacheSizeRegistration};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum RemovalCause {
24 Expired,
26 Explicit,
28 Replaced,
30 Size,
32}
33
34impl RemovalCause {
35 pub fn as_str(self) -> &'static str {
37 match self {
38 Self::Expired => "expired",
39 Self::Explicit => "explicit",
40 Self::Replaced => "replaced",
41 Self::Size => "size",
42 }
43 }
44}
45
46impl From<moka::notification::RemovalCause> for RemovalCause {
47 fn from(value: moka::notification::RemovalCause) -> Self {
48 match value {
49 moka::notification::RemovalCause::Expired => Self::Expired,
50 moka::notification::RemovalCause::Explicit => Self::Explicit,
51 moka::notification::RemovalCause::Replaced => Self::Replaced,
52 moka::notification::RemovalCause::Size => Self::Size,
53 }
54 }
55}
56
57#[derive(Clone)]
61pub struct Cache<K, V> {
62 inner: moka::future::Cache<K, V>,
63 capacity: u64,
64 size_registration: Option<CacheSizeRegistration>,
65 metrics: Option<Arc<CacheMetrics>>,
66}
67
68impl<K, V> Cache<K, V>
69where
70 K: Eq + Hash + Send + Sync + 'static,
71 V: Clone + Send + Sync + 'static,
72{
73 pub fn new<W, L>(name: &str, capacity: u64, weigher: W, listener: L) -> Self
75 where
76 W: Fn(&K, &V) -> u32 + Send + Sync + 'static,
77 L: Fn(Arc<K>, V, RemovalCause) + Send + Sync + 'static,
78 {
79 Self {
80 inner: moka::future::Cache::builder()
81 .name(name)
82 .max_capacity(capacity)
83 .support_invalidation_closures()
84 .weigher(weigher)
85 .eviction_listener(move |key, value, cause| listener(key, value, cause.into()))
86 .build(),
87 capacity,
88 size_registration: None,
89 metrics: None,
90 }
91 }
92
93 pub fn with_metrics(mut self, metrics: Arc<CacheMetrics>) -> Self {
95 self.metrics = Some(metrics);
96 self
97 }
98
99 pub fn with_size_metric(mut self, metric: CacheSizeMetric) -> Self {
101 let cache = self.inner.clone();
102 self.size_registration = Some(metric.register(move || cache.weighted_size()));
103 self
104 }
105
106 pub fn get<'a, Q>(&'a self, key: &'a Q) -> impl Future<Output = Option<V>> + Send + 'a
108 where
109 K: Borrow<Q>,
110 Q: Hash + Eq + Sync + ?Sized,
111 {
112 self.inner.get(key)
113 }
114
115 pub async fn try_get_with<Q, F, E>(&self, key: &Q, load: F) -> Result<V, Arc<E>>
119 where
120 K: Borrow<Q>,
121 Q: ToOwned<Owned = K> + Hash + Eq + ?Sized,
122 F: Future<Output = Result<V, E>>,
123 E: Send + Sync + 'static,
124 {
125 let request = self
126 .metrics
127 .as_ref()
128 .map(|metrics| metrics.requests.start());
129 let result = self
130 .inner
131 .try_get_with_by_ref(key, async {
132 let operation = self.metrics.as_ref().map(|metrics| metrics.loads.start());
134 let result = load.await;
135 if let Some(operation) = operation {
136 operation.finish(&result);
137 }
138 result
139 })
140 .await;
141 if let Some(request) = request {
142 request.finish(&result);
143 }
144 result
145 }
146
147 pub async fn insert(&self, key: K, value: V) {
149 self.inner.insert(key, value).await;
150 }
151
152 pub async fn invalidate<Q>(&self, key: &Q)
154 where
155 K: Borrow<Q>,
156 Q: Hash + Eq + ?Sized,
157 {
158 self.inner.invalidate(key).await;
159 }
160
161 pub fn invalidate_if<F>(&self, predicate: F) -> std::io::Result<()>
164 where
165 F: Fn(&K, &V) -> bool + Send + Sync + 'static,
166 {
167 self.inner
168 .invalidate_entries_if(predicate)
169 .map(|_| ())
170 .map_err(std::io::Error::other)
171 }
172
173 pub fn capacity(&self) -> u64 {
175 self.capacity
176 }
177
178 pub fn entry_count(&self) -> u64 {
180 self.inner.entry_count()
181 }
182
183 pub fn weighted_size(&self) -> u64 {
185 self.inner.weighted_size()
186 }
187
188 pub async fn run_pending_tasks(&self) {
190 self.inner.run_pending_tasks().await;
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use std::sync::atomic::{AtomicUsize, Ordering};
197
198 use tokio::sync::Notify;
199
200 use super::*;
201
202 fn send_lookup<'a, K, V, Q>(
204 cache: &'a Cache<K, V>,
205 key: &'a Q,
206 ) -> impl Future<Output = Option<V>> + Send + 'a
207 where
208 K: Borrow<Q> + Eq + Hash + Send + Sync + 'static,
209 V: Clone + Send + Sync + 'static,
210 Q: Hash + Eq + Sync + ?Sized,
211 {
212 cache.get(key)
213 }
214
215 #[tokio::test]
216 async fn test_borrowed_lookup_has_a_send_future() {
217 let cache = cache(1024);
218 cache
219 .insert("key".to_string(), Arc::new("value".to_string()))
220 .await;
221 let future: futures::future::BoxFuture<'_, Option<Arc<String>>> =
222 Box::pin(send_lookup(&cache, "key"));
223 assert_eq!(future.await.as_deref().map(String::as_str), Some("value"));
224 }
225
226 #[tokio::test]
227 async fn test_predicate_invalidation_preserves_other_entries() {
228 let cache = cache(1024);
229 cache
230 .insert("old".to_string(), Arc::new("a".to_string()))
231 .await;
232 cache
233 .insert("keep".to_string(), Arc::new("b".to_string()))
234 .await;
235 cache.run_pending_tasks().await;
236 cache.invalidate_if(|key, _| key == "old").unwrap();
237 assert!(cache.get("old").await.is_none());
238 assert_eq!(
239 cache.get("keep").await.as_deref().map(String::as_str),
240 Some("b")
241 );
242 }
243
244 fn cache(capacity: u64) -> Cache<String, Arc<String>> {
245 Cache::new(
246 "test",
247 capacity,
248 |_, value: &Arc<String>| value.len() as u32,
249 |_, _, _| {},
250 )
251 }
252
253 fn operation_metrics(name: &str) -> crate::metrics::OperationMetrics {
254 use prometheus::{Histogram, HistogramOpts, IntCounter, IntGauge};
255
256 crate::metrics::OperationMetrics {
257 started: IntCounter::new(format!("{name}_started"), "test").unwrap(),
258 succeeded: IntCounter::new(format!("{name}_succeeded"), "test").unwrap(),
259 failed: IntCounter::new(format!("{name}_failed"), "test").unwrap(),
260 cancelled: IntCounter::new(format!("{name}_cancelled"), "test").unwrap(),
261 active: IntGauge::new(format!("{name}_active"), "test").unwrap(),
262 elapsed: Histogram::with_opts(HistogramOpts::new(format!("{name}_seconds"), "test"))
263 .unwrap(),
264 }
265 }
266
267 fn instrumented_cache() -> (Cache<String, Arc<String>>, Arc<CacheMetrics>) {
268 let metrics = Arc::new(CacheMetrics {
269 requests: operation_metrics("requests"),
270 loads: operation_metrics("loads"),
271 });
272 (cache(1024).with_metrics(metrics.clone()), metrics)
273 }
274
275 #[tokio::test]
276 async fn test_metrics_distinguish_requests_from_executed_loads() {
277 let (cache, metrics) = instrumented_cache();
278 drop(cache.try_get_with("unused", async { Ok::<_, ()>(Arc::new(String::new())) }));
280 assert_eq!(0, metrics.requests.started.get());
281 let gate = Notify::new();
282 let mut first = Box::pin(cache.try_get_with("key", async {
283 gate.notified().await;
284 Ok::<_, ()>(Arc::new("value".to_string()))
285 }));
286 let mut second = Box::pin(cache.try_get_with("key", async { Err::<Arc<String>, _>(()) }));
287 assert!(futures::poll!(&mut first).is_pending());
288 assert!(futures::poll!(&mut second).is_pending());
289 assert_eq!(2, metrics.requests.active.get());
290 assert_eq!(1, metrics.loads.active.get());
291 gate.notify_waiters();
292 let (first, second) = tokio::join!(first, second);
293 assert!(Arc::ptr_eq(&first.unwrap(), &second.unwrap()));
294 cache
295 .try_get_with("key", async { Err::<Arc<String>, _>(()) })
296 .await
297 .unwrap();
298 assert_eq!(3, metrics.requests.started.get());
299 assert_eq!(3, metrics.requests.succeeded.get());
300 assert_eq!(1, metrics.loads.started.get());
301 assert_eq!(1, metrics.loads.succeeded.get());
302 assert_eq!(0, metrics.requests.active.get());
303 assert_eq!(0, metrics.loads.active.get());
304 assert_eq!(3, metrics.requests.elapsed.get_sample_count());
305 assert_eq!(1, metrics.loads.elapsed.get_sample_count());
306 assert_eq!(0, metrics.requests.failed.get());
307 assert_eq!(0, metrics.requests.cancelled.get());
308 }
309
310 #[tokio::test]
311 async fn test_metrics_release_active_work_on_cancellation_and_error() {
312 let (cache, metrics) = instrumented_cache();
313 let mut producer =
314 Box::pin(cache.try_get_with("key", std::future::pending::<Result<Arc<String>, ()>>()));
315 let mut waiter =
316 Box::pin(cache.try_get_with("key", std::future::pending::<Result<Arc<String>, ()>>()));
317 assert!(futures::poll!(&mut producer).is_pending());
318 assert!(futures::poll!(&mut waiter).is_pending());
319 drop(waiter);
320 assert_eq!(1, metrics.requests.cancelled.get());
321 assert_eq!(0, metrics.loads.cancelled.get());
322 drop(producer);
323 assert_eq!(2, metrics.requests.cancelled.get());
324 assert_eq!(1, metrics.loads.cancelled.get());
325 let result = cache
326 .try_get_with("key", async { Err::<Arc<String>, _>(()) })
327 .await;
328 assert!(result.is_err());
329 assert_eq!(1, metrics.requests.failed.get());
330 assert_eq!(1, metrics.loads.failed.get());
331 assert_eq!(0, metrics.requests.active.get());
332 assert_eq!(0, metrics.loads.active.get());
333 assert_eq!(3, metrics.requests.elapsed.get_sample_count());
334 assert_eq!(2, metrics.loads.elapsed.get_sample_count());
335 }
336
337 #[tokio::test]
338 async fn test_instrumented_loading_future_is_send() {
339 fn assert_send<T: Send>(value: T) -> T {
340 value
341 }
342
343 let (cache, _) = instrumented_cache();
344 assert_send(
345 cache.try_get_with("key", async { Ok::<_, ()>(Arc::new("value".to_string())) }),
346 )
347 .await
348 .unwrap();
349 }
350
351 #[tokio::test]
352 async fn test_shares_load_and_bypasses_loader_on_hit() {
353 let cache = cache(1024);
354 let calls = AtomicUsize::new(0);
355 let gate = Notify::new();
356 let load = || async {
357 calls.fetch_add(1, Ordering::Relaxed);
358 gate.notified().await;
359 Ok::<_, ()>(Arc::new("value".to_string()))
360 };
361 let mut first = Box::pin(cache.try_get_with("key", load()));
362 let mut second = Box::pin(cache.try_get_with("key", load()));
363 assert!(futures::poll!(&mut first).is_pending());
364 assert!(futures::poll!(&mut second).is_pending());
365 assert_eq!(1, calls.load(Ordering::Relaxed));
366 gate.notify_waiters();
367 let (first, second) = tokio::join!(first, second);
368 let first = first.unwrap();
369 assert!(Arc::ptr_eq(&first, &second.unwrap()));
370 let hit = cache
371 .try_get_with("key", async { Err::<Arc<String>, _>(()) })
372 .await
373 .unwrap();
374 assert!(Arc::ptr_eq(&first, &hit));
375 }
376
377 #[tokio::test]
378 async fn test_remaining_waiter_loads_after_producer_cancellation() {
379 let (cache, metrics) = instrumented_cache();
380 let mut first =
381 Box::pin(cache.try_get_with("key", std::future::pending::<Result<Arc<String>, ()>>()));
382 assert!(futures::poll!(&mut first).is_pending());
383 let mut second = Box::pin(
384 cache.try_get_with("key", async { Ok::<_, ()>(Arc::new("retry".to_string())) }),
385 );
386 assert!(futures::poll!(&mut second).is_pending());
387 drop(first);
388 assert_eq!("retry", second.await.unwrap().as_str());
389 assert_eq!(2, metrics.requests.started.get());
390 assert_eq!(1, metrics.requests.cancelled.get());
391 assert_eq!(1, metrics.requests.succeeded.get());
392 assert_eq!(2, metrics.loads.started.get());
393 assert_eq!(1, metrics.loads.cancelled.get());
394 assert_eq!(1, metrics.loads.succeeded.get());
395 assert_eq!(0, metrics.requests.active.get());
396 assert_eq!(0, metrics.loads.active.get());
397 }
398
399 #[tokio::test]
400 async fn test_errors_are_shared_but_not_cached() {
401 let (cache, metrics) = instrumented_cache();
402 let calls = AtomicUsize::new(0);
403 let gate = Notify::new();
404 let load = || async {
405 calls.fetch_add(1, Ordering::Relaxed);
406 gate.notified().await;
407 Err::<Arc<String>, _>("read failed")
408 };
409 let mut first = Box::pin(cache.try_get_with("key", load()));
410 let mut second = Box::pin(cache.try_get_with("key", load()));
411 assert!(futures::poll!(&mut first).is_pending());
412 assert!(futures::poll!(&mut second).is_pending());
413 gate.notify_waiters();
414 let (first, second) = tokio::join!(first, second);
415 assert!(Arc::ptr_eq(&first.unwrap_err(), &second.unwrap_err()));
416 assert_eq!(2, metrics.requests.failed.get());
417 assert_eq!(1, metrics.loads.failed.get());
418 assert_eq!(0, metrics.requests.active.get());
419 assert_eq!(0, metrics.loads.active.get());
420 assert_eq!(1, calls.load(Ordering::Relaxed));
421 assert!(cache.get("key").await.is_none());
422 assert_eq!(
423 "retry",
424 cache
425 .try_get_with("key", async {
426 Ok::<_, &'static str>(Arc::new("retry".to_string()))
427 })
428 .await
429 .unwrap()
430 .as_str()
431 );
432 }
433
434 #[tokio::test]
435 async fn test_sampled_size_sums_instances_and_preserves_manual_bytes() {
436 use crate::metrics::CacheSizeMetrics;
437 use prometheus::Opts;
438 use prometheus::core::Collector;
439
440 let metrics =
441 CacheSizeMetrics::new(Opts::new("test_cache_bytes", "test"), &["type"]).unwrap();
442 let manual = metrics.with_label_values(&["shared"]);
443 manual.set(3);
444 let make_cache = || {
445 Cache::new("sampled", 100, |_: &u64, _: &Arc<String>| 7, |_, _, _| {})
446 .with_size_metric(metrics.bind(&["shared"]))
447 };
448 let a = make_cache();
449 let b = make_cache();
450 let sample = || {
451 metrics.collect()[0].get_metric()[0]
452 .get_gauge()
453 .as_ref()
454 .map_or(0.0, |gauge| gauge.value())
455 };
456 a.insert(1, Arc::new("one".into())).await;
457 b.insert(2, Arc::new("two".into())).await;
458 a.run_pending_tasks().await;
459 b.run_pending_tasks().await;
460 assert_eq!(17.0, sample());
461 assert_eq!(3, manual.get());
462 let consumer = a.get(&1).await.unwrap();
463 a.invalidate(&1).await;
464 a.run_pending_tasks().await;
465 assert_eq!(10.0, sample());
466 drop(consumer);
467 let cloned = b.clone();
468 drop(b);
469 assert_eq!(10.0, sample());
470 drop(cloned);
471 assert_eq!(3.0, sample());
472 }
473}