Skip to main content

yeti_types/backend/
config.rs

1//! Storage backend configuration: `BackendType`, `ConsistencyMode`,
2//! `StorageConfig`.
3
4use crate::error::{Result, YetiError};
5
6// ============================================================================
7// BackendType
8// ============================================================================
9
10/// Storage backend type — binary choice: disk (persistent) vs memory
11/// (volatile). Replication is a separate axis handled by `@distribute`;
12/// this enum only says whether the bytes hit disk or stay in RAM.
13///
14/// User-facing values: `"memory"` or `"disk"`. `"disk"` is the default
15/// and typically omitted in schema; `"memory"` is the opt-in for
16/// volatility-tolerant workloads.
17///
18/// The concrete disk engine (`RocksDB` today; potentially others later)
19/// is an implementation detail the operator doesn't care about —
20/// hence "disk" rather than naming the engine. Legacy values
21/// `"rocksdb"` and `"distributed"` still parse for backward
22/// compatibility; new code should use `"disk"`.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
24pub enum BackendType {
25    /// On-disk persistent storage. Engine is a platform detail
26    /// (currently sharded `RocksDB`; swappable without schema changes).
27    /// Default; users typically don't set this explicitly.
28    #[default]
29    Disk,
30    /// In-memory `HashMap` — volatile, fast, no disk. First-class
31    /// production option for workloads whose data tolerates
32    /// reset-on-crash: rate-limit buckets, session caches, dedup
33    /// sets, leaderboards rebuilt from source-of-truth, ephemeral
34    /// workflow scratch. ~10x lower write latency than disk, no
35    /// fsync tail.
36    ///
37    /// Replication (cross-node state) is handled by the `@distribute`
38    /// directive, which wraps any `KvBackend` regardless of concrete
39    /// type — memory-backed tables can still participate in
40    /// replication when explicitly opted in.
41    InMemory,
42}
43
44impl std::str::FromStr for BackendType {
45    type Err = YetiError;
46
47    fn from_str(s: &str) -> Result<Self, Self::Err> {
48        if s.eq_ignore_ascii_case("memory")
49            || s.eq_ignore_ascii_case("inmemory")
50            || s.eq_ignore_ascii_case("in-memory")
51        {
52            Ok(Self::InMemory)
53        } else if s.eq_ignore_ascii_case("disk")
54            || s.eq_ignore_ascii_case("rocksdb")
55            || s.eq_ignore_ascii_case("rocks")
56            || s.eq_ignore_ascii_case("distributed")
57        {
58            Ok(Self::Disk)
59        } else {
60            Err(YetiError::Validation(format!(
61                "Unknown storage type: '{s}'. Valid options: 'disk' (default) | 'memory'"
62            )))
63        }
64    }
65}
66
67impl std::fmt::Display for BackendType {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        match self {
70            Self::Disk => write!(f, "disk"),
71            Self::InMemory => write!(f, "memory"),
72        }
73    }
74}
75
76// ============================================================================
77// ConsistencyMode
78// ============================================================================
79
80/// Consistency mode for table writes.
81///
82/// - `Eventual` (default): Writes append to WAL, consumer drains in batches.
83/// - `Strong`: Writes go directly to the backend, read-after-write consistency.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
85#[serde(rename_all = "lowercase")]
86pub enum ConsistencyMode {
87    /// Writes go directly to the backend. Read-after-write consistency.
88    Strong,
89    /// Writes go through WAL with batched consumer. Higher throughput.
90    #[default]
91    Eventual,
92}
93
94impl std::fmt::Display for ConsistencyMode {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        match self {
97            Self::Strong => write!(f, "strong"),
98            Self::Eventual => write!(f, "eventual"),
99        }
100    }
101}
102
103impl std::str::FromStr for ConsistencyMode {
104    type Err = YetiError;
105
106    fn from_str(s: &str) -> Result<Self, Self::Err> {
107        match s.to_lowercase().as_str() {
108            "strong" => Ok(Self::Strong),
109            "eventual" => Ok(Self::Eventual),
110            _ => Err(YetiError::Validation(format!(
111                "Unknown consistency mode: '{s}'. Valid options: 'strong', 'eventual'"
112            ))),
113        }
114    }
115}
116
117// ============================================================================
118// StorageConfig
119// ============================================================================
120
121/// Configuration for storage backends.
122#[derive(Debug, Clone)]
123#[expect(
124    clippy::struct_excessive_bools,
125    reason = "config DTO; each bool maps 1:1 to a documented yeti-config.yaml storage knob (compression, sync_writes, disable_wal, in_memory). Bitflags would obscure call-site clarity at construction (StorageConfig { sync_writes: true, .. }) and the Default::default() shape readers expect."
126)]
127pub struct StorageConfig {
128    /// Cache size in MB
129    pub cache_size_mb: usize,
130    /// Write buffer size in MB
131    pub write_buffer_size_mb: usize,
132    /// Enable compression
133    pub enable_compression: bool,
134    /// Sync writes to disk
135    pub sync_writes: bool,
136    /// Disable write-ahead log (WAL).
137    ///
138    /// **Default: `true`** — Yeti follows Harper's "WAL off by default for
139    /// user data, durability via replication" model (`harperfast/harper`
140    /// `resources/databases.ts:openRocksDatabase` defaults `disableWAL ??=
141    /// true`). User-data backends opened via `create_rocksdb_backend_manager`
142    /// inherit this default. System databases that need crash durability
143    /// (`yeti-auth`, `yeti-admin`, `yeti-audit`, `yeti-queue`,
144    /// fabric/control-plane stores) are opened with `with_wal_enabled()` in
145    /// `crates/runtime/yeti-server/src/app_loader/backends.rs`.
146    pub disable_wal: bool,
147    /// Time-to-live for records (None = disabled)
148    pub ttl: Option<std::time::Duration>,
149    /// Override shard count (default: `num_cpus` / 2, min 2)
150    pub shard_count: Option<usize>,
151    /// When true, ALL tables use the in-memory backend (`HashMap`)
152    /// rather than `RocksDB` — typical for ephemeral sidecar
153    /// deployments, cache-tier nodes, and dev environments. Per-table
154    /// `BackendType::InMemory` is the more targeted knob: operators
155    /// who want one volatile table alongside persistent ones should
156    /// use that instead. RocksDB-specific knobs above are ignored
157    /// when this is true.
158    pub in_memory: bool,
159}
160
161impl Default for StorageConfig {
162    fn default() -> Self {
163        Self {
164            cache_size_mb: 2048,
165            write_buffer_size_mb: 512,
166            enable_compression: true,
167            sync_writes: false,
168            // Mirror Harper's policy: user-data backends skip the WAL by
169            // default (~6× write throughput). Crash durability is provided by
170            // peer-to-peer replication when configured. Single-node
171            // deployments without replication should pass an explicit config
172            // with `with_wal_enabled()` for system data.
173            disable_wal: true,
174            ttl: None,
175            shard_count: None,
176            in_memory: false,
177        }
178    }
179}
180
181impl StorageConfig {
182    /// Set cache size in MB.
183    #[must_use]
184    pub const fn with_cache_mb(mut self, size_mb: usize) -> Self {
185        self.cache_size_mb = size_mb;
186        self
187    }
188
189    /// Set write buffer size in MB.
190    #[must_use]
191    pub const fn with_write_buffer_mb(mut self, size_mb: usize) -> Self {
192        self.write_buffer_size_mb = size_mb;
193        self
194    }
195
196    /// Enable synchronous writes to disk.
197    #[must_use]
198    pub const fn with_sync_writes(mut self) -> Self {
199        self.sync_writes = true;
200        self
201    }
202
203    /// Disable the Write-Ahead-Log explicitly. Default is already
204    /// `disable_wal: true`, so this is now a no-op idempotent setter — kept
205    /// for call-sites that want the intent visible in code (e.g. test
206    /// fixtures, doc examples).
207    #[must_use]
208    pub const fn with_disable_wal(mut self) -> Self {
209        self.disable_wal = true;
210        self
211    }
212
213    /// Enable the Write-Ahead-Log. Use for backends that hold data with no
214    /// recovery path other than the WAL — `yeti-auth` (users/roles),
215    /// `yeti-admin` (app catalog), `yeti-audit` (audit log itself),
216    /// `yeti-queue` (durable queue), and fabric/control-plane state.
217    /// Roughly 6× slower writes vs. WAL-off on the bench harness; the
218    /// tradeoff is crash durability without replication.
219    #[must_use]
220    pub const fn with_wal_enabled(mut self) -> Self {
221        self.disable_wal = false;
222        self
223    }
224
225    /// Enable TTL (time-to-live) for records.
226    #[must_use]
227    pub const fn with_ttl(mut self, ttl: std::time::Duration) -> Self {
228        self.ttl = Some(ttl);
229        self
230    }
231}
232
233/// Get recommended shard count based on available CPUs.
234#[must_use]
235pub fn default_shard_count(num_cpus: usize) -> usize {
236    (num_cpus / 2).max(2)
237}