hydracache 0.54.0

User-facing HydraCache runtime crate.
Documentation
use std::fmt;
use std::time::Duration;

use crate::{
    CacheOptions, ConsistencyMode, ConsistencyOutcome, ConsistencyToken, DegradeReason, HydraCache,
};

#[derive(Debug)]
struct LoadError;

impl fmt::Display for LoadError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str("load failed")
    }
}

impl std::error::Error for LoadError {}

#[test]
fn default_mode_is_eventual() {
    assert_eq!(ConsistencyMode::default(), ConsistencyMode::Eventual);
}

#[test]
fn token_carries_generation_namespace_origin() {
    let token = ConsistencyToken::new(7, "tenant-a", "node-a");

    assert_eq!(token.generation(), 7);
    assert_eq!(token.namespace(), "tenant-a");
    assert_eq!(token.origin_node(), "node-a");
}

#[test]
fn degraded_outcome_shape() {
    let outcome = ConsistencyOutcome::Degraded {
        value: 42_u64,
        reason: DegradeReason::Timeout,
    };

    assert!(matches!(
        outcome,
        ConsistencyOutcome::Degraded {
            value: 42,
            reason: DegradeReason::Timeout
        }
    ));
}

#[tokio::test]
async fn local_generation_prevents_stale_overwrite() {
    let cache = HydraCache::local().build();
    cache
        .put(
            "user:1",
            "stale".to_owned(),
            CacheOptions::new().tag("user:1"),
        )
        .await
        .unwrap();

    let token = cache
        .invalidate_after_write("user:1")
        .namespace("profiles")
        .consistency(ConsistencyMode::LocalReadYourWrites)
        .await
        .unwrap();
    let outcome = cache
        .get_with_consistency(
            "user:1",
            &token,
            ConsistencyMode::LocalReadYourWrites,
            CacheOptions::new().tag("user:1"),
            || async { Ok::<_, LoadError>("fresh".to_owned()) },
        )
        .await
        .unwrap();

    assert_eq!(outcome, ConsistencyOutcome::Fresh("fresh".to_owned()));
    assert_eq!(
        cache.get::<String>("user:1").await.unwrap(),
        Some("fresh".to_owned())
    );
}

#[tokio::test]
async fn strict_mode_does_not_return_pre_invalidation_value() {
    let cache = HydraCache::local().build();
    cache
        .put("user:1", "stale".to_owned(), CacheOptions::new())
        .await
        .unwrap();
    let future_token = ConsistencyToken::new(99, "local", "other-node");

    let outcome = cache
        .get_with_consistency(
            "user:1",
            &future_token,
            ConsistencyMode::FailClosed,
            CacheOptions::new(),
            || async { Ok::<_, LoadError>("fresh".to_owned()) },
        )
        .await
        .unwrap();

    assert_eq!(
        outcome,
        ConsistencyOutcome::FailedClosed {
            reason: DegradeReason::Timeout
        }
    );
    assert_eq!(
        cache.get::<String>("user:1").await.unwrap(),
        Some("stale".to_owned())
    );
}

#[tokio::test]
async fn strict_mode_timeout_returns_explicit_error() {
    let cache = HydraCache::local().build();
    let future_token = ConsistencyToken::new(99, "local", "other-node");

    let outcome = cache
        .get_with_consistency(
            "missing",
            &future_token,
            ConsistencyMode::ClusterReadYourWrites {
                timeout: Duration::from_millis(1),
            },
            CacheOptions::new(),
            || async { Ok::<_, LoadError>("fresh".to_owned()) },
        )
        .await
        .unwrap();

    assert_eq!(outcome, ConsistencyOutcome::TimedOut);
    assert_eq!(cache.stats().consistency_wait_timeouts, 1);
}

#[tokio::test]
async fn degraded_mode_returns_stale_value_with_reason() {
    let cache = HydraCache::local().build();
    cache
        .put("user:1", "stale".to_owned(), CacheOptions::new())
        .await
        .unwrap();
    let future_token = ConsistencyToken::new(99, "local", "other-node");

    let outcome = cache
        .get_with_consistency(
            "user:1",
            &future_token,
            ConsistencyMode::DegradedOk,
            CacheOptions::new(),
            || async { Ok::<_, LoadError>("fresh".to_owned()) },
        )
        .await
        .unwrap();

    assert_eq!(
        outcome,
        ConsistencyOutcome::Degraded {
            value: "stale".to_owned(),
            reason: DegradeReason::Timeout,
        }
    );
    assert_eq!(cache.stats().consistency_degraded_reads, 1);
}

#[tokio::test]
async fn quorum_waits_while_leader_still_fails_closed() {
    let cache = HydraCache::local().build();
    let token = ConsistencyToken::new(1, "local", "other-node");

    let quorum = cache
        .get_with_consistency(
            "user:1",
            &token,
            ConsistencyMode::Quorum {
                timeout: Duration::from_millis(1),
            },
            CacheOptions::new(),
            || async { Ok::<_, LoadError>("fresh".to_owned()) },
        )
        .await
        .unwrap();
    let leader = cache
        .get_with_consistency(
            "user:1",
            &token,
            ConsistencyMode::Leader,
            CacheOptions::new(),
            || async { Ok::<_, LoadError>("fresh".to_owned()) },
        )
        .await
        .unwrap();

    assert_eq!(quorum, ConsistencyOutcome::TimedOut);
    assert_eq!(
        leader,
        ConsistencyOutcome::FailedClosed {
            reason: DegradeReason::UnsupportedMode("leader")
        }
    );
    assert_eq!(cache.stats().consistency_wait_timeouts, 1);
    assert_eq!(cache.stats().consistency_fail_closed, 1);
}

#[tokio::test]
async fn wait_success_timeout_degraded_counters_move() {
    let cache = HydraCache::local().build();
    let token = cache
        .invalidate_after_write("users")
        .consistency(ConsistencyMode::LocalReadYourWrites)
        .await
        .unwrap();

    let outcome = cache
        .get_with_consistency(
            "users:list",
            &token,
            ConsistencyMode::LocalReadYourWrites,
            CacheOptions::new().tag("users"),
            || async { Ok::<_, LoadError>(vec![1_u64]) },
        )
        .await
        .unwrap();

    assert_eq!(outcome, ConsistencyOutcome::Fresh(vec![1]));
    assert_eq!(cache.stats().consistency_wait_successes, 1);
}