Skip to main content

prax_query/data_cache/
mod.rs

1//! High-performance data caching layer for Prax ORM.
2//!
3//! This module provides a flexible, standalone caching system for query
4//! results with support for:
5//!
6//! - **In-memory caching** using [moka](https://github.com/moka-rs/moka) for
7//!   high-performance concurrent access
8//! - **Tiered caching** layering any two [`CacheBackend`] implementations
9//!   (e.g. memory L1 over a slower L2)
10//! - **Automatic invalidation** based on TTL, entity changes, or custom patterns
11//! - **Cache-aside usage** through the standalone [`CacheManager`] API
12//!
13//! > **Not yet available:**
14//! > - Transparent query integration — there is no `.cache(...)` method on
15//! >   query operations; wrap queries manually with [`CacheManager`] as
16//! >   shown below.
17//! > - The Redis backend ([`RedisCache`]) — construction and every operation
18//! >   return an error because no Redis client is compiled in.
19//!
20//! # Architecture
21//!
22//! ```text
23//! ┌─────────────────────────────────────────────────────────────────┐
24//! │                        Application                               │
25//! │         (standalone CacheManager; no query integration yet)      │
26//! └─────────────────────────────────────────────────────────────────┘
27//!                                │
28//!                                ▼
29//! ┌─────────────────────────────────────────────────────────────────┐
30//! │                      Cache Manager                               │
31//! │  ┌─────────────┐    ┌──────────────────┐    ┌─────────────────┐ │
32//! │  │ L1: Memory  │ -> │ L2: CacheBackend │ -> │   Database      │ │
33//! │  │ (< 1ms)     │    │ (e.g. NoopCache) │    │   (10-100ms)    │ │
34//! │  └─────────────┘    └──────────────────┘    └─────────────────┘ │
35//! └─────────────────────────────────────────────────────────────────┘
36//! ```
37//!
38//! # Quick Start
39//!
40//! ```rust,ignore
41//! use prax_query::data_cache::{CacheKey, CacheManager, MemoryCache};
42//! use std::time::Duration;
43//!
44//! // In-memory backend (single instance)
45//! let backend = MemoryCache::builder()
46//!     .max_capacity(10_000)
47//!     .time_to_live(Duration::from_secs(300))
48//!     .build();
49//! let cache = CacheManager::new(backend);
50//!
51//! // Standalone cache-aside usage around your own data access
52//! let key = CacheKey::new("User", "find_many:all");
53//! let users: Vec<User> = cache
54//!     .get_or_set(
55//!         &key,
56//!         || async { Ok(load_all_users().await) },
57//!         None,
58//!     )
59//!     .await?;
60//! ```
61//!
62//! # Cache Invalidation
63//!
64//! ```rust,ignore
65//! use prax_query::data_cache::{EntityTag, KeyPattern};
66//!
67//! // Invalidate by entity type
68//! cache.invalidate_entity("User").await?;
69//!
70//! // Invalidate by specific record
71//! cache.invalidate_record("User", &user_id).await?;
72//!
73//! // Invalidate by pattern
74//! cache.invalidate_pattern(&KeyPattern::entity("User")).await?;
75//!
76//! // Tag-based invalidation
77//! cache.invalidate_tags(&[EntityTag::new("User"), EntityTag::new("tenant:123")]).await?;
78//! ```
79//!
80//! # Performance Characteristics
81//!
82//! | Backend | Latency | Capacity | Distribution | Best For |
83//! |---------|---------|----------|--------------|----------|
84//! | Memory | < 1ms | Limited by RAM | Single instance | Hot data, sessions |
85//! | Tiered | < 1ms (L1 hit) | Both layers | Depends on L2 | Layered setups |
86//!
87//! A Redis backend for distributed, multi-instance caching is planned but
88//! not yet implemented.
89
90mod backend;
91mod invalidation;
92mod key;
93mod memory;
94mod options;
95mod redis;
96mod stats;
97mod tiered;
98
99pub use backend::{CacheBackend, CacheEntry, CacheError, CacheResult};
100pub use invalidation::{EntityTag, InvalidationEvent, InvalidationStrategy};
101pub use key::{CacheKey, CacheKeyBuilder, KeyPattern};
102pub use memory::{MemoryCache, MemoryCacheBuilder, MemoryCacheConfig};
103pub use options::{CacheOptions, CachePolicy, WritePolicy};
104pub use redis::{RedisCache, RedisCacheConfig, RedisConnection};
105pub use stats::{CacheMetrics, CacheStats};
106pub use tiered::{TieredCache, TieredCacheConfig};
107
108use std::sync::Arc;
109
110/// The main cache manager that coordinates caching operations.
111///
112/// This is the primary entry point for the caching system. It wraps any
113/// `CacheBackend` implementation and provides a unified API.
114#[derive(Clone)]
115pub struct CacheManager<B: CacheBackend> {
116    backend: Arc<B>,
117    default_options: CacheOptions,
118    metrics: Arc<CacheMetrics>,
119}
120
121impl<B: CacheBackend> CacheManager<B> {
122    /// Create a new cache manager with the given backend.
123    pub fn new(backend: B) -> Self {
124        Self {
125            backend: Arc::new(backend),
126            default_options: CacheOptions::default(),
127            metrics: Arc::new(CacheMetrics::new()),
128        }
129    }
130
131    /// Create with custom default options.
132    pub fn with_options(backend: B, options: CacheOptions) -> Self {
133        Self {
134            backend: Arc::new(backend),
135            default_options: options,
136            metrics: Arc::new(CacheMetrics::new()),
137        }
138    }
139
140    /// Get the cache backend.
141    pub fn backend(&self) -> &B {
142        &self.backend
143    }
144
145    /// Get the metrics collector.
146    pub fn metrics(&self) -> &CacheMetrics {
147        &self.metrics
148    }
149
150    /// Get a value from the cache.
151    pub async fn get<T>(&self, key: &CacheKey) -> CacheResult<Option<T>>
152    where
153        T: serde::de::DeserializeOwned,
154    {
155        let start = std::time::Instant::now();
156        let result = self.backend.get(key).await;
157        let duration = start.elapsed();
158
159        match &result {
160            Ok(Some(_)) => self.metrics.record_hit(duration),
161            Ok(None) => self.metrics.record_miss(duration),
162            Err(_) => self.metrics.record_error(),
163        }
164
165        result
166    }
167
168    /// Set a value in the cache.
169    pub async fn set<T>(
170        &self,
171        key: &CacheKey,
172        value: &T,
173        options: Option<&CacheOptions>,
174    ) -> CacheResult<()>
175    where
176        T: serde::Serialize + Sync,
177    {
178        let opts = options.unwrap_or(&self.default_options);
179        let start = std::time::Instant::now();
180        let result = self.backend.set(key, value, opts.ttl).await;
181        let duration = start.elapsed();
182
183        if result.is_ok() {
184            self.metrics.record_write(duration);
185        } else {
186            self.metrics.record_error();
187        }
188
189        result
190    }
191
192    /// Get or compute a value.
193    ///
194    /// If the value exists in cache, returns it. Otherwise, calls the
195    /// provided function to compute the value, caches it, and returns it.
196    pub async fn get_or_set<T, F, Fut>(
197        &self,
198        key: &CacheKey,
199        f: F,
200        options: Option<&CacheOptions>,
201    ) -> CacheResult<T>
202    where
203        T: serde::Serialize + serde::de::DeserializeOwned + Sync,
204        F: FnOnce() -> Fut,
205        Fut: std::future::Future<Output = CacheResult<T>>,
206    {
207        // Try to get from cache first
208        if let Some(value) = self.get::<T>(key).await? {
209            return Ok(value);
210        }
211
212        // Compute the value
213        let value = f().await?;
214
215        // Store in cache (ignore errors - cache is best-effort)
216        let _ = self.set(key, &value, options).await;
217
218        Ok(value)
219    }
220
221    /// Delete a value from the cache.
222    pub async fn delete(&self, key: &CacheKey) -> CacheResult<bool> {
223        self.backend.delete(key).await
224    }
225
226    /// Check if a key exists in the cache.
227    pub async fn exists(&self, key: &CacheKey) -> CacheResult<bool> {
228        self.backend.exists(key).await
229    }
230
231    /// Invalidate cache entries by pattern.
232    pub async fn invalidate_pattern(&self, pattern: &KeyPattern) -> CacheResult<u64> {
233        self.backend.invalidate_pattern(pattern).await
234    }
235
236    /// Invalidate all entries for an entity type.
237    pub async fn invalidate_entity(&self, entity: &str) -> CacheResult<u64> {
238        let pattern = KeyPattern::entity(entity);
239        self.invalidate_pattern(&pattern).await
240    }
241
242    /// Invalidate a specific record.
243    pub async fn invalidate_record<I: std::fmt::Display>(
244        &self,
245        entity: &str,
246        id: I,
247    ) -> CacheResult<u64> {
248        let pattern = KeyPattern::record(entity, id);
249        self.invalidate_pattern(&pattern).await
250    }
251
252    /// Invalidate entries by tags.
253    pub async fn invalidate_tags(&self, tags: &[EntityTag]) -> CacheResult<u64> {
254        self.backend.invalidate_tags(tags).await
255    }
256
257    /// Clear all entries from the cache.
258    pub async fn clear(&self) -> CacheResult<()> {
259        self.backend.clear().await
260    }
261
262    /// Get cache statistics.
263    pub fn stats(&self) -> CacheStats {
264        self.metrics.snapshot()
265    }
266}
267
268/// Builder for creating cache managers with different configurations.
269pub struct CacheManagerBuilder {
270    default_options: CacheOptions,
271}
272
273impl Default for CacheManagerBuilder {
274    fn default() -> Self {
275        Self::new()
276    }
277}
278
279impl CacheManagerBuilder {
280    /// Create a new builder.
281    pub fn new() -> Self {
282        Self {
283            default_options: CacheOptions::default(),
284        }
285    }
286
287    /// Set default cache options.
288    pub fn default_options(mut self, options: CacheOptions) -> Self {
289        self.default_options = options;
290        self
291    }
292
293    /// Build a cache manager with an in-memory backend.
294    pub fn memory(self, config: MemoryCacheConfig) -> CacheManager<MemoryCache> {
295        let backend = MemoryCache::new(config);
296        CacheManager::with_options(backend, self.default_options)
297    }
298
299    /// Build a cache manager with a Redis backend.
300    ///
301    /// **Not yet implemented:** the Redis backend has no client compiled in,
302    /// so this currently always returns `Err(CacheError::Backend)`.
303    pub async fn redis(self, config: RedisCacheConfig) -> CacheResult<CacheManager<RedisCache>> {
304        let backend = RedisCache::new(config).await?;
305        Ok(CacheManager::with_options(backend, self.default_options))
306    }
307
308    /// Build a cache manager with a tiered backend.
309    ///
310    /// **Not yet usable with Redis L2:** this constructs a [`RedisCache`],
311    /// which is not yet implemented, so this currently always returns
312    /// `Err(CacheError::Backend)`. For a working tiered setup, compose two
313    /// implemented backends with [`TieredCache::new`] directly.
314    pub async fn tiered(
315        self,
316        memory_config: MemoryCacheConfig,
317        redis_config: RedisCacheConfig,
318    ) -> CacheResult<CacheManager<TieredCache<MemoryCache, RedisCache>>> {
319        let memory = MemoryCache::new(memory_config);
320        let redis = RedisCache::new(redis_config).await?;
321        let backend = TieredCache::new(memory, redis);
322        Ok(CacheManager::with_options(backend, self.default_options))
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329    use std::time::Duration;
330
331    #[tokio::test]
332    async fn test_memory_cache_basic() {
333        let cache = CacheManager::new(MemoryCache::new(MemoryCacheConfig::default()));
334
335        let key = CacheKey::new("test", "key1");
336
337        // Set a value
338        cache.set(&key, &"hello world", None).await.unwrap();
339
340        // Get it back
341        let value: Option<String> = cache.get(&key).await.unwrap();
342        assert_eq!(value, Some("hello world".to_string()));
343
344        // Delete it
345        cache.delete(&key).await.unwrap();
346
347        // Should be gone
348        let value: Option<String> = cache.get(&key).await.unwrap();
349        assert!(value.is_none());
350    }
351
352    #[tokio::test]
353    async fn test_get_or_set() {
354        let cache = CacheManager::new(MemoryCache::new(MemoryCacheConfig::default()));
355
356        let key = CacheKey::new("test", "computed");
357        let mut call_count = 0;
358
359        // First call should compute
360        let value: String = cache
361            .get_or_set(
362                &key,
363                || {
364                    call_count += 1;
365                    async { Ok("computed value".to_string()) }
366                },
367                None,
368            )
369            .await
370            .unwrap();
371
372        assert_eq!(value, "computed value");
373        assert_eq!(call_count, 1);
374
375        // Second call should use cache
376        let value: String = cache
377            .get_or_set(
378                &key,
379                || {
380                    call_count += 1;
381                    async { Ok("should not be called".to_string()) }
382                },
383                None,
384            )
385            .await
386            .unwrap();
387
388        assert_eq!(value, "computed value");
389        assert_eq!(call_count, 1); // Not incremented
390    }
391}