Skip to main content

kevy_config/
schema.rs

1//! kevy `Config` schema, defaults, and error type. Apply-from-parser and
2//! value-coercion logic lives in `apply.rs` so this file stays focused on
3//! "what the settings ARE".
4
5use std::path::PathBuf;
6
7// ───────────── enums ─────────────
8// The four Redis-compatible enums live in `crate::enums` (500-LOC house
9// cap); re-exported here so `crate::schema::{AppendFsync, …}` paths keep
10// working unchanged.
11pub use crate::enums::{AppendFsync, EvictionPolicy, LogLevel, LogOutput};
12
13// ───────────── sections ─────────────
14
15/// `[server]` section.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct ServerSection {
18    /// IPv4 bind address. Default `127.0.0.1`.
19    pub bind: [u8; 4],
20    /// TCP port. Default `6004`.
21    pub port: u16,
22    /// Shard / reactor thread count. `0` = auto (CPU count). Default `0`.
23    pub threads: usize,
24    /// Only shards `0..N` arm accept SQE; rest stay compute-only.
25    pub accept_shards: Option<usize>,
26    /// Cap on total active client connections. `0` = unlimited.
27    /// Default `10000` (matches Redis). New connection past cap is closed
28    /// + `rejected_connections` counter increments + INFO clients reports.
29    pub max_clients: usize,
30    /// Snapshot + AOF location. Default `.`.
31    pub data_dir: PathBuf,
32}
33
34impl Default for ServerSection {
35    fn default() -> Self {
36        Self {
37            bind: [127, 0, 0, 1],
38            port: 6004,
39            threads: 0,
40            accept_shards: None,
41            max_clients: 10_000,
42            data_dir: PathBuf::from("."),
43        }
44    }
45}
46
47/// `[persistence]` section.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct PersistenceSection {
50    /// Append-only file enabled. Default `true`.
51    pub aof: bool,
52    /// AOF fsync policy. Default `EverySec`.
53    pub appendfsync: AppendFsync,
54    /// Trigger BGREWRITEAOF when current AOF is at least this fraction
55    /// (as a percent — 100 = 2× the last-rewrite size) larger than the
56    /// last rewrite. Default `100`.
57    pub auto_aof_rewrite_percentage: u32,
58    /// Never auto-rewrite an AOF smaller than this. Default `64mb` =
59    /// `64 * 1024 * 1024`.
60    pub auto_aof_rewrite_min_size: u64,
61    /// Absolute-size auto-rewrite trigger: compact whenever the AOF
62    /// reaches this many bytes, regardless of growth ratio. `0` = rule
63    /// off (the default). The growth rule alone lets a large log double
64    /// before compacting — this caps it outright.
65    pub auto_aof_rewrite_bytes: u64,
66    /// Time-based auto-rewrite trigger: compact at least this often (in
67    /// seconds) while the log grows. `0` = rule off (the default).
68    pub auto_aof_rewrite_interval_secs: u64,
69    /// Best-effort boot replay: recover the good records behind a corrupt
70    /// v2 AOF record instead of dropping them. Default `false` (strict).
71    pub replay_resync: bool,
72}
73
74impl Default for PersistenceSection {
75    fn default() -> Self {
76        Self {
77            aof: true,
78            appendfsync: AppendFsync::EverySec,
79            auto_aof_rewrite_percentage: 100,
80            auto_aof_rewrite_min_size: 64 * 1024 * 1024,
81            auto_aof_rewrite_bytes: 0,
82            auto_aof_rewrite_interval_secs: 0,
83            replay_resync: false,
84        }
85    }
86}
87
88/// `[memory]` section.
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub struct MemorySection {
91    /// Soft memory ceiling in bytes. `0` = unlimited. Default `0`.
92    pub maxmemory: u64,
93    /// Action when `maxmemory` is hit. Default `NoEviction`.
94    pub maxmemory_policy: EvictionPolicy,
95}
96
97impl Default for MemorySection {
98    fn default() -> Self {
99        Self {
100            maxmemory: 0,
101            maxmemory_policy: EvictionPolicy::NoEviction,
102        }
103    }
104}
105
106/// `[metrics]` section — Prometheus-format HTTP exposition.
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108#[derive(Default)]
109pub struct MetricsSection {
110    /// TCP port for the `/metrics` HTTP endpoint. `0` = OFF (default).
111    pub listen_port: u16,
112}
113
114
115/// `[audit]` section — append-only audit log of ADMIN-class
116/// commands (`CONFIG SET` / `CONFIG REWRITE` / `DEBUG` / `FLUSHDB` /
117/// `FLUSHALL` / `CLIENT KILL` / `SCRIPT FLUSH` etc.).
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct AuditSection {
120    /// Append-only audit log file. Empty string = OFF (default).
121    pub log_path: PathBuf,
122}
123
124impl Default for AuditSection {
125    fn default() -> Self {
126        Self { log_path: PathBuf::new() }
127    }
128}
129
130/// `[expiry]` section. Controls the TTL background reaper.
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub struct ExpirySection {
133    /// Reaper frequency in Hz. Default `10` (every 100 ms).
134    pub hz: u32,
135    /// Keys sampled per reaper cycle. Default `20`.
136    pub sample: u32,
137}
138
139impl Default for ExpirySection {
140    fn default() -> Self {
141        Self { hz: 10, sample: 20 }
142    }
143}
144
145/// `[log]` section.
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct LogSection {
148    /// Log verbosity. Default `Info`.
149    pub level: LogLevel,
150    /// Log sink. Default `Stderr`.
151    pub output: LogOutput,
152}
153
154impl Default for LogSection {
155    fn default() -> Self {
156        Self {
157            level: LogLevel::Info,
158            output: LogOutput::Stderr,
159        }
160    }
161}
162
163/// `[advanced]` section — reactor-loop tuning knobs that used to be
164/// hardcoded `const`s in `kevy-rt`. Defaults match the previously
165/// hardcoded values, so the existing benchmark numbers
166/// translate one-to-one. Tune only if you know what you're doing
167/// (`bench/REPORT.md` documents the trade-offs).
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub struct AdvancedSection {
170    /// Iterations the per-core reactor spins on `poll(timeout=0)`
171    /// before parking on a blocking wait. Higher = lower wake-up
172    /// latency under contention, higher idle CPU; lower = the inverse.
173    /// Default `256` (matches the original hardcoded const).
174    pub spin_limit: u32,
175    /// Bounded blocking wait in ms once the reactor parks. Acts as a
176    /// safety backstop for any missed cross-core wake (the per-pair
177    /// SeqCst fence is the primary mechanism).
178    /// Default `50` ms.
179    pub park_timeout_ms: u32,
180    /// How many reactor loop iterations between wall-clock reads for
181    /// the tick (TTL reaper / auto-AOF-rewrite / live-config refresh).
182    /// In busy-poll mode (~1M iter/s) the default `256` is one check
183    /// per ~256 µs — plenty for a 10 Hz tick. In park mode the
184    /// reactor bypasses this throttle (each iter is already ≥ 1 ms),
185    /// so the value only matters under sustained load. Default `256`.
186    pub tick_check_every: u32,
187    /// Per-direction SPSC ring slot count (one ring per ordered
188    /// core-pair). Must be a power of two; the ring code rounds up.
189    /// Overflow spills to a local backlog Vec rather than blocking,
190    /// so a small ring just shifts work to the slower path. Default
191    /// `1024`.
192    pub ring_capacity: usize,
193}
194
195impl Default for AdvancedSection {
196    fn default() -> Self {
197        Self {
198            spin_limit: 256,
199            park_timeout_ms: 50,
200            tick_check_every: 256,
201            ring_capacity: 1024,
202        }
203    }
204}
205
206/// `[notification]` section. `notify_keyspace_events` is a string of
207/// flag chars (Redis convention): `K` keyspace channel, `E` keyevent
208/// channel, `g` generic cmds, `$` string cmds, `l` list, `s` set, `h`
209/// hash, `z` zset, `t` stream, `x` expired events, `e` evicted
210/// events, `n` new-key events, `A` alias for `g$lshztxe` (every
211/// event class except `n`, matching Redis's `A`). Default empty =
212/// OFF (Redis default — zero hot-path cost). Any other character is
213/// a config error.
214///
215/// Example: `notify_keyspace_events = "KEA"` enables every event
216/// class on BOTH channels. `"K$"` enables only string events on the
217/// keyspace channel.
218#[derive(Debug, Clone, Default, PartialEq, Eq)]
219pub struct NotificationSection {
220    /// Flag string controlling which keyspace notifications fire. Empty
221    /// (default) = OFF: writes pay one atomic load + skip, no publish.
222    pub notify_keyspace_events: String,
223}
224
225/// Parsed view of [`NotificationSection::notify_keyspace_events`]. The
226/// runtime caches this struct per-shard (hot-reload via the existing
227/// `LiveRuntimeConfig` tick path) so the per-write-command check
228/// reduces to four bool reads on the hot path.
229// struct_excessive_bools: each field mirrors one independent letter of the
230// redis notify-keyspace-events flag string; they are flags, not a state machine.
231#[allow(clippy::struct_excessive_bools)]
232#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
233pub struct NotificationFlags {
234    /// `K` — publish on `__keyspace@<db>__:<key>` channel.
235    pub keyspace: bool,
236    /// `E` — publish on `__keyevent@<db>__:<event>` channel.
237    pub keyevent: bool,
238    /// `g` — DEL / EXPIRE / PERSIST / RENAME / TYPE / FLUSH etc.
239    pub generic: bool,
240    /// `$` — SET / GETSET / INCR* / APPEND / MSET / etc.
241    pub string: bool,
242    /// `l` — LPUSH / RPUSH / LPOP / RPOP / LREM / LSET / LTRIM / …
243    pub list: bool,
244    /// `s` — SADD / SREM / SPOP / SMOVE / …
245    pub set: bool,
246    /// `h` — HSET / HDEL / HINCRBY / HSETNX / …
247    pub hash: bool,
248    /// `z` — ZADD / ZINCRBY / ZREM / ZREMRANGEBY* / …
249    pub zset: bool,
250    /// `t` — XADD / XDEL / XTRIM / XGROUP / XACK / XCLAIM / XREADGROUP …
251    pub stream: bool,
252    /// `x` — `expired` events, fired when a TTL'd key is removed
253    /// (lazily on access or by the active reaper).
254    pub expired: bool,
255    /// `e` — `evicted` events, fired when maxmemory pressure removes
256    /// a key.
257    pub evicted: bool,
258    /// `n` — `new` events, fired when a key is added to the keyspace.
259    /// Not part of the `A` alias (Redis convention).
260    pub new_key: bool,
261}
262
263impl NotificationFlags {
264    /// Notifications are entirely off (no channel enabled OR no class
265    /// enabled). The hot-path emits skip via this check before any
266    /// further classification or string formatting.
267    pub fn is_empty(&self) -> bool {
268        !(self.keyspace || self.keyevent)
269            || !(self.generic
270                || self.string
271                || self.list
272                || self.set
273                || self.hash
274                || self.zset
275                || self.stream
276                || self.expired
277                || self.evicted
278                || self.new_key)
279    }
280}
281
282/// `[slowlog]` section — controls the per-shard slow-command ring
283/// buffer surfaced by `SLOWLOG GET/LEN/RESET`. Default is OFF
284/// (`slower_than_micros = -1`) so the hot path never pays the
285/// `Instant::now()` pair around dispatch (~30 ns/op, ≈9 % at 3 M
286/// ops/s). To enable Redis-style 10 ms tracking, set
287/// `slower_than_micros = 10000` in `[slowlog]` or run
288/// `CONFIG SET slowlog-log-slower-than 10000`.
289/// `[lua]` section — Lua scripting limits.
290#[derive(Debug, Clone, PartialEq, Eq)]
291pub struct LuaSection {
292    /// Hard cap on per-`EVAL` Lua execution time in milliseconds.
293    /// Matches Redis's `lua-time-limit`. The bridge translates this
294    /// to a luna-core instruction budget at VM construction time using
295    /// a conservative 40 000-instr/ms estimate (so 5000 ms ≈ 200 M
296    /// instructions, the same default that used to be hard-coded).
297    /// Set to 0 to disable the cap (unlimited execution).
298    /// Default: 5000.
299    pub time_limit_ms: u64,
300    /// Whitelist of allowed Lua dialects. Empty = all five
301    /// (5.1/5.2/5.3/5.4/5.5) accepted. Set to `["5.1"]` to lock the
302    /// server to pure Redis ecosystem-compat mode and reject any
303    /// EVAL whose `#!lua version=N` shebang asks for a newer
304    /// dialect. Default: empty (all dialects).
305    pub allow_dialects: Vec<String>,
306}
307
308impl Default for LuaSection {
309    fn default() -> Self {
310        Self {
311            time_limit_ms: 5000,
312            allow_dialects: Vec::new(),
313        }
314    }
315}
316
317/// `[slowlog]` section — ring buffer of slow commands per shard.
318#[derive(Debug, Clone, Copy, PartialEq, Eq)]
319pub struct SlowlogSection {
320    /// Record any command whose execution took at least this many
321    /// microseconds (Redis: `< slower_than_micros` is skipped). `-1`
322    /// disables the log (zero hot-path cost — no `Instant::now()`
323    /// taken); `0` records every command. Default `-1` (OFF).
324    pub slower_than_micros: i64,
325    /// Cap on the per-shard ring buffer. Once exceeded, the oldest
326    /// entry is dropped to make room. Across `nshards` shards the
327    /// effective server-wide cap is `max_len * nshards`. Default `128`.
328    pub max_len: u32,
329}
330
331impl Default for SlowlogSection {
332    fn default() -> Self {
333        Self {
334            slower_than_micros: -1,
335            max_len: 128,
336        }
337    }
338}
339
340/// Parse a Redis-style `notify_keyspace_events` flag string into
341/// [`NotificationFlags`]. The `A` alias enables every event-class
342/// flag except channels and `n` (Redis convention). An unknown char
343/// is an error carrying the offending character — a typo'd flag
344/// string must fail config admission, not silently drop events.
345pub fn parse_notification_flags(s: &str) -> Result<NotificationFlags, char> {
346    let mut f = NotificationFlags::default();
347    for c in s.chars() {
348        match c {
349            'K' => f.keyspace = true,
350            'E' => f.keyevent = true,
351            'g' => f.generic = true,
352            '$' => f.string = true,
353            'l' => f.list = true,
354            's' => f.set = true,
355            'h' => f.hash = true,
356            'z' => f.zset = true,
357            't' => f.stream = true,
358            'x' => f.expired = true,
359            'e' => f.evicted = true,
360            'n' => f.new_key = true,
361            'A' => {
362                // Alias for "g$lshztxe" — every event class except
363                // `n`, per the Redis contract for `A`.
364                f.generic = true;
365                f.string = true;
366                f.list = true;
367                f.set = true;
368                f.hash = true;
369                f.zset = true;
370                f.stream = true;
371                f.expired = true;
372                f.evicted = true;
373            }
374            other => return Err(other),
375        }
376    }
377    Ok(f)
378}
379/// the TOML file + env + CLI.
380#[derive(Debug, Clone, PartialEq, Eq, Default)]
381pub struct Config {
382    /// `[server]` settings.
383    pub server: ServerSection,
384    /// `[persistence]` settings.
385    pub persistence: PersistenceSection,
386    /// `[memory]` settings.
387    pub memory: MemorySection,
388    /// `[metrics]` settings (Prometheus /metrics endpoint).
389    pub metrics: MetricsSection,
390    /// `[audit]` settings (append-only ADMIN-command audit).
391    pub audit: AuditSection,
392    /// `[expiry]` settings.
393    pub expiry: ExpirySection,
394    /// `[log]` settings.
395    pub log: LogSection,
396    /// `[notification]` settings (keyspace events).
397    pub notification: NotificationSection,
398    /// `[advanced]` settings (reactor tuning knobs).
399    pub advanced: AdvancedSection,
400    /// `[slowlog]` settings (slow-command ring buffer).
401    pub slowlog: SlowlogSection,
402    /// `[cluster]` settings (single-node cluster mode).
403    pub cluster: crate::cluster::ClusterSection,
404    /// `[lua]` settings — server-side Lua scripting via the
405    /// `kevy-lua` bridge.
406    pub lua: LuaSection,
407    /// `[replication]` settings — primary/replica streaming.
408    pub replication: crate::replication::ReplicationSection,
409    /// `[feed]` settings — CDC consumer surface (FEED.*).
410    pub feed: FeedSection,
411    /// `[tiering]` settings — the transparent-tiering RAM budget
412    /// (capacity arc). No budget = tiering off.
413    pub tiering: crate::tiering::TieringSection,
414    /// Path the config was loaded from (for `CONFIG REWRITE`). `None` =
415    /// loaded from defaults only / from in-memory string.
416    pub source_path: Option<PathBuf>,
417}
418
419/// `[feed]` — the CDC consumer surface. When enabled every shard
420/// keeps a mutation backlog (even with no replicas) and serves
421/// `FEED.READ` / `FEED.TAIL` under the `(generation, offset)` cursor
422/// contract (docs/cdc.md).
423#[derive(Clone, Debug, PartialEq, Eq)]
424pub struct FeedSection {
425    /// Enable the FEED.* surface. Default `false`.
426    pub enabled: bool,
427    /// Per-shard backlog byte budget. Default `64mb`; hard cap `1gb`
428    /// (bring-up refuses louder budgets — memory formula:
429    /// `nshards × feed_buffer_size` upper bound).
430    pub feed_buffer_size: u64,
431}
432
433impl Default for FeedSection {
434    fn default() -> Self {
435        Self { enabled: false, feed_buffer_size: 64 * 1024 * 1024 }
436    }
437}
438
439// `ConfigError` lives in [`crate::error`] — split out so this file
440// stays under the 500-LOC house rule. Re-exported below for any caller
441// that still does `kevy_config::schema::ConfigError`.
442pub use crate::error::ConfigError;