use crate::backend::BackendScore;
use crate::backend::{
AtomicCacheWriter, BackendKind, CacheBackend, CacheConnector, CacheReader, CacheWriter, SyncCacheBackend,
};
use crate::core::EventPublisher;
use crate::error::{OxCacheError, OxCacheResult};
use crate::infra::metrics::unified::GLOBAL_UNIFIED_METRICS;
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::OnceLock;
use std::time::Duration;
mod builder;
#[cfg(test)]
mod tests;
pub use self::builder::ChainCacheBuilder;
#[derive(Clone)]
pub struct ChainLink {
backend: Arc<dyn CacheBackend>,
backend_sync: Option<Arc<dyn SyncCacheBackend>>,
score: u8,
is_persistent: bool,
name: &'static str,
}
impl ChainLink {
pub fn new<B>(backend: B, score: u8, is_persistent: bool, name: &'static str) -> Self
where
B: CacheBackend + BackendScore + 'static,
{
Self {
backend: Arc::new(backend),
backend_sync: None,
score,
is_persistent,
name,
}
}
pub fn from_backend<B>(backend: B) -> Self
where
B: CacheBackend + BackendScore + 'static,
{
let score = backend.score();
let is_persistent = backend.is_persistent();
let name = backend.backend_name();
Self {
backend: Arc::new(backend),
backend_sync: None,
score,
is_persistent,
name,
}
}
pub fn from_sync_backend<B>(backend: B) -> Self
where
B: CacheBackend + BackendScore + SyncCacheBackend + 'static,
{
let score = backend.score();
let is_persistent = backend.is_persistent();
let name = backend.backend_name();
let arc = Arc::new(backend);
let sync_arc: Arc<dyn SyncCacheBackend> = arc.clone();
Self {
backend: arc,
backend_sync: Some(sync_arc),
score,
is_persistent,
name,
}
}
pub fn backend(&self) -> &Arc<dyn CacheBackend> {
&self.backend
}
pub fn try_as_sync_backend(&self) -> Option<Arc<dyn SyncCacheBackend>> {
self.backend_sync.clone()
}
pub fn score(&self) -> u8 {
self.score
}
pub fn is_persistent(&self) -> bool {
self.is_persistent
}
pub fn name(&self) -> &'static str {
self.name
}
}
impl std::fmt::Debug for ChainLink {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ChainLink")
.field("score", &self.score)
.field("is_persistent", &self.is_persistent)
.field("name", &self.name)
.finish()
}
}
pub struct ChainCache {
links: Vec<ChainLink>,
backfill_enabled: bool,
race_read_enabled: bool,
default_ttl: Option<Duration>,
sync_backends: OnceLock<Option<Vec<Arc<dyn SyncCacheBackend>>>>,
event_publisher: Option<Arc<dyn EventPublisher>>,
}
impl ChainCache {
pub fn new(links: Vec<ChainLink>) -> Self {
Self::builder().links(links).build()
}
pub fn builder() -> ChainCacheBuilder {
ChainCacheBuilder::default()
}
pub fn links(&self) -> &[ChainLink] {
&self.links
}
pub fn len(&self) -> usize {
self.links.len()
}
pub fn is_empty(&self) -> bool {
self.links.is_empty()
}
pub fn get_by_score(&self, score: u8) -> Option<&ChainLink> {
self.links.iter().find(|link| link.score() == score)
}
pub fn highest_score_backend(&self) -> Option<&ChainLink> {
self.links.first()
}
fn emit_backend_error(&self, key: &str, backend: &str, error: &OxCacheError) {
if let Some(publisher) = &self.event_publisher {
let _ = publisher.publish_error(Some(key.to_string()), format!("backend {}: {}", backend, error));
}
}
pub fn lowest_score_backend(&self) -> Option<&ChainLink> {
self.links.last()
}
pub async fn set(&self, key: &str, value: Vec<u8>, ttl: Option<Duration>) -> OxCacheResult<()> {
let key = Arc::from(key);
let value = Arc::new(value);
CacheWriter::set(self, key, value, ttl).await
}
pub fn persistent_backends(&self) -> Vec<&ChainLink> {
self.links.iter().filter(|link| link.is_persistent()).collect()
}
pub fn non_persistent_backends(&self) -> Vec<&ChainLink> {
self.links.iter().filter(|link| !link.is_persistent()).collect()
}
async fn read_from_chain(&self, key: &str) -> OxCacheResult<Option<Vec<u8>>> {
if self.race_read_enabled {
return self.race_read_from_chain(key).await;
}
let mut all_failed = true;
let mut last_err: Option<OxCacheError> = None;
for (index, link) in self.links.iter().enumerate() {
match link.backend().get(key).await {
Ok(Some(value)) => {
if self.backfill_enabled && index > 0 {
let original_ttl = self.links[index].backend().ttl(key).await.ok().flatten();
let value = Arc::new(value);
self.backfill_to_higher_backends(Arc::from(key), value.clone(), index, original_ttl)
.await;
return Ok(Some(Arc::try_unwrap(value).unwrap_or_else(|arc| (*arc).clone())));
}
return Ok(Some(value));
}
Ok(None) => {
all_failed = false; continue;
}
Err(e) => {
self.emit_backend_error(key, link.name(), &e);
last_err = Some(e);
continue;
}
}
}
if all_failed && self.links.is_empty() {
return Ok(None);
}
if all_failed {
return Err(last_err
.unwrap_or_else(|| OxCacheError::Operation("All backends failed during sequential read".to_string())));
}
Ok(None)
}
async fn race_read_from_chain(&self, key: &str) -> OxCacheResult<Option<Vec<u8>>> {
if self.links.is_empty() {
return Ok(None);
}
let mut set = tokio::task::JoinSet::new();
for (index, link) in self.links.iter().enumerate() {
let backend = link.backend().clone();
let key = key.to_string();
set.spawn(async move { (index, backend.get(&key).await) });
}
let mut errs: Vec<(&'static str, OxCacheError)> = Vec::new();
let mut hits: Vec<(usize, Vec<u8>)> = Vec::new();
while let Some(joined) = set.join_next().await {
match joined {
Ok((index, Ok(Some(value)))) => hits.push((index, value)),
Ok((_index, Ok(None))) => {}
Ok((index, Err(e))) => {
self.emit_backend_error(key, self.links[index].name(), &e);
errs.push((self.links[index].name(), e));
}
Err(e) => errs.push(("unknown", OxCacheError::Operation(e.to_string()))),
}
}
if let Some((index, value)) = hits.into_iter().min_by_key(|(i, _)| *i) {
if self.backfill_enabled && index > 0 {
let original_ttl = self.links[index].backend().ttl(key).await.ok().flatten();
self.backfill_to_higher_backends(Arc::from(key), Arc::new(value.clone()), index, original_ttl)
.await;
}
return Ok(Some(value));
}
if errs.len() == self.links.len() {
return Err(OxCacheError::Operation(
"All backends failed during race read".to_string(),
));
}
Ok(None)
}
async fn backfill_to_higher_backends(
&self,
key: Arc<str>,
value: Arc<Vec<u8>>,
from_index: usize,
ttl: Option<Duration>,
) {
for link in &self.links[..from_index] {
let backend = link.backend().clone();
match backend.set(key.clone(), value.clone(), ttl).await {
Ok(()) => {
GLOBAL_UNIFIED_METRICS.record_backfill_success();
}
Err(e) => {
GLOBAL_UNIFIED_METRICS.record_backfill_failed();
self.emit_backend_error(&key, link.name(), &e);
}
}
}
}
async fn write_to_all_backends(
&self,
key: &Arc<str>,
value: &Arc<Vec<u8>>,
ttl: Option<Duration>,
) -> OxCacheResult<()> {
let count = self.links.len();
if count == 0 {
return Ok(());
}
let effective_ttl = ttl.or(self.default_ttl);
let mut errors: Vec<(&'static str, OxCacheError)> = Vec::new();
let mut set = tokio::task::JoinSet::new();
for link in &self.links {
let backend = link.backend().clone();
let name = link.name();
let key = key.clone();
let value = value.clone();
set.spawn(async move { (name, backend.set(key, value, effective_ttl).await) });
}
while let Some(joined) = set.join_next().await {
match joined {
Ok((_name, Ok(()))) => {}
Ok((name, Err(e))) => errors.push((name, e)),
Err(e) => errors.push(("unknown", OxCacheError::Operation(e.to_string()))),
}
}
for (name, e) in &errors {
self.emit_backend_error(key, name, e);
}
if errors.len() == self.links.len() {
return Err(OxCacheError::Operation("All backends failed to write".to_string()));
}
Ok(())
}
async fn delete_from_all_backends(&self, key: &str) -> OxCacheResult<()> {
let count = self.links.len();
if count == 0 {
return Ok(());
}
let mut set = tokio::task::JoinSet::new();
for link in &self.links {
let backend = link.backend().clone();
let name = link.name();
let key = key.to_string();
set.spawn(async move { (name, backend.delete(&key).await) });
}
let mut errors: Vec<(&'static str, OxCacheError)> = Vec::new();
while let Some(joined) = set.join_next().await {
match joined {
Ok((_name, Ok(()))) => {}
Ok((name, Err(e))) => {
self.emit_backend_error(key, name, &e);
errors.push((name, e));
}
Err(e) => errors.push(("unknown", OxCacheError::Operation(e.to_string()))),
}
}
if errors.len() == self.links.len() {
return Err(OxCacheError::Operation(format!(
"All backends failed to delete: {:?}",
errors
)));
}
Ok(())
}
fn collect_sync_backends(&self) -> OxCacheResult<&[Arc<dyn SyncCacheBackend>]> {
let cached = self.sync_backends.get_or_init(|| {
self.links
.iter()
.map(|link| link.try_as_sync_backend())
.collect::<Option<Vec<_>>>()
});
cached.as_deref().ok_or_else(|| {
OxCacheError::NotSupported("chain sync API requires all links to support SyncCacheBackend".to_string())
})
}
pub fn get_sync(&self, key: &str) -> OxCacheResult<Option<Vec<u8>>> {
let sync_backends = self.collect_sync_backends()?;
for backend in sync_backends {
match backend.get(key) {
Ok(Some(value)) => return Ok(Some(value)),
Ok(None) => continue,
Err(_) => continue,
}
}
Ok(None)
}
pub fn set_sync(&self, key: &str, value: Vec<u8>, ttl: Option<Duration>) -> OxCacheResult<()> {
let sync_backends = self.collect_sync_backends()?;
if sync_backends.is_empty() {
return Err(OxCacheError::Operation("Chain has no backends".to_string()));
}
let effective_ttl = ttl.or(self.default_ttl);
let key_arc: Arc<str> = Arc::from(key);
let value_arc: Arc<Vec<u8>> = Arc::new(value);
let mut errors = Vec::new();
for backend in sync_backends.iter() {
if let Err(e) = backend.set(key_arc.clone(), value_arc.clone(), effective_ttl) {
errors.push(e);
}
}
if errors.len() == sync_backends.len() {
return Err(OxCacheError::Operation("All backends failed to write".to_string()));
}
Ok(())
}
pub fn delete_sync(&self, key: &str) -> OxCacheResult<()> {
let sync_backends = self.collect_sync_backends()?;
let mut errors = Vec::new();
for backend in sync_backends {
if let Err(e) = backend.delete(key) {
errors.push(e);
}
}
if errors.len() == sync_backends.len() && !sync_backends.is_empty() {
return Err(OxCacheError::Operation(format!(
"All backends failed to delete: {:?}",
errors
)));
}
Ok(())
}
}
#[async_trait]
impl CacheReader for ChainCache {
async fn get(&self, key: &str) -> OxCacheResult<Option<Vec<u8>>> {
if self.links.is_empty() {
return Ok(None);
}
self.read_from_chain(key).await
}
async fn exists(&self, key: &str) -> OxCacheResult<bool> {
for link in &self.links {
match link.backend().exists(key).await {
Ok(true) => return Ok(true),
Ok(false) => continue,
Err(_) => continue,
}
}
Ok(false)
}
async fn ttl(&self, key: &str) -> OxCacheResult<Option<Duration>> {
for link in &self.links {
match link.backend().ttl(key).await {
Ok(Some(ttl)) => return Ok(Some(ttl)),
Ok(None) => continue,
Err(_) => continue,
}
}
Ok(None)
}
async fn len(&self) -> OxCacheResult<u64> {
if let Some(link) = self.links.first() {
link.backend().len().await
} else {
Ok(0)
}
}
async fn is_empty(&self) -> OxCacheResult<bool> {
if let Some(link) = self.links.first() {
link.backend().is_empty().await
} else {
Ok(true)
}
}
async fn capacity(&self) -> OxCacheResult<u64> {
if let Some(link) = self.links.first() {
link.backend().capacity().await
} else {
Ok(0)
}
}
async fn stats(&self) -> OxCacheResult<HashMap<String, String>> {
let mut stats = HashMap::new();
stats.insert("type".to_string(), "chain".to_string());
stats.insert("backend_count".to_string(), self.links.len().to_string());
for (index, link) in self.links.iter().enumerate() {
stats.insert(format!("backend_{}_name", index), link.name().to_string());
stats.insert(format!("backend_{}_score", index), link.score().to_string());
}
Ok(stats)
}
async fn keys(&self, pattern: &str) -> OxCacheResult<Vec<String>> {
let mut seen = std::collections::HashSet::new();
let mut result = Vec::new();
for link in &self.links {
if let Ok(keys) = link.backend().keys(pattern).await {
for k in keys {
if seen.insert(k.clone()) {
result.push(k);
}
}
}
}
Ok(result)
}
}
#[async_trait]
impl CacheWriter for ChainCache {
async fn set(&self, key: Arc<str>, value: Arc<Vec<u8>>, ttl: Option<Duration>) -> OxCacheResult<()> {
if self.links.is_empty() {
return Err(OxCacheError::Operation("Chain has no backends".to_string()));
}
self.write_to_all_backends(&key, &value, ttl).await
}
async fn delete(&self, key: &str) -> OxCacheResult<()> {
if self.links.is_empty() {
return Ok(());
}
self.delete_from_all_backends(key).await
}
async fn clear(&self) -> OxCacheResult<()> {
let mut errors = Vec::new();
for link in &self.links {
if let Err(e) = link.backend().clear().await {
errors.push((link.name(), e));
}
}
if errors.len() == self.links.len() && !self.links.is_empty() {
return Err(OxCacheError::Operation(format!(
"All backends failed to clear: {:?}",
errors
)));
}
Ok(())
}
async fn expire(&self, key: &str, ttl: Duration) -> OxCacheResult<bool> {
let mut any_success = false;
for link in &self.links {
match link.backend().expire(key, ttl).await {
Ok(true) => any_success = true,
_ => continue,
}
}
Ok(any_success)
}
}
#[async_trait]
impl CacheConnector for ChainCache {
async fn health_check(&self) -> OxCacheResult<()> {
if self.links.is_empty() {
return Ok(());
}
const HEALTH_CHECK_TIMEOUT: Duration = Duration::from_secs(5);
let mut set = tokio::task::JoinSet::new();
for link in &self.links {
let backend = link.backend().clone();
let name = link.name();
set.spawn(async move {
let result = tokio::time::timeout(HEALTH_CHECK_TIMEOUT, backend.health_check()).await;
(name, result)
});
}
let mut failures = Vec::new();
while let Some(joined) = set.join_next().await {
match joined {
Ok((_name, Ok(Ok(())))) => {}
Ok((name, Ok(Err(e)))) => failures.push((name, e)),
Ok((name, Err(_))) => failures.push((
name,
OxCacheError::Timeout("health_check timed out after 5s".to_string()),
)),
Err(e) => failures.push(("unknown", OxCacheError::Operation(e.to_string()))),
}
}
if failures.is_empty() {
Ok(())
} else {
Err(OxCacheError::Operation(format!(
"health_check failed for {} backend(s): {:?}",
failures.len(),
failures
)))
}
}
async fn shutdown(&self) {
for link in &self.links {
link.backend().shutdown().await;
}
}
fn backend_kind(&self) -> BackendKind {
BackendKind::Chain
}
}
#[async_trait]
impl AtomicCacheWriter for ChainCache {
async fn incr(&self, key: &str, delta: i64, ttl: Option<Duration>) -> OxCacheResult<i64> {
let writer = self
.links
.first()
.and_then(|link| link.backend().as_atomic_writer())
.ok_or_else(|| {
OxCacheError::NotSupported("incr: no link in chain implements AtomicCacheWriter".to_string())
})?;
writer.incr(key, delta, ttl).await
}
async fn compare_and_swap(
&self,
key: &str,
expected: Option<&[u8]>,
new: Vec<u8>,
ttl: Option<Duration>,
) -> OxCacheResult<bool> {
let writer = self
.links
.first()
.and_then(|link| link.backend().as_atomic_writer())
.ok_or_else(|| {
OxCacheError::NotSupported(
"compare_and_swap: no link in chain implements AtomicCacheWriter".to_string(),
)
})?;
writer.compare_and_swap(key, expected, new, ttl).await
}
async fn set_if_absent(&self, key: &str, value: Vec<u8>, ttl: Option<Duration>) -> OxCacheResult<bool> {
let writer = self
.links
.first()
.and_then(|link| link.backend().as_atomic_writer())
.ok_or_else(|| {
OxCacheError::NotSupported("set_if_absent: no link in chain implements AtomicCacheWriter".to_string())
})?;
writer.set_if_absent(key, value, ttl).await
}
}