use crate::{CacheError, Result};
use llm_config_core::ConfigEntry;
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::{BufReader, BufWriter, Write};
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
pub struct L2Cache {
cache_dir: PathBuf,
index: Arc<RwLock<HashMap<String, PathBuf>>>,
}
impl L2Cache {
pub fn new(cache_dir: impl AsRef<Path>) -> Result<Self> {
let cache_dir = cache_dir.as_ref();
fs::create_dir_all(cache_dir)?;
let cache = Self {
cache_dir: cache_dir.to_path_buf(),
index: Arc::new(RwLock::new(HashMap::new())),
};
cache.rebuild_index()?;
Ok(cache)
}
fn cache_key(namespace: &str, key: &str, env: &str) -> String {
format!("{}:{}:{}", namespace, key, env)
}
fn cache_file_path(&self, cache_key: &str) -> PathBuf {
let encoded = hex::encode(cache_key.as_bytes());
self.cache_dir.join(format!("{}.cache", encoded))
}
fn rebuild_index(&self) -> Result<()> {
let mut index = self.index.write().unwrap();
index.clear();
if !self.cache_dir.exists() {
return Ok(());
}
for entry in fs::read_dir(&self.cache_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("cache") {
if let Ok(file) = File::open(&path) {
let reader = BufReader::new(file);
if let Ok(cached_entry) = serde_json::from_reader::<_, ConfigEntry>(reader) {
let cache_key = Self::cache_key(
&cached_entry.namespace,
&cached_entry.key,
&cached_entry.environment.to_string(),
);
index.insert(cache_key, path);
}
}
}
}
Ok(())
}
pub fn get(&self, namespace: &str, key: &str, env: &str) -> Result<ConfigEntry> {
let cache_key = Self::cache_key(namespace, key, env);
let index = self.index.read().unwrap();
if let Some(path) = index.get(&cache_key) {
let file = File::open(path)?;
let reader = BufReader::new(file);
let entry = serde_json::from_reader(reader)
.map_err(|e| CacheError::Serialization(e.to_string()))?;
Ok(entry)
} else {
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 path = self.cache_file_path(&cache_key);
let temp_path = path.with_extension("tmp");
{
let file = File::create(&temp_path)?;
let mut writer = BufWriter::new(file);
serde_json::to_writer(&mut writer, entry)
.map_err(|e| CacheError::Serialization(e.to_string()))?;
writer.flush()?;
}
fs::rename(&temp_path, &path)?;
let mut index = self.index.write().unwrap();
index.insert(cache_key, path);
Ok(())
}
pub fn invalidate(&self, namespace: &str, key: &str, env: &str) -> Result<()> {
let cache_key = Self::cache_key(namespace, key, env);
let mut index = self.index.write().unwrap();
if let Some(path) = index.remove(&cache_key) {
let _ = fs::remove_file(path); }
Ok(())
}
pub fn clear(&self) -> Result<()> {
let mut index = self.index.write().unwrap();
index.clear();
if self.cache_dir.exists() {
for entry in fs::read_dir(&self.cache_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("cache") {
let _ = fs::remove_file(path);
}
}
}
Ok(())
}
pub fn size(&self) -> usize {
self.index.read().unwrap().len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use llm_config_core::{ConfigMetadata, ConfigValue, Environment};
use tempfile::TempDir;
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: chrono::Utc::now(),
created_by: "test".to_string(),
updated_at: chrono::Utc::now(),
updated_by: "test".to_string(),
tags: vec![],
description: None,
},
}
}
#[test]
fn test_l2_creation() {
let temp_dir = TempDir::new().unwrap();
let cache = L2Cache::new(temp_dir.path()).unwrap();
assert_eq!(cache.size(), 0);
}
#[test]
fn test_put_and_get() {
let temp_dir = TempDir::new().unwrap();
let cache = L2Cache::new(temp_dir.path()).unwrap();
let entry = create_test_entry("ns", "key1", Environment::Development);
cache.put(&entry).unwrap();
let retrieved = cache.get("ns", "key1", "development").unwrap();
assert_eq!(retrieved.id, entry.id);
}
#[test]
fn test_cache_miss() {
let temp_dir = TempDir::new().unwrap();
let cache = L2Cache::new(temp_dir.path()).unwrap();
let result = cache.get("ns", "nonexistent", "development");
assert!(result.is_err());
}
#[test]
fn test_persistence() {
let temp_dir = TempDir::new().unwrap();
let entry = create_test_entry("ns", "key1", Environment::Development);
{
let cache = L2Cache::new(temp_dir.path()).unwrap();
cache.put(&entry).unwrap();
}
{
let cache = L2Cache::new(temp_dir.path()).unwrap();
let retrieved = cache.get("ns", "key1", "development").unwrap();
assert_eq!(retrieved.id, entry.id);
}
}
#[test]
fn test_invalidate() {
let temp_dir = TempDir::new().unwrap();
let cache = L2Cache::new(temp_dir.path()).unwrap();
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").unwrap();
assert!(cache.get("ns", "key1", "development").is_err());
}
#[test]
fn test_clear() {
let temp_dir = TempDir::new().unwrap();
let cache = L2Cache::new(temp_dir.path()).unwrap();
for i in 0..10 {
let entry = create_test_entry("ns", &format!("key{}", i), Environment::Development);
cache.put(&entry).unwrap();
}
assert_eq!(cache.size(), 10);
cache.clear().unwrap();
assert_eq!(cache.size(), 0);
}
}