use crate::backend::interface::AtomicCacheWriter;
use crate::backend::{BackendKind, CacheConnector, CacheReader, CacheWriter};
use crate::backend::{BackendScore, Scores};
use crate::error::OxCacheResult;
use crate::impl_backend_builder;
use async_trait::async_trait;
use moka::Expiry;
use moka::ops::compute::{CompResult, Op};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
#[derive(Clone, Debug)]
pub(crate) struct MokaEntry {
pub(crate) value: Vec<u8>,
pub(crate) expires_at: Option<Instant>,
}
#[derive(Default, Clone)]
pub(crate) struct MokaExpiry;
impl Expiry<Arc<str>, MokaEntry> for MokaExpiry {
fn expire_after_create(&self, _key: &Arc<str>, val: &MokaEntry, created_at: Instant) -> Option<Duration> {
val.expires_at.map(|e| e.saturating_duration_since(created_at))
}
fn expire_after_update(
&self,
_key: &Arc<str>,
val: &MokaEntry,
updated_at: Instant,
_duration_until_expiry: Option<Duration>,
) -> Option<Duration> {
val.expires_at.map(|e| e.saturating_duration_since(updated_at))
}
}
#[derive(Clone)]
pub struct MokaMemoryBackend {
cache: Arc<moka::future::Cache<Arc<str>, MokaEntry>>,
capacity: u64,
}
impl_backend_builder!(MokaMemoryBackend, MokaMemoryBackendBuilder);
impl MokaMemoryBackend {
pub fn capacity(&self) -> u64 {
self.capacity
}
pub fn entry_count(&self) -> u64 {
self.cache.entry_count()
}
}
impl Default for MokaMemoryBackend {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for MokaMemoryBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MokaMemoryBackend")
.field("capacity", &self.capacity)
.field("entry_count", &self.cache.entry_count())
.finish()
}
}
#[async_trait]
impl CacheReader for MokaMemoryBackend {
async fn get(&self, key: &str) -> OxCacheResult<Option<Vec<u8>>> {
Ok(self.cache.get(key).await.map(|e| e.value))
}
async fn exists(&self, key: &str) -> OxCacheResult<bool> {
Ok(self.cache.contains_key(key))
}
async fn ttl(&self, key: &str) -> OxCacheResult<Option<Duration>> {
let now = Instant::now();
Ok(self
.cache
.get(key)
.await
.and_then(|e| e.expires_at.and_then(|exp| exp.checked_duration_since(now))))
}
async fn len(&self) -> OxCacheResult<u64> {
Ok(self.cache.entry_count())
}
async fn is_empty(&self) -> OxCacheResult<bool> {
Ok(self.cache.entry_count() == 0)
}
async fn capacity(&self) -> OxCacheResult<u64> {
Ok(self.capacity)
}
async fn stats(&self) -> OxCacheResult<HashMap<String, String>> {
let mut stats = HashMap::new();
stats.insert("type".to_string(), "moka".to_string());
stats.insert("capacity".to_string(), self.capacity.to_string());
stats.insert("entry_count".to_string(), self.cache.entry_count().to_string());
Ok(stats)
}
async fn keys(&self, pattern: &str) -> OxCacheResult<Vec<String>> {
Ok(self.keys_matching(pattern).await)
}
}
#[async_trait]
impl CacheWriter for MokaMemoryBackend {
async fn set(&self, key: Arc<str>, value: Arc<Vec<u8>>, ttl: Option<Duration>) -> OxCacheResult<()> {
let expires_at = ttl.map(|d| Instant::now() + d);
let entry = MokaEntry {
value: (*value).clone(),
expires_at,
};
self.cache.insert(key, entry).await;
Ok(())
}
async fn delete(&self, key: &str) -> OxCacheResult<()> {
self.cache.invalidate(key).await;
Ok(())
}
async fn clear(&self) -> OxCacheResult<()> {
self.cache.invalidate_all();
Ok(())
}
async fn expire(&self, key: &str, ttl: Duration) -> OxCacheResult<bool> {
let new_expires_at = Instant::now() + ttl;
let key_arc: Arc<str> = Arc::from(key);
let result = self
.cache
.entry(key_arc)
.and_compute_with(|maybe_entry: Option<moka::Entry<Arc<str>, MokaEntry>>| async move {
match maybe_entry {
Some(entry) => {
let mut old = entry.into_value();
old.expires_at = Some(new_expires_at);
Op::Put(old)
}
None => Op::Nop,
}
})
.await;
match result {
CompResult::ReplacedWith(_) => Ok(true),
_ => Ok(false),
}
}
}
#[async_trait]
impl CacheConnector for MokaMemoryBackend {
async fn health_check(&self) -> OxCacheResult<()> {
Ok(())
}
async fn shutdown(&self) {
self.cache.invalidate_all();
}
fn backend_kind(&self) -> BackendKind {
BackendKind::Moka
}
fn as_atomic_writer(&self) -> Option<&dyn AtomicCacheWriter> {
Some(self)
}
}
fn sync_block_on<F: std::future::Future>(fut: F) -> F::Output {
match tokio::runtime::Handle::try_current() {
Ok(handle) if handle.runtime_flavor() == tokio::runtime::RuntimeFlavor::MultiThread => {
tokio::task::block_in_place(|| handle.block_on(fut))
}
Ok(handle) => {
handle.block_on(fut)
}
Err(_) => {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("failed to create temporary tokio runtime for sync_block_on");
rt.block_on(fut)
}
}
}
impl crate::backend::interface::SyncCacheReader for MokaMemoryBackend {
fn get(&self, key: &str) -> OxCacheResult<Option<Vec<u8>>> {
Ok(sync_block_on(self.cache.get(key)).map(|e| e.value))
}
fn exists(&self, key: &str) -> OxCacheResult<bool> {
Ok(self.cache.contains_key(key))
}
fn ttl(&self, key: &str) -> OxCacheResult<Option<Duration>> {
let now = Instant::now();
Ok(sync_block_on(self.cache.get(key))
.and_then(|e| e.expires_at.and_then(|exp| exp.checked_duration_since(now))))
}
fn len(&self) -> OxCacheResult<u64> {
Ok(self.cache.entry_count())
}
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(), "moka".to_string());
stats.insert("capacity".to_string(), self.capacity.to_string());
stats.insert("entry_count".to_string(), self.cache.entry_count().to_string());
Ok(stats)
}
}
impl crate::backend::interface::SyncCacheWriter for MokaMemoryBackend {
fn set(&self, key: Arc<str>, value: Arc<Vec<u8>>, ttl: Option<Duration>) -> OxCacheResult<()> {
let expires_at = ttl.map(|d| Instant::now() + d);
let entry = MokaEntry {
value: (*value).clone(),
expires_at,
};
sync_block_on(self.cache.insert(key, entry));
Ok(())
}
fn delete(&self, key: &str) -> OxCacheResult<()> {
sync_block_on(self.cache.invalidate(key));
Ok(())
}
fn clear(&self) -> OxCacheResult<()> {
self.cache.invalidate_all();
Ok(())
}
fn expire(&self, key: &str, ttl: Duration) -> OxCacheResult<bool> {
let new_expires_at = Instant::now() + ttl;
let key_arc: Arc<str> = Arc::from(key);
let result = sync_block_on(self.cache.entry(key_arc).and_compute_with(
|maybe_entry: Option<moka::Entry<Arc<str>, MokaEntry>>| async move {
match maybe_entry {
Some(entry) => {
let mut old = entry.into_value();
old.expires_at = Some(new_expires_at);
Op::Put(old)
}
None => Op::Nop,
}
},
));
match result {
CompResult::ReplacedWith(_) => Ok(true),
_ => Ok(false),
}
}
}
impl crate::backend::interface::SyncCacheConnector for MokaMemoryBackend {
fn health_check(&self) -> OxCacheResult<()> {
Ok(())
}
fn shutdown(&self) {
self.cache.invalidate_all();
}
fn backend_kind(&self) -> BackendKind {
BackendKind::Moka
}
}
impl crate::backend::interface::SyncAtomicCacheWriter for MokaMemoryBackend {
fn incr(&self, key: &str, delta: i64, ttl: Option<Duration>) -> OxCacheResult<i64> {
sync_block_on(AtomicCacheWriter::incr(self, key, delta, ttl))
}
fn compare_and_swap(
&self,
key: &str,
expected: Option<&[u8]>,
new: Vec<u8>,
ttl: Option<Duration>,
) -> OxCacheResult<bool> {
sync_block_on(AtomicCacheWriter::compare_and_swap(self, key, expected, new, ttl))
}
fn set_if_absent(&self, key: &str, value: Vec<u8>, ttl: Option<Duration>) -> OxCacheResult<bool> {
sync_block_on(AtomicCacheWriter::set_if_absent(self, key, value, ttl))
}
}
impl BackendScore for MokaMemoryBackend {
fn score(&self) -> u8 {
Scores::MOKA
}
fn is_persistent(&self) -> bool {
false
}
fn backend_name(&self) -> &'static str {
"moka"
}
}
fn glob_matches(pattern: &str, text: &str) -> bool {
let mut p = pattern.chars().peekable();
let mut t = text.chars().peekable();
while let Some(pc) = p.peek() {
match pc {
'*' => {
p.next();
if p.peek().is_none() {
return true;
}
let remaining_pattern: String = p.collect();
let remaining_text: String = t.collect();
for i in 0..=remaining_text.len() {
if glob_matches(&remaining_pattern, &remaining_text[i..]) {
return true;
}
}
return false;
}
'?' => {
if t.next().is_none() {
return false;
}
p.next();
}
_ => match t.next() {
Some(tc) if tc == *pc => {
p.next();
}
_ => return false,
},
}
}
t.peek().is_none()
}
#[async_trait]
impl AtomicCacheWriter for MokaMemoryBackend {
async fn incr(&self, key: &str, delta: i64, ttl: Option<Duration>) -> OxCacheResult<i64> {
let key_arc: Arc<str> = Arc::from(key);
let expires_at = ttl.map(|d| Instant::now() + d);
let result = self
.cache
.entry(key_arc.clone())
.and_compute_with(|maybe_entry: Option<moka::Entry<Arc<str>, MokaEntry>>| async move {
let current_val = match maybe_entry {
Some(entry) => {
let old = entry.into_value();
match String::from_utf8(old.value) {
Ok(s) => match s.parse::<i64>() {
Ok(v) => v,
Err(_) => return Op::Nop,
},
Err(_) => return Op::Nop,
}
}
None => 0,
};
let new_val = match current_val.checked_add(delta) {
Some(v) => v,
None => {
return Op::Nop;
}
};
Op::Put(MokaEntry {
value: new_val.to_string().into_bytes(),
expires_at,
})
})
.await;
match result {
CompResult::Inserted(entry) | CompResult::ReplacedWith(entry) => {
let val_str = String::from_utf8(entry.value().value.clone()).map_err(|e| {
crate::error::OxCacheError::Operation(format!("incr: invalid UTF-8 in stored value: {}", e))
})?;
val_str.parse::<i64>().map_err(|e| {
crate::error::OxCacheError::Operation(format!("incr: invalid integer in stored value: {}", e))
})
}
CompResult::Unchanged(_) | CompResult::StillNone(_) => {
Err(crate::error::OxCacheError::Operation("incr: i64 overflow".to_string()))
}
_ => Err(crate::error::OxCacheError::Operation(
"incr: unexpected compute result".to_string(),
)),
}
}
async fn compare_and_swap(
&self,
key: &str,
expected: Option<&[u8]>,
new: Vec<u8>,
ttl: Option<Duration>,
) -> OxCacheResult<bool> {
let key_arc: Arc<str> = Arc::from(key);
let expires_at = ttl.map(|d| Instant::now() + d);
let expected_owned = expected.map(|b| b.to_vec());
let new_clone = new.clone();
let result = self
.cache
.entry(key_arc)
.and_compute_with(|maybe_entry: Option<moka::Entry<Arc<str>, MokaEntry>>| async move {
match &expected_owned {
None => {
if maybe_entry.is_none() {
Op::Put(MokaEntry {
value: new_clone,
expires_at,
})
} else {
Op::Nop
}
}
Some(exp_bytes) => {
match &maybe_entry {
Some(entry) if entry.value().value == *exp_bytes => Op::Put(MokaEntry {
value: new_clone,
expires_at,
}),
_ => Op::Nop,
}
}
}
})
.await;
match result {
CompResult::Inserted(_) | CompResult::ReplacedWith(_) => Ok(true),
_ => Ok(false),
}
}
async fn set_if_absent(&self, key: &str, value: Vec<u8>, ttl: Option<Duration>) -> OxCacheResult<bool> {
let key_arc: Arc<str> = Arc::from(key);
let expires_at = ttl.map(|d| Instant::now() + d);
let result = self
.cache
.entry(key_arc)
.and_compute_with(|maybe_entry: Option<moka::Entry<Arc<str>, MokaEntry>>| async move {
if maybe_entry.is_none() {
Op::Put(MokaEntry { value, expires_at })
} else {
Op::Nop
}
})
.await;
match result {
CompResult::Inserted(_) => Ok(true),
_ => Ok(false),
}
}
}
impl MokaMemoryBackend {
pub async fn keys_matching(&self, pattern: &str) -> Vec<String> {
let mut keys = Vec::new();
for (key_arc, _entry) in self.cache.iter() {
let key_str: &str = key_arc.as_ref();
if glob_matches(pattern, key_str) {
keys.push(key_str.to_string());
}
}
keys
}
}
#[derive(Default)]
pub struct MokaMemoryBackendBuilder {
capacity: u64,
ttl: Option<Duration>,
time_to_idle: Option<Duration>,
}
impl MokaMemoryBackendBuilder {
pub fn capacity(mut self, capacity: u64) -> Self {
self.capacity = capacity;
self
}
pub fn ttl(mut self, ttl: Duration) -> Self {
self.ttl = Some(ttl);
self
}
pub fn time_to_idle(mut self, ttl: Duration) -> Self {
self.time_to_idle = Some(ttl);
self
}
pub fn build(self) -> MokaMemoryBackend {
let capacity = if self.capacity > 0 {
self.capacity
} else {
10_000 };
let mut builder = moka::future::Cache::builder()
.max_capacity(capacity)
.expire_after(MokaExpiry);
if let Some(ttl) = self.ttl {
builder = builder.time_to_live(ttl);
}
if let Some(tti) = self.time_to_idle {
builder = builder.time_to_idle(tti);
}
let cache = Arc::new(builder.build());
MokaMemoryBackend { cache, capacity }
}
}
pub fn moka_memory() -> MokaMemoryBackend {
MokaMemoryBackend::new()
}
pub fn moka_memory_with_capacity(capacity: u64) -> MokaMemoryBackend {
MokaMemoryBackend::builder().capacity(capacity).build()
}
pub fn moka_memory_with_capacity_and_ttl(capacity: u64, ttl: Duration) -> MokaMemoryBackend {
MokaMemoryBackend::builder().capacity(capacity).ttl(ttl).build()
}
pub fn default_memory_backend() -> MokaMemoryBackend {
moka_memory()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_moka_backend_builder() {
let backend = MokaMemoryBackend::builder()
.capacity(1000)
.ttl(Duration::from_secs(3600))
.time_to_idle(Duration::from_secs(1800))
.build();
assert_eq!(backend.capacity(), 1000);
}
#[test]
fn test_moka_backend_default() {
let backend = MokaMemoryBackend::default();
assert!(backend.capacity() > 0);
}
#[tokio::test]
async fn test_moka_basic_operations() {
let backend = MokaMemoryBackend::new();
backend
.set(Arc::from("key1"), Arc::new(b"value1".to_vec()), None)
.await
.unwrap();
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
let result = backend.get("key1").await.unwrap();
assert_eq!(result, Some(b"value1".to_vec()));
let exists = backend.exists("key1").await.unwrap();
assert!(exists);
backend.delete("key1").await.unwrap();
let exists_after = backend.exists("key1").await.unwrap();
assert!(!exists_after);
}
#[test]
fn test_convenience_functions() {
let backend1 = moka_memory();
let backend2 = moka_memory_with_capacity(1000);
let backend3 = moka_memory_with_capacity_and_ttl(1000, Duration::from_secs(3600));
assert!(backend1.capacity() > 0);
assert_eq!(backend2.capacity(), 1000);
assert_eq!(backend3.capacity(), 1000);
}
#[tokio::test]
async fn test_moka_set_with_ttl_expires_after_timeout() {
let backend = MokaMemoryBackend::new();
backend
.set(Arc::from("k"), Arc::new(b"v".to_vec()), Some(Duration::from_millis(50)))
.await
.unwrap();
assert_eq!(backend.get("k").await.unwrap(), Some(b"v".to_vec()));
tokio::time::sleep(Duration::from_millis(100)).await;
let mut expired = false;
for _ in 0..10 {
if backend.get("k").await.unwrap().is_none() {
expired = true;
break;
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
assert!(expired, "entry should expire after TTL");
}
#[tokio::test]
async fn test_moka_set_with_ttl_readable_within_window() {
let backend = MokaMemoryBackend::new();
backend
.set(Arc::from("k"), Arc::new(b"v".to_vec()), Some(Duration::from_secs(60)))
.await
.unwrap();
assert_eq!(backend.get("k").await.unwrap(), Some(b"v".to_vec()));
}
#[tokio::test]
async fn test_moka_set_without_ttl_uses_global_ttl() {
let backend = MokaMemoryBackend::builder()
.capacity(1000)
.ttl(Duration::from_secs(30))
.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()));
let ttl = backend.ttl("k").await.unwrap();
assert_eq!(ttl, None, "set(None) with global TTL should report None per-entry");
}
#[tokio::test]
async fn test_moka_ttl_returns_remaining() {
let backend = MokaMemoryBackend::new();
backend
.set(Arc::from("k"), Arc::new(b"v".to_vec()), Some(Duration::from_secs(60)))
.await
.unwrap();
let ttl = backend.ttl("k").await.unwrap().expect("ttl should be Some");
assert!(
ttl > Duration::from_secs(58),
"ttl={} should be > 58s",
ttl.as_secs_f64()
);
assert!(
ttl <= Duration::from_secs(60),
"ttl={} should be <= 60s",
ttl.as_secs_f64()
);
}
#[tokio::test]
async fn test_moka_ttl_returns_none_for_missing_key() {
let backend = MokaMemoryBackend::new();
assert_eq!(backend.ttl("missing").await.unwrap(), None);
}
#[tokio::test]
async fn test_moka_ttl_returns_none_for_no_ttl_key() {
let backend = MokaMemoryBackend::new();
backend
.set(Arc::from("k"), Arc::new(b"v".to_vec()), None)
.await
.unwrap();
assert_eq!(backend.ttl("k").await.unwrap(), None);
}
#[tokio::test]
async fn test_moka_expire_extends_ttl() {
let backend = MokaMemoryBackend::new();
backend
.set(Arc::from("k"), Arc::new(b"v".to_vec()), Some(Duration::from_secs(60)))
.await
.unwrap();
let ok = backend.expire("k", Duration::from_secs(120)).await.unwrap();
assert!(ok, "expire on existing key should return true");
let ttl = backend
.ttl("k")
.await
.unwrap()
.expect("ttl should be Some after expire");
assert!(
ttl > Duration::from_secs(118),
"ttl={} should be > 118s",
ttl.as_secs_f64()
);
}
#[tokio::test]
async fn test_moka_expire_shrinks_ttl() {
let backend = MokaMemoryBackend::new();
backend
.set(Arc::from("k"), Arc::new(b"v".to_vec()), Some(Duration::from_secs(60)))
.await
.unwrap();
let ok = backend.expire("k", Duration::from_millis(50)).await.unwrap();
assert!(ok, "expire on existing key should return true");
tokio::time::sleep(Duration::from_millis(100)).await;
let mut expired = false;
for _ in 0..10 {
if backend.get("k").await.unwrap().is_none() {
expired = true;
break;
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
assert!(expired, "entry should expire after shrunk TTL");
}
#[tokio::test]
async fn test_moka_expire_missing_key_returns_false() {
let backend = MokaMemoryBackend::new();
let ok = backend.expire("missing", Duration::from_secs(60)).await.unwrap();
assert!(!ok, "expire on missing key should return false");
}
mod sync_tests {
use super::MokaMemoryBackend;
use crate::backend::{BackendKind, SyncCacheConnector, SyncCacheReader, SyncCacheWriter};
use std::sync::Arc;
use std::time::Duration;
#[test]
fn test_moka_sync_get_set_basic() {
let backend = MokaMemoryBackend::new();
let writer: &dyn SyncCacheWriter = &backend;
writer
.set(Arc::from("key1"), Arc::new(b"value1".to_vec()), None)
.unwrap();
let reader: &dyn SyncCacheReader = &backend;
assert_eq!(reader.get("key1").unwrap(), Some(b"value1".to_vec()));
assert!(reader.exists("key1").unwrap());
assert!(!reader.exists("key2").unwrap());
assert!(reader.capacity().unwrap() > 0);
let stats = reader.stats().unwrap();
assert_eq!(stats.get("type"), Some(&"moka".to_string()));
}
#[test]
fn test_moka_sync_set_with_ttl_expires() {
let backend = MokaMemoryBackend::new();
let writer: &dyn SyncCacheWriter = &backend;
writer
.set(Arc::from("k"), Arc::new(b"v".to_vec()), Some(Duration::from_millis(50)))
.unwrap();
let reader: &dyn SyncCacheReader = &backend;
assert_eq!(reader.get("k").unwrap(), Some(b"v".to_vec()));
std::thread::sleep(Duration::from_millis(120));
let mut expired = false;
for _ in 0..10 {
if reader.get("k").unwrap().is_none() {
expired = true;
break;
}
std::thread::sleep(Duration::from_millis(50));
}
assert!(expired, "entry should expire after TTL via sync get");
}
#[test]
fn test_moka_sync_ttl_returns_remaining() {
let backend = MokaMemoryBackend::new();
let writer: &dyn SyncCacheWriter = &backend;
writer
.set(Arc::from("k"), Arc::new(b"v".to_vec()), Some(Duration::from_secs(60)))
.unwrap();
let reader: &dyn SyncCacheReader = &backend;
let ttl = reader.ttl("k").unwrap().expect("ttl should be Some for TTL'd key");
assert!(
ttl > Duration::from_secs(58),
"ttl={} should be > 58s",
ttl.as_secs_f64()
);
assert!(
ttl <= Duration::from_secs(60),
"ttl={} should be <= 60s",
ttl.as_secs_f64()
);
writer.set(Arc::from("no_ttl"), Arc::new(b"v".to_vec()), None).unwrap();
assert_eq!(reader.ttl("no_ttl").unwrap(), None);
assert_eq!(reader.ttl("missing").unwrap(), None);
}
#[test]
fn test_moka_sync_expire_works() {
let backend = MokaMemoryBackend::new();
let writer: &dyn SyncCacheWriter = &backend;
writer
.set(Arc::from("k"), Arc::new(b"v".to_vec()), Some(Duration::from_secs(60)))
.unwrap();
let ok = writer.expire("k", Duration::from_secs(120)).unwrap();
assert!(ok, "expire on existing key should return true");
let reader: &dyn SyncCacheReader = &backend;
let new_ttl = reader.ttl("k").unwrap().expect("ttl should be Some after expire");
assert!(
new_ttl > Duration::from_secs(118),
"new_ttl={} should be > 118s",
new_ttl.as_secs_f64()
);
let ok = writer.expire("missing", Duration::from_secs(10)).unwrap();
assert!(!ok, "expire on missing key should return false");
}
#[test]
fn test_moka_sync_delete_clear() {
let backend = MokaMemoryBackend::new();
let writer: &dyn SyncCacheWriter = &backend;
writer.set(Arc::from("k1"), Arc::new(b"v1".to_vec()), None).unwrap();
writer.set(Arc::from("k2"), Arc::new(b"v2".to_vec()), None).unwrap();
let reader: &dyn SyncCacheReader = &backend;
assert!(reader.exists("k1").unwrap());
assert!(reader.exists("k2").unwrap());
writer.delete("k1").unwrap();
assert!(!reader.exists("k1").unwrap());
assert!(reader.exists("k2").unwrap());
writer.clear().unwrap();
assert!(!reader.exists("k2").unwrap());
assert_eq!(reader.len().unwrap(), 0);
assert!(reader.is_empty().unwrap());
let connector: &dyn SyncCacheConnector = &backend;
connector.health_check().unwrap();
assert_eq!(connector.backend_kind(), BackendKind::Moka);
connector.shutdown();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_moka_sync_ops_inside_multi_thread_runtime() {
let backend = MokaMemoryBackend::new();
let writer: &dyn SyncCacheWriter = &backend;
writer.set(Arc::from("mt"), Arc::new(b"v".to_vec()), None).unwrap();
let reader: &dyn SyncCacheReader = &backend;
assert_eq!(reader.get("mt").unwrap(), Some(b"v".to_vec()));
assert!(reader.exists("mt").unwrap());
writer.delete("mt").unwrap();
assert!(!reader.exists("mt").unwrap());
}
}
#[test]
fn test_glob_matches_exact() {
assert!(glob_matches("hello", "hello"));
assert!(!glob_matches("hello", "world"));
assert!(!glob_matches("hello", "hell"));
assert!(!glob_matches("hell", "hello"));
}
#[test]
fn test_glob_matches_star() {
assert!(glob_matches("*", ""));
assert!(glob_matches("*", "anything"));
assert!(glob_matches("hello*", "hello"));
assert!(glob_matches("hello*", "helloworld"));
assert!(glob_matches("*world", "helloworld"));
assert!(glob_matches("he*ld", "helloworld"));
assert!(!glob_matches("he*ld", "hello"));
}
#[test]
fn test_glob_matches_question_mark() {
assert!(glob_matches("h?llo", "hello"));
assert!(glob_matches("?????", "hello"));
assert!(!glob_matches("????", "hello"));
assert!(!glob_matches("??????", "hello"));
assert!(!glob_matches("?", ""));
}
#[test]
fn test_glob_matches_combined() {
assert!(glob_matches("h*o", "hello"));
assert!(glob_matches("h*o", "ho"));
assert!(glob_matches("h?l*w", "hellow"));
assert!(glob_matches("a*b*c", "abc"));
assert!(glob_matches("a*b*c", "aXbYc"));
assert!(!glob_matches("a*b*c", "aXbY"));
}
#[test]
fn test_glob_matches_empty() {
assert!(glob_matches("", ""));
assert!(!glob_matches("", "a"));
assert!(glob_matches("*", ""));
}
#[tokio::test]
async fn test_moka_keys_matching_glob() {
let backend = MokaMemoryBackend::new();
backend
.set(Arc::from("user:1"), Arc::new(b"a".to_vec()), None)
.await
.unwrap();
backend
.set(Arc::from("user:2"), Arc::new(b"b".to_vec()), None)
.await
.unwrap();
backend
.set(Arc::from("session:1"), Arc::new(b"c".to_vec()), None)
.await
.unwrap();
let all = backend.keys_matching("*").await;
assert_eq!(all.len(), 3);
let users = backend.keys_matching("user:*").await;
assert_eq!(users.len(), 2);
let sessions = backend.keys_matching("session:*").await;
assert_eq!(sessions.len(), 1);
let none = backend.keys_matching("nope:*").await;
assert!(none.is_empty());
}
#[tokio::test]
async fn test_moka_keys_via_cache_reader() {
let backend = MokaMemoryBackend::new();
backend
.set(Arc::from("a"), Arc::new(b"1".to_vec()), None)
.await
.unwrap();
backend
.set(Arc::from("b"), Arc::new(b"2".to_vec()), None)
.await
.unwrap();
let keys = CacheReader::keys(&backend, "*").await.unwrap();
assert_eq!(keys.len(), 2);
}
#[test]
fn test_moka_backend_debug() {
let backend = MokaMemoryBackend::new();
let debug_str = format!("{:?}", backend);
assert!(debug_str.contains("MokaMemoryBackend"));
assert!(debug_str.contains("capacity"));
}
#[test]
fn test_moka_backend_entry_count() {
let backend = MokaMemoryBackend::new();
assert_eq!(backend.entry_count(), 0);
}
#[tokio::test]
async fn test_moka_backend_entry_count_after_insert() {
let backend = MokaMemoryBackend::new();
backend
.set(Arc::from("k1"), Arc::new(b"v".to_vec()), None)
.await
.unwrap();
backend
.set(Arc::from("k2"), Arc::new(b"v".to_vec()), None)
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(100)).await;
let _ = backend.entry_count();
}
#[test]
fn test_moka_sync_atomic_incr() {
let backend = MokaMemoryBackend::new();
let val = crate::backend::SyncAtomicCacheWriter::incr(&backend, "c", 5, None).unwrap();
assert_eq!(val, 5);
let val = crate::backend::SyncAtomicCacheWriter::incr(&backend, "c", 3, None).unwrap();
assert_eq!(val, 8);
}
#[test]
fn test_moka_sync_atomic_cas() {
let backend = MokaMemoryBackend::new();
let ok =
crate::backend::SyncAtomicCacheWriter::compare_and_swap(&backend, "k", None, b"v1".to_vec(), None).unwrap();
assert!(ok);
let ok =
crate::backend::SyncAtomicCacheWriter::compare_and_swap(&backend, "k", Some(b"v1"), b"v2".to_vec(), None)
.unwrap();
assert!(ok);
let ok =
crate::backend::SyncAtomicCacheWriter::compare_and_swap(&backend, "k", Some(b"v1"), b"v3".to_vec(), None)
.unwrap();
assert!(!ok);
}
#[test]
fn test_moka_sync_atomic_set_if_absent() {
let backend = MokaMemoryBackend::new();
let ok = crate::backend::SyncAtomicCacheWriter::set_if_absent(&backend, "k", b"v".to_vec(), None).unwrap();
assert!(ok);
let ok = crate::backend::SyncAtomicCacheWriter::set_if_absent(&backend, "k", b"v2".to_vec(), None).unwrap();
assert!(!ok);
}
}