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`. Default `"dump-0.rdb"`.
39    pub snapshot_filename: String,
40    /// AOF file name inside `data_dir`. Default `"aof-0.aof"`.
41    pub aof_filename: String,
42    /// TTL reaper mode. Default `Background`.
43    pub ttl_reaper: TtlReaperMode,
44    /// Reaper tick interval. Default 100 ms (10 Hz).
45    pub reaper_interval: Duration,
46    /// `tick_expire` samples per round. Default 20 (matches Redis).
47    pub reaper_samples: usize,
48    /// Max sample rounds per tick. Default 16.
49    pub reaper_max_rounds: u32,
50    /// Auto-`BGREWRITEAOF` trigger: rewrite when the live AOF has grown by at
51    /// least this percent over its size at the previous rewrite. `0` disables
52    /// (call [`crate::Store::rewrite_aof`] manually). Default `100` (Redis).
53    pub auto_aof_rewrite_pct: u32,
54    /// Floor below which auto-rewrite is skipped. Default `64 MiB` (Redis).
55    pub auto_aof_rewrite_min_size: u64,
56}
57
58impl Default for Config {
59    fn default() -> Self {
60        Self {
61            maxmemory: 0,
62            eviction_policy: EvictionPolicy::NoEviction,
63            data_dir: None,
64            aof: true,
65            appendfsync: AppendFsync::EverySec,
66            snapshot_filename: String::from("dump-0.rdb"),
67            aof_filename: String::from("aof-0.aof"),
68            ttl_reaper: TtlReaperMode::Background,
69            reaper_interval: Duration::from_millis(100),
70            reaper_samples: 20,
71            reaper_max_rounds: 16,
72            auto_aof_rewrite_pct: 100,
73            auto_aof_rewrite_min_size: 64 * 1024 * 1024,
74        }
75    }
76}
77
78impl Config {
79    /// Enable persistence under `dir` — snapshot file + AOF land inside.
80    /// AOF defaults on; turn it off with [`Self::without_aof`] for pure
81    /// snapshot-only durability.
82    pub fn with_persist(mut self, dir: impl Into<PathBuf>) -> Self {
83        self.data_dir = Some(dir.into());
84        self
85    }
86
87    /// Disable the AOF (snapshot-only persistence — explicit `save_snapshot`
88    /// calls are the only way data survives restart).
89    pub fn without_aof(mut self) -> Self {
90        self.aof = false;
91        self
92    }
93
94    /// Soft memory ceiling in bytes. `0` keeps the default (unlimited).
95    pub fn with_max_memory(mut self, bytes: u64) -> Self {
96        self.maxmemory = bytes;
97        self
98    }
99
100    /// Eviction policy when over [`Self::with_max_memory`].
101    pub fn with_eviction(mut self, policy: EvictionPolicy) -> Self {
102        self.eviction_policy = policy;
103        self
104    }
105
106    /// AOF fsync policy. Default [`AppendFsync::EverySec`].
107    pub fn with_appendfsync(mut self, fsync: AppendFsync) -> Self {
108        self.appendfsync = fsync;
109        self
110    }
111
112    /// Auto-`BGREWRITEAOF` thresholds: rewrite once the AOF has grown `pct`
113    /// percent past its size at the last rewrite AND is at least `min_size`
114    /// bytes. In `Background` reaper mode the check runs on the reaper tick;
115    /// in `Manual` mode it runs when you call [`crate::Store::tick`]. Pass
116    /// `pct = 0` to disable auto-rewrite (you can still call
117    /// [`crate::Store::rewrite_aof`] yourself). Defaults: 100 % / 64 MiB.
118    pub fn with_auto_aof_rewrite(mut self, pct: u32, min_size: u64) -> Self {
119        self.auto_aof_rewrite_pct = pct;
120        self.auto_aof_rewrite_min_size = min_size;
121        self
122    }
123
124    /// Caller-driven TTL reaping — disables the background thread.
125    /// Required for WASM (no threads available). Call
126    /// [`crate::Store::tick`] yourself from your event loop.
127    pub fn with_ttl_reaper_manual(mut self) -> Self {
128        self.ttl_reaper = TtlReaperMode::Manual;
129        self
130    }
131
132    /// Override the background reaper interval. Default 100 ms.
133    pub fn with_reaper_interval(mut self, iv: Duration) -> Self {
134        self.reaper_interval = iv;
135        self
136    }
137
138    /// Override the snapshot file name inside `data_dir`.
139    pub fn with_snapshot_filename(mut self, name: impl Into<String>) -> Self {
140        self.snapshot_filename = name.into();
141        self
142    }
143
144    /// Override the AOF file name inside `data_dir`.
145    pub fn with_aof_filename(mut self, name: impl Into<String>) -> Self {
146        self.aof_filename = name.into();
147        self
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn default_is_pure_in_memory() {
157        let c = Config::default();
158        assert_eq!(c.maxmemory, 0);
159        assert!(c.data_dir.is_none());
160        assert_eq!(c.ttl_reaper, TtlReaperMode::Background);
161        assert!(c.aof);
162    }
163
164    #[test]
165    fn builder_chains() {
166        let c = Config::default()
167            .with_persist("/tmp/foo")
168            .with_max_memory(1024)
169            .with_eviction(EvictionPolicy::AllKeysLru)
170            .with_ttl_reaper_manual()
171            .with_appendfsync(AppendFsync::Always);
172        assert_eq!(c.data_dir.as_deref(), Some(std::path::Path::new("/tmp/foo")));
173        assert_eq!(c.maxmemory, 1024);
174        assert_eq!(c.eviction_policy, EvictionPolicy::AllKeysLru);
175        assert_eq!(c.ttl_reaper, TtlReaperMode::Manual);
176    }
177
178    #[test]
179    fn without_aof_disables_logging_path() {
180        let c = Config::default().with_persist("/tmp/foo").without_aof();
181        assert!(c.data_dir.is_some());
182        assert!(!c.aof);
183    }
184}