use chrono::Utc;
use std::sync::Arc;
use hitbox_backend::{CacheBackend, CompositionBackend, SyncBackend};
use hitbox_core::{
BoxContext, CacheContext, CacheKey, CacheValue, CacheableResponse, EntityPolicyConfig,
Predicate,
};
use serde::{Deserialize, Serialize};
#[cfg(feature = "rkyv_format")]
use rkyv::{Archive, Serialize as RkyvSerialize};
use crate::common::{TestBackend, TestOffloadManager};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[cfg_attr(
feature = "rkyv_format",
derive(Archive, RkyvSerialize, rkyv::Deserialize)
)]
pub(super) struct TestValue {
pub data: String,
}
impl CacheableResponse for TestValue {
type Cached = Self;
type Subject = Self;
type IntoCachedFuture = std::future::Ready<hitbox_core::CachePolicy<Self::Cached, Self>>;
type FromCachedFuture = std::future::Ready<Self>;
async fn cache_policy<P: Predicate<Subject = Self::Subject> + Send + Sync>(
self,
_predicate: P,
_config: &EntityPolicyConfig,
) -> hitbox_core::ResponseCachePolicy<Self> {
unimplemented!()
}
fn into_cached(self) -> Self::IntoCachedFuture {
unimplemented!()
}
fn from_cached(_cached: Self::Cached) -> Self::FromCachedFuture {
unimplemented!()
}
}
#[tokio::test]
async fn test_nested_composition_static_dispatch() {
let l1 = TestBackend::new();
let l2 = TestBackend::new();
let l3 = TestBackend::new();
let l2_l3 = CompositionBackend::new(l2.clone(), l3.clone(), TestOffloadManager);
let cache = CompositionBackend::new(l1.clone(), l2_l3, TestOffloadManager);
let key = CacheKey::from_str("test", "key1");
let value = CacheValue::new(
TestValue {
data: "test_value".to_string(),
},
Some(Utc::now() + chrono::Duration::seconds(60)),
None,
);
let mut ctx: BoxContext = CacheContext::default().boxed();
cache
.set::<TestValue>(&key, &value, &mut ctx)
.await
.unwrap();
assert!(l1.has(&key), "L1 should have the value");
assert!(l2.has(&key), "L2 should have the value");
assert!(l3.has(&key), "L3 should have the value");
let mut ctx: BoxContext = CacheContext::default().boxed();
let result = cache.get::<TestValue>(&key, &mut ctx).await.unwrap();
assert!(result.is_some());
assert_eq!(
*result.unwrap().data(),
TestValue {
data: "test_value".to_string()
}
);
}
async fn run_refill_3_levels_test<B: CacheBackend + Send + Sync>(
cache: B,
l1: &TestBackend,
l2: &TestBackend,
l3: &TestBackend,
key_suffix: &str,
) {
let key = CacheKey::from_str("test", &format!("deep_refill_{}", key_suffix));
let value = CacheValue::new(
TestValue {
data: "from_l3".to_string(),
},
Some(Utc::now() + chrono::Duration::seconds(60)),
None,
);
let mut ctx: BoxContext = CacheContext::default().boxed();
cache
.set::<TestValue>(&key, &value, &mut ctx)
.await
.unwrap();
assert!(l1.has(&key), "L1 should have data after write");
assert!(l2.has(&key), "L2 should have data after write");
assert!(l3.has(&key), "L3 should have data after write");
l1.clear();
l2.clear();
assert!(!l1.has(&key), "L1 should be empty after clear");
assert!(!l2.has(&key), "L2 should be empty after clear");
let mut ctx: BoxContext = CacheContext::default().boxed();
let result = cache.get::<TestValue>(&key, &mut ctx).await.unwrap();
let cached_value = result.expect("Should get value from L3");
assert_eq!(cached_value.data().data, "from_l3");
cache
.set::<TestValue>(&key, &cached_value, &mut ctx)
.await
.unwrap();
assert!(l1.has(&key), "L1 should be refilled after set()");
assert!(l2.has(&key), "L2 should be refilled after set()");
assert!(l3.has(&key), "L3 should still have data");
let mut ctx: BoxContext = CacheContext::default().boxed();
let result = cache.get::<TestValue>(&key, &mut ctx).await.unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap().data().data, "from_l3");
}
async fn run_refill_4_levels_test<B: CacheBackend + Send + Sync>(
cache: B,
l1: &TestBackend,
l2: &TestBackend,
l3: &TestBackend,
l4: &TestBackend,
key_suffix: &str,
) {
let key = CacheKey::from_str("test", &format!("very_deep_refill_{}", key_suffix));
let value = CacheValue::new(
TestValue {
data: "from_l4".to_string(),
},
Some(Utc::now() + chrono::Duration::seconds(60)),
None,
);
let mut ctx: BoxContext = CacheContext::default().boxed();
cache
.set::<TestValue>(&key, &value, &mut ctx)
.await
.unwrap();
l1.clear();
l2.clear();
l3.clear();
let mut ctx: BoxContext = CacheContext::default().boxed();
let result = cache.get::<TestValue>(&key, &mut ctx).await.unwrap();
let cached_value = result.expect("Should get value from L4");
assert_eq!(cached_value.data().data, "from_l4");
cache
.set::<TestValue>(&key, &cached_value, &mut ctx)
.await
.unwrap();
assert!(l1.has(&key), "L1 should be refilled");
assert!(l2.has(&key), "L2 should be refilled");
assert!(l3.has(&key), "L3 should be refilled");
assert!(l4.has(&key), "L4 should still have data");
}
async fn run_no_refill_never_policy_test<B: CacheBackend + Send + Sync>(
cache: B,
l1: &TestBackend,
l2: &TestBackend,
_l3: &TestBackend,
key_suffix: &str,
) {
use hitbox_core::ReadMode;
let key = CacheKey::from_str("test", &format!("no_refill_{}", key_suffix));
let value = CacheValue::new(
TestValue {
data: "from_l3".to_string(),
},
Some(Utc::now() + chrono::Duration::seconds(60)),
None,
);
let mut ctx: BoxContext = CacheContext::default().boxed();
cache
.set::<TestValue>(&key, &value, &mut ctx)
.await
.unwrap();
l1.clear();
l2.clear();
let mut ctx: BoxContext = CacheContext::default().boxed();
let result = cache.get::<TestValue>(&key, &mut ctx).await.unwrap();
assert!(result.is_some(), "Should get value from L3");
assert_eq!(
ctx.read_mode(),
ReadMode::Direct,
"With Never policy, read_mode should be Direct (not Refill)"
);
assert!(!l1.has(&key), "L1 should NOT be refilled with Never policy");
assert!(!l2.has(&key), "L2 should NOT be refilled with Never policy");
}
async fn run_never_policy_skips_refill_test<B: CacheBackend + Send + Sync>(
cache: B,
l1: &TestBackend,
l2: &TestBackend,
_l3: &TestBackend,
key_suffix: &str,
) {
use hitbox_core::ReadMode;
let key = CacheKey::from_str("test", &format!("no_refill_forced_{}", key_suffix));
let value = CacheValue::new(
TestValue {
data: "from_l3".to_string(),
},
Some(Utc::now() + chrono::Duration::seconds(60)),
None,
);
let mut ctx: BoxContext = CacheContext::default().boxed();
cache
.set::<TestValue>(&key, &value, &mut ctx)
.await
.unwrap();
l1.clear();
l2.clear();
let mut ctx: BoxContext = CacheContext::default().boxed();
let result = cache.get::<TestValue>(&key, &mut ctx).await.unwrap();
let cached_value = result.expect("Should get value from L3");
ctx.set_read_mode(ReadMode::Refill);
cache
.set::<TestValue>(&key, &cached_value, &mut ctx)
.await
.unwrap();
assert!(
!l1.has(&key),
"L1 should NOT be refilled - Never policy should prevent it"
);
assert!(
!l2.has(&key),
"L2 should NOT be refilled - Never policy should prevent it"
);
}
#[tokio::test]
async fn test_deep_nested_refill_3_levels() {
use hitbox_backend::composition::policy::RefillPolicy;
let l1 = TestBackend::new();
let l2 = TestBackend::new();
let l3 = TestBackend::new();
let inner = CompositionBackend::new(l2.clone(), l3.clone(), TestOffloadManager)
.refill(RefillPolicy::Always);
let outer =
CompositionBackend::new(l1.clone(), inner, TestOffloadManager).refill(RefillPolicy::Always);
run_refill_3_levels_test(outer, &l1, &l2, &l3, "static").await;
}
#[tokio::test]
async fn test_deep_nested_refill_4_levels() {
use hitbox_backend::composition::policy::RefillPolicy;
let l1 = TestBackend::new();
let l2 = TestBackend::new();
let l3 = TestBackend::new();
let l4 = TestBackend::new();
let innermost = CompositionBackend::new(l3.clone(), l4.clone(), TestOffloadManager)
.refill(RefillPolicy::Always);
let middle = CompositionBackend::new(l2.clone(), innermost, TestOffloadManager)
.refill(RefillPolicy::Always);
let outer = CompositionBackend::new(l1.clone(), middle, TestOffloadManager)
.refill(RefillPolicy::Always);
run_refill_4_levels_test(outer, &l1, &l2, &l3, &l4, "static").await;
}
#[tokio::test]
async fn test_deep_nested_no_refill_with_never_policy() {
use hitbox_backend::composition::policy::RefillPolicy;
let l1 = TestBackend::new();
let l2 = TestBackend::new();
let l3 = TestBackend::new();
let inner = CompositionBackend::new(l2.clone(), l3.clone(), TestOffloadManager)
.refill(RefillPolicy::Never);
let outer =
CompositionBackend::new(l1.clone(), inner, TestOffloadManager).refill(RefillPolicy::Never);
run_no_refill_never_policy_test(outer, &l1, &l2, &l3, "static").await;
}
#[tokio::test]
async fn test_never_policy_skips_refill_even_with_refill_mode() {
use hitbox_backend::composition::policy::RefillPolicy;
let l1 = TestBackend::new();
let l2 = TestBackend::new();
let l3 = TestBackend::new();
let inner = CompositionBackend::new(l2.clone(), l3.clone(), TestOffloadManager)
.refill(RefillPolicy::Never);
let outer =
CompositionBackend::new(l1.clone(), inner, TestOffloadManager).refill(RefillPolicy::Never);
run_never_policy_skips_refill_test(outer, &l1, &l2, &l3, "static").await;
}
#[tokio::test]
async fn test_deep_nested_refill_3_levels_arc_sync() {
use hitbox_backend::composition::policy::RefillPolicy;
let l1 = TestBackend::new();
let l2 = TestBackend::new();
let l3 = TestBackend::new();
let l1_arc: Arc<SyncBackend> = Arc::new(l1.clone());
let l2_arc: Arc<SyncBackend> = Arc::new(l2.clone());
let l3_arc: Arc<SyncBackend> = Arc::new(l3.clone());
let inner =
CompositionBackend::new(l2_arc, l3_arc, TestOffloadManager).refill(RefillPolicy::Always);
let outer =
CompositionBackend::new(l1_arc, inner, TestOffloadManager).refill(RefillPolicy::Always);
run_refill_3_levels_test(outer, &l1, &l2, &l3, "arc_sync").await;
}
#[tokio::test]
async fn test_deep_nested_refill_4_levels_arc_sync() {
use hitbox_backend::composition::policy::RefillPolicy;
let l1 = TestBackend::new();
let l2 = TestBackend::new();
let l3 = TestBackend::new();
let l4 = TestBackend::new();
let l1_arc: Arc<SyncBackend> = Arc::new(l1.clone());
let l2_arc: Arc<SyncBackend> = Arc::new(l2.clone());
let l3_arc: Arc<SyncBackend> = Arc::new(l3.clone());
let l4_arc: Arc<SyncBackend> = Arc::new(l4.clone());
let innermost =
CompositionBackend::new(l3_arc, l4_arc, TestOffloadManager).refill(RefillPolicy::Always);
let middle =
CompositionBackend::new(l2_arc, innermost, TestOffloadManager).refill(RefillPolicy::Always);
let outer =
CompositionBackend::new(l1_arc, middle, TestOffloadManager).refill(RefillPolicy::Always);
run_refill_4_levels_test(outer, &l1, &l2, &l3, &l4, "arc_sync").await;
}
#[tokio::test]
async fn test_deep_nested_no_refill_with_never_policy_arc_sync() {
use hitbox_backend::composition::policy::RefillPolicy;
let l1 = TestBackend::new();
let l2 = TestBackend::new();
let l3 = TestBackend::new();
let l1_arc: Arc<SyncBackend> = Arc::new(l1.clone());
let l2_arc: Arc<SyncBackend> = Arc::new(l2.clone());
let l3_arc: Arc<SyncBackend> = Arc::new(l3.clone());
let inner =
CompositionBackend::new(l2_arc, l3_arc, TestOffloadManager).refill(RefillPolicy::Never);
let outer =
CompositionBackend::new(l1_arc, inner, TestOffloadManager).refill(RefillPolicy::Never);
run_no_refill_never_policy_test(outer, &l1, &l2, &l3, "arc_sync").await;
}
#[tokio::test]
async fn test_never_policy_skips_refill_even_with_refill_mode_arc_sync() {
use hitbox_backend::composition::policy::RefillPolicy;
let l1 = TestBackend::new();
let l2 = TestBackend::new();
let l3 = TestBackend::new();
let l1_arc: Arc<SyncBackend> = Arc::new(l1.clone());
let l2_arc: Arc<SyncBackend> = Arc::new(l2.clone());
let l3_arc: Arc<SyncBackend> = Arc::new(l3.clone());
let inner =
CompositionBackend::new(l2_arc, l3_arc, TestOffloadManager).refill(RefillPolicy::Never);
let outer =
CompositionBackend::new(l1_arc, inner, TestOffloadManager).refill(RefillPolicy::Never);
run_never_policy_skips_refill_test(outer, &l1, &l2, &l3, "arc_sync").await;
}
#[tokio::test]
async fn test_nested_composition_dynamic_dispatch() {
let l1: Arc<SyncBackend> = Arc::new(TestBackend::new());
let l2: Arc<SyncBackend> = Arc::new(TestBackend::new());
let l3: Arc<SyncBackend> = Arc::new(TestBackend::new());
let l2_l3: Arc<SyncBackend> = Arc::new(CompositionBackend::new(l2, l3, TestOffloadManager));
let cache = CompositionBackend::new(l1, l2_l3, TestOffloadManager);
let key = CacheKey::from_str("test", "dyn_key");
let value = CacheValue::new(
TestValue {
data: "dynamic_value".to_string(),
},
Some(Utc::now() + chrono::Duration::seconds(60)),
None,
);
let mut ctx: BoxContext = CacheContext::default().boxed();
cache
.set::<TestValue>(&key, &value, &mut ctx)
.await
.unwrap();
let mut ctx: BoxContext = CacheContext::default().boxed();
let result = cache.get::<TestValue>(&key, &mut ctx).await.unwrap();
assert!(result.is_some());
assert_eq!(
*result.unwrap().data(),
TestValue {
data: "dynamic_value".to_string()
}
);
}
#[tokio::test]
async fn test_nested_composition_dynamic_as_trait_object() {
let l1 = TestBackend::new();
let l2 = TestBackend::new();
let l3 = TestBackend::new();
let key = CacheKey::from_str("test", "nested_trait");
let value = CacheValue::new(
TestValue {
data: "trait_object_value".to_string(),
},
Some(Utc::now() + chrono::Duration::seconds(60)),
None,
);
let l2_l3 = CompositionBackend::new(l2, l3, TestOffloadManager);
let nested = CompositionBackend::new(l1, l2_l3, TestOffloadManager);
let backend: Arc<SyncBackend> = Arc::new(nested);
let mut ctx: BoxContext = CacheContext::default().boxed();
backend
.set::<TestValue>(&key, &value, &mut ctx)
.await
.unwrap();
let mut ctx: BoxContext = CacheContext::default().boxed();
let result = backend.get::<TestValue>(&key, &mut ctx).await.unwrap();
assert!(result.is_some());
assert_eq!(
*result.unwrap().data(),
TestValue {
data: "trait_object_value".to_string()
}
);
}
struct TtlTestBackends {
l1: TestBackend,
l2: TestBackend,
}
impl TtlTestBackends {
fn two_level() -> Self {
Self {
l1: TestBackend::new(),
l2: TestBackend::new(),
}
}
}
async fn run_ttl_preserved_test<B: CacheBackend + Send + Sync>(
cache: B,
l1: &TestBackend,
l2: &TestBackend,
) {
let key = CacheKey::from_str("test", "ttl_key");
let expire_time = Utc::now() + chrono::Duration::seconds(300);
let value = CacheValue::new(
TestValue {
data: "ttl_test".to_string(),
},
Some(expire_time),
None,
);
let mut ctx: BoxContext = CacheContext::default().boxed();
cache
.set::<TestValue>(&key, &value, &mut ctx)
.await
.unwrap();
let l1_raw = l1.get_raw(&key).expect("L1 should have the value");
assert!(l1_raw.expire().is_some(), "L1 should have expire time");
assert_eq!(
l1_raw.expire(),
Some(expire_time),
"L1 expire time should match"
);
let l2_raw = l2.get_raw(&key).expect("L2 should have the value");
assert!(l2_raw.expire().is_some(), "L2 should have expire time");
assert_eq!(
l2_raw.expire(),
Some(expire_time),
"L2 expire time should match"
);
let mut ctx: BoxContext = CacheContext::default().boxed();
let result = cache.get::<TestValue>(&key, &mut ctx).await.unwrap();
assert!(result.is_some());
let cache_value = result.unwrap();
assert_eq!(
cache_value.expire(),
Some(expire_time),
"Read expire time should match"
);
}
async fn run_stale_preserved_test<B: CacheBackend + Send + Sync>(
cache: B,
l1: &TestBackend,
l2: &TestBackend,
) {
let key = CacheKey::from_str("test", "stale_key");
let expire_time = Utc::now() + chrono::Duration::seconds(300);
let stale_time = Utc::now() + chrono::Duration::seconds(60);
let value = CacheValue::new(
TestValue {
data: "stale_test".to_string(),
},
Some(expire_time),
Some(stale_time),
);
let mut ctx: BoxContext = CacheContext::default().boxed();
cache
.set::<TestValue>(&key, &value, &mut ctx)
.await
.unwrap();
let l1_raw = l1.get_raw(&key).expect("L1 should have the value");
assert!(l1_raw.stale().is_some(), "L1 should have stale time");
assert_eq!(
l1_raw.stale(),
Some(stale_time),
"L1 stale time should match"
);
let l2_raw = l2.get_raw(&key).expect("L2 should have the value");
assert!(l2_raw.stale().is_some(), "L2 should have stale time");
assert_eq!(
l2_raw.stale(),
Some(stale_time),
"L2 stale time should match"
);
let mut ctx: BoxContext = CacheContext::default().boxed();
let result = cache.get::<TestValue>(&key, &mut ctx).await.unwrap();
assert!(result.is_some());
let cache_value = result.unwrap();
assert_eq!(
cache_value.stale(),
Some(stale_time),
"Read stale time should match"
);
assert_eq!(
cache_value.expire(),
Some(expire_time),
"Read expire time should match"
);
}
async fn run_no_ttl_no_stale_test<B: CacheBackend + Send + Sync>(cache: B, l1: &TestBackend) {
let key = CacheKey::from_str("test", "no_ttl_key");
let value = CacheValue::new(
TestValue {
data: "no_ttl_test".to_string(),
},
None,
None,
);
let mut ctx: BoxContext = CacheContext::default().boxed();
cache
.set::<TestValue>(&key, &value, &mut ctx)
.await
.unwrap();
let l1_raw = l1.get_raw(&key).expect("L1 should have the value");
assert!(l1_raw.expire().is_none(), "L1 should have no expire time");
assert!(l1_raw.stale().is_none(), "L1 should have no stale time");
let mut ctx: BoxContext = CacheContext::default().boxed();
let result = cache.get::<TestValue>(&key, &mut ctx).await.unwrap();
assert!(result.is_some());
let cache_value = result.unwrap();
assert!(
cache_value.expire().is_none(),
"Read should have no expire time"
);
assert!(
cache_value.stale().is_none(),
"Read should have no stale time"
);
}
#[tokio::test]
async fn test_ttl_preserved_concrete() {
let backends = TtlTestBackends::two_level();
let cache =
CompositionBackend::new(backends.l1.clone(), backends.l2.clone(), TestOffloadManager);
run_ttl_preserved_test(cache, &backends.l1, &backends.l2).await;
}
#[tokio::test]
async fn test_stale_preserved_concrete() {
let backends = TtlTestBackends::two_level();
let cache =
CompositionBackend::new(backends.l1.clone(), backends.l2.clone(), TestOffloadManager);
run_stale_preserved_test(cache, &backends.l1, &backends.l2).await;
}
#[tokio::test]
async fn test_no_ttl_no_stale_concrete() {
let backends = TtlTestBackends::two_level();
let cache =
CompositionBackend::new(backends.l1.clone(), backends.l2.clone(), TestOffloadManager);
run_no_ttl_no_stale_test(cache, &backends.l1).await;
}
#[tokio::test]
async fn test_ttl_preserved_arc_sync() {
let backends = TtlTestBackends::two_level();
let l1: Arc<SyncBackend> = Arc::new(backends.l1.clone());
let l2: Arc<SyncBackend> = Arc::new(backends.l2.clone());
let cache = CompositionBackend::new(l1, l2, TestOffloadManager);
run_ttl_preserved_test(cache, &backends.l1, &backends.l2).await;
}
#[tokio::test]
async fn test_stale_preserved_arc_sync() {
let backends = TtlTestBackends::two_level();
let l1: Arc<SyncBackend> = Arc::new(backends.l1.clone());
let l2: Arc<SyncBackend> = Arc::new(backends.l2.clone());
let cache = CompositionBackend::new(l1, l2, TestOffloadManager);
run_stale_preserved_test(cache, &backends.l1, &backends.l2).await;
}
#[tokio::test]
async fn test_no_ttl_no_stale_arc_sync() {
let backends = TtlTestBackends::two_level();
let l1: Arc<SyncBackend> = Arc::new(backends.l1.clone());
let l2: Arc<SyncBackend> = Arc::new(backends.l2.clone());
let cache = CompositionBackend::new(l1, l2, TestOffloadManager);
run_no_ttl_no_stale_test(cache, &backends.l1).await;
}
#[tokio::test]
async fn test_nested_composition_delete_cascades() {
let l1 = TestBackend::new();
let l2 = TestBackend::new();
let l3 = TestBackend::new();
let key = CacheKey::from_str("test", "delete_key");
let value = CacheValue::new(
TestValue {
data: "to_delete".to_string(),
},
Some(Utc::now() + chrono::Duration::seconds(60)),
None,
);
let mut ctx: BoxContext = CacheContext::default().boxed();
l1.set::<TestValue>(&key, &value, &mut ctx).await.unwrap();
let mut ctx: BoxContext = CacheContext::default().boxed();
l2.set::<TestValue>(&key, &value, &mut ctx).await.unwrap();
let mut ctx: BoxContext = CacheContext::default().boxed();
l3.set::<TestValue>(&key, &value, &mut ctx).await.unwrap();
assert!(l1.has(&key));
assert!(l2.has(&key));
assert!(l3.has(&key));
let l2_l3 = CompositionBackend::new(l2.clone(), l3.clone(), TestOffloadManager);
let cache = CompositionBackend::new(l1.clone(), l2_l3, TestOffloadManager);
let mut ctx: BoxContext = CacheContext::default().boxed();
cache.delete(&key, &mut ctx).await.unwrap();
assert!(!l1.has(&key), "L1 should be deleted");
assert!(!l2.has(&key), "L2 should be deleted");
assert!(!l3.has(&key), "L3 should be deleted");
}