Skip to main content

autumn_web/cache/
moka_impl.rs

1//! Default cache backend powered by [moka](https://docs.rs/moka).
2//!
3//! `MokaCache` wraps `moka::sync::Cache` with type-erased values
4//! (`Arc<dyn Any>`) so a single instance can serve all `#[cached]`
5//! functions and the `CacheResponseLayer`.
6
7use std::any::Any;
8use std::sync::Arc;
9use std::time::Duration;
10
11use moka::sync::Cache as SyncCache;
12
13use super::Cache;
14
15/// Lock-free, in-process cache backed by moka.
16///
17/// This is the default `Cache` implementation when the `cache-moka`
18/// feature is enabled. Supports max-capacity (LRU eviction) and
19/// time-to-live expiration.
20///
21/// # Examples
22///
23/// ```rust
24/// use autumn_web::cache::{self, MokaCache};
25///
26/// let c = MokaCache::builder().max_capacity(100).build();
27/// cache::insert(&c, "key", 42_i32);
28/// assert_eq!(cache::get::<i32>(&c, "key"), Some(42));
29/// ```
30#[derive(Clone)]
31pub struct MokaCache {
32    inner: SyncCache<String, Arc<dyn Any + Send + Sync>>,
33}
34
35/// Builder for constructing a [`MokaCache`] with capacity and TTL settings.
36pub struct MokaCacheBuilder {
37    max_capacity: u64,
38    ttl: Option<Duration>,
39}
40
41impl MokaCacheBuilder {
42    /// Set the maximum number of entries (default: 10,000).
43    #[must_use]
44    pub const fn max_capacity(mut self, max: u64) -> Self {
45        self.max_capacity = max;
46        self
47    }
48
49    /// Set the time-to-live for entries. `None` means entries never expire
50    /// based on time (only evicted by capacity pressure).
51    #[must_use]
52    pub const fn ttl(mut self, ttl: Duration) -> Self {
53        self.ttl = Some(ttl);
54        self
55    }
56
57    /// Build the [`MokaCache`].
58    #[must_use]
59    pub fn build(self) -> MokaCache {
60        let mut builder = SyncCache::builder().max_capacity(self.max_capacity);
61        if let Some(ttl) = self.ttl {
62            builder = builder.time_to_live(ttl);
63        }
64        MokaCache {
65            inner: builder.build(),
66        }
67    }
68}
69
70impl MokaCache {
71    /// Start building a new `MokaCache`.
72    #[must_use]
73    pub const fn builder() -> MokaCacheBuilder {
74        MokaCacheBuilder {
75            max_capacity: 10_000,
76            ttl: None,
77        }
78    }
79
80    /// Create a `MokaCache` directly from capacity and optional TTL.
81    ///
82    /// This is the constructor used by `#[cached]` macro-generated code.
83    #[must_use]
84    pub fn new(max_capacity: u64, ttl: Option<Duration>) -> Self {
85        let mut b = Self::builder().max_capacity(max_capacity);
86        if let Some(ttl) = ttl {
87            b = b.ttl(ttl);
88        }
89        b.build()
90    }
91}
92
93impl Cache for MokaCache {
94    fn get_value(&self, key: &str) -> Option<Arc<dyn Any + Send + Sync>> {
95        self.inner.get(key)
96    }
97
98    fn insert_value(&self, key: &str, value: Arc<dyn Any + Send + Sync>) {
99        self.inner.insert(key.to_owned(), value);
100    }
101
102    fn invalidate(&self, key: &str) {
103        self.inner.invalidate(key);
104    }
105
106    fn clear(&self) {
107        self.inner.invalidate_all();
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use crate::cache;
115    use std::thread;
116
117    #[test]
118    fn basic_insert_and_get() {
119        let c = MokaCache::new(100, None);
120        cache::insert(&c, "a", 1_i32);
121        assert_eq!(cache::get::<i32>(&c, "a"), Some(1));
122        assert_eq!(cache::get::<i32>(&c, "b"), None);
123    }
124
125    #[test]
126    fn type_mismatch_returns_none() {
127        let c = MokaCache::new(100, None);
128        cache::insert(&c, "a", 1_i32);
129        assert_eq!(cache::get::<String>(&c, "a"), None);
130    }
131
132    #[test]
133    fn ttl_expiry() {
134        let c = MokaCache::new(100, Some(Duration::from_millis(50)));
135        cache::insert(&c, "a", 1_i32);
136        assert_eq!(cache::get::<i32>(&c, "a"), Some(1));
137        thread::sleep(Duration::from_millis(80));
138        c.inner.run_pending_tasks();
139        assert_eq!(cache::get::<i32>(&c, "a"), None);
140    }
141
142    #[test]
143    fn max_capacity_evicts() {
144        // Moka eviction is async; insert many more entries than capacity
145        // and verify the cache eventually stabilises below the limit.
146        let c = MokaCache::new(10, None);
147        for i in 0..100 {
148            cache::insert(&c, &format!("k{i}"), i);
149            // Periodically flush to give moka a chance to evict.
150            if i % 20 == 0 {
151                c.inner.run_pending_tasks();
152            }
153        }
154        c.inner.run_pending_tasks();
155        let count = (0..100)
156            .filter(|i| cache::get::<i32>(&c, &format!("k{i}")).is_some())
157            .count();
158        // Moka may temporarily overshoot, but should be in the right
159        // ballpark. Allow some slack.
160        assert!(
161            count <= 20,
162            "expected roughly <=10 entries (with slack), got {count}"
163        );
164        assert!(count > 0, "cache should not be empty");
165    }
166
167    #[test]
168    fn clear_removes_all() {
169        let c = MokaCache::new(100, None);
170        cache::insert(&c, "a", 1_i32);
171        cache::insert(&c, "b", 2_i32);
172        c.clear();
173        c.inner.run_pending_tasks();
174        assert_eq!(cache::get::<i32>(&c, "a"), None);
175        assert_eq!(cache::get::<i32>(&c, "b"), None);
176    }
177
178    #[test]
179    fn invalidate_removes_key() {
180        let c = MokaCache::new(100, None);
181        cache::insert(&c, "a", 1_i32);
182        c.invalidate("a");
183        c.inner.run_pending_tasks();
184        assert_eq!(cache::get::<i32>(&c, "a"), None);
185    }
186
187    #[test]
188    fn concurrent_access() {
189        let c = MokaCache::new(1000, None);
190        let handles: Vec<_> = (0_i32..10)
191            .map(|i| {
192                let c = c.clone();
193                thread::spawn(move || {
194                    let key = format!("key-{i}");
195                    cache::insert(&c, &key, i * 10);
196                    (i, cache::get::<i32>(&c, &key))
197                })
198            })
199            .collect();
200
201        for h in handles {
202            let (i, val) = h.join().unwrap();
203            assert_eq!(val, Some(i * 10));
204        }
205    }
206
207    #[test]
208    fn heterogeneous_types() {
209        let c = MokaCache::new(100, None);
210        cache::insert(&c, "int", 42_i32);
211        cache::insert(&c, "string", "hello".to_string());
212        cache::insert(&c, "vec", vec![1_u8, 2, 3]);
213
214        assert_eq!(cache::get::<i32>(&c, "int"), Some(42));
215        assert_eq!(
216            cache::get::<String>(&c, "string"),
217            Some("hello".to_string())
218        );
219        assert_eq!(cache::get::<Vec<u8>>(&c, "vec"), Some(vec![1, 2, 3]));
220    }
221
222    #[test]
223    fn builder_pattern() {
224        let c = MokaCache::builder()
225            .max_capacity(500)
226            .ttl(Duration::from_secs(60))
227            .build();
228        cache::insert(&c, "x", 99_i32);
229        assert_eq!(cache::get::<i32>(&c, "x"), Some(99));
230    }
231}