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