prax_query/data_cache/
backend.rs

1//! Cache backend trait and core types.
2
3use super::invalidation::EntityTag;
4use super::key::{CacheKey, KeyPattern};
5use std::future::Future;
6use std::time::Duration;
7use thiserror::Error;
8
9/// Errors that can occur during cache operations.
10#[derive(Error, Debug)]
11pub enum CacheError {
12    /// Serialization error.
13    #[error("serialization error: {0}")]
14    Serialization(String),
15
16    /// Deserialization error.
17    #[error("deserialization error: {0}")]
18    Deserialization(String),
19
20    /// Connection error.
21    #[error("connection error: {0}")]
22    Connection(String),
23
24    /// Operation timeout.
25    #[error("operation timed out")]
26    Timeout,
27
28    /// Key not found.
29    #[error("key not found: {0}")]
30    NotFound(String),
31
32    /// Backend-specific error.
33    #[error("backend error: {0}")]
34    Backend(String),
35
36    /// Configuration error.
37    #[error("configuration error: {0}")]
38    Config(String),
39}
40
41/// Result type for cache operations.
42pub type CacheResult<T> = Result<T, CacheError>;
43
44/// A cached entry with metadata.
45#[derive(Debug, Clone)]
46pub struct CacheEntry<T> {
47    /// The cached value.
48    pub value: T,
49    /// When the entry was created.
50    pub created_at: std::time::Instant,
51    /// Time-to-live for this entry.
52    pub ttl: Option<Duration>,
53    /// Tags associated with this entry.
54    pub tags: Vec<EntityTag>,
55    /// Size in bytes (if known).
56    pub size_bytes: Option<usize>,
57}
58
59impl<T> CacheEntry<T> {
60    /// Create a new cache entry.
61    pub fn new(value: T) -> Self {
62        Self {
63            value,
64            created_at: std::time::Instant::now(),
65            ttl: None,
66            tags: Vec::new(),
67            size_bytes: None,
68        }
69    }
70
71    /// Set the TTL.
72    pub fn with_ttl(mut self, ttl: Duration) -> Self {
73        self.ttl = Some(ttl);
74        self
75    }
76
77    /// Set tags.
78    pub fn with_tags(mut self, tags: Vec<EntityTag>) -> Self {
79        self.tags = tags;
80        self
81    }
82
83    /// Set size.
84    pub fn with_size(mut self, size: usize) -> Self {
85        self.size_bytes = Some(size);
86        self
87    }
88
89    /// Check if the entry is expired.
90    pub fn is_expired(&self) -> bool {
91        if let Some(ttl) = self.ttl {
92            self.created_at.elapsed() >= ttl
93        } else {
94            false
95        }
96    }
97
98    /// Get remaining TTL.
99    pub fn remaining_ttl(&self) -> Option<Duration> {
100        self.ttl.map(|ttl| ttl.saturating_sub(self.created_at.elapsed()))
101    }
102}
103
104/// The core trait for cache backends.
105///
106/// This trait defines the interface that all cache backends must implement.
107/// It supports both synchronous and asynchronous operations.
108pub trait CacheBackend: Send + Sync + 'static {
109    /// Get a value from the cache.
110    fn get<T>(&self, key: &CacheKey) -> impl Future<Output = CacheResult<Option<T>>> + Send
111    where
112        T: serde::de::DeserializeOwned;
113
114    /// Set a value in the cache.
115    fn set<T>(
116        &self,
117        key: &CacheKey,
118        value: &T,
119        ttl: Option<Duration>,
120    ) -> impl Future<Output = CacheResult<()>> + Send
121    where
122        T: serde::Serialize + Sync;
123
124    /// Delete a value from the cache.
125    fn delete(&self, key: &CacheKey) -> impl Future<Output = CacheResult<bool>> + Send;
126
127    /// Check if a key exists.
128    fn exists(&self, key: &CacheKey) -> impl Future<Output = CacheResult<bool>> + Send;
129
130    /// Get multiple values at once.
131    ///
132    /// Default implementation fetches sequentially. Override for batch optimization.
133    fn get_many<T>(
134        &self,
135        keys: &[CacheKey],
136    ) -> impl Future<Output = CacheResult<Vec<Option<T>>>> + Send
137    where
138        T: serde::de::DeserializeOwned + Send,
139    {
140        async move {
141            let mut results = Vec::with_capacity(keys.len());
142            for key in keys {
143                results.push(self.get::<T>(key).await?);
144            }
145            Ok(results)
146        }
147    }
148
149    /// Set multiple values at once.
150    ///
151    /// Default implementation sets sequentially. Override for batch optimization.
152    fn set_many<T>(
153        &self,
154        entries: &[(&CacheKey, &T)],
155        ttl: Option<Duration>,
156    ) -> impl Future<Output = CacheResult<()>> + Send
157    where
158        T: serde::Serialize + Sync + Send,
159    {
160        async move {
161            for (key, value) in entries {
162                self.set(*key, *value, ttl).await?;
163            }
164            Ok(())
165        }
166    }
167
168    /// Delete multiple keys at once.
169    ///
170    /// Default implementation deletes sequentially. Override for batch optimization.
171    fn delete_many(&self, keys: &[CacheKey]) -> impl Future<Output = CacheResult<u64>> + Send {
172        async move {
173            let mut count = 0u64;
174            for key in keys {
175                if self.delete(key).await? {
176                    count += 1;
177                }
178            }
179            Ok(count)
180        }
181    }
182
183    /// Invalidate entries matching a pattern.
184    fn invalidate_pattern(
185        &self,
186        pattern: &KeyPattern,
187    ) -> impl Future<Output = CacheResult<u64>> + Send;
188
189    /// Invalidate entries by tags.
190    fn invalidate_tags(&self, tags: &[EntityTag]) -> impl Future<Output = CacheResult<u64>> + Send;
191
192    /// Clear all entries.
193    fn clear(&self) -> impl Future<Output = CacheResult<()>> + Send;
194
195    /// Get the approximate number of entries.
196    fn len(&self) -> impl Future<Output = CacheResult<usize>> + Send;
197
198    /// Check if the cache is empty.
199    fn is_empty(&self) -> impl Future<Output = CacheResult<bool>> + Send {
200        async move { Ok(self.len().await? == 0) }
201    }
202
203    /// Get cache statistics if available.
204    fn stats(&self) -> impl Future<Output = CacheResult<BackendStats>> + Send {
205        async move {
206            Ok(BackendStats {
207                entries: self.len().await?,
208                ..Default::default()
209            })
210        }
211    }
212}
213
214/// Statistics from a cache backend.
215#[derive(Debug, Clone, Default)]
216pub struct BackendStats {
217    /// Number of entries.
218    pub entries: usize,
219    /// Memory usage in bytes.
220    pub memory_bytes: Option<usize>,
221    /// Number of connections (for Redis).
222    pub connections: Option<usize>,
223    /// Backend-specific info.
224    pub info: Option<String>,
225}
226
227/// A no-op cache backend that doesn't cache anything.
228///
229/// Useful for testing or when caching should be disabled.
230#[derive(Debug, Clone, Copy, Default)]
231pub struct NoopCache;
232
233impl CacheBackend for NoopCache {
234    async fn get<T>(&self, _key: &CacheKey) -> CacheResult<Option<T>>
235    where
236        T: serde::de::DeserializeOwned,
237    {
238        Ok(None)
239    }
240
241    async fn set<T>(
242        &self,
243        _key: &CacheKey,
244        _value: &T,
245        _ttl: Option<Duration>,
246    ) -> CacheResult<()>
247    where
248        T: serde::Serialize + Sync,
249    {
250        Ok(())
251    }
252
253    async fn delete(&self, _key: &CacheKey) -> CacheResult<bool> {
254        Ok(false)
255    }
256
257    async fn exists(&self, _key: &CacheKey) -> CacheResult<bool> {
258        Ok(false)
259    }
260
261    async fn invalidate_pattern(&self, _pattern: &KeyPattern) -> CacheResult<u64> {
262        Ok(0)
263    }
264
265    async fn invalidate_tags(&self, _tags: &[EntityTag]) -> CacheResult<u64> {
266        Ok(0)
267    }
268
269    async fn clear(&self) -> CacheResult<()> {
270        Ok(())
271    }
272
273    async fn len(&self) -> CacheResult<usize> {
274        Ok(0)
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    #[test]
283    fn test_cache_entry() {
284        let entry = CacheEntry::new("test value")
285            .with_ttl(Duration::from_secs(60))
286            .with_tags(vec![EntityTag::new("User")]);
287
288        assert!(!entry.is_expired());
289        assert!(entry.remaining_ttl().unwrap() > Duration::from_secs(59));
290    }
291
292    #[tokio::test]
293    async fn test_noop_cache() {
294        let cache = NoopCache;
295
296        // Set should succeed but not store
297        cache
298            .set(&CacheKey::new("test", "key"), &"value", None)
299            .await
300            .unwrap();
301
302        // Get should return None
303        let result: Option<String> = cache.get(&CacheKey::new("test", "key")).await.unwrap();
304        assert!(result.is_none());
305    }
306}
307
308