metrale_storage/tier/
traits.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: The two tier seams, [`SlotArena`] (hot) and [`SwapStore`] (cold),
4//! and [`SwapStats`], the counters [`Residency`](crate::tier::Residency) keeps.
5//!
6//! Owner: storage (tier).
7//! Invariants: none beyond the types.
8
9use anyhow::Result;
10
11/// 2026-09-25: The hot tier: `num_slots` slots of `slot_bytes` each. Implemented
12/// by [`VecSlotArena`](crate::tier::VecSlotArena) (host RAM), the cache peer's
13/// `MmapSlotArena` and model-engine's `TransportSlotArena`.
14pub trait SlotArena: Send {
15    fn slot_bytes(&self) -> usize;
16    fn num_slots(&self) -> usize;
17    /// 2026-09-25: Copy slot `slot` into `out`, whose length must be
18    /// `slot_bytes()`.
19    fn read_slot(&self, slot: usize, out: &mut [u8]) -> Result<()>;
20    /// 2026-09-25: Copy `bytes`, whose length must be `slot_bytes()`, into slot
21    /// `slot`.
22    fn write_slot(&mut self, slot: usize, bytes: &[u8]) -> Result<()>;
23}
24
25/// 2026-09-25: The cold tier: records of `record_bytes` each, addressed by a
26/// `disk_slot` index that [`Residency`](crate::tier::Residency) allocates and
27/// reuses. Implemented by [`DirectSwapFile`](crate::tier::DirectSwapFile) (a
28/// file) and [`MemSwapStore`](crate::tier::MemSwapStore) (host RAM).
29pub trait SwapStore: Send {
30    fn record_bytes(&self) -> usize;
31    fn write_record(&mut self, disk_slot: usize, bytes: &[u8]) -> Result<()>;
32    fn read_record(&self, disk_slot: usize, out: &mut [u8]) -> Result<()>;
33    /// 2026-09-25: Called when `disk_slot` is freed. The default does nothing;
34    /// `Residency` reuses the index from its free list either way.
35    fn discard_record(&mut self, _disk_slot: usize) {}
36}
37
38// 2026-09-25: Boxes implement the traits, so a `Residency<Box<dyn SlotArena>,
39// Box<dyn SwapStore>>` takes an arena and a store chosen at run time
40// (model-engine `UnifiedSnapshotStore`).
41impl<T: SlotArena + ?Sized> SlotArena for Box<T> {
42    fn slot_bytes(&self) -> usize {
43        (**self).slot_bytes()
44    }
45    fn num_slots(&self) -> usize {
46        (**self).num_slots()
47    }
48    fn read_slot(&self, slot: usize, out: &mut [u8]) -> Result<()> {
49        (**self).read_slot(slot, out)
50    }
51    fn write_slot(&mut self, slot: usize, bytes: &[u8]) -> Result<()> {
52        (**self).write_slot(slot, bytes)
53    }
54}
55
56impl<T: SwapStore + ?Sized> SwapStore for Box<T> {
57    fn record_bytes(&self) -> usize {
58        (**self).record_bytes()
59    }
60    fn write_record(&mut self, disk_slot: usize, bytes: &[u8]) -> Result<()> {
61        (**self).write_record(disk_slot, bytes)
62    }
63    fn read_record(&self, disk_slot: usize, out: &mut [u8]) -> Result<()> {
64        (**self).read_record(disk_slot, out)
65    }
66    fn discard_record(&mut self, disk_slot: usize) {
67        (**self).discard_record(disk_slot)
68    }
69}
70
71#[derive(Default, Debug, Clone)]
72pub struct SwapStats {
73    pub puts: u64,
74    pub gets: u64,
75    pub get_miss: u64,
76    pub spills_to_disk: u64,
77    pub faults_from_disk: u64,
78    pub resident_hits: u64,
79    /// 2026-09-25: On-disk records dropped by the disk cap; a later get of a
80    /// dropped key is a miss.
81    pub disk_evictions: u64,
82}