1use std::collections::{HashMap, HashSet, VecDeque};
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::sync::{Arc, Mutex};
10use std::time::{Duration, Instant};
11
12#[derive(Debug, Clone, Hash, PartialEq, Eq)]
17pub struct CacheKey {
18 pub tenant_id: String,
20 pub query_hash: u64,
22 pub variables_hash: u64,
24}
25
26impl CacheKey {
27 pub fn new(tenant_id: &str, query: &str, variables: Option<&str>) -> Self {
32 Self {
33 tenant_id: tenant_id.to_string(),
34 query_hash: fnv1a_hash(query),
35 variables_hash: variables.map(fnv1a_hash).unwrap_or(0),
36 }
37 }
38}
39
40fn fnv1a_hash(s: &str) -> u64 {
42 const FNV_OFFSET_BASIS: u64 = 14695981039346656037;
43 const FNV_PRIME: u64 = 1099511628211;
44
45 let mut hash = FNV_OFFSET_BASIS;
46 for byte in s.bytes() {
47 hash ^= u64::from(byte);
48 hash = hash.wrapping_mul(FNV_PRIME);
49 }
50 hash
51}
52
53#[derive(Debug, Clone)]
55pub struct CacheEntry {
56 pub key: CacheKey,
58 pub response_json: String,
60 pub accessed_graphs: Vec<String>,
63 pub accessed_predicates: Vec<String>,
65 pub created_at: Instant,
67 pub ttl: Duration,
69 pub hit_count: u64,
71}
72
73impl CacheEntry {
74 pub fn is_expired(&self) -> bool {
76 self.created_at.elapsed() >= self.ttl
77 }
78
79 pub fn age(&self) -> Duration {
81 self.created_at.elapsed()
82 }
83
84 pub fn remaining_ttl(&self) -> Duration {
86 self.ttl.saturating_sub(self.created_at.elapsed())
87 }
88}
89
90struct CacheStore {
92 entries: HashMap<CacheKey, CacheEntry>,
93 lru_order: VecDeque<CacheKey>,
95 graph_index: HashMap<String, HashSet<CacheKey>>,
97 tenant_index: HashMap<String, HashSet<CacheKey>>,
99 max_entries: usize,
100}
101
102impl CacheStore {
103 fn new(max_entries: usize) -> Self {
104 Self {
105 entries: HashMap::new(),
106 lru_order: VecDeque::new(),
107 graph_index: HashMap::new(),
108 tenant_index: HashMap::new(),
109 max_entries,
110 }
111 }
112
113 fn insert(&mut self, entry: CacheEntry) {
115 let key = entry.key.clone();
116
117 for graph in &entry.accessed_graphs {
119 self.graph_index
120 .entry(graph.clone())
121 .or_default()
122 .insert(key.clone());
123 }
124
125 self.tenant_index
127 .entry(key.tenant_id.clone())
128 .or_default()
129 .insert(key.clone());
130
131 if self.entries.contains_key(&key) {
133 self.lru_order.retain(|k| k != &key);
134 }
135
136 self.entries.insert(key.clone(), entry);
137 self.lru_order.push_back(key);
138
139 while self.entries.len() > self.max_entries {
141 if let Some(oldest_key) = self.lru_order.pop_front() {
142 self.remove_key(&oldest_key);
143 } else {
144 break;
145 }
146 }
147 }
148
149 fn remove_key(&mut self, key: &CacheKey) {
151 if let Some(entry) = self.entries.remove(key) {
152 for graph in &entry.accessed_graphs {
154 if let Some(set) = self.graph_index.get_mut(graph) {
155 set.remove(key);
156 }
157 }
158 if let Some(set) = self.tenant_index.get_mut(&entry.key.tenant_id) {
160 set.remove(key);
161 }
162 }
163 self.lru_order.retain(|k| k != key);
164 }
165
166 fn touch(&mut self, key: &CacheKey) {
168 self.lru_order.retain(|k| k != key);
169 self.lru_order.push_back(key.clone());
170 }
171
172 fn remove_where<F>(&mut self, predicate: F) -> usize
174 where
175 F: Fn(&CacheKey, &CacheEntry) -> bool,
176 {
177 let to_remove: Vec<CacheKey> = self
178 .entries
179 .iter()
180 .filter(|(k, v)| predicate(k, v))
181 .map(|(k, _)| k.clone())
182 .collect();
183
184 let count = to_remove.len();
185 for key in to_remove {
186 self.remove_key(&key);
187 }
188 count
189 }
190}
191
192pub struct GqlQueryCache {
200 store: Arc<Mutex<CacheStore>>,
201 default_ttl: Duration,
202 hit_count: Arc<AtomicU64>,
203 miss_count: Arc<AtomicU64>,
204 eviction_count: Arc<AtomicU64>,
205}
206
207impl std::fmt::Debug for GqlQueryCache {
208 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209 f.debug_struct("GqlQueryCache")
210 .field("default_ttl", &self.default_ttl)
211 .field("hits", &self.hit_count.load(Ordering::Relaxed))
212 .field("misses", &self.miss_count.load(Ordering::Relaxed))
213 .finish()
214 }
215}
216
217impl GqlQueryCache {
218 pub fn new(max_entries: usize, default_ttl: Duration) -> Self {
223 Self {
224 store: Arc::new(Mutex::new(CacheStore::new(max_entries))),
225 default_ttl,
226 hit_count: Arc::new(AtomicU64::new(0)),
227 miss_count: Arc::new(AtomicU64::new(0)),
228 eviction_count: Arc::new(AtomicU64::new(0)),
229 }
230 }
231
232 pub fn get(&self, key: &CacheKey) -> Option<String> {
238 let mut store = self
239 .store
240 .lock()
241 .unwrap_or_else(|poisoned| poisoned.into_inner());
242
243 match store.entries.get(key) {
244 Some(entry) if entry.is_expired() => {
245 let key_clone = key.clone();
246 store.remove_key(&key_clone);
247 self.miss_count.fetch_add(1, Ordering::Relaxed);
248 self.eviction_count.fetch_add(1, Ordering::Relaxed);
249 None
250 }
251 Some(entry) => {
252 let response = entry.response_json.clone();
253 if let Some(e) = store.entries.get_mut(key) {
254 e.hit_count += 1;
255 }
256 store.touch(key);
257 self.hit_count.fetch_add(1, Ordering::Relaxed);
258 Some(response)
259 }
260 None => {
261 self.miss_count.fetch_add(1, Ordering::Relaxed);
262 None
263 }
264 }
265 }
266
267 pub fn put(
275 &self,
276 key: CacheKey,
277 response: String,
278 graphs: Vec<String>,
279 predicates: Vec<String>,
280 ) -> bool {
281 self.put_with_ttl(key, response, graphs, predicates, self.default_ttl)
282 }
283
284 pub fn put_with_ttl(
286 &self,
287 key: CacheKey,
288 response: String,
289 graphs: Vec<String>,
290 predicates: Vec<String>,
291 ttl: Duration,
292 ) -> bool {
293 let entry = CacheEntry {
294 key: key.clone(),
295 response_json: response,
296 accessed_graphs: graphs,
297 accessed_predicates: predicates,
298 created_at: Instant::now(),
299 ttl,
300 hit_count: 0,
301 };
302
303 match self.store.lock() {
304 Ok(mut store) => {
305 store.insert(entry);
306 true
307 }
308 Err(_) => false,
309 }
310 }
311
312 pub fn invalidate_by_graph(&self, graph: &str) -> usize {
316 let mut store = self
317 .store
318 .lock()
319 .unwrap_or_else(|poisoned| poisoned.into_inner());
320
321 let keys_to_remove: Vec<CacheKey> = store
323 .graph_index
324 .get(graph)
325 .cloned()
326 .unwrap_or_default()
327 .into_iter()
328 .collect();
329
330 let count = keys_to_remove.len();
331 for key in keys_to_remove {
332 store.remove_key(&key);
333 }
334 self.eviction_count
335 .fetch_add(count as u64, Ordering::Relaxed);
336 count
337 }
338
339 pub fn invalidate_by_tenant(&self, tenant_id: &str) -> usize {
343 let mut store = self
344 .store
345 .lock()
346 .unwrap_or_else(|poisoned| poisoned.into_inner());
347
348 let keys_to_remove: Vec<CacheKey> = store
349 .tenant_index
350 .get(tenant_id)
351 .cloned()
352 .unwrap_or_default()
353 .into_iter()
354 .collect();
355
356 let count = keys_to_remove.len();
357 for key in keys_to_remove {
358 store.remove_key(&key);
359 }
360 self.eviction_count
361 .fetch_add(count as u64, Ordering::Relaxed);
362 count
363 }
364
365 pub fn evict_expired(&self) -> usize {
369 let mut store = self
370 .store
371 .lock()
372 .unwrap_or_else(|poisoned| poisoned.into_inner());
373
374 let count = store.remove_where(|_, entry| entry.is_expired());
375 self.eviction_count
376 .fetch_add(count as u64, Ordering::Relaxed);
377 count
378 }
379
380 pub fn clear(&self) -> usize {
384 let mut store = self
385 .store
386 .lock()
387 .unwrap_or_else(|poisoned| poisoned.into_inner());
388
389 let count = store.entries.len();
390 store.entries.clear();
391 store.lru_order.clear();
392 store.graph_index.clear();
393 store.tenant_index.clear();
394 count
395 }
396
397 pub fn hit_rate(&self) -> f64 {
401 let hits = self.hit_count.load(Ordering::Relaxed) as f64;
402 let misses = self.miss_count.load(Ordering::Relaxed) as f64;
403 let total = hits + misses;
404 if total == 0.0 {
405 0.0
406 } else {
407 hits / total
408 }
409 }
410
411 pub fn size(&self) -> usize {
413 self.store.lock().map(|s| s.entries.len()).unwrap_or(0)
414 }
415
416 pub fn stats(&self) -> CacheStats {
418 CacheStats {
419 hits: self.hit_count.load(Ordering::Relaxed),
420 misses: self.miss_count.load(Ordering::Relaxed),
421 evictions: self.eviction_count.load(Ordering::Relaxed),
422 current_size: self.size(),
423 }
424 }
425}
426
427#[derive(Debug, Clone)]
429pub struct CacheStats {
430 pub hits: u64,
432 pub misses: u64,
434 pub evictions: u64,
436 pub current_size: usize,
438}
439
440impl CacheStats {
441 pub fn hit_rate(&self) -> f64 {
443 let total = (self.hits + self.misses) as f64;
444 if total == 0.0 {
445 0.0
446 } else {
447 self.hits as f64 / total
448 }
449 }
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455
456 fn key(tenant: &str, query: &str) -> CacheKey {
457 CacheKey::new(tenant, query, None)
458 }
459
460 fn key_with_vars(tenant: &str, query: &str, vars: &str) -> CacheKey {
461 CacheKey::new(tenant, query, Some(vars))
462 }
463
464 #[test]
465 fn test_cache_key_equality() {
466 let k1 = key("t1", "{ hello }");
467 let k2 = key("t1", "{ hello }");
468 assert_eq!(k1, k2);
469 }
470
471 #[test]
472 fn test_cache_key_different_tenants() {
473 let k1 = key("t1", "{ hello }");
474 let k2 = key("t2", "{ hello }");
475 assert_ne!(k1, k2);
476 }
477
478 #[test]
479 fn test_cache_key_different_queries() {
480 let k1 = key("t1", "{ hello }");
481 let k2 = key("t1", "{ world }");
482 assert_ne!(k1, k2);
483 }
484
485 #[test]
486 fn test_cache_key_with_variables() {
487 let k1 = key_with_vars("t1", "{ q }", r#"{"id":1}"#);
488 let k2 = key_with_vars("t1", "{ q }", r#"{"id":2}"#);
489 assert_ne!(k1, k2);
490 }
491
492 #[test]
493 fn test_put_and_get_hit() {
494 let cache = GqlQueryCache::new(100, Duration::from_secs(60));
495 let k = key("tenant1", "{ data }");
496 cache.put(
497 k.clone(),
498 r#"{"data":{"data":"ok"}}"#.to_string(),
499 vec![],
500 vec![],
501 );
502
503 let result = cache.get(&k);
504 assert_eq!(result.as_deref(), Some(r#"{"data":{"data":"ok"}}"#));
505 }
506
507 #[test]
508 fn test_get_miss_returns_none() {
509 let cache = GqlQueryCache::new(100, Duration::from_secs(60));
510 let k = key("tenant1", "{ missing }");
511 assert!(cache.get(&k).is_none());
512 }
513
514 #[test]
515 fn test_expired_entry_returns_none() {
516 let cache = GqlQueryCache::new(100, Duration::from_nanos(1));
517 let k = key("t", "q");
518 cache.put_with_ttl(
519 k.clone(),
520 "response".to_string(),
521 vec![],
522 vec![],
523 Duration::from_nanos(1),
524 );
525 std::thread::sleep(Duration::from_millis(5));
527 assert!(cache.get(&k).is_none());
528 assert_eq!(cache.size(), 0);
529 }
530
531 #[test]
532 fn test_invalidate_by_graph() {
533 let cache = GqlQueryCache::new(100, Duration::from_secs(60));
534
535 let k1 = key("t", "q1");
536 let k2 = key("t", "q2");
537 let k3 = key("t", "q3");
538
539 cache.put(
540 k1.clone(),
541 "r1".to_string(),
542 vec!["http://ex.org/g1".to_string()],
543 vec![],
544 );
545 cache.put(
546 k2.clone(),
547 "r2".to_string(),
548 vec![
549 "http://ex.org/g1".to_string(),
550 "http://ex.org/g2".to_string(),
551 ],
552 vec![],
553 );
554 cache.put(
555 k3.clone(),
556 "r3".to_string(),
557 vec!["http://ex.org/g2".to_string()],
558 vec![],
559 );
560
561 let removed = cache.invalidate_by_graph("http://ex.org/g1");
563 assert_eq!(removed, 2);
564 assert!(cache.get(&k1).is_none());
565 assert!(cache.get(&k2).is_none());
566 assert!(cache.get(&k3).is_some());
568 }
569
570 #[test]
571 fn test_invalidate_by_tenant() {
572 let cache = GqlQueryCache::new(100, Duration::from_secs(60));
573
574 let ka = CacheKey::new("tenantA", "q1", None);
575 let kb = CacheKey::new("tenantB", "q1", None);
576
577 cache.put(ka.clone(), "ra".to_string(), vec![], vec![]);
578 cache.put(kb.clone(), "rb".to_string(), vec![], vec![]);
579
580 let removed = cache.invalidate_by_tenant("tenantA");
581 assert_eq!(removed, 1);
582 assert!(cache.get(&ka).is_none());
583 assert!(cache.get(&kb).is_some());
584 }
585
586 #[test]
587 fn test_evict_expired() {
588 let cache = GqlQueryCache::new(100, Duration::from_secs(60));
589
590 let expired_key = key("t", "expiring");
592 cache.put_with_ttl(
593 expired_key.clone(),
594 "exp".to_string(),
595 vec![],
596 vec![],
597 Duration::from_nanos(1),
598 );
599
600 let live_key = key("t", "live");
601 cache.put(live_key.clone(), "live".to_string(), vec![], vec![]);
602
603 std::thread::sleep(Duration::from_millis(5));
604
605 let removed = cache.evict_expired();
606 assert_eq!(removed, 1);
607 assert_eq!(cache.size(), 1);
608 assert!(cache.get(&live_key).is_some());
609 }
610
611 #[test]
612 fn test_hit_rate_calculation() {
613 let cache = GqlQueryCache::new(100, Duration::from_secs(60));
614 let k = key("t", "q");
615 cache.put(k.clone(), "resp".to_string(), vec![], vec![]);
616
617 cache.get(&k); cache.get(&key("t", "other")); let rate = cache.hit_rate();
621 assert!((rate - 0.5).abs() < f64::EPSILON);
622 }
623
624 #[test]
625 fn test_size_tracking() {
626 let cache = GqlQueryCache::new(100, Duration::from_secs(60));
627 assert_eq!(cache.size(), 0);
628
629 cache.put(key("t", "q1"), "r1".to_string(), vec![], vec![]);
630 assert_eq!(cache.size(), 1);
631
632 cache.put(key("t", "q2"), "r2".to_string(), vec![], vec![]);
633 assert_eq!(cache.size(), 2);
634
635 cache.invalidate_by_tenant("t");
636 assert_eq!(cache.size(), 0);
637 }
638
639 #[test]
640 fn test_lru_eviction_on_max_capacity() {
641 let cache = GqlQueryCache::new(3, Duration::from_secs(60));
642
643 let k1 = key("t", "q1");
644 let k2 = key("t", "q2");
645 let k3 = key("t", "q3");
646 let k4 = key("t", "q4");
647
648 cache.put(k1.clone(), "r1".to_string(), vec![], vec![]);
649 cache.put(k2.clone(), "r2".to_string(), vec![], vec![]);
650 cache.put(k3.clone(), "r3".to_string(), vec![], vec![]);
651 cache.get(&k1);
653
654 cache.put(k4.clone(), "r4".to_string(), vec![], vec![]);
656
657 assert_eq!(cache.size(), 3);
658 assert!(cache.get(&k2).is_none(), "k2 should have been evicted");
659 assert!(cache.get(&k1).is_some());
660 assert!(cache.get(&k3).is_some());
661 assert!(cache.get(&k4).is_some());
662 }
663
664 #[test]
665 fn test_clear() {
666 let cache = GqlQueryCache::new(100, Duration::from_secs(60));
667 cache.put(key("t", "q1"), "r1".to_string(), vec![], vec![]);
668 cache.put(key("t", "q2"), "r2".to_string(), vec![], vec![]);
669
670 let removed = cache.clear();
671 assert_eq!(removed, 2);
672 assert_eq!(cache.size(), 0);
673 }
674
675 #[test]
676 fn test_stats_snapshot() {
677 let cache = GqlQueryCache::new(100, Duration::from_secs(60));
678 let k = key("t", "q");
679 cache.put(k.clone(), "r".to_string(), vec![], vec![]);
680 cache.get(&k); cache.get(&key("t", "miss")); let stats = cache.stats();
684 assert_eq!(stats.hits, 1);
685 assert_eq!(stats.misses, 1);
686 assert_eq!(stats.current_size, 1);
687 }
688
689 #[test]
690 fn test_fnv1a_deterministic() {
691 assert_eq!(fnv1a_hash("hello"), fnv1a_hash("hello"));
692 assert_ne!(fnv1a_hash("hello"), fnv1a_hash("world"));
693 }
694
695 #[test]
696 fn test_cache_entry_is_expired() {
697 let entry = CacheEntry {
698 key: key("t", "q"),
699 response_json: String::new(),
700 accessed_graphs: vec![],
701 accessed_predicates: vec![],
702 created_at: Instant::now(),
703 ttl: Duration::from_secs(100),
704 hit_count: 0,
705 };
706 assert!(!entry.is_expired());
707 }
708}