fact_tools/
cache.rs

1//! High-performance caching implementation for FACT
2
3use ahash::AHashMap;
4use parking_lot::RwLock;
5use serde::{Deserialize, Serialize};
6use std::sync::Arc;
7use std::time::Instant;
8
9/// Statistics for cache performance
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct CacheStats {
12    pub entries: usize,
13    pub size_bytes: usize,
14    pub hits: u64,
15    pub misses: u64,
16    pub evictions: u64,
17    pub hit_rate: f64,
18}
19
20/// Entry in the cache
21#[derive(Debug, Clone)]
22struct CacheEntry {
23    value: serde_json::Value,
24    size: usize,
25    created_at: Instant,
26    last_accessed: Instant,
27    access_count: u64,
28}
29
30/// High-performance cache with LRU eviction
31pub struct Cache {
32    entries: AHashMap<String, CacheEntry>,
33    max_size: usize,
34    current_size: usize,
35    hits: u64,
36    misses: u64,
37    evictions: u64,
38}
39
40impl Cache {
41    /// Create a new cache with default size (100MB)
42    pub fn new() -> Self {
43        Self::with_capacity(100 * 1024 * 1024)
44    }
45    
46    /// Create a new cache with specified capacity
47    pub fn with_capacity(max_size: usize) -> Self {
48        Self {
49            entries: AHashMap::new(),
50            max_size,
51            current_size: 0,
52            hits: 0,
53            misses: 0,
54            evictions: 0,
55        }
56    }
57    
58    /// Get a value from the cache
59    pub fn get(&mut self, key: &str) -> Option<serde_json::Value> {
60        if let Some(entry) = self.entries.get_mut(key) {
61            entry.last_accessed = Instant::now();
62            entry.access_count += 1;
63            self.hits += 1;
64            Some(entry.value.clone())
65        } else {
66            self.misses += 1;
67            None
68        }
69    }
70    
71    /// Put a value in the cache
72    pub fn put(&mut self, key: String, value: serde_json::Value) {
73        let size = estimate_size(&value);
74        
75        // Evict entries if needed
76        while self.current_size + size > self.max_size && !self.entries.is_empty() {
77            self.evict_lru();
78        }
79        
80        let entry = CacheEntry {
81            value,
82            size,
83            created_at: Instant::now(),
84            last_accessed: Instant::now(),
85            access_count: 1,
86        };
87        
88        // Remove old entry if exists
89        if let Some(old_entry) = self.entries.remove(&key) {
90            self.current_size -= old_entry.size;
91        }
92        
93        self.current_size += size;
94        self.entries.insert(key, entry);
95    }
96    
97    /// Clear all entries from the cache
98    pub fn clear(&mut self) {
99        self.entries.clear();
100        self.current_size = 0;
101        self.hits = 0;
102        self.misses = 0;
103        self.evictions = 0;
104    }
105    
106    /// Get cache statistics
107    pub fn stats(&self) -> CacheStats {
108        let total = self.hits + self.misses;
109        let hit_rate = if total > 0 {
110            self.hits as f64 / total as f64
111        } else {
112            0.0
113        };
114        
115        CacheStats {
116            entries: self.entries.len(),
117            size_bytes: self.current_size,
118            hits: self.hits,
119            misses: self.misses,
120            evictions: self.evictions,
121            hit_rate,
122        }
123    }
124    
125    /// Evict the least recently used entry
126    fn evict_lru(&mut self) {
127        if let Some((key, _)) = self
128            .entries
129            .iter()
130            .min_by_key(|(_, entry)| entry.last_accessed)
131            .map(|(k, v)| (k.clone(), v.clone()))
132        {
133            if let Some(entry) = self.entries.remove(&key) {
134                self.current_size -= entry.size;
135                self.evictions += 1;
136            }
137        }
138    }
139}
140
141impl Default for Cache {
142    fn default() -> Self {
143        Self::new()
144    }
145}
146
147/// Estimate the memory size of a JSON value
148fn estimate_size(value: &serde_json::Value) -> usize {
149    match value {
150        serde_json::Value::Null => 8,
151        serde_json::Value::Bool(_) => 8,
152        serde_json::Value::Number(_) => 16,
153        serde_json::Value::String(s) => 24 + s.len() * 2,
154        serde_json::Value::Array(arr) => {
155            24 + arr.iter().map(estimate_size).sum::<usize>()
156        }
157        serde_json::Value::Object(map) => {
158            24 + map
159                .iter()
160                .map(|(k, v)| 24 + k.len() * 2 + estimate_size(v))
161                .sum::<usize>()
162        }
163    }
164}
165
166/// Thread-safe cache wrapper
167pub struct ThreadSafeCache {
168    inner: Arc<RwLock<Cache>>,
169}
170
171impl ThreadSafeCache {
172    pub fn new() -> Self {
173        Self {
174            inner: Arc::new(RwLock::new(Cache::new())),
175        }
176    }
177    
178    pub fn get(&self, key: &str) -> Option<serde_json::Value> {
179        self.inner.write().get(key)
180    }
181    
182    pub fn put(&self, key: String, value: serde_json::Value) {
183        self.inner.write().put(key, value);
184    }
185    
186    pub fn stats(&self) -> CacheStats {
187        self.inner.read().stats()
188    }
189    
190    pub fn clear(&self) {
191        self.inner.write().clear();
192    }
193}
194
195impl Default for ThreadSafeCache {
196    fn default() -> Self {
197        Self::new()
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    
205    #[test]
206    fn test_cache_basic_operations() {
207        let mut cache = Cache::with_capacity(1024);
208        
209        // Test put and get
210        cache.put("key1".to_string(), serde_json::json!({"test": "value"}));
211        assert!(cache.get("key1").is_some());
212        assert!(cache.get("key2").is_none());
213        
214        // Check stats
215        let stats = cache.stats();
216        assert_eq!(stats.entries, 1);
217        assert_eq!(stats.hits, 1);
218        assert_eq!(stats.misses, 1);
219    }
220    
221    #[test]
222    fn test_cache_eviction() {
223        let mut cache = Cache::with_capacity(100); // Very small cache
224        
225        // Fill cache with entries
226        for i in 0..10 {
227            cache.put(
228                format!("key{}", i),
229                serde_json::json!({"data": format!("value{}", i)}),
230            );
231        }
232        
233        // Should have evicted some entries
234        assert!(cache.entries.len() < 10);
235        assert!(cache.evictions > 0);
236    }
237}