Skip to main content

csm_memory/
singularity_cache.rs

1use 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    /// Reset all counters to zero.
84    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// ============================================================================
92// TESTS
93// ============================================================================
94
95#[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        // Cache is at capacity
116        assert_eq!(cache.results.len(), 3);
117
118        // Inserting 4 should evict 1 (oldest)
119        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        // Get key 1 - moves it to most-recently-used position
135        cache.get(1);
136
137        // Now insert 4 - should evict 2 (not 1)
138        cache.put(4, make_results(&["d"]));
139        assert!(cache.results.contains_key(&1)); // 1 should still be there
140        assert!(!cache.results.contains_key(&2)); // 2 should be evicted
141    }
142
143    #[test]
144    fn multiple_evictions_under_pressure() {
145        let mut cache = QueryCache::with_capacity(2);
146
147        // Insert 10 items, should cause 8 evictions
148        for i in 0..10 {
149            cache.put(i, make_results(&[&format!("item-{i}")]));
150        }
151
152        assert_eq!(cache.results.len(), 2);
153        // Only 8 and 9 should remain
154        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        // Every insert should evict
166        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"])); // Update same key
204
205        assert!(!evicted); // No eviction for update
206        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}