Skip to main content

nodedb_lite/config/
types.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! The [`LiteConfig`] struct and its defaults.
4
5use serde::{Deserialize, Serialize};
6
7use super::defaults::{
8    default_argon2_m_cost, default_argon2_p_cost, default_argon2_t_cost, default_array_percent,
9    default_auto_compact_ms, default_auto_flush_ms, default_columnar_percent,
10    default_document_percent, default_fts_percent, default_kv_cache_capacity, default_kv_percent,
11    default_outbound_queue_cap, default_sparse_percent, default_spatial_percent,
12    default_strict_percent, default_sync_enabled, default_timeseries_percent,
13};
14use crate::storage::corruption::CorruptionPolicy;
15
16/// Runtime configuration for a NodeDB-Lite instance.
17///
18/// All percentage fields express a fraction of `memory_budget` allocated to
19/// the corresponding engine. The remaining percentage is headroom (untracked).
20///
21/// # Example
22/// ```
23/// use nodedb_lite::config::LiteConfig;
24///
25/// let cfg = LiteConfig {
26///     memory_budget: 256 * 1024 * 1024, // 256 MiB
27///     ..LiteConfig::default()
28/// };
29/// ```
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct LiteConfig {
32    /// Total memory budget in bytes. Default: 100 MiB.
33    pub memory_budget: usize,
34
35    /// Percentage of `memory_budget` reserved for HNSW vector index. Default: 40.
36    pub hnsw_percent: usize,
37
38    /// Percentage of `memory_budget` reserved for CSR graph index. Default: 15.
39    pub csr_percent: usize,
40
41    /// Percentage of `memory_budget` reserved for Loro CRDT engine. Default: 15.
42    pub loro_percent: usize,
43
44    /// Percentage of `memory_budget` reserved for query scratch space. Default: 15.
45    pub query_percent: usize,
46
47    /// Percentage of `memory_budget` reserved for the key-value engine. Default: 2.
48    #[serde(default = "default_kv_percent")]
49    pub kv_percent: usize,
50
51    /// Percentage of `memory_budget` reserved for the schemaless document engine. Default: 2.
52    #[serde(default = "default_document_percent")]
53    pub document_percent: usize,
54
55    /// Percentage of `memory_budget` reserved for the strict document engine. Default: 2.
56    #[serde(default = "default_strict_percent")]
57    pub strict_percent: usize,
58
59    /// Percentage of `memory_budget` reserved for the columnar engine. Default: 2.
60    #[serde(default = "default_columnar_percent")]
61    pub columnar_percent: usize,
62
63    /// Percentage of `memory_budget` reserved for the timeseries engine. Default: 1.
64    #[serde(default = "default_timeseries_percent")]
65    pub timeseries_percent: usize,
66
67    /// Percentage of `memory_budget` reserved for the spatial engine. Default: 1.
68    #[serde(default = "default_spatial_percent")]
69    pub spatial_percent: usize,
70
71    /// Percentage of `memory_budget` reserved for the full-text search engine. Default: 1.
72    #[serde(default = "default_fts_percent")]
73    pub fts_percent: usize,
74
75    /// Percentage of `memory_budget` reserved for the array engine. Default: 1.
76    #[serde(default = "default_array_percent")]
77    pub array_percent: usize,
78
79    /// Percentage of `memory_budget` reserved for the sparse-vector metadata engine. Default: 1.
80    #[serde(default = "default_sparse_percent")]
81    pub sparse_percent: usize,
82
83    /// Open the durable outbound sync queues. Default: `true`.
84    ///
85    /// When `true`, KV, columnar, timeseries, vector, FTS and spatial writes
86    /// record themselves in their outbound queue, and the push loop sends
87    /// them to Origin. When `false`, those writes stay local. Documents sync
88    /// through Loro either way.
89    #[serde(default = "default_sync_enabled")]
90    pub sync_enabled: bool,
91
92    /// Argon2id memory cost in KiB. Default: 19 MiB (19_456 KiB).
93    /// Corresponds to the OWASP recommended minimum for interactive login.
94    #[serde(default = "default_argon2_m_cost")]
95    pub argon2_m_cost: u32,
96
97    /// Argon2id iteration count. Default: 2.
98    #[serde(default = "default_argon2_t_cost")]
99    pub argon2_t_cost: u32,
100
101    /// Argon2id parallelism lanes. Default: 1.
102    #[serde(default = "default_argon2_p_cost")]
103    pub argon2_p_cost: u32,
104
105    /// Maximum entries in the in-memory KV read cache. Default: 10_000.
106    ///
107    /// Each entry holds the raw encoded value (typically ~80 bytes for a
108    /// 64-byte payload + 8-byte TTL framing), so 10 000 entries ≈ 800 KB.
109    ///
110    /// A value of 0 is rejected at open time; use 1 as the effective minimum.
111    #[serde(default = "default_kv_cache_capacity")]
112    pub kv_cache_capacity: usize,
113
114    /// Maximum number of pending entries in each durable outbound queue
115    /// (columnar and timeseries). Default: 100_000.
116    ///
117    /// When a queue reaches this cap, write operations return
118    /// [`LiteError::Backpressure`] until the sync transport drains entries.
119    /// This bounds RAM usage to the key/pointer overhead regardless of how
120    /// long the device stays offline; the payloads themselves are on disk.
121    ///
122    /// Can also be set via the `NODEDB_LITE_OUTBOUND_QUEUE_CAP` environment
123    /// variable.
124    #[serde(default = "default_outbound_queue_cap")]
125    pub outbound_queue_cap: usize,
126
127    /// Scheduled interval between background flushes, in milliseconds. Default: 1000 (one second).
128    ///
129    /// Opening a database starts a task that applies buffered document, CRDT, and KV state and persists derived checkpoints.
130    /// Scheduling delays and storage errors prevent a hard wall-clock durability bound.
131    /// The task stops when the last database handle disappears.
132    ///
133    /// Strict row writes commit before success. Native filesystem pagedb commits survive reopening without an explicit flush.
134    /// Generic storage backends provide their own commit durability guarantees.
135    /// Unindexed KV puts and all KV deletes remain buffered. Indexed KV puts and SQL KV writes commit directly.
136    /// Derived checkpoint maintenance remains separate from strict commits. Later maintenance errors do not roll back committed rows.
137    ///
138    /// Set to 0 to disable background flushing. Call `flush()` before dropping buffered state.
139    #[serde(default = "default_auto_flush_ms")]
140    pub auto_flush_ms: u64,
141
142    /// Interval between automatic background compactions, in milliseconds.
143    /// Default: 0 (disabled).
144    ///
145    /// When non-zero, opening a database spawns a background task that calls
146    /// the global `compact()` every `auto_compact_ms` milliseconds, reclaiming
147    /// dead pages and truncating the backing file to bound on-disk growth.
148    /// Unlike auto-flush this is **opt-in**: compaction is a heavier operation
149    /// (it repacks B+ trees and truncates the file, and no-ops while a reader
150    /// pins the reclaimable range), so it is not imposed on every embedder by
151    /// default.
152    ///
153    /// Enable it when writing one commit per entry (where the deferred-free
154    /// list would otherwise grow unbounded). A much larger interval than
155    /// `auto_flush_ms` is appropriate — e.g. minutes, not seconds. Set to 0 to
156    /// leave compaction fully manual via `compact()`.
157    #[serde(default = "default_auto_compact_ms")]
158    pub auto_compact_ms: u64,
159
160    /// What to do when the store being opened cannot be read.
161    /// Default: [`CorruptionPolicy::FailClosed`].
162    ///
163    /// The default refuses to open and leaves the damaged store untouched, so
164    /// an embedder that has no Origin to re-sync from does not silently start
165    /// against an empty database. Selecting
166    /// [`CorruptionPolicy::DiscardStoreAndRecreate`] is a statement that the
167    /// data can be rebuilt from somewhere else.
168    #[serde(default)]
169    pub corruption_policy: CorruptionPolicy,
170}
171
172impl Default for LiteConfig {
173    fn default() -> Self {
174        Self {
175            memory_budget: 100 * 1024 * 1024, // 100 MiB
176            hnsw_percent: 40,
177            csr_percent: 15,
178            loro_percent: 15,
179            query_percent: 15,
180            kv_percent: default_kv_percent(),
181            document_percent: default_document_percent(),
182            strict_percent: default_strict_percent(),
183            columnar_percent: default_columnar_percent(),
184            timeseries_percent: default_timeseries_percent(),
185            spatial_percent: default_spatial_percent(),
186            fts_percent: default_fts_percent(),
187            array_percent: default_array_percent(),
188            sparse_percent: default_sparse_percent(),
189            sync_enabled: true,
190            outbound_queue_cap: default_outbound_queue_cap(),
191            argon2_m_cost: default_argon2_m_cost(),
192            argon2_t_cost: default_argon2_t_cost(),
193            argon2_p_cost: default_argon2_p_cost(),
194            kv_cache_capacity: default_kv_cache_capacity(),
195            auto_flush_ms: default_auto_flush_ms(),
196            auto_compact_ms: default_auto_compact_ms(),
197            corruption_policy: CorruptionPolicy::FailClosed,
198        }
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn default_config() {
208        let cfg = LiteConfig::default();
209        assert_eq!(cfg.memory_budget, 100 * 1024 * 1024);
210        assert_eq!(cfg.hnsw_percent, 40);
211        assert_eq!(cfg.csr_percent, 15);
212        assert_eq!(cfg.loro_percent, 15);
213        assert_eq!(cfg.query_percent, 15);
214        assert_eq!(cfg.kv_percent, 2);
215        assert_eq!(cfg.document_percent, 2);
216        assert_eq!(cfg.strict_percent, 2);
217        assert_eq!(cfg.columnar_percent, 2);
218        assert_eq!(cfg.timeseries_percent, 1);
219        assert_eq!(cfg.spatial_percent, 1);
220        assert_eq!(cfg.fts_percent, 1);
221        assert_eq!(cfg.array_percent, 1);
222        assert_eq!(cfg.sparse_percent, 1);
223        assert_eq!(cfg.argon2_m_cost, 19_456);
224        assert_eq!(cfg.argon2_t_cost, 2);
225        assert_eq!(cfg.argon2_p_cost, 1);
226        assert_eq!(cfg.auto_flush_ms, 1_000);
227        // Auto-compaction is opt-in: disabled by default.
228        assert_eq!(cfg.auto_compact_ms, 0);
229    }
230
231    #[test]
232    fn serde_roundtrip() {
233        let cfg = LiteConfig::default();
234        let json = sonic_rs::to_string(&cfg).unwrap();
235        let parsed: LiteConfig = sonic_rs::from_str(&json).unwrap();
236        assert_eq!(parsed, cfg);
237    }
238}