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