armdb 0.2.0

sharded bitcask key-value storage optimized for NVMe
Documentation
#![cfg(feature = "var-collections")]

use armdb::{Config, VarMap, VarTree};

fn config_with_cache() -> Config {
    let mut config = Config::default();
    config.cache.max_size = 64;
    config
}

#[test]
fn var_tree_warmup_does_not_cache_active_file_block() {
    let dir = tempfile::tempdir().unwrap();
    let tree: VarTree<[u8; 8]> = VarTree::open(&dir, config_with_cache()).unwrap();

    let key = [1u8; 8];
    let value = b"hello_active";
    tree.put(&key, value).unwrap();

    tree.warmup().unwrap();

    let got = tree.get(&key).unwrap();
    assert_eq!(
        &*got, value,
        "get() after warmup() must return write_buf data, not stale cache"
    );
}

#[test]
fn var_map_warmup_does_not_cache_active_file_block() {
    let dir = tempfile::tempdir().unwrap();
    let map: VarMap<[u8; 8]> = VarMap::open(&dir, config_with_cache()).unwrap();

    let key = [1u8; 8];
    let value = b"hello_active";
    map.put(&key, value).unwrap();

    map.warmup().unwrap();

    let got = map.get(&key).unwrap();
    assert_eq!(
        &*got, value,
        "get() after warmup() must return write_buf data, not stale cache"
    );
}

#[test]
fn var_tree_warmup_does_not_cache_partial_immutable_tail() {
    let dir = tempfile::tempdir().unwrap();
    let mut config = config_with_cache();
    config.max_file_size = 4096 * 2;
    config.write_buffer_size = 4096;
    let tree: VarTree<[u8; 8]> = VarTree::open(&dir, config).unwrap();

    // Write enough entries to trigger at least one rotation,
    // producing an immutable file whose tail block is likely partial.
    let value = vec![0xBB; 200];
    for i in 0u64..100 {
        tree.put(&i.to_le_bytes(), &value).unwrap();
    }
    tree.flush_buffers().unwrap();

    // Re-open to get a fresh cache, then warmup
    drop(tree);
    let mut config2 = config_with_cache();
    config2.max_file_size = 4096 * 2;
    config2.write_buffer_size = 4096;
    let tree2: VarTree<[u8; 8]> = VarTree::open(&dir, config2).unwrap();
    tree2.warmup().unwrap();

    // Verify all keys are readable (warmup didn't corrupt the read path)
    for i in 0u64..100 {
        let got = tree2.get(&i.to_le_bytes());
        assert!(got.is_some(), "key {i} must be readable after warmup");
        assert_eq!(&*got.unwrap(), &value[..]);
    }
}

#[test]
fn var_map_warmup_does_not_cache_partial_immutable_tail() {
    let dir = tempfile::tempdir().unwrap();
    let mut config = config_with_cache();
    config.max_file_size = 4096 * 2;
    config.write_buffer_size = 4096;
    let map: VarMap<[u8; 8]> = VarMap::open(&dir, config).unwrap();

    let value = vec![0xBB; 200];
    for i in 0u64..100 {
        map.put(&i.to_le_bytes(), &value).unwrap();
    }
    map.flush_buffers().unwrap();

    drop(map);
    let mut config2 = config_with_cache();
    config2.max_file_size = 4096 * 2;
    config2.write_buffer_size = 4096;
    let map2: VarMap<[u8; 8]> = VarMap::open(&dir, config2).unwrap();
    map2.warmup().unwrap();

    for i in 0u64..100 {
        let got = map2.get(&i.to_le_bytes());
        assert!(got.is_some(), "key {i} must be readable after warmup");
        assert_eq!(&*got.unwrap(), &value[..]);
    }
}