use std::num::NonZeroUsize;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use chrono::{DateTime, Utc};
use lru::LruCache;
use sha2::{Digest, Sha256};
use crate::types::requests::EvaluationType;
use crate::types::responses::EvaluationResult;
#[derive(Debug, Clone)]
pub struct CachedResult {
pub result: EvaluationResult,
pub cached_at: DateTime<Utc>,
}
impl CachedResult {
pub fn new(result: EvaluationResult) -> Self {
Self {
result,
cached_at: Utc::now(),
}
}
pub fn is_expired(&self, ttl: Duration) -> bool {
let elapsed = Utc::now()
.signed_duration_since(self.cached_at)
.to_std()
.unwrap_or(Duration::MAX);
elapsed > ttl
}
}
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
pub size: usize,
pub capacity: usize,
pub hits: u64,
pub misses: u64,
}
impl CacheStats {
pub fn hit_rate(&self) -> f64 {
let total = self.hits + self.misses;
if total == 0 {
0.0
} else {
self.hits as f64 / total as f64
}
}
}
pub struct EvaluationCache {
cache: LruCache<String, CachedResult>,
ttl: Duration,
hits: AtomicU64,
misses: AtomicU64,
}
impl EvaluationCache {
pub fn new(capacity: usize, ttl: Duration) -> Self {
let cap = NonZeroUsize::new(capacity).unwrap_or(NonZeroUsize::new(100).unwrap());
Self {
cache: LruCache::new(cap),
ttl,
hits: AtomicU64::new(0),
misses: AtomicU64::new(0),
}
}
pub fn default_config() -> Self {
Self::new(100, Duration::from_secs(300)) }
pub fn cache_key(code: &str, language: &str, eval_type: &EvaluationType) -> String {
let normalized = Self::normalize_code(code);
let eval_type_str = match eval_type {
EvaluationType::Plan => "plan",
EvaluationType::Code => "code",
EvaluationType::Tests => "tests",
EvaluationType::FinalCheck => "final",
};
let mut hasher = Sha256::new();
hasher.update(normalized.as_bytes());
hasher.update(language.as_bytes());
hasher.update(eval_type_str.as_bytes());
hex::encode(hasher.finalize())
}
fn normalize_code(code: &str) -> String {
code.lines()
.map(|l| l.trim())
.filter(|l| !l.is_empty())
.collect::<Vec<_>>()
.join("\n")
}
pub fn get(&mut self, key: &str) -> Option<&EvaluationResult> {
let is_expired = self.cache.peek(key).map(|c| c.is_expired(self.ttl));
match is_expired {
Some(true) => {
self.cache.pop(key);
self.misses.fetch_add(1, Ordering::Relaxed);
None
}
Some(false) => {
self.hits.fetch_add(1, Ordering::Relaxed);
self.cache.get(key).map(|c| &c.result)
}
None => {
self.misses.fetch_add(1, Ordering::Relaxed);
None
}
}
}
pub fn get_by_code(
&mut self,
code: &str,
language: &str,
eval_type: &EvaluationType,
) -> Option<&EvaluationResult> {
let key = Self::cache_key(code, language, eval_type);
self.get(&key)
}
pub fn insert(&mut self, key: String, result: EvaluationResult) {
self.cache.put(key, CachedResult::new(result));
}
pub fn insert_by_code(
&mut self,
code: &str,
language: &str,
eval_type: &EvaluationType,
result: EvaluationResult,
) {
let key = Self::cache_key(code, language, eval_type);
self.insert(key, result);
}
pub fn invalidate(&mut self, key: &str) {
self.cache.pop(key);
}
pub fn clear(&mut self) {
self.cache.clear();
}
pub fn stats(&self) -> CacheStats {
CacheStats {
size: self.cache.len(),
capacity: self.cache.cap().get(),
hits: self.hits.load(Ordering::Relaxed),
misses: self.misses.load(Ordering::Relaxed),
}
}
pub fn cleanup_expired(&mut self) {
let expired_keys: Vec<String> = self
.cache
.iter()
.filter(|(_, v)| v.is_expired(self.ttl))
.map(|(k, _)| k.clone())
.collect();
for key in expired_keys {
self.cache.pop(&key);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::responses::Decision;
fn create_test_result() -> EvaluationResult {
EvaluationResult {
request_id: "test-123".to_string(),
decision: Decision::Pass,
score: 85,
consensus_achieved: true,
votes: std::collections::HashMap::new(),
findings: vec![],
feedback: "Test feedback".to_string(),
timestamp: Utc::now(),
}
}
#[test]
fn test_cache_key_generation() {
let key1 = EvaluationCache::cache_key("fn main() {}", "rust", &EvaluationType::Code);
let key2 = EvaluationCache::cache_key("fn main() {}", "rust", &EvaluationType::Code);
let key3 = EvaluationCache::cache_key("fn main() {}", "python", &EvaluationType::Code);
assert_eq!(key1, key2);
assert_ne!(key1, key3);
}
#[test]
fn test_cache_key_normalization() {
let key1 = EvaluationCache::cache_key("fn main() {}", "rust", &EvaluationType::Code);
let key2 = EvaluationCache::cache_key(" fn main() {} ", "rust", &EvaluationType::Code);
assert_eq!(key1, key2);
}
#[test]
fn test_cache_hit() {
let mut cache = EvaluationCache::new(10, Duration::from_secs(60));
let result = create_test_result();
cache.insert("test-key".to_string(), result.clone());
let cached = cache.get("test-key");
assert!(cached.is_some());
assert_eq!(cached.unwrap().request_id, "test-123");
let stats = cache.stats();
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 0);
}
#[test]
fn test_cache_miss() {
let mut cache = EvaluationCache::new(10, Duration::from_secs(60));
let cached = cache.get("nonexistent");
assert!(cached.is_none());
let stats = cache.stats();
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 1);
}
#[test]
fn test_cache_expiration() {
let mut cache = EvaluationCache::new(10, Duration::from_secs(0));
let result = create_test_result();
cache.insert("test-key".to_string(), result);
let cached = cache.get("test-key");
assert!(cached.is_none());
}
#[test]
fn test_cache_lru_eviction() {
let mut cache = EvaluationCache::new(2, Duration::from_secs(60));
let result = create_test_result();
cache.insert("key1".to_string(), result.clone());
cache.insert("key2".to_string(), result.clone());
cache.insert("key3".to_string(), result);
assert!(cache.get("key1").is_none()); assert!(cache.get("key2").is_some());
assert!(cache.get("key3").is_some());
}
#[test]
fn test_cache_invalidate() {
let mut cache = EvaluationCache::new(10, Duration::from_secs(60));
let result = create_test_result();
cache.insert("test-key".to_string(), result);
assert!(cache.get("test-key").is_some());
cache.invalidate("test-key");
assert!(cache.get("test-key").is_none());
}
#[test]
fn test_cache_clear() {
let mut cache = EvaluationCache::new(10, Duration::from_secs(60));
let result = create_test_result();
cache.insert("key1".to_string(), result.clone());
cache.insert("key2".to_string(), result);
cache.clear();
assert!(cache.get("key1").is_none());
assert!(cache.get("key2").is_none());
assert_eq!(cache.stats().size, 0);
}
#[test]
fn test_cache_stats() {
let mut cache = EvaluationCache::new(10, Duration::from_secs(60));
let result = create_test_result();
cache.insert("key1".to_string(), result);
cache.get("key1"); cache.get("key2"); cache.get("key1");
let stats = cache.stats();
assert_eq!(stats.size, 1);
assert_eq!(stats.capacity, 10);
assert_eq!(stats.hits, 2);
assert_eq!(stats.misses, 1);
assert!((stats.hit_rate() - 0.666).abs() < 0.01);
}
#[test]
fn test_insert_by_code() {
let mut cache = EvaluationCache::new(10, Duration::from_secs(60));
let result = create_test_result();
cache.insert_by_code("fn main() {}", "rust", &EvaluationType::Code, result);
let cached = cache.get_by_code("fn main() {}", "rust", &EvaluationType::Code);
assert!(cached.is_some());
}
#[test]
fn test_cached_result_is_expired() {
let result = create_test_result();
let cached = CachedResult::new(result);
assert!(!cached.is_expired(Duration::from_secs(3600)));
assert!(cached.is_expired(Duration::from_secs(0)));
}
}