1use parking_lot::RwLock;
29use std::collections::HashMap;
30use std::time::{Duration, Instant};
31
32#[derive(Debug, Clone)]
38pub struct QueryCacheConfig {
39 pub max_entries: usize,
41 pub ttl: Duration,
43 pub max_value_size: usize,
45 pub invalidation_policy: InvalidationPolicy,
47}
48
49impl Default for QueryCacheConfig {
50 fn default() -> Self {
51 Self {
52 max_entries: 1000,
53 ttl: Duration::from_secs(60),
54 max_value_size: 1024 * 1024, invalidation_policy: InvalidationPolicy::OnWrite,
56 }
57 }
58}
59
60impl QueryCacheConfig {
61 #[must_use]
63 pub fn with_max_entries(mut self, max_entries: usize) -> Self {
64 self.max_entries = max_entries;
65 self
66 }
67
68 #[must_use]
70 pub fn with_ttl(mut self, ttl: Duration) -> Self {
71 self.ttl = ttl;
72 self
73 }
74
75 #[must_use]
77 pub fn with_max_value_size(mut self, max_value_size: usize) -> Self {
78 self.max_value_size = max_value_size;
79 self
80 }
81
82 #[must_use]
84 pub fn with_invalidation_policy(mut self, policy: InvalidationPolicy) -> Self {
85 self.invalidation_policy = policy;
86 self
87 }
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum InvalidationPolicy {
93 OnWrite,
95 Manual,
97 None,
99}
100
101#[derive(Debug, Clone)]
107pub struct CacheStats {
108 pub hits: u64,
110 pub misses: u64,
112 pub evictions: u64,
114 pub size: usize,
116 pub total_inserts: u64,
118 pub invalidations: u64,
120}
121
122impl CacheStats {
123 pub fn hit_rate(&self) -> f64 {
126 let total = self.hits + self.misses;
127 if total == 0 {
128 0.0
129 } else {
130 self.hits as f64 / total as f64
131 }
132 }
133}
134
135#[derive(Debug, Clone)]
141struct CachedResult {
142 data: Vec<u8>,
144 inserted_at: Instant,
146 hit_count: u64,
148 collection: Option<String>,
150}
151
152#[derive(Debug, Clone)]
157struct LruNode {
158 prev: Option<CacheKey>,
159 next: Option<CacheKey>,
160}
161
162type CacheKey = [u8; 32];
164
165struct CacheInner {
167 entries: HashMap<CacheKey, (CachedResult, LruNode)>,
169 head: Option<CacheKey>,
171 tail: Option<CacheKey>,
173 collection_index: HashMap<String, Vec<CacheKey>>,
175 stats: CacheStats,
177}
178
179impl CacheInner {
180 fn new() -> Self {
181 Self {
182 entries: HashMap::new(),
183 head: None,
184 tail: None,
185 collection_index: HashMap::new(),
186 stats: CacheStats {
187 hits: 0,
188 misses: 0,
189 evictions: 0,
190 size: 0,
191 total_inserts: 0,
192 invalidations: 0,
193 },
194 }
195 }
196
197 fn detach(&mut self, key: &CacheKey) {
201 let node = if let Some((_, node)) = self.entries.get(key) {
202 node.clone()
203 } else {
204 return;
205 };
206
207 if let Some(prev_key) = &node.prev {
209 if let Some((_, prev_node)) = self.entries.get_mut(prev_key) {
210 prev_node.next = node.next;
211 }
212 } else {
213 self.head = node.next;
215 }
216
217 if let Some(next_key) = &node.next {
219 if let Some((_, next_node)) = self.entries.get_mut(next_key) {
220 next_node.prev = node.prev;
221 }
222 } else {
223 self.tail = node.prev;
225 }
226
227 if let Some((_, n)) = self.entries.get_mut(key) {
229 n.prev = None;
230 n.next = None;
231 }
232 }
233
234 fn push_front(&mut self, key: CacheKey) {
237 if let Some(old_head) = self.head {
238 if old_head == key {
239 return; }
241 if let Some((_, node)) = self.entries.get_mut(&old_head) {
243 node.prev = Some(key);
244 }
245 }
246
247 if let Some((_, node)) = self.entries.get_mut(&key) {
249 node.prev = None;
250 node.next = self.head;
251 }
252
253 self.head = Some(key);
254
255 if self.tail.is_none() {
256 self.tail = Some(key);
257 }
258 }
259
260 fn touch(&mut self, key: &CacheKey) {
262 let k = *key;
263 self.detach(&k);
264 self.push_front(k);
265 }
266
267 fn evict_lru(&mut self) -> Option<CacheKey> {
269 let tail_key = self.tail?;
270 self.remove_entry(&tail_key);
271 self.stats.evictions += 1;
272 Some(tail_key)
273 }
274
275 fn remove_entry(&mut self, key: &CacheKey) {
277 self.detach(key);
278 if let Some((result, _)) = self.entries.remove(key) {
279 self.stats.size = self.stats.size.saturating_sub(1);
280 if let Some(ref coll) = result.collection {
282 if let Some(keys) = self.collection_index.get_mut(coll) {
283 keys.retain(|k| k != key);
284 if keys.is_empty() {
285 self.collection_index.remove(coll);
286 }
287 }
288 }
289 }
290 }
291}
292
293pub struct QueryCache {
303 inner: RwLock<CacheInner>,
304 config: QueryCacheConfig,
305}
306
307impl QueryCache {
308 pub fn new(config: QueryCacheConfig) -> Self {
310 Self {
311 inner: RwLock::new(CacheInner::new()),
312 config,
313 }
314 }
315
316 pub fn get(&self, key: &[u8]) -> Option<Vec<u8>> {
322 let cache_key = Self::hash_key(key);
323
324 {
326 let inner = self.inner.read();
327 match inner.entries.get(&cache_key) {
328 Some((result, _)) => {
329 if result.inserted_at.elapsed() > self.config.ttl {
330 drop(inner);
332 let mut inner = self.inner.write();
333 inner.remove_entry(&cache_key);
334 inner.stats.misses += 1;
335 return None;
336 }
337 }
338 None => {
339 drop(inner);
340 let mut inner = self.inner.write();
341 inner.stats.misses += 1;
342 return None;
343 }
344 }
345 }
346
347 let mut inner = self.inner.write();
349 if let Some((result, _)) = inner.entries.get_mut(&cache_key) {
351 if result.inserted_at.elapsed() > self.config.ttl {
352 inner.remove_entry(&cache_key);
353 inner.stats.misses += 1;
354 return None;
355 }
356 result.hit_count += 1;
357 let data = result.data.clone();
358 inner.stats.hits += 1;
359 inner.touch(&cache_key);
360 Some(data)
361 } else {
362 inner.stats.misses += 1;
363 None
364 }
365 }
366
367 pub fn put(&self, key: &[u8], value: Vec<u8>) {
372 self.put_with_collection(key, value, None);
373 }
374
375 pub fn put_with_collection(&self, key: &[u8], value: Vec<u8>, collection: Option<&str>) {
379 if value.len() > self.config.max_value_size {
380 return; }
382
383 let cache_key = Self::hash_key(key);
384
385 let mut inner = self.inner.write();
386
387 if inner.entries.contains_key(&cache_key) {
389 inner.remove_entry(&cache_key);
390 }
391
392 while inner.entries.len() >= self.config.max_entries {
394 inner.evict_lru();
395 }
396
397 let coll_string = collection.map(String::from);
398
399 if let Some(ref coll) = coll_string {
401 inner
402 .collection_index
403 .entry(coll.clone())
404 .or_default()
405 .push(cache_key);
406 }
407
408 let result = CachedResult {
409 data: value,
410 inserted_at: Instant::now(),
411 hit_count: 0,
412 collection: coll_string,
413 };
414
415 let node = LruNode {
416 prev: None,
417 next: None,
418 };
419
420 inner.entries.insert(cache_key, (result, node));
421 inner.stats.size += 1;
422 inner.stats.total_inserts += 1;
423 inner.push_front(cache_key);
424 }
425
426 pub fn invalidate(&self, key: &[u8]) {
428 let cache_key = Self::hash_key(key);
429 let mut inner = self.inner.write();
430 if inner.entries.contains_key(&cache_key) {
431 inner.remove_entry(&cache_key);
432 inner.stats.invalidations += 1;
433 }
434 }
435
436 pub fn invalidate_collection(&self, collection: &str) {
438 let mut inner = self.inner.write();
439 if let Some(keys) = inner.collection_index.remove(collection) {
440 for key in &keys {
441 inner.detach(key);
442 inner.entries.remove(key);
443 inner.stats.size = inner.stats.size.saturating_sub(1);
444 inner.stats.invalidations += 1;
445 }
446 }
447 }
448
449 pub fn clear(&self) {
451 let mut inner = self.inner.write();
452 let prev_size = inner.entries.len();
453 inner.entries.clear();
454 inner.head = None;
455 inner.tail = None;
456 inner.collection_index.clear();
457 inner.stats.size = 0;
458 inner.stats.invalidations += prev_size as u64;
459 }
460
461 pub fn stats(&self) -> CacheStats {
463 let inner = self.inner.read();
464 inner.stats.clone()
465 }
466
467 pub fn len(&self) -> usize {
469 let inner = self.inner.read();
470 inner.entries.len()
471 }
472
473 pub fn is_empty(&self) -> bool {
475 self.len() == 0
476 }
477
478 pub fn config(&self) -> &QueryCacheConfig {
480 &self.config
481 }
482
483 pub fn invalidation_policy(&self) -> InvalidationPolicy {
485 self.config.invalidation_policy
486 }
487
488 pub fn make_key(collection: &str, query_key: &[u8]) -> Vec<u8> {
492 let mut buf = Vec::with_capacity(collection.len() + 1 + query_key.len());
493 buf.extend_from_slice(collection.as_bytes());
494 buf.push(b':');
495 buf.extend_from_slice(query_key);
496 buf
497 }
498
499 fn hash_key(key: &[u8]) -> CacheKey {
501 let hash = blake3::hash(key);
502 *hash.as_bytes()
503 }
504}
505
506#[cfg(test)]
511mod tests {
512 use super::*;
513 use std::thread;
514 use std::time::Duration;
515
516 fn default_cache() -> QueryCache {
517 QueryCache::new(QueryCacheConfig::default())
518 }
519
520 #[test]
523 fn test_cache_hit() {
524 let cache = default_cache();
525 cache.put(b"key1", vec![10, 20, 30]);
526
527 let result = cache.get(b"key1");
528 assert!(result.is_some());
529 assert_eq!(result.expect("should have value"), vec![10, 20, 30]);
530
531 let stats = cache.stats();
532 assert_eq!(stats.hits, 1);
533 assert_eq!(stats.misses, 0);
534 }
535
536 #[test]
537 fn test_cache_miss() {
538 let cache = default_cache();
539
540 let result = cache.get(b"nonexistent");
541 assert!(result.is_none());
542
543 let stats = cache.stats();
544 assert_eq!(stats.hits, 0);
545 assert_eq!(stats.misses, 1);
546 }
547
548 #[test]
551 fn test_ttl_expiry() {
552 let config = QueryCacheConfig::default().with_ttl(Duration::from_millis(50));
553 let cache = QueryCache::new(config);
554
555 cache.put(b"key1", vec![1, 2, 3]);
556 assert!(cache.get(b"key1").is_some());
557
558 thread::sleep(Duration::from_millis(80));
560
561 assert!(cache.get(b"key1").is_none());
562
563 let stats = cache.stats();
564 assert_eq!(stats.hits, 1);
565 assert_eq!(stats.misses, 1); }
567
568 #[test]
571 fn test_lru_eviction() {
572 let config = QueryCacheConfig::default().with_max_entries(3);
573 let cache = QueryCache::new(config);
574
575 cache.put(b"a", vec![1]);
576 cache.put(b"b", vec![2]);
577 cache.put(b"c", vec![3]);
578
579 cache.put(b"d", vec![4]);
581
582 assert!(cache.get(b"a").is_none(), "a should have been evicted");
583 assert!(cache.get(b"b").is_some());
584 assert!(cache.get(b"c").is_some());
585 assert!(cache.get(b"d").is_some());
586
587 let stats = cache.stats();
588 assert_eq!(stats.evictions, 1);
589 }
590
591 #[test]
592 fn test_lru_access_order() {
593 let config = QueryCacheConfig::default().with_max_entries(3);
594 let cache = QueryCache::new(config);
595
596 cache.put(b"a", vec![1]);
597 cache.put(b"b", vec![2]);
598 cache.put(b"c", vec![3]);
599
600 let _ = cache.get(b"a");
602
603 cache.put(b"d", vec![4]);
605
606 assert!(
607 cache.get(b"a").is_some(),
608 "a was accessed and should not be evicted"
609 );
610 assert!(cache.get(b"b").is_none(), "b should have been evicted");
611 assert!(cache.get(b"c").is_some());
612 assert!(cache.get(b"d").is_some());
613 }
614
615 #[test]
618 fn test_invalidate_key() {
619 let cache = default_cache();
620
621 cache.put(b"key1", vec![1]);
622 cache.put(b"key2", vec![2]);
623
624 cache.invalidate(b"key1");
625
626 assert!(cache.get(b"key1").is_none());
627 assert!(cache.get(b"key2").is_some());
628
629 let stats = cache.stats();
630 assert_eq!(stats.invalidations, 1);
631 }
632
633 #[test]
636 fn test_invalidate_collection() {
637 let cache = default_cache();
638
639 let key1 = QueryCache::make_key("users", b"u1");
640 let key2 = QueryCache::make_key("users", b"u2");
641 let key3 = QueryCache::make_key("orders", b"o1");
642
643 cache.put_with_collection(&key1, vec![1], Some("users"));
644 cache.put_with_collection(&key2, vec![2], Some("users"));
645 cache.put_with_collection(&key3, vec![3], Some("orders"));
646
647 cache.invalidate_collection("users");
648
649 assert!(cache.get(&key1).is_none());
650 assert!(cache.get(&key2).is_none());
651 assert!(cache.get(&key3).is_some(), "orders entry should remain");
652
653 let stats = cache.stats();
654 assert_eq!(stats.invalidations, 2);
655 }
656
657 #[test]
660 fn test_stats_accuracy() {
661 let cache = default_cache();
662
663 cache.put(b"a", vec![1]);
665 cache.put(b"b", vec![2]);
666 cache.put(b"c", vec![3]);
667
668 let _ = cache.get(b"a");
670 let _ = cache.get(b"b");
671
672 let _ = cache.get(b"z");
674
675 cache.invalidate(b"c");
677
678 let stats = cache.stats();
679 assert_eq!(stats.total_inserts, 3);
680 assert_eq!(stats.hits, 2);
681 assert_eq!(stats.misses, 1);
682 assert_eq!(stats.invalidations, 1);
683 assert_eq!(stats.size, 2);
684
685 let rate = stats.hit_rate();
686 assert!((rate - 2.0 / 3.0).abs() < 1e-9);
688 }
689
690 #[test]
691 fn test_hit_rate_no_lookups() {
692 let cache = default_cache();
693 let stats = cache.stats();
694 assert!((stats.hit_rate() - 0.0).abs() < f64::EPSILON);
695 }
696
697 #[test]
700 fn test_concurrent_access() {
701 use std::sync::Arc;
702
703 let cache = Arc::new(QueryCache::new(
704 QueryCacheConfig::default().with_max_entries(500),
705 ));
706
707 let mut handles = Vec::new();
708
709 for t in 0..4 {
711 let cache = Arc::clone(&cache);
712 handles.push(thread::spawn(move || {
713 for i in 0..200 {
714 let key = format!("thread-{}-key-{}", t, i);
715 cache.put(key.as_bytes(), vec![t as u8; 64]);
716 }
717 }));
718 }
719
720 for t in 0..4 {
722 let cache = Arc::clone(&cache);
723 handles.push(thread::spawn(move || {
724 for i in 0..200 {
725 let key = format!("thread-{}-key-{}", t, i);
726 let _ = cache.get(key.as_bytes());
727 }
728 }));
729 }
730
731 for h in handles {
732 h.join().expect("thread should not panic");
733 }
734
735 let stats = cache.stats();
736 assert!(stats.total_inserts > 0);
738 assert!(stats.size <= 500);
739 }
740
741 #[test]
744 fn test_max_value_size_enforcement() {
745 let config = QueryCacheConfig::default().with_max_value_size(100);
746 let cache = QueryCache::new(config);
747
748 cache.put(b"small", vec![0u8; 100]);
750 assert!(cache.get(b"small").is_some());
751
752 cache.put(b"big", vec![0u8; 101]);
754 assert!(cache.get(b"big").is_none());
755
756 let stats = cache.stats();
757 assert_eq!(stats.total_inserts, 1); }
759
760 #[test]
763 fn test_clear() {
764 let cache = default_cache();
765
766 cache.put(b"a", vec![1]);
767 cache.put(b"b", vec![2]);
768 cache.put(b"c", vec![3]);
769
770 assert_eq!(cache.len(), 3);
771
772 cache.clear();
773
774 assert_eq!(cache.len(), 0);
775 assert!(cache.is_empty());
776 assert!(cache.get(b"a").is_none());
777
778 let stats = cache.stats();
779 assert_eq!(stats.size, 0);
780 assert_eq!(stats.invalidations, 3);
781 }
782
783 #[test]
786 fn test_make_key() {
787 let key = QueryCache::make_key("users", b"abc");
788 assert_eq!(key, b"users:abc");
789 }
790
791 #[test]
794 fn test_overwrite_existing_key() {
795 let cache = default_cache();
796
797 cache.put(b"key", vec![1, 2, 3]);
798 assert_eq!(cache.get(b"key").expect("should exist"), vec![1, 2, 3]);
799
800 cache.put(b"key", vec![4, 5, 6]);
801 assert_eq!(cache.get(b"key").expect("should exist"), vec![4, 5, 6]);
802
803 assert_eq!(cache.len(), 1);
804
805 let stats = cache.stats();
806 assert_eq!(stats.total_inserts, 2);
807 }
808
809 #[test]
812 fn test_invalidation_policy_config() {
813 let config =
814 QueryCacheConfig::default().with_invalidation_policy(InvalidationPolicy::Manual);
815 let cache = QueryCache::new(config);
816 assert_eq!(cache.invalidation_policy(), InvalidationPolicy::Manual);
817 }
818
819 #[test]
822 fn test_single_entry_cache() {
823 let config = QueryCacheConfig::default().with_max_entries(1);
824 let cache = QueryCache::new(config);
825
826 cache.put(b"a", vec![1]);
827 assert!(cache.get(b"a").is_some());
828
829 cache.put(b"b", vec![2]);
830 assert!(cache.get(b"a").is_none());
831 assert!(cache.get(b"b").is_some());
832
833 let stats = cache.stats();
834 assert_eq!(stats.evictions, 1);
835 }
836}