1use std::collections::HashMap;
7
8use std::time::{Duration, Instant};
9
10use parking_lot::RwLock;
11use rand::Rng;
12
13#[derive(Debug, Clone)]
15pub struct QueryCacheConfig {
16 pub ttl: Duration,
18 pub max_entries: usize,
20 pub enable_null_cache: bool,
22 pub enable_singleflight: bool,
24 pub ttl_jitter: f64,
26}
27
28impl Default for QueryCacheConfig {
29 fn default() -> Self {
30 Self {
31 ttl: Duration::from_secs(60),
32 max_entries: 10000,
33 enable_null_cache: true,
34 enable_singleflight: true,
35 ttl_jitter: 0.1,
36 }
37 }
38}
39
40#[derive(Debug, Clone)]
42struct CacheEntry {
43 data: Vec<u8>,
44 expires_at: Instant,
45 #[allow(dead_code)]
47 is_null: bool,
48}
49
50impl CacheEntry {
51 fn is_expired(&self) -> bool {
52 Instant::now() > self.expires_at
53 }
54}
55
56#[derive(Debug, Clone, Default)]
58struct CacheStats {
59 hits: u64,
60 misses: u64,
61 evictions: u64,
62}
63
64pub struct QueryCache {
66 config: QueryCacheConfig,
67 entries: RwLock<HashMap<String, CacheEntry>>,
68 stats: RwLock<CacheStats>,
69}
70
71impl QueryCache {
72 pub fn new(config: QueryCacheConfig) -> Self {
74 Self {
75 config,
76 entries: RwLock::new(HashMap::new()),
77 stats: RwLock::new(CacheStats::default()),
78 }
79 }
80
81 pub fn make_key(sql: &str, params: &[&str]) -> String {
83 let mut key = String::with_capacity(sql.len() + params.len() * 8);
84 key.push_str(sql);
85 for p in params {
86 key.push('|');
87 key.push_str(p);
88 }
89 key
90 }
91
92 pub async fn get_or_query<F, Fut>(
96 &self,
97 key: &str,
98 query_fn: F,
99 ) -> Result<Vec<u8>, QueryCacheError>
100 where
101 F: FnOnce() -> Fut,
102 Fut: std::future::Future<Output = Result<Vec<u8>, QueryCacheError>>,
103 {
104 if let Some(entry) = self.entries.read().get(key) {
105 if !entry.is_expired() {
106 self.stats.write().hits += 1;
107 return Ok(entry.data.clone());
108 }
109 }
110
111 self.stats.write().misses += 1;
112 let data = query_fn().await?;
113 self.put(key, data.clone());
114 Ok(data)
115 }
116
117 fn put(&self, key: &str, data: Vec<u8>) {
119 let mut entries = self.entries.write();
120 if entries.len() >= self.config.max_entries {
121 self.evict_oldest(&mut entries);
122 }
123 let ttl = self.jitter_ttl();
124 let is_null = data.is_empty();
125 entries.insert(
126 key.to_string(),
127 CacheEntry {
128 data,
129 expires_at: Instant::now() + ttl,
130 is_null,
131 },
132 );
133 }
134
135 pub fn invalidate(&self, pattern: &str) -> usize {
137 let mut entries = self.entries.write();
138 let keys_to_remove: Vec<String> = entries
139 .keys()
140 .filter(|k| k.contains(pattern))
141 .cloned()
142 .collect();
143 let count = keys_to_remove.len();
144 for k in keys_to_remove {
145 entries.remove(&k);
146 }
147 count
148 }
149
150 pub fn clear(&self) {
152 self.entries.write().clear();
153 }
154
155 pub fn len(&self) -> usize {
157 self.entries.read().len()
158 }
159
160 pub fn is_empty(&self) -> bool {
162 self.len() == 0
163 }
164
165 pub fn hit_rate(&self) -> f64 {
167 let stats = self.stats.read();
168 let total = stats.hits + stats.misses;
169 if total == 0 {
170 0.0
171 } else {
172 stats.hits as f64 / total as f64
173 }
174 }
175
176 pub fn hits(&self) -> u64 {
178 self.stats.read().hits
179 }
180
181 pub fn misses(&self) -> u64 {
183 self.stats.read().misses
184 }
185
186 fn evict_oldest(&self, entries: &mut HashMap<String, CacheEntry>) {
188 if let Some((oldest_key, _)) = entries
189 .iter()
190 .min_by_key(|(_, e)| e.expires_at)
191 .map(|(k, _)| (k.clone(), ()))
192 {
193 entries.remove(&oldest_key);
194 self.stats.write().evictions += 1;
195 }
196 }
197
198 fn jitter_ttl(&self) -> Duration {
200 if self.config.ttl_jitter == 0.0 {
201 return self.config.ttl;
202 }
203 let mut rng = rand::thread_rng();
204 let jitter = rng.gen_range(-self.config.ttl_jitter..=self.config.ttl_jitter);
205 let base_ms = self.config.ttl.as_millis() as f64;
206 let adjusted_ms = base_ms * (1.0 + jitter);
207 Duration::from_millis(adjusted_ms as u64)
208 }
209}
210
211impl std::fmt::Debug for QueryCache {
212 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213 write!(
214 f,
215 "QueryCache {{ entries: {}, hits: {}, misses: {} }}",
216 self.len(),
217 self.hits(),
218 self.misses()
219 )
220 }
221}
222
223#[derive(Debug, thiserror::Error)]
225pub enum QueryCacheError {
226 #[error("query failed: {0}")]
228 QueryFailed(String),
229 #[error("serialize failed: {0}")]
231 SerializeFailed(String),
232}
233
234#[cfg(test)]
239mod tests {
240 use super::*;
241
242 fn make_key(sql: &str, params: &[&str]) -> String {
243 QueryCache::make_key(sql, params)
244 }
245
246 #[test]
247 fn test_make_key_consistency() {
248 let k1 = make_key("SELECT * FROM users WHERE id = ?", &["1"]);
249 let k2 = make_key("SELECT * FROM users WHERE id = ?", &["1"]);
250 assert_eq!(k1, k2);
251 }
252
253 #[test]
254 fn test_make_key_different_params() {
255 let k1 = make_key("SELECT * FROM users WHERE id = ?", &["1"]);
256 let k2 = make_key("SELECT * FROM users WHERE id = ?", &["2"]);
257 assert_ne!(k1, k2);
258 }
259
260 #[test]
261 fn test_make_key_different_sql() {
262 let k1 = make_key("SELECT * FROM users", &[]);
263 let k2 = make_key("SELECT * FROM orders", &[]);
264 assert_ne!(k1, k2);
265 }
266
267 #[test]
268 fn test_config_default() {
269 let config = QueryCacheConfig::default();
270 assert_eq!(config.ttl, Duration::from_secs(60));
271 assert_eq!(config.max_entries, 10000);
272 assert!(config.enable_null_cache);
273 assert!(config.enable_singleflight);
274 assert_eq!(config.ttl_jitter, 0.1);
275 }
276
277 #[test]
278 fn test_cache_entry_expiry() {
279 let entry = CacheEntry {
280 data: vec![1, 2, 3],
281 expires_at: Instant::now() + Duration::from_secs(60),
282 is_null: false,
283 };
284 assert!(!entry.is_expired());
285 }
286
287 #[test]
288 fn test_cache_entry_expired() {
289 let entry = CacheEntry {
290 data: vec![1, 2, 3],
291 expires_at: Instant::now() - Duration::from_secs(1),
292 is_null: false,
293 };
294 assert!(entry.is_expired());
295 }
296
297 #[test]
298 fn test_jitter_ttl() {
299 let config = QueryCacheConfig {
300 ttl: Duration::from_secs(100),
301 ttl_jitter: 0.1,
302 ..Default::default()
303 };
304 let cache = QueryCache::new(config);
305 for _ in 0..100 {
306 let ttl = cache.jitter_ttl();
307 let ms = ttl.as_millis();
308 assert!(
309 (90_000..=110_000).contains(&ms),
310 "jitter TTL out of range: {ms}ms"
311 );
312 }
313 }
314
315 #[test]
316 fn test_hit_rate_zero() {
317 let cache = QueryCache::new(QueryCacheConfig::default());
318 assert_eq!(cache.hit_rate(), 0.0);
319 }
320
321 #[test]
322 fn test_invalidate() {
323 let cache = QueryCache::new(QueryCacheConfig::default());
324 cache.put("users:1", b"data1".to_vec());
325 cache.put("users:2", b"data2".to_vec());
326 cache.put("orders:1", b"data3".to_vec());
327 let removed = cache.invalidate("users");
328 assert_eq!(removed, 2);
329 assert_eq!(cache.len(), 1);
330 }
331
332 #[test]
333 fn test_clear() {
334 let cache = QueryCache::new(QueryCacheConfig::default());
335 cache.put("key1", b"data".to_vec());
336 cache.put("key2", b"data".to_vec());
337 assert_eq!(cache.len(), 2);
338 cache.clear();
339 assert!(cache.is_empty());
340 }
341}