pub mod compose;
pub mod policy;
mod context;
mod envelope;
mod format;
pub use compose::Compose;
pub use policy::CompositionPolicy;
pub(crate) use context::{CompositionContext, CompositionLayer};
pub(crate) use format::CompositionFormat;
use crate::format::Format;
use crate::metrics::Timer;
use crate::{
Backend, BackendError, BackendResult, CacheBackend, CacheKeyFormat, Compressor, DeleteStatus,
PassthroughCompressor,
};
use async_trait::async_trait;
use envelope::CompositionEnvelope;
use hitbox_core::{
BackendLabel, BoxContext, CacheContext, CacheKey, CacheStatus, CacheValue, Cacheable,
CacheableResponse, Offload, Raw, ResponseSource,
};
use policy::{
CompositionReadPolicy, CompositionWritePolicy, OptimisticParallelWritePolicy, ReadResult,
RefillPolicy, SequentialReadPolicy,
};
use smol_str::SmolStr;
use std::sync::Arc;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum CompositionError {
#[error("Both cache layers failed - L1: {l1}, L2: {l2}")]
BothLayersFailed {
l1: BackendError,
l2: BackendError,
},
}
pub struct CompositionBackend<
L1,
L2,
O,
R = SequentialReadPolicy,
W = OptimisticParallelWritePolicy,
> where
L1: Backend,
L2: Backend,
O: Offload<'static>,
R: CompositionReadPolicy,
W: CompositionWritePolicy,
{
l1: L1,
l2: L2,
format: CompositionFormat,
offload: O,
read_policy: R,
write_policy: W,
refill_policy: RefillPolicy,
label: BackendLabel,
l1_label: SmolStr,
l2_label: SmolStr,
}
#[inline]
fn compose_label(prefix: &str, suffix: &str) -> SmolStr {
SmolStr::from(format!("{}.{}", prefix, suffix))
}
impl<L1, L2, O> CompositionBackend<L1, L2, O, SequentialReadPolicy, OptimisticParallelWritePolicy>
where
L1: Backend,
L2: Backend,
O: Offload<'static>,
{
pub fn new(l1: L1, l2: L2, offload: O) -> Self {
let label = BackendLabel::new_static("composition");
let l1_label = compose_label(label.as_str(), l1.label().as_str());
let l2_label = compose_label(label.as_str(), l2.label().as_str());
let format = CompositionFormat::new(
Arc::new(l1.value_format().clone_box()),
Arc::new(l2.value_format().clone_box()),
Arc::new(l1.compressor().clone_box()),
Arc::new(l2.compressor().clone_box()),
l1_label.clone(),
l2_label.clone(),
);
Self {
l1,
l2,
format,
offload,
read_policy: SequentialReadPolicy::new(),
write_policy: OptimisticParallelWritePolicy::new(),
refill_policy: RefillPolicy::default(),
label,
l1_label,
l2_label,
}
}
}
impl<L1, L2, O, R, W> CompositionBackend<L1, L2, O, R, W>
where
L1: Backend,
L2: Backend,
O: Offload<'static>,
R: CompositionReadPolicy,
W: CompositionWritePolicy,
{
pub fn read_policy(&self) -> &R {
&self.read_policy
}
pub fn write_policy(&self) -> &W {
&self.write_policy
}
pub fn refill_policy(&self) -> &RefillPolicy {
&self.refill_policy
}
pub fn offload(&self) -> &O {
&self.offload
}
pub fn label(mut self, label: impl Into<BackendLabel>) -> Self {
self.label = label.into();
self.l1_label = compose_label(self.label.as_str(), self.l1.label().as_str());
self.l2_label = compose_label(self.label.as_str(), self.l2.label().as_str());
self.format
.set_labels(self.l1_label.clone(), self.l2_label.clone());
self
}
pub fn with_policy<NewR, NewW>(
self,
policy: CompositionPolicy<NewR, NewW>,
) -> CompositionBackend<L1, L2, O, NewR, NewW>
where
NewR: CompositionReadPolicy,
NewW: CompositionWritePolicy,
{
CompositionBackend {
l1: self.l1,
l2: self.l2,
format: self.format,
offload: self.offload,
read_policy: policy.read,
write_policy: policy.write,
refill_policy: policy.refill,
label: self.label,
l1_label: self.l1_label,
l2_label: self.l2_label,
}
}
pub fn read<NewR: CompositionReadPolicy>(
self,
read_policy: NewR,
) -> CompositionBackend<L1, L2, O, NewR, W> {
CompositionBackend {
l1: self.l1,
l2: self.l2,
format: self.format,
offload: self.offload,
read_policy,
write_policy: self.write_policy,
refill_policy: self.refill_policy,
label: self.label,
l1_label: self.l1_label,
l2_label: self.l2_label,
}
}
pub fn write<NewW: CompositionWritePolicy>(
self,
write_policy: NewW,
) -> CompositionBackend<L1, L2, O, R, NewW> {
CompositionBackend {
l1: self.l1,
l2: self.l2,
format: self.format,
offload: self.offload,
read_policy: self.read_policy,
write_policy,
refill_policy: self.refill_policy,
label: self.label,
l1_label: self.l1_label,
l2_label: self.l2_label,
}
}
pub fn refill(mut self, refill_policy: RefillPolicy) -> Self {
self.refill_policy = refill_policy;
self
}
}
impl<L1, L2, O, R, W> Clone for CompositionBackend<L1, L2, O, R, W>
where
L1: Clone + Backend,
L2: Clone + Backend,
O: Offload<'static>,
R: Clone + CompositionReadPolicy,
W: Clone + CompositionWritePolicy,
{
fn clone(&self) -> Self {
Self {
l1: self.l1.clone(),
l2: self.l2.clone(),
format: self.format.clone(),
offload: self.offload.clone(),
read_policy: self.read_policy.clone(),
write_policy: self.write_policy.clone(),
refill_policy: self.refill_policy,
label: self.label.clone(),
l1_label: self.l1_label.clone(),
l2_label: self.l2_label.clone(),
}
}
}
impl<L1, L2, O, R, W> std::fmt::Debug for CompositionBackend<L1, L2, O, R, W>
where
L1: std::fmt::Debug + Backend,
L2: std::fmt::Debug + Backend,
O: std::fmt::Debug + Offload<'static>,
R: std::fmt::Debug + CompositionReadPolicy,
W: std::fmt::Debug + CompositionWritePolicy,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CompositionBackend")
.field("label", &self.label)
.field("l1", &self.l1)
.field("l2", &self.l2)
.field("format", &self.format)
.field("offload", &self.offload)
.field("read_policy", &self.read_policy)
.field("write_policy", &self.write_policy)
.field("refill_policy", &self.refill_policy)
.finish()
}
}
#[async_trait]
impl<L1, L2, O, R, W> Backend for CompositionBackend<L1, L2, O, R, W>
where
L1: Backend + Clone + Send + Sync + 'static,
L2: Backend + Clone + Send + Sync + 'static,
O: Offload<'static>,
R: CompositionReadPolicy,
W: CompositionWritePolicy,
{
#[tracing::instrument(skip(self), level = "trace")]
async fn read(&self, key: &CacheKey) -> BackendResult<Option<CacheValue<Raw>>> {
let l1 = self.l1.clone();
let l2 = self.l2.clone();
let l1_label = self.l1_label.clone();
let l2_label = self.l2_label.clone();
let read_l1_with_envelope = |k: CacheKey| async move {
let ctx: BoxContext = CacheContext::default().boxed();
let timer = Timer::new();
let read_result = l1.read(&k).await;
crate::metrics::record_read(&l1_label, timer.elapsed());
let result = match read_result {
Ok(Some(l1_value)) => {
crate::metrics::record_read_bytes(&l1_label, l1_value.data().len());
let (expire, stale) = (l1_value.expire(), l1_value.stale());
let envelope = CompositionEnvelope::L1(l1_value);
match envelope.serialize() {
Ok(packed) => Ok(Some(CacheValue::new(packed, expire, stale))),
Err(e) => Err(e),
}
}
Ok(None) => Ok(None),
Err(e) => {
crate::metrics::record_read_error(&l1_label);
Err(e)
}
};
(result, ctx)
};
let read_l2_with_envelope = |k: CacheKey| async move {
let ctx: BoxContext = CacheContext::default().boxed();
let timer = Timer::new();
let read_result = l2.read(&k).await;
crate::metrics::record_read(&l2_label, timer.elapsed());
let result = match read_result {
Ok(Some(l2_value)) => {
crate::metrics::record_read_bytes(&l2_label, l2_value.data().len());
let (expire, stale) = (l2_value.expire(), l2_value.stale());
let envelope = CompositionEnvelope::L2(l2_value);
match envelope.serialize() {
Ok(packed) => Ok(Some(CacheValue::new(packed, expire, stale))),
Err(e) => Err(e),
}
}
Ok(None) => Ok(None),
Err(e) => {
crate::metrics::record_read_error(&l2_label);
Err(e)
}
};
(result, ctx)
};
let ReadResult { value, .. } = self
.read_policy
.execute_with(
key.clone(),
read_l1_with_envelope,
read_l2_with_envelope,
&self.offload,
)
.await?;
Ok(value)
}
#[tracing::instrument(skip(self, value), level = "trace")]
async fn write(&self, key: &CacheKey, value: CacheValue<Raw>) -> BackendResult<()> {
let composition = CompositionEnvelope::deserialize(value.data())?;
match composition {
CompositionEnvelope::Both { l1, l2 } => {
let l1_backend = self.l1.clone();
let l2_backend = self.l2.clone();
let l1_label = self.l1_label.clone();
let l2_label = self.l2_label.clone();
let l1_len = l1.data().len();
let l2_len = l2.data().len();
let write_l1 = |k: CacheKey| async move {
let timer = Timer::new();
let result = l1_backend.write(&k, l1).await;
crate::metrics::record_write(&l1_label, timer.elapsed());
match &result {
Ok(()) => crate::metrics::record_write_bytes(&l1_label, l1_len),
Err(_) => crate::metrics::record_write_error(&l1_label),
}
result
};
let write_l2 = |k: CacheKey| async move {
let timer = Timer::new();
let result = l2_backend.write(&k, l2).await;
crate::metrics::record_write(&l2_label, timer.elapsed());
match &result {
Ok(()) => crate::metrics::record_write_bytes(&l2_label, l2_len),
Err(_) => crate::metrics::record_write_error(&l2_label),
}
result
};
self.write_policy
.execute_with(key.clone(), write_l1, write_l2, &self.offload)
.await
}
CompositionEnvelope::L1(l1) => {
let l1_len = l1.data().len();
let timer = Timer::new();
let result = self.l1.write(key, l1).await;
crate::metrics::record_write(&self.l1_label, timer.elapsed());
match &result {
Ok(()) => crate::metrics::record_write_bytes(&self.l1_label, l1_len),
Err(_) => crate::metrics::record_write_error(&self.l1_label),
}
result
}
CompositionEnvelope::L2(l2) => {
let l2_len = l2.data().len();
let timer = Timer::new();
let result = self.l2.write(key, l2).await;
crate::metrics::record_write(&self.l2_label, timer.elapsed());
match &result {
Ok(()) => crate::metrics::record_write_bytes(&self.l2_label, l2_len),
Err(_) => crate::metrics::record_write_error(&self.l2_label),
}
result
}
}
}
#[tracing::instrument(skip(self), level = "trace")]
async fn remove(&self, key: &CacheKey) -> BackendResult<DeleteStatus> {
let (l1_result, l2_result) = futures::join!(self.l1.remove(key), self.l2.remove(key));
match (l1_result, l2_result) {
(Err(e1), Err(e2)) => {
tracing::error!(l1_error = ?e1, l2_error = ?e2, "Both L1 and L2 delete failed");
Err(BackendError::InternalError(Box::new(
CompositionError::BothLayersFailed { l1: e1, l2: e2 },
)))
}
(Err(e), Ok(status)) => {
tracing::warn!(error = ?e, "L1 delete failed");
Ok(status)
}
(Ok(status), Err(e)) => {
tracing::warn!(error = ?e, "L2 delete failed");
Ok(status)
}
(Ok(DeleteStatus::Deleted(n1)), Ok(DeleteStatus::Deleted(n2))) => {
Ok(DeleteStatus::Deleted(n1 + n2))
}
(Ok(DeleteStatus::Deleted(n)), Ok(DeleteStatus::Missing))
| (Ok(DeleteStatus::Missing), Ok(DeleteStatus::Deleted(n))) => {
Ok(DeleteStatus::Deleted(n))
}
(Ok(DeleteStatus::Missing), Ok(DeleteStatus::Missing)) => Ok(DeleteStatus::Missing),
}
}
fn label(&self) -> BackendLabel {
self.label.clone()
}
fn value_format(&self) -> &dyn Format {
&self.format
}
fn key_format(&self) -> &CacheKeyFormat {
&CacheKeyFormat::Bitcode
}
fn compressor(&self) -> &dyn Compressor {
&PassthroughCompressor
}
}
impl<L1, L2, O, R, W> CacheBackend for CompositionBackend<L1, L2, O, R, W>
where
L1: CacheBackend + Clone + Send + Sync + 'static,
L2: CacheBackend + Clone + Send + Sync + 'static,
O: Offload<'static>,
R: CompositionReadPolicy,
W: CompositionWritePolicy,
{
#[tracing::instrument(skip(self, ctx), level = "trace")]
async fn get<T>(
&self,
key: &CacheKey,
ctx: &mut BoxContext,
) -> BackendResult<Option<CacheValue<T::Cached>>>
where
T: CacheableResponse,
T::Cached: Cacheable,
{
let l1 = self.l1.clone();
let l2 = self.l2.clone();
let l1_label = self.l1_label.clone();
let l2_label = self.l2_label.clone();
let l1_name = l1.label();
let l2_name = l2.label();
let format_for_l1 = self.format.clone();
let format_for_l2 = self.format.clone();
let l1_ctx = ctx.clone_box();
let l2_ctx = ctx.clone_box();
let read_l1 = |k: CacheKey| async move {
let mut internal_ctx = l1_ctx;
let read_timer = Timer::new();
let read_result = l1.read(&k).await;
crate::metrics::record_read(&l1_label, read_timer.elapsed());
let result = match read_result {
Ok(Some(raw_value)) => {
let (meta, raw_data) = raw_value.into_parts();
crate::metrics::record_read_bytes(&l1_label, raw_data.len());
let mut deserialized_opt: Option<T::Cached> = None;
match format_for_l1.deserialize_layer(
&raw_data,
CompositionLayer::L1,
&mut |deserializer| {
let value: T::Cached = deserializer.deserialize()?;
deserialized_opt = Some(value);
Ok(())
},
&mut internal_ctx,
) {
Ok(()) => match deserialized_opt {
Some(deserialized) => {
internal_ctx.set_status(CacheStatus::Hit);
let source = if let Some(comp_ctx) =
internal_ctx.as_any().downcast_ref::<CompositionContext>()
{
BackendLabel::from(
comp_ctx.format.label_for_layer(comp_ctx.layer).clone(),
)
} else {
l1_name.clone()
};
internal_ctx.set_source(ResponseSource::Backend(source));
Ok(Some(CacheValue::new(deserialized, meta.expire, meta.stale)))
}
None => Err(BackendError::InternalError(Box::new(
std::io::Error::other("deserialization produced no result"),
))),
},
Err(e) => Err(BackendError::InternalError(Box::new(e))),
}
}
Ok(None) => Ok(None),
Err(e) => {
crate::metrics::record_read_error(&l1_label);
Err(e)
}
};
(result, internal_ctx)
};
let read_l2 = |k: CacheKey| async move {
let mut internal_ctx = l2_ctx;
let read_timer = Timer::new();
let read_result = l2.read(&k).await;
crate::metrics::record_read(&l2_label, read_timer.elapsed());
let result = match read_result {
Ok(Some(raw_value)) => {
let (meta, raw_data) = raw_value.into_parts();
crate::metrics::record_read_bytes(&l2_label, raw_data.len());
let mut deserialized_opt: Option<T::Cached> = None;
match format_for_l2.deserialize_layer(
&raw_data,
CompositionLayer::L2,
&mut |deserializer| {
let value: T::Cached = deserializer.deserialize()?;
deserialized_opt = Some(value);
Ok(())
},
&mut internal_ctx,
) {
Ok(()) => match deserialized_opt {
Some(deserialized) => {
let cache_value =
CacheValue::new(deserialized, meta.expire, meta.stale);
internal_ctx.set_status(CacheStatus::Hit);
let source = if let Some(comp_ctx) =
internal_ctx.as_any().downcast_ref::<CompositionContext>()
{
BackendLabel::from(
comp_ctx.format.label_for_layer(comp_ctx.layer).clone(),
)
} else {
l2_name.clone()
};
internal_ctx.set_source(ResponseSource::Backend(source));
Ok(Some(cache_value))
}
None => Err(BackendError::InternalError(Box::new(
std::io::Error::other("deserialization produced no result"),
))),
},
Err(e) => Err(BackendError::InternalError(Box::new(e))),
}
}
Ok(None) => Ok(None),
Err(e) => {
crate::metrics::record_read_error(&l2_label);
Err(e)
}
};
(result, internal_ctx)
};
let ReadResult {
value,
source,
context: inner_ctx,
} = self
.read_policy
.execute_with(key.clone(), read_l1, read_l2, &self.offload)
.await?;
if let Some(ref _cache_value) = value {
ctx.merge_from(&*inner_ctx, &self.label);
if source == CompositionLayer::L2 && self.refill_policy == RefillPolicy::Always {
ctx.set_read_mode(hitbox_core::ReadMode::Refill);
}
}
Ok(value)
}
#[tracing::instrument(skip(self, value, ctx), level = "trace")]
async fn set<T>(
&self,
key: &CacheKey,
value: &CacheValue<T::Cached>,
ctx: &mut BoxContext,
) -> BackendResult<()>
where
T: CacheableResponse,
T::Cached: Cacheable,
{
use hitbox_core::ReadMode;
if ctx.read_mode() == ReadMode::Refill {
match self.refill_policy {
RefillPolicy::Always => {
let l1_bytes = self
.format
.serialize_layer(
CompositionLayer::L1,
&mut |serializer| {
serializer.serialize(value.data())?;
Ok(())
},
&**ctx,
)
.map_err(|e| BackendError::InternalError(Box::new(e)))?;
let l1_len = l1_bytes.len();
let l1_value = CacheValue::new(l1_bytes, value.expire(), value.stale());
let timer = Timer::new();
let result = self.l1.write(key, l1_value).await;
crate::metrics::record_write(&self.l1_label, timer.elapsed());
match &result {
Ok(()) => crate::metrics::record_write_bytes(&self.l1_label, l1_len),
Err(_) => crate::metrics::record_write_error(&self.l1_label),
}
result?;
return self.l2.set::<T>(key, value, ctx).await;
}
RefillPolicy::Never => {
return Ok(());
}
}
}
if let Some(comp_ctx) = ctx.as_any().downcast_ref::<CompositionContext>()
&& comp_ctx.layer == CompositionLayer::L2
{
match self.refill_policy {
RefillPolicy::Always => {
let l1_bytes = self
.format
.serialize_layer(
CompositionLayer::L1,
&mut |serializer| {
serializer.serialize(value.data())?;
Ok(())
},
&**ctx,
)
.map_err(|e| BackendError::InternalError(Box::new(e)))?;
let l1_len = l1_bytes.len();
let l1_value = CacheValue::new(l1_bytes, value.expire(), value.stale());
let timer = Timer::new();
let result = self.l1.write(key, l1_value).await;
crate::metrics::record_write(&self.l1_label, timer.elapsed());
match &result {
Ok(()) => crate::metrics::record_write_bytes(&self.l1_label, l1_len),
Err(_) => crate::metrics::record_write_error(&self.l1_label),
}
result?;
let mut inner_ctx = comp_ctx.inner().clone_box();
return self.l2.set::<T>(key, value, &mut inner_ctx).await;
}
RefillPolicy::Never => {
let mut inner_ctx = comp_ctx.inner().clone_box();
return self.l2.set::<T>(key, value, &mut inner_ctx).await;
}
}
}
let (l1_bytes, l2_bytes) = self
.format
.serialize_parts(
&mut |serializer| {
serializer.serialize(value.data())?;
Ok(())
},
&**ctx,
)
.map_err(|e| BackendError::InternalError(Box::new(e)))?;
let l1_len = l1_bytes.len();
let l2_len = l2_bytes.len();
let l1_value = CacheValue::new(l1_bytes, value.expire(), value.stale());
let l2_value = CacheValue::new(l2_bytes, value.expire(), value.stale());
let l1 = self.l1.clone();
let l2 = self.l2.clone();
let l1_label = self.l1_label.clone();
let l2_label = self.l2_label.clone();
let write_l1 = |k: CacheKey| async move {
let timer = Timer::new();
let result = l1.write(&k, l1_value).await;
crate::metrics::record_write(&l1_label, timer.elapsed());
match &result {
Ok(()) => crate::metrics::record_write_bytes(&l1_label, l1_len),
Err(_) => crate::metrics::record_write_error(&l1_label),
}
result
};
let write_l2 = |k: CacheKey| async move {
let timer = Timer::new();
let result = l2.write(&k, l2_value).await;
crate::metrics::record_write(&l2_label, timer.elapsed());
match &result {
Ok(()) => crate::metrics::record_write_bytes(&l2_label, l2_len),
Err(_) => crate::metrics::record_write_error(&l2_label),
}
result
};
self.write_policy
.execute_with(key.clone(), write_l1, write_l2, &self.offload)
.await
}
#[tracing::instrument(skip(self, ctx), level = "trace")]
async fn delete(&self, key: &CacheKey, ctx: &mut BoxContext) -> BackendResult<DeleteStatus> {
let mut l1_ctx = ctx.clone_box();
let mut l2_ctx = ctx.clone_box();
let (l1_result, l2_result) = futures::join!(
self.l1.delete(key, &mut l1_ctx),
self.l2.delete(key, &mut l2_ctx)
);
match (l1_result, l2_result) {
(Err(e1), Err(e2)) => {
tracing::error!(l1_error = ?e1, l2_error = ?e2, "Both L1 and L2 delete failed");
Err(BackendError::InternalError(Box::new(
CompositionError::BothLayersFailed { l1: e1, l2: e2 },
)))
}
(Err(e), Ok(status)) => {
tracing::warn!(error = ?e, "L1 delete failed");
Ok(status)
}
(Ok(status), Err(e)) => {
tracing::warn!(error = ?e, "L2 delete failed");
Ok(status)
}
(Ok(DeleteStatus::Deleted(n1)), Ok(DeleteStatus::Deleted(n2))) => {
tracing::trace!("Deleted from both L1 and L2");
Ok(DeleteStatus::Deleted(n1 + n2))
}
(Ok(DeleteStatus::Deleted(n)), Ok(DeleteStatus::Missing))
| (Ok(DeleteStatus::Missing), Ok(DeleteStatus::Deleted(n))) => {
tracing::trace!("Deleted from one layer");
Ok(DeleteStatus::Deleted(n))
}
(Ok(DeleteStatus::Missing), Ok(DeleteStatus::Missing)) => {
tracing::trace!("Key missing from both layers");
Ok(DeleteStatus::Missing)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::format::{Format, JsonFormat};
use crate::{Backend, CacheKeyFormat, Compressor, PassthroughCompressor};
use async_trait::async_trait;
use chrono::Utc;
use hitbox_core::{
BoxContext, CacheContext, CachePolicy, CacheStatus, CacheValue, CacheableResponse,
EntityPolicyConfig, Predicate, Raw, ResponseSource,
};
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>>>>,
backend_label: &'static str,
}
impl TestBackend {
fn new() -> Self {
Self {
store: Arc::new(Mutex::new(HashMap::new())),
backend_label: "test",
}
}
fn with_label(label: &'static str) -> Self {
Self {
store: Arc::new(Mutex::new(HashMap::new())),
backend_label: label,
}
}
}
#[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 label(&self) -> BackendLabel {
BackendLabel::new(self.backend_label)
}
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!("Not used in these tests")
}
fn into_cached(self) -> Self::IntoCachedFuture {
unimplemented!("Not used in these tests")
}
fn from_cached(_cached: Self::Cached) -> Self::FromCachedFuture {
unimplemented!("Not used in these tests")
}
}
#[tokio::test]
async fn test_l1_hit() {
let l1 = TestBackend::with_label("moka");
let l2 = TestBackend::with_label("redis");
let backend = CompositionBackend::new(l1.clone(), l2, TestOffload).label("cache");
let key = CacheKey::from_str("test", "key1");
let value = CacheValue::new(
CachedData {
value: "value1".to_string(),
},
Some(Utc::now() + chrono::Duration::seconds(60)),
None,
);
let mut ctx: BoxContext = CacheContext::default().boxed();
backend
.set::<MockResponse>(&key, &value, &mut ctx)
.await
.unwrap();
let mut ctx: BoxContext = CacheContext::default().boxed();
let result = backend.get::<MockResponse>(&key, &mut ctx).await.unwrap();
assert_eq!(result.unwrap().data().value, "value1");
assert_eq!(ctx.status(), CacheStatus::Hit);
assert_eq!(ctx.source(), &ResponseSource::Backend("cache.moka".into()));
}
#[tokio::test]
async fn test_l2_hit_sets_refill_mode() {
use hitbox_core::ReadMode;
let l1 = TestBackend::with_label("moka");
let l2 = TestBackend::with_label("redis");
let key = CacheKey::from_str("test", "key1");
let value = CacheValue::new(
CachedData {
value: "value1".to_string(),
},
Some(Utc::now() + chrono::Duration::seconds(60)),
None,
);
let backend = CompositionBackend::new(l1.clone(), l2.clone(), TestOffload)
.label("cache")
.refill(RefillPolicy::Always);
let mut ctx: BoxContext = CacheContext::default().boxed();
backend
.set::<MockResponse>(&key, &value, &mut ctx)
.await
.unwrap();
l1.store.lock().unwrap().clear();
let mut ctx: BoxContext = CacheContext::default().boxed();
let result = backend.get::<MockResponse>(&key, &mut ctx).await.unwrap();
assert_eq!(result.unwrap().data().value, "value1");
assert_eq!(ctx.status(), CacheStatus::Hit);
assert_eq!(ctx.source(), &ResponseSource::Backend("cache.redis".into()));
assert_eq!(ctx.read_mode(), ReadMode::Refill);
let mut ctx: BoxContext = CacheContext::default().boxed();
let l1_result = l1.get::<MockResponse>(&key, &mut ctx).await.unwrap();
assert!(
l1_result.is_none(),
"L1 should not be populated directly by get()"
);
}
#[tokio::test]
async fn test_miss_both_layers() {
let l1 = TestBackend::new();
let l2 = TestBackend::new();
let backend = CompositionBackend::new(l1, l2, TestOffload);
let key = CacheKey::from_str("test", "nonexistent");
let mut ctx: BoxContext = CacheContext::default().boxed();
let result = backend.get::<MockResponse>(&key, &mut ctx).await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn test_write_to_both_layers() {
let l1 = TestBackend::new();
let l2 = TestBackend::new();
let key = CacheKey::from_str("test", "key1");
let value = CacheValue::new(
CachedData {
value: "value1".to_string(),
},
Some(Utc::now() + chrono::Duration::seconds(60)),
None,
);
let backend = CompositionBackend::new(l1.clone(), l2.clone(), TestOffload);
let mut ctx: BoxContext = CacheContext::default().boxed();
backend
.set::<MockResponse>(&key, &value, &mut ctx)
.await
.unwrap();
let mut ctx: BoxContext = CacheContext::default().boxed();
let l1_result = l1.get::<MockResponse>(&key, &mut ctx).await.unwrap();
assert_eq!(l1_result.unwrap().data().value, "value1");
let mut ctx: BoxContext = CacheContext::default().boxed();
let l2_result = l2.get::<MockResponse>(&key, &mut ctx).await.unwrap();
assert_eq!(l2_result.unwrap().data().value, "value1");
}
#[tokio::test]
async fn test_delete_from_both_layers() {
let l1 = TestBackend::new();
let l2 = TestBackend::new();
let key = CacheKey::from_str("test", "key1");
let value = CacheValue::new(
CachedData {
value: "value1".to_string(),
},
Some(Utc::now() + chrono::Duration::seconds(60)),
None,
);
let backend = CompositionBackend::new(l1.clone(), l2.clone(), TestOffload);
let mut ctx: BoxContext = CacheContext::default().boxed();
backend
.set::<MockResponse>(&key, &value, &mut ctx)
.await
.unwrap();
let mut ctx: BoxContext = CacheContext::default().boxed();
let status = backend.delete(&key, &mut ctx).await.unwrap();
assert_eq!(status, DeleteStatus::Deleted(2));
let mut ctx: BoxContext = CacheContext::default().boxed();
let l1_result = l1.get::<MockResponse>(&key, &mut ctx).await.unwrap();
assert!(l1_result.is_none());
let mut ctx: BoxContext = CacheContext::default().boxed();
let l2_result = l2.get::<MockResponse>(&key, &mut ctx).await.unwrap();
assert!(l2_result.is_none());
}
#[tokio::test]
async fn test_clone() {
let l1 = TestBackend::new();
let l2 = TestBackend::new();
let backend = CompositionBackend::new(l1, l2, TestOffload);
let cloned = backend.clone();
let key = CacheKey::from_str("test", "key1");
let value = CacheValue::new(
CachedData {
value: "value1".to_string(),
},
Some(Utc::now() + chrono::Duration::seconds(60)),
None,
);
let mut ctx: BoxContext = CacheContext::default().boxed();
backend
.set::<MockResponse>(&key, &value, &mut ctx)
.await
.unwrap();
let mut ctx: BoxContext = CacheContext::default().boxed();
let result = cloned.get::<MockResponse>(&key, &mut ctx).await.unwrap();
assert_eq!(result.unwrap().data().value, "value1");
}
#[tokio::test]
async fn test_nested_composition_source_path() {
let l1 = TestBackend::with_label("moka");
let l2 = TestBackend::with_label("redis");
let l3 = TestBackend::with_label("disk");
let inner = CompositionBackend::new(l1.clone(), l2.clone(), TestOffload).label("inner");
let outer = CompositionBackend::new(inner, l3.clone(), TestOffload).label("outer");
let key = CacheKey::from_str("test", "nested");
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();
l1.set::<MockResponse>(&key, &value, &mut ctx)
.await
.unwrap();
let mut ctx: BoxContext = CacheContext::default().boxed();
let result = outer.get::<MockResponse>(&key, &mut ctx).await.unwrap();
assert_eq!(result.unwrap().data().value, "nested_value");
assert_eq!(ctx.status(), CacheStatus::Hit);
assert_eq!(
ctx.source(),
&ResponseSource::Backend("outer.inner.moka".into())
);
}
#[tokio::test]
async fn test_nested_composition_l2_source_path() {
let l1 = TestBackend::with_label("moka");
let l2 = TestBackend::with_label("redis");
let l3 = TestBackend::with_label("disk");
let inner = CompositionBackend::new(l1.clone(), l2.clone(), TestOffload).label("inner");
let outer = CompositionBackend::new(inner, l3.clone(), TestOffload).label("outer");
let key = CacheKey::from_str("test", "nested_l2");
let value = CacheValue::new(
CachedData {
value: "from_redis".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 = outer.get::<MockResponse>(&key, &mut ctx).await.unwrap();
assert_eq!(result.unwrap().data().value, "from_redis");
assert_eq!(ctx.status(), CacheStatus::Hit);
assert_eq!(
ctx.source(),
&ResponseSource::Backend("outer.inner.redis".into())
);
}
#[tokio::test]
async fn test_nested_composition_outer_l2_source_path() {
let l1 = TestBackend::with_label("moka");
let l2 = TestBackend::with_label("redis");
let l3 = TestBackend::with_label("disk");
let inner = CompositionBackend::new(l1.clone(), l2.clone(), TestOffload).label("inner");
let outer = CompositionBackend::new(inner, l3.clone(), TestOffload).label("outer");
let key = CacheKey::from_str("test", "outer_l2");
let value = CacheValue::new(
CachedData {
value: "from_disk".to_string(),
},
Some(Utc::now() + chrono::Duration::seconds(60)),
None,
);
let mut ctx: BoxContext = CacheContext::default().boxed();
l3.set::<MockResponse>(&key, &value, &mut ctx)
.await
.unwrap();
let mut ctx: BoxContext = CacheContext::default().boxed();
let result = outer.get::<MockResponse>(&key, &mut ctx).await.unwrap();
assert_eq!(result.unwrap().data().value, "from_disk");
assert_eq!(ctx.status(), CacheStatus::Hit);
assert_eq!(ctx.source(), &ResponseSource::Backend("outer.disk".into()));
}
#[tokio::test]
async fn test_l1_hit_status() {
let l1 = TestBackend::with_label("moka");
let l2 = TestBackend::with_label("redis");
let backend = CompositionBackend::new(l1.clone(), l2, TestOffload).label("cache");
let key = CacheKey::from_str("test", "metrics1");
let value = CacheValue::new(
CachedData {
value: "value1".to_string(),
},
Some(Utc::now() + chrono::Duration::seconds(60)),
None,
);
let mut ctx: BoxContext = CacheContext::default().boxed();
l1.set::<MockResponse>(&key, &value, &mut ctx)
.await
.unwrap();
let mut ctx: BoxContext = CacheContext::default().boxed();
let result = backend.get::<MockResponse>(&key, &mut ctx).await.unwrap();
assert_eq!(result.unwrap().data().value, "value1");
assert_eq!(ctx.status(), CacheStatus::Hit);
assert_eq!(ctx.source(), &ResponseSource::Backend("cache.moka".into()));
}
#[tokio::test]
async fn test_l2_hit_with_refill_via_set() {
use hitbox_core::ReadMode;
let l1 = TestBackend::with_label("moka");
let l2 = TestBackend::with_label("redis");
let key = CacheKey::from_str("test", "metrics2");
let value = CacheValue::new(
CachedData {
value: "from_l2".to_string(),
},
Some(Utc::now() + chrono::Duration::seconds(60)),
None,
);
let backend = CompositionBackend::new(l1.clone(), l2.clone(), TestOffload)
.label("cache")
.refill(RefillPolicy::Always);
let mut ctx: BoxContext = CacheContext::default().boxed();
backend
.set::<MockResponse>(&key, &value, &mut ctx)
.await
.unwrap();
l1.store.lock().unwrap().clear();
let mut ctx: BoxContext = CacheContext::default().boxed();
let result = backend.get::<MockResponse>(&key, &mut ctx).await.unwrap();
let cached_value = result.unwrap();
assert_eq!(cached_value.data().value, "from_l2");
assert_eq!(ctx.status(), CacheStatus::Hit);
assert_eq!(ctx.source(), &ResponseSource::Backend("cache.redis".into()));
assert_eq!(ctx.read_mode(), ReadMode::Refill);
backend
.set::<MockResponse>(&key, &cached_value, &mut ctx)
.await
.unwrap();
let mut ctx: BoxContext = CacheContext::default().boxed();
let result = backend.get::<MockResponse>(&key, &mut ctx).await.unwrap();
assert_eq!(result.unwrap().data().value, "from_l2");
assert_eq!(ctx.source(), &ResponseSource::Backend("cache.moka".into()));
}
#[tokio::test]
async fn test_nested_composition_status() {
let l1 = TestBackend::with_label("moka");
let l2 = TestBackend::with_label("redis");
let l3 = TestBackend::with_label("disk");
let inner = CompositionBackend::new(l1.clone(), l2.clone(), TestOffload).label("inner");
let outer = CompositionBackend::new(inner, l3.clone(), TestOffload).label("outer");
let key = CacheKey::from_str("test", "nested_metrics");
let value = CacheValue::new(
CachedData {
value: "nested".to_string(),
},
Some(Utc::now() + chrono::Duration::seconds(60)),
None,
);
let mut ctx: BoxContext = CacheContext::default().boxed();
l1.set::<MockResponse>(&key, &value, &mut ctx)
.await
.unwrap();
let mut ctx: BoxContext = CacheContext::default().boxed();
let result = outer.get::<MockResponse>(&key, &mut ctx).await.unwrap();
assert_eq!(result.unwrap().data().value, "nested");
assert_eq!(ctx.status(), CacheStatus::Hit);
assert_eq!(
ctx.source(),
&ResponseSource::Backend("outer.inner.moka".into())
);
}
}