use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant, SystemTime};
use crate::error::{Result, TronError};
use crate::template::TronTemplate;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CacheKey {
pub path: Option<PathBuf>,
pub content_hash: u64,
}
impl CacheKey {
pub fn from_template(template: &TronTemplate) -> Self {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
template.content().hash(&mut hasher);
Self {
path: template.path().map(|p| p.to_path_buf()),
content_hash: hasher.finish(),
}
}
pub fn from_content(content: &str, path: Option<&std::path::Path>) -> Self {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
content.hash(&mut hasher);
Self {
path: path.map(|p| p.to_path_buf()),
content_hash: hasher.finish(),
}
}
}
#[derive(Debug, Clone)]
pub struct CachedTemplate {
pub template: TronTemplate,
pub cached_at: Instant,
pub file_modified: Option<SystemTime>,
pub access_count: u32,
pub last_accessed: Instant,
}
impl CachedTemplate {
pub fn new(template: TronTemplate) -> Self {
let now = Instant::now();
let file_modified = if let Some(path) = template.path() {
std::fs::metadata(path)
.ok()
.and_then(|m| m.modified().ok())
} else {
None
};
Self {
template,
cached_at: now,
file_modified,
access_count: 0,
last_accessed: now,
}
}
pub fn mark_accessed(&mut self) {
self.access_count += 1;
self.last_accessed = Instant::now();
}
pub fn is_valid(&self, max_age: Option<Duration>) -> bool {
if let Some(max_age) = max_age {
if self.cached_at.elapsed() > max_age {
return false;
}
}
if let (Some(path), Some(cached_modified)) = (self.template.path(), self.file_modified) {
if let Ok(metadata) = std::fs::metadata(path) {
if let Ok(current_modified) = metadata.modified() {
if current_modified > cached_modified {
return false;
}
}
}
}
true
}
}
#[derive(Debug, Clone)]
pub struct CacheConfig {
pub max_size: usize,
pub max_age: Option<Duration>,
pub track_file_changes: bool,
pub enable_stats: bool,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
max_size: 100,
max_age: Some(Duration::from_secs(300)), track_file_changes: true,
enable_stats: true,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
pub hits: u64,
pub misses: u64,
pub evictions: u64,
pub invalidations: u64,
}
impl CacheStats {
pub fn hit_ratio(&self) -> f64 {
let total = self.hits + self.misses;
if total == 0 {
0.0
} else {
self.hits as f64 / total as f64
}
}
pub fn reset(&mut self) {
*self = Self::default();
}
}
pub struct TemplateCache {
cache: Arc<RwLock<HashMap<CacheKey, CachedTemplate>>>,
config: CacheConfig,
stats: Arc<RwLock<CacheStats>>,
}
impl TemplateCache {
pub fn new() -> Self {
Self::with_config(CacheConfig::default())
}
pub fn with_config(config: CacheConfig) -> Self {
Self {
cache: Arc::new(RwLock::new(HashMap::new())),
config,
stats: Arc::new(RwLock::new(CacheStats::default())),
}
}
pub fn insert_template(&self, template: TronTemplate) -> Result<()> {
let key = CacheKey::from_template(&template);
let cached_template = CachedTemplate::new(template);
let mut cache = self.cache.write().unwrap();
if cache.len() >= self.config.max_size {
self.evict_lru(&mut cache);
}
cache.insert(key, cached_template);
Ok(())
}
pub fn get_by_content(&self, content: &str) -> Option<TronTemplate> {
let key = CacheKey::from_content(content, None);
self.get_by_key(&key)
}
pub fn get_by_path(&self, path: &std::path::Path) -> Option<TronTemplate> {
let target_path = path.to_path_buf();
let mut found_key = None;
let mut is_valid = false;
let mut template_clone = None;
{
let cache = self.cache.read().unwrap();
for (cache_key, cached_template) in cache.iter() {
if cache_key.path.as_ref() == Some(&target_path) {
found_key = Some(cache_key.clone());
is_valid = cached_template.is_valid(self.config.max_age);
if is_valid {
template_clone = Some(cached_template.template.clone());
}
break;
}
}
}
if let Some(key) = found_key {
if is_valid {
{
let mut cache = self.cache.write().unwrap();
if let Some(entry) = cache.get_mut(&key) {
entry.mark_accessed();
}
}
if self.config.enable_stats {
let mut stats = self.stats.write().unwrap();
stats.hits += 1;
}
template_clone
} else {
{
let mut cache = self.cache.write().unwrap();
cache.remove(&key);
}
if self.config.enable_stats {
let mut stats = self.stats.write().unwrap();
stats.invalidations += 1;
}
None
}
} else {
if self.config.enable_stats {
let mut stats = self.stats.write().unwrap();
stats.misses += 1;
}
None
}
}
fn get_by_key(&self, key: &CacheKey) -> Option<TronTemplate> {
let cache = self.cache.read().unwrap();
if let Some(cached_template) = cache.get(key) {
if cached_template.is_valid(self.config.max_age) {
drop(cache);
let mut cache = self.cache.write().unwrap();
if let Some(entry) = cache.get_mut(key) {
entry.mark_accessed();
if self.config.enable_stats {
let mut stats = self.stats.write().unwrap();
stats.hits += 1;
}
return Some(entry.template.clone());
}
} else {
drop(cache);
let mut cache = self.cache.write().unwrap();
cache.remove(key);
if self.config.enable_stats {
let mut stats = self.stats.write().unwrap();
stats.invalidations += 1;
}
}
}
if self.config.enable_stats {
let mut stats = self.stats.write().unwrap();
stats.misses += 1;
}
None
}
fn evict_lru(&self, cache: &mut HashMap<CacheKey, CachedTemplate>) {
if cache.is_empty() {
return;
}
let oldest_key = cache
.iter()
.min_by_key(|(_, entry)| entry.last_accessed)
.map(|(key, _)| key.clone());
if let Some(key) = oldest_key {
cache.remove(&key);
if self.config.enable_stats {
if let Ok(mut stats) = self.stats.write() {
stats.evictions += 1;
}
}
}
}
pub fn clear(&self) {
let mut cache = self.cache.write().unwrap();
cache.clear();
}
pub fn size(&self) -> usize {
let cache = self.cache.read().unwrap();
cache.len()
}
pub fn is_empty(&self) -> bool {
self.size() == 0
}
pub fn is_cached<P: AsRef<std::path::Path>>(&self, path: P) -> bool {
let target_path = path.as_ref().to_path_buf();
let cache = self.cache.read().unwrap();
for cache_key in cache.keys() {
if cache_key.path.as_ref() == Some(&target_path) {
return true;
}
}
false
}
pub fn stats(&self) -> Option<CacheStats> {
if self.config.enable_stats {
let stats = self.stats.read().unwrap();
Some(stats.clone())
} else {
None
}
}
pub fn reset_stats(&self) {
if self.config.enable_stats {
let mut stats = self.stats.write().unwrap();
stats.reset();
}
}
pub fn cleanup_expired(&self) {
let mut cache = self.cache.write().unwrap();
let max_age = self.config.max_age;
let expired_keys: Vec<CacheKey> = cache
.iter()
.filter_map(|(key, entry)| {
if !entry.is_valid(max_age) {
Some(key.clone())
} else {
None
}
})
.collect();
for key in expired_keys {
cache.remove(&key);
if self.config.enable_stats {
if let Ok(mut stats) = self.stats.write() {
stats.invalidations += 1;
}
}
}
}
}
impl Default for TemplateCache {
fn default() -> Self {
Self::new()
}
}
unsafe impl Send for TemplateCache {}
unsafe impl Sync for TemplateCache {}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::Duration;
#[test]
fn test_cache_key_creation() -> Result<()> {
let template = TronTemplate::new("Hello @[name]@!")?;
let key1 = CacheKey::from_template(&template);
let key2 = CacheKey::from_content("Hello @[name]@!", None);
assert_eq!(key1.content_hash, key2.content_hash);
assert_eq!(key1.path, key2.path);
Ok(())
}
#[test]
fn test_basic_caching() -> Result<()> {
let cache = TemplateCache::new();
let template = TronTemplate::new("Hello @[name]@!")?;
cache.insert_template(template.clone())?;
assert_eq!(cache.size(), 1);
let cached = cache.get_by_content("Hello @[name]@!");
assert!(cached.is_some());
assert_eq!(cached.unwrap().content(), template.content());
Ok(())
}
#[test]
fn test_cache_miss() {
let cache = TemplateCache::new();
let cached = cache.get_by_content("Nonexistent template");
assert!(cached.is_none());
}
#[test]
fn test_cache_stats() -> Result<()> {
let config = CacheConfig {
enable_stats: true,
..Default::default()
};
let cache = TemplateCache::with_config(config);
let template = TronTemplate::new("Hello @[name]@!")?;
cache.insert_template(template)?;
let _ = cache.get_by_content("Hello @[name]@!");
let _ = cache.get_by_content("Nonexistent");
let stats = cache.stats().unwrap();
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 1);
assert_eq!(stats.hit_ratio(), 0.5);
Ok(())
}
#[test]
fn test_cache_eviction() -> Result<()> {
let config = CacheConfig {
max_size: 2,
..Default::default()
};
let cache = TemplateCache::with_config(config);
let template1 = TronTemplate::new("Template 1 @[name]@")?;
let template2 = TronTemplate::new("Template 2 @[name]@")?;
let template3 = TronTemplate::new("Template 3 @[name]@")?;
cache.insert_template(template1)?;
cache.insert_template(template2)?;
cache.insert_template(template3)?;
assert_eq!(cache.size(), 2);
let cached1 = cache.get_by_content("Template 1 @[name]@");
assert!(cached1.is_none());
let cached2 = cache.get_by_content("Template 2 @[name]@");
let cached3 = cache.get_by_content("Template 3 @[name]@");
assert!(cached2.is_some());
assert!(cached3.is_some());
Ok(())
}
#[test]
fn test_cache_clear() -> Result<()> {
let cache = TemplateCache::new();
let template = TronTemplate::new("Hello @[name]@!")?;
cache.insert_template(template)?;
assert_eq!(cache.size(), 1);
cache.clear();
assert_eq!(cache.size(), 0);
assert!(cache.is_empty());
Ok(())
}
#[test]
fn test_thread_safety() -> Result<()> {
let cache = Arc::new(TemplateCache::new());
let mut handles = vec![];
for i in 0..10 {
let cache = Arc::clone(&cache);
let handle = thread::spawn(move || -> Result<()> {
let template = TronTemplate::new(&format!("Template {} @[name]@", i))?;
cache.insert_template(template)?;
Ok(())
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap()?;
}
assert_eq!(cache.size(), 10);
Ok(())
}
#[test]
fn test_cached_template_validity() -> Result<()> {
let template = TronTemplate::new("Hello @[name]@!")?;
let mut cached = CachedTemplate::new(template);
assert!(cached.is_valid(Some(Duration::from_secs(60))));
assert!(!cached.is_valid(Some(Duration::from_nanos(1))));
assert_eq!(cached.access_count, 0);
cached.mark_accessed();
assert_eq!(cached.access_count, 1);
Ok(())
}
}