hydracache 0.64.0

User-facing HydraCache runtime crate.
Documentation
use hydracache::{
    rebuild_expired_sessionless, recover_session_after_failover, validate_session_lifecycle,
    ClusterEpoch, HybridLogicalClock, PartitionId, PartitionKey, SessionFailoverAction, SessionId,
    SessionLifecycleDecision, SessionRequest, SessionToken, SessionTokenError, SessionTtl,
    SessionWatermark, VersionStamp,
};

#[test]
fn session_ttl_boundaries_accept_then_expire_without_wrapping() {
    assert_eq!(SessionTtl::from_millis(0).as_millis(), 1);
    assert_eq!(SessionTtl::from_secs(2).as_millis(), 2_000);
    assert_eq!(SessionTtl::from_secs(u64::MAX).as_millis(), u64::MAX);
    assert_eq!(SessionTtl::default().as_millis(), 900_000);
    assert!(!SessionTtl::from_millis(10).is_expired(100, 110));
    assert!(SessionTtl::from_millis(10).is_expired(100, 111));
    assert!(!SessionTtl::from_millis(10).is_expired(100, 50));
}

#[test]
fn session_validation_and_rebuild_distinguish_expiry_from_security_errors() {
    let secret = b"session-secret";
    let session = SessionId::new("session-a");
    let token = SessionToken::issue(session.clone(), SessionWatermark::new(4), 7, 100, secret);

    assert!(matches!(
        validate_session_lifecycle(
            &token,
            &session,
            secret,
            7,
            SessionTtl::from_millis(10),
            110,
        )
        .unwrap(),
        SessionLifecycleDecision::Accepted(SessionRequest::Session(_))
    ));
    assert_eq!(
        validate_session_lifecycle(
            &token,
            &session,
            secret,
            7,
            SessionTtl::from_millis(10),
            111,
        ),
        Err(SessionTokenError::Expired)
    );
    assert_eq!(
        rebuild_expired_sessionless(SessionTokenError::Expired),
        Ok(SessionRequest::Sessionless)
    );
    assert_eq!(
        rebuild_expired_sessionless(SessionTokenError::Replayed),
        Err(SessionTokenError::Replayed)
    );
}

#[test]
fn failover_recovery_preserves_empty_repairs_nonempty_or_rebuilds_loudly() {
    let empty = SessionWatermark::new(4);
    let preserved = recover_session_after_failover(&empty, "eu", false);
    assert_eq!(preserved.action, SessionFailoverAction::PreserveEmpty);
    assert!(preserved.guarantees_preserved);
    assert_eq!(preserved.watermark_entries, 0);

    let mut watermark = SessionWatermark::new(4);
    watermark.observe(
        PartitionKey::new(PartitionId::new(1), "us"),
        VersionStamp::new(7, ClusterEpoch::new(2), HybridLogicalClock::new(10, 0)),
    );
    let repaired = recover_session_after_failover(&watermark, "eu", true);
    assert_eq!(repaired.action, SessionFailoverAction::RepairToWatermark);
    assert!(repaired.guarantees_preserved);
    assert_eq!(repaired.watermark_entries, 1);

    let rebuilt = recover_session_after_failover(&watermark, "eu", false);
    assert_eq!(rebuilt.action, SessionFailoverAction::RebuildSessionless);
    assert!(!rebuilt.guarantees_preserved);
    assert_eq!(rebuilt.promoted_region.as_str(), "eu");
}