armdb 0.1.12

sharded bitcask key-value storage optimized for NVMe
Documentation
use serde::Deserialize;

use crate::error::{DbError, DbResult};

/// Database configuration.
///
/// # Immutable vs tunable parameters
///
/// **Immutable** (fixed at creation, changing breaks the database):
/// - `shard_count` — stored in `meta.db`, determines key→shard routing
/// - `shard_prefix_bits` — determines key→shard grouping; changing breaks
///   `atomic` guarantees and dead_bytes tracking across shards
/// - `encryption_key` — presence/absence stored in `meta.db`
///
/// **Tunable** (safe to change between restarts):
/// - `max_file_size`, `compaction_threshold`, `enable_fsync`, `write_buffer_size`
/// - `cache` — in-memory only, no on-disk state
/// - `reversed` — only affects in-memory index ordering
/// - `hints` — controls hint file generation/use during recovery
#[derive(Debug, Clone, Deserialize)]
pub struct Config {
    /// Number of shards. **Immutable** — stored in `meta.db`, changing requires
    /// recreating the database. Default: `available_parallelism()`.
    pub shard_count: usize,
    /// Maximum size of a single data file before rotation. Tunable.
    /// Default: 256 MB.
    pub max_file_size: u64,
    /// Ratio of dead_bytes / total_bytes required to trigger compaction. Tunable.
    /// Default: 0.3.
    pub compaction_threshold: f64,
    /// Whether to fsync after writes. Tunable. Default: false.
    pub enable_fsync: bool,
    /// Write buffer size per shard in bytes. Tunable. Writes are buffered in
    /// memory and flushed to disk when the buffer is full or on explicit
    /// flush/close. Default: 1 MB.
    pub write_buffer_size: usize,
    /// Value cache configuration (for VarTree). Tunable — in-memory only.
    #[cfg(feature = "var-collections")]
    pub cache: CacheConfig,
    /// Number of prefix bits of the key to use for shard routing. **Immutable** —
    /// changing breaks `atomic` guarantees (keys regroup across shards) and
    /// dead_bytes tracking (old entries attributed to wrong shard).
    /// 0 = hash full key (default). Non-zero = hash first N bits for locality.
    pub shard_prefix_bits: usize,
    /// Reverse key ordering in the index. Tunable — only affects in-memory
    /// index ordering. When `true`, forward iteration yields keys in
    /// descending order (newest first for monotonic IDs). Default: true.
    pub reversed: bool,
    /// Generate and use hint files for faster recovery. Tunable.
    /// When `true`, hint files are written on graceful shutdown and file
    /// rotation, and used during recovery to skip full data scans.
    /// Recommended `true` for VarTree (avoids decoding values on recovery),
    /// `false` for ConstTree/TypedTree (values are small, full scan is fast).
    /// Default: false.
    pub hints: bool,
    /// 32-byte AES-256 encryption key. **Immutable** — presence/absence is
    /// stored in `meta.db`. `None` = no encryption.
    /// Use `PageCipher::key_from_env("ARMDB_KEY")` to read from environment.
    #[cfg(feature = "encryption")]
    pub encryption_key: Option<[u8; 32]>,
}

/// Configuration for the value cache used by `VarTree`.
#[cfg(feature = "var-collections")]
#[derive(Debug, Clone, Deserialize)]
pub struct CacheConfig {
    /// Maximum cache size in bytes. 0 = disabled. Default: 0.
    pub max_size: u64,
    /// Estimated number of items (for hash table pre-allocation). Default: 100_000.
    pub estimated_items: usize,
}

impl Config {
    /// Config with small shard count for tests. Keeps fd usage low
    /// so `cargo test` doesn't hit "Too many open files" on default ulimit.
    pub fn test() -> Self {
        Self {
            shard_count: 2,
            hints: true,
            ..Self::default()
        }
    }

    pub fn validate(&self) -> DbResult<()> {
        if self.shard_count == 0 || self.shard_count > 255 {
            return Err(DbError::Config("shard_count must be between 1 and 255"));
        }
        if self.max_file_size < 4096 {
            return Err(DbError::Config("max_file_size must be at least 4096"));
        }
        if self.write_buffer_size < 4096 {
            return Err(DbError::Config("write_buffer_size must be at least 4096"));
        }
        if self.max_file_size > u32::MAX as u64 {
            return Err(DbError::Config(
                "max_file_size must not exceed u32::MAX (4 GiB)",
            ));
        }
        Ok(())
    }
}

impl Default for Config {
    fn default() -> Self {
        #[cfg(debug_assertions)]
        let parallelism = (std::thread::available_parallelism()
            .map(|p| p.get() / 2)
            .unwrap_or(2))
        .min(4);
        #[cfg(not(debug_assertions))]
        let parallelism = std::thread::available_parallelism()
            .map(|p| p.get() / 2)
            .unwrap_or(4)
            .min(8);
        Self {
            shard_count: parallelism,
            max_file_size: 256 * 1024 * 1024,
            compaction_threshold: 0.3,
            enable_fsync: false,
            write_buffer_size: 1024 * 1024, // 1 MB
            #[cfg(feature = "var-collections")]
            cache: CacheConfig::default(),
            shard_prefix_bits: 0,
            reversed: true,
            hints: false,
            #[cfg(feature = "encryption")]
            encryption_key: None,
        }
    }
}

#[cfg(feature = "var-collections")]
impl Default for CacheConfig {
    fn default() -> Self {
        Self {
            max_size: 0,
            estimated_items: 100_000,
        }
    }
}