use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
#[derive(Debug)]
pub struct TemplateCacheEntry {
pub data: Vec<u8>,
last_access: Instant,
access_count: u64,
template_hash: u64,
source_hash: u64,
}
impl TemplateCacheEntry {
pub fn new(data: Vec<u8>, template_hash: u64, source_hash: u64) -> Self {
Self {
data,
last_access: Instant::now(),
access_count: 1,
template_hash,
source_hash,
}
}
fn touch(&mut self) {
self.last_access = Instant::now();
self.access_count += 1;
}
pub fn is_stale(&self, current_source_hash: u64) -> bool {
self.source_hash != current_source_hash
}
pub fn age(&self) -> Duration {
self.last_access.elapsed()
}
}
#[derive(Debug)]
pub struct HotCache {
entries: HashMap<u64, TemplateCacheEntry>,
max_size: usize,
current_size: usize,
hits: AtomicU64,
misses: AtomicU64,
ttl: Duration,
}
impl HotCache {
pub fn new(max_size: usize) -> Self {
Self {
entries: HashMap::new(),
max_size,
current_size: 0,
hits: AtomicU64::new(0),
misses: AtomicU64::new(0),
ttl: Duration::from_secs(3600), }
}
pub fn with_ttl(mut self, ttl: Duration) -> Self {
self.ttl = ttl;
self
}
pub fn get(&mut self, template_hash: u64) -> Option<&[u8]> {
if let Some(entry) = self.entries.get_mut(&template_hash) {
if entry.age() < self.ttl {
entry.touch();
self.hits.fetch_add(1, Ordering::Relaxed);
return Some(&entry.data);
}
}
self.misses.fetch_add(1, Ordering::Relaxed);
None
}
pub fn insert(&mut self, template_hash: u64, source_hash: u64, data: Vec<u8>) {
let entry_size = data.len();
while self.current_size + entry_size > self.max_size && !self.entries.is_empty() {
self.evict_one();
}
if entry_size > self.max_size {
return;
}
if let Some(old) = self.entries.remove(&template_hash) {
self.current_size -= old.data.len();
}
let entry = TemplateCacheEntry::new(data, template_hash, source_hash);
self.current_size += entry_size;
self.entries.insert(template_hash, entry);
}
pub fn invalidate_stale(&mut self, template_hash: u64, current_source_hash: u64) {
if let Some(entry) = self.entries.get(&template_hash) {
if entry.is_stale(current_source_hash) {
if let Some(removed) = self.entries.remove(&template_hash) {
self.current_size -= removed.data.len();
}
}
}
}
pub fn clear(&mut self) {
self.entries.clear();
self.current_size = 0;
}
pub fn stats(&self) -> CacheStats {
let hits = self.hits.load(Ordering::Relaxed);
let misses = self.misses.load(Ordering::Relaxed);
CacheStats {
entries: self.entries.len(),
size_bytes: self.current_size,
max_size_bytes: self.max_size,
hits,
misses,
hit_rate: if hits + misses > 0 {
hits as f64 / (hits + misses) as f64
} else {
0.0
},
}
}
fn evict_one(&mut self) {
let oldest = self
.entries
.iter()
.min_by_key(|(_, e)| e.last_access)
.map(|(k, _)| *k);
if let Some(key) = oldest {
if let Some(entry) = self.entries.remove(&key) {
self.current_size -= entry.data.len();
}
}
}
}
impl Default for HotCache {
fn default() -> Self {
Self::new(64 * 1024 * 1024)
}
}
#[derive(Debug, Clone)]
pub struct CacheStats {
pub entries: usize,
pub size_bytes: usize,
pub max_size_bytes: usize,
pub hits: u64,
pub misses: u64,
pub hit_rate: f64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_hit_miss() {
let mut cache = HotCache::new(1024);
cache.insert(1, 100, vec![1, 2, 3, 4]);
assert!(cache.get(1).is_some());
assert!(cache.get(2).is_none());
let stats = cache.stats();
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 1);
}
#[test]
fn test_cache_eviction() {
let mut cache = HotCache::new(100);
for i in 0..10 {
cache.insert(i, i * 10, vec![0u8; 20]);
}
assert!(cache.entries.len() <= 5);
}
#[test]
fn test_cache_invalidation() {
let mut cache = HotCache::new(1024);
cache.insert(1, 100, vec![1, 2, 3]);
cache.invalidate_stale(1, 100);
assert!(cache.entries.contains_key(&1));
cache.invalidate_stale(1, 200);
assert!(!cache.entries.contains_key(&1));
}
}