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    /// Look up a key. If found, set visited=true and return reference.
57    pub fn get(&mut self, key: u64) -> Option<&V> {
58        if let Some(&idx) = self.index.get(&key) {
59            self.entries[idx].visited = true;
60            Some(&self.entries[idx].value)
61        } else {
62            None
63        }
64    }
65
66    /// Look up a key. If found, set visited=true and return mutable reference.
67    pub fn get_mut(&mut self, key: u64) -> Option<&mut V> {
68        if let Some(&idx) = self.index.get(&key) {
69            self.entries[idx].visited = true;
70            Some(&mut self.entries[idx].value)
71        } else {
72            None
73        }
74    }
75
76    /// Check if a key exists without marking visited.
77    pub fn contains(&self, key: u64) -> bool {
78        self.index.contains_key(&key)
79    }
80
81    /// Insert a value. If cache is full, evict using SIEVE algorithm.
82    /// Returns None if inserted without eviction, or Some((evicted_key, evicted_value))
83    /// if an entry was evicted.
84    ///
85    /// Returns Err(()) if the cache is full and all entries are dirty (pinned).
86    #[allow(clippy::result_unit_err)]
87    pub fn insert(&mut self, key: u64, value: V) -> Result<Option<(u64, V)>, ()> {
88        // If already present, just update
89        if let Some(&idx) = self.index.get(&key) {
90            self.entries[idx].value = value;
91            self.entries[idx].visited = true;
92            return Ok(None);
93        }
94
95        // If cache not full, use next empty slot
96        if self.len < self.capacity {
97            let idx = self.find_empty_slot();
98            self.entries[idx].key = key;
99            self.entries[idx].value = value;
100            self.entries[idx].visited = true;
101            self.entries[idx].dirty = false;
102            self.entries[idx].occupied = true;
103            self.index.insert(key, idx);
104            self.len += 1;
105            return Ok(None);
106        }
107
108        // Cache is full — need to evict using SIEVE
109        let evicted = self.evict()?;
110
111        // Find the slot that was just freed
112        let idx = self.find_empty_slot();
113        self.entries[idx].key = key;
114        self.entries[idx].value = value;
115        self.entries[idx].visited = true;
116        self.entries[idx].dirty = false;
117        self.entries[idx].occupied = true;
118        self.index.insert(key, idx);
119        self.len += 1;
120
121        Ok(Some(evicted))
122    }
123
124    /// Evict one entry using SIEVE algorithm.
125    /// Skips dirty (pinned) entries and visited entries (clears visited on pass).
126    /// Returns the evicted (key, value).
127    fn evict(&mut self) -> Result<(u64, V), ()> {
128        let mut scanned = 0;
129
130        loop {
131            if scanned >= self.capacity * 2 {
132                // All entries are dirty — can't evict
133                return Err(());
134            }
135
136            let idx = self.hand;
137            self.hand = (self.hand + 1) % self.capacity;
138            scanned += 1;
139
140            if !self.entries[idx].occupied {
141                continue;
142            }
143
144            if self.entries[idx].dirty {
145                // Pinned — skip
146                continue;
147            }
148
149            if self.entries[idx].visited {
150                // Give second chance
151                self.entries[idx].visited = false;
152                continue;
153            }
154
155            // Found victim: not visited, not dirty
156            let evicted_key = self.entries[idx].key;
157            let evicted_value = std::mem::take(&mut self.entries[idx].value);
158            self.entries[idx].occupied = false;
159            self.index.remove(&evicted_key);
160            self.len -= 1;
161
162            return Ok((evicted_key, evicted_value));
163        }
164    }
165
166    fn find_empty_slot(&self) -> usize {
167        for (i, entry) in self.entries.iter().enumerate() {
168            if !entry.occupied {
169                return i;
170            }
171        }
172        unreachable!("find_empty_slot called when cache is full");
173    }
174
175    /// Mark an entry as dirty (pinned, not evictable).
176    pub fn set_dirty(&mut self, key: u64) {
177        if let Some(&idx) = self.index.get(&key) {
178            self.entries[idx].dirty = true;
179        }
180    }
181
182    /// Clear dirty flag on an entry (after flush).
183    pub fn clear_dirty(&mut self, key: u64) {
184        if let Some(&idx) = self.index.get(&key) {
185            self.entries[idx].dirty = false;
186        }
187    }
188
189    /// Check if an entry is dirty.
190    pub fn is_dirty(&self, key: u64) -> bool {
191        self.index
192            .get(&key)
193            .map(|&idx| self.entries[idx].dirty)
194            .unwrap_or(false)
195    }
196
197    /// Iterate over all dirty entries. Returns (key, &value) pairs.
198    pub fn dirty_entries(&self) -> impl Iterator<Item = (u64, &V)> {
199        self.entries
200            .iter()
201            .filter(|e| e.occupied && e.dirty)
202            .map(|e| (e.key, &e.value))
203    }
204
205    /// Iterate mutably over all dirty entries.
206    pub fn dirty_entries_mut(&mut self) -> impl Iterator<Item = (u64, &mut V)> {
207        self.entries
208            .iter_mut()
209            .filter(|e| e.occupied && e.dirty)
210            .map(|e| (e.key, &mut e.value))
211    }
212
213    /// Clear all dirty flags (after commit).
214    pub fn clear_all_dirty(&mut self) {
215        for entry in &mut self.entries {
216            if entry.occupied {
217                entry.dirty = false;
218            }
219        }
220    }
221
222    /// Remove an entry by key. Returns the value if present.
223    pub fn remove(&mut self, key: u64) -> Option<V> {
224        if let Some(idx) = self.index.remove(&key) {
225            let value = std::mem::take(&mut self.entries[idx].value);
226            self.entries[idx].occupied = false;
227            self.len -= 1;
228            Some(value)
229        } else {
230            None
231        }
232    }
233
234    pub fn len(&self) -> usize {
235        self.len
236    }
237
238    pub fn is_empty(&self) -> bool {
239        self.len == 0
240    }
241
242    pub fn capacity(&self) -> usize {
243        self.capacity
244    }
245
246    /// Count dirty entries.
247    pub fn dirty_count(&self) -> usize {
248        self.entries
249            .iter()
250            .filter(|e| e.occupied && e.dirty)
251            .count()
252    }
253
254    /// Remove all entries from the cache.
255    pub fn clear(&mut self) {
256        for entry in &mut self.entries {
257            entry.occupied = false;
258            entry.visited = false;
259            entry.dirty = false;
260        }
261        self.index.clear();
262        self.len = 0;
263        self.hand = 0;
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    #[test]
272    fn basic_insert_and_get() {
273        let mut cache = SieveCache::<u32>::new(4);
274        cache.insert(1, 100).unwrap();
275        cache.insert(2, 200).unwrap();
276
277        assert_eq!(cache.get(1), Some(&100));
278        assert_eq!(cache.get(2), Some(&200));
279        assert_eq!(cache.get(3), None);
280    }
281
282    #[test]
283    fn eviction_when_full() {
284        let mut cache = SieveCache::<u32>::new(3);
285        cache.insert(1, 100).unwrap();
286        cache.insert(2, 200).unwrap();
287        cache.insert(3, 300).unwrap();
288        assert_eq!(cache.len(), 3);
289
290        // Don't access any — all are unvisited, one should be evicted
291        // Reset visited flags by clearing them
292        for entry in &mut cache.entries {
293            entry.visited = false;
294        }
295
296        let result = cache.insert(4, 400).unwrap();
297        assert!(result.is_some());
298        assert_eq!(cache.len(), 3);
299    }
300
301    #[test]
302    fn visited_entries_survive_eviction() {
303        let mut cache = SieveCache::<u32>::new(3);
304        cache.insert(1, 100).unwrap();
305        cache.insert(2, 200).unwrap();
306        cache.insert(3, 300).unwrap();
307
308        // Reset all visited flags
309        for entry in &mut cache.entries {
310            entry.visited = false;
311        }
312
313        // Access entry 2 (marks visited)
314        cache.get(2);
315
316        // Insert 4 — should evict 1 or 3 (not 2)
317        let evicted = cache.insert(4, 400).unwrap().unwrap();
318        assert_ne!(evicted.0, 2, "visited entry should not be evicted");
319        assert!(cache.contains(2));
320        assert!(cache.contains(4));
321    }
322
323    #[test]
324    fn dirty_entries_not_evicted() {
325        let mut cache = SieveCache::<u32>::new(2);
326        cache.insert(1, 100).unwrap();
327        cache.insert(2, 200).unwrap();
328
329        // Reset visited
330        for entry in &mut cache.entries {
331            entry.visited = false;
332        }
333
334        // Mark entry 1 dirty
335        cache.set_dirty(1);
336
337        // Insert 3 — must evict 2 (not dirty 1)
338        let evicted = cache.insert(3, 300).unwrap().unwrap();
339        assert_eq!(evicted.0, 2);
340        assert!(cache.contains(1));
341        assert!(cache.contains(3));
342    }
343
344    #[test]
345    fn all_dirty_returns_err() {
346        let mut cache = SieveCache::<u32>::new(2);
347        cache.insert(1, 100).unwrap();
348        cache.insert(2, 200).unwrap();
349
350        cache.set_dirty(1);
351        cache.set_dirty(2);
352
353        // Reset visited
354        for entry in &mut cache.entries {
355            entry.visited = false;
356        }
357
358        let result = cache.insert(3, 300);
359        assert!(result.is_err());
360    }
361
362    #[test]
363    fn clear_dirty() {
364        let mut cache = SieveCache::<u32>::new(2);
365        cache.insert(1, 100).unwrap();
366        cache.set_dirty(1);
367        assert!(cache.is_dirty(1));
368
369        cache.clear_dirty(1);
370        assert!(!cache.is_dirty(1));
371    }
372
373    #[test]
374    fn dirty_entries_iterator() {
375        let mut cache = SieveCache::<u32>::new(4);
376        cache.insert(1, 100).unwrap();
377        cache.insert(2, 200).unwrap();
378        cache.insert(3, 300).unwrap();
379
380        cache.set_dirty(1);
381        cache.set_dirty(3);
382
383        let dirty: Vec<_> = cache.dirty_entries().collect();
384        assert_eq!(dirty.len(), 2);
385        assert_eq!(cache.dirty_count(), 2);
386    }
387
388    #[test]
389    fn clear_all_dirty() {
390        let mut cache = SieveCache::<u32>::new(3);
391        cache.insert(1, 100).unwrap();
392        cache.insert(2, 200).unwrap();
393        cache.set_dirty(1);
394        cache.set_dirty(2);
395
396        cache.clear_all_dirty();
397        assert_eq!(cache.dirty_count(), 0);
398    }
399
400    #[test]
401    fn remove_entry() {
402        let mut cache = SieveCache::<u32>::new(4);
403        cache.insert(1, 100).unwrap();
404        cache.insert(2, 200).unwrap();
405
406        let removed = cache.remove(1);
407        assert_eq!(removed, Some(100));
408        assert!(!cache.contains(1));
409        assert_eq!(cache.len(), 1);
410    }
411
412    #[test]
413    fn update_existing_key() {
414        let mut cache = SieveCache::<u32>::new(4);
415        cache.insert(1, 100).unwrap();
416        cache.insert(1, 200).unwrap();
417
418        assert_eq!(cache.get(1), Some(&200));
419        assert_eq!(cache.len(), 1);
420    }
421}