use crate::error::OxCacheResult;
use async_trait::async_trait;
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackendKind {
Moka,
DashMap,
Redis,
Valkey,
Dragonfly,
Aerospike,
Chain,
Mock,
Unknown,
}
impl BackendKind {
pub fn is_memory(&self) -> bool {
matches!(self, BackendKind::Moka | BackendKind::DashMap | BackendKind::Mock)
}
pub fn is_distributed(&self) -> bool {
matches!(
self,
BackendKind::Redis | BackendKind::Valkey | BackendKind::Dragonfly | BackendKind::Aerospike
)
}
pub fn is_composite(&self) -> bool {
matches!(self, BackendKind::Chain)
}
}
#[async_trait]
pub trait CacheReader: Send + Sync + 'static {
async fn get(&self, key: &str) -> OxCacheResult<Option<Vec<u8>>>;
async fn exists(&self, key: &str) -> OxCacheResult<bool>;
async fn ttl(&self, key: &str) -> OxCacheResult<Option<Duration>>;
async fn len(&self) -> OxCacheResult<u64>;
async fn is_empty(&self) -> OxCacheResult<bool> {
Ok(self.len().await?.eq(&0))
}
async fn capacity(&self) -> OxCacheResult<u64>;
async fn stats(&self) -> OxCacheResult<std::collections::HashMap<String, String>>;
async fn get_many(&self, keys: &[String]) -> OxCacheResult<Vec<Option<Vec<u8>>>> {
let mut results = Vec::with_capacity(keys.len());
for key in keys {
results.push(self.get(key).await?);
}
Ok(results)
}
async fn keys(&self, pattern: &str) -> OxCacheResult<Vec<String>> {
let _ = pattern;
Ok(vec![])
}
}
pub type CacheSetItem = (Arc<str>, Arc<Vec<u8>>, Option<Duration>);
#[async_trait]
pub trait CacheWriter: Send + Sync + 'static {
async fn set(&self, key: Arc<str>, value: Arc<Vec<u8>>, ttl: Option<Duration>) -> OxCacheResult<()>;
async fn delete(&self, key: &str) -> OxCacheResult<()>;
async fn clear(&self) -> OxCacheResult<()>;
async fn expire(&self, key: &str, ttl: Duration) -> OxCacheResult<bool>;
async fn set_many(&self, items: &[CacheSetItem]) -> OxCacheResult<()> {
for (key, value, ttl) in items {
self.set(key.clone(), value.clone(), *ttl).await?;
}
Ok(())
}
async fn delete_many(&self, keys: &[String]) -> OxCacheResult<()> {
for key in keys {
self.delete(key).await?;
}
Ok(())
}
}
#[async_trait]
pub trait CacheConnector: Send + Sync + 'static {
async fn health_check(&self) -> OxCacheResult<()>;
async fn shutdown(&self);
fn backend_kind(&self) -> BackendKind;
#[cfg(feature = "lua-script")]
fn as_lua_executor(&self) -> Option<&dyn LuaExecutor> {
None
}
fn as_atomic_writer(&self) -> Option<&dyn AtomicCacheWriter> {
None
}
}
#[cfg(feature = "lua-script")]
#[async_trait]
pub trait LuaExecutor: Send + Sync {
async fn eval_lua(&self, script: &str, keys: &[&str], args: &[&str]) -> OxCacheResult<redis::Value>;
async fn eval_sha(&self, sha: &str, keys: &[&str], args: &[&str]) -> OxCacheResult<redis::Value>;
async fn script_load(&self, script: &str) -> OxCacheResult<String>;
}
#[async_trait]
pub trait AtomicCacheWriter: Send + Sync + 'static {
async fn incr(&self, key: &str, delta: i64, ttl: Option<Duration>) -> OxCacheResult<i64>;
async fn compare_and_swap(
&self,
key: &str,
expected: Option<&[u8]>,
new: Vec<u8>,
ttl: Option<Duration>,
) -> OxCacheResult<bool>;
async fn set_if_absent(&self, key: &str, value: Vec<u8>, ttl: Option<Duration>) -> OxCacheResult<bool>;
}
#[async_trait]
pub trait CacheBackend: CacheReader + CacheWriter + CacheConnector + 'static {}
#[async_trait]
impl<T: CacheReader + CacheWriter + CacheConnector + 'static> CacheBackend for T {}
pub trait SyncCacheReader: Send + Sync + 'static {
fn get(&self, key: &str) -> OxCacheResult<Option<Vec<u8>>>;
fn exists(&self, key: &str) -> OxCacheResult<bool>;
fn ttl(&self, key: &str) -> OxCacheResult<Option<Duration>>;
fn len(&self) -> OxCacheResult<u64>;
fn is_empty(&self) -> OxCacheResult<bool> {
Ok(self.len()? == 0)
}
fn capacity(&self) -> OxCacheResult<u64>;
fn stats(&self) -> OxCacheResult<std::collections::HashMap<String, String>>;
fn get_many(&self, keys: &[String]) -> OxCacheResult<Vec<Option<Vec<u8>>>> {
let mut results = Vec::with_capacity(keys.len());
for key in keys {
results.push(self.get(key)?);
}
Ok(results)
}
fn keys(&self, pattern: &str) -> OxCacheResult<Vec<String>> {
let _ = pattern;
Ok(vec![])
}
}
pub trait SyncCacheWriter: Send + Sync + 'static {
fn set(&self, key: Arc<str>, value: Arc<Vec<u8>>, ttl: Option<Duration>) -> OxCacheResult<()>;
fn delete(&self, key: &str) -> OxCacheResult<()>;
fn clear(&self) -> OxCacheResult<()>;
fn expire(&self, key: &str, ttl: Duration) -> OxCacheResult<bool>;
fn set_many(&self, items: &[CacheSetItem]) -> OxCacheResult<()> {
for (key, value, ttl) in items {
self.set(key.clone(), value.clone(), *ttl)?;
}
Ok(())
}
fn delete_many(&self, keys: &[String]) -> OxCacheResult<()> {
for key in keys {
self.delete(key)?;
}
Ok(())
}
}
pub trait SyncCacheConnector: Send + Sync + 'static {
fn health_check(&self) -> OxCacheResult<()>;
fn shutdown(&self);
fn backend_kind(&self) -> BackendKind;
}
pub trait SyncCacheBackend: SyncCacheReader + SyncCacheWriter + SyncCacheConnector + 'static {}
impl<T: SyncCacheReader + SyncCacheWriter + SyncCacheConnector + 'static> SyncCacheBackend for T {}
pub trait SyncAtomicCacheWriter: Send + Sync + 'static {
fn incr(&self, key: &str, delta: i64, ttl: Option<Duration>) -> OxCacheResult<i64>;
fn compare_and_swap(
&self,
key: &str,
expected: Option<&[u8]>,
new: Vec<u8>,
ttl: Option<Duration>,
) -> OxCacheResult<bool>;
fn set_if_absent(&self, key: &str, value: Vec<u8>, ttl: Option<Duration>) -> OxCacheResult<bool>;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::MockBackend;
#[tokio::test]
async fn test_mock_backend() {
let backend = MockBackend::new("mock", 50, false);
backend
.set(Arc::from("key1"), Arc::new(b"value1".to_vec()), None)
.await
.unwrap();
let value = backend.get("key1").await.unwrap();
assert_eq!(value, Some(b"value1".to_vec()));
assert!(backend.exists("key1").await.unwrap());
assert!(!backend.exists("key2").await.unwrap());
backend.delete("key1").await.unwrap();
assert!(!backend.exists("key1").await.unwrap());
backend.health_check().await.unwrap();
let stats = backend.stats().await.unwrap();
assert_eq!(stats.get("type"), Some(&"mock".to_string()));
}
#[tokio::test]
async fn test_isp_traits() {
let backend = MockBackend::new("mock", 50, false);
let reader: &dyn CacheReader = &backend;
assert!(reader.get("nonexistent").await.unwrap().is_none());
let writer: &dyn CacheWriter = &backend;
writer
.set(Arc::from("key"), Arc::new(b"value".to_vec()), None)
.await
.unwrap();
let connector: &dyn CacheConnector = &backend;
connector.health_check().await.unwrap();
assert_eq!(connector.backend_kind(), BackendKind::Mock);
}
#[test]
fn test_backend_kind_is_memory_moka() {
assert!(BackendKind::Moka.is_memory());
}
#[test]
fn test_backend_kind_is_memory_dashmap() {
assert!(BackendKind::DashMap.is_memory());
}
#[test]
fn test_backend_kind_is_memory_mock() {
assert!(BackendKind::Mock.is_memory());
}
#[test]
fn test_backend_kind_is_memory_redis_false() {
assert!(!BackendKind::Redis.is_memory());
}
#[test]
fn test_backend_kind_is_memory_chain_false() {
assert!(!BackendKind::Chain.is_memory());
}
#[test]
fn test_backend_kind_is_memory_unknown_false() {
assert!(!BackendKind::Unknown.is_memory());
}
#[test]
fn test_backend_kind_is_distributed_redis() {
assert!(BackendKind::Redis.is_distributed());
}
#[test]
fn test_backend_kind_is_distributed_moka_false() {
assert!(!BackendKind::Moka.is_distributed());
}
#[test]
fn test_backend_kind_is_distributed_dashmap_false() {
assert!(!BackendKind::DashMap.is_distributed());
}
#[test]
fn test_backend_kind_is_distributed_chain_false() {
assert!(!BackendKind::Chain.is_distributed());
}
#[test]
fn test_backend_kind_is_distributed_mock_false() {
assert!(!BackendKind::Mock.is_distributed());
}
#[test]
fn test_backend_kind_is_distributed_unknown_false() {
assert!(!BackendKind::Unknown.is_distributed());
}
#[test]
fn test_backend_kind_debug() {
let kind = BackendKind::Moka;
let debug_str = format!("{:?}", kind);
assert!(debug_str.contains("Moka"));
}
#[test]
fn test_backend_kind_clone() {
let kind = BackendKind::Redis;
let cloned = kind;
assert_eq!(kind, cloned);
}
#[test]
fn test_backend_kind_equality() {
assert_eq!(BackendKind::Moka, BackendKind::Moka);
assert_ne!(BackendKind::Moka, BackendKind::Redis);
}
#[tokio::test]
async fn test_cache_reader_is_empty_default() {
let backend = MockBackend::new("mock", 50, false);
let reader: &dyn CacheReader = &backend;
assert!(reader.is_empty().await.unwrap());
backend
.set(Arc::from("key1"), Arc::new(b"value1".to_vec()), None)
.await
.unwrap();
assert!(!reader.is_empty().await.unwrap());
}
#[tokio::test]
async fn test_cache_reader_get_many_default() {
let backend = MockBackend::new("mock", 50, false);
backend
.set(Arc::from("key1"), Arc::new(b"value1".to_vec()), None)
.await
.unwrap();
backend
.set(Arc::from("key2"), Arc::new(b"value2".to_vec()), None)
.await
.unwrap();
let reader: &dyn CacheReader = &backend;
let keys = vec!["key1".to_string(), "key2".to_string(), "key3".to_string()];
let results = reader.get_many(&keys).await.unwrap();
assert_eq!(results.len(), 3);
assert_eq!(results[0], Some(b"value1".to_vec()));
assert_eq!(results[1], Some(b"value2".to_vec()));
assert_eq!(results[2], None);
}
#[tokio::test]
async fn test_cache_reader_get_many_empty() {
let backend = MockBackend::new("mock", 50, false);
let reader: &dyn CacheReader = &backend;
let keys: Vec<String> = vec![];
let results = reader.get_many(&keys).await.unwrap();
assert!(results.is_empty());
}
#[tokio::test]
async fn test_cache_writer_set_many_default() {
let backend = MockBackend::new("mock", 50, false);
let writer: &dyn CacheWriter = &backend;
let items = vec![
(Arc::from("key1"), Arc::new(b"value1".to_vec()), None),
(Arc::from("key2"), Arc::new(b"value2".to_vec()), None),
];
writer.set_many(&items).await.unwrap();
assert!(backend.exists("key1").await.unwrap());
assert!(backend.exists("key2").await.unwrap());
}
#[tokio::test]
async fn test_cache_writer_delete_many_default() {
let backend = MockBackend::new("mock", 50, false);
backend
.set(Arc::from("key1"), Arc::new(b"value1".to_vec()), None)
.await
.unwrap();
backend
.set(Arc::from("key2"), Arc::new(b"value2".to_vec()), None)
.await
.unwrap();
let writer: &dyn CacheWriter = &backend;
let keys = vec!["key1".to_string(), "key2".to_string()];
writer.delete_many(&keys).await.unwrap();
assert!(!backend.exists("key1").await.unwrap());
assert!(!backend.exists("key2").await.unwrap());
}
#[tokio::test]
async fn test_cache_connector_backend_kind_mock() {
let backend = MockBackend::new("mock", 50, false);
let connector: &dyn CacheConnector = &backend;
assert_eq!(connector.backend_kind(), BackendKind::Mock);
}
#[tokio::test]
async fn test_cache_connector_shutdown() {
let backend = MockBackend::new("mock", 50, false);
let connector: &dyn CacheConnector = &backend;
connector.shutdown().await;
}
#[tokio::test]
async fn test_cache_backend_trait_object() {
let backend = MockBackend::new("mock", 50, false);
let backend_dyn: &dyn CacheBackend = &backend;
backend_dyn
.set(Arc::from("key"), Arc::new(b"value".to_vec()), None)
.await
.unwrap();
let value = backend_dyn.get("key").await.unwrap();
assert_eq!(value, Some(b"value".to_vec()));
}
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::Instant;
type MockSyncEntry = (Vec<u8>, Option<Instant>);
struct MockSyncBackend {
data: Arc<RwLock<HashMap<String, MockSyncEntry>>>,
capacity: u64,
}
impl MockSyncBackend {
fn new(capacity: u64) -> Self {
Self {
data: Arc::new(RwLock::new(HashMap::new())),
capacity,
}
}
}
impl SyncCacheReader for MockSyncBackend {
fn get(&self, key: &str) -> OxCacheResult<Option<Vec<u8>>> {
let data = self.data.read().unwrap();
if let Some((value, expires_at)) = data.get(key) {
if let Some(deadline) = expires_at {
if *deadline <= Instant::now() {
return Ok(None);
}
}
return Ok(Some(value.clone()));
}
Ok(None)
}
fn exists(&self, key: &str) -> OxCacheResult<bool> {
Ok(self.get(key)?.is_some())
}
fn ttl(&self, key: &str) -> OxCacheResult<Option<Duration>> {
let data = self.data.read().unwrap();
if let Some((_, Some(deadline))) = data.get(key) {
return Ok(deadline.checked_duration_since(Instant::now()));
}
Ok(None)
}
fn len(&self) -> OxCacheResult<u64> {
Ok(self.data.read().unwrap().len() as u64)
}
fn capacity(&self) -> OxCacheResult<u64> {
Ok(self.capacity)
}
fn stats(&self) -> OxCacheResult<HashMap<String, String>> {
let mut stats = HashMap::new();
stats.insert("type".to_string(), "mock_sync".to_string());
stats.insert("len".to_string(), self.len()?.to_string());
Ok(stats)
}
}
impl SyncCacheWriter for MockSyncBackend {
fn set(&self, key: Arc<str>, value: Arc<Vec<u8>>, ttl: Option<Duration>) -> OxCacheResult<()> {
let expires_at = ttl.map(|d| Instant::now() + d);
self.data
.write()
.unwrap()
.insert(key.to_string(), ((*value).clone(), expires_at));
Ok(())
}
fn delete(&self, key: &str) -> OxCacheResult<()> {
self.data.write().unwrap().remove(key);
Ok(())
}
fn clear(&self) -> OxCacheResult<()> {
self.data.write().unwrap().clear();
Ok(())
}
fn expire(&self, key: &str, ttl: Duration) -> OxCacheResult<bool> {
let mut data = self.data.write().unwrap();
if let Some(entry) = data.get_mut(key) {
entry.1 = Some(Instant::now() + ttl);
return Ok(true);
}
Ok(false)
}
}
impl SyncCacheConnector for MockSyncBackend {
fn health_check(&self) -> OxCacheResult<()> {
Ok(())
}
fn shutdown(&self) {
self.clear().ok();
}
fn backend_kind(&self) -> BackendKind {
BackendKind::Mock
}
}
#[test]
fn test_sync_cache_backend_trait_object_usable() {
let backend = MockSyncBackend::new(50);
let backend_dyn: &dyn SyncCacheBackend = &backend;
backend_dyn
.set(Arc::from("key1"), Arc::new(b"value1".to_vec()), None)
.unwrap();
let value = backend_dyn.get("key1").unwrap();
assert_eq!(value, Some(b"value1".to_vec()));
assert!(backend_dyn.exists("key1").unwrap());
assert!(!backend_dyn.exists("missing").unwrap());
backend_dyn.delete("key1").unwrap();
assert!(!backend_dyn.exists("key1").unwrap());
backend_dyn.health_check().unwrap();
assert_eq!(backend_dyn.backend_kind(), BackendKind::Mock);
}
#[test]
fn test_sync_reader_default_is_empty_uses_len() {
let backend = MockSyncBackend::new(50);
let reader: &dyn SyncCacheReader = &backend;
assert!(reader.is_empty().unwrap());
backend.set(Arc::from("k"), Arc::new(b"v".to_vec()), None).unwrap();
assert!(!reader.is_empty().unwrap());
}
#[test]
fn test_sync_writer_default_set_many_loops_set() {
let backend = MockSyncBackend::new(50);
let writer: &dyn SyncCacheWriter = &backend;
let items = vec![
(Arc::from("k1"), Arc::new(b"v1".to_vec()), None),
(Arc::from("k2"), Arc::new(b"v2".to_vec()), None),
(Arc::from("k3"), Arc::new(b"v3".to_vec()), None),
];
writer.set_many(&items).unwrap();
assert_eq!(backend.get("k1").unwrap(), Some(b"v1".to_vec()));
assert_eq!(backend.get("k2").unwrap(), Some(b"v2".to_vec()));
assert_eq!(backend.get("k3").unwrap(), Some(b"v3".to_vec()));
assert_eq!(backend.len().unwrap(), 3);
writer.delete_many(&["k1".to_string(), "k2".to_string()]).unwrap();
assert!(!backend.exists("k1").unwrap());
assert!(!backend.exists("k2").unwrap());
assert!(backend.exists("k3").unwrap());
}
#[test]
fn test_sync_reader_default_get_many_loops_get() {
let backend = MockSyncBackend::new(50);
backend.set(Arc::from("k1"), Arc::new(b"v1".to_vec()), None).unwrap();
backend.set(Arc::from("k2"), Arc::new(b"v2".to_vec()), None).unwrap();
let reader: &dyn SyncCacheReader = &backend;
let keys = vec!["k1".to_string(), "k2".to_string(), "k3".to_string()];
let results = reader.get_many(&keys).unwrap();
assert_eq!(results.len(), 3);
assert_eq!(results[0], Some(b"v1".to_vec()));
assert_eq!(results[1], Some(b"v2".to_vec()));
assert_eq!(results[2], None);
}
#[test]
fn test_sync_backend_ttl_and_expire() {
let backend = MockSyncBackend::new(50);
backend
.set(Arc::from("k"), Arc::new(b"v".to_vec()), Some(Duration::from_secs(60)))
.unwrap();
let ttl = backend.ttl("k").unwrap();
assert!(ttl.is_some());
let ttl = ttl.unwrap();
assert!(ttl <= Duration::from_secs(60) && ttl > Duration::from_secs(58));
let result = backend.expire("k", Duration::from_secs(120)).unwrap();
assert!(result);
let new_ttl = backend.ttl("k").unwrap().unwrap();
assert!(new_ttl > Duration::from_secs(118));
let result = backend.expire("missing", Duration::from_secs(10)).unwrap();
assert!(!result);
}
#[test]
fn test_backend_kind_is_composite_chain() {
assert!(BackendKind::Chain.is_composite());
}
#[test]
fn test_backend_kind_is_composite_others_false() {
assert!(!BackendKind::Moka.is_composite());
assert!(!BackendKind::DashMap.is_composite());
assert!(!BackendKind::Redis.is_composite());
assert!(!BackendKind::Mock.is_composite());
assert!(!BackendKind::Unknown.is_composite());
assert!(!BackendKind::Valkey.is_composite());
assert!(!BackendKind::Dragonfly.is_composite());
assert!(!BackendKind::Aerospike.is_composite());
}
#[test]
fn test_backend_kind_is_distributed_valkey() {
assert!(BackendKind::Valkey.is_distributed());
}
#[test]
fn test_backend_kind_is_distributed_dragonfly() {
assert!(BackendKind::Dragonfly.is_distributed());
}
#[test]
fn test_backend_kind_is_distributed_aerospike() {
assert!(BackendKind::Aerospike.is_distributed());
}
#[test]
fn test_backend_kind_is_memory_valkey_false() {
assert!(!BackendKind::Valkey.is_memory());
}
#[test]
fn test_backend_kind_is_memory_dragonfly_false() {
assert!(!BackendKind::Dragonfly.is_memory());
}
#[test]
fn test_backend_kind_is_memory_aerospike_false() {
assert!(!BackendKind::Aerospike.is_memory());
}
#[test]
fn test_backend_kind_is_composite_valkey_false() {
assert!(!BackendKind::Valkey.is_composite());
}
#[test]
fn test_backend_kind_is_composite_dragonfly_false() {
assert!(!BackendKind::Dragonfly.is_composite());
}
#[test]
fn test_backend_kind_is_composite_aerospike_false() {
assert!(!BackendKind::Aerospike.is_composite());
}
#[test]
fn test_backend_kind_new_variants_debug_clone_eq() {
assert!(format!("{:?}", BackendKind::Valkey).contains("Valkey"));
assert!(format!("{:?}", BackendKind::Dragonfly).contains("Dragonfly"));
assert!(format!("{:?}", BackendKind::Aerospike).contains("Aerospike"));
assert_eq!(BackendKind::Valkey, BackendKind::Valkey);
assert_eq!(BackendKind::Dragonfly, BackendKind::Dragonfly);
assert_eq!(BackendKind::Aerospike, BackendKind::Aerospike);
assert_ne!(BackendKind::Valkey, BackendKind::Redis);
assert_ne!(BackendKind::Dragonfly, BackendKind::Valkey);
}
#[tokio::test]
async fn test_cache_reader_keys_default_returns_empty() {
let backend = MockBackend::new("mock", 50, false);
let reader: &dyn CacheReader = &backend;
let keys = reader.keys("*").await.unwrap();
assert!(keys.is_empty());
}
#[test]
fn test_sync_reader_keys_default_returns_empty() {
let backend = MockSyncBackend::new(50);
let reader: &dyn SyncCacheReader = &backend;
let keys = reader.keys("*").unwrap();
assert!(keys.is_empty());
}
#[tokio::test]
async fn test_cache_connector_as_atomic_writer_default_none() {
let backend = MockBackend::new("mock", 50, false);
let connector: &dyn CacheConnector = &backend;
assert!(connector.as_atomic_writer().is_some());
}
#[tokio::test]
async fn test_cache_connector_as_atomic_writer_dashmap_none() {
use crate::backend::DashMapMemoryBackend;
let backend = DashMapMemoryBackend::new();
let connector: &dyn CacheConnector = &backend;
assert!(connector.as_atomic_writer().is_none());
}
}