dmsc/cache/backends/hybrid_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//! # Hybrid Cache Backend
19//!
20//! This module provides a hybrid cache implementation that combines the speed of
21//! in-memory caching with the persistence and distributed capabilities of Redis.
22//! It follows a two-level caching strategy for optimal performance and reliability.
23//!
24//! ## Key Features
25//!
26//! - **Two-Level Caching**: Fast in-memory access with Redis persistence
27//! - **Automatic Sync**: Writes propagated to both cache levels
28//! - **Lazy Population**: Redis results cached in memory for future requests
29//! - **Consistency**: Deletes affect both caches simultaneously
30//!
31//! ## Caching Strategy
32//!
33//! 1. **Read Operations**: Check memory first, then Redis, populate memory on miss
34//! 2. **Write Operations**: Write to both memory and Redis simultaneously
35//! 3. **Delete Operations**: Remove from both caches for consistency
36//! 4. **Clear Operations**: Clear both caches together
37//!
38//! ## Design Principles
39//!
40//! 1. **Performance**: Fast in-memory access for frequently accessed data
41//! 2. **Persistence**: Redis provides data persistence and crash recovery
42//! 3. **Consistency**: Writes are propagated to both caches
43//! 4. **Scalability**: Redis enables distributed caching across multiple instances
44//! 5. **Efficiency**: Automatic caching of Redis results in memory
45//! 6. **Transparency**: Implements the same DMSCCache trait as other backends
46//!
47//! ## Usage Example
48//!
49//! ```rust,ignore
50//! use dmsc::cache::backends::DMSCHybridCache;
51//!
52//! async fn example() -> dmsc::core::DMSCResult<()> {
53//! // Create a hybrid cache with Redis connection
54//! let hybrid_cache = DMSCHybridCache::new("redis://localhost:6379").await?;
55//!
56//! // Set a value (stored in both memory and Redis)
57//! hybrid_cache.set("user:123", "{\"name\": \"Alice\"}", Some(3600)).await?;
58//!
59//! // Get a value (checked in memory first, then Redis)
60//! let value = hybrid_cache.get("user:123").await?;
61//!
62//! // Delete a value (removed from both caches)
63//! hybrid_cache.delete("user:123").await?;
64//!
65//! // Get combined statistics
66//! let stats = hybrid_cache.stats().await;
67//!
68//! Ok(())
69//! }
70//! ```
71
72#![allow(non_snake_case)]
73
74use std::sync::Arc;
75use crate::cache::{DMSCCache, DMSCCacheStats};
76use crate::core::DMSCResult;
77
78/// Hybrid cache implementation combining memory and Redis backends.
79///
80/// This struct implements a two-level caching strategy that leverages both
81/// in-memory caching for speed and Redis for persistence and distributed caching.
82pub struct DMSCHybridCache {
83 /// Fast in-memory cache
84 memory_cache: Arc<crate::cache::backends::DMSCMemoryCache>,
85 /// Persistent Redis cache
86 redis_cache: Arc<crate::cache::backends::DMSCRedisCache>,
87}
88
89impl DMSCHybridCache {
90 /// Creates a new hybrid cache instance.
91 ///
92 /// # Parameters
93 ///
94 /// - `redis_url`: Redis connection URL (e.g., "redis://localhost:6379")
95 ///
96 /// # Returns
97 ///
98 /// A new instance of `DMSCHybridCache`
99 pub async fn new(redis_url: &str) -> crate::core::DMSCResult<Self> {
100 let memory_cache = Arc::new(crate::cache::backends::DMSCMemoryCache::new());
101 let redis_cache = Arc::new(crate::cache::backends::DMSCRedisCache::new(redis_url).await?);
102
103 Ok(Self {
104 memory_cache,
105 redis_cache,
106 })
107 }
108}
109
110#[async_trait::async_trait]
111impl DMSCCache for DMSCHybridCache {
112 /// Gets a value from the hybrid cache.
113 ///
114 /// Follows a two-level lookup strategy:
115 /// 1. First checks the in-memory cache for fast access
116 /// 2. If not found, checks Redis
117 /// 3. If found in Redis, caches the result in memory for future requests
118 ///
119 /// # Parameters
120 ///
121 /// - `key`: Cache key to retrieve
122 ///
123 /// # Returns
124 ///
125 /// `Option<String>` containing the value if the key exists in either cache, otherwise `None`
126 async fn get(&self, key: &str) -> DMSCResult<Option<String>> {
127 // First check memory cache
128 if let Ok(Some(value)) = self.memory_cache.get(key).await {
129 return Ok(Some(value));
130 }
131
132 // If not in memory, check Redis
133 if let Ok(Some(value)) = self.redis_cache.get(key).await {
134 // Store in memory cache for future requests
135 let _ = self.memory_cache.set(key, &value, Some(3600)).await;
136 return Ok(Some(value));
137 }
138
139 Ok(None)
140 }
141
142 /// Sets a value in both caches.
143 ///
144 /// Writes the value to both the in-memory cache and Redis simultaneously
145 /// to ensure consistency across both cache levels.
146 ///
147 /// # Parameters
148 ///
149 /// - `key`: Cache key to set
150 /// - `value`: Value to store in the cache
151 /// - `ttl_seconds`: Optional TTL in seconds
152 ///
153 /// # Returns
154 ///
155 /// `Ok(())` if the value was successfully set in both caches
156 async fn set(&self, key: &str, value: &str, ttl_seconds: Option<u64>) -> crate::core::DMSCResult<()> {
157 // Set in both caches
158 self.memory_cache.set(key, value, ttl_seconds).await?;
159 self.redis_cache.set(key, value, ttl_seconds).await?;
160 Ok(())
161 }
162
163 /// Deletes a value from both caches.
164 ///
165 /// Removes the value from both the in-memory cache and Redis to ensure
166 /// consistency across both cache levels.
167 ///
168 /// # Parameters
169 ///
170 /// - `key`: Cache key to delete
171 ///
172 /// # Returns
173 ///
174 /// `Ok(bool)` indicating whether the key was found in either cache
175 async fn delete(&self, key: &str) -> crate::core::DMSCResult<bool> {
176 // Delete from both caches
177 let memory_deleted = self.memory_cache.delete(key).await?;
178 let redis_deleted = self.redis_cache.delete(key).await?;
179 Ok(memory_deleted || redis_deleted)
180 }
181
182 /// Checks if a key exists in either cache.
183 ///
184 /// First checks the in-memory cache, then Redis if not found.
185 ///
186 /// # Parameters
187 ///
188 /// - `key`: Cache key to check
189 ///
190 /// # Returns
191 ///
192 /// `true` if the key exists in either cache, otherwise `false`
193 async fn exists(&self, key: &str) -> bool {
194 // Check memory first, then Redis
195 self.memory_cache.exists(key).await || self.redis_cache.exists(key).await
196 }
197
198 /// Gets all cache keys from both caches.
199 ///
200 /// Returns keys from both the in-memory cache and Redis.
201 ///
202 /// # Returns
203 ///
204 /// A `DMSCResult<Vec<String>>` containing all cache keys
205 async fn keys(&self) -> crate::core::DMSCResult<Vec<String>> {
206 let memory_keys = self.memory_cache.keys().await?;
207 let redis_keys = self.redis_cache.keys().await?;
208
209 // Combine and deduplicate keys
210 let mut all_keys = memory_keys;
211 for key in redis_keys {
212 if !all_keys.contains(&key) {
213 all_keys.push(key);
214 }
215 }
216
217 Ok(all_keys)
218 }
219
220 /// Clears both caches.
221 ///
222 /// Removes all entries from both the in-memory cache and Redis.
223 ///
224 /// # Returns
225 ///
226 /// `Ok(())` if both caches were successfully cleared
227 async fn clear(&self) -> crate::core::DMSCResult<()> {
228 // Clear both caches
229 self.memory_cache.clear().await?;
230 self.redis_cache.clear().await?;
231 Ok(())
232 }
233
234 /// Gets combined statistics from both caches.
235 ///
236 /// Aggregates statistics from both the in-memory cache and Redis,
237 /// including total keys, memory usage, hit/miss counts, and eviction counts.
238 ///
239 /// # Returns
240 ///
241 /// A `DMSCCacheStats` struct containing combined statistics from both caches
242 async fn stats(&self) -> DMSCCacheStats {
243 let memory_stats = self.memory_cache.stats().await;
244 let redis_stats = self.redis_cache.stats().await;
245
246 DMSCCacheStats {
247 hits: memory_stats.hits + redis_stats.hits,
248 misses: memory_stats.misses + redis_stats.misses,
249 entries: memory_stats.entries + redis_stats.entries,
250 memory_usage_bytes: memory_stats.memory_usage_bytes + redis_stats.memory_usage_bytes,
251 avg_hit_rate: (memory_stats.avg_hit_rate + redis_stats.avg_hit_rate) / 2.0,
252 hit_count: memory_stats.hit_count + redis_stats.hit_count,
253 miss_count: memory_stats.miss_count + redis_stats.miss_count,
254 eviction_count: memory_stats.eviction_count + redis_stats.eviction_count,
255 }
256 }
257
258 /// Cleans up expired entries from both caches.
259 ///
260 /// Removes expired entries from both the in-memory cache and Redis.
261 ///
262 /// # Returns
263 ///
264 /// The total number of expired entries cleaned up from both caches
265 async fn cleanup_expired(&self) -> crate::core::DMSCResult<usize> {
266 let memory_cleaned = self.memory_cache.cleanup_expired().await?;
267 let redis_cleaned = self.redis_cache.cleanup_expired().await?;
268 Ok(memory_cleaned + redis_cleaned)
269 }
270}