use std::collections::{HashMap, VecDeque};
fn fnv1a_hash(data: &[u8]) -> u64 {
const FNV_OFFSET_BASIS: u64 = 14_695_981_039_346_656_037;
const FNV_PRIME: u64 = 1_099_511_628_211;
let mut hash = FNV_OFFSET_BASIS;
for byte in data {
hash ^= *byte as u64;
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
fn query_hash(query: &str) -> u64 {
fnv1a_hash(query.as_bytes())
}
#[derive(Debug, Clone)]
pub struct CacheEntry {
pub query_hash: u64,
pub response: String,
pub created_at: u64,
pub access_count: usize,
pub ttl_ms: u64,
}
impl CacheEntry {
pub fn is_valid(&self, now_ms: u64) -> bool {
now_ms < self.created_at.saturating_add(self.ttl_ms)
}
}
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
pub hits: u64,
pub misses: u64,
pub evictions: u64,
pub entries: usize,
}
pub struct ResponseCache {
capacity: usize,
default_ttl_ms: u64,
entries: HashMap<u64, CacheEntry>,
lru_order: VecDeque<u64>,
stats: CacheStats,
}
impl ResponseCache {
pub fn new(capacity: usize, default_ttl_ms: u64) -> Self {
Self {
capacity: capacity.max(1),
default_ttl_ms,
entries: HashMap::new(),
lru_order: VecDeque::new(),
stats: CacheStats::default(),
}
}
pub fn get(&mut self, query: &str, now_ms: u64) -> Option<String> {
let key = query_hash(query);
match self.entries.get_mut(&key) {
Some(entry) if entry.is_valid(now_ms) => {
entry.access_count += 1;
self.stats.hits += 1;
let response = entry.response.clone();
self.touch_lru(key);
Some(response)
}
_ => {
self.stats.misses += 1;
None
}
}
}
pub fn put(&mut self, query: &str, response: String, now_ms: u64) {
let key = query_hash(query);
if !self.entries.contains_key(&key) && self.entries.len() >= self.capacity {
self.evict_lru();
}
if self.entries.contains_key(&key) {
self.lru_order.retain(|&k| k != key);
}
let entry = CacheEntry {
query_hash: key,
response,
created_at: now_ms,
access_count: 0,
ttl_ms: self.default_ttl_ms,
};
self.entries.insert(key, entry);
self.lru_order.push_front(key);
self.stats.entries = self.entries.len();
}
pub fn invalidate(&mut self, query: &str) -> bool {
let key = query_hash(query);
if self.entries.remove(&key).is_some() {
self.lru_order.retain(|&k| k != key);
self.stats.entries = self.entries.len();
true
} else {
false
}
}
pub fn invalidate_expired(&mut self, now_ms: u64) -> usize {
let expired_keys: Vec<u64> = self
.entries
.iter()
.filter(|(_, e)| !e.is_valid(now_ms))
.map(|(k, _)| *k)
.collect();
let count = expired_keys.len();
for key in expired_keys {
self.entries.remove(&key);
self.lru_order.retain(|&k| k != key);
}
self.stats.entries = self.entries.len();
count
}
pub fn stats(&self) -> &CacheStats {
&self.stats
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn default_ttl_ms(&self) -> u64 {
self.default_ttl_ms
}
pub fn clear(&mut self) {
self.entries.clear();
self.lru_order.clear();
self.stats = CacheStats::default();
}
fn touch_lru(&mut self, key: u64) {
self.lru_order.retain(|&k| k != key);
self.lru_order.push_front(key);
}
fn evict_lru(&mut self) {
if let Some(lru_key) = self.lru_order.pop_back() {
self.entries.remove(&lru_key);
self.stats.evictions += 1;
self.stats.entries = self.entries.len();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hash_deterministic() {
assert_eq!(query_hash("hello"), query_hash("hello"));
}
#[test]
fn test_hash_different_inputs() {
assert_ne!(query_hash("hello"), query_hash("world"));
}
#[test]
fn test_hash_empty_string() {
let h = query_hash("");
let _ = h;
}
#[test]
fn test_entry_is_valid_before_expiry() {
let entry = CacheEntry {
query_hash: 1,
response: "r".to_string(),
created_at: 1000,
access_count: 0,
ttl_ms: 5000,
};
assert!(entry.is_valid(5999));
}
#[test]
fn test_entry_is_invalid_after_expiry() {
let entry = CacheEntry {
query_hash: 1,
response: "r".to_string(),
created_at: 1000,
access_count: 0,
ttl_ms: 5000,
};
assert!(!entry.is_valid(6001));
}
#[test]
fn test_entry_exact_boundary_invalid() {
let entry = CacheEntry {
query_hash: 1,
response: "r".to_string(),
created_at: 1000,
access_count: 0,
ttl_ms: 5000,
};
assert!(!entry.is_valid(6000));
}
#[test]
fn test_new_cache_is_empty() {
let cache = ResponseCache::new(100, 60_000);
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
}
#[test]
fn test_new_cache_capacity_stored() {
let cache = ResponseCache::new(50, 10_000);
assert_eq!(cache.capacity(), 50);
}
#[test]
fn test_new_cache_ttl_stored() {
let cache = ResponseCache::new(50, 99_999);
assert_eq!(cache.default_ttl_ms(), 99_999);
}
#[test]
fn test_basic_put_and_get() {
let mut cache = ResponseCache::new(10, 60_000);
cache.put("q1", "response1".to_string(), 0);
assert_eq!(cache.get("q1", 1000), Some("response1".to_string()));
}
#[test]
fn test_get_missing_returns_none() {
let mut cache = ResponseCache::new(10, 60_000);
assert!(cache.get("nonexistent", 0).is_none());
}
#[test]
fn test_get_after_ttl_expiry_returns_none() {
let mut cache = ResponseCache::new(10, 5_000);
cache.put("q", "r".to_string(), 0);
assert!(cache.get("q", 5_001).is_none());
}
#[test]
fn test_get_within_ttl_succeeds() {
let mut cache = ResponseCache::new(10, 5_000);
cache.put("q", "r".to_string(), 1000);
assert!(cache.get("q", 5_999).is_some());
}
#[test]
fn test_put_updates_existing_entry() {
let mut cache = ResponseCache::new(10, 60_000);
cache.put("q", "v1".to_string(), 0);
cache.put("q", "v2".to_string(), 0);
assert_eq!(cache.get("q", 0), Some("v2".to_string()));
assert_eq!(cache.len(), 1); }
#[test]
fn test_put_multiple_entries() {
let mut cache = ResponseCache::new(10, 60_000);
cache.put("q1", "r1".to_string(), 0);
cache.put("q2", "r2".to_string(), 0);
cache.put("q3", "r3".to_string(), 0);
assert_eq!(cache.len(), 3);
}
#[test]
fn test_eviction_at_capacity() {
let mut cache = ResponseCache::new(3, 60_000);
cache.put("q1", "r1".to_string(), 0);
cache.put("q2", "r2".to_string(), 0);
cache.put("q3", "r3".to_string(), 0);
cache.get("q1", 0);
cache.put("q4", "r4".to_string(), 0);
assert_eq!(cache.len(), 3);
assert!(cache.get("q2", 0).is_none()); assert!(cache.get("q1", 0).is_some()); assert!(cache.get("q3", 0).is_some()); assert!(cache.get("q4", 0).is_some()); }
#[test]
fn test_eviction_counter_increments() {
let mut cache = ResponseCache::new(2, 60_000);
cache.put("q1", "r1".to_string(), 0);
cache.put("q2", "r2".to_string(), 0);
cache.put("q3", "r3".to_string(), 0); assert_eq!(cache.stats().evictions, 1);
}
#[test]
fn test_no_eviction_within_capacity() {
let mut cache = ResponseCache::new(5, 60_000);
cache.put("q1", "r1".to_string(), 0);
cache.put("q2", "r2".to_string(), 0);
assert_eq!(cache.stats().evictions, 0);
}
#[test]
fn test_invalidate_existing_returns_true() {
let mut cache = ResponseCache::new(10, 60_000);
cache.put("q", "r".to_string(), 0);
assert!(cache.invalidate("q"));
}
#[test]
fn test_invalidate_missing_returns_false() {
let mut cache = ResponseCache::new(10, 60_000);
assert!(!cache.invalidate("nonexistent"));
}
#[test]
fn test_invalidate_removes_entry() {
let mut cache = ResponseCache::new(10, 60_000);
cache.put("q", "r".to_string(), 0);
cache.invalidate("q");
assert!(cache.get("q", 0).is_none());
assert_eq!(cache.len(), 0);
}
#[test]
fn test_invalidate_allows_reinsertion() {
let mut cache = ResponseCache::new(10, 60_000);
cache.put("q", "r1".to_string(), 0);
cache.invalidate("q");
cache.put("q", "r2".to_string(), 0);
assert_eq!(cache.get("q", 0), Some("r2".to_string()));
}
#[test]
fn test_invalidate_expired_removes_stale() {
let mut cache = ResponseCache::new(10, 1_000);
cache.put("q1", "r1".to_string(), 0);
cache.put("q2", "r2".to_string(), 0);
let purged = cache.invalidate_expired(2_000); assert_eq!(purged, 2);
assert!(cache.is_empty());
}
#[test]
fn test_invalidate_expired_keeps_fresh() {
let mut cache = ResponseCache::new(10, 10_000);
cache.put("q1", "r1".to_string(), 0); cache.put("q2", "r2".to_string(), 5_000); let purged = cache.invalidate_expired(11_000); assert_eq!(purged, 1);
assert_eq!(cache.len(), 1);
assert!(cache.get("q2", 11_000).is_some());
}
#[test]
fn test_invalidate_expired_none_expired() {
let mut cache = ResponseCache::new(10, 60_000);
cache.put("q", "r".to_string(), 0);
let purged = cache.invalidate_expired(1_000);
assert_eq!(purged, 0);
assert_eq!(cache.len(), 1);
}
#[test]
fn test_invalidate_expired_empty_cache() {
let mut cache = ResponseCache::new(10, 60_000);
assert_eq!(cache.invalidate_expired(0), 0);
}
#[test]
fn test_stats_initial_zeros() {
let cache = ResponseCache::new(10, 60_000);
let s = cache.stats();
assert_eq!(s.hits, 0);
assert_eq!(s.misses, 0);
assert_eq!(s.evictions, 0);
assert_eq!(s.entries, 0);
}
#[test]
fn test_stats_hit_increments() {
let mut cache = ResponseCache::new(10, 60_000);
cache.put("q", "r".to_string(), 0);
cache.get("q", 0);
assert_eq!(cache.stats().hits, 1);
}
#[test]
fn test_stats_miss_increments() {
let mut cache = ResponseCache::new(10, 60_000);
cache.get("q", 0);
assert_eq!(cache.stats().misses, 1);
}
#[test]
fn test_stats_miss_on_expired() {
let mut cache = ResponseCache::new(10, 1_000);
cache.put("q", "r".to_string(), 0);
cache.get("q", 2_000); assert_eq!(cache.stats().misses, 1);
assert_eq!(cache.stats().hits, 0);
}
#[test]
fn test_stats_entries_tracks_len() {
let mut cache = ResponseCache::new(10, 60_000);
cache.put("q1", "r1".to_string(), 0);
cache.put("q2", "r2".to_string(), 0);
assert_eq!(cache.stats().entries, 2);
cache.invalidate("q1");
assert_eq!(cache.stats().entries, 1);
}
#[test]
fn test_access_count_increments_on_hit() {
let mut cache = ResponseCache::new(10, 60_000);
cache.put("q", "r".to_string(), 0);
cache.get("q", 0);
cache.get("q", 0);
let entry = cache.entries.get(&query_hash("q")).expect("entry exists");
assert_eq!(entry.access_count, 2);
}
#[test]
fn test_is_empty_after_clear() {
let mut cache = ResponseCache::new(10, 60_000);
cache.put("q", "r".to_string(), 0);
cache.clear();
assert!(cache.is_empty());
}
#[test]
fn test_len_after_clear() {
let mut cache = ResponseCache::new(10, 60_000);
cache.put("q1", "r1".to_string(), 0);
cache.put("q2", "r2".to_string(), 0);
cache.clear();
assert_eq!(cache.len(), 0);
}
#[test]
fn test_clear_resets_stats() {
let mut cache = ResponseCache::new(10, 60_000);
cache.put("q", "r".to_string(), 0);
cache.get("q", 0);
cache.clear();
assert_eq!(cache.stats().hits, 0);
assert_eq!(cache.stats().entries, 0);
}
#[test]
fn test_minimum_capacity_is_one() {
let cache = ResponseCache::new(0, 60_000);
assert_eq!(cache.capacity(), 1);
}
#[test]
fn test_large_capacity() {
let cache = ResponseCache::new(100_000, 3_600_000);
assert_eq!(cache.capacity(), 100_000);
}
#[test]
fn test_lru_order_after_access() {
let mut cache = ResponseCache::new(3, 60_000);
cache.put("q1", "r1".to_string(), 0);
cache.put("q2", "r2".to_string(), 0);
cache.put("q3", "r3".to_string(), 0);
cache.get("q1", 0);
cache.get("q3", 0);
cache.put("q4", "r4".to_string(), 0);
assert!(cache.get("q2", 0).is_none());
assert!(cache.get("q1", 0).is_some());
assert!(cache.get("q3", 0).is_some());
assert!(cache.get("q4", 0).is_some());
}
#[test]
fn test_zero_ttl_always_expired() {
let mut cache = ResponseCache::new(10, 0);
cache.put("q", "r".to_string(), 0);
assert!(cache.get("q", 0).is_none());
}
#[test]
fn test_very_large_ttl() {
let mut cache = ResponseCache::new(10, u64::MAX / 2);
cache.put("q", "r".to_string(), 0);
assert!(cache.get("q", u64::MAX / 4).is_some());
}
}