strixonomy_reasoner/
cache.rs1use crate::adapter::ReasonerId;
2use crate::input::ReasonerInput;
3use crate::result::{ClassificationResult, ReasonerSnapshot};
4use std::collections::HashMap;
5
6const 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 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 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 pub fn update_snapshot(
104 &mut self,
105 content_hash: &str,
106 profile: ReasonerId,
107 snapshot: ReasonerSnapshot,
108 ) -> bool {
109 let key = (content_hash.to_string(), profile.as_str().to_string());
110 if let Some(entry) = self.entries.get_mut(&key) {
111 entry.classification.consistent = snapshot.consistent;
112 entry.snapshot = snapshot;
113 true
114 } else {
115 false
116 }
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::input::ReasonerInput;
124 use crate::result::{ClassificationResult, InferredHierarchy};
125 use ontologos_core::Ontology;
126 use std::collections::HashMap;
127 use std::path::PathBuf;
128 use strixonomy_catalog::ClassHierarchy;
129
130 fn dummy_input(hash: &str) -> ReasonerInput {
131 ReasonerInput {
132 workspace: PathBuf::from("/tmp"),
133 content_hash: hash.to_string(),
134 ontology: Ontology::new(),
135 asserted_hierarchy: ClassHierarchy::default(),
136 document_overrides: HashMap::new(),
137 }
138 }
139
140 fn dummy_classification(profile: &str) -> ClassificationResult {
141 ClassificationResult {
142 profile_used: profile.to_string(),
143 consistent: true,
144 unsatisfiable: Vec::new(),
145 inferred: InferredHierarchy {
146 edges: Vec::new(),
147 unsatisfiable: Vec::new(),
148 combined: ClassHierarchy::default(),
149 },
150 new_inferences: Vec::new(),
151 warnings: Vec::new(),
152 duration_ms: 0,
153 subsumption_count: 0,
154 inferred_axiom_count: 0,
155 equivalences: Vec::new(),
156 }
157 }
158
159 #[test]
160 fn one_entry_per_profile() {
161 let mut store = ReasonerCacheStore::new();
162 store.store_classification(dummy_input("a"), ReasonerId::El, dummy_classification("el"));
163 store.store_classification(dummy_input("b"), ReasonerId::El, dummy_classification("el"));
164 assert_eq!(store.len(), 1);
165 assert!(store.get("b", ReasonerId::El).is_some());
166 assert!(store.get("a", ReasonerId::El).is_none());
167 }
168
169 #[test]
170 fn update_snapshot_preserves_realization() {
171 let mut store = ReasonerCacheStore::new();
172 let mut snapshot = store.store_classification(
173 dummy_input("h"),
174 ReasonerId::Dl,
175 dummy_classification("dl"),
176 );
177 assert!(snapshot.realization.is_none());
178 snapshot.realization = Some(crate::result::RealizationResult {
179 profile_used: "dl".into(),
180 individuals: Vec::new(),
181 duration_ms: 1,
182 truncated: false,
183 entailment_errors: 0,
184 });
185 assert!(store.update_snapshot("h", ReasonerId::Dl, snapshot.clone()));
186 let cached = store.get("h", ReasonerId::Dl).expect("cached");
187 assert!(cached.snapshot.realization.is_some());
188 }
189}