Skip to main content

autumn_web/cache/
mod.rs

1//! Caching infrastructure for the Autumn framework.
2//!
3//! This module provides:
4//!
5//! - [`Cache`] — a trait abstracting over cache backends (moka by default,
6//!   swap in Redis, memcached, etc.)
7//! - [`MokaCache`] — the default, lock-free, in-process cache powered by
8//!   [moka](https://docs.rs/moka) (behind the `cache-moka` feature)
9//! - [`CacheResponseLayer`] — a Tower middleware that caches HTTP GET
10//!   responses, usable via `#[intercept(CacheResponseLayer::new(...))]`
11//! - [`CacheableResult`] — helper trait used by `#[cached(result)]` to
12//!   only cache `Ok` values
13//!
14//! The `#[cached]` proc macro generates a per-function static `MokaCache`
15//! for function-level memoization. The `CacheResponseLayer` operates at
16//! the HTTP level using a shared `Arc<dyn Cache>`.
17//!
18//! # Swapping backends
19//!
20//! Implement the [`Cache`] trait for your backend:
21//!
22//! ```rust,ignore
23//! use autumn_web::cache::Cache;
24//!
25//! #[derive(Clone)]
26//! struct RedisCache { /* ... */ }
27//!
28//! impl Cache for RedisCache {
29//!     fn get_value(&self, key: &str) -> Option<Box<dyn std::any::Any + Send + Sync>> { /* ... */ }
30//!     fn insert_value(&self, key: &str, value: Box<dyn std::any::Any + Send + Sync>) { /* ... */ }
31//!     fn invalidate(&self, key: &str) { /* ... */ }
32//!     fn clear(&self) { /* ... */ }
33//! }
34//! ```
35
36mod layer;
37#[cfg(feature = "cache-moka")]
38mod moka_impl;
39
40pub use layer::{CacheResponseLayer, CacheResponseService};
41#[cfg(feature = "cache-moka")]
42pub use moka_impl::MokaCache;
43
44use std::any::Any;
45use std::hash::{DefaultHasher, Hash, Hasher};
46use std::sync::{Arc, RwLock};
47
48// ── Global cache registry ────────────────────────────────────────────
49
50/// Process-level shared cache backend.
51///
52/// Set once at startup by [`set_global_cache`]; read by every
53/// `#[cached]`-annotated function to decide which store to use.
54static GLOBAL_CACHE: RwLock<Option<Arc<dyn Cache>>> = RwLock::new(None);
55
56/// Register (or replace) the process-level shared cache.
57///
58/// Called automatically by [`crate::app::AppBuilder`] when
59/// `.with_cache_backend(...)` has been used. Also called by
60/// [`crate::state::AppState::set_cache`] when a plugin installs a backend
61/// during the startup-hook phase.
62///
63/// # Panics
64///
65/// Panics if the internal `RwLock` is poisoned.
66pub fn set_global_cache(cache: Arc<dyn Cache>) {
67    *GLOBAL_CACHE.write().expect("global cache lock poisoned") = Some(cache);
68}
69
70/// Return a clone of the process-level shared cache, if one is registered.
71///
72/// `None` means no global backend has been set and `#[cached]` functions
73/// fall back to their per-function Moka stores.
74///
75/// # Panics
76///
77/// Panics if the internal `RwLock` is poisoned.
78#[must_use]
79pub fn global_cache() -> Option<Arc<dyn Cache>> {
80    GLOBAL_CACHE
81        .read()
82        .expect("global cache lock poisoned")
83        .clone()
84}
85
86/// Remove the process-level shared cache.
87///
88/// Primarily useful in tests that need per-test isolation.
89///
90/// # Panics
91///
92/// Panics if the internal `RwLock` is poisoned.
93pub fn clear_global_cache() {
94    *GLOBAL_CACHE.write().expect("global cache lock poisoned") = None;
95}
96
97// ── Cache trait ──────────────────────────────────────────────────────
98
99/// Raw JSON bytes stored by serializing cache backends (e.g. Redis).
100///
101/// Backends that cannot store `Arc<dyn Any>` directly (because values must
102/// survive across process boundaries) return this from [`Cache::get_value`]
103/// instead. [`get_cached`] and [`insert_cached`] transparently deserialize it
104/// back into the concrete type `V` using `serde_json`.
105#[derive(Clone)]
106pub struct RawCacheBytes(pub Vec<u8>);
107
108/// A type-erased, thread-safe cache store.
109///
110/// Implementations must be `Send + Sync` so they can be shared across
111/// handlers and tasks. Values are stored as `Arc<dyn Any>` for type
112/// erasure, allowing a single cache instance to store heterogeneous
113/// types from different `#[cached]` functions.
114///
115/// Use the free functions [`get`] / [`insert`] for in-process-only values,
116/// or [`get_cached`] / [`insert_cached`] for types that also implement
117/// `serde`, which is required for cross-replica backends like Redis.
118/// [`CacheResponseLayer`] uses the serde-aware path so HTTP response caching
119/// works with both in-process and raw-byte backends.
120pub trait Cache: Send + Sync + 'static {
121    /// Retrieve a type-erased value by key. Returns `None` on miss.
122    ///
123    /// Backends that store serialized data (e.g. Redis) may return
124    /// <code>Arc<[RawCacheBytes]></code> here; [`get_cached`] handles the
125    /// JSON deserialization transparently.
126    fn get_value(&self, key: &str) -> Option<Arc<dyn Any + Send + Sync>>;
127
128    /// Store a type-erased value by key.
129    fn insert_value(&self, key: &str, value: Arc<dyn Any + Send + Sync>);
130
131    /// Remove a specific key.
132    fn invalidate(&self, key: &str);
133
134    /// Remove all entries.
135    fn clear(&self);
136
137    /// Store pre-serialized JSON bytes for backends that persist data across
138    /// process boundaries (e.g. Redis). The default is a no-op; in-process
139    /// backends store values via [`insert_value`] instead.
140    ///
141    /// `ttl` carries the same time-to-live that was declared on the
142    /// `#[cached(ttl = "…")]` attribute so backends can apply native expiry
143    /// (e.g. Redis `SET EX`). `None` means no expiry.
144    ///
145    /// [`insert_value`]: Cache::insert_value
146    fn insert_raw_bytes(&self, _key: &str, _bytes: Vec<u8>, _ttl: Option<std::time::Duration>) {}
147}
148
149// ── Typed convenience functions ──────────────────────────────────────
150
151/// Typed get: retrieve and downcast a cached value.
152///
153/// Returns `None` if the key is absent or the stored type doesn't
154/// match `V`. Works with any `Cache` implementation.
155///
156/// For cross-replica backends (Redis) use [`get_cached`] instead, which
157/// also handles JSON deserialization of [`RawCacheBytes`].
158pub fn get<V: Clone + Send + Sync + 'static>(cache: &dyn Cache, key: &str) -> Option<V> {
159    cache
160        .get_value(key)
161        .and_then(|arc| arc.downcast_ref::<V>().cloned())
162}
163
164/// Typed insert: wrap the value in an `Arc` and store it.
165///
166/// Works with any `Cache` implementation.
167///
168/// For cross-replica backends (Redis) use [`insert_cached`] instead,
169/// which also serializes the value for storage across process boundaries.
170pub fn insert<V: Clone + Send + Sync + 'static>(cache: &dyn Cache, key: &str, value: V) {
171    cache.insert_value(key, Arc::new(value));
172}
173
174/// Serde-aware get: retrieve a cached value, deserializing from JSON if needed.
175///
176/// First tries a direct in-memory downcast (fast path for `MokaCache`). If
177/// that fails — because the backend stored [`RawCacheBytes`] (e.g. Redis) —
178/// the bytes are deserialized with `serde_json`. This is what the `#[cached]`
179/// macro uses so that values survive across replicas when a shared backend
180/// is configured.
181pub fn get_cached<V>(cache: &dyn Cache, key: &str) -> Option<V>
182where
183    V: Clone + serde::de::DeserializeOwned + Send + Sync + 'static,
184{
185    let arc = cache.get_value(key)?;
186    // Fast path: in-memory backend stored the concrete type directly.
187    if let Some(v) = arc.downcast_ref::<V>() {
188        return Some(v.clone());
189    }
190    // Slow path: serializing backend (e.g. Redis) stored RawCacheBytes.
191    arc.downcast_ref::<RawCacheBytes>()
192        .and_then(|raw| serde_json::from_slice::<V>(&raw.0).ok())
193}
194
195/// Serde-aware insert: store the value both in-memory and as JSON bytes.
196///
197/// Calls [`Cache::insert_value`] (for in-process backends like Moka) **and**
198/// [`Cache::insert_raw_bytes`] (for cross-replica backends like Redis). This
199/// is what the `#[cached]` macro uses so that the stored value is accessible
200/// both within the same process and on other replicas.
201///
202/// `ttl` is forwarded verbatim to [`Cache::insert_raw_bytes`] so backends
203/// like Redis can apply a native entry expiry (e.g. `SET EX`). In-process
204/// backends (Moka) manage TTL via the per-function static cache instance
205/// and ignore this parameter.
206pub fn insert_cached<V>(cache: &dyn Cache, key: &str, value: V, ttl: Option<std::time::Duration>)
207where
208    V: Clone + serde::Serialize + Send + Sync + 'static,
209{
210    // In-memory path (MokaCache, CountingCache in tests, …)
211    cache.insert_value(key, Arc::new(value.clone()));
212    // Serialized path (RedisCache, any cross-replica backend)
213    if let Ok(bytes) = serde_json::to_vec(&value) {
214        cache.insert_raw_bytes(key, bytes, ttl);
215    }
216}
217
218// ── CacheableResult trait ────────────────────────────────────────────
219
220/// Helper trait used by `#[cached(result)]` to extract the `Ok` type
221/// from a `Result<T, E>` return type at the type level.
222///
223/// This avoids the need for the proc macro to syntactically parse
224/// generic arguments out of the return type.
225pub trait CacheableResult {
226    /// The success type to cache.
227    type Ok: Clone;
228    /// The error type (passed through uncached).
229    type Err;
230
231    /// Convert into a standard `Result` for pattern matching.
232    ///
233    /// # Errors
234    ///
235    /// Returns `Err` if the original result was an error.
236    fn into_result(self) -> Result<Self::Ok, Self::Err>;
237    /// Wrap a cached `Ok` value back into the original result type.
238    fn from_ok(ok: Self::Ok) -> Self;
239}
240
241impl<T: Clone, E> CacheableResult for Result<T, E> {
242    type Ok = T;
243    type Err = E;
244
245    fn into_result(self) -> Self {
246        self
247    }
248
249    fn from_ok(ok: T) -> Self {
250        Ok(ok)
251    }
252}
253
254// ── Cache key helper ─────────────────────────────────────────────────
255
256/// Build a cache key from a function name and its hashable arguments.
257///
258/// Used by `#[cached]` macro-generated code. The key is
259/// `"{fn_name}:{hash_hex}"` where the hash is a 64-bit `DefaultHasher`
260/// digest of the argument tuple.
261#[must_use]
262pub fn make_cache_key<K: Hash>(fn_name: &str, args: &K) -> String {
263    let mut hasher = DefaultHasher::new();
264    args.hash(&mut hasher);
265    format!("{}:{:x}", fn_name, hasher.finish())
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn cache_key_deterministic() {
274        let k1 = make_cache_key("get_user", &(42_i64,));
275        let k2 = make_cache_key("get_user", &(42_i64,));
276        assert_eq!(k1, k2);
277    }
278
279    #[test]
280    fn cache_key_differs_by_fn_name() {
281        let k1 = make_cache_key("get_user", &(42_i64,));
282        let k2 = make_cache_key("find_user", &(42_i64,));
283        assert_ne!(k1, k2);
284    }
285
286    #[test]
287    fn cache_key_differs_by_args() {
288        let k1 = make_cache_key("get_user", &(1_i64,));
289        let k2 = make_cache_key("get_user", &(2_i64,));
290        assert_ne!(k1, k2);
291    }
292
293    #[test]
294    fn cache_key_no_args() {
295        let k = make_cache_key("get_config", &());
296        assert!(k.starts_with("get_config:"));
297    }
298
299    #[cfg(feature = "cache-moka")]
300    #[test]
301    fn insert_cached_and_get_cached_round_trip() {
302        let cache = MokaCache::new(10, None);
303        insert_cached(&cache, "key", "hello".to_string(), None);
304        let val: Option<String> = get_cached(&cache, "key");
305        assert_eq!(val.as_deref(), Some("hello"));
306    }
307
308    #[cfg(feature = "cache-moka")]
309    #[test]
310    fn get_cached_raw_bytes_slow_path() {
311        // Simulate a cross-replica backend: store RawCacheBytes directly, then
312        // verify get_cached deserializes it back to the concrete type.
313        let cache = MokaCache::new(10, None);
314        let bytes = serde_json::to_vec(&42_i32).unwrap();
315        cache.insert_value("k", Arc::new(RawCacheBytes(bytes)));
316        let val: Option<i32> = get_cached(&cache, "k");
317        assert_eq!(val, Some(42));
318    }
319
320    #[cfg(feature = "cache-moka")]
321    #[test]
322    fn get_cached_miss_returns_none() {
323        let cache = MokaCache::new(10, None);
324        let val: Option<String> = get_cached(&cache, "missing");
325        assert!(val.is_none());
326    }
327
328    #[test]
329    fn cacheable_result_ok_round_trips() {
330        let r: Result<i32, &str> = Result::from_ok(42);
331        assert_eq!(r, Ok(42));
332        assert_eq!(r.into_result(), Ok(42));
333    }
334
335    #[test]
336    fn cacheable_result_err_passes_through() {
337        let r: Result<i32, &str> = Err("oops");
338        assert_eq!(r.into_result(), Err("oops"));
339    }
340}