use super::policy::{CompositionReadPolicy, CompositionWritePolicy};
use super::{CompositionBackend, CompositionPolicy};
use crate::Backend;
use hitbox_core::Offload;
pub trait Compose: Backend + Sized {
fn compose<L2, O>(self, l2: L2, offload: O) -> CompositionBackend<Self, L2, O>
where
L2: Backend,
O: Offload<'static>,
{
CompositionBackend::new(self, l2, offload)
}
fn compose_with<L2, O, R, W>(
self,
l2: L2,
offload: O,
policy: CompositionPolicy<R, W>,
) -> CompositionBackend<Self, L2, O, R, W>
where
L2: Backend,
O: Offload<'static>,
R: CompositionReadPolicy,
W: CompositionWritePolicy,
{
CompositionBackend::new(self, l2, offload).with_policy(policy)
}
}
impl<T: Backend> Compose for T {}
#[cfg(test)]
mod tests {
use super::*;
use crate::format::{Format, JsonFormat};
use crate::{
Backend, BackendResult, CacheBackend, CacheKeyFormat, Compressor, DeleteStatus,
PassthroughCompressor,
};
use async_trait::async_trait;
use chrono::Utc;
use hitbox_core::{
BoxContext, CacheContext, CacheKey, CachePolicy, CacheValue, CacheableResponse,
EntityPolicyConfig, Predicate, Raw,
};
use serde::{Deserialize, Serialize};
use smol_str::SmolStr;
use std::collections::HashMap;
use std::future::Future;
use std::sync::{Arc, Mutex};
#[cfg(feature = "rkyv_format")]
use rkyv::{Archive, Serialize as RkyvSerialize};
#[derive(Clone, Debug)]
struct TestOffload;
impl Offload<'static> for TestOffload {
#[allow(deprecated)]
fn spawn<F>(&self, _kind: impl Into<SmolStr>, future: F)
where
F: Future<Output = ()> + Send + 'static,
{
tokio::spawn(future);
}
}
#[derive(Clone, Debug)]
struct TestBackend {
store: Arc<Mutex<HashMap<CacheKey, CacheValue<Raw>>>>,
}
impl TestBackend {
fn new() -> Self {
Self {
store: Arc::new(Mutex::new(HashMap::new())),
}
}
}
#[async_trait]
impl Backend for TestBackend {
async fn read(&self, key: &CacheKey) -> BackendResult<Option<CacheValue<Raw>>> {
Ok(self.store.lock().unwrap().get(key).cloned())
}
async fn write(&self, key: &CacheKey, value: CacheValue<Raw>) -> BackendResult<()> {
self.store.lock().unwrap().insert(key.clone(), value);
Ok(())
}
async fn remove(&self, key: &CacheKey) -> BackendResult<DeleteStatus> {
match self.store.lock().unwrap().remove(key) {
Some(_) => Ok(DeleteStatus::Deleted(1)),
None => Ok(DeleteStatus::Missing),
}
}
fn value_format(&self) -> &dyn Format {
&JsonFormat
}
fn key_format(&self) -> &CacheKeyFormat {
&CacheKeyFormat::Bitcode
}
fn compressor(&self) -> &dyn Compressor {
&PassthroughCompressor
}
}
impl CacheBackend for TestBackend {}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(
feature = "rkyv_format",
derive(Archive, RkyvSerialize, rkyv::Deserialize)
)]
struct CachedData {
value: String,
}
struct MockResponse;
impl CacheableResponse for MockResponse {
type Cached = CachedData;
type Subject = MockResponse;
type IntoCachedFuture = std::future::Ready<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,
) -> CachePolicy<CacheValue<Self::Cached>, Self> {
unimplemented!()
}
fn into_cached(self) -> Self::IntoCachedFuture {
unimplemented!()
}
fn from_cached(_cached: Self::Cached) -> Self::FromCachedFuture {
unimplemented!()
}
}
#[tokio::test]
async fn test_compose_basic() {
let l1 = TestBackend::new();
let l2 = TestBackend::new();
let offload = TestOffload;
let cache = l1.clone().compose(l2.clone(), offload);
let key = CacheKey::from_str("test", "key1");
let value = CacheValue::new(
CachedData {
value: "test_value".to_string(),
},
Some(Utc::now() + chrono::Duration::seconds(60)),
None,
);
let mut ctx: BoxContext = CacheContext::default().boxed();
cache
.set::<MockResponse>(&key, &value, &mut ctx)
.await
.unwrap();
let mut ctx: BoxContext = CacheContext::default().boxed();
let result = cache.get::<MockResponse>(&key, &mut ctx).await.unwrap();
assert_eq!(result.unwrap().data().value, "test_value");
let mut ctx: BoxContext = CacheContext::default().boxed();
assert!(
l1.get::<MockResponse>(&key, &mut ctx)
.await
.unwrap()
.is_some()
);
let mut ctx: BoxContext = CacheContext::default().boxed();
assert!(
l2.get::<MockResponse>(&key, &mut ctx)
.await
.unwrap()
.is_some()
);
}
#[tokio::test]
async fn test_compose_with_policy() {
use super::super::policy::{RaceReadPolicy, RefillPolicy};
let l1 = TestBackend::new();
let l2 = TestBackend::new();
let offload = TestOffload;
let policy = CompositionPolicy::new()
.read(RaceReadPolicy::new())
.refill(RefillPolicy::Never);
let cache = l1.clone().compose_with(l2.clone(), offload, policy);
let key = CacheKey::from_str("test", "key1");
let value = CacheValue::new(
CachedData {
value: "from_l2".to_string(),
},
Some(Utc::now() + chrono::Duration::seconds(60)),
None,
);
let mut ctx: BoxContext = CacheContext::default().boxed();
l2.set::<MockResponse>(&key, &value, &mut ctx)
.await
.unwrap();
let mut ctx: BoxContext = CacheContext::default().boxed();
let result = cache.get::<MockResponse>(&key, &mut ctx).await.unwrap();
assert_eq!(result.unwrap().data().value, "from_l2");
let mut ctx: BoxContext = CacheContext::default().boxed();
assert!(
l1.get::<MockResponse>(&key, &mut ctx)
.await
.unwrap()
.is_none()
);
}
#[tokio::test]
async fn test_compose_nested() {
let l1 = TestBackend::new();
let l2 = TestBackend::new();
let l3 = TestBackend::new();
let offload = TestOffload;
let l2_l3 = l2.clone().compose(l3.clone(), offload.clone());
let cache = l1.clone().compose(l2_l3, offload);
let key = CacheKey::from_str("test", "nested_key");
let value = CacheValue::new(
CachedData {
value: "nested_value".to_string(),
},
Some(Utc::now() + chrono::Duration::seconds(60)),
None,
);
let mut ctx: BoxContext = CacheContext::default().boxed();
cache
.set::<MockResponse>(&key, &value, &mut ctx)
.await
.unwrap();
let mut ctx: BoxContext = CacheContext::default().boxed();
assert!(
l1.get::<MockResponse>(&key, &mut ctx)
.await
.unwrap()
.is_some()
);
let mut ctx: BoxContext = CacheContext::default().boxed();
assert!(
l2.get::<MockResponse>(&key, &mut ctx)
.await
.unwrap()
.is_some()
);
let mut ctx: BoxContext = CacheContext::default().boxed();
assert!(
l3.get::<MockResponse>(&key, &mut ctx)
.await
.unwrap()
.is_some()
);
}
#[tokio::test]
async fn test_compose_chaining() {
use super::super::policy::RaceReadPolicy;
let l1 = TestBackend::new();
let l2 = TestBackend::new();
let offload = TestOffload;
let cache = l1
.clone()
.compose(l2.clone(), offload)
.read(RaceReadPolicy::new());
let key = CacheKey::from_str("test", "chain_key");
let value = CacheValue::new(
CachedData {
value: "chain_value".to_string(),
},
Some(Utc::now() + chrono::Duration::seconds(60)),
None,
);
let mut ctx: BoxContext = CacheContext::default().boxed();
cache
.set::<MockResponse>(&key, &value, &mut ctx)
.await
.unwrap();
let mut ctx: BoxContext = CacheContext::default().boxed();
let result = cache.get::<MockResponse>(&key, &mut ctx).await.unwrap();
assert_eq!(result.unwrap().data().value, "chain_value");
}
}