chie_core/
cache.rs

1//! Advanced caching utilities with TTL and memory management.
2//!
3//! This module provides caching structures with time-to-live (TTL) support,
4//! automatic eviction, and memory management for efficient data caching.
5//!
6//! # Examples
7//!
8//! ```
9//! use chie_core::cache::TtlCache;
10//! use std::time::Duration;
11//!
12//! let mut cache = TtlCache::new(100, Duration::from_secs(60));
13//!
14//! // Insert with TTL
15//! cache.insert("key1".to_string(), "value1".to_string());
16//!
17//! // Retrieve (returns cloned value)
18//! assert_eq!(cache.get(&"key1".to_string()), Some("value1".to_string()));
19//!
20//! // Entry expires after TTL
21//! std::thread::sleep(Duration::from_millis(100));
22//! // Still valid within TTL
23//! assert!(cache.get(&"key1".to_string()).is_some());
24//! ```
25
26use std::collections::HashMap;
27use std::hash::Hash;
28use std::time::{Duration, Instant};
29
30/// Cache entry with expiration time.
31#[derive(Debug, Clone)]
32struct CacheEntry<V> {
33    value: V,
34    inserted_at: Instant,
35    last_accessed: Instant,
36    access_count: u64,
37}
38
39impl<V> CacheEntry<V> {
40    #[inline]
41    fn new(value: V) -> Self {
42        let now = Instant::now();
43        Self {
44            value,
45            inserted_at: now,
46            last_accessed: now,
47            access_count: 0,
48        }
49    }
50
51    #[inline]
52    fn is_expired(&self, ttl: Duration) -> bool {
53        self.inserted_at.elapsed() > ttl
54    }
55
56    #[inline]
57    fn access(&mut self) {
58        self.last_accessed = Instant::now();
59        self.access_count += 1;
60    }
61}
62
63/// Time-to-live cache with automatic expiration.
64pub struct TtlCache<K, V> {
65    entries: HashMap<K, CacheEntry<V>>,
66    max_capacity: usize,
67    ttl: Duration,
68    hits: u64,
69    misses: u64,
70}
71
72impl<K: Eq + Hash + Clone, V: Clone> TtlCache<K, V> {
73    /// Create a new TTL cache.
74    ///
75    /// # Arguments
76    /// * `max_capacity` - Maximum number of entries
77    /// * `ttl` - Time-to-live for entries
78    #[must_use]
79    #[inline]
80    pub fn new(max_capacity: usize, ttl: Duration) -> Self {
81        Self {
82            entries: HashMap::with_capacity(max_capacity),
83            max_capacity,
84            ttl,
85            hits: 0,
86            misses: 0,
87        }
88    }
89
90    /// Insert a value into the cache.
91    #[inline]
92    pub fn insert(&mut self, key: K, value: V) {
93        // Evict if at capacity
94        if self.entries.len() >= self.max_capacity && !self.entries.contains_key(&key) {
95            self.evict_one();
96        }
97
98        self.entries.insert(key, CacheEntry::new(value));
99    }
100
101    /// Get a value from the cache (returns cloned value).
102    #[inline]
103    pub fn get(&mut self, key: &K) -> Option<V> {
104        // Clean expired entries periodically
105        if self.entries.len() > 10 && self.hits % 100 == 0 {
106            self.cleanup_expired();
107        }
108
109        // Check if entry exists and is not expired
110        let is_expired = match self.entries.get(key) {
111            Some(entry) => entry.is_expired(self.ttl),
112            None => {
113                self.misses += 1;
114                return None;
115            }
116        };
117
118        if is_expired {
119            // Entry expired
120            self.entries.remove(key);
121            self.misses += 1;
122            None
123        } else {
124            // Update access time and return cloned value
125            if let Some(entry) = self.entries.get_mut(key) {
126                entry.access();
127                self.hits += 1;
128                Some(entry.value.clone())
129            } else {
130                self.misses += 1;
131                None
132            }
133        }
134    }
135
136    /// Check if a key exists and is not expired.
137    #[must_use]
138    #[inline]
139    pub fn contains_key(&mut self, key: &K) -> bool {
140        self.get(key).is_some()
141    }
142
143    /// Remove a key from the cache.
144    #[inline]
145    pub fn remove(&mut self, key: &K) -> Option<V> {
146        self.entries.remove(key).map(|e| e.value)
147    }
148
149    /// Clear all entries.
150    #[inline]
151    pub fn clear(&mut self) {
152        self.entries.clear();
153        self.hits = 0;
154        self.misses = 0;
155    }
156
157    /// Get current cache size.
158    #[must_use]
159    #[inline]
160    pub fn len(&self) -> usize {
161        self.entries.len()
162    }
163
164    /// Check if cache is empty.
165    #[must_use]
166    #[inline]
167    pub fn is_empty(&self) -> bool {
168        self.entries.is_empty()
169    }
170
171    /// Get cache hit rate.
172    #[must_use]
173    #[inline]
174    pub fn hit_rate(&self) -> f64 {
175        let total = self.hits + self.misses;
176        if total == 0 {
177            0.0
178        } else {
179            self.hits as f64 / total as f64
180        }
181    }
182
183    /// Get statistics.
184    #[must_use]
185    #[inline]
186    pub fn stats(&self) -> CacheStats {
187        CacheStats {
188            size: self.entries.len(),
189            capacity: self.max_capacity,
190            hits: self.hits,
191            misses: self.misses,
192            hit_rate: self.hit_rate(),
193        }
194    }
195
196    /// Remove expired entries.
197    fn cleanup_expired(&mut self) {
198        self.entries.retain(|_, entry| !entry.is_expired(self.ttl));
199    }
200
201    /// Evict one entry (least recently used).
202    fn evict_one(&mut self) {
203        if let Some(lru_key) = self.find_lru_key() {
204            self.entries.remove(&lru_key);
205        }
206    }
207
208    /// Find the least recently used key.
209    fn find_lru_key(&self) -> Option<K> {
210        self.entries
211            .iter()
212            .min_by_key(|(_, entry)| entry.last_accessed)
213            .map(|(k, _)| k.clone())
214    }
215}
216
217/// Cache statistics.
218#[derive(Debug, Clone)]
219pub struct CacheStats {
220    /// Current number of entries.
221    pub size: usize,
222    /// Maximum capacity.
223    pub capacity: usize,
224    /// Total cache hits.
225    pub hits: u64,
226    /// Total cache misses.
227    pub misses: u64,
228    /// Hit rate (0.0 to 1.0).
229    pub hit_rate: f64,
230}
231
232/// Two-level cache with L1 (fast, small) and L2 (larger).
233pub struct TieredCache<K, V> {
234    l1: TtlCache<K, V>,
235    l2: TtlCache<K, V>,
236}
237
238impl<K: Eq + Hash + Clone, V: Clone> TieredCache<K, V> {
239    /// Create a new tiered cache.
240    ///
241    /// # Arguments
242    /// * `l1_capacity` - L1 cache capacity (fast)
243    /// * `l2_capacity` - L2 cache capacity (slower but larger)
244    /// * `ttl` - Time-to-live for entries
245    #[must_use]
246    #[inline]
247    pub fn new(l1_capacity: usize, l2_capacity: usize, ttl: Duration) -> Self {
248        Self {
249            l1: TtlCache::new(l1_capacity, ttl),
250            l2: TtlCache::new(l2_capacity, ttl),
251        }
252    }
253
254    /// Insert a value into the cache (goes to L1).
255    #[inline]
256    pub fn insert(&mut self, key: K, value: V) {
257        self.l1.insert(key, value);
258    }
259
260    /// Get a value from the cache (checks L1, then L2).
261    #[inline]
262    pub fn get(&mut self, key: &K) -> Option<V> {
263        // Try L1 first
264        if let Some(value) = self.l1.get(key) {
265            return Some(value.clone());
266        }
267
268        // Try L2 and promote to L1 if found
269        if let Some(value) = self.l2.get(key) {
270            let value_clone = value.clone();
271            self.l1.insert(key.clone(), value_clone.clone());
272            return Some(value_clone);
273        }
274
275        None
276    }
277
278    /// Clear both cache levels.
279    pub fn clear(&mut self) {
280        self.l1.clear();
281        self.l2.clear();
282    }
283
284    /// Get combined statistics.
285    #[must_use]
286    #[inline]
287    pub fn stats(&self) -> (CacheStats, CacheStats) {
288        (self.l1.stats(), self.l2.stats())
289    }
290}
291
292/// Cache with size-based eviction (for byte-counted values).
293pub struct SizedCache<K> {
294    entries: HashMap<K, (Vec<u8>, Instant)>,
295    current_size: usize,
296    max_size: usize,
297    ttl: Duration,
298}
299
300impl<K: Eq + Hash + Clone> SizedCache<K> {
301    /// Create a new size-based cache.
302    #[must_use]
303    #[inline]
304    pub fn new(max_size: usize, ttl: Duration) -> Self {
305        Self {
306            entries: HashMap::new(),
307            current_size: 0,
308            max_size,
309            ttl,
310        }
311    }
312
313    /// Insert data into the cache.
314    #[inline]
315    pub fn insert(&mut self, key: K, data: Vec<u8>) {
316        let size = data.len();
317
318        // Evict until we have space
319        while self.current_size + size > self.max_size && !self.entries.is_empty() {
320            self.evict_oldest();
321        }
322
323        // Don't insert if single item is larger than max size
324        if size > self.max_size {
325            return;
326        }
327
328        // Remove old entry if updating
329        if let Some((old_data, _)) = self.entries.remove(&key) {
330            self.current_size -= old_data.len();
331        }
332
333        self.entries.insert(key, (data, Instant::now()));
334        self.current_size += size;
335    }
336
337    /// Get data from the cache.
338    #[inline]
339    pub fn get(&mut self, key: &K) -> Option<Vec<u8>> {
340        self.cleanup_expired();
341
342        // Check if key exists and is not expired
343        let is_expired = match self.entries.get(key) {
344            Some((_, inserted_at)) => inserted_at.elapsed() >= self.ttl,
345            None => return None,
346        };
347
348        if is_expired {
349            // Remove expired entry
350            if let Some((data, _)) = self.entries.remove(key) {
351                self.current_size -= data.len();
352            }
353            None
354        } else {
355            // Return cloned data
356            self.entries.get(key).map(|(data, _)| data.clone())
357        }
358    }
359
360    /// Get current size in bytes.
361    #[must_use]
362    #[inline]
363    pub fn current_size(&self) -> usize {
364        self.current_size
365    }
366
367    /// Clear the cache.
368    #[inline]
369    pub fn clear(&mut self) {
370        self.entries.clear();
371        self.current_size = 0;
372    }
373
374    fn cleanup_expired(&mut self) {
375        let ttl = self.ttl;
376        let mut removed_size = 0;
377
378        self.entries.retain(|_, (data, inserted_at)| {
379            if inserted_at.elapsed() >= ttl {
380                removed_size += data.len();
381                false
382            } else {
383                true
384            }
385        });
386
387        self.current_size -= removed_size;
388    }
389
390    fn evict_oldest(&mut self) {
391        if let Some(oldest_key) = self.find_oldest_key() {
392            if let Some((data, _)) = self.entries.remove(&oldest_key) {
393                self.current_size -= data.len();
394            }
395        }
396    }
397
398    fn find_oldest_key(&self) -> Option<K> {
399        self.entries
400            .iter()
401            .min_by_key(|(_, (_, inserted_at))| inserted_at)
402            .map(|(k, _)| k.clone())
403    }
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409    use std::thread;
410
411    #[test]
412    fn test_ttl_cache_basic() {
413        let mut cache = TtlCache::new(10, Duration::from_secs(60));
414
415        cache.insert("key1".to_string(), "value1".to_string());
416        assert_eq!(cache.get(&"key1".to_string()), Some("value1".to_string()));
417        assert_eq!(cache.len(), 1);
418    }
419
420    #[test]
421    fn test_ttl_cache_expiration() {
422        let mut cache = TtlCache::new(10, Duration::from_millis(50));
423
424        cache.insert("key1".to_string(), "value1".to_string());
425        assert!(cache.get(&"key1".to_string()).is_some());
426
427        thread::sleep(Duration::from_millis(100));
428        assert!(cache.get(&"key1".to_string()).is_none());
429    }
430
431    #[test]
432    fn test_ttl_cache_eviction() {
433        let mut cache = TtlCache::new(3, Duration::from_secs(60));
434
435        cache.insert(1, "a");
436        cache.insert(2, "b");
437        cache.insert(3, "c");
438
439        // Access key 1 to make it recently used
440        cache.get(&1);
441
442        thread::sleep(Duration::from_millis(10));
443
444        // Insert 4th item, should evict LRU (key 2)
445        cache.insert(4, "d");
446
447        assert!(cache.get(&1).is_some());
448        assert!(cache.get(&2).is_none()); // Evicted
449        assert!(cache.get(&3).is_some());
450        assert!(cache.get(&4).is_some());
451    }
452
453    #[test]
454    fn test_ttl_cache_stats() {
455        let mut cache = TtlCache::new(10, Duration::from_secs(60));
456
457        cache.insert("key1".to_string(), "value1".to_string());
458
459        cache.get(&"key1".to_string()); // Hit
460        cache.get(&"key2".to_string()); // Miss
461
462        let stats = cache.stats();
463        assert_eq!(stats.hits, 1);
464        assert_eq!(stats.misses, 1);
465        assert_eq!(stats.hit_rate, 0.5);
466    }
467
468    #[test]
469    fn test_tiered_cache() {
470        let mut cache = TieredCache::new(2, 5, Duration::from_secs(60));
471
472        cache.insert("key1".to_string(), "value1".to_string());
473        cache.insert("key2".to_string(), "value2".to_string());
474
475        assert_eq!(cache.get(&"key1".to_string()), Some("value1".to_string()));
476    }
477
478    #[test]
479    fn test_sized_cache() {
480        let mut cache = SizedCache::new(100, Duration::from_secs(60));
481
482        cache.insert("key1", vec![1u8; 40]);
483        cache.insert("key2", vec![2u8; 40]);
484
485        assert_eq!(cache.current_size(), 80);
486
487        // This should evict key1
488        cache.insert("key3", vec![3u8; 40]);
489
490        assert!(cache.get(&"key1").is_none());
491        assert_eq!(cache.get(&"key2"), Some(vec![2u8; 40]));
492        assert_eq!(cache.get(&"key3"), Some(vec![3u8; 40]));
493    }
494
495    #[test]
496    fn test_sized_cache_expiration() {
497        let mut cache = SizedCache::new(100, Duration::from_millis(50));
498
499        cache.insert("key1", vec![1u8; 40]);
500        assert_eq!(cache.get(&"key1"), Some(vec![1u8; 40]));
501
502        thread::sleep(Duration::from_millis(100));
503        assert!(cache.get(&"key1").is_none());
504        assert_eq!(cache.current_size(), 0);
505    }
506}