1use crate::error::CacheResult;
4use crate::traits::CacheStore;
5use async_trait::async_trait;
6use std::collections::HashMap;
7use std::sync::Arc;
8use std::time::Duration;
9use tokio::sync::RwLock;
10
11pub struct TieredCache<L1, L2>
13where
14 L1: CacheStore,
15 L2: CacheStore,
16{
17 l1: Arc<L1>,
19
20 l2: Arc<L2>,
22
23 config: TieredCacheConfig,
25}
26
27#[derive(Debug, Clone)]
29pub struct TieredCacheConfig {
30 pub enable_l1: bool,
32
33 pub enable_l2: bool,
35
36 pub write_through: bool,
38
39 pub promote_to_l1: bool,
41
42 pub l1_ttl_fraction: f64,
44}
45
46impl Default for TieredCacheConfig {
47 fn default() -> Self {
48 Self {
49 enable_l1: true,
50 enable_l2: true,
51 write_through: true,
52 promote_to_l1: true,
53 l1_ttl_fraction: 0.25, }
55 }
56}
57
58impl<L1, L2> TieredCache<L1, L2>
59where
60 L1: CacheStore,
61 L2: CacheStore,
62{
63 pub fn new(l1: Arc<L1>, l2: Arc<L2>) -> Self {
75 Self::with_config(l1, l2, TieredCacheConfig::default())
76 }
77
78 pub fn with_config(l1: Arc<L1>, l2: Arc<L2>, config: TieredCacheConfig) -> Self {
80 Self { l1, l2, config }
81 }
82
83 pub async fn get(&self, key: &str) -> CacheResult<Option<String>> {
85 if self.config.enable_l1
87 && let Some(value) = self.l1.get_json(key).await?
88 {
89 return Ok(Some(value));
90 }
91
92 if self.config.enable_l2
94 && let Some(value) = self.l2.get_json(key).await?
95 {
96 if self.config.enable_l1 && self.config.promote_to_l1 {
98 let l2_ttl = self.l2.ttl(key).await?;
100 let l1_ttl = l2_ttl.map(|ttl| {
101 Duration::from_secs_f64(ttl.as_secs_f64() * self.config.l1_ttl_fraction)
102 });
103 let _ = self.l1.set_json(key, value.clone(), l1_ttl).await;
104 }
105 return Ok(Some(value));
106 }
107
108 Ok(None)
109 }
110
111 pub async fn set(&self, key: &str, value: String, ttl: Option<Duration>) -> CacheResult<()> {
113 if self.config.enable_l2 {
115 self.l2.set_json(key, value.clone(), ttl).await?;
116 }
117
118 if self.config.enable_l1 && (self.config.write_through || !self.config.enable_l2) {
120 let l1_ttl = ttl.map(|ttl| {
121 Duration::from_secs_f64(ttl.as_secs_f64() * self.config.l1_ttl_fraction)
122 });
123 self.l1.set_json(key, value, l1_ttl).await?;
124 }
125
126 Ok(())
127 }
128
129 pub async fn delete(&self, key: &str) -> CacheResult<()> {
131 if self.config.enable_l1 {
132 self.l1.delete(key).await?;
133 }
134 if self.config.enable_l2 {
135 self.l2.delete(key).await?;
136 }
137 Ok(())
138 }
139
140 pub async fn exists(&self, key: &str) -> CacheResult<bool> {
142 if self.config.enable_l1 && self.l1.exists(key).await? {
143 return Ok(true);
144 }
145 if self.config.enable_l2 {
146 return self.l2.exists(key).await;
147 }
148 Ok(false)
149 }
150
151 pub async fn clear(&self) -> CacheResult<()> {
153 if self.config.enable_l1 {
154 self.l1.clear().await?;
155 }
156 if self.config.enable_l2 {
157 self.l2.clear().await?;
158 }
159 Ok(())
160 }
161
162 pub async fn stats(&self) -> CacheStats {
164 CacheStats {
165 l1_enabled: self.config.enable_l1,
166 l2_enabled: self.config.enable_l2,
167 write_through: self.config.write_through,
168 promote_to_l1: self.config.promote_to_l1,
169 }
170 }
171}
172
173impl<L1, L2> Clone for TieredCache<L1, L2>
174where
175 L1: CacheStore,
176 L2: CacheStore,
177{
178 fn clone(&self) -> Self {
179 Self {
180 l1: self.l1.clone(),
181 l2: self.l2.clone(),
182 config: self.config.clone(),
183 }
184 }
185}
186
187#[derive(Debug, Clone)]
189pub struct CacheStats {
190 pub l1_enabled: bool,
191 pub l2_enabled: bool,
192 pub write_through: bool,
193 pub promote_to_l1: bool,
194}
195
196pub struct InMemoryCache {
198 data: Arc<RwLock<HashMap<String, CacheEntry>>>,
199}
200
201#[derive(Clone)]
202struct CacheEntry {
203 value: String,
204 expires_at: Option<tokio::time::Instant>,
205}
206
207impl InMemoryCache {
208 pub fn new() -> Self {
210 Self {
211 data: Arc::new(RwLock::new(HashMap::new())),
212 }
213 }
214
215 #[allow(dead_code)]
217 async fn cleanup_expired(&self) {
218 let mut data = self.data.write().await;
219 let now = tokio::time::Instant::now();
220 data.retain(|_, entry| entry.expires_at.is_none_or(|exp| exp > now));
221 }
222}
223
224impl Default for InMemoryCache {
225 fn default() -> Self {
226 Self::new()
227 }
228}
229
230#[async_trait]
231impl CacheStore for InMemoryCache {
232 async fn get_json(&self, key: &str) -> CacheResult<Option<String>> {
233 let data = self.data.read().await;
234 if let Some(entry) = data.get(key) {
235 if let Some(expires_at) = entry.expires_at
236 && tokio::time::Instant::now() > expires_at
237 {
238 return Ok(None); }
240 Ok(Some(entry.value.clone()))
241 } else {
242 Ok(None)
243 }
244 }
245
246 async fn set_json(&self, key: &str, value: String, ttl: Option<Duration>) -> CacheResult<()> {
247 let expires_at = ttl.map(|d| tokio::time::Instant::now() + d);
248 let entry = CacheEntry { value, expires_at };
249 self.data.write().await.insert(key.to_string(), entry);
250 Ok(())
251 }
252
253 async fn delete(&self, key: &str) -> CacheResult<()> {
254 self.data.write().await.remove(key);
255 Ok(())
256 }
257
258 async fn exists(&self, key: &str) -> CacheResult<bool> {
259 self.get_json(key).await.map(|v| v.is_some())
260 }
261
262 async fn clear(&self) -> CacheResult<()> {
263 self.data.write().await.clear();
264 Ok(())
265 }
266
267 async fn ttl(&self, key: &str) -> CacheResult<Option<Duration>> {
268 let data = self.data.read().await;
269 if let Some(entry) = data.get(key) {
270 if let Some(expires_at) = entry.expires_at {
271 let now = tokio::time::Instant::now();
272 if expires_at > now {
273 Ok(Some(expires_at - now))
274 } else {
275 Ok(None)
276 }
277 } else {
278 Ok(None)
279 }
280 } else {
281 Ok(None)
282 }
283 }
284
285 async fn expire(&self, key: &str, ttl: Duration) -> CacheResult<()> {
286 let mut data = self.data.write().await;
287 if let Some(entry) = data.get_mut(key) {
288 entry.expires_at = Some(tokio::time::Instant::now() + ttl);
289 }
290 Ok(())
291 }
292
293 async fn increment(&self, key: &str, delta: i64) -> CacheResult<i64> {
294 let mut data = self.data.write().await;
295 let entry = data.entry(key.to_string()).or_insert_with(|| CacheEntry {
296 value: "0".to_string(),
297 expires_at: None,
298 });
299
300 let current: i64 = entry.value.parse().unwrap_or(0);
301 let new_value = current + delta;
302 entry.value = new_value.to_string();
303
304 Ok(new_value)
305 }
306
307 async fn decrement(&self, key: &str, delta: i64) -> CacheResult<i64> {
308 self.increment(key, -delta).await
309 }
310}
311
312#[cfg(test)]
313mod tests_tiered {
314 use super::*;
315
316 #[tokio::test]
317 async fn test_tiered_cache() {
318 let l1 = Arc::new(InMemoryCache::new());
319 let l2 = Arc::new(InMemoryCache::new());
320 let cache = TieredCache::new(l1.clone(), l2.clone());
321
322 cache.set("test", "value".to_string(), None).await.unwrap();
324
325 let value = l1.get_json("test").await.unwrap();
327 assert!(value.is_some());
328
329 let value = cache.get("test").await.unwrap();
331 assert_eq!(value, Some("value".to_string()));
332
333 cache.delete("test").await.unwrap();
335 let value = cache.get("test").await.unwrap();
336 assert_eq!(value, None);
337 }
338
339 #[tokio::test]
340 async fn test_l2_promotion() {
341 let l1 = Arc::new(InMemoryCache::new());
342 let l2 = Arc::new(InMemoryCache::new());
343 let cache = TieredCache::new(l1.clone(), l2.clone());
344
345 l2.set_json("key", "value".to_string(), None).await.unwrap();
347
348 let value = cache.get("key").await.unwrap();
350 assert_eq!(value, Some("value".to_string()));
351
352 let l1_value = l1.get_json("key").await.unwrap();
354 assert!(l1_value.is_some());
355 }
356}