multi_tier_cache/backends/
quickcache_cache.rs1use crate::error::CacheResult;
6use bytes::Bytes;
7use futures_util::future::BoxFuture;
8use quick_cache::sync::Cache;
9use std::sync::Arc;
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::time::{Duration, Instant};
12use tracing::{debug, info};
13
14#[derive(Debug, Clone)]
16struct CacheEntry {
17 value: Bytes,
18 expires_at: Instant,
19}
20
21impl CacheEntry {
22 fn new(value: Bytes, ttl: Duration) -> Self {
23 Self {
24 value,
25 expires_at: Instant::now() + ttl,
26 }
27 }
28
29 fn is_expired(&self) -> bool {
30 Instant::now() > self.expires_at
31 }
32}
33
34pub struct QuickCacheBackend {
47 cache: Cache<String, Arc<CacheEntry>>,
49 hits: Arc<AtomicU64>,
51 misses: Arc<AtomicU64>,
53 sets: Arc<AtomicU64>,
55}
56
57impl QuickCacheBackend {
58 pub fn new(max_capacity: u64) -> CacheResult<Self> {
68 info!(capacity = max_capacity, "Initializing QuickCache");
69
70 let cache = Cache::new(usize::try_from(max_capacity)?);
71
72 Ok(Self {
73 cache,
74 hits: Arc::new(AtomicU64::new(0)),
75 misses: Arc::new(AtomicU64::new(0)),
76 sets: Arc::new(AtomicU64::new(0)),
77 })
78 }
79
80 #[must_use]
82 pub const fn size(&self) -> usize {
83 0 }
85}
86
87use crate::traits::{CacheBackend, L2CacheBackend};
90
91impl CacheBackend for QuickCacheBackend {
93 fn get<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Option<Bytes>> {
94 Box::pin(async move {
95 if let Some(entry) = self.cache.get(key) {
96 if entry.is_expired() {
97 self.cache.remove(key);
98 self.misses.fetch_add(1, Ordering::Relaxed);
99 None
100 } else {
101 self.hits.fetch_add(1, Ordering::Relaxed);
102 Some(entry.value.clone())
103 }
104 } else {
105 self.misses.fetch_add(1, Ordering::Relaxed);
106 None
107 }
108 })
109 }
110
111 fn set_with_ttl<'a>(
112 &'a self,
113 key: &'a str,
114 value: Bytes,
115 ttl: Duration,
116 ) -> BoxFuture<'a, CacheResult<()>> {
117 Box::pin(async move {
118 let entry = Arc::new(CacheEntry::new(value, ttl));
119 self.cache.insert(key.to_string(), entry);
120 self.sets.fetch_add(1, Ordering::Relaxed);
121 debug!(key = %key, ttl_secs = %ttl.as_secs(), "[QuickCache] Cached key with TTL");
122 Ok(())
123 })
124 }
125
126 fn remove<'a>(&'a self, key: &'a str) -> BoxFuture<'a, CacheResult<()>> {
127 Box::pin(async move {
128 self.cache.remove(key);
129 Ok(())
130 })
131 }
132
133 fn health_check(&self) -> BoxFuture<'_, bool> {
134 Box::pin(async move {
135 let test_key = "health_check_quickcache";
136 let test_value = Bytes::from_static(b"health_check");
137
138 match self
139 .set_with_ttl(test_key, test_value.clone(), Duration::from_mins(1))
140 .await
141 {
142 Ok(()) => match self.get(test_key).await {
143 Some(retrieved) => {
144 let _ = self.remove(test_key).await;
145 retrieved == test_value
146 }
147 None => false,
148 },
149 Err(_) => false,
150 }
151 })
152 }
153
154 fn name(&self) -> &'static str {
155 "QuickCache"
156 }
157}
158
159impl L2CacheBackend for QuickCacheBackend {
160 fn get_with_ttl<'a>(
161 &'a self,
162 key: &'a str,
163 ) -> BoxFuture<'a, Option<(Bytes, Option<Duration>)>> {
164 Box::pin(async move {
165 if let Some(entry) = self.cache.get(key) {
166 if entry.is_expired() {
167 self.cache.remove(key);
168 self.misses.fetch_add(1, Ordering::Relaxed);
169 None
170 } else {
171 self.hits.fetch_add(1, Ordering::Relaxed);
172 let now = Instant::now();
173 let remaining = if entry.expires_at > now {
174 Some(entry.expires_at.duration_since(now))
175 } else {
176 None
177 };
178 Some((entry.value.clone(), remaining))
179 }
180 } else {
181 self.misses.fetch_add(1, Ordering::Relaxed);
182 None
183 }
184 })
185 }
186}