use async_trait::async_trait;
use futures::future::{Either, select};
use hitbox_core::{BoxContext, CacheContext, CacheKey, CacheValue, Offload};
use std::future::Future;
use super::{CompositionReadPolicy, ReadResult};
use crate::composition::CompositionLayer;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum RaceLoserPolicy {
#[default]
Offload,
Drop,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct RaceReadPolicy {
loser_policy: RaceLoserPolicy,
}
impl RaceReadPolicy {
pub fn new() -> Self {
Self::default()
}
pub fn loser_policy(mut self, policy: RaceLoserPolicy) -> Self {
self.loser_policy = policy;
self
}
}
#[async_trait]
impl CompositionReadPolicy for RaceReadPolicy {
#[tracing::instrument(skip(self, key, read_l1, read_l2, offload), level = "trace")]
async fn execute_with<T, E, F1, F2, Fut1, Fut2, O>(
&self,
key: CacheKey,
read_l1: F1,
read_l2: F2,
offload: &O,
) -> Result<ReadResult<T>, E>
where
T: Send + 'static,
E: Send + std::fmt::Debug + 'static,
F1: FnOnce(CacheKey) -> Fut1 + Send,
F2: FnOnce(CacheKey) -> Fut2 + Send,
Fut1: Future<Output = (Result<Option<CacheValue<T>>, E>, BoxContext)> + Send + 'static,
Fut2: Future<Output = (Result<Option<CacheValue<T>>, E>, BoxContext)> + Send + 'static,
O: Offload<'static>,
{
let l1_fut = Box::pin(read_l1(key.clone()));
let l2_fut = Box::pin(read_l2(key));
match select(l1_fut, l2_fut).await {
Either::Left(((l1_result, l1_ctx), l2_fut)) => {
if let Ok(Some(value)) = l1_result {
tracing::trace!("L1 hit (won race)");
match self.loser_policy {
RaceLoserPolicy::Offload => {
offload.register(
hitbox_core::OffloadKey::auto("race_read_l2_loser"),
async move {
let _ = l2_fut.await;
},
);
}
RaceLoserPolicy::Drop => {
drop(l2_fut);
}
}
return Ok(ReadResult {
value: Some(value),
source: CompositionLayer::L1,
context: l1_ctx,
});
}
tracing::trace!("L1 completed first without hit, waiting for L2");
let (l2_result, l2_ctx) = l2_fut.await;
match (l1_result, l2_result) {
(Ok(Some(value)), _) => {
tracing::trace!("L1 hit");
Ok(ReadResult {
value: Some(value),
source: CompositionLayer::L1,
context: l1_ctx,
})
}
(_, Ok(Some(value))) => {
tracing::trace!("L2 hit");
Ok(ReadResult {
value: Some(value),
source: CompositionLayer::L2,
context: l2_ctx,
})
}
(Ok(None), Ok(None)) => {
tracing::trace!("Both layers miss");
Ok(ReadResult {
value: None,
source: CompositionLayer::L2,
context: CacheContext::default().boxed(),
})
}
(Err(e1), Err(e2)) => {
tracing::error!(l1_error = ?e1, l2_error = ?e2, "Both layers failed");
Err(e2)
}
(Ok(None), Err(e)) | (Err(e), Ok(None)) => {
tracing::warn!(error = ?e, "One layer failed, one missed");
Ok(ReadResult {
value: None,
source: CompositionLayer::L2,
context: CacheContext::default().boxed(),
})
}
}
}
Either::Right(((l2_result, l2_ctx), l1_fut)) => {
if let Ok(Some(value)) = l2_result {
tracing::trace!("L2 hit (won race)");
match self.loser_policy {
RaceLoserPolicy::Offload => {
offload.register(
hitbox_core::OffloadKey::auto("race_read_l1_loser"),
async move {
let _ = l1_fut.await;
},
);
}
RaceLoserPolicy::Drop => {
drop(l1_fut);
}
}
return Ok(ReadResult {
value: Some(value),
source: CompositionLayer::L2,
context: l2_ctx,
});
}
tracing::trace!("L2 completed first without hit, waiting for L1");
let (l1_result, l1_ctx) = l1_fut.await;
match (l1_result, l2_result) {
(Ok(Some(value)), _) => {
tracing::trace!("L1 hit");
Ok(ReadResult {
value: Some(value),
source: CompositionLayer::L1,
context: l1_ctx,
})
}
(_, Ok(Some(value))) => {
tracing::trace!("L2 hit");
Ok(ReadResult {
value: Some(value),
source: CompositionLayer::L2,
context: l2_ctx,
})
}
(Ok(None), Ok(None)) => {
tracing::trace!("Both layers miss");
Ok(ReadResult {
value: None,
source: CompositionLayer::L2,
context: CacheContext::default().boxed(),
})
}
(Err(e1), Err(e2)) => {
tracing::error!(l1_error = ?e1, l2_error = ?e2, "Both layers failed");
Err(e2)
}
(Ok(None), Err(e)) | (Err(e), Ok(None)) => {
tracing::warn!(error = ?e, "One layer failed, one missed");
Ok(ReadResult {
value: None,
source: CompositionLayer::L2,
context: CacheContext::default().boxed(),
})
}
}
}
}
}
}