Skip to main content

citadel_buffer/
sieve.rs

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