1use std::time::Duration;
32
33use super::backend::{BackendStats, CacheBackend, CacheError, CacheResult};
34use super::invalidation::EntityTag;
35use super::key::{CacheKey, KeyPattern};
36
37fn unavailable() -> CacheError {
39 CacheError::Backend("redis backend not available: no redis client compiled in".to_string())
40}
41
42#[derive(Debug, Clone)]
44pub struct RedisCacheConfig {
45 pub url: String,
47 pub pool_size: u32,
49 pub connection_timeout: Duration,
51 pub command_timeout: Duration,
53 pub key_prefix: String,
55 pub default_ttl: Option<Duration>,
57 pub cluster_mode: bool,
59 pub database: u8,
61 pub tls: bool,
63 pub username: Option<String>,
65 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 pub fn new(url: impl Into<String>) -> Self {
90 Self {
91 url: url.into(),
92 ..Default::default()
93 }
94 }
95
96 pub fn with_pool_size(mut self, size: u32) -> Self {
98 self.pool_size = size;
99 self
100 }
101
102 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
104 self.key_prefix = prefix.into();
105 self
106 }
107
108 pub fn with_ttl(mut self, ttl: Duration) -> Self {
110 self.default_ttl = Some(ttl);
111 self
112 }
113
114 pub fn cluster(mut self) -> Self {
116 self.cluster_mode = true;
117 self
118 }
119
120 pub fn database(mut self, db: u8) -> Self {
122 self.database = db;
123 self
124 }
125
126 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 fn full_key(&self, key: &CacheKey) -> String {
135 format!("{}:{}", self.key_prefix, key.as_str())
136 }
137}
138
139#[derive(Clone)]
146pub struct RedisConnection {
147 config: RedisCacheConfig,
148 }
150
151impl RedisConnection {
152 pub async fn new(config: RedisCacheConfig) -> CacheResult<Self> {
159 let _ = config;
160 Err(unavailable())
161 }
162
163 pub fn config(&self) -> &RedisCacheConfig {
165 &self.config
166 }
167
168 async fn execute<T>(&self, _cmd: &str, _args: &[&str]) -> CacheResult<T>
170 where
171 T: Default,
172 {
173 Err(unavailable())
178 }
179
180 pub async fn get(&self, key: &str) -> CacheResult<Option<Vec<u8>>> {
182 let _ = key;
183 Err(unavailable())
184 }
185
186 pub async fn set(&self, key: &str, value: &[u8], ttl: Option<Duration>) -> CacheResult<()> {
188 let _ = (key, value, ttl);
189 Err(unavailable())
190 }
191
192 pub async fn del(&self, key: &str) -> CacheResult<bool> {
194 let _ = key;
195 Err(unavailable())
196 }
197
198 pub async fn exists(&self, key: &str) -> CacheResult<bool> {
200 let _ = key;
201 Err(unavailable())
202 }
203
204 pub async fn keys(&self, pattern: &str) -> CacheResult<Vec<String>> {
206 let _ = pattern;
207 Err(unavailable())
208 }
209
210 pub async fn mget(&self, keys: &[String]) -> CacheResult<Vec<Option<Vec<u8>>>> {
212 let _ = keys;
213 Err(unavailable())
214 }
215
216 pub async fn mset(&self, pairs: &[(String, Vec<u8>)]) -> CacheResult<()> {
218 let _ = pairs;
219 Err(unavailable())
220 }
221
222 pub async fn flush(&self) -> CacheResult<()> {
224 Err(unavailable())
225 }
226
227 pub async fn dbsize(&self) -> CacheResult<usize> {
229 Err(unavailable())
230 }
231
232 pub async fn info(&self) -> CacheResult<String> {
234 Err(unavailable())
235 }
236
237 pub async fn scan(&self, pattern: &str, count: usize) -> CacheResult<Vec<String>> {
239 let _ = (pattern, count);
240 Err(unavailable())
241 }
242
243 pub fn pipeline(&self) -> RedisPipeline {
245 RedisPipeline::new(self.clone())
246 }
247}
248
249pub 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 pub fn get(mut self, key: impl Into<String>) -> Self {
271 self.commands.push(PipelineCommand::Get(key.into()));
272 self
273 }
274
275 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 pub fn del(mut self, key: impl Into<String>) -> Self {
284 self.commands.push(PipelineCommand::Del(key.into()));
285 self
286 }
287
288 pub async fn execute(self) -> CacheResult<Vec<PipelineResult>> {
290 Err(unavailable())
292 }
293}
294
295#[derive(Debug, Clone)]
297pub enum PipelineResult {
298 Ok,
299 Value(Option<Vec<u8>>),
300 Error(String),
301}
302
303#[derive(Clone)]
309pub struct RedisCache {
310 conn: RedisConnection,
311 config: RedisCacheConfig,
312}
313
314impl RedisCache {
315 pub async fn new(config: RedisCacheConfig) -> CacheResult<Self> {
320 let conn = RedisConnection::new(config.clone()).await?;
321 Ok(Self { conn, config })
322 }
323
324 pub async fn from_url(url: &str) -> CacheResult<Self> {
328 Self::new(RedisCacheConfig::new(url)).await
329 }
330
331 pub fn connection(&self) -> &RedisConnection {
333 &self.conn
334 }
335
336 pub fn config(&self) -> &RedisCacheConfig {
338 &self.config
339 }
340
341 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 let keys = self.conn.scan(&full_pattern, 1000).await?;
410
411 if keys.is_empty() {
412 return Ok(0);
413 }
414
415 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 let _ = tags;
431 Err(unavailable())
432 }
433
434 async fn clear(&self) -> CacheResult<()> {
435 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, 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 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 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}