1use 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
15type CacheStorage<K, V, M> = Arc<RwLock<HashMap<K, Vec<CacheEntry<K, V, M>>>>>;
17
18type EvictionStrategyBox<K, V, M> = Box<dyn EvictionStrategy<K, V, M>>;
20
21type 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
39pub trait CacheKey: Hash + Eq + Clone + Send + Sync + 'static {}
41impl<T> CacheKey for T where T: Hash + Eq + Clone + Send + Sync + 'static {}
42
43pub trait CacheValue: Clone + Send + Sync + 'static {}
45impl<T> CacheValue for T where T: Clone + Send + Sync + 'static {}
46
47#[async_trait]
49pub trait AsyncCache<K, V>: Send + Sync
50where
51 K: CacheKey,
52 V: CacheValue,
53{
54 type Error;
56
57 async fn get(&self, key: &K) -> std::result::Result<Option<V>, Self::Error>;
59
60 async fn put(&self, key: K, value: V) -> std::result::Result<(), Self::Error>;
62
63 async fn remove(&self, key: &K) -> std::result::Result<Option<V>, Self::Error>;
65
66 async fn clear(&self) -> std::result::Result<(), Self::Error>;
68
69 async fn contains(&self, key: &K) -> std::result::Result<bool, Self::Error>;
71
72 async fn len(&self) -> std::result::Result<usize, Self::Error>;
74
75 async fn is_empty(&self) -> std::result::Result<bool, Self::Error> {
77 Ok(self.len().await? == 0)
78 }
79}
80
81#[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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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#[derive(Debug, Clone, Default)]
544pub struct CacheStats {
545 pub total_entries: usize,
547 pub total_keys: usize,
549 pub total_access_count: u64,
551 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 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 assert!(cache.contains(&"key1".to_string()).await.unwrap());
581 assert!(!cache.contains(&"key2".to_string()).await.unwrap());
582
583 assert_eq!(cache.len().await.unwrap(), 1);
585
586 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 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); }
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 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 assert!(cache.contains(&"loaded".to_string()).await.unwrap());
717 assert_eq!(*backend.load_calls.read().await, 1);
718
719 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}