atlas_tier/
traits.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! The two tier seams — [`SlotArena`] (hot) and [`SwapStore`] (cold) — plus
4//! [`SwapStats`], the counters [`crate::Residency`] keeps over them.
5
6use anyhow::Result;
7
8/// The hot tier: a RAM arena as a set of `num_slots` fixed-size slots. The
9/// cache peer implements this over its `mmap`'d MR (page-aligned →
10/// O_DIRECT-safe); in-process consumers use [`crate::VecSlotArena`].
11pub trait SlotArena: Send {
12    fn slot_bytes(&self) -> usize;
13    fn num_slots(&self) -> usize;
14    /// Copy arena slot → `out` (for spilling a victim to disk). `out.len()`
15    /// MUST equal `slot_bytes()`.
16    fn read_slot(&self, slot: usize, out: &mut [u8]) -> Result<()>;
17    /// Copy `bytes` → arena slot (for faulting a record back in). `bytes.len()`
18    /// MUST equal `slot_bytes()`.
19    fn write_slot(&mut self, slot: usize, bytes: &[u8]) -> Result<()>;
20}
21
22/// The cold tier: an unbounded fixed-stride record store addressed by a
23/// monotonic `disk_slot` index. The peer implements this over an O_DIRECT NVMe
24/// file ([`crate::DirectSwapFile`]); [`crate::MemSwapStore`] is the host-RAM
25/// variant.
26pub trait SwapStore: Send {
27    fn record_bytes(&self) -> usize;
28    fn write_record(&mut self, disk_slot: usize, bytes: &[u8]) -> Result<()>;
29    fn read_record(&self, disk_slot: usize, out: &mut [u8]) -> Result<()>;
30    /// Optional: reclaim disk space for a freed slot (default no-op; a hole in
31    /// a preallocated file is fine — the free-list reuses the index).
32    fn discard_record(&mut self, _disk_slot: usize) {}
33}
34
35// Boxed trait objects compose (lets a consumer pick arena/swap impls at
36// runtime: `Residency<Box<dyn SlotArena>, Box<dyn SwapStore>>`).
37impl<T: SlotArena + ?Sized> SlotArena for Box<T> {
38    fn slot_bytes(&self) -> usize {
39        (**self).slot_bytes()
40    }
41    fn num_slots(&self) -> usize {
42        (**self).num_slots()
43    }
44    fn read_slot(&self, slot: usize, out: &mut [u8]) -> Result<()> {
45        (**self).read_slot(slot, out)
46    }
47    fn write_slot(&mut self, slot: usize, bytes: &[u8]) -> Result<()> {
48        (**self).write_slot(slot, bytes)
49    }
50}
51
52impl<T: SwapStore + ?Sized> SwapStore for Box<T> {
53    fn record_bytes(&self) -> usize {
54        (**self).record_bytes()
55    }
56    fn write_record(&mut self, disk_slot: usize, bytes: &[u8]) -> Result<()> {
57        (**self).write_record(disk_slot, bytes)
58    }
59    fn read_record(&self, disk_slot: usize, out: &mut [u8]) -> Result<()> {
60        (**self).read_record(disk_slot, out)
61    }
62    fn discard_record(&mut self, disk_slot: usize) {
63        (**self).discard_record(disk_slot)
64    }
65}
66
67#[derive(Default, Debug, Clone)]
68pub struct SwapStats {
69    pub puts: u64,
70    pub gets: u64,
71    pub get_miss: u64,
72    pub spills_to_disk: u64,
73    pub faults_from_disk: u64,
74    pub resident_hits: u64,
75    /// Cold on-disk snapshots dropped because the disk cap was hit (a later GET
76    /// for one cleanly misses → recompute).
77    pub disk_evictions: u64,
78}