Skip to main content

oxirs_rule/
cache.rs

1//! # Advanced Caching System for Rule Engines
2//!
3//! This module provides sophisticated caching mechanisms to improve rule engine performance:
4//! - Rule result caching with LRU eviction
5//! - Fact derivation memoization
6//! - Query result caching
7//! - Intelligent cache warming and prefetching
8//! - Cache statistics and monitoring
9
10use serde::{Deserialize, Serialize};
11use std::collections::{HashMap, VecDeque};
12use std::hash::Hash;
13use std::sync::{Arc, RwLock};
14use std::time::{Duration, Instant};
15
16use crate::{Rule, RuleAtom};
17
18/// Cache key for rule execution results
19#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub struct RuleCacheKey {
21    pub rule_name: String,
22    pub input_facts: Vec<RuleAtom>,
23}
24
25/// Cache key for fact derivation queries
26#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub struct DerivationCacheKey {
28    pub goal_fact: RuleAtom,
29    pub context_facts: Vec<RuleAtom>,
30}
31
32/// Cached result entry
33#[derive(Debug, Clone)]
34pub struct CacheEntry<T> {
35    pub value: T,
36    pub timestamp: Instant,
37    pub access_count: usize,
38    pub last_access: Instant,
39    pub ttl: Option<Duration>,
40}
41
42/// Cache eviction policies
43#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
44pub enum EvictionPolicy {
45    LRU,      // Least Recently Used
46    LFU,      // Least Frequently Used
47    FIFO,     // First In, First Out
48    TTL,      // Time To Live
49    Adaptive, // Combines multiple strategies
50}
51
52/// Cache statistics and monitoring
53#[derive(Debug, Clone)]
54pub struct CacheStatistics {
55    pub hits: usize,
56    pub misses: usize,
57    pub evictions: usize,
58    pub insertions: usize,
59    pub memory_usage: usize,
60    pub hit_rate: f64,
61    pub average_access_time: Duration,
62    pub cache_size: usize,
63    pub max_size: usize,
64}
65
66/// LRU cache implementation with advanced features
67#[derive(Debug)]
68pub struct SmartCache<K, V>
69where
70    K: Clone + Eq + Hash,
71    V: Clone,
72{
73    entries: HashMap<K, CacheEntry<V>>,
74    access_order: VecDeque<K>,
75    max_size: usize,
76    policy: EvictionPolicy,
77    stats: CacheStatistics,
78    default_ttl: Option<Duration>,
79}
80
81/// Rule-specific caching system
82#[derive(Debug)]
83pub struct RuleCache {
84    rule_results: Arc<RwLock<SmartCache<RuleCacheKey, Vec<RuleAtom>>>>,
85    derivation_results: Arc<RwLock<SmartCache<DerivationCacheKey, bool>>>,
86    unification_cache: Arc<RwLock<SmartCache<String, HashMap<String, String>>>>,
87    pattern_cache: Arc<RwLock<SmartCache<String, Vec<RuleAtom>>>>,
88    enabled: bool,
89}
90
91impl<K, V> SmartCache<K, V>
92where
93    K: Clone + Eq + Hash,
94    V: Clone,
95{
96    /// Create a new smart cache with specified capacity and policy
97    pub fn new(max_size: usize, policy: EvictionPolicy) -> Self {
98        Self {
99            entries: HashMap::new(),
100            access_order: VecDeque::new(),
101            max_size,
102            policy,
103            stats: CacheStatistics::default(),
104            default_ttl: None,
105        }
106    }
107
108    /// Create cache with time-to-live
109    pub fn with_ttl(max_size: usize, policy: EvictionPolicy, ttl: Duration) -> Self {
110        let mut cache = Self::new(max_size, policy);
111        cache.default_ttl = Some(ttl);
112        cache
113    }
114
115    /// Get value from cache
116    pub fn get(&mut self, key: &K) -> Option<V> {
117        let now = Instant::now();
118
119        // Check if entry exists and get value without holding the mutable borrow
120        let result = if let Some(entry) = self.entries.get(key) {
121            // Check TTL expiration
122            if let Some(ttl) = entry.ttl {
123                if now.duration_since(entry.timestamp) > ttl {
124                    None // Will be removed below
125                } else {
126                    Some(entry.value.clone())
127                }
128            } else {
129                Some(entry.value.clone())
130            }
131        } else {
132            None
133        };
134
135        match result {
136            Some(value) => {
137                // Update access statistics - now we can safely get mutable access
138                if let Some(entry) = self.entries.get_mut(key) {
139                    entry.access_count += 1;
140                    entry.last_access = now;
141                }
142
143                // Update access order for LRU
144                self.update_access_order(key);
145
146                self.stats.hits += 1;
147                self.update_hit_rate();
148
149                Some(value)
150            }
151            None => {
152                // Check if we need to remove expired entry
153                if self.entries.contains_key(key) {
154                    self.entries.remove(key);
155                    self.remove_from_access_order(key);
156                }
157
158                self.stats.misses += 1;
159                self.update_hit_rate();
160                None
161            }
162        }
163    }
164
165    /// Insert value into cache
166    pub fn insert(&mut self, key: K, value: V) {
167        self.insert_with_ttl(key, value, self.default_ttl);
168    }
169
170    /// Insert value with custom TTL
171    pub fn insert_with_ttl(&mut self, key: K, value: V, ttl: Option<Duration>) {
172        let now = Instant::now();
173
174        // Check if we need to evict
175        if self.entries.len() >= self.max_size && !self.entries.contains_key(&key) {
176            self.evict();
177        }
178
179        let entry = CacheEntry {
180            value,
181            timestamp: now,
182            access_count: 1,
183            last_access: now,
184            ttl,
185        };
186
187        // If key already exists, remove from access order
188        if self.entries.contains_key(&key) {
189            self.remove_from_access_order(&key);
190        } else {
191            self.stats.insertions += 1;
192        }
193
194        self.entries.insert(key.clone(), entry);
195        self.access_order.push_back(key);
196
197        self.update_memory_usage();
198    }
199
200    /// Remove entry from cache
201    pub fn remove(&mut self, key: &K) -> Option<V> {
202        match self.entries.remove(key) {
203            Some(entry) => {
204                self.remove_from_access_order(key);
205                self.update_memory_usage();
206                Some(entry.value)
207            }
208            _ => None,
209        }
210    }
211
212    /// Clear all entries
213    pub fn clear(&mut self) {
214        self.entries.clear();
215        self.access_order.clear();
216        self.stats = CacheStatistics::default();
217        self.stats.max_size = self.max_size;
218    }
219
220    /// Get cache statistics
221    pub fn stats(&self) -> &CacheStatistics {
222        &self.stats
223    }
224
225    /// Clean expired entries
226    pub fn cleanup_expired(&mut self) {
227        let now = Instant::now();
228        let mut expired_keys = Vec::new();
229
230        for (key, entry) in &self.entries {
231            if let Some(ttl) = entry.ttl {
232                if now.duration_since(entry.timestamp) > ttl {
233                    expired_keys.push(key.clone());
234                }
235            }
236        }
237
238        for key in expired_keys {
239            self.remove(&key);
240        }
241    }
242
243    /// Evict entries based on policy
244    fn evict(&mut self) {
245        if self.entries.is_empty() {
246            return;
247        }
248
249        let key_to_evict = match self.policy {
250            EvictionPolicy::LRU => self.access_order.front().cloned(),
251            EvictionPolicy::LFU => self.find_least_frequently_used(),
252            EvictionPolicy::FIFO => self.access_order.front().cloned(),
253            EvictionPolicy::TTL => self.find_expired_entry(),
254            EvictionPolicy::Adaptive => self.adaptive_eviction(),
255        };
256
257        if let Some(key) = key_to_evict {
258            self.remove(&key);
259            self.stats.evictions += 1;
260        }
261    }
262
263    fn find_least_frequently_used(&self) -> Option<K> {
264        self.entries
265            .iter()
266            .min_by_key(|(_, entry)| entry.access_count)
267            .map(|(key, _)| key.clone())
268    }
269
270    fn find_expired_entry(&self) -> Option<K> {
271        let now = Instant::now();
272        for (key, entry) in &self.entries {
273            if let Some(ttl) = entry.ttl {
274                if now.duration_since(entry.timestamp) > ttl {
275                    return Some(key.clone());
276                }
277            }
278        }
279        // If no expired entries, fall back to LRU
280        self.access_order.front().cloned()
281    }
282
283    fn adaptive_eviction(&self) -> Option<K> {
284        // Adaptive strategy: consider both frequency and recency
285        let now = Instant::now();
286        let mut best_score = f64::INFINITY;
287        let mut best_key = None;
288
289        for (key, entry) in &self.entries {
290            let recency_score = now.duration_since(entry.last_access).as_secs_f64();
291            let frequency_score = 1.0 / (entry.access_count as f64 + 1.0);
292            let combined_score = recency_score * frequency_score;
293
294            if combined_score < best_score {
295                best_score = combined_score;
296                best_key = Some(key.clone());
297            }
298        }
299
300        best_key
301    }
302
303    fn update_access_order(&mut self, key: &K) {
304        self.remove_from_access_order(key);
305        self.access_order.push_back(key.clone());
306    }
307
308    fn remove_from_access_order(&mut self, key: &K) {
309        if let Some(pos) = self.access_order.iter().position(|x| x == key) {
310            self.access_order.remove(pos);
311        }
312    }
313
314    fn update_hit_rate(&mut self) {
315        let total_requests = self.stats.hits + self.stats.misses;
316        if total_requests > 0 {
317            self.stats.hit_rate = self.stats.hits as f64 / total_requests as f64;
318        }
319    }
320
321    fn update_memory_usage(&mut self) {
322        self.stats.memory_usage = self.entries.len() * 128; // Rough estimate
323        self.stats.cache_size = self.entries.len();
324    }
325}
326
327impl RuleCache {
328    /// Create a new rule cache system
329    pub fn new() -> Self {
330        Self {
331            rule_results: Arc::new(RwLock::new(SmartCache::new(1000, EvictionPolicy::Adaptive))),
332            derivation_results: Arc::new(RwLock::new(SmartCache::new(500, EvictionPolicy::LRU))),
333            unification_cache: Arc::new(RwLock::new(SmartCache::new(200, EvictionPolicy::LFU))),
334            pattern_cache: Arc::new(RwLock::new(SmartCache::with_ttl(
335                300,
336                EvictionPolicy::TTL,
337                Duration::from_secs(300),
338            ))),
339            enabled: true,
340        }
341    }
342
343    /// Create cache with custom sizes
344    pub fn with_sizes(
345        rule_cache_size: usize,
346        derivation_cache_size: usize,
347        unification_cache_size: usize,
348        pattern_cache_size: usize,
349    ) -> Self {
350        Self {
351            rule_results: Arc::new(RwLock::new(SmartCache::new(
352                rule_cache_size,
353                EvictionPolicy::Adaptive,
354            ))),
355            derivation_results: Arc::new(RwLock::new(SmartCache::new(
356                derivation_cache_size,
357                EvictionPolicy::LRU,
358            ))),
359            unification_cache: Arc::new(RwLock::new(SmartCache::new(
360                unification_cache_size,
361                EvictionPolicy::LFU,
362            ))),
363            pattern_cache: Arc::new(RwLock::new(SmartCache::with_ttl(
364                pattern_cache_size,
365                EvictionPolicy::TTL,
366                Duration::from_secs(300),
367            ))),
368            enabled: true,
369        }
370    }
371
372    /// Enable or disable caching
373    pub fn set_enabled(&mut self, enabled: bool) {
374        self.enabled = enabled;
375    }
376
377    /// Cache rule execution result
378    pub fn cache_rule_result(
379        &self,
380        rule_name: &str,
381        input_facts: &[RuleAtom],
382        result: Vec<RuleAtom>,
383    ) {
384        if !self.enabled {
385            return;
386        }
387
388        let key = RuleCacheKey {
389            rule_name: rule_name.to_string(),
390            input_facts: input_facts.to_vec(),
391        };
392
393        if let Ok(mut cache) = self.rule_results.write() {
394            cache.insert(key, result);
395        }
396    }
397
398    /// Get cached rule result
399    pub fn get_rule_result(
400        &self,
401        rule_name: &str,
402        input_facts: &[RuleAtom],
403    ) -> Option<Vec<RuleAtom>> {
404        if !self.enabled {
405            return None;
406        }
407
408        let key = RuleCacheKey {
409            rule_name: rule_name.to_string(),
410            input_facts: input_facts.to_vec(),
411        };
412
413        match self.rule_results.write() {
414            Ok(mut cache) => cache.get(&key),
415            _ => None,
416        }
417    }
418
419    /// Cache derivation result
420    pub fn cache_derivation(&self, goal: &RuleAtom, context: &[RuleAtom], result: bool) {
421        if !self.enabled {
422            return;
423        }
424
425        let key = DerivationCacheKey {
426            goal_fact: goal.clone(),
427            context_facts: context.to_vec(),
428        };
429
430        if let Ok(mut cache) = self.derivation_results.write() {
431            cache.insert(key, result);
432        }
433    }
434
435    /// Get cached derivation result
436    pub fn get_derivation(&self, goal: &RuleAtom, context: &[RuleAtom]) -> Option<bool> {
437        if !self.enabled {
438            return None;
439        }
440
441        let key = DerivationCacheKey {
442            goal_fact: goal.clone(),
443            context_facts: context.to_vec(),
444        };
445
446        match self.derivation_results.write() {
447            Ok(mut cache) => cache.get(&key),
448            _ => None,
449        }
450    }
451
452    /// Cache unification result
453    pub fn cache_unification(&self, pattern: &str, bindings: HashMap<String, String>) {
454        if !self.enabled {
455            return;
456        }
457
458        if let Ok(mut cache) = self.unification_cache.write() {
459            cache.insert(pattern.to_string(), bindings);
460        }
461    }
462
463    /// Get cached unification result
464    pub fn get_unification(&self, pattern: &str) -> Option<HashMap<String, String>> {
465        if !self.enabled {
466            return None;
467        }
468
469        match self.unification_cache.write() {
470            Ok(mut cache) => cache.get(&pattern.to_string()),
471            _ => None,
472        }
473    }
474
475    /// Cache pattern matching result
476    pub fn cache_pattern(&self, pattern: &str, matches: Vec<RuleAtom>) {
477        if !self.enabled {
478            return;
479        }
480
481        if let Ok(mut cache) = self.pattern_cache.write() {
482            cache.insert(pattern.to_string(), matches);
483        }
484    }
485
486    /// Get cached pattern result
487    pub fn get_pattern(&self, pattern: &str) -> Option<Vec<RuleAtom>> {
488        if !self.enabled {
489            return None;
490        }
491
492        match self.pattern_cache.write() {
493            Ok(mut cache) => cache.get(&pattern.to_string()),
494            _ => None,
495        }
496    }
497
498    /// Clear all caches
499    pub fn clear_all(&self) {
500        if let Ok(mut cache) = self.rule_results.write() {
501            cache.clear();
502        }
503        if let Ok(mut cache) = self.derivation_results.write() {
504            cache.clear();
505        }
506        if let Ok(mut cache) = self.unification_cache.write() {
507            cache.clear();
508        }
509        if let Ok(mut cache) = self.pattern_cache.write() {
510            cache.clear();
511        }
512    }
513
514    /// Clean expired entries from all caches
515    pub fn cleanup_expired(&self) {
516        if let Ok(mut cache) = self.rule_results.write() {
517            cache.cleanup_expired();
518        }
519        if let Ok(mut cache) = self.derivation_results.write() {
520            cache.cleanup_expired();
521        }
522        if let Ok(mut cache) = self.unification_cache.write() {
523            cache.cleanup_expired();
524        }
525        if let Ok(mut cache) = self.pattern_cache.write() {
526            cache.cleanup_expired();
527        }
528    }
529
530    /// Get combined cache statistics
531    pub fn get_statistics(&self) -> CachingStatistics {
532        let rule_stats = match self.rule_results.read() {
533            Ok(cache) => cache.stats().clone(),
534            _ => CacheStatistics::default(),
535        };
536
537        let derivation_stats = match self.derivation_results.read() {
538            Ok(cache) => cache.stats().clone(),
539            _ => CacheStatistics::default(),
540        };
541
542        let unification_stats = match self.unification_cache.read() {
543            Ok(cache) => cache.stats().clone(),
544            _ => CacheStatistics::default(),
545        };
546
547        let pattern_stats = match self.pattern_cache.read() {
548            Ok(cache) => cache.stats().clone(),
549            _ => CacheStatistics::default(),
550        };
551
552        CachingStatistics {
553            rule_cache: rule_stats,
554            derivation_cache: derivation_stats,
555            unification_cache: unification_stats,
556            pattern_cache: pattern_stats,
557            enabled: self.enabled,
558        }
559    }
560
561    /// Warm cache with common patterns
562    pub fn warm_cache(&self, rules: &[Rule], common_facts: &[RuleAtom]) {
563        if !self.enabled {
564            return;
565        }
566
567        // Pre-populate pattern cache with rule patterns
568        for rule in rules {
569            for atom in &rule.body {
570                let pattern = format!("{atom:?}");
571                self.cache_pattern(&pattern, vec![atom.clone()]);
572            }
573        }
574
575        // Pre-populate with common fact patterns
576        for fact in common_facts {
577            let pattern = format!("{fact:?}");
578            self.cache_pattern(&pattern, vec![fact.clone()]);
579        }
580    }
581}
582
583/// Combined caching statistics
584#[derive(Debug, Clone)]
585pub struct CachingStatistics {
586    pub rule_cache: CacheStatistics,
587    pub derivation_cache: CacheStatistics,
588    pub unification_cache: CacheStatistics,
589    pub pattern_cache: CacheStatistics,
590    pub enabled: bool,
591}
592
593impl Default for CacheStatistics {
594    fn default() -> Self {
595        Self {
596            hits: 0,
597            misses: 0,
598            evictions: 0,
599            insertions: 0,
600            memory_usage: 0,
601            hit_rate: 0.0,
602            average_access_time: Duration::from_nanos(0),
603            cache_size: 0,
604            max_size: 0,
605        }
606    }
607}
608
609impl Default for RuleCache {
610    fn default() -> Self {
611        Self::new()
612    }
613}
614
615#[cfg(test)]
616mod tests {
617    use super::*;
618    use crate::Term;
619
620    #[test]
621    fn test_smart_cache_basic_operations() {
622        let mut cache = SmartCache::new(3, EvictionPolicy::LRU);
623
624        cache.insert("key1".to_string(), "value1".to_string());
625        cache.insert("key2".to_string(), "value2".to_string());
626
627        assert_eq!(cache.get(&"key1".to_string()), Some("value1".to_string()));
628        assert_eq!(cache.get(&"key2".to_string()), Some("value2".to_string()));
629        assert_eq!(cache.get(&"key3".to_string()), None);
630    }
631
632    #[test]
633    fn test_cache_eviction() {
634        let mut cache = SmartCache::new(2, EvictionPolicy::LRU);
635
636        cache.insert("key1", "value1");
637        cache.insert("key2", "value2");
638        cache.insert("key3", "value3"); // Should evict key1
639
640        assert_eq!(cache.get(&"key1"), None);
641        assert_eq!(cache.get(&"key2"), Some("value2"));
642        assert_eq!(cache.get(&"key3"), Some("value3"));
643    }
644
645    #[test]
646    fn test_ttl_expiration() {
647        let mut cache = SmartCache::with_ttl(5, EvictionPolicy::TTL, Duration::from_millis(10));
648
649        cache.insert("key1", "value1");
650        assert_eq!(cache.get(&"key1"), Some("value1"));
651
652        std::thread::sleep(Duration::from_millis(15));
653        assert_eq!(cache.get(&"key1"), None);
654    }
655
656    #[test]
657    fn test_rule_cache_operations() {
658        let cache = RuleCache::new();
659
660        let rule_name = "test_rule";
661        let input_facts = vec![RuleAtom::Triple {
662            subject: Term::Constant("test".to_string()),
663            predicate: Term::Constant("type".to_string()),
664            object: Term::Constant("entity".to_string()),
665        }];
666        let result = vec![RuleAtom::Triple {
667            subject: Term::Constant("test".to_string()),
668            predicate: Term::Constant("derived".to_string()),
669            object: Term::Constant("property".to_string()),
670        }];
671
672        // Cache and retrieve
673        cache.cache_rule_result(rule_name, &input_facts, result.clone());
674        let cached_result = cache.get_rule_result(rule_name, &input_facts);
675
676        assert_eq!(cached_result, Some(result));
677    }
678
679    #[test]
680    fn test_cache_statistics() {
681        let mut cache = SmartCache::new(10, EvictionPolicy::LRU);
682
683        // Generate some activity
684        cache.insert("key1", "value1");
685        cache.insert("key2", "value2");
686        cache.get(&"key1");
687        cache.get(&"key3"); // Miss
688
689        let stats = cache.stats();
690        assert_eq!(stats.hits, 1);
691        assert_eq!(stats.misses, 1);
692        assert_eq!(stats.insertions, 2);
693        assert!(stats.hit_rate > 0.0);
694    }
695
696    #[test]
697    fn test_cache_warm_up() {
698        let cache = RuleCache::new();
699
700        let rules = vec![Rule {
701            name: "test_rule".to_string(),
702            body: vec![RuleAtom::Triple {
703                subject: Term::Variable("X".to_string()),
704                predicate: Term::Constant("type".to_string()),
705                object: Term::Constant("test".to_string()),
706            }],
707            head: vec![RuleAtom::Triple {
708                subject: Term::Variable("X".to_string()),
709                predicate: Term::Constant("derived".to_string()),
710                object: Term::Constant("property".to_string()),
711            }],
712        }];
713
714        let facts = vec![RuleAtom::Triple {
715            subject: Term::Constant("entity1".to_string()),
716            predicate: Term::Constant("type".to_string()),
717            object: Term::Constant("test".to_string()),
718        }];
719
720        cache.warm_cache(&rules, &facts);
721
722        // Should have populated pattern cache
723        let stats = cache.get_statistics();
724        assert!(stats.pattern_cache.cache_size > 0);
725    }
726}