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}