use super::chain::{ChainCache, ChainLink};
use crate::backend::CacheBackend;
use crate::error::{OxCacheError, OxCacheResult};
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,
race_read_enabled: bool,
default_ttl: Option<Duration>,
}
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.race_read_enabled = true;
self
}
pub fn default_time_to_live(mut self, ttl: Duration) -> Self {
self.default_ttl = Some(ttl);
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.race_read_enabled {
builder = builder.enable_race_read();
}
if let Some(ttl) = self.default_ttl {
builder = builder.default_time_to_live(ttl);
}
Ok(builder.build())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::BackendKind;
use crate::backend::interface::{CacheConnector, CacheReader, CacheWriter};
#[tokio::test]
async fn l1_builder_moka_default() {
let backend = L1Builder::new().build();
assert_eq!(backend.backend_kind(), BackendKind::Moka);
assert_eq!(backend.capacity().await.unwrap(), 10_000);
}
#[tokio::test]
async fn l1_builder_capacity_ttl_and_dashmap() {
let backend = L1Builder::new()
.dashmap()
.capacity(64)
.ttl(Duration::from_secs(30))
.build();
assert_eq!(backend.backend_kind(), BackendKind::DashMap);
assert_eq!(backend.capacity().await.unwrap(), 64);
backend
.set(Arc::from("k"), Arc::new(b"v".to_vec()), None)
.await
.unwrap();
let ttl = backend.ttl("k").await.unwrap();
assert!(ttl.is_some(), "DashMap default_ttl 应生效");
}
#[tokio::test]
async fn l1_builder_applies_decorators() {
let backend = L1Builder::new()
.decorate(|inner| Arc::new(TracingProbe { inner }))
.decorate(|inner| Arc::new(TracingProbe { inner }))
.build();
backend
.set(Arc::from("k"), Arc::new(b"v".to_vec()), None)
.await
.unwrap();
assert_eq!(backend.get("k").await.unwrap(), Some(b"v".to_vec()));
}
struct TracingProbe {
inner: Arc<dyn CacheBackend>,
}
#[async_trait::async_trait]
impl CacheReader for TracingProbe {
async fn get(&self, key: &str) -> OxCacheResult<Option<Vec<u8>>> {
self.inner.get(key).await
}
async fn exists(&self, key: &str) -> OxCacheResult<bool> {
self.inner.exists(key).await
}
async fn ttl(&self, key: &str) -> OxCacheResult<Option<Duration>> {
self.inner.ttl(key).await
}
async fn len(&self) -> OxCacheResult<u64> {
self.inner.len().await
}
async fn capacity(&self) -> OxCacheResult<u64> {
self.inner.capacity().await
}
async fn stats(&self) -> OxCacheResult<std::collections::HashMap<String, String>> {
self.inner.stats().await
}
}
#[async_trait::async_trait]
impl CacheWriter for TracingProbe {
async fn set(
&self,
key: Arc<str>,
value: Arc<Vec<u8>>,
ttl: Option<Duration>,
) -> OxCacheResult<()> {
self.inner.set(key, value, ttl).await
}
async fn delete(&self, key: &str) -> OxCacheResult<()> {
self.inner.delete(key).await
}
async fn clear(&self) -> OxCacheResult<()> {
self.inner.clear().await
}
async fn expire(&self, key: &str, ttl: Duration) -> OxCacheResult<bool> {
self.inner.expire(key, ttl).await
}
}
#[async_trait::async_trait]
impl CacheConnector for TracingProbe {
async fn health_check(&self) -> OxCacheResult<()> {
self.inner.health_check().await
}
async fn shutdown(&self) {
self.inner.shutdown().await;
}
fn backend_kind(&self) -> BackendKind {
BackendKind::Unknown
}
}
#[tokio::test]
async fn l2_builder_custom_backend() {
let inner: Arc<dyn CacheBackend> =
Arc::new(crate::backend::MockBackend::new("mock-l2", 50, true));
let l2 = L2Builder::new().custom(inner).build().await.unwrap();
assert!(l2.exists("nothing").await.unwrap().eq(&false));
}
#[tokio::test]
async fn l2_builder_without_backend_is_error() {
let err = match L2Builder::new().build().await {
Err(e) => e,
Ok(_) => panic!("无后端必须报错"),
};
assert!(matches!(err, OxCacheError::InvalidInput(_)));
}
#[tokio::test]
async fn chain_builder_assembles_l1_l2() {
let l2_backend: Arc<dyn CacheBackend> =
Arc::new(crate::backend::MockBackend::new("mock-l2", 50, false));
l2_backend
.set(Arc::from("user:1"), Arc::new(b"from-l2".to_vec()), None)
.await
.unwrap();
let chain = ChainBuilder::new()
.l1(L1Builder::new().capacity(100))
.l2(L2Builder::new().custom(l2_backend))
.enable_backfill()
.build()
.await
.unwrap();
let value = chain.get("user:1").await.unwrap();
assert_eq!(value, Some(b"from-l2".to_vec()));
assert_eq!(chain.len(), 2);
}
#[tokio::test]
async fn chain_builder_requires_at_least_one_layer() {
let err = match ChainBuilder::new().build().await {
Err(e) => e,
Ok(_) => panic!("空链必须报错"),
};
assert!(matches!(err, OxCacheError::InvalidInput(_)));
assert!(err.to_string().contains(".l1("));
}
#[tokio::test]
async fn chain_builder_l1_only_works() {
let chain = ChainBuilder::new()
.l1(L1Builder::new())
.build()
.await
.unwrap();
chain.set("k", b"v".to_vec(), None).await.unwrap();
assert_eq!(chain.get("k").await.unwrap(), Some(b"v".to_vec()));
}
}