use dashmap::DashMap;
use parking_lot::Mutex;
use std::collections::hash_map::DefaultHasher;
use std::collections::VecDeque;
use std::hash::{Hash, Hasher};
use std::sync::atomic::{AtomicU64, Ordering};
use super::Query;
type CacheKey = (String, u64);
#[derive(Clone, Debug)]
struct CacheEntry {
merkle_root: Vec<u8>,
keys: Vec<String>,
}
pub struct SearchCache {
cache: DashMap<CacheKey, CacheEntry>,
order: Mutex<VecDeque<CacheKey>>,
max_entries: usize,
hits: AtomicU64,
misses: AtomicU64,
stale: AtomicU64,
}
#[derive(Debug, Clone)]
pub struct SearchCacheStats {
pub hits: u64,
pub misses: u64,
pub stale: u64,
pub entry_count: usize,
pub hit_rate: f64,
}
impl SearchCache {
pub fn new(max_entries: usize) -> Self {
Self {
cache: DashMap::new(),
order: Mutex::new(VecDeque::new()),
max_entries,
hits: AtomicU64::new(0),
misses: AtomicU64::new(0),
stale: AtomicU64::new(0),
}
}
pub fn get(&self, prefix: &str, query: &Query, current_merkle_root: &[u8]) -> Option<Vec<String>> {
let key = self.cache_key(prefix, query);
if let Some(entry) = self.cache.get(&key) {
if entry.merkle_root == current_merkle_root {
self.hits.fetch_add(1, Ordering::Relaxed);
return Some(entry.keys.clone());
}
self.stale.fetch_add(1, Ordering::Relaxed);
drop(entry); self.cache.remove(&key);
}
self.misses.fetch_add(1, Ordering::Relaxed);
None
}
pub fn insert(&self, prefix: &str, query: &Query, merkle_root: Vec<u8>, keys: Vec<String>) {
let key = self.cache_key(prefix, query);
if self.cache.len() >= self.max_entries {
let mut order = self.order.lock();
while self.cache.len() >= self.max_entries {
if let Some(old_key) = order.pop_front() {
self.cache.remove(&old_key);
} else {
break;
}
}
}
let is_new = !self.cache.contains_key(&key);
self.cache.insert(key.clone(), CacheEntry { merkle_root, keys });
if is_new {
let mut order = self.order.lock();
order.push_back(key);
}
}
pub fn stats(&self) -> SearchCacheStats {
let hits = self.hits.load(Ordering::Relaxed);
let misses = self.misses.load(Ordering::Relaxed);
let total = hits + misses;
SearchCacheStats {
hits,
misses,
stale: self.stale.load(Ordering::Relaxed),
entry_count: self.cache.len(),
hit_rate: if total > 0 {
hits as f64 / total as f64
} else {
0.0
},
}
}
pub fn clear(&self) {
self.cache.clear();
self.order.lock().clear();
}
fn cache_key(&self, prefix: &str, query: &Query) -> CacheKey {
(prefix.to_string(), Self::hash_query(query))
}
fn hash_query(query: &Query) -> u64 {
let mut hasher = DefaultHasher::new();
format!("{:?}", query.root).hash(&mut hasher);
hasher.finish()
}
}
impl Default for SearchCache {
fn default() -> Self {
Self::new(1000)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_hit() {
let cache = SearchCache::new(100);
let query = Query::field_eq("name", "Alice");
let merkle_root = vec![1, 2, 3, 4];
let keys = vec!["crdt:users:1".to_string(), "crdt:users:2".to_string()];
cache.insert("crdt:users:", &query, merkle_root.clone(), keys.clone());
let result = cache.get("crdt:users:", &query, &merkle_root);
assert_eq!(result, Some(keys));
let stats = cache.stats();
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 0);
}
#[test]
fn test_cache_miss() {
let cache = SearchCache::new(100);
let query = Query::field_eq("name", "Alice");
let merkle_root = vec![1, 2, 3, 4];
let result = cache.get("crdt:users:", &query, &merkle_root);
assert_eq!(result, None);
let stats = cache.stats();
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 1);
}
#[test]
fn test_cache_stale_merkle() {
let cache = SearchCache::new(100);
let query = Query::field_eq("name", "Alice");
let old_root = vec![1, 2, 3, 4];
let new_root = vec![5, 6, 7, 8];
let keys = vec!["crdt:users:1".to_string()];
cache.insert("crdt:users:", &query, old_root, keys);
let result = cache.get("crdt:users:", &query, &new_root);
assert_eq!(result, None);
let stats = cache.stats();
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 1);
assert_eq!(stats.stale, 1);
}
#[test]
fn test_different_queries() {
let cache = SearchCache::new(100);
let query1 = Query::field_eq("name", "Alice");
let query2 = Query::field_eq("name", "Bob");
let merkle_root = vec![1, 2, 3, 4];
cache.insert(
"crdt:users:",
&query1,
merkle_root.clone(),
vec!["alice".to_string()],
);
cache.insert(
"crdt:users:",
&query2,
merkle_root.clone(),
vec!["bob".to_string()],
);
assert_eq!(
cache.get("crdt:users:", &query1, &merkle_root),
Some(vec!["alice".to_string()])
);
assert_eq!(
cache.get("crdt:users:", &query2, &merkle_root),
Some(vec!["bob".to_string()])
);
}
#[test]
fn test_different_prefixes() {
let cache = SearchCache::new(100);
let query = Query::field_eq("name", "Alice");
let root1 = vec![1, 2, 3];
let root2 = vec![4, 5, 6];
cache.insert("crdt:users:", &query, root1.clone(), vec!["u1".to_string()]);
cache.insert("crdt:posts:", &query, root2.clone(), vec!["p1".to_string()]);
assert_eq!(
cache.get("crdt:users:", &query, &root1),
Some(vec!["u1".to_string()])
);
assert_eq!(
cache.get("crdt:posts:", &query, &root2),
Some(vec!["p1".to_string()])
);
}
#[test]
fn test_hit_rate() {
let cache = SearchCache::new(100);
let query = Query::field_eq("name", "Alice");
let merkle_root = vec![1, 2, 3, 4];
let keys = vec!["crdt:users:1".to_string()];
cache.insert("crdt:users:", &query, merkle_root.clone(), keys);
cache.get("crdt:users:", &query, &merkle_root);
cache.get("crdt:users:", &query, &merkle_root);
cache.get("crdt:users:", &query, &merkle_root);
cache.get("crdt:users:", &Query::field_eq("x", "y"), &merkle_root);
let stats = cache.stats();
assert_eq!(stats.hits, 3);
assert_eq!(stats.misses, 1);
assert!((stats.hit_rate - 0.75).abs() < 0.01);
}
#[test]
fn test_eviction_oldest() {
let cache = SearchCache::new(3); let root = vec![1, 2, 3];
cache.insert("p:", &Query::field_eq("n", "1"), root.clone(), vec!["k1".into()]);
cache.insert("p:", &Query::field_eq("n", "2"), root.clone(), vec!["k2".into()]);
cache.insert("p:", &Query::field_eq("n", "3"), root.clone(), vec!["k3".into()]);
assert_eq!(cache.stats().entry_count, 3);
cache.insert("p:", &Query::field_eq("n", "4"), root.clone(), vec!["k4".into()]);
assert_eq!(cache.stats().entry_count, 3);
assert!(cache.get("p:", &Query::field_eq("n", "1"), &root).is_none());
assert!(cache.get("p:", &Query::field_eq("n", "4"), &root).is_some());
}
}