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}