feltdb 0.2.0

Embedded durable state store: an append-only log with atomic multi-record transactions, version-conditional writes, and idempotent retries
Documentation
use std::{fs, path::PathBuf};

use serde_json::json;

use crate::{FeltDb, JsonCasResult};

fn database(name: &str) -> (FeltDb, PathBuf) {
    let path = std::env::temp_dir().join(format!(
        "feltdb-managed-cas-{name}-{}-{}.log",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos()
    ));
    (FeltDb::open(&path).unwrap(), path)
}

#[test]
fn version_and_epoch_are_one_atomic_predicate() {
    let (db, path) = database("predicate");
    let key = "_cells:cell-1";
    db.insert(
        key,
        json!({
            "id": "cell-1",
            "version": 42,
            "value": "before",
            "authority": { "owner": "A", "epoch": 7, "lifecycle": "active" },
            "__version": 1
        }),
    )
    .unwrap();

    let stale_epoch = db
        .compare_and_set_json(
            key,
            1,
            Some(6),
            None,
            false,
            json!({
                "id": "cell-1",
                "version": 42,
                "value": "stale",
                "authority": { "owner": "stale", "epoch": 7, "lifecycle": "active" }
            }),
        )
        .unwrap();
    assert_eq!(
        stale_epoch,
        JsonCasResult::AuthorityConflict {
            current_version: 1,
            current_epoch: 7,
        }
    );
    assert_eq!(db.get_value(key).unwrap().unwrap()["value"], "before");

    let updated = db
        .compare_and_set_json(
            key,
            1,
            Some(7),
            None,
            false,
            json!({
                "id": "cell-1",
                "version": 42,
                "value": "after",
                "authority": { "owner": "B", "epoch": 8, "lifecycle": "active" }
            }),
        )
        .unwrap();
    assert!(matches!(
        updated,
        JsonCasResult::Updated {
            current_version: 2,
            current_epoch: 8,
            ..
        }
    ));

    let stale_version = db
        .compare_and_set_json(key, 1, Some(8), None, false, json!({ "value": "lost" }))
        .unwrap();
    assert_eq!(
        stale_version,
        JsonCasResult::VersionConflict {
            current_version: 2,
            current_epoch: 8,
        }
    );
    let final_value = db.get_value(key).unwrap().unwrap();
    assert_eq!(final_value["value"], "after");
    assert_eq!(final_value["__version"], 2);
    assert_eq!(final_value["authority"]["epoch"], 8);

    drop(db);
    let _ = fs::remove_file(path);
}

#[test]
fn lease_is_checked_with_the_server_clock_inside_cas() {
    let (db, path) = database("lease");
    let key = "_cells:leased";
    let future = crate::now_ms() as u64 + 60_000;
    db.insert(
        key,
        json!({
            "value": "before",
            "authority": { "epoch": 3 },
            "lease": { "leaseId": "lease-good", "expiresAt": future },
            "__version": 1
        }),
    )
    .unwrap();

    for expected_lease_id in [None, Some("lease-wrong")] {
        assert_eq!(
            db.compare_and_set_json(
                key,
                1,
                Some(3),
                expected_lease_id,
                false,
                json!({ "value": "rejected", "authority": { "epoch": 3 } }),
            )
            .unwrap(),
            JsonCasResult::LeaseConflict {
                current_version: 1,
                current_epoch: 3,
            }
        );
        assert_eq!(db.get_value(key).unwrap().unwrap()["value"], "before");
    }

    assert!(matches!(
        db.compare_and_set_json(
            key,
            1,
            Some(3),
            Some("lease-good"),
            false,
            json!({ "value": "accepted", "authority": { "epoch": 3 }, "lease": { "leaseId": "lease-good", "expiresAt": future } }),
        )
        .unwrap(),
        JsonCasResult::Updated { .. }
    ));

    let expired_key = "_cells:expired";
    db.insert(
        expired_key,
        json!({
            "value": "before",
            "authority": { "epoch": 3 },
            "lease": { "leaseId": "lease-expired", "expiresAt": 1 },
            "__version": 1
        }),
    )
    .unwrap();
    assert_eq!(
        db.compare_and_set_json(
            expired_key,
            1,
            Some(3),
            Some("lease-expired"),
            false,
            json!({ "value": "rejected", "authority": { "epoch": 3 } }),
        )
        .unwrap(),
        JsonCasResult::LeaseConflict {
            current_version: 1,
            current_epoch: 3,
        }
    );
    assert_eq!(
        db.get_value(expired_key).unwrap().unwrap()["value"],
        "before"
    );

    drop(db);
    let _ = fs::remove_file(path);
}

#[test]
fn divergent_replica_blocks_promotion_inside_cas() {
    let (db, path) = database("divergence");
    let cell_key = "_cells:replica";
    let replica_key = "_replicas:replica";
    db.insert(
        cell_key,
        json!({
            "version": 42,
            "value": "A",
            "authority": { "owner": "A", "epoch": 7 },
            "lease": null,
            "__version": 42
        }),
    )
    .unwrap();
    db.insert(
        replica_key,
        json!({ "version": 42, "value": "B", "divergent": true }),
    )
    .unwrap();
    let sequence_before = db.sequence().unwrap();
    let value_before = db.get_value(cell_key).unwrap().unwrap();

    assert_eq!(
        db.compare_and_set_json(
            cell_key,
            42,
            Some(7),
            None,
            true,
            json!({
                "version": 43,
                "value": "A",
                "authority": { "owner": "B", "epoch": 8 },
                "lease": null
            }),
        )
        .unwrap(),
        JsonCasResult::ReplicationConflict {
            current_version: 42,
            current_epoch: 7,
        }
    );
    assert_eq!(db.sequence().unwrap(), sequence_before);
    assert_eq!(db.get_value(cell_key).unwrap().unwrap(), value_before);

    drop(db);
    let _ = fs::remove_file(path);
}