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}