csm_memory/
singularity_cache.rs1use serde::{Deserialize, Serialize};
2use std::collections::hash_map::Entry;
3use std::collections::{HashMap, VecDeque};
4use std::sync::Arc;
5use std::sync::atomic::{AtomicU64, Ordering};
6
7#[derive(Debug, Default)]
8pub(crate) struct QueryCache {
9 pub(crate) capacity: usize,
10 pub(crate) order: VecDeque<u64>,
11 pub(crate) results: HashMap<u64, Arc<[(String, f32)]>>,
12}
13
14impl QueryCache {
15 pub(crate) fn with_capacity(capacity: usize) -> Self {
16 Self {
17 capacity: capacity.max(1),
18 order: VecDeque::new(),
19 results: HashMap::new(),
20 }
21 }
22
23 pub(crate) fn get(&mut self, key: u64) -> Option<Arc<[(String, f32)]>> {
24 let value = Arc::clone(self.results.get(&key)?);
25 if let Some(pos) = self.order.iter().position(|k| *k == key) {
26 self.order.remove(pos);
27 }
28 self.order.push_back(key);
29 Some(value)
30 }
31
32 pub(crate) fn put(&mut self, key: u64, value: Arc<[(String, f32)]>) -> bool {
33 if let Entry::Occupied(mut entry) = self.results.entry(key) {
34 entry.insert(value);
35 if let Some(pos) = self.order.iter().position(|k| *k == key) {
36 self.order.remove(pos);
37 }
38 self.order.push_back(key);
39 return false;
40 }
41
42 let mut evicted = false;
43 if self.results.len() >= self.capacity {
44 if let Some(oldest) = self.order.pop_front() {
45 self.results.remove(&oldest);
46 evicted = true;
47 }
48 }
49 self.order.push_back(key);
50 self.results.insert(key, value);
51 evicted
52 }
53
54 pub(crate) fn clear(&mut self) {
55 self.order.clear();
56 self.results.clear();
57 }
58}
59
60#[derive(Debug, Default)]
61pub struct CacheMetrics {
62 pub hits_total: AtomicU64,
63 pub misses_total: AtomicU64,
64 pub evictions_total: AtomicU64,
65}
66
67#[derive(Debug, Clone, Default, Serialize, Deserialize)]
68pub struct CacheMetricsSnapshot {
69 pub cache_hits_total: u64,
70 pub cache_misses_total: u64,
71 pub cache_evictions_total: u64,
72}
73
74impl CacheMetrics {
75 pub fn snapshot(&self) -> CacheMetricsSnapshot {
76 CacheMetricsSnapshot {
77 cache_hits_total: self.hits_total.load(Ordering::Relaxed),
78 cache_misses_total: self.misses_total.load(Ordering::Relaxed),
79 cache_evictions_total: self.evictions_total.load(Ordering::Relaxed),
80 }
81 }
82
83 pub fn reset(&self) {
85 self.hits_total.store(0, Ordering::Relaxed);
86 self.misses_total.store(0, Ordering::Relaxed);
87 self.evictions_total.store(0, Ordering::Relaxed);
88 }
89}
90
91#[cfg(test)]
96mod tests {
97 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
98 use super::*;
99
100 fn make_results(ids: &[&str]) -> Arc<[(String, f32)]> {
101 ids.iter()
102 .map(|id| (id.to_string(), 0.5))
103 .collect::<Vec<_>>()
104 .into()
105 }
106
107 #[test]
108 fn lru_eviction_at_capacity() {
109 let mut cache = QueryCache::with_capacity(3);
110
111 cache.put(1, make_results(&["a"]));
112 cache.put(2, make_results(&["b"]));
113 cache.put(3, make_results(&["c"]));
114
115 assert_eq!(cache.results.len(), 3);
117
118 let evicted = cache.put(4, make_results(&["d"]));
120 assert!(evicted);
121 assert_eq!(cache.results.len(), 3);
122 assert!(!cache.results.contains_key(&1));
123 assert!(cache.results.contains_key(&4));
124 }
125
126 #[test]
127 fn lru_get_updates_order() {
128 let mut cache = QueryCache::with_capacity(3);
129
130 cache.put(1, make_results(&["a"]));
131 cache.put(2, make_results(&["b"]));
132 cache.put(3, make_results(&["c"]));
133
134 cache.get(1);
136
137 cache.put(4, make_results(&["d"]));
139 assert!(cache.results.contains_key(&1)); assert!(!cache.results.contains_key(&2)); }
142
143 #[test]
144 fn multiple_evictions_under_pressure() {
145 let mut cache = QueryCache::with_capacity(2);
146
147 for i in 0..10 {
149 cache.put(i, make_results(&[&format!("item-{i}")]));
150 }
151
152 assert_eq!(cache.results.len(), 2);
153 assert!(cache.results.contains_key(&8));
155 assert!(cache.results.contains_key(&9));
156 }
157
158 #[test]
159 fn capacity_one_edge_case() {
160 let mut cache = QueryCache::with_capacity(1);
161
162 cache.put(1, make_results(&["a"]));
163 assert_eq!(cache.results.len(), 1);
164
165 let evicted = cache.put(2, make_results(&["b"]));
167 assert!(evicted);
168 assert!(!cache.results.contains_key(&1));
169
170 let evicted = cache.put(3, make_results(&["c"]));
171 assert!(evicted);
172 assert!(!cache.results.contains_key(&2));
173 }
174
175 #[test]
176 fn clear_removes_all_entries() {
177 let mut cache = QueryCache::with_capacity(10);
178
179 for i in 0..5 {
180 cache.put(i, make_results(&[&format!("item-{i}")]));
181 }
182
183 assert_eq!(cache.results.len(), 5);
184 cache.clear();
185 assert_eq!(cache.results.len(), 0);
186 assert_eq!(cache.order.len(), 0);
187 }
188
189 #[test]
190 fn get_returns_none_for_missing_key() {
191 let mut cache = QueryCache::with_capacity(5);
192 cache.put(1, make_results(&["a"]));
193
194 let result = cache.get(999);
195 assert!(result.is_none());
196 }
197
198 #[test]
199 fn put_returns_false_for_update_without_eviction() {
200 let mut cache = QueryCache::with_capacity(5);
201
202 cache.put(1, make_results(&["a"]));
203 let evicted = cache.put(1, make_results(&["b"])); assert!(!evicted); assert_eq!(cache.results.len(), 1);
207 }
208
209 #[test]
210 fn cache_metrics_snapshot() {
211 let metrics = CacheMetrics::default();
212 metrics.hits_total.store(10, Ordering::Relaxed);
213 metrics.misses_total.store(5, Ordering::Relaxed);
214 metrics.evictions_total.store(2, Ordering::Relaxed);
215
216 let snap = metrics.snapshot();
217 assert_eq!(snap.cache_hits_total, 10);
218 assert_eq!(snap.cache_misses_total, 5);
219 assert_eq!(snap.cache_evictions_total, 2);
220 }
221
222 #[test]
223 fn with_capacity_zero_becomes_one() {
224 let cache = QueryCache::with_capacity(0);
225 assert_eq!(cache.capacity, 1);
226 }
227}