armdb 0.7.0

sharded bitcask key-value storage optimized for NVMe
Documentation
//! Directory-lock regressions at the collection level.
//!
//! The replication test is the load-bearing one: Bitcask hands `Arc<Shards>` to
//! replication threads that outlive the collection, so the lease must follow the
//! shards rather than the `Engine` value.
#![cfg(feature = "armour")]

use armdb::{Config, ConstTree, DbError, FixedConfig, FixedTree};
use tempfile::tempdir;

type Tree = ConstTree<[u8; 8], 8>;

#[test]
fn bitcask_collection_open_twice_errors() {
    let dir = tempdir().unwrap();
    let _first = Tree::open(dir.path(), Config::test()).expect("first open");
    let res = Tree::open(dir.path(), Config::test());
    let err = match res {
        Err(e) => e,
        Ok(_) => panic!("second open of a live collection must fail"),
    };
    assert!(matches!(err, DbError::Locked { .. }), "got {err:?}");
}

#[test]
fn bitcask_collection_lock_released_on_drop() {
    let dir = tempdir().unwrap();
    let first = Tree::open(dir.path(), Config::test()).expect("first open");
    drop(first);
    Tree::open(dir.path(), Config::test()).expect("reopen after drop must succeed");
}

// R1: the whole point of putting the lease inside `Arc<Shards>`. A replication
// server keeps the shards alive after the collection is dropped and keeps
// touching the files, so the lock must NOT be released until it is gone.
#[cfg(feature = "replication")]
#[test]
fn bitcask_replication_server_retains_the_lock_after_the_collection_drops() {
    use armdb::ShutdownSignal;

    let dir = tempdir().unwrap();
    let tree = Tree::open(dir.path(), Config::test()).expect("open");
    let signal = ShutdownSignal::new();
    let server = tree
        .start_replication_server("127.0.0.1:0".parse().unwrap(), signal.clone())
        .expect("start replication server");

    drop(tree); // collection gone; the server threads still own the shards

    let res = Tree::open(dir.path(), Config::test());
    let err = match res {
        Err(e) => e,
        Ok(_) => panic!("lock must hold while replication still owns the shards"),
    };
    assert!(matches!(err, DbError::Locked { .. }), "got {err:?}");

    signal.shutdown();
    drop(server);

    Tree::open(dir.path(), Config::test()).expect("lock must release once replication is gone");
}

// Fixed collection lock tests — FixedEngine lease is a plain field (no Arc restructuring needed).

type FxTree = FixedTree<[u8; 8], 8>;

#[test]
fn fixed_collection_open_twice_errors() {
    let dir = tempdir().unwrap();
    let _first = FxTree::open(dir.path(), FixedConfig::test()).expect("first open");
    let res = FxTree::open(dir.path(), FixedConfig::test());
    let err = match res {
        Err(e) => e,
        Ok(_) => panic!("second open of a live fixed collection must fail"),
    };
    assert!(matches!(err, DbError::Locked { .. }), "got {err:?}");
}

#[test]
fn fixed_collection_lock_released_on_drop() {
    let dir = tempdir().unwrap();
    let first = FxTree::open(dir.path(), FixedConfig::test()).expect("first open");
    drop(first);
    FxTree::open(dir.path(), FixedConfig::test()).expect("reopen after drop must succeed");
}

// No companion test for `start_replication_client` — deliberately.
//
// The client takes its shards through the very same `engine.shards().clone()`
// expression the server test covers, and `shards()` cannot return anything but
// `Arc<Shards>`, which owns the lease. There is no way to write a client that
// holds shards without the lease, so such a test would be asserting on rustc,
// not on this crate.
//
// (Do not restore this as an *ergonomics* argument — "a registry needs
// `as_replication_target`, which clones the whole `Arc<ConstTree>`, so the test
// could not fail". That is true of an integration test today but rots the moment
// someone adds a `Registry::empty()` or re-exports `ShardInner`: a crate-internal
// unit test can already build a stub target over `Shards::new(Vec::new(), lock)`.
// The type-level argument above is what actually holds.)