use super::chain::{ChainCache, ChainLink, ChainReadStrategy};
use crate::backend::CacheBackend;
use crate::error::{OxCacheError, OxCacheResult};
#[cfg(feature = "invalidation")]
use crate::features::invalidation::InvalidationBus;
use std::sync::Arc;
use std::time::Duration;
pub type BackendDecorator =
Arc<dyn Fn(Arc<dyn CacheBackend>) -> Arc<dyn CacheBackend> + Send + Sync>;
#[derive(Default)]
pub struct L1Builder {
capacity: Option<u64>,
ttl: Option<Duration>,
tti: Option<Duration>,
kind: L1Kind,
decorators: Vec<BackendDecorator>,
score: u8,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum L1Kind {
#[default]
Moka,
DashMap,
}
impl L1Builder {
pub fn new() -> Self {
Self::default()
}
pub fn capacity(mut self, capacity: u64) -> Self {
self.capacity = Some(capacity);
self
}
pub fn ttl(mut self, ttl: Duration) -> Self {
self.ttl = Some(ttl);
self
}
pub fn tti(mut self, tti: Duration) -> Self {
self.tti = Some(tti);
self
}
pub fn dashmap(mut self) -> Self {
self.kind = L1Kind::DashMap;
self
}
pub fn moka(mut self) -> Self {
self.kind = L1Kind::Moka;
self
}
pub fn decorate(
mut self,
f: impl Fn(Arc<dyn CacheBackend>) -> Arc<dyn CacheBackend> + Send + Sync + 'static,
) -> Self {
self.decorators.push(Arc::new(f));
self
}
pub fn score(mut self, score: u8) -> Self {
self.score = score;
self
}
pub fn build(self) -> Arc<dyn CacheBackend> {
let base: Arc<dyn CacheBackend> = match self.kind {
L1Kind::Moka => {
let mut builder = crate::backend::MokaMemoryBackend::builder()
.capacity(self.capacity.unwrap_or(10_000));
if let Some(ttl) = self.ttl {
builder = builder.ttl(ttl);
}
if let Some(tti) = self.tti {
builder = builder.time_to_idle(tti);
}
Arc::new(builder.build())
}
L1Kind::DashMap => {
let mut builder = crate::backend::DashMapMemoryBackend::builder();
if let Some(capacity) = self.capacity {
builder = builder.capacity(capacity as usize);
}
if let Some(ttl) = self.ttl {
builder = builder.default_ttl(ttl);
}
Arc::new(builder.build())
}
};
self.decorators.into_iter().fold(base, |acc, d| d(acc))
}
pub fn get_score(&self) -> u8 {
self.score
}
}
#[derive(Default)]
pub struct L2Builder {
backend: Option<Arc<dyn CacheBackend>>,
#[cfg(feature = "redis")]
redis_url: Option<String>,
decorators: Vec<BackendDecorator>,
score: u8,
persistent: bool,
}
impl L2Builder {
pub fn new() -> Self {
Self {
score: 50,
..Default::default()
}
}
pub fn custom(mut self, backend: Arc<dyn CacheBackend>) -> Self {
self.backend = Some(backend);
self
}
#[cfg(feature = "redis")]
pub fn redis(mut self, url: &str) -> Self {
self.redis_url = Some(url.to_string());
self
}
pub fn decorate(
mut self,
f: impl Fn(Arc<dyn CacheBackend>) -> Arc<dyn CacheBackend> + Send + Sync + 'static,
) -> Self {
self.decorators.push(Arc::new(f));
self
}
pub fn score(mut self, score: u8) -> Self {
self.score = score;
self
}
pub fn persistent(mut self, persistent: bool) -> Self {
self.persistent = persistent;
self
}
pub async fn build(self) -> OxCacheResult<Arc<dyn CacheBackend>> {
let base: Arc<dyn CacheBackend> = if let Some(backend) = self.backend {
backend
} else {
#[cfg(feature = "redis")]
{
match &self.redis_url {
Some(url) => Arc::new(crate::backend::RedisBackend::new(url).await?),
None => {
return Err(OxCacheError::InvalidInput(
"L2Builder requires .custom(backend) or .redis(url)".to_string(),
));
}
}
}
#[cfg(not(feature = "redis"))]
{
return Err(OxCacheError::InvalidInput(
"L2Builder requires .custom(backend); enable the `redis` feature for .redis(url)"
.to_string(),
));
}
};
Ok(self.decorators.into_iter().fold(base, |acc, d| d(acc)))
}
pub fn get_score(&self) -> u8 {
self.score
}
}
#[derive(Default)]
pub struct ChainBuilder {
l1: Option<L1Builder>,
l2: Option<L2Builder>,
extra: Vec<(Arc<dyn CacheBackend>, u8, bool, &'static str)>,
backfill_enabled: bool,
read_strategy: ChainReadStrategy,
default_ttl: Option<Duration>,
#[cfg(feature = "invalidation")]
invalidation_bus: Option<Arc<InvalidationBus>>,
}
impl ChainBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn l1(mut self, l1: L1Builder) -> Self {
self.l1 = Some(l1);
self
}
pub fn l2(mut self, l2: L2Builder) -> Self {
self.l2 = Some(l2);
self
}
pub fn extra_backend(
mut self,
backend: Arc<dyn CacheBackend>,
score: u8,
is_persistent: bool,
name: &'static str,
) -> Self {
self.extra.push((backend, score, is_persistent, name));
self
}
pub fn enable_backfill(mut self) -> Self {
self.backfill_enabled = true;
self
}
pub fn enable_race_read(mut self) -> Self {
self.read_strategy = ChainReadStrategy::Race;
self
}
pub fn read_strategy(mut self, strategy: ChainReadStrategy) -> Self {
self.read_strategy = strategy;
self
}
pub fn default_time_to_live(mut self, ttl: Duration) -> Self {
self.default_ttl = Some(ttl);
self
}
#[cfg(feature = "invalidation")]
pub fn with_invalidation(mut self, bus: Arc<InvalidationBus>) -> Self {
self.invalidation_bus = Some(bus);
self
}
pub async fn build(self) -> OxCacheResult<ChainCache> {
let mut links: Vec<ChainLink> = Vec::new();
if let Some(l1) = self.l1 {
let score = l1.get_score();
links.push(ChainLink::from_arc(l1.build(), score, false, "l1"));
}
if let Some(l2) = self.l2 {
let score = l2.get_score();
let persistent = l2.persistent;
links.push(ChainLink::from_arc(
l2.build().await?,
score,
persistent,
"l2",
));
}
for (backend, score, persistent, name) in self.extra {
links.push(ChainLink::from_arc(backend, score, persistent, name));
}
if links.is_empty() {
return Err(OxCacheError::InvalidInput(
"ChainBuilder requires at least one layer: add .l1(...) and/or .l2(...)"
.to_string(),
));
}
let mut builder = ChainCache::builder().links(links);
if self.backfill_enabled {
builder = builder.enable_backfill();
}
if self.read_strategy != ChainReadStrategy::Sequential {
builder = builder.read_strategy(self.read_strategy);
}
if let Some(ttl) = self.default_ttl {
builder = builder.default_time_to_live(ttl);
}
#[cfg(feature = "invalidation")]
if let Some(bus) = self.invalidation_bus {
builder = builder.with_invalidation(bus);
}
Ok(builder.build())
}
}
#[cfg(feature = "invalidation")]
pub async fn tiered_with_invalidation(
l1: L1Builder,
l2: L2Builder,
bus: Arc<InvalidationBus>,
) -> OxCacheResult<ChainCache> {
ChainBuilder::new()
.l1(l1)
.l2(l2)
.with_invalidation(bus)
.build()
.await
}
#[cfg(test)]
mod tests;