use crate::{CacheError, Result};
use chrono::{DateTime, Utc};
use llm_config_core::ConfigEntry;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
#[derive(Debug, Clone)]
struct CachedEntry {
entry: ConfigEntry,
accessed_at: DateTime<Utc>,
access_count: u64,
}
pub struct L1Cache {
cache: Arc<RwLock<HashMap<String, CachedEntry>>>,
max_size: usize,
hit_count: Arc<RwLock<u64>>,
miss_count: Arc<RwLock<u64>>,
}
impl L1Cache {
pub fn new(max_size: usize) -> Self {
Self {
cache: Arc::new(RwLock::new(HashMap::new())),
max_size,
hit_count: Arc::new(RwLock::new(0)),
miss_count: Arc::new(RwLock::new(0)),
}
}
fn cache_key(namespace: &str, key: &str, env: &str) -> String {
format!("{}:{}:{}", namespace, key, env)
}
pub fn get(&self, namespace: &str, key: &str, env: &str) -> Result<ConfigEntry> {
let cache_key = Self::cache_key(namespace, key, env);
let mut cache = self.cache.write().unwrap();
if let Some(cached) = cache.get_mut(&cache_key) {
cached.accessed_at = Utc::now();
cached.access_count += 1;
*self.hit_count.write().unwrap() += 1;
Ok(cached.entry.clone())
} else {
*self.miss_count.write().unwrap() += 1;
Err(CacheError::CacheMiss(cache_key))
}
}
pub fn put(&self, entry: ConfigEntry) -> Result<()> {
let cache_key = Self::cache_key(&entry.namespace, &entry.key, &entry.environment.to_string());
let mut cache = self.cache.write().unwrap();
if cache.len() >= self.max_size && !cache.contains_key(&cache_key) {
self.evict_lru(&mut cache)?;
}
cache.insert(
cache_key,
CachedEntry {
entry,
accessed_at: Utc::now(),
access_count: 1,
},
);
Ok(())
}
fn evict_lru(&self, cache: &mut HashMap<String, CachedEntry>) -> Result<()> {
if cache.is_empty() {
return Ok(());
}
let lru_key = cache
.iter()
.min_by_key(|(_, entry)| entry.accessed_at)
.map(|(k, _)| k.clone())
.ok_or_else(|| CacheError::Eviction("Failed to find LRU entry".to_string()))?;
cache.remove(&lru_key);
Ok(())
}
pub fn invalidate(&self, namespace: &str, key: &str, env: &str) {
let cache_key = Self::cache_key(namespace, key, env);
let mut cache = self.cache.write().unwrap();
cache.remove(&cache_key);
}
pub fn clear(&self) {
let mut cache = self.cache.write().unwrap();
cache.clear();
*self.hit_count.write().unwrap() = 0;
*self.miss_count.write().unwrap() = 0;
}
pub fn stats(&self) -> CacheStats {
let cache = self.cache.read().unwrap();
let hit_count = *self.hit_count.read().unwrap();
let miss_count = *self.miss_count.read().unwrap();
CacheStats {
size: cache.len(),
max_size: self.max_size,
hit_count,
miss_count,
hit_rate: if hit_count + miss_count > 0 {
hit_count as f64 / (hit_count + miss_count) as f64
} else {
0.0
},
}
}
}
#[derive(Debug, Clone)]
pub struct CacheStats {
pub size: usize,
pub max_size: usize,
pub hit_count: u64,
pub miss_count: u64,
pub hit_rate: f64,
}
#[cfg(test)]
mod tests {
use super::*;
use llm_config_core::{ConfigMetadata, ConfigValue, Environment};
use uuid::Uuid;
fn create_test_entry(namespace: &str, key: &str, env: Environment) -> ConfigEntry {
ConfigEntry {
id: Uuid::new_v4(),
namespace: namespace.to_string(),
key: key.to_string(),
value: ConfigValue::String("test-value".to_string()),
environment: env,
version: 1,
metadata: ConfigMetadata {
created_at: Utc::now(),
created_by: "test".to_string(),
updated_at: Utc::now(),
updated_by: "test".to_string(),
tags: vec![],
description: None,
},
}
}
#[test]
fn test_cache_creation() {
let cache = L1Cache::new(100);
let stats = cache.stats();
assert_eq!(stats.size, 0);
assert_eq!(stats.max_size, 100);
}
#[test]
fn test_put_and_get() {
let cache = L1Cache::new(100);
let entry = create_test_entry("ns", "key1", Environment::Development);
cache.put(entry.clone()).unwrap();
let retrieved = cache.get("ns", "key1", "development").unwrap();
assert_eq!(retrieved.id, entry.id);
}
#[test]
fn test_cache_miss() {
let cache = L1Cache::new(100);
let result = cache.get("ns", "nonexistent", "development");
assert!(result.is_err());
}
#[test]
fn test_lru_eviction() {
let cache = L1Cache::new(3);
for i in 0..3 {
let entry = create_test_entry("ns", &format!("key{}", i), Environment::Development);
cache.put(entry).unwrap();
}
cache.get("ns", "key1", "development").unwrap();
let entry = create_test_entry("ns", "key3", Environment::Development);
cache.put(entry).unwrap();
assert!(cache.get("ns", "key0", "development").is_err());
assert!(cache.get("ns", "key1", "development").is_ok());
assert!(cache.get("ns", "key2", "development").is_ok());
assert!(cache.get("ns", "key3", "development").is_ok());
}
#[test]
fn test_invalidate() {
let cache = L1Cache::new(100);
let entry = create_test_entry("ns", "key1", Environment::Development);
cache.put(entry).unwrap();
assert!(cache.get("ns", "key1", "development").is_ok());
cache.invalidate("ns", "key1", "development");
assert!(cache.get("ns", "key1", "development").is_err());
}
#[test]
fn test_clear() {
let cache = L1Cache::new(100);
for i in 0..10 {
let entry = create_test_entry("ns", &format!("key{}", i), Environment::Development);
cache.put(entry).unwrap();
}
assert_eq!(cache.stats().size, 10);
cache.clear();
assert_eq!(cache.stats().size, 0);
}
#[test]
fn test_cache_stats() {
let cache = L1Cache::new(100);
let entry = create_test_entry("ns", "key1", Environment::Development);
cache.put(entry).unwrap();
cache.get("ns", "key1", "development").unwrap(); cache.get("ns", "key1", "development").unwrap(); let _ = cache.get("ns", "nonexistent", "development");
let stats = cache.stats();
assert_eq!(stats.hit_count, 2);
assert_eq!(stats.miss_count, 1);
assert!((stats.hit_rate - 0.666).abs() < 0.01);
}
}