nodedb_lite/nodedb/core/auto_compact.rs
1// SPDX-License-Identifier: Apache-2.0
2
3//! `NodeDbLite::start_auto_compact` — background storage-compaction task.
4
5use std::sync::{Arc, Weak};
6use std::time::Duration;
7
8use crate::storage::engine::StorageEngine;
9
10use super::types::NodeDbLite;
11
12impl<S: StorageEngine> NodeDbLite<S> {
13 /// Start a background task that calls the global `compact()` every
14 /// `interval_ms` milliseconds, reclaiming dead pages and truncating the
15 /// backing file so on-disk growth stays bounded.
16 ///
17 /// # When to use
18 ///
19 /// Compaction is opt-in and unnecessary for most workloads. Enable it when
20 /// writing one commit per entry, where the pagedb deferred-free list would
21 /// otherwise accumulate and the file would grow without bound. For engines
22 /// with nothing to compact (in-memory / test stores) the call is a cheap
23 /// no-op every tick.
24 ///
25 /// # Cost vs. auto-flush
26 ///
27 /// This is heavier than `start_auto_flush`: compaction repacks the B+ trees
28 /// and truncates the file, and it no-ops while a reader pins the reclaimable
29 /// page range. Use a much larger interval than the flush interval — minutes,
30 /// not seconds.
31 ///
32 /// # Usage
33 ///
34 /// The `open*` constructors already start this task from
35 /// [`LiteConfig::auto_compact_ms`](crate::config::LiteConfig::auto_compact_ms),
36 /// which defaults to 0 (disabled). Set that field to enable compaction:
37 ///
38 /// ```ignore
39 /// let config = LiteConfig { auto_compact_ms: 300_000, ..LiteConfig::default() };
40 /// let db = NodeDbLite::open_with_config(storage, config).await?;
41 /// ```
42 ///
43 /// Calling this directly spawns an additional task rather than replacing a
44 /// running one.
45 ///
46 /// # Task lifecycle
47 ///
48 /// The task is registered with the database's task registry, so
49 /// [`NodeDbLite::shutdown`](crate::NodeDbLite::shutdown) stops it before
50 /// the host drops its async runtime. It also holds a `Weak` reference, so
51 /// a database dropped without a shutdown still ends the loop at its next
52 /// tick rather than keeping the database alive.
53 ///
54 /// # Disabling
55 ///
56 /// Pass `interval_ms = 0` to skip spawning entirely (auto-compaction
57 /// disabled — the default). Compaction can still be triggered manually via
58 /// `compact()`.
59 pub fn start_auto_compact(self: &Arc<Self>, interval_ms: u64) {
60 if interval_ms == 0 {
61 return;
62 }
63
64 let weak: Weak<Self> = Arc::downgrade(self);
65 let period = Duration::from_millis(interval_ms);
66
67 let (stop_tx, mut stop) = crate::tasks::TaskRegistry::signal();
68 let handle = crate::runtime::spawn(async move {
69 let mut ticker = crate::runtime::interval(period);
70 // A compaction that outlasts its own period must not be followed by
71 // the ticks it missed, back to back — repacking is the heavier of
72 // the two periodic tasks, so a burst of it is worse.
73 ticker.delay_missed_ticks();
74 // Consume the first tick so the initial period elapses before the
75 // first compaction (matches Tokio's immediate-first-tick semantics
76 // on native; on WASM the first tick already waits one period).
77 ticker.tick().await;
78
79 loop {
80 if !stop.tick_or_stop(&mut ticker).await {
81 break;
82 }
83
84 let db = match weak.upgrade() {
85 Some(db) => db,
86 None => break,
87 };
88
89 match db.compact().await {
90 Ok(outcome) => {
91 if outcome.reclaimed_pages > 0 || outcome.file_bytes_freed > 0 {
92 tracing::debug!(
93 reclaimed_pages = outcome.reclaimed_pages,
94 segments_repacked = outcome.segments_repacked,
95 file_bytes_freed = outcome.file_bytes_freed,
96 "auto-compact reclaimed space"
97 );
98 }
99 }
100 Err(e) => {
101 tracing::warn!(error = %e, "auto-compact failed");
102 }
103 }
104
105 // Drop the strong Arc before the next tick so the loop does not
106 // keep the database alive between ticks.
107 drop(db);
108 }
109 });
110 self.tasks
111 .track(crate::tasks::TaskKind::AutoCompact, stop_tx, handle);
112 }
113}