Skip to main content

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
101            .map(|ttl| ttl.saturating_sub(self.created_at.elapsed()))
102    }
103}
104
105/// The core trait for cache backends.
106///
107/// This trait defines the interface that all cache backends must implement.
108/// It supports both synchronous and asynchronous operations.
109pub trait CacheBackend: Send + Sync + 'static {
110    /// Get a value from the cache.
111    fn get<T>(&self, key: &CacheKey) -> impl Future<Output = CacheResult<Option<T>>> + Send
112    where
113        T: serde::de::DeserializeOwned;
114
115    /// Set a value in the cache.
116    fn set<T>(
117        &self,
118        key: &CacheKey,
119        value: &T,
120        ttl: Option<Duration>,
121    ) -> impl Future<Output = CacheResult<()>> + Send
122    where
123        T: serde::Serialize + Sync;
124
125    /// Delete a value from the cache.
126    fn delete(&self, key: &CacheKey) -> impl Future<Output = CacheResult<bool>> + Send;
127
128    /// Check if a key exists.
129    fn exists(&self, key: &CacheKey) -> impl Future<Output = CacheResult<bool>> + Send;
130
131    /// Get multiple values at once.
132    ///
133    /// Default implementation fetches sequentially. Override for batch optimization.
134    fn get_many<T>(
135        &self,
136        keys: &[CacheKey],
137    ) -> impl Future<Output = CacheResult<Vec<Option<T>>>> + Send
138    where
139        T: serde::de::DeserializeOwned + Send,
140    {
141        async move {
142            let mut results = Vec::with_capacity(keys.len());
143            for key in keys {
144                results.push(self.get::<T>(key).await?);
145            }
146            Ok(results)
147        }
148    }
149
150    /// Set multiple values at once.
151    ///
152    /// Default implementation sets sequentially. Override for batch optimization.
153    fn set_many<T>(
154        &self,
155        entries: &[(&CacheKey, &T)],
156        ttl: Option<Duration>,
157    ) -> impl Future<Output = CacheResult<()>> + Send
158    where
159        T: serde::Serialize + Sync + Send,
160    {
161        async move {
162            for (key, value) in entries {
163                self.set(*key, *value, ttl).await?;
164            }
165            Ok(())
166        }
167    }
168
169    /// Delete multiple keys at once.
170    ///
171    /// Default implementation deletes sequentially. Override for batch optimization.
172    fn delete_many(&self, keys: &[CacheKey]) -> impl Future<Output = CacheResult<u64>> + Send {
173        async move {
174            let mut count = 0u64;
175            for key in keys {
176                if self.delete(key).await? {
177                    count += 1;
178                }
179            }
180            Ok(count)
181        }
182    }
183
184    /// Invalidate entries matching a pattern.
185    fn invalidate_pattern(
186        &self,
187        pattern: &KeyPattern,
188    ) -> impl Future<Output = CacheResult<u64>> + Send;
189
190    /// Invalidate entries by tags.
191    fn invalidate_tags(&self, tags: &[EntityTag]) -> impl Future<Output = CacheResult<u64>> + Send;
192
193    /// Clear all entries.
194    fn clear(&self) -> impl Future<Output = CacheResult<()>> + Send;
195
196    /// Get the approximate number of entries.
197    fn len(&self) -> impl Future<Output = CacheResult<usize>> + Send;
198
199    /// Check if the cache is empty.
200    fn is_empty(&self) -> impl Future<Output = CacheResult<bool>> + Send {
201        async move { Ok(self.len().await? == 0) }
202    }
203
204    /// Get cache statistics if available.
205    fn stats(&self) -> impl Future<Output = CacheResult<BackendStats>> + Send {
206        async move {
207            Ok(BackendStats {
208                entries: self.len().await?,
209                ..Default::default()
210            })
211        }
212    }
213}
214
215/// Statistics from a cache backend.
216#[derive(Debug, Clone, Default)]
217pub struct BackendStats {
218    /// Number of entries.
219    pub entries: usize,
220    /// Memory usage in bytes.
221    pub memory_bytes: Option<usize>,
222    /// Number of connections (for Redis).
223    pub connections: Option<usize>,
224    /// Backend-specific info.
225    pub info: Option<String>,
226}
227
228/// A no-op cache backend that doesn't cache anything.
229///
230/// Useful for testing or when caching should be disabled.
231#[derive(Debug, Clone, Copy, Default)]
232pub struct NoopCache;
233
234impl CacheBackend for NoopCache {
235    async fn get<T>(&self, _key: &CacheKey) -> CacheResult<Option<T>>
236    where
237        T: serde::de::DeserializeOwned,
238    {
239        Ok(None)
240    }
241
242    async fn set<T>(&self, _key: &CacheKey, _value: &T, _ttl: Option<Duration>) -> CacheResult<()>
243    where
244        T: serde::Serialize + Sync,
245    {
246        Ok(())
247    }
248
249    async fn delete(&self, _key: &CacheKey) -> CacheResult<bool> {
250        Ok(false)
251    }
252
253    async fn exists(&self, _key: &CacheKey) -> CacheResult<bool> {
254        Ok(false)
255    }
256
257    async fn invalidate_pattern(&self, _pattern: &KeyPattern) -> CacheResult<u64> {
258        Ok(0)
259    }
260
261    async fn invalidate_tags(&self, _tags: &[EntityTag]) -> CacheResult<u64> {
262        Ok(0)
263    }
264
265    async fn clear(&self) -> CacheResult<()> {
266        Ok(())
267    }
268
269    async fn len(&self) -> CacheResult<usize> {
270        Ok(0)
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    #[test]
279    fn test_cache_entry() {
280        let entry = CacheEntry::new("test value")
281            .with_ttl(Duration::from_secs(60))
282            .with_tags(vec![EntityTag::new("User")]);
283
284        assert!(!entry.is_expired());
285        assert!(entry.remaining_ttl().unwrap() > Duration::from_secs(59));
286    }
287
288    #[tokio::test]
289    async fn test_noop_cache() {
290        let cache = NoopCache;
291
292        // Set should succeed but not store
293        cache
294            .set(&CacheKey::new("test", "key"), &"value", None)
295            .await
296            .unwrap();
297
298        // Get should return None
299        let result: Option<String> = cache.get(&CacheKey::new("test", "key")).await.unwrap();
300        assert!(result.is_none());
301    }
302}