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
5use std::path::PathBuf;
6use std::time::Duration;
7
8pub use kevy_persist::Fsync as AppendFsync;
9pub use kevy_store::EvictionPolicy;
10
11/// How the active TTL reaper runs.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum TtlReaperMode {
14    /// Spawn a background thread that ticks at the configured interval
15    /// (default 100 ms / 10 Hz, matching Redis's `hz=10`). Default.
16    Background,
17    /// Caller-driven via [`crate::Store::tick`]. Required for WASM
18    /// targets (no threads) and single-threaded apps that don't want a
19    /// background worker.
20    Manual,
21}
22
23/// Embedded-store config. Build by chaining `with_*` methods on
24/// [`Config::default`].
25#[derive(Debug, Clone)]
26pub struct Config {
27    /// Soft memory ceiling in bytes. `0` (default) = unlimited.
28    pub maxmemory: u64,
29    /// Eviction policy when over `maxmemory`. Default `NoEviction`.
30    pub eviction_policy: EvictionPolicy,
31    /// Persistence directory. `None` = pure in-memory (no AOF, no snapshot).
32    pub data_dir: Option<PathBuf>,
33    /// AOF on/off when `data_dir` is set. Defaults to `true` (on) when
34    /// `with_persist` was called; ignored if `data_dir` is `None`.
35    pub aof: bool,
36    /// AOF fsync policy. Default `EverySec` (matches Redis: ≤ 1 s loss).
37    pub appendfsync: AppendFsync,
38    /// Snapshot file name inside `data_dir` (single-shard only; `n > 1`
39    /// always uses `dump-{i}.rdb`). Default `"dump-0.rdb"`. A custom name
40    /// opts the dir out of server interop: no `shards.meta` is recorded,
41    /// and a `kevy` server opening the same dir won't find the files.
42    pub snapshot_filename: String,
43    /// AOF file name inside `data_dir` (single-shard only; `n > 1` always
44    /// uses `aof-{i}.aof`). Default `"aof-0.aof"`. Same interop opt-out as
45    /// [`Self::snapshot_filename`].
46    pub aof_filename: String,
47    /// TTL reaper mode. Default `Background`.
48    pub ttl_reaper: TtlReaperMode,
49    /// Reaper tick interval. Default 100 ms (10 Hz).
50    pub reaper_interval: Duration,
51    /// `tick_expire` samples per round. Default 20 (matches Redis).
52    pub reaper_samples: usize,
53    /// Max sample rounds per tick. Default 16.
54    pub reaper_max_rounds: u32,
55    /// Auto-`BGREWRITEAOF` trigger: rewrite when the live AOF has grown by at
56    /// least this percent over its size at the previous rewrite. `0` disables
57    /// (call [`crate::Store::rewrite_aof`] manually). Default `100` (Redis).
58    pub auto_aof_rewrite_pct: u32,
59    /// Floor below which auto-rewrite is skipped. Default `64 MiB` (Redis).
60    pub auto_aof_rewrite_min_size: u64,
61    /// Optional push-style metric callback (replay / rewrite events). Default
62    /// `None`. Set via [`Self::with_metric_sink`]; not part of `Debug` output.
63    pub(crate) metric_sink: Option<crate::metric::MetricSink>,
64    /// Keyspace shard count (`hash(key) % shards`), each a fully independent
65    /// lock + keyspace + AOF (shared-nothing) — concurrent access scales across
66    /// cores. **Default `1`** (single shard = the original single-lock /
67    /// single-`aof-0.aof` layout, zero migration). Set `> 1` via
68    /// [`Self::with_shards`]; the first open with `> 1` re-shards an existing
69    /// single AOF into per-shard files.
70    pub shards: usize,
71}
72
73impl Default for Config {
74    fn default() -> Self {
75        Self {
76            maxmemory: 0,
77            eviction_policy: EvictionPolicy::NoEviction,
78            data_dir: None,
79            aof: true,
80            appendfsync: AppendFsync::EverySec,
81            snapshot_filename: String::from("dump-0.rdb"),
82            aof_filename: String::from("aof-0.aof"),
83            ttl_reaper: TtlReaperMode::Background,
84            reaper_interval: Duration::from_millis(100),
85            reaper_samples: 20,
86            reaper_max_rounds: 16,
87            auto_aof_rewrite_pct: 100,
88            auto_aof_rewrite_min_size: 64 * 1024 * 1024,
89            metric_sink: None,
90            shards: 1,
91        }
92    }
93}
94
95impl Config {
96    /// Enable persistence under `dir` — snapshot file + AOF land inside.
97    /// AOF defaults on; turn it off with [`Self::without_aof`] for pure
98    /// snapshot-only durability.
99    #[must_use]
100    pub fn with_persist(mut self, dir: impl Into<PathBuf>) -> Self {
101        self.data_dir = Some(dir.into());
102        self
103    }
104
105    /// Disable the AOF (snapshot-only persistence — explicit `save_snapshot`
106    /// calls are the only way data survives restart).
107    #[must_use]
108    pub fn without_aof(mut self) -> Self {
109        self.aof = false;
110        self
111    }
112
113    /// Soft memory ceiling in bytes. `0` keeps the default (unlimited).
114    #[must_use]
115    pub fn with_max_memory(mut self, bytes: u64) -> Self {
116        self.maxmemory = bytes;
117        self
118    }
119
120    /// Eviction policy when over [`Self::with_max_memory`].
121    #[must_use]
122    pub fn with_eviction(mut self, policy: EvictionPolicy) -> Self {
123        self.eviction_policy = policy;
124        self
125    }
126
127    /// AOF fsync policy. Default [`AppendFsync::EverySec`].
128    #[must_use]
129    pub fn with_appendfsync(mut self, fsync: AppendFsync) -> Self {
130        self.appendfsync = fsync;
131        self
132    }
133
134    /// Auto-`BGREWRITEAOF` thresholds: rewrite once the AOF has grown `pct`
135    /// percent past its size at the last rewrite AND is at least `min_size`
136    /// bytes. In `Background` reaper mode the check runs on the reaper tick;
137    /// in `Manual` mode it runs when you call [`crate::Store::tick`]. Pass
138    /// `pct = 0` to disable auto-rewrite (you can still call
139    /// [`crate::Store::rewrite_aof`] yourself). Defaults: 100 % / 64 MiB.
140    #[must_use]
141    pub fn with_auto_aof_rewrite(mut self, pct: u32, min_size: u64) -> Self {
142        self.auto_aof_rewrite_pct = pct;
143        self.auto_aof_rewrite_min_size = min_size;
144        self
145    }
146
147    /// Shard the keyspace into `n` shared-nothing partitions (`hash(key) % n`),
148    /// each with its own lock + keyspace + AOF, so concurrent access scales
149    /// across cores. `n` clamps to ≥ 1; `1` (default) is the original
150    /// single-shard layout. Going from a single-AOF store to `n > 1`
151    /// re-shards the existing `aof-0.aof` into `aof-0..aof-{n-1}` on the next
152    /// open (the old file is backed up to `aof-0.aof.premigration.<ts>` first).
153    /// Pub/sub is process-wide (handled on shard 0), not sharded.
154    #[must_use]
155    pub fn with_shards(mut self, n: usize) -> Self {
156        self.shards = n.max(1);
157        self
158    }
159
160    /// Register a push-style metric callback. It receives a [`crate::KevyMetric`] for
161    /// each AOF replay (startup) and AOF rewrite (compaction) — wire it to
162    /// Prometheus / a log line / a counter. The callback runs synchronously on
163    /// the emitting thread (reaper thread for background rewrites), so keep it
164    /// fast and non-blocking. Replaces any previously-set sink.
165    #[must_use]
166    pub fn with_metric_sink(
167        mut self,
168        sink: impl Fn(crate::KevyMetric) + Send + Sync + 'static,
169    ) -> Self {
170        self.metric_sink = Some(crate::metric::MetricSink::new(sink));
171        self
172    }
173
174    /// Caller-driven TTL reaping — disables the background thread.
175    /// Required for WASM (no threads available). Call
176    /// [`crate::Store::tick`] yourself from your event loop.
177    #[must_use]
178    pub fn with_ttl_reaper_manual(mut self) -> Self {
179        self.ttl_reaper = TtlReaperMode::Manual;
180        self
181    }
182
183    /// Override the background reaper interval. Default 100 ms.
184    #[must_use]
185    pub fn with_reaper_interval(mut self, iv: Duration) -> Self {
186        self.reaper_interval = iv;
187        self
188    }
189
190    /// Override the snapshot file name inside `data_dir`.
191    #[must_use]
192    pub fn with_snapshot_filename(mut self, name: impl Into<String>) -> Self {
193        self.snapshot_filename = name.into();
194        self
195    }
196
197    /// Override the AOF file name inside `data_dir`.
198    #[must_use]
199    pub fn with_aof_filename(mut self, name: impl Into<String>) -> Self {
200        self.aof_filename = name.into();
201        self
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn default_is_pure_in_memory() {
211        let c = Config::default();
212        assert_eq!(c.maxmemory, 0);
213        assert!(c.data_dir.is_none());
214        assert_eq!(c.ttl_reaper, TtlReaperMode::Background);
215        assert!(c.aof);
216    }
217
218    #[test]
219    fn builder_chains() {
220        let c = Config::default()
221            .with_persist("/tmp/foo")
222            .with_max_memory(1024)
223            .with_eviction(EvictionPolicy::AllKeysLru)
224            .with_ttl_reaper_manual()
225            .with_appendfsync(AppendFsync::Always);
226        assert_eq!(c.data_dir.as_deref(), Some(std::path::Path::new("/tmp/foo")));
227        assert_eq!(c.maxmemory, 1024);
228        assert_eq!(c.eviction_policy, EvictionPolicy::AllKeysLru);
229        assert_eq!(c.ttl_reaper, TtlReaperMode::Manual);
230    }
231
232    #[test]
233    fn without_aof_disables_logging_path() {
234        let c = Config::default().with_persist("/tmp/foo").without_aof();
235        assert!(c.data_dir.is_some());
236        assert!(!c.aof);
237    }
238}