dmsc/cache/backends/
redis_backend.rs

1//! Copyright © 2025-2026 Wenze Wei. All Rights Reserved.
2//!
3//! This file is part of DMSC.
4//! The DMSC project belongs to the Dunimd Team.
5//!
6//! Licensed under the Apache License, Version 2.0 (the "License");
7//! You may not use this file except in compliance with the License.
8//! You may obtain a copy of the License at
9//!
10//!     http://www.apache.org/licenses/LICENSE-2.0
11//!
12//! Unless required by applicable law or agreed to in writing, software
13//! distributed under the License is distributed on an "AS IS" BASIS,
14//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15//! See the License for the specific language governing permissions and
16//! limitations under the License.
17
18#![cfg(feature = "redis")]
19
20//! # Redis Cache Backend
21//!
22//! This module provides a Redis-based cache implementation that offers persistence,
23//! distributed caching capabilities, and automatic expiration handling. It implements
24//! the [`DMSCCache`](crate::cache::DMSCCache) trait for consistency with other cache backends.
25//!
26//! ## Key Features
27//!
28//! - **Persistence**: Redis provides data persistence to disk
29//! - **Distributed**: Supports distributed caching across multiple instances
30//! - **Automatic Expiration**: Leverages Redis' built-in TTL (Time-To-Live) mechanism
31//! - **Connection Pooling**: Uses Redis connection pooling for efficient resource usage
32//! - **Statistics Tracking**: Tracks hit/miss counts and error rates
33//! - **Safety**: Uses pattern matching to avoid clearing all Redis data
34//! - **Async Operations**: Fully asynchronous implementation
35//!
36//! ## Design Principles
37//!
38//! 1. **Reliability**: Leverages Redis' proven persistence and clustering capabilities
39//! 2. **Efficiency**: Connection pooling for optimal resource utilization
40//! 3. **Consistency**: Same interface as other backends via DMSCCache trait
41//! 4. **Observability**: Comprehensive statistics for monitoring cache performance
42//! 5. **Safety First**: Pattern-based operations prevent accidental data loss
43//!
44//! ## Usage Example
45//!
46//! ```rust,ignore
47//! use dmsc::cache::backends::DMSCRedisCache;
48//!
49//! async fn example() -> dmsc::core::DMSCResult<()> {
50//!     // Create a Redis cache instance
51//!     let redis_cache = DMSCRedisCache::new("redis://localhost:6379").await?;
52//!
53//!     // Set a value with expiration
54//!     redis_cache.set("user:123", "{\"name\": \"Alice\"}", Some(3600)).await?;
55//!
56//!     // Get a value
57//!     let value = redis_cache.get("user:123").await?;
58//!
59//!     // Check if a key exists
60//!     let exists = redis_cache.exists("user:123").await;
61//!
62//!     // Delete a value
63//!     redis_cache.delete("user:123").await?;
64//!
65//!     // Get cache statistics
66//!     let stats = redis_cache.stats().await;
67//!
68//!     Ok(())
69//! }
70//! ```
71
72#![allow(non_snake_case)]
73
74use redis::{AsyncCommands, Client};
75use redis::aio::ConnectionManager;
76use std::sync::Arc;
77use std::ops::AddAssign;
78use crate::cache::{DMSCCache, DMSCCacheStats};
79use crate::core::DMSCResult;
80
81/// Redis cache implementation.
82///
83/// This struct provides a Redis-based cache implementation that leverages Redis'
84/// persistence, distributed capabilities, and built-in expiration mechanism.
85pub struct DMSCRedisCache {
86    /// Redis connection manager for efficient connection pooling
87    connection: Arc<ConnectionManager>,
88    /// Thread-safe statistics tracking
89    stats: Arc<dashmap::DashMap<&'static str, u64>>,
90}
91
92impl DMSCRedisCache {
93    /// Creates a new Redis cache instance.
94    ///
95    /// # Parameters
96    ///
97    /// - `redis_url`: Redis connection URL (e.g., "redis://localhost:6379")
98    ///
99    /// # Returns
100    ///
101    /// A new instance of `DMSCRedisCache`
102    ///
103    /// # Errors
104    ///
105    /// Returns an error if the Redis client cannot be created or if the connection fails
106    pub async fn new(redis_url: &str) -> crate::core::DMSCResult<Self> {
107        let client = Client::open(redis_url)
108            .map_err(|e| crate::core::DMSCError::Other(format!("Redis client error: {e}")))?;
109
110        let connection = ConnectionManager::new(client).await
111            .map_err(|e| crate::core::DMSCError::Other(format!("Redis connection error: {e}")))?;
112
113        let stats = dashmap::DashMap::new();
114        stats.insert("hit_count", 0);
115        stats.insert("miss_count", 0);
116        stats.insert("error_count", 0);
117
118        Ok(Self {
119            connection: Arc::new(connection),
120            stats: Arc::new(stats),
121        })
122    }
123}
124
125#[async_trait::async_trait]
126impl DMSCCache for DMSCRedisCache {
127    /// Gets a value from Redis cache.
128    ///
129    /// # Parameters
130    ///
131    /// - `key`: Cache key to retrieve
132    ///
133    /// # Returns
134    ///
135    /// `Option<String>` containing the value if the key exists, otherwise `None`
136    ///
137    /// # Implementation Details
138    ///
139    /// 1. Retrieves the value from Redis
140    /// 2. Attempts to parse as JSON string
141    /// 3. Updates hit/miss statistics accordingly
142    /// 4. Returns the parsed string value
143    async fn get(&self, key: &str) -> DMSCResult<Option<String>> {
144        let mut conn = (*self.connection).clone();
145
146        let result: redis::RedisResult<String> = conn.get(key).await;
147        match result {
148            Ok(json_str) => {
149                let json_str_owned = json_str.to_owned();
150                // Try to parse as simple string first, then as JSON if that fails
151                if let Ok(value) = serde_json::from_str::<serde_json::Value>(&json_str_owned) {
152                    if let Some(str_value) = value.as_str() {
153                        if let Some(mut hit_count) = self.stats.get_mut("hit_count") {
154                            hit_count.value_mut().add_assign(1);
155                        }
156                        Ok(Some(str_value.to_string()))
157                    } else {
158                        if let Some(mut error_count) = self.stats.get_mut("error_count") {
159                            error_count.value_mut().add_assign(1);
160                        }
161                        Ok(None)
162                    }
163                } else {
164                    // If not valid JSON, treat as plain string
165                    if let Some(mut hit_count) = self.stats.get_mut("hit_count") {
166                        hit_count.value_mut().add_assign(1);
167                    }
168                    Ok(Some(json_str_owned))
169                }
170            }
171            Err(_) => {
172                if let Some(mut miss_count) = self.stats.get_mut("miss_count") {
173                    miss_count.value_mut().add_assign(1);
174                }
175                Ok(None)
176            }
177        }
178    }
179
180    /// Sets a value in Redis cache.
181    ///
182    /// # Parameters
183    ///
184    /// - `key`: Cache key to set
185    /// - `value`: Value to store in the cache
186    /// - `ttl_seconds`: Optional TTL in seconds
187    ///
188    /// # Returns
189    ///
190    /// `Ok(())` if the value was successfully set, otherwise an error
191    ///
192    /// # Implementation Details
193    ///
194    /// 1. Serializes the string value
195    /// 2. Uses SET or SETEX command depending on TTL specification
196    async fn set(&self, key: &str, value: &str, ttl_seconds: Option<u64>) -> crate::core::DMSCResult<()> {
197        let mut conn = (*self.connection).clone();
198
199        let result: redis::RedisResult<()> = match ttl_seconds {
200            Some(ttl_secs) => {
201                conn.set_ex(key, value, ttl_secs).await
202            }
203            None => {
204                conn.set(key, value).await
205            }
206        };
207
208        result.map_err(|e| crate::core::DMSCError::Other(format!("Redis set error: {e}")))?;
209        Ok(())
210    }
211
212    /// Deletes a value from Redis cache.
213    ///
214    /// # Parameters
215    ///
216    /// - `key`: Cache key to delete
217    ///
218    /// # Returns
219    ///
220    /// `Ok(true)` if the key was found and deleted, `Ok(false)` if the key didn't exist
221    async fn delete(&self, key: &str) -> crate::core::DMSCResult<bool> {
222        let mut conn = (*self.connection).clone();
223        let result: redis::RedisResult<bool> = conn.del(key).await;
224        result.map_err(|e| crate::core::DMSCError::Other(format!("Redis delete error: {e}")))
225    }
226
227    /// Checks if a key exists in Redis cache.
228    ///
229    /// # Parameters
230    ///
231    /// - `key`: Cache key to check
232    ///
233    /// # Returns
234    ///
235    /// `true` if the key exists, otherwise `false`
236    async fn exists(&self, key: &str) -> bool {
237        let mut conn = (*self.connection).clone();
238
239        let result: redis::RedisResult<bool> = conn.exists(key).await;
240        result.unwrap_or_default()
241    }
242
243    /// Gets all cache keys from Redis.
244    ///
245    /// # Returns
246    ///
247    /// A `DMSCResult<Vec<String>>` containing all cache keys matching the DMSC pattern
248    async fn keys(&self) -> crate::core::DMSCResult<Vec<String>> {
249        let mut conn = (*self.connection).clone();
250
251        let pattern = "dmsc:cache:*";
252        let keys: Vec<String> = conn.keys(pattern).await
253            .map_err(|e| crate::core::DMSCError::Other(format!("Redis keys error: {e}")))?;
254
255        Ok(keys)
256    }
257
258    /// Clears all DMSC-related cache entries from Redis.
259    ///
260    /// # Returns
261    ///
262    /// `Ok(())` if the cache was successfully cleared, otherwise an error
263    ///
264    /// # Notes
265    ///
266    /// - Uses the pattern "dmsc:cache:*" to avoid clearing all Redis data
267    /// - Only clears keys matching the DMSC cache pattern
268    async fn clear(&self) -> crate::core::DMSCResult<()> {
269        let mut conn = (*self.connection).clone();
270
271        // Use a specific pattern to avoid clearing all Redis data
272        let pattern = "dmsc:cache:*";
273        let keys: Vec<String> = conn.keys(pattern).await
274            .map_err(|e| crate::core::DMSCError::Other(format!("Redis keys error: {e}")))?;
275
276        if !keys.is_empty() {
277            conn.del::<_, ()>(keys).await
278                .map_err(|e| crate::core::DMSCError::Other(format!("Redis clear error: {e}")))?;
279        }
280
281        Ok(())
282    }
283
284    /// Gets cache statistics.
285    ///
286    /// # Returns
287    ///
288    /// A `DMSCCacheStats` struct containing cache statistics
289    ///
290    /// # Statistics Included
291    ///
292    /// - Total keys (approximate using DBSIZE command)
293    /// - Hit count
294    /// - Miss count
295    /// - Error count (used as eviction count)
296    /// - Average hit rate
297    /// - Memory usage (always 0 as Redis manages memory)
298    async fn stats(&self) -> DMSCCacheStats {
299        let hit_count = self.stats.get("hit_count")
300            .map(|entry| *entry.value())
301            .unwrap_or(0);
302        let miss_count = self.stats.get("miss_count")
303            .map(|entry| *entry.value())
304            .unwrap_or(0);
305        let error_count = self.stats.get("error_count")
306            .map(|entry| *entry.value())
307            .unwrap_or(0);
308
309        let total_requests = hit_count + miss_count;
310        let avg_hit_rate = if total_requests > 0 {
311            hit_count as f64 / total_requests as f64
312        } else {
313            0.0
314        };
315
316        // Get total keys (approximate)
317        let total_keys = match redis::cmd("DBSIZE").query_async::<_, u64>(&mut (*self.connection).clone()).await {
318            Ok(size) => size as usize,
319            Err(_) => 0,
320        };
321
322        DMSCCacheStats {
323            hits: hit_count,
324            misses: miss_count,
325            entries: total_keys,
326            memory_usage_bytes: 0, // Redis manages memory
327            avg_hit_rate,
328            hit_count,
329            miss_count,
330            eviction_count: error_count,
331        }
332    }
333
334    /// Cleans up expired entries from the cache.
335    ///
336    /// # Returns
337    ///
338    /// Always returns `Ok(0)` as Redis automatically handles expiration
339    ///
340    /// # Notes
341    ///
342    /// Redis uses an active expiration policy with lazy deletion, so no manual cleanup is needed
343    async fn cleanup_expired(&self) -> crate::core::DMSCResult<usize> {
344        // Redis automatically handles expiration
345        Ok(0)
346    }
347}