armdb 0.1.12

sharded bitcask key-value storage optimized for NVMe
Documentation
use std::time::Duration;

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

/// Configuration for the fixed-slot storage engine.
///
/// # Immutable vs tunable parameters
///
/// **Immutable** (fixed at creation):
/// - `shard_count` — determines key-to-shard routing
/// - `shard_prefix_bits` — determines key-to-shard grouping
///
/// **Tunable** (safe to change between restarts):
/// - `grow_step`, `sync_interval`, `sync_batch_size`, `enable_fsync`
#[derive(Debug, Clone)]
pub struct FixedConfig {
    /// Number of shards. **Immutable** — changing requires recreating the
    /// database. Default: `available_parallelism()`.
    pub shard_count: usize,
    /// Number of prefix bits of the key to use for shard routing. **Immutable**.
    /// 0 = hash full key (default). Non-zero = hash first N bits for locality.
    pub shard_prefix_bits: usize,
    /// Number of slots to pre-allocate when the file grows. Tunable.
    /// Default: 1_000_000.
    pub grow_step: u32,
    /// Interval between background sync flushes. Tunable. Default: 50 ms.
    pub sync_interval: Duration,
    /// Maximum number of dirty slots to sync per batch. Tunable.
    /// Default: 1000.
    pub sync_batch_size: u32,
    /// Whether to fsync after writes. Tunable. Default: false.
    pub enable_fsync: bool,
}

impl Default for FixedConfig {
    fn default() -> Self {
        let shard_count = std::thread::available_parallelism()
            .map(|p| p.get())
            .unwrap_or(4);
        Self {
            shard_count,
            shard_prefix_bits: 0,
            grow_step: 1_000_000,
            sync_interval: Duration::from_millis(50),
            sync_batch_size: 1000,
            enable_fsync: false,
        }
    }
}

impl FixedConfig {
    /// 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: 3,
            grow_step: 1_000,
            sync_interval: Duration::from_millis(10),
            sync_batch_size: 100,
            ..Default::default()
        }
    }

    pub fn validate(&self) -> DbResult<()> {
        if self.shard_count == 0 || self.shard_count > 255 {
            return Err(DbError::Config("shard_count must be 1..=255"));
        }
        if self.grow_step == 0 {
            return Err(DbError::Config("grow_step must be > 0"));
        }
        if self.sync_batch_size == 0 {
            return Err(DbError::Config("sync_batch_size must be > 0"));
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_default_config_is_valid() {
        FixedConfig::default().validate().unwrap();
    }

    #[test]
    fn test_test_config_is_valid() {
        FixedConfig::test().validate().unwrap();
    }

    #[test]
    fn test_invalid_shard_count() {
        let mut c = FixedConfig::test();
        c.shard_count = 0;
        assert!(c.validate().is_err());
        c.shard_count = 256;
        assert!(c.validate().is_err());
    }

    #[test]
    fn test_invalid_grow_step() {
        let mut c = FixedConfig::test();
        c.grow_step = 0;
        assert!(c.validate().is_err());
    }

    #[test]
    fn test_invalid_sync_batch_size() {
        let mut c = FixedConfig::test();
        c.sync_batch_size = 0;
        assert!(c.validate().is_err());
    }
}