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