kevy_embedded/config.rs
1//! Embedded-store configuration. Builder-style — every knob has a sane
2//! default so `Config::default()` works for the simplest use case
3//! (in-memory, no persistence, background TTL reaper).
4
5#[cfg(feature = "persist")]
6use std::path::PathBuf;
7use std::time::Duration;
8
9#[cfg(feature = "persist")]
10pub use kevy_persist::Fsync as AppendFsync;
11#[cfg(feature = "persist")]
12pub use kevy_persist::ReplayMode;
13pub use kevy_store::EvictionPolicy;
14
15#[cfg(feature = "tier")]
16pub use crate::config_tier::TierBudgetSpec;
17
18pub use crate::modes::TtlReaperMode;
19
20/// Embedded-store config. Build by chaining `with_*` methods on
21/// [`Config::default`], or assign its public fields.
22///
23/// ```
24/// use kevy_embedded::{Config, Store};
25///
26/// let mut cfg = Config::default().with_ttl_reaper_manual();
27/// cfg.reaper_samples = 40;
28/// let store = Store::open(cfg)?;
29/// # Ok::<(), kevy_embedded::KevyError>(())
30/// ```
31#[derive(Debug, Clone)]
32#[non_exhaustive]
33pub struct Config {
34 /// Optional READ-ONLY RESP listener address (ops tooling —
35 /// redis-cli against a live embedded store). `None` (default) =
36 /// no listener thread, no socket, zero tax.
37 ///
38 /// ```
39 /// let addr: std::net::SocketAddr = "127.0.0.1:6009".parse()?;
40 /// assert_eq!(kevy_embedded::Config::default().resp_listener, None);
41 /// let cfg = kevy_embedded::Config::default().with_resp_listener(addr);
42 /// assert_eq!(cfg.resp_listener, Some(addr));
43 /// # Ok::<(), Box<dyn std::error::Error>>(())
44 /// ```
45 #[cfg(feature = "listener")]
46 pub resp_listener: Option<std::net::SocketAddr>,
47 /// Soft memory ceiling in bytes. `0` (default) = unlimited.
48 ///
49 /// ```
50 /// let cfg = kevy_embedded::Config::default().with_max_memory(64 << 20);
51 /// assert_eq!(cfg.maxmemory, 64 << 20);
52 /// assert_eq!(kevy_embedded::Config::default().maxmemory, 0); // unlimited
53 /// ```
54 pub maxmemory: u64,
55 /// Eviction policy when over `maxmemory`. Default `NoEviction`.
56 ///
57 /// ```
58 /// use kevy_embedded::{Config, EvictionPolicy, Store};
59 ///
60 /// let cfg = Config::default()
61 /// .with_max_memory(256 << 10)
62 /// .with_eviction(EvictionPolicy::AllKeysLru);
63 /// assert_eq!(cfg.eviction_policy, EvictionPolicy::AllKeysLru);
64 /// let s = Store::open(cfg)?;
65 /// for i in 0..2000 {
66 /// s.set(format!("k{i}").as_bytes(), &[0u8; 512])?;
67 /// }
68 /// assert!(s.info().evictions > 0, "old keys made room for new ones");
69 /// # Ok::<(), kevy_embedded::KevyError>(())
70 /// ```
71 pub eviction_policy: EvictionPolicy,
72 /// Persistence directory. `None` = pure in-memory (no AOF, no snapshot).
73 ///
74 /// ```
75 /// use kevy_embedded::{Config, Store};
76 ///
77 /// let dir = kevy_tmpdir::TmpDir::new("config-data-dir");
78 /// let cfg = Config::default().with_persist(dir.path());
79 /// assert_eq!(cfg.data_dir.as_deref(), Some(dir.path()));
80 /// Store::open(cfg.clone())?.set(b"k", b"v")?;
81 /// assert_eq!(Store::open(cfg)?.get(b"k")?.as_deref(), Some(&b"v"[..]));
82 /// # Ok::<(), kevy_embedded::KevyError>(())
83 /// ```
84 #[cfg(feature = "persist")]
85 pub data_dir: Option<PathBuf>,
86 /// AOF on/off when `data_dir` is set. Defaults to `true` (on) when
87 /// `with_persist` was called; ignored if `data_dir` is `None`.
88 ///
89 /// ```
90 /// use kevy_embedded::{Config, Store};
91 ///
92 /// let dir = kevy_tmpdir::TmpDir::new("config-aof");
93 /// let cfg = Config::default().with_persist(dir.path()).without_aof();
94 /// assert!(!cfg.aof);
95 /// let s = Store::open(cfg)?;
96 /// s.set(b"k", b"v")?;
97 /// assert_eq!(s.info().aof_bytes, 0); // nothing is logged
98 /// # Ok::<(), kevy_embedded::KevyError>(())
99 /// ```
100 #[cfg(feature = "persist")]
101 pub aof: bool,
102 /// AOF fsync policy. Default `EverySec` (matches Redis: ≤ 1 s loss).
103 ///
104 /// ```
105 /// use kevy_embedded::{AppendFsync, Config};
106 ///
107 /// assert_eq!(Config::default().appendfsync, AppendFsync::EverySec);
108 /// let cfg = Config::default().with_appendfsync(AppendFsync::Always);
109 /// assert_eq!(cfg.appendfsync, AppendFsync::Always);
110 /// ```
111 #[cfg(feature = "persist")]
112 pub appendfsync: AppendFsync,
113 /// Transparent-tiering RAM budget (capacity arc). `None` (default)
114 /// = tiering off — today's paths byte-identical. `Some` requires a
115 /// disk `data_dir` (the cold value log lives at `<data_dir>/tier/`);
116 /// a mem-only store rejects the combo at open. The budget is the
117 /// WHOLE store's (split evenly across shards); auto/percent forms
118 /// resolve against the detected memory bound at open and re-resolve
119 /// on every reaper tick.
120 ///
121 /// ```
122 /// use kevy_embedded::Config;
123 ///
124 /// assert!(Config::default().tier_budget.is_none()); // tiering off
125 /// let cfg = Config::default().with_tier_budget(64 << 20);
126 /// assert!(cfg.tier_budget.is_some());
127 /// ```
128 #[cfg(feature = "tier")]
129 pub tier_budget: Option<TierBudgetSpec>,
130 /// Largest value the tier may spill (bytes; 0 = unlimited). Default
131 /// 256 KiB (RFC §7): an embedded cold read holds the shard lock for
132 /// the pread, so the cap bounds that hold time. Over-cap values
133 /// simply stay hot.
134 ///
135 /// ```
136 /// let mut cfg = kevy_embedded::Config::default();
137 /// assert_eq!(cfg.max_spill_value, 256 << 10);
138 /// cfg.max_spill_value = 0; // any value may spill
139 /// assert_eq!(cfg.max_spill_value, 0);
140 /// ```
141 pub max_spill_value: u64,
142 /// TTL reaper mode. Default `Background`.
143 ///
144 /// ```
145 /// use kevy_embedded::{Config, Store, TtlReaperMode};
146 ///
147 /// let cfg = Config::default().with_ttl_reaper_manual();
148 /// assert_eq!(cfg.ttl_reaper, TtlReaperMode::Manual);
149 /// let s = Store::open(cfg)?;
150 /// s.tick(); // the caller drives the reaper
151 /// # Ok::<(), kevy_embedded::KevyError>(())
152 /// ```
153 pub ttl_reaper: TtlReaperMode,
154 /// Reaper tick interval. Default 100 ms (10 Hz).
155 ///
156 /// ```
157 /// use std::time::Duration;
158 ///
159 /// let cfg = kevy_embedded::Config::default().with_reaper_interval(Duration::from_millis(10));
160 /// assert_eq!(cfg.reaper_interval, Duration::from_millis(10));
161 /// ```
162 pub reaper_interval: Duration,
163 /// `tick_expire` samples per round. Default 20 (matches Redis).
164 ///
165 /// ```
166 /// let mut cfg = kevy_embedded::Config::default();
167 /// assert_eq!(cfg.reaper_samples, 20);
168 /// cfg.reaper_samples = 100; // reap harder per round
169 /// assert_eq!(cfg.reaper_samples, 100);
170 /// ```
171 pub reaper_samples: usize,
172 /// Max sample rounds per tick. Default 16.
173 ///
174 /// ```
175 /// let mut cfg = kevy_embedded::Config::default();
176 /// assert_eq!(cfg.reaper_max_rounds, 16);
177 /// cfg.reaper_max_rounds = 4; // bound each tick's work
178 /// assert_eq!(cfg.reaper_max_rounds, 4);
179 /// ```
180 pub reaper_max_rounds: u32,
181 /// Auto-`BGREWRITEAOF` trigger: rewrite when the live AOF has grown by at
182 /// least this percent over its size at the previous rewrite. `0` disables
183 /// (call [`crate::Store::rewrite_aof`] manually). Default `100` (Redis).
184 ///
185 /// ```
186 /// let cfg = kevy_embedded::Config::default().with_auto_aof_rewrite(50, 1 << 20);
187 /// assert_eq!((cfg.auto_aof_rewrite_pct, cfg.auto_aof_rewrite_min_size), (50, 1 << 20));
188 /// assert_eq!(kevy_embedded::Config::default().auto_aof_rewrite_pct, 100);
189 /// ```
190 #[cfg(feature = "persist")]
191 pub auto_aof_rewrite_pct: u32,
192 /// Floor below which auto-rewrite is skipped. Default `64 MiB` (Redis).
193 ///
194 /// ```
195 /// let cfg = kevy_embedded::Config::default();
196 /// assert_eq!(cfg.auto_aof_rewrite_min_size, 64 << 20);
197 /// let cfg = cfg.with_auto_aof_rewrite(100, 8 << 20);
198 /// assert_eq!(cfg.auto_aof_rewrite_min_size, 8 << 20);
199 /// ```
200 #[cfg(feature = "persist")]
201 pub auto_aof_rewrite_min_size: u64,
202 /// Absolute-size auto-rewrite trigger in bytes (0 = off). The growth
203 /// rule alone lets a large log double before compacting — a 2.2 GB AOF
204 /// waits for 4.4 GB; this caps it outright.
205 ///
206 /// ```
207 /// let mut cfg = kevy_embedded::Config::default();
208 /// assert_eq!(cfg.auto_aof_rewrite_bytes, 0); // off
209 /// cfg.auto_aof_rewrite_bytes = 1 << 30; // compact once the log reaches 1 GiB
210 /// assert_eq!(cfg.auto_aof_rewrite_bytes, 1 << 30);
211 /// ```
212 pub auto_aof_rewrite_bytes: u64,
213 /// Time-based auto-rewrite trigger in seconds (0 = off): compact at
214 /// least this often while the log grows.
215 ///
216 /// ```
217 /// let mut cfg = kevy_embedded::Config::default();
218 /// assert_eq!(cfg.auto_aof_rewrite_interval_secs, 0); // off
219 /// cfg.auto_aof_rewrite_interval_secs = 3600; // compact at least hourly
220 /// assert_eq!(cfg.auto_aof_rewrite_interval_secs, 3600);
221 /// ```
222 pub auto_aof_rewrite_interval_secs: u64,
223 /// What replay does at a corrupt v2 record: stop there
224 /// ([`ReplayMode::Strict`], the default), or hop to the next valid
225 /// record (length + CRC + parse all agree) instead of dropping the
226 /// good tail behind it ([`ReplayMode::Resync`]).
227 ///
228 /// ```
229 /// use kevy_embedded::{Config, ReplayMode};
230 ///
231 /// assert_eq!(Config::default().replay_mode, ReplayMode::Strict);
232 /// let cfg = Config::default().with_replay_mode(ReplayMode::Resync);
233 /// assert_eq!(cfg.replay_mode, ReplayMode::Resync);
234 /// ```
235 #[cfg(feature = "persist")]
236 pub replay_mode: ReplayMode,
237 /// Size in bytes of each shard's staging ring (0 = off): appends land
238 /// in a shared file mapping, so a process that is killed keeps every
239 /// write that returned. Only `EverySec` and `No` stage. Default 4 MiB.
240 ///
241 /// ```
242 /// let cfg = kevy_embedded::Config::default();
243 /// assert_eq!(cfg.stage_bytes, 4 << 20);
244 /// assert_eq!(cfg.with_stage_ring(0).stage_bytes, 0); // staging off
245 /// ```
246 pub stage_bytes: u64,
247 /// Append by copying into a mapping of the AOF's preallocated tail
248 /// instead of `write()`, under `EverySec` and `No`. It keeps a killed
249 /// process's writes as the staging ring does, and replaces it. On by
250 /// default on Apple platforms only, where a fresh mapped page is cheap;
251 /// on Linux the page fault makes it slower than `write()`.
252 ///
253 /// ```
254 /// let cfg = kevy_embedded::Config::default();
255 /// assert_eq!(cfg.mapped_aof, cfg!(target_vendor = "apple"));
256 /// assert!(!cfg.with_mapped_aof(false).mapped_aof);
257 /// ```
258 pub mapped_aof: bool,
259 /// Optional push-style metric callback (replay / rewrite events). Default
260 /// `None`. Set via [`Self::with_metric_sink`]; not part of `Debug` output.
261 #[cfg(feature = "persist")]
262 pub(crate) metric_sink: Option<crate::metric::MetricSink>,
263 /// Keyspace shard count (`hash(key) % shards`), each a fully independent
264 /// lock + keyspace + AOF (shared-nothing) — concurrent access scales across
265 /// cores. **Default `1`** (single shard = the original single-lock /
266 /// single-`aof-0.aof` layout, zero migration). Set `> 1` via
267 /// [`Self::with_shards`]; the first open with `> 1` re-shards an existing
268 /// single AOF into per-shard files.
269 ///
270 /// ```
271 /// use kevy_embedded::{Config, Store};
272 ///
273 /// assert_eq!(Config::default().with_shards(0).shards, 1); // clamped
274 /// let s = Store::open(Config::default().with_shards(4))?;
275 /// s.set(b"a", b"1")?;
276 /// s.set(b"b", b"2")?;
277 /// assert_eq!(s.info().keys, 2); // counted across all four shards
278 /// # Ok::<(), kevy_embedded::KevyError>(())
279 /// ```
280 pub shards: usize,
281 /// Replication upstream. `Some("host:port")` makes this store a
282 /// read-replica that streams writes from the named primary; `None`
283 /// (default) is a normal primary store. Configured via
284 /// [`Self::with_replica_upstream`] or the convenience constructor
285 /// [`crate::Store::open_replica`].
286 ///
287 /// ```
288 /// let cfg = kevy_embedded::Config::default().with_replica_upstream("10.0.0.5:6380");
289 /// assert_eq!(cfg.replica_upstream.as_deref(), Some("10.0.0.5:6380"));
290 /// assert!(kevy_embedded::Config::default().replica_upstream.is_none());
291 /// ```
292 #[cfg(feature = "replicate")]
293 pub replica_upstream: Option<String>,
294 /// Replica identity string sent to the primary at handshake
295 /// (`REPLICATE FROM <offset> ID <replica_id>`). Default
296 /// `"kevy-embedded-replica"`. Override per-process when multiple
297 /// embed replicas connect to the same primary (they'd otherwise
298 /// share the slot and clobber each other's session state on the
299 /// primary side).
300 ///
301 /// ```
302 /// let cfg = kevy_embedded::Config::default();
303 /// assert_eq!(cfg.replica_id, "kevy-embedded-replica");
304 /// assert_eq!(cfg.with_replica_id("edge-7").replica_id, "edge-7");
305 /// ```
306 #[cfg(feature = "replicate")]
307 pub replica_id: String,
308 /// Replica reconnect backoff: lower bound. Default 100 ms. The
309 /// runner sleeps this long after the first connection failure;
310 /// each subsequent failure doubles the wait up to
311 /// [`Self::replica_reconnect_max`].
312 ///
313 /// ```
314 /// use std::time::Duration;
315 ///
316 /// let cfg = kevy_embedded::Config::default()
317 /// .with_replica_reconnect(Duration::from_millis(50), Duration::from_secs(2));
318 /// assert_eq!(cfg.replica_reconnect_min, Duration::from_millis(50));
319 /// ```
320 #[cfg(feature = "replicate")]
321 pub replica_reconnect_min: Duration,
322 /// Replica reconnect backoff: upper bound. Default 5 s — matches
323 /// the server-side replica reconnect default so embed replicas and
324 /// server replicas behave identically when the same primary
325 /// disappears.
326 ///
327 /// ```
328 /// use std::time::Duration;
329 ///
330 /// let min = Duration::from_secs(1);
331 /// let cfg = kevy_embedded::Config::default().with_replica_reconnect(min, Duration::from_millis(10));
332 /// assert_eq!(cfg.replica_reconnect_max, min); // never below the lower bound
333 /// ```
334 #[cfg(feature = "replicate")]
335 pub replica_reconnect_max: Duration,
336 /// Embed-as-writer bind address (`"host:port"` or
337 /// `"0.0.0.0:port"`) for the replication source listener. When
338 /// `Some`, every commit on this store pushes its argv into a
339 /// process-local `ReplicationSource` backlog, and replicas (other
340 /// embeds, server-as-replicas) connect to this port to stream the
341 /// writes. `None` (default) keeps the embed in pure-local mode.
342 /// Mutually exclusive in spirit with `replica_upstream` (a single
343 /// store should be either a writer source or a reader sink, not
344 /// both); the builder does not reject the combo so tests can
345 /// exercise the guard rails.
346 ///
347 /// ```
348 /// let cfg = kevy_embedded::Config::default().with_embed_writer("127.0.0.1:6390");
349 /// assert_eq!(cfg.embed_writer_listen_addr.as_deref(), Some("127.0.0.1:6390"));
350 /// assert!(kevy_embedded::Config::default().embed_writer_listen_addr.is_none());
351 /// ```
352 #[cfg(feature = "replicate")]
353 pub embed_writer_listen_addr: Option<String>,
354 /// Noise keys for either replication direction; both plaintext by
355 /// default. Set via [`Self::with_replica_security`] and
356 /// [`Self::with_writer_security`].
357 #[cfg(feature = "replicate")]
358 pub(crate) link_security: crate::config_secure::LinkSecurity,
359 /// CDC feed (changes_since / changes_tail). Default off.
360 ///
361 /// ```
362 /// use kevy_embedded::{Config, Store};
363 ///
364 /// let cfg = Config::default().with_feed(0);
365 /// assert!(cfg.feed_enabled);
366 /// let s = Store::open(cfg)?;
367 /// assert!(s.changes_tail().is_ok()); // a disabled feed answers Disabled
368 /// # Ok::<(), Box<dyn std::error::Error>>(())
369 /// ```
370 #[cfg(feature = "replicate")]
371 pub feed_enabled: bool,
372 /// Feed backlog byte budget. Default 64 MB, capped at 1 GB.
373 ///
374 /// ```
375 /// let cfg = kevy_embedded::Config::default().with_feed(0);
376 /// assert_eq!(cfg.feed_buffer_size, 64 << 20);
377 /// let cfg = kevy_embedded::Config::default().with_feed(4 << 30);
378 /// assert_eq!(cfg.feed_buffer_size, 1 << 30); // capped at 1 GB
379 /// ```
380 #[cfg(feature = "replicate")]
381 pub feed_buffer_size: u64,
382 /// Backlog byte budget for the embed-as-writer source. Default
383 /// `1 MiB` (matches the server replication default).
384 /// Set higher when consumers may disconnect for longer than
385 /// the backlog can buffer (otherwise a reconnect falls past the
386 /// backlog and is re-seeded with a full snapshot ship instead of
387 /// an incremental stream).
388 ///
389 /// ```
390 /// let cfg = kevy_embedded::Config::default();
391 /// assert_eq!(cfg.embed_writer_backlog_bytes, 1 << 20);
392 /// assert_eq!(cfg.with_embed_writer_backlog(1).embed_writer_backlog_bytes, 64 << 10); // floor
393 /// ```
394 #[cfg(feature = "replicate")]
395 pub embed_writer_backlog_bytes: usize,
396}
397
398impl Default for Config {
399 // LOC-WAIVER: pure field-default table, one line (and its cfg) per field
400 fn default() -> Self {
401 Self {
402 #[cfg(feature = "listener")]
403 resp_listener: None,
404 maxmemory: 0,
405 eviction_policy: EvictionPolicy::NoEviction,
406 #[cfg(feature = "persist")]
407 data_dir: None,
408 #[cfg(feature = "persist")]
409 aof: true,
410 #[cfg(feature = "persist")]
411 appendfsync: AppendFsync::EverySec,
412 #[cfg(feature = "tier")]
413 tier_budget: None,
414 max_spill_value: 256 << 10,
415 ttl_reaper: TtlReaperMode::Background,
416 reaper_interval: Duration::from_millis(100),
417 reaper_samples: 20,
418 reaper_max_rounds: 16,
419 #[cfg(feature = "persist")]
420 auto_aof_rewrite_pct: 100,
421 #[cfg(feature = "persist")]
422 auto_aof_rewrite_min_size: 64 * 1024 * 1024,
423 auto_aof_rewrite_bytes: 0,
424 auto_aof_rewrite_interval_secs: 0,
425 #[cfg(feature = "persist")]
426 replay_mode: ReplayMode::Strict,
427 stage_bytes: 4 * 1024 * 1024,
428 mapped_aof: cfg!(target_vendor = "apple"),
429 #[cfg(feature = "persist")]
430 metric_sink: None,
431 shards: 1,
432 #[cfg(feature = "replicate")]
433 replica_upstream: None,
434 #[cfg(feature = "replicate")]
435 replica_id: String::from("kevy-embedded-replica"),
436 #[cfg(feature = "replicate")]
437 replica_reconnect_min: Duration::from_millis(100),
438 #[cfg(feature = "replicate")]
439 replica_reconnect_max: Duration::from_secs(5),
440 #[cfg(feature = "replicate")]
441 embed_writer_listen_addr: None,
442 #[cfg(feature = "replicate")]
443 link_security: Default::default(),
444 #[cfg(feature = "replicate")]
445 feed_enabled: false,
446 #[cfg(feature = "replicate")]
447 feed_buffer_size: 64 * 1024 * 1024,
448 #[cfg(feature = "replicate")]
449 embed_writer_backlog_bytes: 1024 * 1024,
450 }
451 }
452}
453
454#[path = "config_builders.rs"]
455mod config_builders;
456
457#[path = "config_tier_builders.rs"]
458mod config_tier_builders;