armdb 0.14.2

sharded bitcask key-value storage optimized for NVMe
Documentation
/// Common trait for armdb collections managed by [`Db`](super::tree_db::Db).
///
/// Implemented for `TypedTree`, `TypedMap`, `ZeroTree`, `ZeroMap`
/// where the value type implements [`CollectionMeta`](crate::CollectionMeta).
pub trait Collection: Send + Sync {
    /// Collection name (from `CollectionMeta::NAME`).
    fn name(&self) -> &str;
    /// Number of entries in the collection.
    fn len(&self) -> usize;
    fn is_empty(&self) -> bool {
        self.len() == 0
    }
    /// Run a compaction pass across all shards.
    fn compact(&self) -> crate::DbResult<usize>;
    /// Flush in-memory write buffers and pending hint data so an immediate
    /// process exit will not lose committed writes. No default — every impl
    /// must provide this to avoid silent durability gaps.
    fn flush(&self) -> crate::DbResult<()>;
    /// The tick's barrier, per backend policy. Backend-specific:
    /// - Bitcask: flush write buffers and fsync dirty shards only (same as
    ///   [`durable_flush`](Self::durable_flush)).
    /// - FixedStore: fdatasync shards with unsynced data only.
    /// - `LsmTree`: `persist` with the collection's configured `PersistPolicy`.
    ///
    /// The background flush worker calls this. Write-triggered `Config::sync_interval`
    /// checks run only as writes return, so this tick is the durability deadline
    /// while a collection is silent. No default — every impl must provide it.
    fn periodic_flush(&self) -> crate::DbResult<()>;
    /// The strongest barrier on this collection: Bitcask drains dirty shard
    /// buffers under each shard lock, then fsyncs their captured active files
    /// outside it without rebuilding hints; FixedStore — `fdatasync` unsynced
    /// shards; LSM — `PersistMode::SyncAll`. A clean Bitcask shard is skipped,
    /// and a failed drain or fsync is returned to the caller.
    ///
    /// On Bitcask and FixedStore this coincides with
    /// [`periodic_flush`](Self::periodic_flush). On LSM it does not: the tick
    /// honours `PersistPolicy`, this method always uses `SyncAll`. The
    /// transaction checkpoint uses it before retiring a log record. Each
    /// completed Bitcask fsync stage emits
    /// `armdb.flush.barriers{result="ok"|"failed"}`, and a shard that needs no
    /// barrier records `result="skipped"`; a pre-fsync drain failure stays in
    /// flush-pass accounting because no fsync stage completed.
    fn durable_flush(&self) -> crate::DbResult<()>;
    /// Full clean shutdown for this collection. Heavier than [`flush`](Self::flush):
    /// - Bitcask/Var: flush write buffers, write hint files, fsync data files.
    /// - FixedStore: fdatasync data, write the `fixed.versions` sidecar and set
    ///   the clean-shutdown flag so the NEXT open takes Clean recovery.
    ///
    /// # Contract
    ///
    /// The caller must stop all application writers first. Any write after this
    /// call — a local mutation *or* a FixedStore replication-apply from a leader —
    /// downgrades the affected shard back to Dirty recovery (safe, slow); it never
    /// corrupts. Background [`periodic_flush`](Self::periodic_flush) and
    /// [`compact`](Self::compact) are not writes and are harmless afterwards.
    /// [`Db::close`](super::db::Db::close) / [`Db::clean_shutdown`](super::db::Db::clean_shutdown)
    /// also gate TTL sweep (write source) before calling this; joining the sweeper
    /// thread remains [`Db::shutdown`](super::db::Db::shutdown).
    /// Idempotent; [`Db`](super::db::Db) drop after it keeps the clean state intact.
    ///
    /// # Comparison with other shutdown paths
    ///
    /// | Method | Hints | Fixed sidecar/flag | Final fsync | Txlog checkpoint | Stops workers | Error |
    /// |---|---|---|---|---|---|---|
    /// | periodic_flush | no | no | dirty Bitcask/Fixed; LSM policy | no | no | logged |
    /// | close | writes | no | dirty shards only | yes | no | returned |
    /// | clean_shutdown | writes | yes | yes | yes | no | returned |
    /// | shutdown(&mut self) | via clean_shutdown | yes | yes | yes | yes | returned |
    /// | Drop | via close | no | dirty shards only | yes | yes | logged only |
    ///
    /// Hinted Bitcask collections re-barrier once per shard during
    /// [`Db`](super::db::Db) drop
    /// after `clean_shutdown`; clean no-hints shards issue zero fsyncs.
    fn clean_shutdown(&self) -> crate::DbResult<()>;
    /// Sweep expired rows up to `budget` candidates. `now_ms` is wall-clock millis.
    fn sweep_expired(
        &self,
        _now_ms: u64,
        _budget: usize,
    ) -> crate::DbResult<crate::ttl::SweepOutcome> {
        Ok(crate::ttl::SweepOutcome::default())
    }
    /// Enable or disable background TTL sweep for this collection.
    fn set_ttl_sweep(&self, _enabled: bool) -> crate::DbResult<()> {
        Ok(())
    }
    /// Rebuild the in-memory TTL index from live rows.
    fn rebuild_ttl_index(&self) -> crate::DbResult<()> {
        Err(crate::DbError::Client("collection has no TTL policy"))
    }
    /// Consume the `counters_dirty` marker (T9) and report whether it was set
    /// — i.e. whether **this session's open of this collection as a counted
    /// source** is what persisted the flag.
    ///
    /// `Db::close` / `Db::clean_shutdown` clear the durable flag only for a
    /// collection that answers `true`: the flag also carries a second,
    /// marker-less meaning set by `run_migration` ("my content moved under
    /// some counter"), which only a *source's* open consumes. Clearing that
    /// one at close would drop the sole evidence that a recount is owed;
    /// leaving it costs at most one redundant recount per open. (Once a
    /// source's open-time recount *has* consumed it, `Db` records that
    /// separately and the close retires the target's flag too.)
    ///
    /// Default `false` — a collection with no bound counter marker never
    /// marked anything.
    fn reset_counter_dirty_marker(&self) -> bool {
        false
    }
    /// Open-time / on-demand counter recount (T9 Task 8) — see
    /// `ManagedOpen::reconcile_counters` for the two-pass walk this reaches.
    /// [`Db::recount_counters`](super::db::Db::recount_counters) uses this
    /// by-name entry point since it only has a collection name, not a
    /// concrete type. Default no-op (`Ok(0)`) — only a collection with a
    /// bound counter carrier needs it.
    fn reconcile_counters(&self) -> crate::DbResult<u64> {
        Ok(0)
    }
    /// The collections this one declares as counter targets (T9 Task 8) —
    /// empty when this collection is not itself a counted source (it may be
    /// a plain counter *target*, or unrelated to the counter system).
    /// [`Db::recount_counters`](super::db::Db::recount_counters) uses this to
    /// refuse recounting — and, critically, clearing the `counters_dirty`
    /// flag of — a collection that isn't a source: for a plain target, that
    /// flag is the only evidence a source's own later open reads to know a
    /// recount is owed (see `Db::open_managed`), so clearing it here would
    /// silently destroy recovery state instead of recounting anything.
    /// Default empty; overridden by the six typed families.
    fn counter_target_names(&self) -> Vec<&'static str> {
        Vec::new()
    }
    /// Shards holding writes no durability barrier covers yet — this
    /// collection's share of the power-loss window between two flush ticks.
    ///
    /// `None` means the backend has no shard-level dirty state to report
    /// (`LsmTree`, whose durability is fjall's own `PersistPolicy`), so a
    /// caller summing these values never reads an unknown as a clean zero.
    /// Sampled shard by shard without a consistent cut: a concurrent writer or
    /// barrier can move the answer mid-count.
    fn dirty_shards(&self) -> Option<usize> {
        None
    }
    /// Terminally close active realtime receivers after all database writers stop.
    fn close_watches(&self) {}
}