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}