Skip to main content

prax_query/data_cache/
redis.rs

1//! Redis cache backend for distributed caching.
2//!
3//! > **Status: not yet implemented.** No Redis client is compiled into this
4//! > crate, so this backend is a stub that fails loudly instead of silently
5//! > succeeding:
6//! >
7//! > - [`RedisConnection::new`] / [`RedisCache::new`] return an error at
8//! >   construction time.
9//! > - Every command (`get`, `set`, `del`, `scan`, `mget`, pipeline
10//! >   execution, …) returns [`CacheError::Backend`] with a
11//! >   "redis backend not available" message.
12//!
13//! A real implementation — with connection pooling (bb8/deadpool), cluster
14//! support, pipelining, Lua scripting, and Pub/Sub invalidation — requires
15//! adding a Redis client dependency (e.g. `redis-rs` or `fred`) and is
16//! planned for a future release.
17//!
18//! # Example
19//!
20//! ```rust,ignore
21//! use prax_query::data_cache::redis::{RedisCache, RedisCacheConfig};
22//!
23//! // Currently returns Err(CacheError::Backend) — the backend is not implemented.
24//! let cache = RedisCache::new(RedisCacheConfig {
25//!     url: "redis://localhost:6379".to_string(),
26//!     pool_size: 10,
27//!     ..Default::default()
28//! }).await?;
29//! ```
30
31use std::time::Duration;
32
33use super::backend::{BackendStats, CacheBackend, CacheError, CacheResult};
34use super::invalidation::EntityTag;
35use super::key::{CacheKey, KeyPattern};
36
37/// Error returned by every Redis operation: no Redis client is compiled in.
38fn unavailable() -> CacheError {
39    CacheError::Backend("redis backend not available: no redis client compiled in".to_string())
40}
41
42/// Configuration for Redis cache.
43#[derive(Debug, Clone)]
44pub struct RedisCacheConfig {
45    /// Redis connection URL.
46    pub url: String,
47    /// Connection pool size.
48    pub pool_size: u32,
49    /// Connection timeout.
50    pub connection_timeout: Duration,
51    /// Command timeout.
52    pub command_timeout: Duration,
53    /// Key prefix for all entries.
54    pub key_prefix: String,
55    /// Default TTL.
56    pub default_ttl: Option<Duration>,
57    /// Enable cluster mode.
58    pub cluster_mode: bool,
59    /// Database number (0-15).
60    pub database: u8,
61    /// Enable TLS.
62    pub tls: bool,
63    /// Username for AUTH.
64    pub username: Option<String>,
65    /// Password for AUTH.
66    pub password: Option<String>,
67}
68
69impl Default for RedisCacheConfig {
70    fn default() -> Self {
71        Self {
72            url: "redis://localhost:6379".to_string(),
73            pool_size: 10,
74            connection_timeout: Duration::from_secs(5),
75            command_timeout: Duration::from_secs(2),
76            key_prefix: "prax:cache".to_string(),
77            default_ttl: Some(Duration::from_secs(300)),
78            cluster_mode: false,
79            database: 0,
80            tls: false,
81            username: None,
82            password: None,
83        }
84    }
85}
86
87impl RedisCacheConfig {
88    /// Create a new config with the given URL.
89    pub fn new(url: impl Into<String>) -> Self {
90        Self {
91            url: url.into(),
92            ..Default::default()
93        }
94    }
95
96    /// Set pool size.
97    pub fn with_pool_size(mut self, size: u32) -> Self {
98        self.pool_size = size;
99        self
100    }
101
102    /// Set key prefix.
103    pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
104        self.key_prefix = prefix.into();
105        self
106    }
107
108    /// Set default TTL.
109    pub fn with_ttl(mut self, ttl: Duration) -> Self {
110        self.default_ttl = Some(ttl);
111        self
112    }
113
114    /// Enable cluster mode.
115    pub fn cluster(mut self) -> Self {
116        self.cluster_mode = true;
117        self
118    }
119
120    /// Set database number.
121    pub fn database(mut self, db: u8) -> Self {
122        self.database = db;
123        self
124    }
125
126    /// Set authentication.
127    pub fn auth(mut self, username: Option<String>, password: impl Into<String>) -> Self {
128        self.username = username;
129        self.password = Some(password.into());
130        self
131    }
132
133    /// Build the full key with prefix.
134    fn full_key(&self, key: &CacheKey) -> String {
135        format!("{}:{}", self.key_prefix, key.as_str())
136    }
137}
138
139/// Represents a Redis connection (placeholder for actual implementation).
140///
141/// **Not yet implemented:** no Redis client is compiled in. Construction via
142/// [`RedisConnection::new`] fails, and every command returns
143/// [`CacheError::Backend`]. A real implementation would use the `redis-rs`
144/// or `fred` crate.
145#[derive(Clone)]
146pub struct RedisConnection {
147    config: RedisCacheConfig,
148    // In real impl: pool: Pool<RedisConnectionManager>
149}
150
151impl RedisConnection {
152    /// Create a new connection.
153    ///
154    /// **Not yet implemented:** always returns [`CacheError::Backend`] —
155    /// no Redis client is compiled in. A real implementation would create a
156    /// connection pool (bb8/deadpool), establish initial connections, and
157    /// verify connectivity.
158    pub async fn new(config: RedisCacheConfig) -> CacheResult<Self> {
159        let _ = config;
160        Err(unavailable())
161    }
162
163    /// Get the config.
164    pub fn config(&self) -> &RedisCacheConfig {
165        &self.config
166    }
167
168    /// Execute a Redis command (not implemented — always errors).
169    async fn execute<T>(&self, _cmd: &str, _args: &[&str]) -> CacheResult<T>
170    where
171        T: Default,
172    {
173        // Not implemented: no redis client compiled in.
174        // Example with redis-rs:
175        // let mut conn = self.pool.get().await?;
176        // redis::cmd(cmd).arg(args).query_async(&mut *conn).await
177        Err(unavailable())
178    }
179
180    /// GET command (not implemented — always errors).
181    pub async fn get(&self, key: &str) -> CacheResult<Option<Vec<u8>>> {
182        let _ = key;
183        Err(unavailable())
184    }
185
186    /// SET command with optional TTL (not implemented — always errors).
187    pub async fn set(&self, key: &str, value: &[u8], ttl: Option<Duration>) -> CacheResult<()> {
188        let _ = (key, value, ttl);
189        Err(unavailable())
190    }
191
192    /// DEL command (not implemented — always errors).
193    pub async fn del(&self, key: &str) -> CacheResult<bool> {
194        let _ = key;
195        Err(unavailable())
196    }
197
198    /// EXISTS command (not implemented — always errors).
199    pub async fn exists(&self, key: &str) -> CacheResult<bool> {
200        let _ = key;
201        Err(unavailable())
202    }
203
204    /// KEYS command (not implemented — always errors; a real impl would use SCAN in production).
205    pub async fn keys(&self, pattern: &str) -> CacheResult<Vec<String>> {
206        let _ = pattern;
207        Err(unavailable())
208    }
209
210    /// MGET command (not implemented — always errors).
211    pub async fn mget(&self, keys: &[String]) -> CacheResult<Vec<Option<Vec<u8>>>> {
212        let _ = keys;
213        Err(unavailable())
214    }
215
216    /// MSET command (not implemented — always errors).
217    pub async fn mset(&self, pairs: &[(String, Vec<u8>)]) -> CacheResult<()> {
218        let _ = pairs;
219        Err(unavailable())
220    }
221
222    /// FLUSHDB command (not implemented — always errors).
223    pub async fn flush(&self) -> CacheResult<()> {
224        Err(unavailable())
225    }
226
227    /// DBSIZE command (not implemented — always errors).
228    pub async fn dbsize(&self) -> CacheResult<usize> {
229        Err(unavailable())
230    }
231
232    /// INFO command (not implemented — always errors).
233    pub async fn info(&self) -> CacheResult<String> {
234        Err(unavailable())
235    }
236
237    /// SCAN for pattern matching (not implemented — always errors).
238    pub async fn scan(&self, pattern: &str, count: usize) -> CacheResult<Vec<String>> {
239        let _ = (pattern, count);
240        Err(unavailable())
241    }
242
243    /// Pipeline multiple commands.
244    pub fn pipeline(&self) -> RedisPipeline {
245        RedisPipeline::new(self.clone())
246    }
247}
248
249/// A Redis pipeline for batching commands.
250pub struct RedisPipeline {
251    conn: RedisConnection,
252    commands: Vec<PipelineCommand>,
253}
254
255enum PipelineCommand {
256    Get(String),
257    Set(String, Vec<u8>, Option<Duration>),
258    Del(String),
259}
260
261impl RedisPipeline {
262    fn new(conn: RedisConnection) -> Self {
263        Self {
264            conn,
265            commands: Vec::new(),
266        }
267    }
268
269    /// Add a GET command.
270    pub fn get(mut self, key: impl Into<String>) -> Self {
271        self.commands.push(PipelineCommand::Get(key.into()));
272        self
273    }
274
275    /// Add a SET command.
276    pub fn set(mut self, key: impl Into<String>, value: Vec<u8>, ttl: Option<Duration>) -> Self {
277        self.commands
278            .push(PipelineCommand::Set(key.into(), value, ttl));
279        self
280    }
281
282    /// Add a DEL command.
283    pub fn del(mut self, key: impl Into<String>) -> Self {
284        self.commands.push(PipelineCommand::Del(key.into()));
285        self
286    }
287
288    /// Execute the pipeline (not implemented — always errors).
289    pub async fn execute(self) -> CacheResult<Vec<PipelineResult>> {
290        // Not implemented: no redis client compiled in.
291        Err(unavailable())
292    }
293}
294
295/// Result of a pipeline command.
296#[derive(Debug, Clone)]
297pub enum PipelineResult {
298    Ok,
299    Value(Option<Vec<u8>>),
300    Error(String),
301}
302
303/// Redis cache backend.
304///
305/// **Not yet implemented:** [`RedisCache::new`] fails at construction and
306/// every [`CacheBackend`] operation returns [`CacheError::Backend`] — nothing
307/// is ever silently stored, read, or invalidated. See the module-level docs.
308#[derive(Clone)]
309pub struct RedisCache {
310    conn: RedisConnection,
311    config: RedisCacheConfig,
312}
313
314impl RedisCache {
315    /// Create a new Redis cache.
316    ///
317    /// **Not yet implemented:** always returns [`CacheError::Backend`] —
318    /// no Redis client is compiled in.
319    pub async fn new(config: RedisCacheConfig) -> CacheResult<Self> {
320        let conn = RedisConnection::new(config.clone()).await?;
321        Ok(Self { conn, config })
322    }
323
324    /// Create from a URL.
325    ///
326    /// **Not yet implemented:** always returns [`CacheError::Backend`].
327    pub async fn from_url(url: &str) -> CacheResult<Self> {
328        Self::new(RedisCacheConfig::new(url)).await
329    }
330
331    /// Get the connection.
332    pub fn connection(&self) -> &RedisConnection {
333        &self.conn
334    }
335
336    /// Get the config.
337    pub fn config(&self) -> &RedisCacheConfig {
338        &self.config
339    }
340
341    /// Build the full key with prefix.
342    fn full_key(&self, key: &CacheKey) -> String {
343        self.config.full_key(key)
344    }
345}
346
347impl CacheBackend for RedisCache {
348    async fn get<T>(&self, key: &CacheKey) -> CacheResult<Option<T>>
349    where
350        T: serde::de::DeserializeOwned,
351    {
352        let full_key = self.full_key(key);
353
354        match self.conn.get(&full_key).await? {
355            Some(data) => {
356                let value: T = serde_json::from_slice(&data)
357                    .map_err(|e| CacheError::Deserialization(e.to_string()))?;
358                Ok(Some(value))
359            }
360            None => Ok(None),
361        }
362    }
363
364    async fn set<T>(&self, key: &CacheKey, value: &T, ttl: Option<Duration>) -> CacheResult<()>
365    where
366        T: serde::Serialize + Sync,
367    {
368        let full_key = self.full_key(key);
369        let data =
370            serde_json::to_vec(value).map_err(|e| CacheError::Serialization(e.to_string()))?;
371
372        let effective_ttl = ttl.or(self.config.default_ttl);
373        self.conn.set(&full_key, &data, effective_ttl).await
374    }
375
376    async fn delete(&self, key: &CacheKey) -> CacheResult<bool> {
377        let full_key = self.full_key(key);
378        self.conn.del(&full_key).await
379    }
380
381    async fn exists(&self, key: &CacheKey) -> CacheResult<bool> {
382        let full_key = self.full_key(key);
383        self.conn.exists(&full_key).await
384    }
385
386    async fn get_many<T>(&self, keys: &[CacheKey]) -> CacheResult<Vec<Option<T>>>
387    where
388        T: serde::de::DeserializeOwned,
389    {
390        let full_keys: Vec<String> = keys.iter().map(|k| self.full_key(k)).collect();
391        let results = self.conn.mget(&full_keys).await?;
392
393        results
394            .into_iter()
395            .map(|opt| {
396                opt.map(|data| {
397                    serde_json::from_slice(&data)
398                        .map_err(|e| CacheError::Deserialization(e.to_string()))
399                })
400                .transpose()
401            })
402            .collect()
403    }
404
405    async fn invalidate_pattern(&self, pattern: &KeyPattern) -> CacheResult<u64> {
406        let full_pattern = format!("{}:{}", self.config.key_prefix, pattern.to_redis_pattern());
407
408        // Use SCAN to find matching keys
409        let keys = self.conn.scan(&full_pattern, 1000).await?;
410
411        if keys.is_empty() {
412            return Ok(0);
413        }
414
415        // Delete in batches
416        let mut deleted = 0u64;
417        for key in keys {
418            if self.conn.del(&key).await? {
419                deleted += 1;
420            }
421        }
422
423        Ok(deleted)
424    }
425
426    async fn invalidate_tags(&self, tags: &[EntityTag]) -> CacheResult<u64> {
427        // Not implemented: no redis client compiled in.
428        // Real impl: tags stored as sets (tag:<tag_value> -> [key1, ...]);
429        // SMEMBERS to get keys, then DEL.
430        let _ = tags;
431        Err(unavailable())
432    }
433
434    async fn clear(&self) -> CacheResult<()> {
435        // In production, use SCAN + DEL with prefix
436        // FLUSHDB would clear everything
437        self.conn.flush().await
438    }
439
440    async fn len(&self) -> CacheResult<usize> {
441        self.conn.dbsize().await
442    }
443
444    async fn stats(&self) -> CacheResult<BackendStats> {
445        let info = self.conn.info().await?;
446        let entries = self.conn.dbsize().await?;
447
448        Ok(BackendStats {
449            entries,
450            memory_bytes: None, // Parse from INFO
451            connections: Some(self.config.pool_size as usize),
452            info: Some(info),
453        })
454    }
455}
456
457#[cfg(test)]
458mod tests {
459    use super::*;
460
461    #[test]
462    fn test_redis_config() {
463        let config = RedisCacheConfig::new("redis://localhost:6379")
464            .with_pool_size(20)
465            .with_prefix("myapp")
466            .with_ttl(Duration::from_secs(600));
467
468        assert_eq!(config.pool_size, 20);
469        assert_eq!(config.key_prefix, "myapp");
470        assert_eq!(config.default_ttl, Some(Duration::from_secs(600)));
471    }
472
473    #[test]
474    fn test_full_key() {
475        let config = RedisCacheConfig::new("redis://localhost").with_prefix("app:cache");
476
477        let key = CacheKey::new("User", "id:123");
478        let full = config.full_key(&key);
479
480        assert_eq!(full, "app:cache:prax:User:id:123");
481    }
482
483    #[tokio::test]
484    async fn test_redis_cache_creation() {
485        // The Redis backend is not implemented: construction fails and every
486        // operation errors instead of silently succeeding. Real integration
487        // tests would need a Redis instance and a compiled-in client.
488        let config = RedisCacheConfig::default();
489
490        let err = RedisCache::new(config.clone())
491            .await
492            .err()
493            .expect("redis construction should fail: no client compiled in");
494        match &err {
495            CacheError::Backend(msg) => assert!(msg.contains("not available")),
496            other => panic!("expected backend error, got {other:?}"),
497        }
498
499        // Build directly (same-module access to private fields) to verify a
500        // constructed instance also errors loudly on use.
501        let conn = RedisConnection {
502            config: config.clone(),
503        };
504        let cache = RedisCache { conn, config };
505
506        assert_eq!(cache.config().pool_size, 10);
507
508        let key = CacheKey::new("test", "key");
509        assert!(cache.set(&key, &"value", None).await.is_err());
510        let value: CacheResult<Option<String>> = cache.get(&key).await;
511        assert!(value.is_err());
512    }
513}