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}