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}