Skip to main content

backbone_cache/
redis.rs

1//! Redis cache implementation
2
3use redis::{Client, AsyncCommands, aio::ConnectionManager};
4use async_trait::async_trait;
5use serde::{Serialize, Deserialize};
6use crate::{CacheResult, CacheError, CacheStats, Cache};
7
8/// Redis cache implementation
9pub struct RedisCache {
10    #[allow(dead_code)] // Client kept for future use (health checks, etc.)
11    client: Client,
12    connection: ConnectionManager,
13    key_prefix: Option<String>,
14}
15
16impl RedisCache {
17    /// Create new Redis cache
18    pub async fn new(redis_url: &str) -> CacheResult<Self> {
19        let client = Client::open(redis_url)
20            .map_err(|e| CacheError::RedisConnection(e.to_string()))?;
21
22        let connection = client.get_connection_manager()
23            .await
24            .map_err(|e| CacheError::RedisConnection(e.to_string()))?;
25
26        Ok(Self {
27            client,
28            connection,
29            key_prefix: None,
30        })
31    }
32
33    /// Create Redis cache with configuration
34    pub async fn with_config(redis_url: &str, key_prefix: Option<String>) -> CacheResult<Self> {
35        let mut cache = Self::new(redis_url).await?;
36        cache.key_prefix = key_prefix;
37        Ok(cache)
38    }
39
40    /// Build full key with prefix
41    fn build_key(&self, key: &str) -> String {
42        match &self.key_prefix {
43            Some(prefix) => format!("{}:{}", prefix, key),
44            None => key.to_string(),
45        }
46    }
47
48    /// Serialize value to JSON
49    fn serialize<T: Serialize>(value: &T) -> CacheResult<String> {
50        serde_json::to_string(value)
51            .map_err(|e| CacheError::Serialization(e.to_string()))
52    }
53
54    /// Deserialize value from JSON
55    fn deserialize<T: for<'de> Deserialize<'de>>(value: &str) -> CacheResult<T> {
56        serde_json::from_str(value)
57            .map_err(|e| CacheError::Deserialization(e.to_string()))
58    }
59}
60
61#[async_trait]
62impl Cache for RedisCache {
63    async fn set<T>(&self, key: &str, value: &T, ttl_seconds: Option<u64>) -> CacheResult<()>
64    where
65        T: Serialize + Send + Sync,
66    {
67        let full_key = self.build_key(key);
68        let serialized = Self::serialize(value)?;
69
70        let mut conn = self.connection.clone();
71        let result: redis::RedisResult<()> = match ttl_seconds {
72            Some(ttl) => conn.set_ex(&full_key, serialized, ttl).await,
73            None => conn.set(&full_key, serialized).await,
74        };
75
76        result.map_err(|e| CacheError::RedisOperation(e.to_string()))
77    }
78
79    async fn get<T>(&self, key: &str) -> CacheResult<Option<T>>
80    where
81        T: for<'de> Deserialize<'de> + Send + Sync,
82    {
83        let full_key = self.build_key(key);
84        let mut conn = self.connection.clone();
85
86        let result: Option<String> = conn.get(&full_key).await
87            .map_err(|e| CacheError::RedisOperation(e.to_string()))?;
88
89        let Some(value) = result else {
90            return Ok(None);
91        };
92
93        let deserialized = Self::deserialize::<T>(&value)?;
94        Ok(Some(deserialized))
95    }
96
97    async fn delete(&self, key: &str) -> CacheResult<bool> {
98        let full_key = self.build_key(key);
99        let mut conn = self.connection.clone();
100
101        let count: i32 = conn.del(&full_key).await
102            .map_err(|e| CacheError::RedisOperation(e.to_string()))?;
103
104        Ok(count > 0)
105    }
106
107    async fn exists(&self, key: &str) -> CacheResult<bool> {
108        let full_key = self.build_key(key);
109
110        let mut conn = self.connection.clone();
111        let result: redis::RedisResult<bool> = conn.exists(&full_key).await;
112
113        result.map_err(|e| CacheError::RedisOperation(e.to_string()))
114    }
115
116    async fn expire(&self, key: &str, ttl_seconds: u64) -> CacheResult<bool> {
117        let full_key = self.build_key(key);
118
119        let mut conn = self.connection.clone();
120        let result: redis::RedisResult<bool> = conn.expire(&full_key, ttl_seconds as i64).await;
121
122        result.map_err(|e| CacheError::RedisOperation(e.to_string()))
123    }
124
125    async fn ttl(&self, key: &str) -> CacheResult<Option<u64>> {
126        let full_key = self.build_key(key);
127        let mut conn = self.connection.clone();
128
129        let ttl: i64 = conn.ttl(&full_key).await
130            .map_err(|e| CacheError::RedisOperation(e.to_string()))?;
131
132        match ttl {
133            -1 => Ok(None), // No expiration
134            -2 => Ok(Some(0)), // Key doesn't exist
135            ttl if ttl >= 0 => Ok(Some(ttl as u64)),
136            _ => Err(CacheError::RedisOperation(format!("Invalid TTL: {}", ttl))),
137        }
138    }
139
140    async fn clear(&self) -> CacheResult<()> {
141        let mut conn = self.connection.clone();
142
143        // Only delete keys with our prefix to avoid clearing entire Redis database
144        let Some(prefix) = &self.key_prefix else {
145            return Err(CacheError::Other(
146                "Cannot clear Redis cache without key prefix for safety".to_string()
147            ));
148        };
149
150        let pattern = format!("{}:*", prefix);
151        let keys: redis::RedisResult<Vec<String>> = conn.keys(&pattern).await;
152
153        let key_list = keys.map_err(|e| CacheError::RedisOperation(e.to_string()))?;
154
155        // Delete keys if any exist
156        if !key_list.is_empty() {
157            let _: redis::RedisResult<i32> = conn.del(&key_list).await;
158        }
159
160        Ok(())
161    }
162
163    async fn stats(&self) -> CacheResult<CacheStats> {
164        let mut conn = self.connection.clone();
165        let mut stats = CacheStats::default();
166
167        // Get Redis info
168        let info: redis::RedisResult<String> = redis::cmd("INFO")
169            .arg("memory")
170            .arg("stats")
171            .query_async(&mut conn)
172            .await;
173
174        // Parse Redis info for stats if successful
175        if let Ok(info_str) = info {
176            for line in info_str.lines() {
177                let Some(value_str) = line.split(':').nth(1) else {
178                    continue;
179                };
180
181                let Ok(value) = value_str.parse::<u64>() else {
182                    continue;
183                };
184
185                match line {
186                    l if l.starts_with("used_memory:") => stats.memory_usage = Some(value),
187                    l if l.starts_with("keyspace_hits:") => stats.hits = value,
188                    l if l.starts_with("keyspace_misses:") => stats.misses = value,
189                    _ => {}
190                }
191            }
192        }
193
194        // Get total keys count (for our namespace only if prefix is set)
195        if let Some(prefix) = &self.key_prefix {
196            let pattern = format!("{}:*", prefix);
197            let keys: redis::RedisResult<Vec<String>> = conn.keys(&pattern).await;
198
199            if let Ok(key_list) = keys {
200                stats.total_entries = key_list.len() as u64;
201            }
202        }
203
204        stats.update_hit_rate();
205        Ok(stats)
206    }
207
208    async fn mget<T>(&self, keys: Vec<String>) -> CacheResult<Vec<(String, Option<T>)>>
209    where
210        T: for<'de> Deserialize<'de> + Send + Sync,
211    {
212        let full_keys: Vec<String> = keys.iter()
213            .map(|key| self.build_key(key))
214            .collect();
215
216        let mut conn = self.connection.clone();
217        let values: Vec<Option<String>> = conn.mget(&full_keys).await
218            .map_err(|e| CacheError::RedisOperation(e.to_string()))?;
219
220        let mut results = Vec::with_capacity(keys.len());
221
222        for (i, key) in keys.into_iter().enumerate() {
223            let result = match &values[i] {
224                Some(value) => {
225                    Self::deserialize::<T>(value)
226                        .map(Some)
227                        .unwrap_or(None)
228                }
229                None => None,
230            };
231            results.push((key, result));
232        }
233
234        Ok(results)
235    }
236
237    async fn mset<T>(&self, entries: Vec<(String, T, Option<u64>)>) -> CacheResult<()>
238    where
239        T: Serialize + Send + Sync,
240    {
241        if entries.is_empty() {
242            return Ok(());
243        }
244
245        // Group entries by TTL to optimize Redis operations
246        let mut with_ttl = Vec::new();
247        let mut without_ttl = Vec::new();
248
249        for (key, value, ttl) in entries {
250            let full_key = self.build_key(&key);
251            let serialized = Self::serialize(&value)?;
252
253            match ttl {
254                Some(ttl_seconds) => with_ttl.push((full_key, serialized, ttl_seconds)),
255                None => without_ttl.push((full_key, serialized)),
256            }
257        }
258
259        let mut conn = self.connection.clone();
260
261        // Set entries without TTL using MSET
262        if !without_ttl.is_empty() {
263            let mut pipeline = redis::pipe();
264            for (key, value) in without_ttl {
265                pipeline.set(&key, value);
266            }
267
268            pipeline.query_async::<_, ()>(&mut conn).await
269                .map_err(|e| CacheError::RedisOperation(e.to_string()))?;
270        }
271
272        // Set entries with TTL individually (Redis doesn't support MSET with TTL)
273        for (key, value, ttl_seconds) in with_ttl {
274            conn.set_ex::<_, _, ()>(&key, value, ttl_seconds).await
275                .map_err(|e| CacheError::RedisOperation(e.to_string()))?;
276        }
277
278        Ok(())
279    }
280
281    async fn mdelete(&self, keys: Vec<String>) -> CacheResult<u64> {
282        if keys.is_empty() {
283            return Ok(0);
284        }
285
286        let full_keys: Vec<String> = keys.iter()
287            .map(|key| self.build_key(key))
288            .collect();
289
290        let mut conn = self.connection.clone();
291        let count: i32 = conn.del(&full_keys).await
292            .map_err(|e| CacheError::RedisOperation(e.to_string()))?;
293
294        Ok(count as u64)
295    }
296}