hydracache 0.17.0

User-facing HydraCache runtime crate.
Documentation
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;

use hydracache::{cacheable, cacheable_infallible, HydraCache, TagSet};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct ExpensiveValue {
    id: u64,
}

#[derive(Debug)]
struct LoadError;

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

impl Error for LoadError {}

#[tokio::test]
async fn cacheable_macro_caches_loader_result() {
    let cache = HydraCache::local().build();
    let calls = Arc::new(AtomicUsize::new(0));

    let first: ExpensiveValue = cacheable!(
        cache = cache,
        key = "expensive:1",
        tag = "expensive",
        ttl_secs = 60,
        load = {
            let calls = Arc::clone(&calls);
            move || async move {
                calls.fetch_add(1, Ordering::SeqCst);
                Ok::<_, LoadError>(ExpensiveValue { id: 1 })
            }
        },
    )
    .await
    .unwrap();

    let second: ExpensiveValue = cacheable!(
        cache = cache,
        key = "expensive:1",
        tag = "expensive",
        ttl_secs = 60,
        load = {
            let calls = Arc::clone(&calls);
            move || async move {
                calls.fetch_add(1, Ordering::SeqCst);
                Ok::<_, LoadError>(ExpensiveValue { id: 2 })
            }
        },
    )
    .await
    .unwrap();

    assert_eq!(first, ExpensiveValue { id: 1 });
    assert_eq!(second, ExpensiveValue { id: 1 });
    assert_eq!(calls.load(Ordering::SeqCst), 1);
}

#[tokio::test]
async fn cacheable_macro_applies_tags_and_ttl() {
    let cache = HydraCache::local().build();
    let ttl_calls = Arc::new(AtomicUsize::new(0));

    let first: ExpensiveValue = cacheable!(
        cache = cache,
        key = "expiring:1",
        tag = "expiring",
        ttl = Duration::from_millis(20),
        load = {
            let ttl_calls = Arc::clone(&ttl_calls);
            move || async move {
                ttl_calls.fetch_add(1, Ordering::SeqCst);
                Ok::<_, LoadError>(ExpensiveValue { id: 1 })
            }
        },
    )
    .await
    .unwrap();

    tokio::time::sleep(Duration::from_millis(30)).await;

    let second: ExpensiveValue = cacheable!(
        cache = cache,
        key = "expiring:1",
        tag = "expiring",
        ttl = Duration::from_millis(20),
        load = {
            let ttl_calls = Arc::clone(&ttl_calls);
            move || async move {
                ttl_calls.fetch_add(1, Ordering::SeqCst);
                Ok::<_, LoadError>(ExpensiveValue { id: 2 })
            }
        },
    )
    .await
    .unwrap();

    assert_eq!(first, ExpensiveValue { id: 1 });
    assert_eq!(second, ExpensiveValue { id: 2 });
    assert_eq!(ttl_calls.load(Ordering::SeqCst), 2);

    cache.invalidate_tag("expiring").await.unwrap();
    assert!(!cache.contains_key("expiring:1").await);
}

#[tokio::test]
async fn cacheable_macro_accepts_tags_expression() {
    let cache = HydraCache::local().build();
    let calls = Arc::new(AtomicUsize::new(0));

    let first: ExpensiveValue = cacheable!(
        cache = cache,
        key = "tag-list:1",
        tags = ["tag-list", "tag-list:1"],
        load = {
            let calls = Arc::clone(&calls);
            move || async move {
                calls.fetch_add(1, Ordering::SeqCst);
                Ok::<_, LoadError>(ExpensiveValue { id: 1 })
            }
        },
    )
    .await
    .unwrap();

    let second: ExpensiveValue = cacheable!(
        cache = cache,
        key = "tag-list:1",
        tags = ["tag-list", "tag-list:1"],
        load = {
            let calls = Arc::clone(&calls);
            move || async move {
                calls.fetch_add(1, Ordering::SeqCst);
                Ok::<_, LoadError>(ExpensiveValue { id: 2 })
            }
        },
    )
    .await
    .unwrap();

    assert_eq!(first, ExpensiveValue { id: 1 });
    assert_eq!(second, ExpensiveValue { id: 1 });
    assert_eq!(calls.load(Ordering::SeqCst), 1);

    cache.invalidate_tag("tag-list").await.unwrap();
    assert!(!cache.contains_key("tag-list:1").await);
}

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

    let value: ExpensiveValue = cacheable!(
        cache = cache,
        key = "tag-set:1",
        tags = TagSet::new().tag("tag-set").entity("value", 1),
        load = || async { Ok::<_, LoadError>(ExpensiveValue { id: 1 }) },
    )
    .await
    .unwrap();

    assert_eq!(value, ExpensiveValue { id: 1 });

    cache.invalidate_tag("value:1").await.unwrap();
    assert!(!cache.contains_key("tag-set:1").await);
}

#[tokio::test]
async fn cacheable_infallible_macro_caches_loader_result() {
    let cache = HydraCache::local().build();
    let calls = Arc::new(AtomicUsize::new(0));

    let first: ExpensiveValue = cacheable_infallible!(
        cache = cache,
        key = "infallible:1",
        tags = ["infallible", "infallible:1"],
        ttl_secs = 60,
        load = {
            let calls = Arc::clone(&calls);
            move || async move {
                calls.fetch_add(1, Ordering::SeqCst);
                ExpensiveValue { id: 1 }
            }
        },
    )
    .await
    .unwrap();

    let second: ExpensiveValue = cacheable_infallible!(
        cache = cache,
        key = "infallible:1",
        tags = ["infallible", "infallible:1"],
        ttl_secs = 60,
        load = {
            let calls = Arc::clone(&calls);
            move || async move {
                calls.fetch_add(1, Ordering::SeqCst);
                ExpensiveValue { id: 2 }
            }
        },
    )
    .await
    .unwrap();

    assert_eq!(first, ExpensiveValue { id: 1 });
    assert_eq!(second, ExpensiveValue { id: 1 });
    assert_eq!(calls.load(Ordering::SeqCst), 1);
}