Skip to main content

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;