metrale_storage/tier/
residency.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: [`Residency`], the page table over a bounded hot [`SlotArena`]
4//! and a cold [`SwapStore`].
5//!
6//! Owner: storage (tier).
7//! Invariants, between calls:
8//! - A key is in `lru` if and only if it is `Resident` and holds no read-pin.
9//! - Every `OnDisk` key is in `disk_lru` exactly once.
10
11use std::collections::{HashMap, VecDeque};
12
13use anyhow::{Result, bail};
14
15use crate::tier::aligned::PageAlignedBuf;
16use crate::tier::traits::{SlotArena, SwapStats, SwapStore};
17
18/// 2026-09-25: Where a key's blob is.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20enum Loc {
21    /// 2026-09-25: An arena slot handed out by `alloc` and not yet committed.
22    /// It is not in `lru`, so it is never an eviction victim.
23    Reserved(usize),
24    /// 2026-09-25: Committed in an arena slot.
25    Resident(usize),
26    /// 2026-09-25: Spilled to a store record; `locate` faults it back into a
27    /// slot.
28    OnDisk(usize),
29}
30
31/// 2026-09-25: The page table from a `u64` key to a [`SlotArena`] slot or a
32/// [`SwapStore`] record, evicting the least recently used resident to the store
33/// when the arena is full.
34pub struct Residency<A: SlotArena, S: SwapStore> {
35    arena: A,
36    swap: S,
37    blob_bytes: usize,
38    map: HashMap<u64, Loc>,
39    /// 2026-09-25: Free arena slots, reused last-in first-out.
40    free_slots: Vec<usize>,
41    /// 2026-09-25: Unpinned resident keys, coldest first: the front is the next
42    /// eviction victim.
43    lru: VecDeque<u64>,
44    /// 2026-09-25: On-disk keys, coldest first: the front is the next record the
45    /// disk cap drops.
46    disk_lru: VecDeque<u64>,
47    /// 2026-09-25: The on-disk record cap, 0 for none. At the cap a spill first
48    /// drops the coldest on-disk key from the map, so a later get of it misses.
49    max_disk_slots: usize,
50    /// 2026-09-25: Free record indices, reused before `next_disk` grows.
51    free_disk: Vec<usize>,
52    next_disk: usize,
53    /// 2026-09-25: The buffer every spill and fault-in moves a blob through,
54    /// `blob_bytes` long and 4 KiB-aligned: the unix
55    /// [`DirectSwapFile`](crate::tier::DirectSwapFile) copies a buffer that is
56    /// not page-aligned through its own bounce buffer first.
57    scratch: PageAlignedBuf,
58    /// 2026-09-25: Read-pins, key to reader count. A peer hands a client the
59    /// slot offset for a one-sided RDMA read that happens after the lock is
60    /// released; a pinned key stays out of `lru`, so `evict_coldest_to_disk`
61    /// cannot reuse its slot during that read.
62    read_pins: HashMap<u64, u32>,
63    stats: SwapStats,
64}
65
66impl<A: SlotArena, S: SwapStore> Residency<A, S> {
67    /// 2026-09-25: [`Residency::new_capped`] with no disk cap.
68    pub fn new(arena: A, swap: S) -> Result<Self> {
69        Self::new_capped(arena, swap, 0)
70    }
71
72    /// 2026-09-25: A residency over `arena` and `swap` with at most
73    /// `max_disk_slots` live on-disk records (0: no cap). The file can still
74    /// reach `max_disk_slots + 1` records (see [`Residency::disk_high_water`]).
75    ///
76    /// # Errors
77    /// `slot_bytes()` is 0, differs from `swap.record_bytes()`, or the arena has
78    /// no slot.
79    pub fn new_capped(arena: A, swap: S, max_disk_slots: usize) -> Result<Self> {
80        let blob_bytes = arena.slot_bytes();
81        if blob_bytes == 0 {
82            bail!("Residency: slot_bytes must be > 0");
83        }
84        if swap.record_bytes() != blob_bytes {
85            bail!(
86                "Residency: arena slot ({}) and swap record ({}) sizes differ",
87                blob_bytes,
88                swap.record_bytes()
89            );
90        }
91        let n = arena.num_slots();
92        if n == 0 {
93            bail!("Residency: arena must have >= 1 slot");
94        }
95        Ok(Self {
96            arena,
97            swap,
98            blob_bytes,
99            map: HashMap::new(),
100            free_slots: (0..n).rev().collect(),
101            lru: VecDeque::new(),
102            disk_lru: VecDeque::new(),
103            max_disk_slots,
104            free_disk: Vec::new(),
105            next_disk: 0,
106            scratch: PageAlignedBuf::new(blob_bytes),
107            read_pins: HashMap::new(),
108            stats: SwapStats::default(),
109        })
110    }
111
112    pub fn blob_bytes(&self) -> usize {
113        self.blob_bytes
114    }
115    /// 2026-09-25: Address of the scratch buffer, for the alignment test.
116    #[doc(hidden)]
117    pub fn scratch_addr(&self) -> usize {
118        self.scratch.as_slice().as_ptr() as usize
119    }
120    pub fn stats(&self) -> &SwapStats {
121        &self.stats
122    }
123    pub fn resident_count(&self) -> usize {
124        self.lru.len()
125    }
126    pub fn total_keys(&self) -> usize {
127        self.map.len()
128    }
129    /// 2026-09-25: Keys on disk now; at most `max_disk_slots` when capped.
130    pub fn disk_count(&self) -> usize {
131        self.disk_lru.len()
132    }
133    /// 2026-09-25: The highest record index ever allocated, plus one: the swap
134    /// file's size in records. [`DirectSwapFile`](crate::tier::DirectSwapFile)
135    /// keeps the default no-op `discard_record`, so its file never shrinks and
136    /// only index reuse through `free_disk` bounds it. Under a cap this reaches
137    /// `max_disk_slots + 1`: [`Residency::locate`]'s `OnDisk` arm takes the
138    /// faulting key out of `disk_lru` while its record is still live, so
139    /// `make_disk_room` counts one fewer record than exist.
140    pub fn disk_high_water(&self) -> usize {
141        self.next_disk
142    }
143    /// 2026-09-25: The on-disk record cap, 0 for none.
144    pub fn max_disk_slots(&self) -> usize {
145        self.max_disk_slots
146    }
147
148    /// 2026-09-25: The arena itself, for callers that move the bytes of a slot
149    /// they got from [`Residency::alloc`] or [`Residency::locate`];
150    /// [`Residency::put_blob`] and [`Residency::get_blob`] do both steps.
151    pub fn arena(&self) -> &A {
152        &self.arena
153    }
154    pub fn arena_mut(&mut self) -> &mut A {
155        &mut self.arena
156    }
157
158    /// 2026-09-25: Byte offset of `slot` in the arena: `slot * blob_bytes`.
159    pub fn slot_offset(&self, slot: usize) -> u64 {
160        (slot as u64) * (self.blob_bytes as u64)
161    }
162
163    /// 2026-09-25: Put, step 1: reserve an arena slot for `key`, spilling the
164    /// coldest unpinned resident to the store when no slot is free. The caller
165    /// writes the blob into the slot and then calls [`Residency::commit`]. A
166    /// resident or reserved key keeps its slot; an on-disk key gets a new slot
167    /// and its record is freed.
168    ///
169    /// # Errors
170    /// Every slot is reserved or read-pinned, or the spill's arena read or
171    /// store write fails. On error the key keeps its previous location.
172    pub fn alloc(&mut self, key: u64) -> Result<usize> {
173        self.stats.puts += 1;
174        match self.map.get(&key).copied() {
175            Some(Loc::Resident(slot)) => {
176                self.lru_remove(key);
177                self.map.insert(key, Loc::Reserved(slot));
178                return Ok(slot);
179            }
180            Some(Loc::Reserved(slot)) => return Ok(slot),
181            Some(Loc::OnDisk(disk_slot)) => {
182                // 2026-09-25: The slot is acquired before the key's record is
183                // freed. `acquire_slot` can fail in its spill (a store write
184                // error such as ENOSPC); had the record already been freed, it
185                // would sit on `free_disk` while `map[key]` still says
186                // `OnDisk(disk_slot)`, and a retried put or a `remove` would free
187                // it a second time, handing one record to two keys.
188                //
189                // The key also leaves `disk_lru` first, so `make_disk_room` in
190                // that spill cannot pick it and free its record the same way.
191                self.disk_lru_remove(key);
192                let slot = match self.acquire_slot() {
193                    Ok(s) => s,
194                    Err(e) => {
195                        // 2026-09-25: The key is still `OnDisk` with its own
196                        // record; put it back at the cold end.
197                        self.disk_lru.push_front(key);
198                        return Err(e);
199                    }
200                };
201                self.free_disk.push(disk_slot);
202                self.swap.discard_record(disk_slot);
203                self.map.insert(key, Loc::Reserved(slot));
204                return Ok(slot);
205            }
206            None => {}
207        }
208        let slot = self.acquire_slot()?;
209        self.map.insert(key, Loc::Reserved(slot));
210        Ok(slot)
211    }
212
213    /// 2026-09-25: Put, step 2: the blob is in the reserved slot; mark `key`
214    /// resident and, unless read-pinned, the hottest in `lru`. Committing a
215    /// resident key does nothing.
216    ///
217    /// # Errors
218    /// `key` is neither reserved nor resident.
219    pub fn commit(&mut self, key: u64) -> Result<()> {
220        match self.map.get(&key).copied() {
221            Some(Loc::Reserved(slot)) => {
222                self.map.insert(key, Loc::Resident(slot));
223                // 2026-09-25: A key pinned while its re-put was in flight stays
224                // out of `lru`; `unpin_read` adds it back.
225                if !self.read_pins.contains_key(&key) {
226                    self.lru.push_back(key);
227                }
228                Ok(())
229            }
230            Some(Loc::Resident(_)) => Ok(()),
231            _ => bail!("commit({key:#x}): no reserved slot (alloc not called / evicted)"),
232        }
233    }
234
235    /// 2026-09-25: Get: the arena slot holding `key`, faulting an on-disk key
236    /// back into a slot (which may spill another key). `Ok(None)` for an unknown
237    /// key. A reserved key returns its slot, whose bytes the caller has not
238    /// committed.
239    ///
240    /// # Errors
241    /// No slot can be acquired, or the store read or arena write fails; the key
242    /// then stays on disk and the acquired slot is freed.
243    pub fn locate(&mut self, key: u64) -> Result<Option<usize>> {
244        self.stats.gets += 1;
245        match self.map.get(&key).copied() {
246            Some(Loc::Resident(slot)) => {
247                self.stats.resident_hits += 1;
248                // 2026-09-25: A pinned key is not in `lru`, and touching it would
249                // put it back there while it is being read.
250                if !self.read_pins.contains_key(&key) {
251                    self.lru_touch(key);
252                }
253                Ok(Some(slot))
254            }
255            Some(Loc::Reserved(slot)) => {
256                // 2026-09-25: Not yet committed; the slot is returned as it is.
257                Ok(Some(slot))
258            }
259            Some(Loc::OnDisk(disk_slot)) => {
260                // 2026-09-25: Out of `disk_lru` while the fault runs, so the
261                // `make_disk_room` in `acquire_slot`'s spill cannot drop it.
262                self.disk_lru_remove(key);
263                let slot = match self.acquire_slot() {
264                    Ok(s) => s,
265                    Err(e) => {
266                        self.disk_lru.push_front(key);
267                        return Err(e);
268                    }
269                };
270                // 2026-09-25: `&mut self` makes this the only move using
271                // `scratch`.
272                let mut buf = std::mem::take(&mut self.scratch);
273                let r = self
274                    .swap
275                    .read_record(disk_slot, buf.as_mut_slice())
276                    .and_then(|_| self.arena.write_slot(slot, buf.as_slice()));
277                // 2026-09-25: `scratch` goes back before any return: while it is
278                // taken, `self.scratch` is the empty default, and a later move
279                // through it would fail its length check.
280                self.scratch = buf;
281                if let Err(e) = r {
282                    self.free_slots.push(slot);
283                    self.disk_lru.push_front(key);
284                    return Err(e);
285                }
286                self.free_disk.push(disk_slot);
287                self.swap.discard_record(disk_slot);
288                self.map.insert(key, Loc::Resident(slot));
289                self.lru.push_back(key);
290                self.stats.faults_from_disk += 1;
291                Ok(Some(slot))
292            }
293            None => {
294                self.stats.get_miss += 1;
295                Ok(None)
296            }
297        }
298    }
299
300    /// 2026-09-25: Forget `key`, freeing its arena slot or store record. A read
301    /// pin on it stays until `unpin_read`.
302    pub fn remove(&mut self, key: u64) {
303        match self.map.remove(&key) {
304            Some(Loc::Resident(slot)) | Some(Loc::Reserved(slot)) => {
305                self.lru_remove(key);
306                self.free_slots.push(slot);
307            }
308            Some(Loc::OnDisk(disk_slot)) => {
309                self.disk_lru_remove(key);
310                self.free_disk.push(disk_slot);
311                self.swap.discard_record(disk_slot);
312            }
313            None => {}
314        }
315    }
316
317    /// 2026-09-25: Read-pin `key` so its slot is not an eviction victim while a
318    /// client reads it. Counted per reader; the first pin takes the key out of
319    /// `lru`. Does nothing unless `key` is resident.
320    pub fn pin_read(&mut self, key: u64) {
321        if !matches!(self.map.get(&key), Some(Loc::Resident(_))) {
322            return;
323        }
324        let n = self.read_pins.get(&key).copied().unwrap_or(0);
325        if n == 0 {
326            self.lru_remove(key);
327        }
328        self.read_pins.insert(key, n + 1);
329    }
330
331    /// 2026-09-25: Release one pin taken by [`Residency::pin_read`]. After the
332    /// last one, a key that is still resident rejoins `lru` as the hottest. Does
333    /// nothing for a key with no pin.
334    pub fn unpin_read(&mut self, key: u64) {
335        let Some(n) = self.read_pins.get_mut(&key) else {
336            return;
337        };
338        *n -= 1;
339        if *n == 0 {
340            self.read_pins.remove(&key);
341            if matches!(self.map.get(&key), Some(Loc::Resident(_))) && !self.lru.contains(&key) {
342                self.lru.push_back(key);
343            }
344        }
345    }
346
347    /// 2026-09-25: The number of read-pins on `key`.
348    pub fn read_pin_count(&self, key: u64) -> u32 {
349        self.read_pins.get(&key).copied().unwrap_or(0)
350    }
351
352    /// 2026-09-25: Put in one call: [`Residency::alloc`], copy `bytes` into the
353    /// slot, [`Residency::commit`].
354    ///
355    /// # Errors
356    /// `bytes` is not `blob_bytes` long, `alloc` fails, or the arena write fails,
357    /// after which `key` is removed.
358    pub fn put_blob(&mut self, key: u64, bytes: &[u8]) -> Result<()> {
359        if bytes.len() != self.blob_bytes {
360            bail!(
361                "put_blob({key:#x}): {} bytes, expected {}",
362                bytes.len(),
363                self.blob_bytes
364            );
365        }
366        let slot = self.alloc(key)?;
367        if let Err(e) = self.arena.write_slot(slot, bytes) {
368            self.remove(key);
369            return Err(e);
370        }
371        self.commit(key)
372    }
373
374    /// 2026-09-25: Get in one call: [`Residency::locate`], then copy the slot
375    /// into `out`. `Ok(false)` for an unknown key.
376    ///
377    /// # Errors
378    /// `out` is not `blob_bytes` long, or `locate` or the arena read fails.
379    pub fn get_blob(&mut self, key: u64, out: &mut [u8]) -> Result<bool> {
380        if out.len() != self.blob_bytes {
381            bail!(
382                "get_blob({key:#x}): {} bytes, expected {}",
383                out.len(),
384                self.blob_bytes
385            );
386        }
387        match self.locate(key)? {
388            Some(slot) => {
389                self.arena.read_slot(slot, out)?;
390                Ok(true)
391            }
392            None => Ok(false),
393        }
394    }
395
396    /// 2026-09-25: A free arena slot, or the slot of the coldest unpinned
397    /// resident after spilling it.
398    fn acquire_slot(&mut self) -> Result<usize> {
399        if let Some(s) = self.free_slots.pop() {
400            return Ok(s);
401        }
402        self.evict_coldest_to_disk()
403    }
404
405    /// 2026-09-25: Spill the front of `lru` to a store record and return its
406    /// slot. Reserved and read-pinned keys are not in `lru`. On a failed store
407    /// write the victim stays resident at the cold end.
408    fn evict_coldest_to_disk(&mut self) -> Result<usize> {
409        let Some(victim) = self.lru.pop_front() else {
410            bail!(
411                "Residency: arena exhausted — all {} slots reserved (uncommitted \
412                 PUTs) or read-pinned (in-flight RDMA READs)",
413                self.arena.num_slots()
414            );
415        };
416        let slot = match self.map.get(&victim).copied() {
417            Some(Loc::Resident(slot)) => slot,
418            other => bail!("LRU/map desync: victim {victim:#x} is {other:?}, expected Resident"),
419        };
420        self.make_disk_room();
421        let disk_slot = self.alloc_disk_slot();
422        let mut buf = std::mem::take(&mut self.scratch);
423        let res = self
424            .arena
425            .read_slot(slot, buf.as_mut_slice())
426            .and_then(|_| self.swap.write_record(disk_slot, buf.as_slice()));
427        self.scratch = buf;
428        if let Err(e) = res {
429            self.free_disk.push(disk_slot);
430            self.lru.push_front(victim);
431            return Err(e);
432        }
433        self.map.insert(victim, Loc::OnDisk(disk_slot));
434        self.disk_lru.push_back(victim);
435        self.stats.spills_to_disk += 1;
436        Ok(slot)
437    }
438
439    /// 2026-09-25: Drop the coldest on-disk keys until `disk_lru` holds fewer
440    /// than `max_disk_slots` (nothing without a cap). A dropped key leaves the
441    /// map, so a later get of it misses.
442    fn make_disk_room(&mut self) {
443        if self.max_disk_slots == 0 {
444            return;
445        }
446        while self.disk_lru.len() >= self.max_disk_slots {
447            let Some(cold) = self.disk_lru.pop_front() else {
448                break;
449            };
450            if let Some(Loc::OnDisk(ds)) = self.map.remove(&cold) {
451                self.free_disk.push(ds);
452                self.swap.discard_record(ds);
453                self.stats.disk_evictions += 1;
454            }
455        }
456    }
457
458    fn alloc_disk_slot(&mut self) -> usize {
459        if let Some(d) = self.free_disk.pop() {
460            d
461        } else {
462            let d = self.next_disk;
463            self.next_disk += 1;
464            d
465        }
466    }
467
468    fn disk_lru_remove(&mut self, key: u64) {
469        if let Some(pos) = self.disk_lru.iter().position(|&k| k == key) {
470            self.disk_lru.remove(pos);
471        }
472    }
473
474    fn lru_touch(&mut self, key: u64) {
475        self.lru_remove(key);
476        self.lru.push_back(key);
477    }
478
479    fn lru_remove(&mut self, key: u64) {
480        if let Some(pos) = self.lru.iter().position(|&k| k == key) {
481            self.lru.remove(pos);
482        }
483    }
484}