Skip to main content

citadel_buffer/
sieve.rs

1use std::collections::HashMap;
2
3/// SIEVE eviction cache.
4///
5/// Single FIFO queue with a moving eviction hand and a visited bit.
6/// Dirty entries are pinned and never evictable.
7///
8/// O(1) amortized eviction.
9pub struct SieveCache<V> {
10    entries: Vec<SieveEntry<V>>,
11    /// Maps key -> index in entries vec.
12    index: HashMap<u64, usize>,
13    /// Moving eviction pointer.
14    hand: usize,
15    /// Number of occupied slots.
16    len: usize,
17    capacity: usize,
18}
19
20struct SieveEntry<V> {
21    key: u64,
22    value: V,
23    visited: bool,
24    dirty: bool,
25    occupied: bool,
26}
27
28impl<V> SieveEntry<V> {
29    fn empty(value: V) -> Self {
30        Self {
31            key: 0,
32            value,
33            visited: false,
34            dirty: false,
35            occupied: false,
36        }
37    }
38}
39
40impl<V: Default> SieveCache<V> {
41    pub fn new(capacity: usize) -> Self {
42        assert!(capacity > 0, "cache capacity must be > 0");
43        let mut entries = Vec::with_capacity(capacity);
44        for _ in 0..capacity {
45            entries.push(SieveEntry::empty(V::default()));
46        }
47        Self {
48            entries,
49            index: HashMap::with_capacity(capacity),
50            hand: 0,
51            len: 0,
52            capacity,
53        }
54    }
55
56    pub fn get(&mut self, key: u64) -> Option<&V> {
57        if let Some(&idx) = self.index.get(&key) {
58            self.entries[idx].visited = true;
59            Some(&self.entries[idx].value)
60        } else {
61            None
62        }
63    }
64
65    pub fn get_mut(&mut self, key: u64) -> Option<&mut V> {
66        if let Some(&idx) = self.index.get(&key) {
67            self.entries[idx].visited = true;
68            Some(&mut self.entries[idx].value)
69        } else {
70            None
71        }
72    }
73
74    pub fn contains(&self, key: u64) -> bool {
75        self.index.contains_key(&key)
76    }
77
78    /// Returns Err if all entries are dirty (pinned) and eviction is impossible.
79    #[allow(clippy::result_unit_err)]
80    pub fn insert(&mut self, key: u64, value: V) -> Result<Option<(u64, V)>, ()> {
81        if let Some(&idx) = self.index.get(&key) {
82            self.entries[idx].value = value;
83            self.entries[idx].visited = true;
84            return Ok(None);
85        }
86
87        if self.len < self.capacity {
88            let idx = self.find_empty_slot();
89            self.entries[idx].key = key;
90            self.entries[idx].value = value;
91            self.entries[idx].visited = true;
92            self.entries[idx].dirty = false;
93            self.entries[idx].occupied = true;
94            self.index.insert(key, idx);
95            self.len += 1;
96            return Ok(None);
97        }
98
99        let evicted = self.evict()?;
100        let idx = self.find_empty_slot();
101        self.entries[idx].key = key;
102        self.entries[idx].value = value;
103        self.entries[idx].visited = true;
104        self.entries[idx].dirty = false;
105        self.entries[idx].occupied = true;
106        self.index.insert(key, idx);
107        self.len += 1;
108
109        Ok(Some(evicted))
110    }
111
112    fn evict(&mut self) -> Result<(u64, V), ()> {
113        let mut scanned = 0;
114
115        loop {
116            if scanned >= self.capacity * 2 {
117                // All entries are dirty - can't evict
118                return Err(());
119            }
120
121            let idx = self.hand;
122            self.hand = (self.hand + 1) % self.capacity;
123            scanned += 1;
124
125            if !self.entries[idx].occupied {
126                continue;
127            }
128
129            if self.entries[idx].dirty {
130                continue;
131            }
132
133            if self.entries[idx].visited {
134                self.entries[idx].visited = false;
135                continue;
136            }
137
138            let evicted_key = self.entries[idx].key;
139            let evicted_value = std::mem::take(&mut self.entries[idx].value);
140            self.entries[idx].occupied = false;
141            self.index.remove(&evicted_key);
142            self.len -= 1;
143
144            return Ok((evicted_key, evicted_value));
145        }
146    }
147
148    fn find_empty_slot(&self) -> usize {
149        for (i, entry) in self.entries.iter().enumerate() {
150            if !entry.occupied {
151                return i;
152            }
153        }
154        unreachable!("find_empty_slot called when cache is full");
155    }
156
157    pub fn set_dirty(&mut self, key: u64) {
158        if let Some(&idx) = self.index.get(&key) {
159            self.entries[idx].dirty = true;
160        }
161    }
162
163    pub fn clear_dirty(&mut self, key: u64) {
164        if let Some(&idx) = self.index.get(&key) {
165            self.entries[idx].dirty = false;
166        }
167    }
168
169    pub fn is_dirty(&self, key: u64) -> bool {
170        self.index
171            .get(&key)
172            .map(|&idx| self.entries[idx].dirty)
173            .unwrap_or(false)
174    }
175
176    pub fn dirty_entries(&self) -> impl Iterator<Item = (u64, &V)> {
177        self.entries
178            .iter()
179            .filter(|e| e.occupied && e.dirty)
180            .map(|e| (e.key, &e.value))
181    }
182
183    pub fn dirty_entries_mut(&mut self) -> impl Iterator<Item = (u64, &mut V)> {
184        self.entries
185            .iter_mut()
186            .filter(|e| e.occupied && e.dirty)
187            .map(|e| (e.key, &mut e.value))
188    }
189
190    pub fn clear_all_dirty(&mut self) {
191        for entry in &mut self.entries {
192            if entry.occupied {
193                entry.dirty = false;
194            }
195        }
196    }
197
198    pub fn remove(&mut self, key: u64) -> Option<V> {
199        if let Some(idx) = self.index.remove(&key) {
200            let value = std::mem::take(&mut self.entries[idx].value);
201            self.entries[idx].occupied = false;
202            self.len -= 1;
203            Some(value)
204        } else {
205            None
206        }
207    }
208
209    pub fn len(&self) -> usize {
210        self.len
211    }
212
213    pub fn is_empty(&self) -> bool {
214        self.len == 0
215    }
216
217    pub fn capacity(&self) -> usize {
218        self.capacity
219    }
220
221    pub fn dirty_count(&self) -> usize {
222        self.entries
223            .iter()
224            .filter(|e| e.occupied && e.dirty)
225            .count()
226    }
227
228    pub fn clear(&mut self) {
229        for entry in &mut self.entries {
230            entry.occupied = false;
231            entry.visited = false;
232            entry.dirty = false;
233        }
234        self.index.clear();
235        self.len = 0;
236        self.hand = 0;
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    #[test]
245    fn basic_insert_and_get() {
246        let mut cache = SieveCache::<u32>::new(4);
247        cache.insert(1, 100).unwrap();
248        cache.insert(2, 200).unwrap();
249
250        assert_eq!(cache.get(1), Some(&100));
251        assert_eq!(cache.get(2), Some(&200));
252        assert_eq!(cache.get(3), None);
253    }
254
255    #[test]
256    fn eviction_when_full() {
257        let mut cache = SieveCache::<u32>::new(3);
258        cache.insert(1, 100).unwrap();
259        cache.insert(2, 200).unwrap();
260        cache.insert(3, 300).unwrap();
261        assert_eq!(cache.len(), 3);
262
263        // Don't access any - all are unvisited, one should be evicted
264        // Reset visited flags by clearing them
265        for entry in &mut cache.entries {
266            entry.visited = false;
267        }
268
269        let result = cache.insert(4, 400).unwrap();
270        assert!(result.is_some());
271        assert_eq!(cache.len(), 3);
272    }
273
274    #[test]
275    fn visited_entries_survive_eviction() {
276        let mut cache = SieveCache::<u32>::new(3);
277        cache.insert(1, 100).unwrap();
278        cache.insert(2, 200).unwrap();
279        cache.insert(3, 300).unwrap();
280
281        // Reset all visited flags
282        for entry in &mut cache.entries {
283            entry.visited = false;
284        }
285
286        // Access entry 2 (marks visited)
287        cache.get(2);
288
289        // Insert 4 - should evict 1 or 3 (not 2)
290        let evicted = cache.insert(4, 400).unwrap().unwrap();
291        assert_ne!(evicted.0, 2, "visited entry should not be evicted");
292        assert!(cache.contains(2));
293        assert!(cache.contains(4));
294    }
295
296    #[test]
297    fn dirty_entries_not_evicted() {
298        let mut cache = SieveCache::<u32>::new(2);
299        cache.insert(1, 100).unwrap();
300        cache.insert(2, 200).unwrap();
301
302        // Reset visited
303        for entry in &mut cache.entries {
304            entry.visited = false;
305        }
306
307        // Mark entry 1 dirty
308        cache.set_dirty(1);
309
310        // Insert 3 - must evict 2 (not dirty 1)
311        let evicted = cache.insert(3, 300).unwrap().unwrap();
312        assert_eq!(evicted.0, 2);
313        assert!(cache.contains(1));
314        assert!(cache.contains(3));
315    }
316
317    #[test]
318    fn all_dirty_returns_err() {
319        let mut cache = SieveCache::<u32>::new(2);
320        cache.insert(1, 100).unwrap();
321        cache.insert(2, 200).unwrap();
322
323        cache.set_dirty(1);
324        cache.set_dirty(2);
325
326        // Reset visited
327        for entry in &mut cache.entries {
328            entry.visited = false;
329        }
330
331        let result = cache.insert(3, 300);
332        assert!(result.is_err());
333    }
334
335    #[test]
336    fn clear_dirty() {
337        let mut cache = SieveCache::<u32>::new(2);
338        cache.insert(1, 100).unwrap();
339        cache.set_dirty(1);
340        assert!(cache.is_dirty(1));
341
342        cache.clear_dirty(1);
343        assert!(!cache.is_dirty(1));
344    }
345
346    #[test]
347    fn dirty_entries_iterator() {
348        let mut cache = SieveCache::<u32>::new(4);
349        cache.insert(1, 100).unwrap();
350        cache.insert(2, 200).unwrap();
351        cache.insert(3, 300).unwrap();
352
353        cache.set_dirty(1);
354        cache.set_dirty(3);
355
356        let dirty: Vec<_> = cache.dirty_entries().collect();
357        assert_eq!(dirty.len(), 2);
358        assert_eq!(cache.dirty_count(), 2);
359    }
360
361    #[test]
362    fn clear_all_dirty() {
363        let mut cache = SieveCache::<u32>::new(3);
364        cache.insert(1, 100).unwrap();
365        cache.insert(2, 200).unwrap();
366        cache.set_dirty(1);
367        cache.set_dirty(2);
368
369        cache.clear_all_dirty();
370        assert_eq!(cache.dirty_count(), 0);
371    }
372
373    #[test]
374    fn remove_entry() {
375        let mut cache = SieveCache::<u32>::new(4);
376        cache.insert(1, 100).unwrap();
377        cache.insert(2, 200).unwrap();
378
379        let removed = cache.remove(1);
380        assert_eq!(removed, Some(100));
381        assert!(!cache.contains(1));
382        assert_eq!(cache.len(), 1);
383    }
384
385    #[test]
386    fn update_existing_key() {
387        let mut cache = SieveCache::<u32>::new(4);
388        cache.insert(1, 100).unwrap();
389        cache.insert(1, 200).unwrap();
390
391        assert_eq!(cache.get(1), Some(&200));
392        assert_eq!(cache.len(), 1);
393    }
394}