metrale_storage/
cascade_policy.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Slot bookkeeping for the cascade's first tier (T1), the
4//! write-back group cache of `CascadeBackend`: placement, LRU eviction and the
5//! hit/miss split of a read. No I/O and no CUDA.
6//!
7//! Owner: metrale-storage KV tier.
8//! Invariants:
9//! - An occupied slot holds one `GroupKey`, recorded in both `lookup` and
10//!   `slot_key`; eviction clears both before the slot is reused.
11
12use std::collections::{HashMap, VecDeque};
13
14use crate::group::GroupKey;
15
16/// 2026-09-25: The slot to write a group into and, when a resident group was
17/// evicted for it, that victim and its slot. The victim's bytes stay in the
18/// slot until the caller overwrites it.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub struct WritePlan {
21    pub slot: u32,
22    pub flush_victim: Option<(GroupKey, u32)>,
23}
24
25/// 2026-09-25: A fixed-capacity LRU set of resident groups over `cap_slots`
26/// slots.
27pub struct SlotCache {
28    cap_slots: u32,
29    lookup: HashMap<GroupKey, u32>,
30    slot_key: Vec<Option<GroupKey>>,
31    free: VecDeque<u32>,
32    /// 2026-09-25: Front is least recently used (the eviction target), back is
33    /// most recently used.
34    lru: VecDeque<u32>,
35}
36
37impl SlotCache {
38    pub fn new(cap_slots: u32) -> Self {
39        assert!(cap_slots > 0, "SlotCache needs at least one slot");
40        Self {
41            cap_slots,
42            lookup: HashMap::new(),
43            slot_key: vec![None; cap_slots as usize],
44            free: (0..cap_slots).collect(),
45            lru: VecDeque::with_capacity(cap_slots as usize),
46        }
47    }
48
49    pub fn capacity(&self) -> u32 {
50        self.cap_slots
51    }
52
53    fn move_to_back(&mut self, slot: u32) {
54        if let Some(pos) = self.lru.iter().position(|&s| s == slot) {
55            self.lru.remove(pos);
56        }
57        self.lru.push_back(slot);
58    }
59
60    /// 2026-09-25: Make `slot` the most recently used. Callers pass occupied
61    /// slots, from `plan_read` hits.
62    pub fn touch(&mut self, slot: u32) {
63        self.move_to_back(slot);
64    }
65
66    /// 2026-09-25: Plan a write of `key`: its own slot if resident, else a free
67    /// slot, else the least recently used slot, whose group is returned as the
68    /// victim.
69    pub fn plan_write(&mut self, key: GroupKey) -> WritePlan {
70        if let Some(&slot) = self.lookup.get(&key) {
71            self.move_to_back(slot);
72            return WritePlan {
73                slot,
74                flush_victim: None,
75            };
76        }
77        if let Some(slot) = self.free.pop_front() {
78            self.install(key, slot);
79            return WritePlan {
80                slot,
81                flush_victim: None,
82            };
83        }
84        let victim_slot = self.lru.pop_front().expect("full cache has an LRU entry");
85        let victim_key = self.slot_key[victim_slot as usize]
86            .take()
87            .expect("occupied slot has a key");
88        self.lookup.remove(&victim_key);
89        self.install(key, victim_slot);
90        WritePlan {
91            slot: victim_slot,
92            flush_victim: Some((victim_key, victim_slot)),
93        }
94    }
95
96    fn install(&mut self, key: GroupKey, slot: u32) {
97        self.lookup.insert(key, slot);
98        self.slot_key[slot as usize] = Some(key);
99        self.lru.push_back(slot);
100    }
101
102    /// 2026-09-25: Split `keys` into hits `(index, slot)` and misses `index`,
103    /// without changing LRU order; `CascadeBackend` touches each hit after
104    /// queueing its copy.
105    pub fn plan_read(&self, keys: &[GroupKey]) -> (Vec<(usize, u32)>, Vec<usize>) {
106        let mut hits = Vec::new();
107        let mut misses = Vec::new();
108        for (i, k) in keys.iter().enumerate() {
109            match self.lookup.get(k) {
110                Some(&slot) => hits.push((i, slot)),
111                None => misses.push(i),
112            }
113        }
114        (hits, misses)
115    }
116
117    /// 2026-09-25: Every resident `(key, slot)`.
118    pub fn residents(&self) -> Vec<(GroupKey, u32)> {
119        self.lookup.iter().map(|(&k, &s)| (k, s)).collect()
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use crate::group::KvKind;
127
128    fn k(block: u32) -> GroupKey {
129        GroupKey::new(0, block, 0, KvKind::K)
130    }
131
132    #[test]
133    fn free_slots_then_overwrite_in_place() {
134        let mut c = SlotCache::new(3);
135        let p0 = c.plan_write(k(0));
136        assert_eq!(p0.flush_victim, None);
137        let p1 = c.plan_write(k(1));
138        assert_ne!(p1.slot, p0.slot);
139        let p0b = c.plan_write(k(0));
140        assert_eq!(p0b.slot, p0.slot);
141        assert_eq!(p0b.flush_victim, None);
142    }
143
144    #[test]
145    fn fill_then_evict_lru_tail() {
146        let mut c = SlotCache::new(2);
147        let s0 = c.plan_write(k(0)).slot;
148        let _s1 = c.plan_write(k(1)).slot;
149        let (hits, _) = c.plan_read(&[k(0)]);
150        c.touch(hits[0].1);
151        let p2 = c.plan_write(k(2));
152        assert_eq!(p2.flush_victim.map(|(vk, _)| vk), Some(k(1)));
153        assert_ne!(p2.slot, s0);
154    }
155
156    #[test]
157    fn hit_miss_partition() {
158        let mut c = SlotCache::new(4);
159        c.plan_write(k(0));
160        c.plan_write(k(2));
161        let (hits, misses) = c.plan_read(&[k(0), k(1), k(2), k(3)]);
162        let hit_idx: Vec<usize> = hits.iter().map(|(i, _)| *i).collect();
163        assert_eq!(hit_idx, vec![0, 2]);
164        assert_eq!(misses, vec![1, 3]);
165    }
166
167    #[test]
168    fn residents_lists_all_live_groups() {
169        let mut c = SlotCache::new(3);
170        c.plan_write(k(0));
171        c.plan_write(k(1));
172        let mut r: Vec<GroupKey> = c.residents().into_iter().map(|(k, _)| k).collect();
173        r.sort_by_key(|g| g.block);
174        assert_eq!(r, vec![k(0), k(1)]);
175    }
176
177    #[test]
178    fn evicted_group_is_no_longer_a_hit() {
179        let mut c = SlotCache::new(1);
180        c.plan_write(k(0));
181        let p = c.plan_write(k(1));
182        assert_eq!(p.flush_victim.map(|(vk, _)| vk), Some(k(0)));
183        let (hits, misses) = c.plan_read(&[k(0), k(1)]);
184        assert_eq!(hits.iter().map(|(i, _)| *i).collect::<Vec<_>>(), vec![1]);
185        assert_eq!(misses, vec![0]);
186    }
187}