nodedb-lite 0.1.0

Embedded edge client for NodeDB — offline vector search, graph traversal, and CRDT sync
Documentation
// 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);
    }
}