accelerator 0.1.1

MVP multi-level cache runtime with singleflight load de-duplication
Documentation
use accelerator::CacheError;

use crate::support::{CacheableBatchHarness, MacroTestError, User};

#[tokio::test]
async fn cacheable_batch_merges_cache_hit_and_load_miss() {
    let harness = CacheableBatchHarness::new();
    harness.cache.seed(
        1,
        Some(User {
            id: 1,
            name: "cached-1".to_string(),
        }),
    );

    let values = harness.load_users(vec![1, 2, 4]).await.unwrap();

    assert_eq!(
        values.get(&1).cloned().unwrap(),
        Some(User {
            id: 1,
            name: "cached-1".to_string()
        })
    );
    assert_eq!(
        values.get(&2).cloned().unwrap(),
        Some(User {
            id: 2,
            name: "u2".to_string()
        })
    );
    assert_eq!(values.get(&4).cloned().unwrap(), None);
    assert_eq!(harness.load_inputs(), vec![vec![2, 4]]);
    assert_eq!(harness.cache.mget_calls(), 1);
    assert_eq!(harness.cache.mset_calls(), 1);
    assert_eq!(harness.cache.last_mset_sizes(), vec![2]);
}

#[tokio::test]
async fn cacheable_batch_deduplicates_duplicate_keys() {
    let harness = CacheableBatchHarness::new();

    let values = harness.load_users(vec![2, 2, 2, 3, 3]).await.unwrap();

    assert_eq!(values.len(), 2);
    assert_eq!(
        values.get(&2).cloned().unwrap(),
        Some(User {
            id: 2,
            name: "u2".to_string()
        })
    );
    assert_eq!(
        values.get(&3).cloned().unwrap(),
        Some(User {
            id: 3,
            name: "u3".to_string()
        })
    );
    assert_eq!(harness.load_inputs(), vec![vec![2, 3]]);
}

#[tokio::test]
async fn cacheable_batch_empty_input_short_circuits() {
    let harness = CacheableBatchHarness::new();

    let values = harness.load_users(Vec::new()).await.unwrap();

    assert!(values.is_empty());
    assert_eq!(harness.load_inputs().len(), 0);
    assert_eq!(harness.cache.mget_calls(), 0);
    assert_eq!(harness.cache.mset_calls(), 0);
}

#[tokio::test]
async fn cacheable_batch_ignore_mget_error_keeps_business_flow() {
    let harness = CacheableBatchHarness::new();
    harness.cache.set_fail_mget(true);

    let values = harness.load_users(vec![1, 2]).await.unwrap();

    assert_eq!(
        values.get(&1).cloned().unwrap(),
        Some(User {
            id: 1,
            name: "u1".to_string()
        })
    );
    assert_eq!(
        values.get(&2).cloned().unwrap(),
        Some(User {
            id: 2,
            name: "u2".to_string()
        })
    );
    assert_eq!(harness.load_inputs(), vec![vec![1, 2]]);
}

#[tokio::test]
async fn cacheable_batch_propagate_mget_error_returns_cache_error() {
    let harness = CacheableBatchHarness::new();
    harness.cache.set_fail_mget(true);

    let err = harness.load_users_propagate(vec![1, 2]).await.unwrap_err();

    assert!(matches!(
        err,
        MacroTestError::Cache(CacheError::Backend(ref msg)) if msg.contains("mock mget failed")
    ));
    assert_eq!(harness.load_inputs().len(), 0);
}

#[tokio::test]
async fn cacheable_batch_propagate_mset_error_returns_cache_error() {
    let harness = CacheableBatchHarness::new();
    harness.cache.set_fail_mset(true);

    let err = harness.load_users_propagate(vec![2, 3]).await.unwrap_err();

    assert!(matches!(
        err,
        MacroTestError::Cache(CacheError::Backend(ref msg)) if msg.contains("mock mset failed")
    ));
    assert_eq!(harness.load_inputs(), vec![vec![2, 3]]);
    assert_eq!(harness.cache.mset_calls(), 1);
}

#[tokio::test]
async fn cacheable_batch_business_error_is_returned_and_skips_mset() {
    let harness = CacheableBatchHarness::new();

    let err = harness.load_users_fail(vec![1, 2]).await.unwrap_err();

    assert!(matches!(err, MacroTestError::Biz("batch-load-failed")));
    assert_eq!(harness.load_inputs(), vec![vec![1, 2]]);
    assert_eq!(harness.cache.mset_calls(), 0);
}