hydracache 0.59.0

User-facing HydraCache runtime crate.
Documentation
use hydracache::{
    partition_for_key, CasResult, ClusterEpoch, ConsistencyLevel, ReplicatedValueRecord,
    SingleKeyConditionalStore,
};

fn store() -> SingleKeyConditionalStore {
    SingleKeyConditionalStore::new(ClusterEpoch::new(7), 16)
}

#[test]
fn remove_if_value_writes_tombstone_at_new_version() {
    let mut store = store();
    assert_eq!(
        store
            .compare_and_set(
                "user:42",
                None,
                b"active".to_vec(),
                ConsistencyLevel::Quorum
            )
            .unwrap(),
        CasResult::Applied { new_version: 1 }
    );

    let removed = store
        .remove_if_value("user:42", b"active", ConsistencyLevel::Quorum)
        .unwrap();

    assert_eq!(removed, CasResult::Applied { new_version: 2 });
    let record = store.record("user:42").expect("tombstone record");
    assert!(record.is_tombstone());
    assert_eq!(record.version, 2);
    assert_eq!(store.current_value("user:42"), None);
    assert_eq!(store.metrics().cas_applied_total, 2);
}

#[test]
fn remove_if_value_mismatch_leaves_record_untouched() {
    let mut store = store();
    store
        .compare_and_set(
            "user:42",
            None,
            b"active".to_vec(),
            ConsistencyLevel::Quorum,
        )
        .unwrap();
    let before = store.record("user:42").cloned();

    let mismatch = store
        .remove_if_value("user:42", b"disabled", ConsistencyLevel::Quorum)
        .unwrap();

    assert_eq!(
        mismatch,
        CasResult::Mismatch {
            current: Some(b"active".to_vec())
        }
    );
    assert_eq!(store.record("user:42").cloned(), before);
    assert_eq!(store.metrics().cas_mismatch_total, 1);
}

#[test]
fn tombstone_from_remove_if_value_is_not_resurrected_by_stale_put() {
    let mut store = store();
    store
        .compare_and_set(
            "user:42",
            None,
            b"active".to_vec(),
            ConsistencyLevel::Quorum,
        )
        .unwrap();
    store
        .remove_if_value("user:42", b"active", ConsistencyLevel::Quorum)
        .unwrap();
    let tombstone = store.record("user:42").expect("tombstone").clone();
    let stale_value = ReplicatedValueRecord::value(
        partition_for_key("user:42", 16),
        tombstone.version,
        ClusterEpoch::new(7),
        b"stale".to_vec(),
    );

    let merged = tombstone.merge(stale_value);

    assert!(merged.is_tombstone());
    assert_eq!(merged.version, 2);
}

#[test]
fn removed_key_reads_as_absent_after_tombstone() {
    let mut store = store();
    store
        .compare_and_set(
            "user:42",
            None,
            b"active".to_vec(),
            ConsistencyLevel::Quorum,
        )
        .unwrap();

    store
        .remove_if_value("user:42", b"active", ConsistencyLevel::Quorum)
        .unwrap();

    assert_eq!(store.current_value("user:42"), None);
    assert_eq!(
        store
            .remove_if_value("user:42", b"active", ConsistencyLevel::Quorum)
            .unwrap(),
        CasResult::Mismatch { current: None }
    );
}

#[test]
fn replace_if_present_applies_only_when_live_value_exists() {
    let mut store = store();
    store
        .compare_and_set(
            "user:42",
            None,
            b"active".to_vec(),
            ConsistencyLevel::Quorum,
        )
        .unwrap();

    let replaced = store
        .replace_if_present("user:42", b"disabled".to_vec(), ConsistencyLevel::Quorum)
        .unwrap();

    assert_eq!(replaced, CasResult::Applied { new_version: 2 });
    assert_eq!(store.current_value("user:42"), Some(b"disabled".to_vec()));
}

#[test]
fn replace_if_present_on_absent_is_mismatch_not_insert() {
    let mut store = store();

    let result = store
        .replace_if_present("missing", b"created".to_vec(), ConsistencyLevel::Quorum)
        .unwrap();

    assert_eq!(result, CasResult::Mismatch { current: None });
    assert_eq!(store.current_value("missing"), None);
    assert_eq!(store.metrics().cas_mismatch_total, 1);
}

#[test]
fn replace_if_present_after_tombstone_is_mismatch_not_resurrection() {
    let mut store = store();
    store
        .compare_and_set(
            "user:42",
            None,
            b"active".to_vec(),
            ConsistencyLevel::Quorum,
        )
        .unwrap();
    store
        .remove_if_value("user:42", b"active", ConsistencyLevel::Quorum)
        .unwrap();

    let result = store
        .replace_if_present("user:42", b"resurrected".to_vec(), ConsistencyLevel::Quorum)
        .unwrap();

    assert_eq!(result, CasResult::Mismatch { current: None });
    assert_eq!(store.current_value("user:42"), None);
    assert!(store.record("user:42").expect("tombstone").is_tombstone());
}