hydracache 0.46.0

User-facing HydraCache runtime crate.
Documentation
use std::sync::Arc;

use hydracache::{
    ClusterCandidate, ClusterGeneration, HydraCache, InMemoryCluster, ReplicatedSlot,
    ReplicationConfig, ReplicationConfigError,
};

#[test]
fn replication_disabled_keeps_local_behavior() {
    let cache = HydraCache::local().build();

    assert!(!cache.replication_config().replicate_values);
    assert_eq!(cache.replication_config(), ReplicationConfig::default());
}

#[test]
fn oversized_value_rejected_and_counted() {
    let cache = HydraCache::local()
        .replicate_values(true)
        .max_replicated_entry_bytes(4)
        .build();

    if b"large-value".len() > cache.replication_config().max_replicated_entry_bytes {
        cache.record_cluster_replication_oversized_rejected();
    }

    assert_eq!(
        cache
            .cluster_grid_counters()
            .replication_oversized_rejected_total,
        1
    );
}

#[test]
fn replication_success_tracks_bytes() {
    let cache = HydraCache::local().build();

    cache.record_cluster_replication_success(128);
    cache.record_cluster_replication_success(64);

    let counters = cache.cluster_grid_counters();
    assert_eq!(counters.replication_success_total, 2);
    assert_eq!(counters.bytes_replicated_total, 192);
}

#[test]
fn replication_failure_increments_counter_and_reports_degraded() {
    let cache = HydraCache::local().build();

    cache.record_cluster_replication_failure();
    cache.set_cluster_under_replicated_keys(1);

    let counters = cache.cluster_grid_counters();
    assert_eq!(counters.replication_failure_total, 1);
    assert_eq!(counters.under_replicated_keys, 1);
}

#[test]
fn sync_async_counts_validated() {
    assert_eq!(
        ReplicationConfig {
            replication_factor: 3,
            sync_backups: 1,
            async_backups: 2,
            ..ReplicationConfig::default()
        }
        .validate(),
        Err(ReplicationConfigError::BackupCountExceedsReplicationFactor)
    );
}

#[tokio::test]
async fn member_start_rejects_missing_replication_byte_cap() {
    let cluster = Arc::new(InMemoryCluster::new("replication"));
    cluster
        .join_member(ClusterCandidate::member("already"))
        .expect("seed member");

    let error = HydraCache::member()
        .shared_cluster(cluster)
        .node_id("member-a")
        .generation(ClusterGeneration::new(1))
        .replicate_values(true)
        .replication_factor(2)
        .sync_backups(1)
        .start()
        .await
        .expect_err("missing cap should fail before startup");

    assert!(error.to_string().contains("max_replicated_entry_bytes"));
}

#[test]
fn sync_backup_acked_before_client_response() {
    let config = ReplicationConfig {
        replication_factor: 3,
        sync_backups: 1,
        async_backups: 1,
        replicate_values: true,
        max_replicated_entry_bytes: 1024,
        ..ReplicationConfig::default()
    };

    assert!(config.validate().is_ok());
    assert_eq!(config.sync_backups, 1);
}

#[test]
fn async_backup_does_not_block_write() {
    let config = ReplicationConfig {
        replication_factor: 2,
        sync_backups: 0,
        async_backups: 1,
        replicate_values: true,
        max_replicated_entry_bytes: 1024,
        ..ReplicationConfig::default()
    };

    assert!(config.validate().is_ok());
    assert_eq!(config.sync_backups, 0);
    assert_eq!(config.async_backups, 1);
}

#[test]
fn stale_generation_replication_is_rejected() {
    let live = ReplicatedSlot::Value {
        value: b"fresh".to_vec(),
        version: 10,
    };
    let stale = ReplicatedSlot::Value {
        value: b"stale".to_vec(),
        version: 9,
    };

    assert_eq!(live.clone().merge(stale), live);
}