1use serde::{Deserialize, Serialize};
35use std::collections::{HashMap, HashSet};
36use std::time::{Duration, SystemTime};
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
40pub enum CachePolicy {
41 NoCache,
43 Ttl(Duration),
45 Indefinite,
47 Lru {
49 max_size: usize,
50 ttl: Option<Duration>,
51 },
52 Lfu {
54 max_size: usize,
55 ttl: Option<Duration>,
56 },
57}
58
59impl Default for CachePolicy {
60 fn default() -> Self {
61 CachePolicy::Ttl(Duration::from_secs(3600)) }
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
67pub enum InvalidationStrategy {
68 All,
70 Pattern(String),
72 NodeIds(Vec<String>),
74 OlderThan(Duration),
76 Tags(Vec<String>),
78 Prefix(String),
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct CacheConfig {
85 pub policy: CachePolicy,
87 pub enable_warming: bool,
89 pub max_size_bytes: u64,
91 pub enable_compression: bool,
93 pub invalidation_strategy: InvalidationStrategy,
95 pub enable_stats: bool,
97}
98
99impl Default for CacheConfig {
100 fn default() -> Self {
101 Self {
102 policy: CachePolicy::default(),
103 enable_warming: false,
104 max_size_bytes: 0,
105 enable_compression: false,
106 invalidation_strategy: InvalidationStrategy::OlderThan(Duration::from_secs(86400)), enable_stats: true,
108 }
109 }
110}
111
112impl CacheConfig {
113 pub fn with_ttl(mut self, ttl: Duration) -> Self {
115 self.policy = CachePolicy::Ttl(ttl);
116 self
117 }
118
119 pub fn with_max_size(mut self, max_size: usize) -> Self {
121 self.policy = match self.policy {
122 CachePolicy::Lru { ttl, .. } => CachePolicy::Lru { max_size, ttl },
123 CachePolicy::Lfu { ttl, .. } => CachePolicy::Lfu { max_size, ttl },
124 _ => CachePolicy::Lru {
125 max_size,
126 ttl: Some(Duration::from_secs(3600)),
127 },
128 };
129 self
130 }
131
132 pub fn with_warming(mut self, enable: bool) -> Self {
134 self.enable_warming = enable;
135 self
136 }
137
138 pub fn with_max_bytes(mut self, max_bytes: u64) -> Self {
140 self.max_size_bytes = max_bytes;
141 self
142 }
143
144 pub fn with_compression(mut self, enable: bool) -> Self {
146 self.enable_compression = enable;
147 self
148 }
149
150 pub fn with_invalidation(mut self, strategy: InvalidationStrategy) -> Self {
152 self.invalidation_strategy = strategy;
153 self
154 }
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct CacheEntry {
160 pub key: String,
162 pub value: Vec<u8>,
164 pub created_at: SystemTime,
166 pub last_accessed: SystemTime,
168 pub access_count: u64,
170 pub size_bytes: usize,
172 pub tags: Vec<String>,
174 pub ttl: Option<Duration>,
176}
177
178impl CacheEntry {
179 pub fn is_expired(&self) -> bool {
181 if let Some(ttl) = self.ttl {
182 if let Ok(elapsed) = self.created_at.elapsed() {
183 return elapsed > ttl;
184 }
185 }
186 false
187 }
188
189 pub fn record_access(&mut self) {
191 self.last_accessed = SystemTime::now();
192 self.access_count += 1;
193 }
194
195 pub fn age(&self) -> Duration {
197 self.created_at.elapsed().unwrap_or(Duration::from_secs(0))
198 }
199}
200
201pub struct CacheKeyGenerator;
203
204impl CacheKeyGenerator {
205 pub fn llm_prompt_key(model: &str, prompt: &str, params: &[(&str, &str)]) -> String {
212 use std::collections::BTreeMap;
213
214 let sorted_params: BTreeMap<_, _> = params.iter().map(|(k, v)| (*k, *v)).collect();
216 let params_str = sorted_params
217 .iter()
218 .map(|(k, v)| format!("{}={}", k, v))
219 .collect::<Vec<_>>()
220 .join("&");
221
222 format!(
223 "llm:{}:{}:{}",
224 model,
225 Self::hash_string(prompt),
226 Self::hash_string(¶ms_str)
227 )
228 }
229
230 pub fn code_execution_key(
232 runtime: &str,
233 code: &str,
234 input_vars: &HashMap<String, String>,
235 ) -> String {
236 use std::collections::BTreeMap;
237
238 let sorted_vars: BTreeMap<_, _> = input_vars.iter().collect();
239 let vars_str = sorted_vars
240 .iter()
241 .map(|(k, v)| format!("{}={}", k, Self::hash_string(v)))
242 .collect::<Vec<_>>()
243 .join("&");
244
245 format!(
246 "code:{}:{}:{}",
247 runtime,
248 Self::hash_string(code),
249 Self::hash_string(&vars_str)
250 )
251 }
252
253 pub fn vector_retrieval_key(
255 collection: &str,
256 query: &str,
257 top_k: usize,
258 filters: &HashMap<String, String>,
259 ) -> String {
260 use std::collections::BTreeMap;
261
262 let sorted_filters: BTreeMap<_, _> = filters.iter().collect();
263 let filters_str = sorted_filters
264 .iter()
265 .map(|(k, v)| format!("{}={}", k, v))
266 .collect::<Vec<_>>()
267 .join("&");
268
269 format!(
270 "vector:{}:{}:{}:{}",
271 collection,
272 Self::hash_string(query),
273 top_k,
274 Self::hash_string(&filters_str)
275 )
276 }
277
278 pub fn workflow_execution_key(
280 workflow_id: &str,
281 version: &str,
282 inputs: &HashMap<String, String>,
283 ) -> String {
284 use std::collections::BTreeMap;
285
286 let sorted_inputs: BTreeMap<_, _> = inputs.iter().collect();
287 let inputs_str = sorted_inputs
288 .iter()
289 .map(|(k, v)| format!("{}={}", k, Self::hash_string(v)))
290 .collect::<Vec<_>>()
291 .join("&");
292
293 format!(
294 "workflow:{}:{}:{}",
295 workflow_id,
296 version,
297 Self::hash_string(&inputs_str)
298 )
299 }
300
301 fn hash_string(s: &str) -> String {
303 use std::collections::hash_map::DefaultHasher;
304 use std::hash::{Hash, Hasher};
305
306 let mut hasher = DefaultHasher::new();
307 s.hash(&mut hasher);
308 format!("{:x}", hasher.finish())
309 }
310}
311
312#[derive(Debug, Clone, Default, Serialize, Deserialize)]
314pub struct CacheStats {
315 pub hits: u64,
317 pub misses: u64,
319 pub evictions: u64,
321 pub entry_count: usize,
323 pub total_bytes: u64,
325 pub hit_rate: f64,
327 pub avg_access_count: f64,
329}
330
331impl CacheStats {
332 pub fn calculate_hit_rate(&mut self) {
334 let total = self.hits + self.misses;
335 self.hit_rate = if total > 0 {
336 (self.hits as f64 / total as f64) * 100.0
337 } else {
338 0.0
339 };
340 }
341
342 pub fn record_hit(&mut self) {
344 self.hits += 1;
345 self.calculate_hit_rate();
346 }
347
348 pub fn record_miss(&mut self) {
350 self.misses += 1;
351 self.calculate_hit_rate();
352 }
353
354 pub fn record_eviction(&mut self) {
356 self.evictions += 1;
357 }
358
359 pub fn update_stats(&mut self, entry_count: usize, total_bytes: u64, total_accesses: u64) {
361 self.entry_count = entry_count;
362 self.total_bytes = total_bytes;
363 self.avg_access_count = if entry_count > 0 {
364 total_accesses as f64 / entry_count as f64
365 } else {
366 0.0
367 };
368 }
369}
370
371#[derive(Debug, Clone, Serialize, Deserialize)]
373pub struct CacheWarmingConfig {
374 pub enabled: bool,
376 pub node_ids: Vec<String>,
378 pub max_entries: usize,
380 pub strategy: WarmingStrategy,
382}
383
384#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
386pub enum WarmingStrategy {
387 MostFrequent,
389 MostRecent,
391 Pattern(Vec<String>),
393 All,
395}
396
397impl Default for CacheWarmingConfig {
398 fn default() -> Self {
399 Self {
400 enabled: false,
401 node_ids: Vec::new(),
402 max_entries: 100,
403 strategy: WarmingStrategy::MostFrequent,
404 }
405 }
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct InvalidationPlan {
411 pub strategy: InvalidationStrategy,
413 pub estimated_count: usize,
415 pub affected_tags: Vec<String>,
417 pub affected_nodes: Vec<String>,
419}
420
421impl InvalidationPlan {
422 pub fn new(strategy: InvalidationStrategy) -> Self {
424 Self {
425 strategy,
426 estimated_count: 0,
427 affected_tags: Vec::new(),
428 affected_nodes: Vec::new(),
429 }
430 }
431
432 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
434 self.affected_tags = tags;
435 self
436 }
437
438 pub fn with_nodes(mut self, nodes: Vec<String>) -> Self {
440 self.affected_nodes = nodes;
441 self
442 }
443
444 pub fn with_count(mut self, count: usize) -> Self {
446 self.estimated_count = count;
447 self
448 }
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize)]
453pub struct CacheManager {
454 pub config: CacheConfig,
456 pub stats: CacheStats,
458 entries: HashMap<String, CacheEntry>,
460 tag_index: HashMap<String, HashSet<String>>,
462}
463
464impl CacheManager {
465 pub fn new(config: CacheConfig) -> Self {
467 Self {
468 config,
469 stats: CacheStats::default(),
470 entries: HashMap::new(),
471 tag_index: HashMap::new(),
472 }
473 }
474
475 pub fn get(&mut self, key: &str) -> Option<&CacheEntry> {
477 if let Some(entry) = self.entries.get_mut(key) {
478 if entry.is_expired() {
479 self.stats.record_miss();
480 self.entries.remove(key);
481 return None;
482 }
483 entry.record_access();
484 self.stats.record_hit();
485 self.entries.get(key)
486 } else {
487 self.stats.record_miss();
488 None
489 }
490 }
491
492 pub fn put(&mut self, mut entry: CacheEntry) {
494 if self.config.max_size_bytes > 0 {
496 let total_size = self.total_size();
497 if total_size + entry.size_bytes as u64 > self.config.max_size_bytes {
498 self.evict_entries(entry.size_bytes);
499 }
500 }
501
502 for tag in &entry.tags {
504 self.tag_index
505 .entry(tag.clone())
506 .or_default()
507 .insert(entry.key.clone());
508 }
509
510 if entry.ttl.is_none() {
512 entry.ttl = match &self.config.policy {
513 CachePolicy::Ttl(duration) => Some(*duration),
514 CachePolicy::Lru { ttl, .. } | CachePolicy::Lfu { ttl, .. } => *ttl,
515 _ => None,
516 };
517 }
518
519 self.entries.insert(entry.key.clone(), entry);
520 self.update_stats();
521 }
522
523 pub fn invalidate(&mut self, strategy: &InvalidationStrategy) -> usize {
525 let keys_to_remove: Vec<String> = match strategy {
526 InvalidationStrategy::All => self.entries.keys().cloned().collect(),
527
528 InvalidationStrategy::Pattern(pattern) => self
529 .entries
530 .keys()
531 .filter(|k| k.contains(pattern))
532 .cloned()
533 .collect(),
534
535 InvalidationStrategy::NodeIds(node_ids) => self
536 .entries
537 .keys()
538 .filter(|k| node_ids.iter().any(|nid| k.contains(nid)))
539 .cloned()
540 .collect(),
541
542 InvalidationStrategy::OlderThan(duration) => self
543 .entries
544 .iter()
545 .filter(|(_, entry)| entry.age() > *duration)
546 .map(|(k, _)| k.clone())
547 .collect(),
548
549 InvalidationStrategy::Tags(tags) => {
550 let mut keys = HashSet::new();
551 for tag in tags {
552 if let Some(tag_keys) = self.tag_index.get(tag) {
553 keys.extend(tag_keys.iter().cloned());
554 }
555 }
556 keys.into_iter().collect()
557 }
558
559 InvalidationStrategy::Prefix(prefix) => self
560 .entries
561 .keys()
562 .filter(|k| k.starts_with(prefix))
563 .cloned()
564 .collect(),
565 };
566
567 let count = keys_to_remove.len();
568 for key in keys_to_remove {
569 self.remove(&key);
570 }
571 count
572 }
573
574 pub fn remove(&mut self, key: &str) {
576 if let Some(entry) = self.entries.remove(key) {
577 for tag in &entry.tags {
579 if let Some(tag_keys) = self.tag_index.get_mut(tag) {
580 tag_keys.remove(key);
581 if tag_keys.is_empty() {
582 self.tag_index.remove(tag);
583 }
584 }
585 }
586 self.stats.record_eviction();
587 }
588 self.update_stats();
589 }
590
591 fn evict_entries(&mut self, needed_space: usize) {
593 let strategy = &self.config.policy;
594 match strategy {
595 CachePolicy::Lru { .. } => self.evict_lru(needed_space),
596 CachePolicy::Lfu { .. } => self.evict_lfu(needed_space),
597 _ => self.evict_oldest(needed_space),
598 }
599 }
600
601 fn evict_lru(&mut self, needed_space: usize) {
603 let mut entries: Vec<_> = self.entries.iter().collect();
604 entries.sort_by_key(|(_, e)| e.last_accessed);
605
606 let mut freed = 0;
607 let mut to_remove = Vec::new();
608
609 for (key, entry) in entries {
610 to_remove.push(key.clone());
611 freed += entry.size_bytes;
612 if freed >= needed_space {
613 break;
614 }
615 }
616
617 for key in to_remove {
618 self.remove(&key);
619 }
620 }
621
622 fn evict_lfu(&mut self, needed_space: usize) {
624 let mut entries: Vec<_> = self.entries.iter().collect();
625 entries.sort_by_key(|(_, e)| e.access_count);
626
627 let mut freed = 0;
628 let mut to_remove = Vec::new();
629
630 for (key, entry) in entries {
631 to_remove.push(key.clone());
632 freed += entry.size_bytes;
633 if freed >= needed_space {
634 break;
635 }
636 }
637
638 for key in to_remove {
639 self.remove(&key);
640 }
641 }
642
643 fn evict_oldest(&mut self, needed_space: usize) {
645 let mut entries: Vec<_> = self.entries.iter().collect();
646 entries.sort_by_key(|(_, e)| e.created_at);
647
648 let mut freed = 0;
649 let mut to_remove = Vec::new();
650
651 for (key, entry) in entries {
652 to_remove.push(key.clone());
653 freed += entry.size_bytes;
654 if freed >= needed_space {
655 break;
656 }
657 }
658
659 for key in to_remove {
660 self.remove(&key);
661 }
662 }
663
664 fn total_size(&self) -> u64 {
666 self.entries.values().map(|e| e.size_bytes as u64).sum()
667 }
668
669 fn update_stats(&mut self) {
671 let total_accesses: u64 = self.entries.values().map(|e| e.access_count).sum();
672 self.stats
673 .update_stats(self.entries.len(), self.total_size(), total_accesses);
674 }
675
676 pub fn get_stats(&self) -> &CacheStats {
678 &self.stats
679 }
680
681 pub fn clear(&mut self) {
683 let count = self.entries.len();
684 self.entries.clear();
685 self.tag_index.clear();
686 for _ in 0..count {
687 self.stats.record_eviction();
688 }
689 self.update_stats();
690 }
691}
692
693impl Default for CacheManager {
694 fn default() -> Self {
695 Self::new(CacheConfig::default())
696 }
697}
698
699#[cfg(test)]
700mod tests {
701 use super::*;
702
703 #[test]
704 fn test_cache_config_default() {
705 let config = CacheConfig::default();
706 assert!(config.enable_stats);
707 assert!(!config.enable_warming);
708 assert_eq!(config.max_size_bytes, 0);
709 }
710
711 #[test]
712 fn test_cache_config_builder() {
713 let config = CacheConfig::default()
714 .with_ttl(Duration::from_secs(1800))
715 .with_max_size(500)
716 .with_warming(true)
717 .with_compression(true);
718
719 assert!(config.enable_warming);
720 assert!(config.enable_compression);
721 assert!(matches!(config.policy, CachePolicy::Lru { .. }));
722 }
723
724 #[test]
725 fn test_llm_prompt_key_generation() {
726 let key1 = CacheKeyGenerator::llm_prompt_key(
727 "gpt-4",
728 "Hello world",
729 &[("temperature", "0.7"), ("max_tokens", "100")],
730 );
731
732 let key2 = CacheKeyGenerator::llm_prompt_key(
733 "gpt-4",
734 "Hello world",
735 &[("max_tokens", "100"), ("temperature", "0.7")],
736 );
737
738 assert_eq!(key1, key2);
740 assert!(key1.starts_with("llm:gpt-4:"));
741 }
742
743 #[test]
744 fn test_code_execution_key_generation() {
745 let mut vars = HashMap::new();
746 vars.insert("x".to_string(), "10".to_string());
747 vars.insert("y".to_string(), "20".to_string());
748
749 let key = CacheKeyGenerator::code_execution_key("rust", "fn main() {}", &vars);
750 assert!(key.starts_with("code:rust:"));
751 }
752
753 #[test]
754 fn test_vector_retrieval_key_generation() {
755 let mut filters = HashMap::new();
756 filters.insert("category".to_string(), "tech".to_string());
757
758 let key = CacheKeyGenerator::vector_retrieval_key("docs", "search query", 10, &filters);
759 assert!(key.starts_with("vector:docs:"));
760 assert!(key.contains(":10:"));
761 }
762
763 #[test]
764 fn test_workflow_execution_key_generation() {
765 let mut inputs = HashMap::new();
766 inputs.insert("user_id".to_string(), "123".to_string());
767
768 let key = CacheKeyGenerator::workflow_execution_key("workflow-1", "v1.0.0", &inputs);
769 assert!(key.starts_with("workflow:workflow-1:v1.0.0:"));
770 }
771
772 #[test]
773 fn test_cache_entry_expiration() {
774 let entry = CacheEntry {
775 key: "test".to_string(),
776 value: vec![1, 2, 3],
777 created_at: SystemTime::now() - Duration::from_secs(3700),
778 last_accessed: SystemTime::now(),
779 access_count: 1,
780 size_bytes: 3,
781 tags: vec![],
782 ttl: Some(Duration::from_secs(3600)), };
784
785 assert!(entry.is_expired());
786 }
787
788 #[test]
789 fn test_cache_entry_not_expired() {
790 let entry = CacheEntry {
791 key: "test".to_string(),
792 value: vec![1, 2, 3],
793 created_at: SystemTime::now(),
794 last_accessed: SystemTime::now(),
795 access_count: 1,
796 size_bytes: 3,
797 tags: vec![],
798 ttl: Some(Duration::from_secs(3600)),
799 };
800
801 assert!(!entry.is_expired());
802 }
803
804 #[test]
805 fn test_cache_stats_hit_rate() {
806 let mut stats = CacheStats::default();
807
808 stats.record_hit();
809 stats.record_hit();
810 stats.record_miss();
811
812 assert_eq!(stats.hits, 2);
813 assert_eq!(stats.misses, 1);
814 assert!((stats.hit_rate - 66.666).abs() < 0.01);
815 }
816
817 #[test]
818 fn test_cache_manager_put_and_get() {
819 let config = CacheConfig::default();
820 let mut manager = CacheManager::new(config);
821
822 let entry = CacheEntry {
823 key: "test-key".to_string(),
824 value: vec![1, 2, 3, 4],
825 created_at: SystemTime::now(),
826 last_accessed: SystemTime::now(),
827 access_count: 0,
828 size_bytes: 4,
829 tags: vec!["test".to_string()],
830 ttl: Some(Duration::from_secs(3600)),
831 };
832
833 manager.put(entry);
834
835 let retrieved = manager.get("test-key");
836 assert!(retrieved.is_some());
837 assert_eq!(retrieved.unwrap().value, vec![1, 2, 3, 4]);
838 assert_eq!(manager.stats.hits, 1);
839 }
840
841 #[test]
842 fn test_cache_manager_miss() {
843 let config = CacheConfig::default();
844 let mut manager = CacheManager::new(config);
845
846 let result = manager.get("nonexistent");
847 assert!(result.is_none());
848 assert_eq!(manager.stats.misses, 1);
849 }
850
851 #[test]
852 fn test_cache_invalidation_all() {
853 let config = CacheConfig::default();
854 let mut manager = CacheManager::new(config);
855
856 for i in 0..5 {
857 let entry = CacheEntry {
858 key: format!("key-{}", i),
859 value: vec![i as u8],
860 created_at: SystemTime::now(),
861 last_accessed: SystemTime::now(),
862 access_count: 0,
863 size_bytes: 1,
864 tags: vec![],
865 ttl: None,
866 };
867 manager.put(entry);
868 }
869
870 let count = manager.invalidate(&InvalidationStrategy::All);
871 assert_eq!(count, 5);
872 assert_eq!(manager.entries.len(), 0);
873 }
874
875 #[test]
876 fn test_cache_invalidation_pattern() {
877 let config = CacheConfig::default();
878 let mut manager = CacheManager::new(config);
879
880 let entry1 = CacheEntry {
881 key: "llm:gpt-4:test".to_string(),
882 value: vec![1],
883 created_at: SystemTime::now(),
884 last_accessed: SystemTime::now(),
885 access_count: 0,
886 size_bytes: 1,
887 tags: vec![],
888 ttl: None,
889 };
890
891 let entry2 = CacheEntry {
892 key: "code:rust:test".to_string(),
893 value: vec![2],
894 created_at: SystemTime::now(),
895 last_accessed: SystemTime::now(),
896 access_count: 0,
897 size_bytes: 1,
898 tags: vec![],
899 ttl: None,
900 };
901
902 manager.put(entry1);
903 manager.put(entry2);
904
905 let count = manager.invalidate(&InvalidationStrategy::Pattern("llm:".to_string()));
906 assert_eq!(count, 1);
907 assert_eq!(manager.entries.len(), 1);
908 }
909
910 #[test]
911 fn test_cache_invalidation_tags() {
912 let config = CacheConfig::default();
913 let mut manager = CacheManager::new(config);
914
915 let entry1 = CacheEntry {
916 key: "key1".to_string(),
917 value: vec![1],
918 created_at: SystemTime::now(),
919 last_accessed: SystemTime::now(),
920 access_count: 0,
921 size_bytes: 1,
922 tags: vec!["tag1".to_string()],
923 ttl: None,
924 };
925
926 let entry2 = CacheEntry {
927 key: "key2".to_string(),
928 value: vec![2],
929 created_at: SystemTime::now(),
930 last_accessed: SystemTime::now(),
931 access_count: 0,
932 size_bytes: 1,
933 tags: vec!["tag2".to_string()],
934 ttl: None,
935 };
936
937 manager.put(entry1);
938 manager.put(entry2);
939
940 let count = manager.invalidate(&InvalidationStrategy::Tags(vec!["tag1".to_string()]));
941 assert_eq!(count, 1);
942 assert!(manager.get("key2").is_some());
943 }
944
945 #[test]
946 fn test_cache_invalidation_prefix() {
947 let config = CacheConfig::default();
948 let mut manager = CacheManager::new(config);
949
950 let entry1 = CacheEntry {
951 key: "workflow:123:v1".to_string(),
952 value: vec![1],
953 created_at: SystemTime::now(),
954 last_accessed: SystemTime::now(),
955 access_count: 0,
956 size_bytes: 1,
957 tags: vec![],
958 ttl: None,
959 };
960
961 let entry2 = CacheEntry {
962 key: "llm:gpt-4:test".to_string(),
963 value: vec![2],
964 created_at: SystemTime::now(),
965 last_accessed: SystemTime::now(),
966 access_count: 0,
967 size_bytes: 1,
968 tags: vec![],
969 ttl: None,
970 };
971
972 manager.put(entry1);
973 manager.put(entry2);
974
975 let count = manager.invalidate(&InvalidationStrategy::Prefix("workflow:".to_string()));
976 assert_eq!(count, 1);
977 assert!(manager.get("llm:gpt-4:test").is_some());
978 }
979
980 #[test]
981 fn test_cache_manager_clear() {
982 let config = CacheConfig::default();
983 let mut manager = CacheManager::new(config);
984
985 for i in 0..3 {
986 let entry = CacheEntry {
987 key: format!("key-{}", i),
988 value: vec![i as u8],
989 created_at: SystemTime::now(),
990 last_accessed: SystemTime::now(),
991 access_count: 0,
992 size_bytes: 1,
993 tags: vec![],
994 ttl: None,
995 };
996 manager.put(entry);
997 }
998
999 manager.clear();
1000 assert_eq!(manager.entries.len(), 0);
1001 assert_eq!(manager.stats.evictions, 3);
1002 }
1003
1004 #[test]
1005 fn test_cache_lru_eviction() {
1006 let config = CacheConfig::default().with_max_bytes(10);
1007 let mut manager = CacheManager::new(config);
1008
1009 for i in 0..5 {
1011 let entry = CacheEntry {
1012 key: format!("key-{}", i),
1013 value: vec![i as u8],
1014 created_at: SystemTime::now(),
1015 last_accessed: SystemTime::now() - Duration::from_secs((5 - i) as u64),
1016 access_count: 0,
1017 size_bytes: 3,
1018 tags: vec![],
1019 ttl: None,
1020 };
1021 manager.put(entry);
1022 }
1023
1024 assert!(manager.total_size() <= 10);
1026 }
1027
1028 #[test]
1029 fn test_invalidation_plan() {
1030 let plan = InvalidationPlan::new(InvalidationStrategy::All)
1031 .with_tags(vec!["tag1".to_string()])
1032 .with_nodes(vec!["node1".to_string()])
1033 .with_count(10);
1034
1035 assert_eq!(plan.estimated_count, 10);
1036 assert_eq!(plan.affected_tags.len(), 1);
1037 assert_eq!(plan.affected_nodes.len(), 1);
1038 }
1039}