rs-zero 0.2.6

Rust-first microservice framework inspired by go-zero engineering practices
Documentation
#![cfg(feature = "cache-redis")]

use std::time::Duration;

use rs_zero::{
    cache::{CacheKey, CacheStore},
    cache_redis::{
        RedisCacheConfig, RedisCacheStore, RedisClusterRedirect, RedisClusterRouter,
        RedisUnavailablePolicy,
    },
};

#[tokio::test]
#[ignore = "requires RS_ZERO_TEST_REDIS_URL pointing to a running Redis instance"]
async fn redis_single_node_cache_roundtrip_and_degradation_config() {
    let url = std::env::var("RS_ZERO_TEST_REDIS_URL")
        .unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
    let store = RedisCacheStore::new(RedisCacheConfig {
        url,
        namespace: format!("rs-zero-redis-hardening-{}", unique_suffix()),
        unavailable_policy: RedisUnavailablePolicy::fail_open_for_cache(),
        ..RedisCacheConfig::default()
    })
    .expect("store");

    store.health_check().await.expect("health");
    let key = CacheKey::new(store.namespace(), ["cluster", "roundtrip"]);
    store
        .set_raw(&key, b"ok".to_vec(), Some(Duration::from_secs(5)))
        .await
        .expect("set");
    assert_eq!(
        store.get_raw(&key).await.expect("get"),
        Some(b"ok".to_vec())
    );
    store.delete(&key).await.expect("delete");
}

#[tokio::test]
#[ignore = "requires RS_ZERO_TEST_REDIS_CLUSTER_URL pointing to a Redis Cluster node"]
async fn redis_cluster_env_is_available_for_manual_moved_ask_validation() {
    let url = std::env::var("RS_ZERO_TEST_REDIS_CLUSTER_URL")
        .expect("RS_ZERO_TEST_REDIS_CLUSTER_URL is required");
    let store = RedisCacheStore::new(RedisCacheConfig {
        url,
        namespace: format!("rs-zero-redis-cluster-{}", unique_suffix()),
        cluster: rs_zero::cache_redis::RedisClusterConfig {
            enabled: true,
            max_redirects: 8,
            read_from_replicas: false,
        },
        ..RedisCacheConfig::default()
    })
    .expect("cluster store");

    store.health_check().await.expect("cluster health");
    let first = CacheKey::new(store.namespace(), ["{manual-cluster-a}", "value"]);
    let second = CacheKey::new(store.namespace(), ["{manual-cluster-b}", "value"]);
    store
        .set_raw(&first, b"first".to_vec(), Some(Duration::from_secs(5)))
        .await
        .expect("set first");
    store
        .set_raw(&second, b"second".to_vec(), Some(Duration::from_secs(5)))
        .await
        .expect("set second");
    assert_eq!(
        store.get_raw(&first).await.expect("get first"),
        Some(b"first".to_vec())
    );
    assert_eq!(
        store.get_raw(&second).await.expect("get second"),
        Some(b"second".to_vec())
    );
    store
        .delete_many(&[first.clone(), second.clone()])
        .await
        .expect("delete many");
    assert!(
        store
            .get_raw(&first)
            .await
            .expect("get deleted first")
            .is_none()
    );
    assert!(
        store
            .get_raw(&second)
            .await
            .expect("get deleted second")
            .is_none()
    );

    let mut router = RedisClusterRouter::new(8);
    let redirect = RedisClusterRedirect::parse(&format!(
        "MOVED {} 127.0.0.1:6379",
        RedisClusterRouter::slot_for_key(&first)
    ))
    .expect("redirect parse");
    router.apply_redirect(&redirect);
    assert!(router.endpoint_for_key(&first).is_some());
}

fn unique_suffix() -> String {
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_nanos();
    format!("{nanos}-{}", std::process::id())
}