Skip to main content

mytheclipse_cache/
cache_aside.rs

1//! Cache-aside with read-through (feature `cache-aside`).
2//!
3//! [`CacheAside`] wires a [`Cache`] to a data source: on a miss it invokes a
4//! user-provided async fetcher, stores the result (with an optional TTL), and
5//! returns it. This is the standard cache-aside pattern — reads bypass a cold
6//! cache by falling back to the source of truth.
7
8use std::time::Duration;
9
10use crate::traits::{Cache, CacheError};
11
12/// A generic read-through cache-aside helper.
13///
14/// `F` is the data source: an async closure `(owned key) -> Option<Vec<u8>>`.
15/// The key is passed by value ([`String`]) so the returned future does not
16/// borrow from the caller, which keeps the API simple and `'static`-friendly.
17#[derive(Clone)]
18pub struct CacheAside<C, F> {
19    cache: C,
20    fetcher: F,
21    ttl: Option<Duration>,
22}
23
24impl<C, F, Fut> CacheAside<C, F>
25where
26    C: Cache,
27    F: Fn(String) -> Fut + Send + Sync,
28    Fut: std::future::Future<Output = Option<Vec<u8>>> + Send,
29{
30    /// Builds a cache-aside wrapper around `cache` using `fetcher` to fill
31    /// misses. Entries are stored without expiry unless `with_ttl` is used.
32    pub fn new(cache: C, fetcher: F) -> Self {
33        Self {
34            cache,
35            fetcher,
36            ttl: None,
37        }
38    }
39
40    /// Applies a `ttl` to every entry written by this wrapper.
41    pub fn with_ttl(mut self, ttl: Duration) -> Self {
42        self.ttl = Some(ttl);
43        self
44    }
45
46    /// Returns a value for `key`, reading through to the fetcher on a miss and
47    /// caching the result.
48    pub async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, CacheError> {
49        if let Some(value) = self.cache.get(key).await? {
50            return Ok(Some(value));
51        }
52        if let Some(value) = (self.fetcher)(key.to_string()).await {
53            self.cache.set(key, value.clone(), self.ttl).await?;
54            Ok(Some(value))
55        } else {
56            Ok(None)
57        }
58    }
59
60    /// Explicitly evicts `key`.
61    pub async fn invalidate(&self, key: &str) -> Result<(), CacheError> {
62        self.cache.invalidate(key).await
63    }
64
65    /// Returns a reference to the underlying cache.
66    pub fn cache(&self) -> &C {
67        &self.cache
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use crate::memory::MemoryCache;
75    use std::sync::atomic::{AtomicU64, Ordering};
76    use std::sync::Arc;
77
78    fn fetcher(
79        hits: Arc<AtomicU64>,
80    ) -> impl Fn(String) -> std::future::Ready<Option<Vec<u8>>> + Send + Sync {
81        move |_key: String| {
82            let n = hits.fetch_add(1, Ordering::SeqCst) + 1;
83            std::future::ready(Some(format!("fetched-{n}").into_bytes()))
84        }
85    }
86
87    #[tokio::test]
88    async fn miss_reads_through_and_caches() {
89        let hits = Arc::new(AtomicU64::new(0));
90        let aside = CacheAside::new(MemoryCache::new(), fetcher(hits.clone()));
91
92        let first = aside.get("k").await.unwrap().unwrap();
93        let second = aside.get("k").await.unwrap().unwrap();
94        assert_eq!(first, b"fetched-1");
95        // Cache hit — fetcher not called again.
96        assert_eq!(second, b"fetched-1");
97        assert_eq!(hits.load(Ordering::SeqCst), 1);
98    }
99
100    #[tokio::test]
101    async fn invalidate_forces_refetch() {
102        let hits = Arc::new(AtomicU64::new(0));
103        let aside = CacheAside::new(MemoryCache::new(), fetcher(hits.clone()));
104        let _ = aside.get("k").await.unwrap();
105        aside.invalidate("k").await.unwrap();
106        let again = aside.get("k").await.unwrap().unwrap();
107        assert_eq!(again, b"fetched-2");
108        assert_eq!(hits.load(Ordering::SeqCst), 2);
109    }
110
111    #[tokio::test]
112    async fn ttl_applies_to_writes() {
113        let aside = CacheAside::new(MemoryCache::new(), fetcher(Arc::new(AtomicU64::new(0))))
114            .with_ttl(Duration::from_millis(30));
115        let _ = aside.get("k").await.unwrap();
116        assert_eq!(
117            aside.cache().get("k").await.unwrap(),
118            Some(b"fetched-1".to_vec())
119        );
120        tokio::time::sleep(Duration::from_millis(60)).await;
121        assert_eq!(aside.cache().get("k").await.unwrap(), None);
122    }
123}