Skip to main content

threatflux_cache/
cache.rs

1//! Core cache implementation
2
3use async_trait::async_trait;
4use std::collections::HashMap;
5use std::hash::Hash;
6use std::sync::Arc;
7use tokio::sync::{Mutex, RwLock};
8
9use crate::{
10    CacheConfig, CacheEntry, CacheError, EntryMetadata, Result, StorageBackend,
11    eviction::{EvictionContext, EvictionStrategy},
12    search::Searchable,
13};
14
15/// Type alias for cache entries storage
16type CacheStorage<K, V, M> = Arc<RwLock<HashMap<K, Vec<CacheEntry<K, V, M>>>>>;
17
18/// Type alias for eviction strategy
19type EvictionStrategyBox<K, V, M> = Box<dyn EvictionStrategy<K, V, M>>;
20
21/// Type alias for cache entry
22type Entry<K, V, M> = CacheEntry<K, V, M>;
23
24macro_rules! impl_cache_common {
25    ($(#[$meta:meta])? $trait:path, $($body:tt)*) => {
26        $(#[$meta])?
27        impl<K, V, M, B> $trait for Cache<K, V, M, B>
28        where
29            K: CacheKey,
30            V: CacheValue,
31            M: EntryMetadata + Default,
32            B: StorageBackend<Key = K, Value = V, Metadata = M>,
33        {
34            $($body)*
35        }
36    };
37}
38
39/// Common bounds for cache keys
40pub trait CacheKey: Hash + Eq + Clone + Send + Sync + 'static {}
41impl<T> CacheKey for T where T: Hash + Eq + Clone + Send + Sync + 'static {}
42
43/// Common bounds for cache values
44pub trait CacheValue: Clone + Send + Sync + 'static {}
45impl<T> CacheValue for T where T: Clone + Send + Sync + 'static {}
46
47/// Async cache trait defining the core cache operations
48#[async_trait]
49pub trait AsyncCache<K, V>: Send + Sync
50where
51    K: CacheKey,
52    V: CacheValue,
53{
54    /// Error type for cache operations
55    type Error;
56
57    /// Get a value from the cache
58    async fn get(&self, key: &K) -> std::result::Result<Option<V>, Self::Error>;
59
60    /// Put a value into the cache
61    async fn put(&self, key: K, value: V) -> std::result::Result<(), Self::Error>;
62
63    /// Remove a value from the cache
64    async fn remove(&self, key: &K) -> std::result::Result<Option<V>, Self::Error>;
65
66    /// Clear all entries from the cache
67    async fn clear(&self) -> std::result::Result<(), Self::Error>;
68
69    /// Check if the cache contains a key
70    async fn contains(&self, key: &K) -> std::result::Result<bool, Self::Error>;
71
72    /// Get the number of entries in the cache
73    async fn len(&self) -> std::result::Result<usize, Self::Error>;
74
75    /// Check if the cache is empty
76    async fn is_empty(&self) -> std::result::Result<bool, Self::Error> {
77        Ok(self.len().await? == 0)
78    }
79}
80
81/// Main cache implementation
82#[allow(clippy::type_complexity)]
83pub struct Cache<K, V, M = (), B = crate::backends::memory::MemoryBackend<K, V, M>>
84where
85    K: CacheKey,
86    V: CacheValue,
87    M: EntryMetadata + Default,
88    B: StorageBackend<Key = K, Value = V, Metadata = M>,
89{
90    entries: CacheStorage<K, V, M>,
91    config: CacheConfig,
92    backend: Arc<B>,
93    operation_lock: Arc<Mutex<()>>,
94    operation_count: Arc<RwLock<usize>>,
95    eviction_strategy: EvictionStrategyBox<K, V, M>,
96}
97
98impl<K, V, M, B> Cache<K, V, M, B>
99where
100    K: CacheKey,
101    V: CacheValue,
102    M: EntryMetadata + Default,
103    B: StorageBackend<Key = K, Value = V, Metadata = M>,
104{
105    /// Create a new cache with the given configuration and backend
106    pub async fn new(config: CacheConfig, backend: B) -> Result<Self> {
107        Self::validate_config(&config)?;
108        let eviction_strategy = crate::eviction::create_strategy(&config.eviction_policy);
109
110        let cache = Self {
111            entries: Arc::new(RwLock::new(HashMap::new())),
112            config,
113            backend: Arc::new(backend),
114            operation_lock: Arc::new(Mutex::new(())),
115            operation_count: Arc::new(RwLock::new(0)),
116            eviction_strategy,
117        };
118
119        // Load existing cache if configured
120        if cache.config.persistence.enabled && cache.config.persistence.load_on_startup {
121            cache.load_from_storage().await?;
122        }
123
124        Ok(cache)
125    }
126
127    fn validate_config(config: &CacheConfig) -> Result<()> {
128        if config.max_entries_per_key == 0 {
129            return Err(CacheError::InvalidConfiguration(
130                "max_entries_per_key must be greater than zero".to_string(),
131            ));
132        }
133        if config.max_total_entries == 0 {
134            return Err(CacheError::InvalidConfiguration(
135                "max_total_entries must be greater than zero".to_string(),
136            ));
137        }
138        if config.persistence.enabled && config.persistence.sync_interval == 0 {
139            return Err(CacheError::InvalidConfiguration(
140                "persistence.sync_interval must be greater than zero".to_string(),
141            ));
142        }
143        if let Some(ttl) = config.default_ttl {
144            chrono::Duration::from_std(ttl).map_err(|_| {
145                CacheError::InvalidConfiguration(
146                    "default_ttl exceeds the supported timestamp range".to_string(),
147                )
148            })?;
149        }
150        Ok(())
151    }
152
153    fn apply_default_ttl(&self, mut entry: Entry<K, V, M>) -> Result<Entry<K, V, M>> {
154        if entry.expiry.is_none()
155            && let Some(ttl) = self.config.default_ttl
156        {
157            let ttl = chrono::Duration::from_std(ttl).map_err(|_| {
158                CacheError::InvalidConfiguration(
159                    "default_ttl exceeds the supported timestamp range".to_string(),
160                )
161            })?;
162            entry = entry.with_ttl(ttl);
163        }
164        Ok(entry)
165    }
166
167    /// Create a new cache with default memory backend
168    pub async fn with_config(config: CacheConfig) -> Result<Self>
169    where
170        B: Default,
171    {
172        Self::new(config, B::default()).await
173    }
174
175    /// Add an entry to the cache
176    #[allow(clippy::type_complexity)]
177    pub async fn add_entry(&self, entry: Entry<K, V, M>) -> Result<()> {
178        let _operation = self.operation_lock.lock().await;
179        let entry = self.apply_default_ttl(entry)?;
180        {
181            let mut entries = self.entries.write().await;
182            self.insert_entry(&mut entries, entry).await?;
183        }
184
185        // Increment operation count and check if we need to sync
186        self.increment_and_maybe_sync().await?;
187
188        Ok(())
189    }
190
191    async fn insert_entry(
192        &self,
193        entries: &mut HashMap<K, Vec<CacheEntry<K, V, M>>>,
194        entry: Entry<K, V, M>,
195    ) -> Result<()> {
196        entries.retain(|_, key_entries| {
197            key_entries.retain(|entry| !entry.is_expired());
198            !key_entries.is_empty()
199        });
200
201        let total_entries = entries.values().try_fold(0usize, |total, key_entries| {
202            total
203                .checked_add(key_entries.len())
204                .ok_or_else(|| CacheError::CapacityExceeded {
205                    message: "cache entry count overflowed usize".to_string(),
206                })
207        })?;
208        let grows = entries
209            .get(&entry.key)
210            .is_none_or(|key_entries| key_entries.len() < self.config.max_entries_per_key);
211        if grows
212            && total_entries >= self.config.max_total_entries
213            && self.config.eviction_policy == crate::EvictionPolicy::None
214        {
215            return Err(CacheError::CapacityExceeded {
216                message: format!(
217                    "max_total_entries ({}) has been reached and eviction is disabled",
218                    self.config.max_total_entries
219                ),
220            });
221        }
222
223        let key_entries = entries.entry(entry.key.clone()).or_default();
224        key_entries.push(entry);
225        key_entries.sort_by_key(|entry| entry.timestamp);
226
227        // Limit entries per key while retaining the newest timestamps.
228        if key_entries.len() > self.config.max_entries_per_key {
229            let excess = key_entries.len() - self.config.max_entries_per_key;
230            key_entries.drain(..excess);
231        }
232
233        // Check if we need to evict
234        let total_entries = entries.values().try_fold(0usize, |total, key_entries| {
235            total
236                .checked_add(key_entries.len())
237                .ok_or_else(|| CacheError::CapacityExceeded {
238                    message: "cache entry count overflowed usize".to_string(),
239                })
240        })?;
241        if total_entries > self.config.max_total_entries {
242            let context = EvictionContext {
243                max_total_entries: self.config.max_total_entries,
244                current_total_entries: total_entries,
245            };
246            self.eviction_strategy.evict(entries, &context).await;
247        }
248        Ok(())
249    }
250
251    /// Get all entries for a key
252    pub async fn get_entries(&self, key: &K) -> Option<Vec<CacheEntry<K, V, M>>> {
253        let mut entries = self.entries.write().await;
254        let result = entries.get_mut(key).and_then(|key_entries| {
255            key_entries.retain(|entry| !entry.is_expired());
256            if key_entries.is_empty() {
257                None
258            } else {
259                for entry in key_entries.iter_mut() {
260                    entry.record_access();
261                }
262                Some(key_entries.clone())
263            }
264        });
265        if result.is_none() {
266            entries.remove(key);
267        }
268        result
269    }
270
271    /// Get the latest entry for a key
272    pub async fn get_latest(&self, key: &K) -> Option<CacheEntry<K, V, M>> {
273        let mut entries = self.entries.write().await;
274        let result = entries.get_mut(key).and_then(|key_entries| {
275            key_entries.retain(|entry| !entry.is_expired());
276            key_entries
277                .iter_mut()
278                .max_by_key(|entry| entry.timestamp)
279                .map(|entry| {
280                    entry.record_access();
281                    entry.clone()
282                })
283        });
284        if result.is_none() {
285            entries.remove(key);
286        }
287        result
288    }
289
290    /// Search entries based on a query
291    pub async fn search<Q>(&self, query: &Q) -> Vec<CacheEntry<K, V, M>>
292    where
293        CacheEntry<K, V, M>: Searchable<Query = Q>,
294    {
295        let entries = self.entries.read().await;
296        entries
297            .values()
298            .flat_map(|v| v.iter())
299            .filter(|entry| entry.matches(query))
300            .cloned()
301            .collect()
302    }
303
304    /// Aggregate statistics for a slice of cache entries
305    fn entry_vec_stats(entry_vec: &[CacheEntry<K, V, M>]) -> (usize, u64, usize) {
306        entry_vec
307            .iter()
308            .fold((0, 0, 0), |(count, access, expired), entry| {
309                (
310                    count.saturating_add(1),
311                    access.saturating_add(entry.access_count),
312                    expired.saturating_add(usize::from(entry.is_expired())),
313                )
314            })
315    }
316
317    /// Get cache statistics
318    pub async fn get_stats(&self) -> CacheStats {
319        let entries = self.entries.read().await;
320        let total_keys = entries.len();
321
322        let (total_entries, total_access_count, expired_count) =
323            entries
324                .values()
325                .fold((0usize, 0u64, 0usize), |acc, entry_vec| {
326                    let (e, a, exp) = Self::entry_vec_stats(entry_vec);
327                    (
328                        acc.0.saturating_add(e),
329                        acc.1.saturating_add(a),
330                        acc.2.saturating_add(exp),
331                    )
332                });
333
334        CacheStats {
335            total_entries,
336            total_keys,
337            total_access_count,
338            expired_count,
339        }
340    }
341
342    /// Save cache to storage backend
343    async fn save_to_storage(&self) -> Result<()> {
344        if !self.config.persistence.enabled {
345            return Ok(());
346        }
347
348        let snapshot = self.entries.read().await.clone();
349        self.backend.save(&snapshot).await
350    }
351
352    /// Persist the current cache state and wait for the backend to finish.
353    ///
354    /// This is a no-op when persistence is disabled. Call this before shutdown
355    /// when durability of recent operations is required.
356    pub async fn flush(&self) -> Result<()> {
357        let _operation = self.operation_lock.lock().await;
358        self.save_to_storage().await?;
359        *self.operation_count.write().await = 0;
360        Ok(())
361    }
362
363    /// Load cache from storage backend
364    async fn load_from_storage(&self) -> Result<()> {
365        if !self.config.persistence.enabled {
366            return Ok(());
367        }
368
369        let mut loaded_entries = self.backend.load().await?;
370        for (key, key_entries) in &mut loaded_entries {
371            if key_entries.iter().any(|entry| &entry.key != key) {
372                return Err(CacheError::Deserialization(
373                    "persisted entry key does not match its containing key".to_string(),
374                ));
375            }
376            key_entries.retain(|entry| !entry.is_expired());
377            key_entries.sort_by_key(|entry| entry.timestamp);
378            if key_entries.len() > self.config.max_entries_per_key {
379                let excess = key_entries.len() - self.config.max_entries_per_key;
380                key_entries.drain(..excess);
381            }
382        }
383        loaded_entries.retain(|_, key_entries| !key_entries.is_empty());
384
385        let total_entries = loaded_entries
386            .values()
387            .try_fold(0usize, |total, key_entries| {
388                total
389                    .checked_add(key_entries.len())
390                    .ok_or_else(|| CacheError::CapacityExceeded {
391                        message: "persisted entry count overflowed usize".to_string(),
392                    })
393            })?;
394        if total_entries > self.config.max_total_entries {
395            let mut flattened = Vec::new();
396            flattened
397                .try_reserve(total_entries)
398                .map_err(|error| CacheError::CapacityExceeded {
399                    message: format!("could not allocate persisted entries: {error}"),
400                })?;
401            for (_, key_entries) in loaded_entries.drain() {
402                flattened.extend(
403                    key_entries
404                        .into_iter()
405                        .map(|entry| (entry.key.clone(), entry)),
406                );
407            }
408            flattened.sort_unstable_by_key(|(_, entry)| entry.timestamp);
409            flattened.drain(..total_entries - self.config.max_total_entries);
410
411            loaded_entries
412                .try_reserve(flattened.len())
413                .map_err(|error| CacheError::CapacityExceeded {
414                    message: format!("could not allocate persisted cache index: {error}"),
415                })?;
416            for (key, entry) in flattened {
417                loaded_entries.entry(key).or_default().push(entry);
418            }
419        }
420        let mut entries = self.entries.write().await;
421        *entries = loaded_entries;
422        Ok(())
423    }
424
425    /// Force the next mutation to retry a full persistence sync.
426    async fn mark_sync_pending(&self) {
427        if self.config.persistence.enabled {
428            *self.operation_count.write().await = self.config.persistence.sync_interval;
429        }
430    }
431
432    /// Increment operation count and sync if needed
433    async fn increment_and_maybe_sync(&self) -> Result<()> {
434        if !self.config.persistence.enabled {
435            return Ok(());
436        }
437        let mut count = self.operation_count.write().await;
438        *count = count.saturating_add(1);
439
440        if *count >= self.config.persistence.sync_interval {
441            *count = 0;
442            drop(count);
443            if let Err(error) = self.save_to_storage().await {
444                self.mark_sync_pending().await;
445                return Err(error);
446            }
447        }
448
449        Ok(())
450    }
451}
452
453impl_cache_common!(
454    Clone,
455    fn clone(&self) -> Self {
456        Self {
457            entries: Arc::clone(&self.entries),
458            config: self.config.clone(),
459            backend: Arc::clone(&self.backend),
460            operation_lock: Arc::clone(&self.operation_lock),
461            operation_count: Arc::clone(&self.operation_count),
462            eviction_strategy: crate::eviction::create_strategy(&self.config.eviction_policy),
463        }
464    }
465);
466
467impl_cache_common!(#[async_trait] AsyncCache<K, V>,
468    type Error = CacheError;
469
470    async fn get(&self, key: &K) -> std::result::Result<Option<V>, Self::Error> {
471        Ok(self.get_latest(key).await.map(|entry| entry.value))
472    }
473
474    async fn put(&self, key: K, value: V) -> std::result::Result<(), Self::Error> {
475        let _operation = self.operation_lock.lock().await;
476        let entry = self.apply_default_ttl(CacheEntry::new(key.clone(), value))?;
477        {
478            let mut entries = self.entries.write().await;
479            entries.remove(&key);
480            self.insert_entry(&mut entries, entry).await?;
481        }
482
483        // Increment operation count and check if we need to sync
484        self.increment_and_maybe_sync().await?;
485        Ok(())
486    }
487
488    async fn remove(&self, key: &K) -> std::result::Result<Option<V>, Self::Error> {
489        let _operation = self.operation_lock.lock().await;
490        let removed = self.entries.write().await.remove(key);
491
492        if removed.is_some() {
493            if self.config.persistence.enabled
494                && let Err(error) = self.backend.remove(key).await
495            {
496                self.mark_sync_pending().await;
497                return Err(error);
498            }
499            self.increment_and_maybe_sync().await?;
500        }
501
502        Ok(removed.and_then(|entries| {
503            entries
504                .into_iter()
505                .max_by_key(|entry| entry.timestamp)
506                .map(|entry| entry.value)
507        }))
508    }
509
510    async fn clear(&self) -> std::result::Result<(), Self::Error> {
511        let _operation = self.operation_lock.lock().await;
512        self.entries.write().await.clear();
513
514        if self.config.persistence.enabled
515            && let Err(error) = self.backend.clear().await
516        {
517            self.mark_sync_pending().await;
518            return Err(error);
519        }
520
521        *self.operation_count.write().await = 0;
522        Ok(())
523    }
524
525    async fn contains(&self, key: &K) -> std::result::Result<bool, Self::Error> {
526        let entries = self.entries.read().await;
527        Ok(entries
528            .get(key)
529            .is_some_and(|key_entries| key_entries.iter().any(|entry| !entry.is_expired())))
530    }
531
532    async fn len(&self) -> std::result::Result<usize, Self::Error> {
533        let entries = self.entries.read().await;
534        Ok(entries
535            .values()
536            .flat_map(|key_entries| key_entries.iter())
537            .filter(|entry| !entry.is_expired())
538            .count())
539    }
540);
541
542/// Cache statistics
543#[derive(Debug, Clone, Default)]
544pub struct CacheStats {
545    /// Total number of entries
546    pub total_entries: usize,
547    /// Total number of unique keys
548    pub total_keys: usize,
549    /// Total access count across all entries
550    pub total_access_count: u64,
551    /// Number of expired entries
552    pub expired_count: usize,
553}
554
555#[cfg(test)]
556mod tests {
557    use super::*;
558    use crate::SearchQuery;
559    use crate::backends::memory::MemoryBackend;
560
561    async fn create_cache() -> Cache<String, String> {
562        let config = CacheConfig::default();
563        let backend = MemoryBackend::new();
564        Cache::new(config, backend).await.unwrap()
565    }
566
567    #[tokio::test]
568    async fn test_cache_basic_operations() {
569        let cache = create_cache().await;
570
571        // Test put and get
572        cache
573            .put("key1".to_string(), "value1".to_string())
574            .await
575            .unwrap();
576        let value = cache.get(&"key1".to_string()).await.unwrap();
577        assert_eq!(value, Some("value1".to_string()));
578
579        // Test contains
580        assert!(cache.contains(&"key1".to_string()).await.unwrap());
581        assert!(!cache.contains(&"key2".to_string()).await.unwrap());
582
583        // Test len
584        assert_eq!(cache.len().await.unwrap(), 1);
585
586        // Test remove
587        let removed = cache.remove(&"key1".to_string()).await.unwrap();
588        assert_eq!(removed, Some("value1".to_string()));
589        assert_eq!(cache.len().await.unwrap(), 0);
590    }
591
592    #[tokio::test]
593    async fn test_cache_clear() {
594        let cache = create_cache().await;
595
596        cache
597            .put("key1".to_string(), "value1".to_string())
598            .await
599            .unwrap();
600        cache
601            .put("key2".to_string(), "value2".to_string())
602            .await
603            .unwrap();
604
605        assert_eq!(cache.len().await.unwrap(), 2);
606
607        cache.clear().await.unwrap();
608        assert_eq!(cache.len().await.unwrap(), 0);
609        assert!(!cache.contains(&"key1".to_string()).await.unwrap());
610    }
611
612    #[tokio::test]
613    async fn test_entry_limits_and_eviction() {
614        let config = CacheConfig {
615            max_entries_per_key: 2,
616            max_total_entries: 3,
617            ..CacheConfig::default()
618        };
619        let backend: MemoryBackend<String, String> = MemoryBackend::new();
620        let cache: Cache<String, String> = Cache::new(config, backend).await.unwrap();
621
622        cache
623            .add_entry(CacheEntry::new("k1".to_string(), "v1".to_string()))
624            .await
625            .unwrap();
626        cache
627            .add_entry(CacheEntry::new("k1".to_string(), "v2".to_string()))
628            .await
629            .unwrap();
630        cache
631            .add_entry(CacheEntry::new("k1".to_string(), "v3".to_string()))
632            .await
633            .unwrap();
634
635        let k1_entries = cache.get_entries(&"k1".to_string()).await.unwrap();
636        assert_eq!(k1_entries.len(), 2);
637        assert_eq!(k1_entries[0].value, "v2");
638        assert_eq!(k1_entries[1].value, "v3");
639
640        cache
641            .add_entry(CacheEntry::new("k2".to_string(), "v".to_string()))
642            .await
643            .unwrap();
644        cache
645            .add_entry(CacheEntry::new("k3".to_string(), "v".to_string()))
646            .await
647            .unwrap();
648
649        assert!(cache.len().await.unwrap() <= 3);
650    }
651
652    #[tokio::test]
653    async fn test_cache_entries_search_stats() {
654        let cache = create_cache().await;
655
656        let mut first = CacheEntry::new("key".to_string(), "v1".to_string());
657        first.timestamp = chrono::Utc::now() - chrono::Duration::seconds(1);
658        first.last_accessed = first.timestamp;
659        cache.add_entry(first).await.unwrap();
660        cache
661            .add_entry(CacheEntry::new("key".to_string(), "v2".to_string()))
662            .await
663            .unwrap();
664
665        let entries = cache.get_entries(&"key".to_string()).await.unwrap();
666        assert_eq!(entries.len(), 2);
667        let latest = cache.get_latest(&"key".to_string()).await.unwrap();
668        assert_eq!(latest.value, "v2");
669
670        let results = cache.search(&SearchQuery::new().with_pattern("key")).await;
671        assert_eq!(results.len(), 2);
672
673        // Add expired entry for stats
674        let expired = CacheEntry::new("expired".to_string(), "v".to_string())
675            .with_ttl(chrono::Duration::seconds(-1));
676        cache.add_entry(expired).await.unwrap();
677
678        let stats = cache.get_stats().await;
679        assert_eq!(stats.total_entries, 3);
680        assert_eq!(stats.expired_count, 1);
681        assert_eq!(stats.total_access_count, 3); // accesses from get_entries/get_latest
682    }
683
684    #[tokio::test]
685    async fn test_empty_cache_stats() {
686        let cache = create_cache().await;
687        let stats = cache.get_stats().await;
688        assert_eq!(stats.total_entries, 0);
689        assert_eq!(stats.total_keys, 0);
690        assert_eq!(stats.total_access_count, 0);
691        assert_eq!(stats.expired_count, 0);
692    }
693
694    #[tokio::test]
695    async fn test_cache_persistence() {
696        use crate::test_utils::TestBackend;
697
698        let backend = TestBackend::default();
699        // Preload backend
700        backend
701            .save(&HashMap::from([(
702                "loaded".to_string(),
703                vec![CacheEntry::new("loaded".to_string(), "v".to_string())],
704            )]))
705            .await
706            .unwrap();
707
708        let mut config = CacheConfig::default();
709        config.persistence.enabled = true;
710        config.persistence.load_on_startup = true;
711        config.persistence.sync_interval = 1;
712
713        let cache: Cache<String, String, (), TestBackend> =
714            Cache::new(config, backend.clone()).await.unwrap();
715        // Loaded entry should be present
716        assert!(cache.contains(&"loaded".to_string()).await.unwrap());
717        assert_eq!(*backend.load_calls.read().await, 1);
718
719        // Put new entry triggers save due to sync_interval=1
720        cache.put("k".to_string(), "v".to_string()).await.unwrap();
721        assert!(*backend.save_calls.read().await >= 1);
722        assert!(backend.entries.read().await.contains_key("k"));
723    }
724
725    #[tokio::test]
726    async fn invalid_capacity_and_sync_configuration_is_rejected() {
727        for config in [
728            CacheConfig {
729                max_entries_per_key: 0,
730                ..CacheConfig::default()
731            },
732            CacheConfig {
733                max_total_entries: 0,
734                ..CacheConfig::default()
735            },
736            CacheConfig {
737                persistence: crate::PersistenceConfig {
738                    enabled: true,
739                    sync_interval: 0,
740                    load_on_startup: false,
741                },
742                ..CacheConfig::default()
743            },
744        ] {
745            let result: Result<Cache<String, String>> =
746                Cache::new(config, MemoryBackend::new()).await;
747            assert!(matches!(result, Err(CacheError::InvalidConfiguration(_))));
748        }
749    }
750
751    #[tokio::test]
752    async fn default_ttl_is_applied_and_expired_entries_are_hidden() {
753        let config = CacheConfig::default().with_default_ttl(std::time::Duration::ZERO);
754        let cache: Cache<String, String> = Cache::new(config, MemoryBackend::new()).await.unwrap();
755        let key = "expired".to_string();
756        cache.put(key.clone(), "value".to_string()).await.unwrap();
757
758        assert_eq!(cache.get(&key).await.unwrap(), None);
759        assert!(!cache.contains(&key).await.unwrap());
760        assert_eq!(cache.len().await.unwrap(), 0);
761        assert!(cache.get_entries(&key).await.is_none());
762    }
763
764    #[tokio::test]
765    async fn explicit_entry_ttl_overrides_default_ttl() {
766        let config = CacheConfig::default().with_default_ttl(std::time::Duration::ZERO);
767        let cache: Cache<String, String> = Cache::new(config, MemoryBackend::new()).await.unwrap();
768        cache
769            .add_entry(
770                CacheEntry::new("key".to_string(), "value".to_string())
771                    .with_ttl(chrono::Duration::hours(1)),
772            )
773            .await
774            .unwrap();
775        assert_eq!(
776            cache.get(&"key".to_string()).await.unwrap(),
777            Some("value".to_string())
778        );
779    }
780
781    #[tokio::test]
782    async fn expired_entries_are_reclaimed_before_capacity_checks() {
783        let config = CacheConfig {
784            max_total_entries: 1,
785            eviction_policy: crate::EvictionPolicy::None,
786            ..CacheConfig::default()
787        };
788        let cache: Cache<String, String> = Cache::new(config, MemoryBackend::new()).await.unwrap();
789        cache
790            .add_entry(
791                CacheEntry::new("expired".to_string(), "old".to_string())
792                    .with_ttl(chrono::Duration::seconds(-1)),
793            )
794            .await
795            .unwrap();
796
797        assert_eq!(cache.len().await.unwrap(), 0);
798        cache
799            .put("live".to_string(), "new".to_string())
800            .await
801            .unwrap();
802
803        assert_eq!(cache.len().await.unwrap(), 1);
804        assert_eq!(
805            cache.get(&"live".to_string()).await.unwrap(),
806            Some("new".to_string())
807        );
808        let stats = cache.get_stats().await;
809        assert_eq!(stats.total_entries, 1);
810        assert_eq!(stats.expired_count, 0);
811    }
812
813    #[tokio::test]
814    async fn add_entry_orders_history_and_retains_newest_timestamps() {
815        let config = CacheConfig {
816            max_entries_per_key: 2,
817            ..CacheConfig::default()
818        };
819        let cache: Cache<String, String> = Cache::new(config, MemoryBackend::new()).await.unwrap();
820        let key = "history".to_string();
821        let now = chrono::Utc::now();
822
823        let mut newest = CacheEntry::new(key.clone(), "newest".to_string());
824        newest.timestamp = now;
825        newest.last_accessed = now;
826        let mut oldest = CacheEntry::new(key.clone(), "oldest".to_string());
827        oldest.timestamp = now - chrono::Duration::hours(2);
828        oldest.last_accessed = oldest.timestamp;
829        let mut middle = CacheEntry::new(key.clone(), "middle".to_string());
830        middle.timestamp = now - chrono::Duration::hours(1);
831        middle.last_accessed = middle.timestamp;
832
833        cache.add_entry(newest).await.unwrap();
834        cache.add_entry(oldest).await.unwrap();
835        cache.add_entry(middle).await.unwrap();
836
837        let history = cache.get_entries(&key).await.unwrap();
838        assert_eq!(
839            history
840                .iter()
841                .map(|entry| entry.value.as_str())
842                .collect::<Vec<_>>(),
843            vec!["middle", "newest"]
844        );
845        assert!(
846            history
847                .windows(2)
848                .all(|pair| pair[0].timestamp <= pair[1].timestamp)
849        );
850        assert_eq!(cache.get_latest(&key).await.unwrap().value, "newest");
851    }
852
853    #[tokio::test]
854    async fn no_eviction_policy_returns_capacity_error_without_growing() {
855        let config = CacheConfig {
856            max_total_entries: 1,
857            eviction_policy: crate::EvictionPolicy::None,
858            ..CacheConfig::default()
859        };
860        let cache: Cache<String, String> = Cache::new(config, MemoryBackend::new()).await.unwrap();
861        cache.put("one".to_string(), "1".to_string()).await.unwrap();
862        let result = cache.put("two".to_string(), "2".to_string()).await;
863        assert!(matches!(result, Err(CacheError::CapacityExceeded { .. })));
864        assert_eq!(cache.len().await.unwrap(), 1);
865        assert_eq!(
866            cache.get(&"one".to_string()).await.unwrap(),
867            Some("1".to_string())
868        );
869    }
870
871    #[tokio::test]
872    async fn startup_rejects_mismatched_embedded_keys() {
873        use crate::test_utils::TestBackend;
874
875        let backend = TestBackend::default();
876        backend
877            .save(&HashMap::from([(
878                "outer".to_string(),
879                vec![CacheEntry::new("inner".to_string(), "value".to_string())],
880            )]))
881            .await
882            .unwrap();
883        let config = CacheConfig {
884            persistence: crate::PersistenceConfig::enabled(),
885            ..CacheConfig::default()
886        };
887        let result: Result<Cache<String, String, (), TestBackend>> =
888            Cache::new(config, backend).await;
889        assert!(matches!(result, Err(CacheError::Deserialization(_))));
890    }
891
892    #[tokio::test]
893    async fn flush_waits_for_persistence() {
894        use crate::test_utils::TestBackend;
895
896        let backend = TestBackend::default();
897        let config = CacheConfig {
898            persistence: crate::PersistenceConfig {
899                enabled: true,
900                sync_interval: 100,
901                load_on_startup: false,
902            },
903            ..CacheConfig::default()
904        };
905        let cache: Cache<String, String, (), TestBackend> =
906            Cache::new(config, backend.clone()).await.unwrap();
907        cache
908            .put("key".to_string(), "value".to_string())
909            .await
910            .unwrap();
911        assert_eq!(*backend.save_calls.read().await, 0);
912        cache.flush().await.unwrap();
913        assert_eq!(*backend.save_calls.read().await, 1);
914        assert!(backend.entries.read().await.contains_key("key"));
915    }
916
917    #[tokio::test]
918    async fn remove_error_keeps_memory_mutation_and_next_mutation_reconciles() {
919        use crate::test_utils::TestBackend;
920
921        let backend = TestBackend::default();
922        let removed_key = "removed".to_string();
923        backend
924            .save(&HashMap::from([(
925                removed_key.clone(),
926                vec![CacheEntry::new(removed_key.clone(), "value".to_string())],
927            )]))
928            .await
929            .unwrap();
930        let config = CacheConfig {
931            persistence: crate::PersistenceConfig {
932                enabled: true,
933                sync_interval: 100,
934                load_on_startup: true,
935            },
936            ..CacheConfig::default()
937        };
938        let cache: Cache<String, String, (), TestBackend> =
939            Cache::new(config, backend.clone()).await.unwrap();
940        *backend.remove_error_after_mutation.write().await = true;
941
942        let error = cache.remove(&removed_key).await.unwrap_err();
943        assert!(matches!(error, CacheError::StorageBackend(_)));
944        assert!(!cache.contains(&removed_key).await.unwrap());
945        assert_eq!(
946            *cache.operation_count.read().await,
947            cache.config.persistence.sync_interval
948        );
949
950        *backend.remove_error_after_mutation.write().await = false;
951        cache
952            .put("new".to_string(), "value".to_string())
953            .await
954            .unwrap();
955        let persisted = backend.entries.read().await;
956        assert!(!persisted.contains_key(&removed_key));
957        assert!(persisted.contains_key("new"));
958        assert_eq!(*cache.operation_count.read().await, 0);
959    }
960
961    #[tokio::test]
962    async fn clear_error_keeps_memory_mutation_and_flush_reconciles() {
963        use crate::test_utils::TestBackend;
964
965        let backend = TestBackend::default();
966        backend
967            .save(&HashMap::from([(
968                "key".to_string(),
969                vec![CacheEntry::new("key".to_string(), "value".to_string())],
970            )]))
971            .await
972            .unwrap();
973        let config = CacheConfig {
974            persistence: crate::PersistenceConfig {
975                enabled: true,
976                sync_interval: 100,
977                load_on_startup: true,
978            },
979            ..CacheConfig::default()
980        };
981        let cache: Cache<String, String, (), TestBackend> =
982            Cache::new(config, backend.clone()).await.unwrap();
983        *backend.clear_error_after_mutation.write().await = true;
984
985        let error = cache.clear().await.unwrap_err();
986        assert!(matches!(error, CacheError::StorageBackend(_)));
987        assert!(cache.is_empty().await.unwrap());
988        assert_eq!(
989            *cache.operation_count.read().await,
990            cache.config.persistence.sync_interval
991        );
992
993        *backend.clear_error_after_mutation.write().await = false;
994        cache.flush().await.unwrap();
995        assert!(backend.entries.read().await.is_empty());
996        assert_eq!(*cache.operation_count.read().await, 0);
997    }
998
999    #[tokio::test]
1000    async fn successful_clear_resets_a_pending_sync() {
1001        use crate::test_utils::TestBackend;
1002
1003        let backend = TestBackend::default();
1004        let first_key = "first".to_string();
1005        backend
1006            .save(&HashMap::from([
1007                (
1008                    first_key.clone(),
1009                    vec![CacheEntry::new(first_key.clone(), "1".to_string())],
1010                ),
1011                (
1012                    "second".to_string(),
1013                    vec![CacheEntry::new("second".to_string(), "2".to_string())],
1014                ),
1015            ]))
1016            .await
1017            .unwrap();
1018        let config = CacheConfig {
1019            persistence: crate::PersistenceConfig {
1020                enabled: true,
1021                sync_interval: 2,
1022                load_on_startup: true,
1023            },
1024            ..CacheConfig::default()
1025        };
1026        let cache: Cache<String, String, (), TestBackend> =
1027            Cache::new(config, backend.clone()).await.unwrap();
1028        *backend.remove_error_after_mutation.write().await = true;
1029        assert!(cache.remove(&first_key).await.is_err());
1030        *backend.remove_error_after_mutation.write().await = false;
1031
1032        cache.clear().await.unwrap();
1033        assert_eq!(*cache.operation_count.read().await, 0);
1034        let save_calls = *backend.save_calls.read().await;
1035        cache
1036            .put("new".to_string(), "value".to_string())
1037            .await
1038            .unwrap();
1039        assert_eq!(*backend.save_calls.read().await, save_calls);
1040    }
1041
1042    #[cfg(feature = "filesystem-backend")]
1043    #[tokio::test]
1044    async fn startup_propagates_corrupt_filesystem_snapshot() {
1045        use crate::FilesystemBackend;
1046
1047        let directory = tempfile::TempDir::new().unwrap();
1048        tokio::fs::write(directory.path().join("cache.json"), b"not json")
1049            .await
1050            .unwrap();
1051        let backend: FilesystemBackend<String, String> =
1052            FilesystemBackend::new(directory.path()).await.unwrap();
1053        let config = CacheConfig {
1054            persistence: crate::PersistenceConfig::enabled(),
1055            ..CacheConfig::default()
1056        };
1057        let result: Result<Cache<String, String, (), _>> = Cache::new(config, backend).await;
1058        assert!(matches!(result, Err(CacheError::Deserialization(_))));
1059    }
1060}