Skip to main content

ontocore_reasoner/
cache.rs

1use crate::adapter::ReasonerId;
2use crate::input::ReasonerInput;
3use crate::result::{ClassificationResult, ReasonerSnapshot};
4use std::collections::HashMap;
5
6/// Keep at most one classification entry per profile (plus a small global cap).
7const MAX_CACHE_ENTRIES: usize = 8;
8
9#[derive(Debug, Clone)]
10pub struct ReasonerCache {
11    pub content_hash: String,
12    pub profile: ReasonerId,
13    pub snapshot: ReasonerSnapshot,
14    pub classification: ClassificationResult,
15    pub input: ReasonerInput,
16}
17
18#[derive(Debug, Default)]
19pub struct ReasonerCacheStore {
20    entries: HashMap<(String, String), ReasonerCache>,
21    /// Insertion order for eviction (oldest first).
22    order: Vec<(String, String)>,
23}
24
25impl ReasonerCacheStore {
26    pub fn new() -> Self {
27        Self::default()
28    }
29
30    pub fn get(&self, content_hash: &str, profile: ReasonerId) -> Option<&ReasonerCache> {
31        self.entries.get(&(content_hash.to_string(), profile.as_str().to_string()))
32    }
33
34    pub fn get_any_for_profile(&self, profile: ReasonerId) -> Option<&ReasonerCache> {
35        self.entries.values().find(|e| e.profile == profile)
36    }
37
38    pub fn insert(&mut self, cache: ReasonerCache) {
39        let profile_key = cache.profile.as_str().to_string();
40        let key = (cache.content_hash.clone(), profile_key.clone());
41
42        // One entry per profile: drop any prior entry for the same profile.
43        let stale: Vec<_> =
44            self.entries.keys().filter(|(_, p)| p == &profile_key).cloned().collect();
45        for stale_key in stale {
46            self.entries.remove(&stale_key);
47            self.order.retain(|k| k != &stale_key);
48        }
49
50        if self.entries.insert(key.clone(), cache).is_none() {
51            self.order.push(key);
52        } else if let Some(pos) = self.order.iter().position(|k| k == &key) {
53            let k = self.order.remove(pos);
54            self.order.push(k);
55        }
56
57        while self.entries.len() > MAX_CACHE_ENTRIES {
58            if let Some(oldest) = self.order.first().cloned() {
59                self.entries.remove(&oldest);
60                self.order.remove(0);
61            } else {
62                break;
63            }
64        }
65    }
66
67    pub fn invalidate(&mut self) {
68        self.entries.clear();
69        self.order.clear();
70    }
71
72    pub fn latest_snapshot(&self) -> Option<&ReasonerSnapshot> {
73        self.entries.values().max_by_key(|e| e.snapshot.classified_at).map(|e| &e.snapshot)
74    }
75
76    pub fn len(&self) -> usize {
77        self.entries.len()
78    }
79
80    pub fn is_empty(&self) -> bool {
81        self.entries.is_empty()
82    }
83
84    pub fn store_classification(
85        &mut self,
86        input: ReasonerInput,
87        profile: ReasonerId,
88        classification: ClassificationResult,
89    ) -> ReasonerSnapshot {
90        let snapshot = ReasonerSnapshot::from(classification.clone());
91        let cache = ReasonerCache {
92            content_hash: input.content_hash.clone(),
93            profile,
94            snapshot: snapshot.clone(),
95            classification,
96            input,
97        };
98        self.insert(cache);
99        snapshot
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use crate::input::ReasonerInput;
107    use crate::result::{ClassificationResult, InferredHierarchy};
108    use ontocore_catalog::ClassHierarchy;
109    use ontologos_core::Ontology;
110    use std::collections::HashMap;
111    use std::path::PathBuf;
112
113    fn dummy_input(hash: &str) -> ReasonerInput {
114        ReasonerInput {
115            workspace: PathBuf::from("/tmp"),
116            content_hash: hash.to_string(),
117            ontology: Ontology::new(),
118            asserted_hierarchy: ClassHierarchy::default(),
119            document_overrides: HashMap::new(),
120        }
121    }
122
123    fn dummy_classification(profile: &str) -> ClassificationResult {
124        ClassificationResult {
125            profile_used: profile.to_string(),
126            consistent: true,
127            unsatisfiable: Vec::new(),
128            inferred: InferredHierarchy {
129                edges: Vec::new(),
130                unsatisfiable: Vec::new(),
131                combined: ClassHierarchy::default(),
132            },
133            new_inferences: Vec::new(),
134            warnings: Vec::new(),
135            duration_ms: 0,
136            subsumption_count: 0,
137            inferred_axiom_count: 0,
138        }
139    }
140
141    #[test]
142    fn one_entry_per_profile() {
143        let mut store = ReasonerCacheStore::new();
144        store.store_classification(dummy_input("a"), ReasonerId::El, dummy_classification("el"));
145        store.store_classification(dummy_input("b"), ReasonerId::El, dummy_classification("el"));
146        assert_eq!(store.len(), 1);
147        assert!(store.get("b", ReasonerId::El).is_some());
148        assert!(store.get("a", ReasonerId::El).is_none());
149    }
150}