kevy_rt/lib.rs
1//! kevy-rt — shared-nothing, thread-per-core runtime.
2//!
3//! Each core runs its own reactor (kqueue/epoll) and owns one **shard** of the
4//! keyspace (`hash(key) % nshards`). There is no shared mutable state and no
5//! lock on the hot path — cores communicate only by message passing over
6//! channels, woken via a self-pipe ([`kevy_sys::Waker`]). Connections are spread
7//! across cores by `SO_REUSEPORT`; a command whose key lives on another core is
8//! forwarded to that core, executed there, and the reply routed back to the
9//! originating connection.
10//!
11//! Per-connection reply ordering is preserved (RESP is pipelined): each command
12//! gets a monotonic seq; replies are emitted only in contiguous seq order, so an
13//! async cross-core reply never overtakes an earlier one.
14//!
15//! The cross-core channel currently uses `std::sync::mpsc` (pure Rust, zero
16//! deps); swapping in a lock-free SPSC/MPSC ring is a perf-polish item.
17//! Command semantics are injected via the [`Commands`] trait, keeping the
18//! runtime independent of the concrete command set. Part of the [kevy] server.
19//!
20//! [kevy]: https://crates.io/crates/kevy
21//!
22//! # Module map
23//!
24//! - [`Runtime`] (in `runtime`) — public entry point; spawns one `shard` per core.
25//! - `shard` — the per-core reactor: sockets, the inbound queue, reply flushing.
26//! - `exec` — command semantics: routing, execution, and result reduction.
27//! - `message` — internal cross-core work/result types.
28//! - `conn` — per-connection state (input/output, seq ring, subscriptions).
29//! - `reduce` — reply reduction (`materialize`) and pure helpers (set algebra,
30//! shard hashing, pub/sub framing).
31//!
32//! # Example
33//!
34//! Implement [`Commands`] for your command set and run it. ([`Store`] is
35//! re-exported so you don't need a separate dependency.)
36//!
37//! ```no_run
38//! use kevy_rt::{ArgvView, Commands, Route, Runtime, Store, TxnKind};
39//! use std::sync::Arc;
40//! use std::sync::atomic::AtomicBool;
41//!
42//! #[derive(Clone)]
43//! struct MyCommands;
44//! impl Commands for MyCommands {
45//! fn route<A: ArgvView + ?Sized>(&self, args: &A) -> Route {
46//! if args.len() >= 2 { Route::Single(1) } else { Route::Local }
47//! }
48//! fn dispatch<A: ArgvView + ?Sized>(&self, _store: &mut Store, _args: &A) -> Vec<u8> {
49//! b"+OK\r\n".to_vec()
50//! }
51//! fn is_quit<A: ArgvView + ?Sized>(&self, args: &A) -> bool {
52//! args.first().is_some_and(|c| c.eq_ignore_ascii_case(b"QUIT"))
53//! }
54//! fn is_write<A: ArgvView + ?Sized>(&self, _args: &A) -> bool { false }
55//! fn txn_kind<A: ArgvView + ?Sized>(&self, _args: &A) -> TxnKind { TxnKind::Other }
56//! }
57//!
58//! // One shard per core, listening on 127.0.0.1:6379, until `stop` is set.
59//! let rt = Runtime::new([127, 0, 0, 1], 6379, 4, MyCommands);
60//! rt.run(Arc::new(AtomicBool::new(false))).unwrap();
61//! ```
62// Almost entirely safe: the only `unsafe` is in `uring_reactor` (Linux io_uring),
63// which needs raw buffer pointers for zero-allocation completion I/O — on the hot
64// path toward kevy's disk-I/O-ceiling goal, where a buffer-ownership safe wrapper
65// would add per-op cost. Each such block documents its invariant; the
66// epoll/kqueue path and every other module stay safe, and all libc lives in
67// kevy-sys.
68#![deny(unsafe_op_in_unsafe_fn)]
69
70mod bio;
71mod block_xshard;
72mod blocked;
73mod lua_wake_bridge;
74mod cache_padded;
75mod cluster;
76mod conn;
77mod exec;
78mod exec_build;
79mod exec_client_intercept;
80mod exec_crossslot;
81mod exec_dispatch;
82mod exec_notify;
83mod exec_op;
84mod exec_pubsub;
85mod exec_pubsub_pattern;
86mod exec_rename;
87mod exec_replwait;
88mod exec_feed;
89mod exec_zalgebra;
90mod exec_slowlog;
91mod exec_watch;
92mod inbox;
93mod persist_worker;
94mod message;
95mod reduce;
96mod replica_inbox;
97mod replication;
98mod replication_apply;
99mod replication_gate;
100mod replication_io;
101mod replication_pump;
102mod reshard;
103mod route;
104mod runtime;
105mod runtime_builders;
106mod shard;
107mod shard_flush;
108mod shard_lifecycle;
109mod shard_tick;
110#[cfg(target_os = "linux")]
111mod uring_arm;
112#[cfg(target_os = "linux")]
113mod uring_bigbulk;
114#[cfg(target_os = "linux")]
115mod uring_bigbulk_b2alt;
116#[cfg(target_os = "linux")]
117mod uring_bigbulk_probe;
118#[cfg(target_os = "linux")]
119mod uring_conn;
120#[cfg(target_os = "linux")]
121mod uring_inbox;
122#[cfg(target_os = "linux")]
123mod uring_io;
124#[cfg(target_os = "linux")]
125mod uring_park;
126#[cfg(target_os = "linux")]
127mod uring_reactor;
128#[cfg(target_os = "linux")]
129mod uring_setup;
130
131pub use blocked::{BlockHint, BlockKind};
132pub use lua_wake_bridge::push_lua_wake_key;
133pub use reduce::shard_of as shard_of_key;
134pub use cluster::shard_slot_range;
135pub use exec_slowlog::{SlowlogSub, parse_slowlog_sub};
136pub use kevy_config::NotificationFlags;
137pub use kevy_persist::Fsync;
138pub use kevy_resp::{Argv, ArgvBorrowed, ArgvView, RespVersion};
139pub use kevy_store::Store;
140pub use replica_inbox::{ReplicaApply, ReplicaInboxReceiver, ReplicaInboxSender, replica_inbox_pair};
141pub use replication_gate::ReplicatedApplyGuard;
142pub use route::{Route, XGroupCtx};
143pub use message::ZCombine;
144pub use runtime::Runtime;
145
146/// Command-set semantics injected into the runtime. Cloned to every core, so it
147/// must be cheap/stateless to clone.
148pub trait Commands: Clone + Send + 'static {
149 /// Classify how a command is routed across shards.
150 fn route<A: ArgvView + ?Sized>(&self, args: &A) -> Route;
151 /// Execute a full command against one shard's store, returning RESP bytes.
152 fn dispatch<A: ArgvView + ?Sized>(&self, store: &mut Store, args: &A) -> Vec<u8>;
153 /// RESP3 variant of [`Self::dispatch`] — called when the connection
154 /// has negotiated `HELLO 3`. Default: delegate to the RESP2 path
155 /// (the cross-shard forward carries a per-cmd `RespVersion`
156 /// so a V2 client and a V3 client can share the owning shard).
157 fn dispatch_resp3<A: ArgvView + ?Sized>(&self, store: &mut Store, args: &A) -> Vec<u8> {
158 self.dispatch(store, args)
159 }
160 /// Execute a command, appending the RESP reply to `out`. The in-order local
161 /// fast path uses this to write straight into the connection's output buffer
162 /// (no per-command reply `Vec`). Default: delegate to [`dispatch`](Self::dispatch).
163 fn dispatch_into<A: ArgvView + ?Sized>(&self, store: &mut Store, args: &A, out: &mut Vec<u8>) {
164 out.extend_from_slice(&self.dispatch(store, args));
165 }
166 /// RESP3 variant of [`Self::dispatch_into`] — called when the
167 /// connection has negotiated `HELLO 3`. Default: delegate to the
168 /// RESP2 path (so a server that hasn't migrated any replies still
169 /// works correctly with a RESP3 client, per spec). Override per
170 /// command to emit RESP3 shapes (Map / Set / Double / …).
171 fn dispatch_into_resp3<A: ArgvView + ?Sized>(
172 &self,
173 store: &mut Store,
174 args: &A,
175 out: &mut Vec<u8>,
176 ) {
177 self.dispatch_into(store, args, out);
178 }
179 /// Classify a command for keyspace notifications. Returns `Some`
180 /// for write commands that should fire a notification when the
181 /// corresponding flag is enabled; `None` for read-only / no-op /
182 /// not-yet-classified commands (those never publish). Default
183 /// `None` so non-kevy embedders pay nothing.
184 fn notify_class<A: ArgvView + ?Sized>(&self, _args: &A) -> Option<NotifyClass> {
185 None
186 }
187
188 /// Handle `HELLO` — return the new connection protocol version + the
189 /// reply bytes. The runtime applies the new version to the conn
190 /// before scheduling the reply, so a `HELLO 3` ack itself comes out
191 /// shaped as a RESP3 Map (the new protocol is in effect for its own
192 /// reply).
193 ///
194 /// Default: ignore the args, keep `current_proto`, emit a minimal
195 /// RESP2 +OK so embedders that don't care still see a sane reply.
196 /// kevy's own impl in `kevy::KevyCommands` parses the optional
197 /// protover and emits the full server-info shape.
198 fn hello_reply<A: ArgvView + ?Sized>(
199 &self,
200 _args: &A,
201 current_proto: RespVersion,
202 ) -> (RespVersion, Vec<u8>) {
203 (current_proto, b"+OK\r\n".to_vec())
204 }
205 /// Whether this command should close the connection (QUIT).
206 fn is_quit<A: ArgvView + ?Sized>(&self, args: &A) -> bool;
207 /// Whether this command mutates the keyspace (so it must be logged to the AOF).
208 fn is_write<A: ArgvView + ?Sized>(&self, args: &A) -> bool;
209 /// Transaction-control classification (MULTI/EXEC/DISCARD vs anything else).
210 fn txn_kind<A: ArgvView + ?Sized>(&self, args: &A) -> TxnKind;
211 /// Called once per shard, immediately after [`Store::new`], before the
212 /// reactor enters its event loop. Implementations install per-shard
213 /// configuration that the runtime doesn't know about — currently the
214 /// `maxmemory` + eviction-policy pair, which kevy ships via its own
215 /// process-wide config snapshot. Default: no-op so non-kevy embedders
216 /// aren't forced to override.
217 fn on_shard_init(&self, _store: &mut Store) {}
218
219 /// Called once on the shard's own thread, first thing in the reactor
220 /// entry (both reactors), before restore/replay. Implementations that
221 /// need per-shard identity at dispatch time (e.g. kevy's `CLUSTER MYID`
222 /// / `CLUSTER NODES` `myself` flag) stash `shard` in a thread-local here
223 /// — in a thread-per-core runtime the current thread *is* the shard.
224 /// Default: no-op.
225 fn on_shard_start(&self, _shard: usize) {}
226
227 /// Per-tick persistence-stats publication: whether this shard has a
228 /// background save/rewrite in flight and how many AOF rewrites have
229 /// completed since open. Command layers that serve `INFO persistence`
230 /// stash these in a thread-local (thread-per-core: the answering
231 /// thread *is* the shard, same pattern as [`Self::on_shard_start`]).
232 /// Default: no-op.
233 fn on_persist_stats(&self, _in_flight: bool, _aof_rewrites_total: u64) {}
234
235 /// Per-tick replication-view publication: the answering shard's
236 /// current `master_repl_offset` (== `ReplicationSource::next_offset()`)
237 /// plus the per-replica `(ipv4, port, sent_offset)` triple for
238 /// every handshake-complete replica (in `AckSent`, `Streaming`,
239 /// or `SnapshotShipping`). `connected_slaves` for `INFO` /
240 /// `ROLE` is derived as `replicas.len()`.
241 /// Only called when this shard has a `ReplicationSource`
242 /// installed (i.e. `Runtime::with_replication(true, ...)` was
243 /// requested); standalone setups pay nothing. Command layers
244 /// that serve `ROLE` / `INFO replication` stash the values in a
245 /// thread-local (thread-per-core: the answering thread *is* the
246 /// shard, same pattern as [`Self::on_persist_stats`]). Default
247 /// no-op.
248 fn on_replication_view(
249 &self,
250 _master_repl_offset: u64,
251 _replicas: Vec<(std::net::Ipv4Addr, u16, u64, Option<u64>)>,
252 ) {}
253
254 /// Periodic shard housekeeping (the equivalent of Redis's `serverCron`).
255 /// kevy uses this to run [`Store::tick_expire`] at the configured
256 /// `[expiry].hz`. Default no-op so non-kevy embedders / runtimes can
257 /// ignore it.
258 fn on_shard_tick(&self, _store: &mut Store) {}
259
260 /// v2.5: per-shard half of an extension fan-out command (IDX.* /
261 /// future VIEW.* / FT.*): compute this shard's raw chunk for
262 /// `argv`. The payload encoding is the embedder's own — the
263 /// runtime treats it as opaque bytes and hands all chunks to
264 /// [`Commands::extension_reduce`] at the origin.
265 fn extension_op(&self, _store: &mut Store, _argv: &[Vec<u8>]) -> Vec<u8> {
266 Vec::new()
267 }
268
269 /// v2.5: origin-side reduce of an extension fan-out — merge every
270 /// shard's chunk into the final RESP reply bytes.
271 /// v3.14 A0 — pre-dispatch write gate. `Some(err_bytes)` rejects
272 /// every data-write client command with that RESP error before any
273 /// routing (replication apply does NOT pass through here, so a
274 /// read-only replica keeps applying its feed). Admin verbs
275 /// (REPLICAOF / CONFIG) are not classified as writes and stay
276 /// available as the operator escape hatch.
277 fn write_denied(&self) -> Option<Vec<u8>> {
278 None
279 }
280
281 /// v3.16 D3 — bounded staleness: called before READ verbs; return
282 /// `Some(error_bytes)` to refuse the read (a replica whose feed
283 /// is staler than the configured bound answers `-STALE` so the
284 /// client falls back to the primary). Default: reads always
285 /// allowed.
286 fn read_denied(&self) -> Option<Vec<u8>> {
287 None
288 }
289
290 fn extension_reduce_v3(
291 &self,
292 argv: &[Vec<u8>],
293 chunks: Vec<Vec<u8>>,
294 _proto: kevy_resp::RespVersion,
295 ) -> Vec<u8> {
296 // Default: proto-blind reduce (RESP2 wire on both protos).
297 self.extension_reduce(argv, chunks)
298 }
299
300 fn extension_reduce(&self, _argv: &[Vec<u8>], _chunks: Vec<Vec<u8>>) -> Vec<u8> {
301 b"-ERR extension commands not supported\r\n".to_vec()
302 }
303
304 /// v2.5: called after every applied write with the written key
305 /// (when the resolver knew one). Default no-op; kevy uses it for
306 /// synchronous secondary-index maintenance (derived-by-
307 /// construction). Runs on the shard thread with store access —
308 /// implementations must be cheap when their feature is off.
309 fn on_write(&self, _store: &mut Store, _key: &[u8]) {}
310
311 /// Called once per client command at dispatch entry (before routing /
312 /// fan-out, so a multi-key command counts once). kevy uses it for
313 /// `INFO stats: total_commands_processed`. Hot path — keep it to a single
314 /// thread-local bump. Default no-op so non-kevy embedders pay nothing.
315 fn on_command(&self) {}
316
317 /// Called once per accepted client connection. kevy uses it for
318 /// `INFO stats: total_connections_received`. Default no-op.
319 fn on_connection(&self) {}
320
321 /// Interval between [`Self::on_shard_tick`] calls. Default 100 ms
322 /// (matching Redis's `hz = 10`). `0` disables ticking entirely.
323 fn shard_tick_interval_ms(&self) -> u64 {
324 100
325 }
326
327 /// Snapshot of the runtime-owned knobs that can be hot-modified
328 /// (the kevy server wires this to `CONFIG SET`). Called once per
329 /// shard tick — each `Some` value is applied to the shard's live
330 /// state; each `None` keeps the existing setting untouched.
331 ///
332 /// Default returns all-None so embedders that never hot-swap config
333 /// pay nothing beyond one struct-build per tick. The cost lives in
334 /// the impl's read of its own config source.
335 fn live_runtime_config(&self) -> LiveRuntimeConfig {
336 LiveRuntimeConfig::default()
337 }
338
339 /// Index into `args` of the key whose write may wake a blocked waiter
340 /// (`LPUSH` / `RPUSH` feed `BLPOP` / `BRPOP`; `XADD` feeds the stream
341 /// blocks). `Some(1)` for those verbs, `None` for everything else. The
342 /// in-shard fast path reads this off [`ResolvedCmd::wake_idx`]; the
343 /// cross-shard write path (`exec_op`, where a forwarded write
344 /// lands on the key's owning shard) re-derives it via this method since
345 /// the forwarded envelope doesn't carry the resolved hint. Default
346 /// `None` so non-blocking embedders pay nothing.
347 fn wake_idx<A: ArgvView + ?Sized>(&self, _args: &A) -> Option<u8> {
348 None
349 }
350
351 /// Classify a command for blocking semantics. `BlockHint::None`
352 /// (default) is the zero-cost answer for every non-blocking verb;
353 /// the dispatcher only registers a waiter when this returns
354 /// `BlockHint::Block` *and* the command's `dispatch_into` produced no
355 /// reply (i.e. it could not satisfy itself immediately — e.g. BLPOP
356 /// on an empty list). Concrete impls should fold this into their
357 /// override of [`Self::resolve`] so the verb-table lookup happens
358 /// once per command.
359 fn block_hint<A: ArgvView + ?Sized>(&self, _args: &A) -> BlockHint {
360 BlockHint::None
361 }
362
363 /// Rewrite `args` into the owned [`Argv`] that the dispatcher will
364 /// store as the parked waiter's command and replay on wake. Lets a
365 /// command set normalise positional ID / cursor arguments that would
366 /// otherwise re-resolve to a different value on retry — most notably
367 /// `XREAD BLOCK ... STREAMS k $`, where leaving `$` literal in the
368 /// retried argv causes a fresh re-resolve to the post-`XADD` last_id
369 /// and zero matching entries (the wake hangs).
370 ///
371 /// Default: just materialise the argv unchanged. Concrete impls only
372 /// need to override when a registered command carries an arg whose
373 /// meaning depends on store state at park time (`XREAD $`, the
374 /// classic case).
375 ///
376 /// For the cross-shard arbiter this runs on the **target** shard (the
377 /// one that owns the key) when the waiter is armed, so `$` snapshots
378 /// the target's real `last_id` — not the origin shard's (which may not
379 /// hold the stream at all).
380 fn resolve_block_argv<A: ArgvView + ?Sized>(
381 &self,
382 _store: &mut Store,
383 args: &A,
384 _kind: BlockKind,
385 ) -> Argv {
386 args.to_argv()
387 }
388
389 /// Build the **single-key** command the dispatcher will replay to
390 /// satisfy one watched `key` of a (possibly multi-key) blocking
391 /// command. `args` is the original command; `key` is one of its
392 /// watched keys. Returns an [`Argv`] that, when dispatched, pops /
393 /// reads only `key` — e.g. `BLPOP k1 k2 0` watching `k2` yields
394 /// `BLPOP k2 0`; `XREAD … STREAMS s1 s2 id1 id2` watching `s2`
395 /// yields `XREAD … STREAMS s2 id2`.
396 ///
397 /// Any state-dependent positional arg (`$`) is left **literal** here —
398 /// it's frozen later by [`Self::resolve_block_argv`] on the key's
399 /// owning shard. No store access needed (pure argv slicing). Default:
400 /// the unchanged argv (single-key blocking commands need no rewrite).
401 fn block_serve_argv<A: ArgvView + ?Sized>(
402 &self,
403 args: &A,
404 _kind: BlockKind,
405 _key: &[u8],
406 ) -> Argv {
407 args.to_argv()
408 }
409
410 /// Non-destructive readiness peek for a parked waiter: would replaying
411 /// `serve_argv` (built by [`Self::block_serve_argv`], `$` already
412 /// frozen) produce a reply right now? Runs on the key's owning shard
413 /// when arming and is the gate for emitting a cross-shard wake. Must
414 /// NOT mutate the store (no pop / no group-cursor advance). Default
415 /// `false` so non-blocking embedders never spuriously wake.
416 fn block_ready<A: ArgvView + ?Sized>(
417 &self,
418 _store: &mut Store,
419 _serve_argv: &A,
420 _kind: BlockKind,
421 ) -> bool {
422 false
423 }
424
425 /// Resolve all verb-dependent attributes in **one** verb-table lookup.
426 /// The default implementation calls the per-attribute methods above
427 /// (five upper_verb scans + matches); concrete impls SHOULD override
428 /// this with a single match so the reactor's hot path pays the verb-
429 /// resolution cost only once per command.
430 fn resolve<A: ArgvView + ?Sized>(&self, args: &A) -> ResolvedCmd {
431 ResolvedCmd {
432 txn_kind: self.txn_kind(args),
433 route: self.route(args),
434 is_quit: self.is_quit(args),
435 is_write: self.is_write(args),
436 block_hint: self.block_hint(args),
437 wake_idx: None,
438 }
439 }
440}
441
442/// Per-command verb-resolution result. Produced once by [`Commands::resolve`]
443/// in the reactor's parse-then-dispatch loop, reused for routing decisions,
444/// AOF logging, and the QUIT branch — so the per-cmd `upper_verb` cost goes
445/// from 4× down to 1×.
446pub struct ResolvedCmd {
447 pub txn_kind: TxnKind,
448 pub route: Route,
449 pub is_quit: bool,
450 pub is_write: bool,
451 /// Blocking-command classification (see [`Commands::block_hint`]).
452 /// `BlockHint::None` for every non-blocking verb.
453 pub block_hint: BlockHint,
454 /// Index into `args` whose write may wake a `BLPOP` / `XREAD BLOCK`
455 /// waiter parked on that key — `Some(1)` for `LPUSH` / `RPUSH` /
456 /// `XADD`, `None` for every other command (including reads). The
457 /// dispatcher's wake hook is gated on both this being `Some` *and*
458 /// the per-shard `BlockedClients` registry being non-empty, so the
459 /// steady-state cost when nobody is parked is one `is_empty()` check.
460 pub wake_idx: Option<u8>,
461}
462
463/// Keyspace-notification event class — what category a write command
464/// belongs to, so the runtime can match it against the per-conn
465/// notify_keyspace_events flags before publishing.
466#[derive(Debug, Clone, Copy, PartialEq, Eq)]
467pub enum NotifyClass {
468 /// `g` — generic key commands (DEL / EXPIRE / PERSIST / RENAME / TYPE).
469 Generic,
470 /// `$` — string commands (SET / GETSET / INCR / APPEND / MSET).
471 String,
472 /// `l` — list commands (LPUSH / RPUSH / LPOP / LREM / LTRIM / …).
473 List,
474 /// `s` — set commands (SADD / SREM / SPOP / …).
475 Set,
476 /// `h` — hash commands (HSET / HDEL / HINCRBY / …).
477 Hash,
478 /// `z` — sorted-set commands (ZADD / ZREM / ZINCRBY / …).
479 Zset,
480 /// `t` — stream commands (XADD / XDEL / XTRIM / XGROUP / XACK /
481 /// XCLAIM / XREADGROUP / …). Matches Redis's `t` class.
482 Stream,
483}
484
485impl NotifyClass {
486 /// Whether `flags` enables this event class.
487 #[inline]
488 pub fn enabled_in(self, flags: &NotificationFlags) -> bool {
489 match self {
490 NotifyClass::Generic => flags.generic,
491 NotifyClass::String => flags.string,
492 NotifyClass::List => flags.list,
493 NotifyClass::Set => flags.set,
494 NotifyClass::Hash => flags.hash,
495 NotifyClass::Zset => flags.zset,
496 NotifyClass::Stream => flags.stream,
497 }
498 }
499}
500
501/// Transaction-control classification for a command.
502pub enum TxnKind {
503 Multi,
504 Exec,
505 Discard,
506 /// `WATCH` — outside MULTI runs the fan-out; inside MULTI is rejected
507 /// with an error (Redis semantics: `WATCH inside MULTI is not allowed`).
508 /// `UNWATCH` is plain [`Self::Other`] — outside MULTI it routes to
509 /// [`Route::Unwatch`] (clear + OK); inside MULTI it queues as a no-op
510 /// that dispatch resolves to +OK at EXEC time.
511 Watch,
512 Other,
513}
514
515/// Live snapshot of the runtime-owned knobs that may have been changed
516/// since this shard's last tick. Built by the [`Commands`] impl from
517/// its own config source (e.g. kevy reads `config_global`). Each
518/// `Some(_)` is applied to the shard; each `None` leaves the existing
519/// setting alone.
520///
521/// One snapshot is built per tick (every 100 ms by default), so its
522/// cost is amortised across thousands of commands.
523#[derive(Debug, Default, Clone, Copy)]
524pub struct LiveRuntimeConfig {
525 /// AOF fsync policy. Applied via `Aof::set_fsync` — switching to
526 /// `Always` mid-flight also flushes any buffered bytes so the new
527 /// "every write is on disk before reply" contract is honoured from
528 /// the next append onward.
529 pub appendfsync: Option<Fsync>,
530 /// `auto_aof_rewrite_percentage`. `0` disables the auto-trigger.
531 pub auto_aof_rewrite_pct: Option<u32>,
532 /// `auto_aof_rewrite_min_size` in bytes.
533 pub auto_aof_rewrite_min_size: Option<u64>,
534 /// New tick interval in ms (`1000/hz`). `0` disables ticking
535 /// entirely — note that disabling also turns off active TTL
536 /// expiry and the auto-rewrite tick path. Lazy expiry on access
537 /// always still works.
538 pub tick_interval_ms: Option<u64>,
539 /// `notify_keyspace_events` flags. Parsed by the [`Commands`]
540 /// impl from its config source (e.g. kevy reads
541 /// `config_global` + [`kevy_config::parse_notification_flags`]).
542 /// Default-empty flags mean OFF — writes pay one bool-OR check
543 /// and skip every per-key keyspace notification publish.
544 pub notify_flags: Option<NotificationFlags>,
545 /// `[slowlog].slower_than_micros` — `-1` disables, `0` records all,
546 /// `>0` is the strict micros threshold. `None` keeps the existing
547 /// shard setting (set by the [`Runtime`] builder at startup).
548 pub slowlog_slower_than_micros: Option<i64>,
549 /// `[slowlog].max_len` — ring cap per shard. Shrinking trims the
550 /// oldest entries on the next tick application.
551 pub slowlog_max_len: Option<u32>,
552 /// v3.16 D2 — monotonic promotion counter. The command layer bumps
553 /// it every time this process is PROMOTED (replica → primary:
554 /// `REPLICAOF NO ONE` on a following replica, or an election win).
555 /// Each shard tracks the last value it saw; an increase makes the
556 /// shard bump its feed generation (offsets restart at 0, persisted
557 /// via the feed-gen sidecar) — so a REPL.TOKEN minted before the
558 /// failover can never falsely satisfy a REPL.WAIT against the new
559 /// primary's unrelated offset space. Not an Option: `0` (the
560 /// default) means "never promoted" and embedders pay nothing.
561 pub promotion_epoch: u64,
562}