use crate::episode::Episode;
use crate::retrieval::cache::types::{
CacheKey, CacheMetrics, CachedResult, DEFAULT_CACHE_TTL, DEFAULT_MAX_ENTRIES,
};
use lru::LruCache;
use parking_lot::RwLock;
use std::collections::{HashMap, HashSet};
use std::num::NonZeroUsize;
use std::sync::Arc;
use std::time::{Duration, Instant};
pub struct QueryCache {
cache: Arc<RwLock<LruCache<u64, CachedResult>>>,
domain_index: Arc<RwLock<HashMap<Arc<str>, HashSet<u64>>>>,
invalidated_hashes: Arc<RwLock<HashSet<u64>>>,
metrics: Arc<RwLock<CacheMetrics>>,
default_ttl: Duration,
max_entries: usize,
}
impl QueryCache {
#[must_use]
pub fn new() -> Self {
Self::with_capacity_and_ttl(DEFAULT_MAX_ENTRIES, DEFAULT_CACHE_TTL)
}
#[must_use]
pub fn with_capacity_and_ttl(capacity: usize, ttl: Duration) -> Self {
let safe_capacity = capacity.max(1);
let cache = LruCache::new(
NonZeroUsize::new(safe_capacity)
.expect("QueryCache: capacity is guaranteed to be non-zero after max(1)"),
);
let metrics = CacheMetrics {
capacity: safe_capacity,
..Default::default()
};
Self {
cache: Arc::new(RwLock::new(cache)),
domain_index: Arc::new(RwLock::new(HashMap::new())),
invalidated_hashes: Arc::new(RwLock::new(HashSet::new())),
metrics: Arc::new(RwLock::new(metrics)),
default_ttl: ttl,
max_entries: safe_capacity,
}
}
#[must_use]
pub fn get(&self, key: &CacheKey) -> Option<Vec<Arc<Episode>>> {
let key_hash = key.compute_hash();
{
let invalidated = self.invalidated_hashes.read();
if invalidated.contains(&key_hash) {
let mut metrics = self.metrics.write();
metrics.misses += 1;
return None;
}
}
let mut cache = self.cache.write();
let mut metrics = self.metrics.write();
if let Some(result) = cache.get(&key_hash) {
if result.is_expired() {
cache.pop(&key_hash);
metrics.misses += 1;
metrics.evictions += 1;
metrics.size = cache.len();
return None;
}
metrics.hits += 1;
let episodes: Vec<Arc<Episode>> = result.episodes.to_vec();
Some(episodes)
} else {
metrics.misses += 1;
metrics.size = cache.len();
None
}
}
pub fn put(&self, key: CacheKey, episodes: Vec<Arc<Episode>>) {
let key_hash = key.compute_hash();
let episodes_slice: Arc<[Arc<Episode>]> = episodes.into();
let cached_result = CachedResult {
episodes: episodes_slice,
cached_at: Instant::now(),
ttl: self.default_ttl,
};
let mut cache = self.cache.write();
let was_present = cache.contains(&key_hash);
let was_at_capacity = cache.len() >= self.max_entries;
cache.put(key_hash, cached_result);
if let Some(ref domain) = key.domain {
let mut domain_index = self.domain_index.write();
domain_index
.entry(Arc::clone(domain))
.or_default()
.insert(key_hash);
}
let mut metrics = self.metrics.write();
metrics.size = cache.len();
if was_present {
return;
}
if was_at_capacity {
metrics.evictions += 1;
}
}
pub fn invalidate_all(&self) {
let mut cache = self.cache.write();
let count = cache.len();
cache.clear();
let mut domain_index = self.domain_index.write();
domain_index.clear();
let mut invalidated = self.invalidated_hashes.write();
invalidated.clear();
let mut metrics = self.metrics.write();
metrics.size = 0;
metrics.invalidations += count as u64;
}
pub fn invalidate_domain(&self, domain: &str) {
let hashes_to_invalidate = {
let domain_index = self.domain_index.read();
domain_index.get(domain).cloned()
};
if let Some(hashes) = hashes_to_invalidate {
let count = hashes.len();
{
let mut invalidated = self.invalidated_hashes.write();
for hash in &hashes {
invalidated.insert(*hash);
}
}
{
let mut domain_index = self.domain_index.write();
domain_index.remove(domain);
}
{
let mut metrics = self.metrics.write();
metrics.invalidations += count as u64;
} }
}
#[must_use]
pub fn metrics(&self) -> CacheMetrics {
let metrics = self.metrics.read();
metrics.clone()
}
pub fn clear_metrics(&self) {
let mut metrics = self.metrics.write();
*metrics = CacheMetrics {
capacity: self.max_entries,
..Default::default()
};
}
#[must_use]
pub fn size(&self) -> usize {
self.cache.read().len()
}
#[must_use]
pub fn effective_size(&self) -> usize {
let cache_size = self.cache.read().len();
let invalidated_size = self.invalidated_hashes.read().len();
cache_size.saturating_sub(invalidated_size)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.cache.read().is_empty()
}
}
impl Default for QueryCache {
fn default() -> Self {
Self::new()
}
}