grafeo_common/memory/buffer/tiered.rs
1//! Tiered storage: trait for data structures that can live in RAM or on disk.
2//!
3//! The [`TieredStore`] trait defines how a storage subsystem transitions
4//! between memory tiers. The [`BufferManager`](super::BufferManager) decides
5//! *when* to transition (based on memory pressure); this trait defines *how*.
6//!
7//! # Storage states
8//!
9//! ```text
10//! ┌──────────────┐ spill() ┌──────────────┐
11//! │ InMemory │ ────────────> │ OnDisk │
12//! │ (RAM, fast) │ │ (mmap, warm) │
13//! └──────────────┘ └──────┬───────┘
14//! ▲ │
15//! └───────── reload() ────────────┘
16//! ```
17//!
18//! All states expose the same read interface. Callers never need to know
19//! which tier is active.
20
21use std::path::Path;
22
23use crate::utils::error::Result;
24
25/// The current storage tier of a data structure.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27#[non_exhaustive]
28pub enum StorageTier {
29 /// Fully in RAM. Fastest access for both reads and writes.
30 InMemory,
31 /// On disk, accessed via mmap. The OS page cache provides warm reads.
32 /// Mutations go through a WAL overlay.
33 OnDisk,
34 /// Not yet initialized (structure exists but has no data).
35 Uninitialized,
36}
37
38impl StorageTier {
39 /// Returns `true` if data is fully in RAM.
40 #[must_use]
41 pub fn is_in_memory(self) -> bool {
42 self == Self::InMemory
43 }
44
45 /// Returns `true` if data is served from disk (mmap).
46 #[must_use]
47 pub fn is_on_disk(self) -> bool {
48 self == Self::OnDisk
49 }
50}
51
52/// A storage subsystem that can transition between RAM and disk tiers.
53///
54/// **Deprecated (Phase 8c, 0.5.42):** the [`crate::storage::section::Section`]
55/// trait subsumed this trait's responsibilities — its `swap_to_mmap` and
56/// `reload_to_ram` methods cover the same lifecycle, and every wired
57/// section type (LPG, RDF, Vector, Ring, Compact) implements `Section`,
58/// not `TieredStore`. This trait was never implemented anywhere; it is
59/// kept for one release as a no-op to avoid breaking downstream callers
60/// who may have prepared their own impls. Migrate to `Section` for new
61/// code; the standalone trait will be removed in 0.6.0.
62///
63/// Implementors manage their own data layout for both tiers. The
64/// [`BufferManager`](super::BufferManager) triggers transitions via the
65/// [`MemoryConsumer`](super::MemoryConsumer) trait; this trait provides
66/// the mechanics of persisting, mapping, and reloading data.
67///
68/// # Crate boundaries
69///
70/// This trait lives in `grafeo-common` so it can be referenced by both
71/// `grafeo-core` (section serializers) and `grafeo-engine` (orchestration).
72/// Implementations that need filesystem I/O (mmap, file creation) should
73/// live in `grafeo-engine`, not `grafeo-core`.
74#[deprecated(
75 since = "0.5.42",
76 note = "Use the `Section` trait (with `swap_to_mmap` / `reload_to_ram`) and `MemoryConsumer` instead. \
77 This trait was never implemented anywhere; it will be removed in 0.6.0."
78)]
79pub trait TieredStore: Send + Sync {
80 /// Estimated RAM footprint in bytes if built entirely in memory.
81 ///
82 /// Returns 0 when the structure is on disk or uninitialized.
83 fn estimated_ram_bytes(&self) -> usize;
84
85 /// Current storage tier.
86 fn tier(&self) -> StorageTier;
87
88 /// Serializes in-memory state to the given path.
89 ///
90 /// After this call, [`open_mmap`](Self::open_mmap) can serve reads
91 /// from the file. This does NOT free RAM: the caller should drop the
92 /// in-memory representation separately (typically via the
93 /// [`MemoryConsumer::spill`](super::MemoryConsumer::spill) path).
94 ///
95 /// # Errors
96 ///
97 /// Returns an error if serialization or I/O fails.
98 fn persist(&self, path: &Path) -> Result<()>;
99
100 /// Switches to mmap-backed reads from a previously persisted file.
101 ///
102 /// Drops the in-memory data and serves reads through the OS page cache.
103 /// The tier transitions to [`StorageTier::OnDisk`].
104 ///
105 /// # Errors
106 ///
107 /// Returns an error if the file cannot be memory-mapped.
108 fn open_mmap(&self, path: &Path) -> Result<()>;
109
110 /// Reloads data from disk back into RAM.
111 ///
112 /// The tier transitions to [`StorageTier::InMemory`]. Used when memory
113 /// becomes available and faster access is desired.
114 ///
115 /// # Errors
116 ///
117 /// Returns an error if deserialization fails.
118 fn reload_to_ram(&self) -> Result<()>;
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn test_storage_tier_predicates() {
127 assert!(StorageTier::InMemory.is_in_memory());
128 assert!(!StorageTier::InMemory.is_on_disk());
129
130 assert!(StorageTier::OnDisk.is_on_disk());
131 assert!(!StorageTier::OnDisk.is_in_memory());
132
133 assert!(!StorageTier::Uninitialized.is_in_memory());
134 assert!(!StorageTier::Uninitialized.is_on_disk());
135 }
136
137 #[test]
138 fn test_storage_tier_equality() {
139 assert_eq!(StorageTier::InMemory, StorageTier::InMemory);
140 assert_ne!(StorageTier::InMemory, StorageTier::OnDisk);
141 assert_ne!(StorageTier::OnDisk, StorageTier::Uninitialized);
142 }
143}