Skip to content

Commit 5aa0448

Browse files
committed
feat(config,core): add auto-flush background task and outbound queue cap config
Add `LiteConfig::auto_flush_ms` (default 1000 ms) that drives a new `NodeDbLite::start_auto_flush` background task. The task calls `flush()` on each tick, bounding the data-loss window uniformly across all engines (KV buffer, CRDT deltas, HNSW id-map, CSR graph, FTS, spatial). Add `LiteConfig::outbound_queue_cap` (default 100 000) that caps the number of pending entries in each durable outbound sync queue. Writes return `LiteError::Backpressure` when the cap is reached. Both settings are also configurable via environment variables: `NODEDB_LITE_AUTO_FLUSH_MS` and `NODEDB_LITE_OUTBOUND_QUEUE_CAP`.
1 parent d221be2 commit 5aa0448

3 files changed

Lines changed: 207 additions & 3 deletions

File tree

nodedb-lite/src/config.rs

Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
//!
77
//! ## Environment variables
88
//!
9-
//! | Variable | Description | Default |
10-
//! |-------------------------|----------------------------------------------|---------|
11-
//! | `NODEDB_LITE_MEMORY_MB` | Total memory budget in mebibytes | 100 |
9+
//! | Variable | Description | Default |
10+
//! |-------------------------------|----------------------------------------------------|---------|
11+
//! | `NODEDB_LITE_MEMORY_MB` | Total memory budget in mebibytes | 100 |
12+
//! | `NODEDB_LITE_AUTO_FLUSH_MS` | Auto-flush interval in milliseconds (0 = disabled) | 1000 |
13+
//! | `NODEDB_LITE_OUTBOUND_QUEUE_CAP` | Max pending entries per durable outbound queue | 100000 |
1214
1315
use nodedb_types::error::{NodeDbError, NodeDbResult};
1416
use serde::{Deserialize, Serialize};
@@ -81,12 +83,47 @@ pub struct LiteConfig {
8183
/// A value of 0 is rejected at open time; use 1 as the effective minimum.
8284
#[serde(default = "default_kv_cache_capacity")]
8385
pub kv_cache_capacity: usize,
86+
87+
/// Maximum number of pending entries in each durable outbound queue
88+
/// (columnar and timeseries). Default: 100_000.
89+
///
90+
/// When a queue reaches this cap, write operations return
91+
/// [`LiteError::Backpressure`] until the sync transport drains entries.
92+
/// This bounds RAM usage to the key/pointer overhead regardless of how
93+
/// long the device stays offline; the payloads themselves are on disk.
94+
///
95+
/// Can also be set via the `NODEDB_LITE_OUTBOUND_QUEUE_CAP` environment
96+
/// variable.
97+
#[serde(default = "default_outbound_queue_cap")]
98+
pub outbound_queue_cap: usize,
99+
100+
/// Interval between automatic background flushes, in milliseconds.
101+
/// Default: 1000 (1 second).
102+
///
103+
/// The auto-flush task calls the global `flush()` every `auto_flush_ms`
104+
/// milliseconds, bounding the data-loss window uniformly across all engines
105+
/// (KV buffer, vector id-map, CRDT deltas, CSR graph, spatial, FTS).
106+
///
107+
/// **Durability contract**: `await`-ing a write operation (e.g. `kv_put`,
108+
/// `vector_insert`) returning `Ok` does NOT guarantee on-disk durability.
109+
/// Durability is bounded by `auto_flush_ms`. Set to 0 to disable the
110+
/// background task; call `flush()` explicitly to guarantee durability.
111+
#[serde(default = "default_auto_flush_ms")]
112+
pub auto_flush_ms: u64,
113+
}
114+
115+
fn default_outbound_queue_cap() -> usize {
116+
100_000
84117
}
85118

86119
fn default_kv_cache_capacity() -> usize {
87120
10_000
88121
}
89122

123+
fn default_auto_flush_ms() -> u64 {
124+
1_000
125+
}
126+
90127
fn default_sync_enabled() -> bool {
91128
true
92129
}
@@ -112,10 +149,12 @@ impl Default for LiteConfig {
112149
loro_percent: 15,
113150
query_percent: 15,
114151
sync_enabled: true,
152+
outbound_queue_cap: default_outbound_queue_cap(),
115153
argon2_m_cost: default_argon2_m_cost(),
116154
argon2_t_cost: default_argon2_t_cost(),
117155
argon2_p_cost: default_argon2_p_cost(),
118156
kv_cache_capacity: default_kv_cache_capacity(),
157+
auto_flush_ms: default_auto_flush_ms(),
119158
}
120159
}
121160
}
@@ -126,6 +165,10 @@ impl LiteConfig {
126165
///
127166
/// Handled variables:
128167
/// - `NODEDB_LITE_MEMORY_MB` — total memory budget in mebibytes (parsed as `usize`)
168+
/// - `NODEDB_LITE_AUTO_FLUSH_MS` — auto-flush interval in milliseconds (parsed as `u64`;
169+
/// 0 = disabled)
170+
/// - `NODEDB_LITE_OUTBOUND_QUEUE_CAP` — max pending entries per durable outbound queue
171+
/// (parsed as `usize`; must be > 0)
129172
pub fn from_env() -> Self {
130173
let mut cfg = Self::default();
131174

@@ -152,6 +195,54 @@ impl LiteConfig {
152195
}
153196
}
154197

198+
if let Ok(val) = std::env::var("NODEDB_LITE_OUTBOUND_QUEUE_CAP") {
199+
match val.trim().parse::<usize>() {
200+
Ok(cap) if cap > 0 => {
201+
tracing::info!(
202+
env_var = "NODEDB_LITE_OUTBOUND_QUEUE_CAP",
203+
value = cap,
204+
"environment variable override applied"
205+
);
206+
cfg.outbound_queue_cap = cap;
207+
}
208+
Ok(_) => {
209+
tracing::warn!(
210+
env_var = "NODEDB_LITE_OUTBOUND_QUEUE_CAP",
211+
"value must be > 0; using default 100_000"
212+
);
213+
}
214+
Err(_) => {
215+
tracing::warn!(
216+
env_var = "NODEDB_LITE_OUTBOUND_QUEUE_CAP",
217+
value = %val,
218+
"ignoring malformed environment variable (expected unsigned integer), \
219+
using default 100_000"
220+
);
221+
}
222+
}
223+
}
224+
225+
if let Ok(val) = std::env::var("NODEDB_LITE_AUTO_FLUSH_MS") {
226+
match val.trim().parse::<u64>() {
227+
Ok(ms) => {
228+
tracing::info!(
229+
env_var = "NODEDB_LITE_AUTO_FLUSH_MS",
230+
value = ms,
231+
"environment variable override applied"
232+
);
233+
cfg.auto_flush_ms = ms;
234+
}
235+
Err(_) => {
236+
tracing::warn!(
237+
env_var = "NODEDB_LITE_AUTO_FLUSH_MS",
238+
value = %val,
239+
"ignoring malformed environment variable (expected unsigned integer), \
240+
using default 1000 ms"
241+
);
242+
}
243+
}
244+
}
245+
155246
cfg
156247
}
157248

@@ -206,6 +297,7 @@ mod tests {
206297
assert_eq!(cfg.argon2_m_cost, 19_456);
207298
assert_eq!(cfg.argon2_t_cost, 2);
208299
assert_eq!(cfg.argon2_p_cost, 1);
300+
assert_eq!(cfg.auto_flush_ms, 1_000);
209301
}
210302

211303
#[test]
@@ -262,6 +354,37 @@ mod tests {
262354

263355
// Cleanup.
264356
unsafe { std::env::remove_var("NODEDB_LITE_MEMORY_MB") };
357+
358+
// NODEDB_LITE_AUTO_FLUSH_MS cases.
359+
360+
// Case A: var absent → default 1000.
361+
unsafe { std::env::remove_var("NODEDB_LITE_AUTO_FLUSH_MS") };
362+
let cfg = LiteConfig::from_env();
363+
assert_eq!(
364+
cfg.auto_flush_ms, 1_000,
365+
"absent var should give default 1000 ms"
366+
);
367+
368+
// Case B: valid integer → applied.
369+
unsafe { std::env::set_var("NODEDB_LITE_AUTO_FLUSH_MS", "500") };
370+
let cfg = LiteConfig::from_env();
371+
assert_eq!(cfg.auto_flush_ms, 500, "500 ms should be applied");
372+
373+
// Case C: 0 = disabled.
374+
unsafe { std::env::set_var("NODEDB_LITE_AUTO_FLUSH_MS", "0") };
375+
let cfg = LiteConfig::from_env();
376+
assert_eq!(cfg.auto_flush_ms, 0, "0 should disable auto-flush");
377+
378+
// Case D: malformed → fallback to default.
379+
unsafe { std::env::set_var("NODEDB_LITE_AUTO_FLUSH_MS", "not_a_number") };
380+
let cfg = LiteConfig::from_env();
381+
assert_eq!(
382+
cfg.auto_flush_ms, 1_000,
383+
"malformed var should fall back to default 1000 ms"
384+
);
385+
386+
// Cleanup.
387+
unsafe { std::env::remove_var("NODEDB_LITE_AUTO_FLUSH_MS") };
265388
}
266389

267390
#[test]
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
//! `NodeDbLite::start_auto_flush` — durable background flush task.
4+
5+
use std::sync::{Arc, Weak};
6+
use std::time::Duration;
7+
8+
use crate::storage::engine::StorageEngine;
9+
10+
use super::types::NodeDbLite;
11+
12+
impl<S: StorageEngine> NodeDbLite<S> {
13+
/// Start a background task that calls the global `flush()` every
14+
/// `interval_ms` milliseconds, bounding the data-loss window uniformly
15+
/// across all engines (KV buffer, vector id-map, CRDT deltas, CSR graph,
16+
/// spatial, FTS).
17+
///
18+
/// # Durability contract
19+
///
20+
/// `await`-ing a write operation (e.g. `kv_put`, `vector_insert`) returning
21+
/// `Ok` does NOT guarantee on-disk durability. Durability is bounded by
22+
/// `interval_ms`. For guaranteed durability, call `flush()` explicitly after
23+
/// writes.
24+
///
25+
/// # Usage
26+
///
27+
/// Call this once after wrapping the database in `Arc`:
28+
///
29+
/// ```ignore
30+
/// let db = Arc::new(NodeDbLite::open(storage, peer_id).await?);
31+
/// db.start_auto_flush(1_000); // flush every second
32+
/// ```
33+
///
34+
/// Direct library users (not using the FFI or WASM wrappers) must call
35+
/// this themselves — the embedded `open*` constructors return `Self`, not
36+
/// `Arc<Self>`, so the task cannot be spawned internally.
37+
///
38+
/// # Task lifecycle
39+
///
40+
/// The spawned task holds a `Weak` reference to the database. When the
41+
/// `Arc<NodeDbLite>` is dropped, the `Weak` upgrade fails and the task
42+
/// exits cleanly — no task leak.
43+
///
44+
/// # Disabling
45+
///
46+
/// Pass `interval_ms = 0` to skip spawning entirely (auto-flush disabled).
47+
pub fn start_auto_flush(self: &Arc<Self>, interval_ms: u64) {
48+
if interval_ms == 0 {
49+
return;
50+
}
51+
52+
let weak: Weak<Self> = Arc::downgrade(self);
53+
let period = Duration::from_millis(interval_ms);
54+
55+
crate::runtime::spawn(async move {
56+
let mut ticker = crate::runtime::interval(period);
57+
// Consume the first tick so the initial period elapses before the
58+
// first flush (matches Tokio's immediate-first-tick semantics on
59+
// native; on WASM the first tick already waits one period).
60+
ticker.tick().await;
61+
62+
loop {
63+
ticker.tick().await;
64+
65+
let db = match weak.upgrade() {
66+
Some(db) => db,
67+
None => break,
68+
};
69+
70+
if let Err(e) = db.flush().await {
71+
tracing::warn!(error = %e, "auto-flush failed");
72+
}
73+
74+
// Drop the strong Arc before the next tick so the loop does
75+
// not keep the database alive between ticks.
76+
drop(db);
77+
}
78+
});
79+
}
80+
}

nodedb-lite/src/nodedb/core/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// SPDX-License-Identifier: Apache-2.0
2+
mod auto_flush;
23
mod flush;
34
mod open;
45
mod ops;

0 commit comments

Comments
 (0)