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
9/// AOF fsync policy. Matches Redis `appendfsync`.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum AppendFsync {
12 /// `fsync` after every write command. Zero data-loss but ~50% throughput.
13 Always,
14 /// Background `fsync` every second. Lose at most 1s on crash. Default.
15 EverySec,
16 /// No explicit `fsync`; let OS pagecache flush. Lose ~30s on crash.
17 No,
18}
19
20impl AppendFsync {
21 /// Canonical Redis-compatible name (`always` / `everysec` / `no`).
22 /// Used by `CONFIG GET appendfsync` and `CONFIG REWRITE`.
23 pub fn as_str(&self) -> &'static str {
24 match self {
25 Self::Always => "always",
26 Self::EverySec => "everysec",
27 Self::No => "no",
28 }
29 }
30 /// Inverse of [`Self::as_str`] — case-insensitive. `None` for any
31 /// other input; used by both the TOML parser and `CONFIG SET`.
32 pub fn parse(s: &str) -> Option<Self> {
33 match s.to_ascii_lowercase().as_str() {
34 "always" => Some(Self::Always),
35 "everysec" => Some(Self::EverySec),
36 "no" => Some(Self::No),
37 _ => None,
38 }
39 }
40}
41
42/// Maxmemory eviction policy. 8 variants matching Redis. `NoEviction`
43/// (default) returns an error on writes once `maxmemory` is hit.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum EvictionPolicy {
46 /// Refuse writes once `maxmemory` is hit. Default.
47 NoEviction,
48 /// Approximated LRU across all keys.
49 AllKeysLru,
50 /// Approximated LFU across all keys.
51 AllKeysLfu,
52 /// Random key across all keys.
53 AllKeysRandom,
54 /// Approximated LRU across keys with a TTL.
55 VolatileLru,
56 /// Approximated LFU across keys with a TTL.
57 VolatileLfu,
58 /// Random key from those with a TTL.
59 VolatileRandom,
60 /// Key with the shortest remaining TTL.
61 VolatileTtl,
62}
63
64impl EvictionPolicy {
65 /// Canonical Redis-compatible name.
66 pub fn as_str(&self) -> &'static str {
67 match self {
68 Self::NoEviction => "noeviction",
69 Self::AllKeysLru => "allkeys-lru",
70 Self::AllKeysLfu => "allkeys-lfu",
71 Self::AllKeysRandom => "allkeys-random",
72 Self::VolatileLru => "volatile-lru",
73 Self::VolatileLfu => "volatile-lfu",
74 Self::VolatileRandom => "volatile-random",
75 Self::VolatileTtl => "volatile-ttl",
76 }
77 }
78 /// Inverse of [`Self::as_str`] — case-insensitive.
79 pub fn parse(s: &str) -> Option<Self> {
80 match s.to_ascii_lowercase().as_str() {
81 "noeviction" => Some(Self::NoEviction),
82 "allkeys-lru" => Some(Self::AllKeysLru),
83 "allkeys-lfu" => Some(Self::AllKeysLfu),
84 "allkeys-random" => Some(Self::AllKeysRandom),
85 "volatile-lru" => Some(Self::VolatileLru),
86 "volatile-lfu" => Some(Self::VolatileLfu),
87 "volatile-random" => Some(Self::VolatileRandom),
88 "volatile-ttl" => Some(Self::VolatileTtl),
89 _ => None,
90 }
91 }
92}
93
94/// Log verbosity.
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum LogLevel {
97 /// Very chatty, useful when debugging a kevy internal bug.
98 Trace,
99 /// Per-command / per-event detail; turn on locally to chase issues.
100 Debug,
101 /// Default; startup banner, WARNs, errors, key lifecycle events.
102 Info,
103 /// Only non-fatal warnings (e.g. unprotected bind) and errors.
104 Warn,
105 /// Only fatal errors.
106 Error,
107}
108
109impl LogLevel {
110 /// Canonical name. `Warn` renders as `warning` (Redis convention).
111 pub fn as_str(&self) -> &'static str {
112 match self {
113 Self::Trace => "trace",
114 Self::Debug => "debug",
115 Self::Info => "info",
116 Self::Warn => "warning",
117 Self::Error => "error",
118 }
119 }
120 /// Inverse of [`Self::as_str`] — case-insensitive; accepts both
121 /// `warn` and `warning` for the Warn level.
122 pub fn parse(s: &str) -> Option<Self> {
123 match s.to_ascii_lowercase().as_str() {
124 "trace" => Some(Self::Trace),
125 "debug" => Some(Self::Debug),
126 "info" => Some(Self::Info),
127 "warn" | "warning" => Some(Self::Warn),
128 "error" => Some(Self::Error),
129 _ => None,
130 }
131 }
132}
133
134/// Where to write log output.
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub enum LogOutput {
137 /// Write to standard error (default).
138 Stderr,
139 /// Write to standard output.
140 Stdout,
141 /// Append to the named file (path resolved relative to cwd at startup).
142 File(PathBuf),
143}
144
145impl LogOutput {
146 /// Canonical name. `File(p)` renders as the path string.
147 pub fn as_str(&self) -> std::borrow::Cow<'_, str> {
148 match self {
149 Self::Stderr => "stderr".into(),
150 Self::Stdout => "stdout".into(),
151 Self::File(p) => p.display().to_string().into(),
152 }
153 }
154 /// Inverse of [`Self::as_str`]: `stderr` / `stdout` reserved; any
155 /// other string is treated as a file path.
156 pub fn parse(s: &str) -> Self {
157 match s {
158 "stderr" => Self::Stderr,
159 "stdout" => Self::Stdout,
160 path => Self::File(PathBuf::from(path)),
161 }
162 }
163}
164
165// ───────────── sections ─────────────
166
167/// `[server]` section.
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct ServerSection {
170 /// IPv4 bind address. Default `127.0.0.1`.
171 pub bind: [u8; 4],
172 /// TCP port. Default `6004`.
173 pub port: u16,
174 /// Shard / reactor thread count. `0` = auto (CPU count). Default `0`.
175 pub threads: usize,
176 /// **v1.30** — Only shards `0..N` arm accept SQE; rest stay compute-only.
177 pub accept_shards: Option<usize>,
178 /// **v1.37** — Cap on total active client connections. `0` = unlimited.
179 /// Default `10000` (matches Redis). New connection past cap is closed
180 /// + `rejected_connections` counter increments + INFO clients reports.
181 pub max_clients: usize,
182 /// Snapshot + AOF location. Default `.`.
183 pub data_dir: PathBuf,
184}
185
186impl Default for ServerSection {
187 fn default() -> Self {
188 Self {
189 bind: [127, 0, 0, 1],
190 port: 6004,
191 threads: 0,
192 accept_shards: None,
193 max_clients: 10_000,
194 data_dir: PathBuf::from("."),
195 }
196 }
197}
198
199/// `[persistence]` section.
200#[derive(Debug, Clone, PartialEq, Eq)]
201pub struct PersistenceSection {
202 /// Append-only file enabled. Default `true`.
203 pub aof: bool,
204 /// AOF fsync policy. Default `EverySec`.
205 pub appendfsync: AppendFsync,
206 /// Trigger BGREWRITEAOF when current AOF is at least this fraction
207 /// (as a percent — 100 = 2× the last-rewrite size) larger than the
208 /// last rewrite. Default `100`.
209 pub auto_aof_rewrite_percentage: u32,
210 /// Never auto-rewrite an AOF smaller than this. Default `64mb` =
211 /// `64 * 1024 * 1024`.
212 pub auto_aof_rewrite_min_size: u64,
213}
214
215impl Default for PersistenceSection {
216 fn default() -> Self {
217 Self {
218 aof: true,
219 appendfsync: AppendFsync::EverySec,
220 auto_aof_rewrite_percentage: 100,
221 auto_aof_rewrite_min_size: 64 * 1024 * 1024,
222 }
223 }
224}
225
226/// `[memory]` section.
227#[derive(Debug, Clone, Copy, PartialEq, Eq)]
228pub struct MemorySection {
229 /// Soft memory ceiling in bytes. `0` = unlimited. Default `0`.
230 pub maxmemory: u64,
231 /// Action when `maxmemory` is hit. Default `NoEviction`.
232 pub maxmemory_policy: EvictionPolicy,
233}
234
235impl Default for MemorySection {
236 fn default() -> Self {
237 Self {
238 maxmemory: 0,
239 maxmemory_policy: EvictionPolicy::NoEviction,
240 }
241 }
242}
243
244/// `[metrics]` section — v1.41. Prometheus-format HTTP exposition.
245#[derive(Debug, Clone, Copy, PartialEq, Eq)]
246pub struct MetricsSection {
247 /// TCP port for the `/metrics` HTTP endpoint. `0` = OFF (default).
248 pub listen_port: u16,
249}
250
251impl Default for MetricsSection {
252 fn default() -> Self {
253 Self { listen_port: 0 }
254 }
255}
256
257/// `[audit]` section — v1.42. Append-only audit log of ADMIN-class
258/// commands (`CONFIG SET` / `CONFIG REWRITE` / `DEBUG` / `FLUSHDB` /
259/// `FLUSHALL` / `CLIENT KILL` / `SCRIPT FLUSH` etc.).
260#[derive(Debug, Clone, PartialEq, Eq)]
261pub struct AuditSection {
262 /// Append-only audit log file. Empty string = OFF (default).
263 pub log_path: PathBuf,
264}
265
266impl Default for AuditSection {
267 fn default() -> Self {
268 Self { log_path: PathBuf::new() }
269 }
270}
271
272/// `[expiry]` section. Controls the TTL background reaper.
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub struct ExpirySection {
275 /// Reaper frequency in Hz. Default `10` (every 100 ms).
276 pub hz: u32,
277 /// Keys sampled per reaper cycle. Default `20`.
278 pub sample: u32,
279}
280
281impl Default for ExpirySection {
282 fn default() -> Self {
283 Self { hz: 10, sample: 20 }
284 }
285}
286
287/// `[log]` section.
288#[derive(Debug, Clone, PartialEq, Eq)]
289pub struct LogSection {
290 /// Log verbosity. Default `Info`.
291 pub level: LogLevel,
292 /// Log sink. Default `Stderr`.
293 pub output: LogOutput,
294}
295
296impl Default for LogSection {
297 fn default() -> Self {
298 Self {
299 level: LogLevel::Info,
300 output: LogOutput::Stderr,
301 }
302 }
303}
304
305/// `[advanced]` section — reactor-loop tuning knobs that used to be
306/// hardcoded `const`s in `kevy-rt`. Defaults match the values shipped
307/// in workspace v1.3 / earlier so the existing benchmark numbers
308/// translate one-to-one. Tune only if you know what you're doing
309/// (`bench/REPORT.md` documents the trade-offs).
310#[derive(Debug, Clone, Copy, PartialEq, Eq)]
311pub struct AdvancedSection {
312 /// Iterations the per-core reactor spins on `poll(timeout=0)`
313 /// before parking on a blocking wait. Higher = lower wake-up
314 /// latency under contention, higher idle CPU; lower = the inverse.
315 /// Default `256` (matches v1.0 const).
316 pub spin_limit: u32,
317 /// Bounded blocking wait in ms once the reactor parks. Acts as a
318 /// safety backstop for any missed cross-core wake (the per-pair
319 /// SeqCst fence is the primary mechanism since workspace v1.3.0).
320 /// Default `50` ms.
321 pub park_timeout_ms: u32,
322 /// How many reactor loop iterations between wall-clock reads for
323 /// the tick (TTL reaper / auto-AOF-rewrite / live-config refresh).
324 /// In busy-poll mode (~1M iter/s) the default `256` is one check
325 /// per ~256 µs — plenty for a 10 Hz tick. In park mode the
326 /// reactor bypasses this throttle (each iter is already ≥ 1 ms),
327 /// so the value only matters under sustained load. Default `256`.
328 pub tick_check_every: u32,
329 /// Per-direction SPSC ring slot count (one ring per ordered
330 /// core-pair). Must be a power of two; the ring code rounds up.
331 /// Overflow spills to a local backlog Vec rather than blocking,
332 /// so a small ring just shifts work to the slower path. Default
333 /// `1024`.
334 pub ring_capacity: usize,
335}
336
337impl Default for AdvancedSection {
338 fn default() -> Self {
339 Self {
340 spin_limit: 256,
341 park_timeout_ms: 50,
342 tick_check_every: 256,
343 ring_capacity: 1024,
344 }
345 }
346}
347
348/// `[notification]` section. `notify_keyspace_events` is a string of
349/// flag chars (Redis convention): `K` keyspace channel, `E` keyevent
350/// channel, `g` generic cmds, `$` string cmds, `l` list, `s` set, `h`
351/// hash, `z` zset, `A` alias for `g$lshz` (every event class except
352/// the not-yet-implemented `x`/`e`/`t`/`n`). Default empty = OFF
353/// (Redis default — zero hot-path cost).
354///
355/// Example: `notify_keyspace_events = "KEA"` enables every event
356/// class on BOTH channels. `"K$"` enables only string events on the
357/// keyspace channel.
358#[derive(Debug, Clone, Default, PartialEq, Eq)]
359pub struct NotificationSection {
360 /// Flag string controlling which keyspace notifications fire. Empty
361 /// (default) = OFF: writes pay one atomic load + skip, no publish.
362 pub notify_keyspace_events: String,
363}
364
365/// Parsed view of [`NotificationSection::notify_keyspace_events`]. The
366/// runtime caches this struct per-shard (hot-reload via the existing
367/// `LiveRuntimeConfig` tick path) so the per-write-command check
368/// reduces to four bool reads on the hot path.
369#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
370pub struct NotificationFlags {
371 /// `K` — publish on `__keyspace@<db>__:<key>` channel.
372 pub keyspace: bool,
373 /// `E` — publish on `__keyevent@<db>__:<event>` channel.
374 pub keyevent: bool,
375 /// `g` — DEL / EXPIRE / PERSIST / RENAME / TYPE / FLUSH etc.
376 pub generic: bool,
377 /// `$` — SET / GETSET / INCR* / APPEND / MSET / etc.
378 pub string: bool,
379 /// `l` — LPUSH / RPUSH / LPOP / RPOP / LREM / LSET / LTRIM / …
380 pub list: bool,
381 /// `s` — SADD / SREM / SPOP / SMOVE / …
382 pub set: bool,
383 /// `h` — HSET / HDEL / HINCRBY / HSETNX / …
384 pub hash: bool,
385 /// `z` — ZADD / ZINCRBY / ZREM / ZREMRANGEBY* / …
386 pub zset: bool,
387 /// `t` — XADD / XDEL / XTRIM / XGROUP / XACK / XCLAIM / XREADGROUP …
388 pub stream: bool,
389}
390
391impl NotificationFlags {
392 /// Notifications are entirely off (no channel enabled OR no class
393 /// enabled). The hot-path emits skip via this check before any
394 /// further classification or string formatting.
395 pub fn is_empty(&self) -> bool {
396 !(self.keyspace || self.keyevent)
397 || !(self.generic
398 || self.string
399 || self.list
400 || self.set
401 || self.hash
402 || self.zset
403 || self.stream)
404 }
405}
406
407/// `[slowlog]` section — controls the per-shard slow-command ring
408/// buffer surfaced by `SLOWLOG GET/LEN/RESET`. Default is OFF
409/// (`slower_than_micros = -1`) so the hot path never pays the
410/// `Instant::now()` pair around dispatch (~30 ns/op, ≈9 % at 3 M
411/// ops/s). To enable Redis-style 10 ms tracking, set
412/// `slower_than_micros = 10000` in `[slowlog]` or run
413/// `CONFIG SET slowlog-log-slower-than 10000`.
414/// `[lua]` section — v1.27 Lua scripting limits.
415#[derive(Debug, Clone, PartialEq, Eq)]
416pub struct LuaSection {
417 /// Hard cap on per-`EVAL` Lua execution time in milliseconds.
418 /// Matches Redis's `lua-time-limit`. The bridge translates this
419 /// to a luna instruction budget at VM construction time using a
420 /// conservative 40 000-instr/ms estimate (so 5000 ms ≈ 200 M
421 /// instructions, the same hard-coded default kevy v1.27 P1-P6
422 /// shipped). Set to 0 to disable the cap (unlimited execution).
423 /// Default: 5000.
424 pub time_limit_ms: u64,
425 /// Whitelist of allowed Lua dialects. Empty = all five
426 /// (5.1/5.2/5.3/5.4/5.5) accepted. Set to `["5.1"]` to lock the
427 /// server to pure Redis ecosystem-compat mode and reject any
428 /// EVAL whose `#!lua version=N` shebang asks for a newer
429 /// dialect. Default: empty (all dialects).
430 pub allow_dialects: Vec<String>,
431}
432
433impl Default for LuaSection {
434 fn default() -> Self {
435 Self {
436 time_limit_ms: 5000,
437 allow_dialects: Vec::new(),
438 }
439 }
440}
441
442/// `[slowlog]` section — ring buffer of slow commands per shard.
443#[derive(Debug, Clone, Copy, PartialEq, Eq)]
444pub struct SlowlogSection {
445 /// Record any command whose execution took at least this many
446 /// microseconds (Redis: `< slower_than_micros` is skipped). `-1`
447 /// disables the log (zero hot-path cost — no `Instant::now()`
448 /// taken); `0` records every command. Default `-1` (OFF).
449 pub slower_than_micros: i64,
450 /// Cap on the per-shard ring buffer. Once exceeded, the oldest
451 /// entry is dropped to make room. Across `nshards` shards the
452 /// effective server-wide cap is `max_len * nshards`. Default `128`.
453 pub max_len: u32,
454}
455
456impl Default for SlowlogSection {
457 fn default() -> Self {
458 Self {
459 slower_than_micros: -1,
460 max_len: 128,
461 }
462 }
463}
464
465/// Parse a Redis-style `notify_keyspace_events` flag string into
466/// [`NotificationFlags`]. Unknown chars are ignored (forward-compat
467/// for `x`/`e`/`t`/`n` not yet implemented — see the section docs).
468/// The `A` alias enables every event-class flag except channels.
469pub fn parse_notification_flags(s: &str) -> NotificationFlags {
470 let mut f = NotificationFlags::default();
471 for c in s.chars() {
472 match c {
473 'K' => f.keyspace = true,
474 'E' => f.keyevent = true,
475 'g' => f.generic = true,
476 '$' => f.string = true,
477 'l' => f.list = true,
478 's' => f.set = true,
479 'h' => f.hash = true,
480 'z' => f.zset = true,
481 't' => f.stream = true,
482 'A' => {
483 // Alias for "g$lshzxetd" — every implemented event class.
484 // Per Redis spec `A` includes the stream `t` class.
485 f.generic = true;
486 f.string = true;
487 f.list = true;
488 f.set = true;
489 f.hash = true;
490 f.zset = true;
491 f.stream = true;
492 }
493 _ => {} // forward-compat: silently ignore unknown chars
494 }
495 }
496 f
497}
498/// the TOML file + env + CLI.
499#[derive(Debug, Clone, PartialEq, Eq, Default)]
500pub struct Config {
501 /// `[server]` settings.
502 pub server: ServerSection,
503 /// `[persistence]` settings.
504 pub persistence: PersistenceSection,
505 /// `[memory]` settings.
506 pub memory: MemorySection,
507 /// `[metrics]` settings (Prometheus /metrics endpoint — v1.41).
508 pub metrics: MetricsSection,
509 /// `[audit]` settings (append-only ADMIN-command audit — v1.42).
510 pub audit: AuditSection,
511 /// `[expiry]` settings.
512 pub expiry: ExpirySection,
513 /// `[log]` settings.
514 pub log: LogSection,
515 /// `[notification]` settings (keyspace events).
516 pub notification: NotificationSection,
517 /// `[advanced]` settings (reactor tuning knobs).
518 pub advanced: AdvancedSection,
519 /// `[slowlog]` settings (slow-command ring buffer).
520 pub slowlog: SlowlogSection,
521 /// `[cluster]` settings (single-node cluster mode).
522 pub cluster: crate::cluster::ClusterSection,
523 /// `[lua]` settings (v1.27 server-side Lua scripting via the
524 /// kevy-lua bridge).
525 pub lua: LuaSection,
526 /// `[replication]` settings (v3-cluster Phase 1 primary/replica).
527 pub replication: crate::replication::ReplicationSection,
528 /// Path the config was loaded from (for `CONFIG REWRITE`). `None` =
529 /// loaded from defaults only / from in-memory string.
530 pub source_path: Option<PathBuf>,
531}
532
533// `ConfigError` lives in [`crate::error`] — split out so this file
534// stays under the 500-LOC house rule. Re-exported below for any caller
535// that still does `kevy_config::schema::ConfigError`.
536pub use crate::error::ConfigError;