Skip to main content

rullst_core/
cache.rs

1//! # Rullst Cache System (`rullst::cache`)
2//!
3//! Provides a unified caching API with pluggable drivers.
4//!
5//! ## Drivers
6//! - **In-Memory** (default): `DashMap`-based concurrent store with TTL support. Zero config.
7//! - **Redis** (optional): Requires the `cache-redis` feature flag.
8//!
9//! ## Quick Start
10//! ```rust,no_run
11//! use rullst_core::cache::{Cache, CacheError};
12//!
13//! async fn cache_example() -> Result<(), CacheError> {
14//!     let cache = Cache::memory();
15//!
16//!     cache.put("user:42:name", "Alice", Some(60)).await?;
17//!     let name = cache.get("user:42:name").await?;
18//!     assert_eq!(name.as_deref().map(String::as_str), Some("Alice"));
19//!
20//!     let value = cache.remember("expensive_key", 300, || async {
21//!         Ok("computed_value".to_string())
22//!     }).await?;
23//!     assert_eq!(value.as_str(), "computed_value");
24//!
25//!     Ok(())
26//! }
27//! ```
28
29use async_trait::async_trait;
30use dashmap::DashMap;
31use std::sync::{
32    Arc,
33    atomic::{AtomicUsize, Ordering},
34};
35use std::time::Instant;
36
37mod tenant;
38pub use tenant::TenantCache;
39
40// ─── Error Types ────────────────────────────────────────────────────────────
41
42/// Errors that can occur during cache operations.
43#[derive(Debug)]
44#[non_exhaustive]
45pub enum CacheError {
46    /// The underlying driver encountered an error.
47    Driver(String),
48    /// Serialization or deserialization failed.
49    Serialization(String),
50    /// A tenant-scoped cache key was empty, oversized, or contained unsafe bytes.
51    InvalidKey(String),
52    /// The selected backend does not expose metadata inspection.
53    InspectionUnsupported,
54    /// A cache inspection requested zero or more than the fixed maximum.
55    InvalidInspectionLimit,
56}
57
58impl std::fmt::Display for CacheError {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        match self {
61            CacheError::Driver(msg) => write!(f, "Cache driver error: {}", msg),
62            CacheError::Serialization(msg) => write!(f, "Cache serialization error: {}", msg),
63            CacheError::InvalidKey(msg) => write!(f, "Invalid cache key: {}", msg),
64            CacheError::InspectionUnsupported => {
65                write!(f, "Cache metadata inspection is unsupported by this driver")
66            }
67            CacheError::InvalidInspectionLimit => write!(
68                f,
69                "Cache inspection limit must be between 1 and {MAX_CACHE_INSPECTION_ENTRIES}"
70            ),
71        }
72    }
73}
74
75impl std::error::Error for CacheError {}
76
77// ─── Cache Driver Trait ─────────────────────────────────────────────────────
78
79/// Abstraction over cache storage backends.
80///
81/// Implement this trait to add support for new cache backends.
82/// The framework ships with `MemoryDriver` and (optionally) `RedisDriver`.
83#[async_trait]
84pub trait CacheDriver: Send + Sync {
85    /// Retrieve a value by key. Returns `None` if the key doesn't exist or has expired.
86    async fn get(&self, key: &str) -> Result<Option<Arc<String>>, CacheError>;
87    /// Store a value with an optional TTL in seconds.
88    async fn put(&self, key: &str, value: &str, ttl_secs: Option<u64>) -> Result<(), CacheError>;
89    /// Remove a key from the cache.
90    async fn forget(&self, key: &str) -> Result<(), CacheError>;
91    /// Remove all keys from the cache.
92    async fn flush(&self) -> Result<(), CacheError>;
93    /// Check if a key exists and is not expired.
94    async fn has(&self, key: &str) -> Result<bool, CacheError>;
95    /// Returns bounded metadata without values when the driver supports it.
96    async fn inspect(&self, _limit: usize) -> Result<CacheInspection, CacheError> {
97        Err(CacheError::InspectionUnsupported)
98    }
99}
100
101#[path = "cache/inspection.rs"]
102mod inspection;
103pub use inspection::{CacheEntryMetadata, CacheInspection, MAX_CACHE_INSPECTION_ENTRIES};
104
105// ─── In-Memory Driver ───────────────────────────────────────────────────────
106
107/// Cache entry holding the value and optional expiration time.
108#[derive(Clone)]
109struct CacheEntry {
110    value: Arc<String>,
111    expires_at: Option<Instant>,
112}
113
114/// In-memory cache driver using `DashMap` for lock-free concurrent access.
115///
116/// Supports TTL-based expiration. Expired entries are lazily cleaned on access.
117/// Perfect for single-instance deployments and development.
118pub struct MemoryDriver {
119    store: DashMap<String, CacheEntry>,
120    operations_since_cleanup: AtomicUsize,
121}
122
123impl MemoryDriver {
124    /// Create a new in-memory cache driver.
125    pub fn new() -> Self {
126        Self {
127            store: DashMap::new(),
128            operations_since_cleanup: AtomicUsize::new(0),
129        }
130    }
131
132    /// Reclaims expired entries opportunistically without spawning a task that
133    /// could outlive the cache. Entries are also removed immediately when read.
134    fn cleanup_if_due(&self) {
135        const CLEANUP_INTERVAL_OPERATIONS: usize = 256;
136        let previous = self
137            .operations_since_cleanup
138            .fetch_add(1, Ordering::Relaxed);
139        if previous > 0 && previous.is_multiple_of(CLEANUP_INTERVAL_OPERATIONS) {
140            let now = Instant::now();
141            self.store
142                .retain(|_, entry| entry.expires_at.is_none_or(|expires_at| now < expires_at));
143        }
144    }
145
146    fn get_sync(&self, key: &str) -> Option<Arc<String>> {
147        self.cleanup_if_due();
148        let entry = self.store.get(key)?;
149        if entry
150            .expires_at
151            .is_some_and(|expires_at| Instant::now() >= expires_at)
152        {
153            drop(entry);
154            self.store.remove(key);
155            return None;
156        }
157        Some(Arc::clone(&entry.value))
158    }
159
160    fn put_sync(&self, key: &str, value: &str, ttl_secs: Option<u64>) {
161        self.cleanup_if_due();
162        let expires_at = ttl_secs.map(|secs| Instant::now() + std::time::Duration::from_secs(secs));
163        self.store.insert(
164            key.to_string(),
165            CacheEntry {
166                value: Arc::new(value.to_string()),
167                expires_at,
168            },
169        );
170    }
171}
172
173impl Default for MemoryDriver {
174    fn default() -> Self {
175        Self::new()
176    }
177}
178
179#[async_trait]
180impl CacheDriver for MemoryDriver {
181    #[cfg_attr(mutants, mutants::skip)]
182    async fn get(&self, key: &str) -> Result<Option<Arc<String>>, CacheError> {
183        Ok(self.get_sync(key))
184    }
185
186    async fn put(&self, key: &str, value: &str, ttl_secs: Option<u64>) -> Result<(), CacheError> {
187        self.put_sync(key, value, ttl_secs);
188        Ok(())
189    }
190
191    async fn forget(&self, key: &str) -> Result<(), CacheError> {
192        self.store.remove(key);
193        Ok(())
194    }
195
196    async fn flush(&self) -> Result<(), CacheError> {
197        self.store.clear();
198        Ok(())
199    }
200
201    async fn has(&self, key: &str) -> Result<bool, CacheError> {
202        Ok(self.get(key).await?.is_some())
203    }
204
205    async fn inspect(&self, limit: usize) -> Result<CacheInspection, CacheError> {
206        inspection::validate_inspection_limit(limit)?;
207        self.cleanup_if_due();
208        let now = Instant::now();
209        let mut entries: Vec<_> = self
210            .store
211            .iter()
212            .filter(|entry| entry.expires_at.is_none_or(|expires_at| expires_at > now))
213            .map(|entry| {
214                CacheEntryMetadata::new(
215                    entry.key().clone(),
216                    entry.value.len(),
217                    entry
218                        .expires_at
219                        .map(|expires_at| expires_at.saturating_duration_since(now).as_millis())
220                        .and_then(|ttl| u64::try_from(ttl).ok()),
221                )
222            })
223            .collect();
224        entries.sort_by(|left, right| left.logical_key.cmp(&right.logical_key));
225        let truncated = entries.len() > limit;
226        entries.truncate(limit);
227        Ok(CacheInspection::new(entries, truncated))
228    }
229}
230
231// ─── Global Memoize Cache ───────────────────────────────────────────────────
232
233/// Global memory cache functions used by the `#[memoize]` macro.
234pub mod memory {
235    use super::MemoryDriver;
236    use std::sync::OnceLock;
237
238    static GLOBAL_MEMO_CACHE: OnceLock<MemoryDriver> = OnceLock::new();
239
240    fn get_cache() -> &'static MemoryDriver {
241        GLOBAL_MEMO_CACHE.get_or_init(MemoryDriver::new)
242    }
243
244    /// Retrieve a value from the global memoize cache.
245    pub fn get(key: &str) -> Option<String> {
246        get_cache().get_sync(key).map(|value| value.to_string())
247    }
248
249    /// Store a value in the global memoize cache.
250    pub fn set(key: &str, value: &str) {
251        get_cache().put_sync(key, value, Some(3600));
252    }
253}
254
255// ─── Redis Driver (behind feature flag) ─────────────────────────────────────
256
257#[cfg(feature = "cache-redis")]
258#[path = "cache/redis_driver.rs"]
259pub mod redis_driver;
260
261// ─── Cache Facade ───────────────────────────────────────────────────────────
262
263/// The main cache facade for storing and retrieving cached values.
264///
265/// Provides a driver-agnostic API. Create with `Cache::memory()` or `Cache::redis()`.
266///
267/// # Thread Safety
268/// The `Cache` is `Send + Sync` and can be safely shared across async tasks
269/// and Axum handlers via `Arc` or Axum's `State`.
270#[derive(Clone)]
271pub struct Cache {
272    driver: Arc<Box<dyn CacheDriver>>,
273}
274
275impl Cache {
276    /// Create a cache backed by an in-memory `DashMap`. Zero configuration.
277    ///
278    /// Data is lost on process restart. Perfect for development and single-instance apps.
279    pub fn memory() -> Self {
280        Self {
281            driver: Arc::new(Box::new(MemoryDriver::new())),
282        }
283    }
284
285    /// Create a cache backed by Redis. Requires the `cache-redis` feature.
286    ///
287    /// Data persists across restarts and is shared between instances.
288    #[cfg(feature = "cache-redis")]
289    #[cfg_attr(mutants, mutants::skip)]
290    pub fn redis(redis_url: impl Into<String>) -> Result<Self, CacheError> {
291        let driver = redis_driver::RedisDriver::new(redis_url)?;
292        Ok(Self {
293            driver: Arc::new(Box::new(driver)),
294        })
295    }
296
297    /// Create a cache from any custom driver implementing `CacheDriver`.
298    pub fn custom(driver: Box<dyn CacheDriver>) -> Self {
299        Self {
300            driver: Arc::new(driver),
301        }
302    }
303
304    /// Retrieve a value by key.
305    pub async fn get(&self, key: &str) -> Result<Option<Arc<String>>, CacheError> {
306        self.driver.get(key).await
307    }
308
309    /// Store a value with an optional TTL in seconds.
310    ///
311    /// Pass `None` for TTL to store indefinitely.
312    pub async fn put(
313        &self,
314        key: &str,
315        value: &str,
316        ttl_secs: Option<u64>,
317    ) -> Result<(), CacheError> {
318        self.driver.put(key, value, ttl_secs).await
319    }
320
321    /// Remove a key from the cache.
322    pub async fn forget(&self, key: &str) -> Result<(), CacheError> {
323        self.driver.forget(key).await
324    }
325
326    /// Remove all keys from the cache.
327    pub async fn flush(&self) -> Result<(), CacheError> {
328        self.driver.flush().await
329    }
330
331    /// Check if a key exists and has not expired.
332    pub async fn has(&self, key: &str) -> Result<bool, CacheError> {
333        self.driver.has(key).await
334    }
335
336    /// Returns a bounded metadata-only snapshot when supported by the driver.
337    pub async fn inspect(&self, limit: usize) -> Result<CacheInspection, CacheError> {
338        self.driver.inspect(limit).await
339    }
340
341    /// Retrieve a cached value, or compute it with the provided closure and cache the result.
342    ///
343    /// This is the **cache-aside** (or "remember") pattern — the most common caching strategy.
344    ///
345    /// # Example
346    /// ```rust,no_run
347    /// use std::sync::Arc;
348    /// use rullst_core::cache::{Cache, CacheError};
349    ///
350    /// async fn load_bio(cache: &Cache) -> Result<Arc<String>, CacheError> {
351    ///     cache.remember("user:42:bio", 300, || async {
352    ///         Ok("Example biography".to_string())
353    ///     }).await
354    /// }
355    /// ```
356    pub async fn remember<F, Fut>(
357        &self,
358        key: &str,
359        ttl_secs: u64,
360        f: F,
361    ) -> Result<Arc<String>, CacheError>
362    where
363        F: FnOnce() -> Fut,
364        Fut: std::future::Future<Output = Result<String, CacheError>>,
365    {
366        // Try the cache first
367        if let Some(cached) = self.get(key).await? {
368            return Ok(cached);
369        }
370        // Cache miss — compute the value
371        let value = f().await?;
372        // Store in cache
373        self.put(key, &value, Some(ttl_secs)).await?;
374        Ok(Arc::new(value))
375    }
376}
377
378#[cfg(test)]
379mod tests;
380
381#[cfg(test)]
382#[path = "cache_contract_tests.rs"]
383mod contract_tests;