#![allow(clippy::module_inception)]
#[allow(unused_imports)]
pub use super::*;
#[cfg(test)]
mod tests {
use super::*;
use crate::core::constants::MAX_JSON_DEPTH;
#[tokio::test]
async fn test_cache_clear() {
let cache: Cache<String, String> = Cache::builder().build().await.unwrap();
cache
.set(&"key".to_string(), &"value".to_string())
.await
.unwrap();
cache.clear().await.unwrap();
assert!(cache.get(&"key".to_string()).await.unwrap().is_none());
}
#[test]
fn test_get_or_shard_index_in_range() {
for key in [
"",
"a",
"key1",
"user:123",
"很长很长的中文key🎯",
"x".repeat(1024).as_str(),
] {
let idx = get_or_shard_index(key);
assert!(
idx < GET_OR_LOCK_SHARDS,
"key={key} shard={idx} out of range"
);
}
}
#[test]
fn test_get_or_shards_distribute() {
let mut seen = std::collections::HashSet::new();
for i in 0..256 {
seen.insert(get_or_shard_index(&format!("key{i}")));
}
assert!(
seen.len() > 1,
"256 keys should spread across shards, only {} distinct shards",
seen.len()
);
}
#[test]
fn test_get_or_shard_index_same_key_same_shard() {
let a = get_or_shard_index("stable-key");
let b = get_or_shard_index("stable-key");
assert_eq!(a, b);
let c = get_or_shard_index("other-key");
let d = get_or_shard_index("another-key");
assert_ne!(c, d, "不同 key 应路由到不同分片(本例期望)");
}
#[tokio::test]
async fn test_get_or_concurrent_different_keys_no_contention_error() {
let cache: Cache<String, String> = Cache::builder().build().await.unwrap();
let cache = Arc::new(cache);
let mut handles = Vec::new();
for i in 0..64u64 {
let cache = cache.clone();
handles.push(tokio::spawn(async move {
let key = format!("concurrent-key-{i}");
let value = cache
.get_or(&key, || async move { Ok(format!("value-{i}")) })
.await
.unwrap();
assert_eq!(value, format!("value-{i}"));
cache.get(&key).await.unwrap().unwrap();
}));
}
for h in handles {
h.await.unwrap();
}
}
#[tokio::test]
async fn test_cache_len() {
let cache: Cache<String, String> = Cache::builder().build().await.unwrap();
cache
.set(&"key1".to_string(), &"v1".to_string())
.await
.unwrap();
let len = cache.len().await.unwrap();
assert!(len <= 100, "len should be reasonable after single insert");
}
#[tokio::test]
async fn test_cache_is_empty() {
let cache: Cache<String, String> = Cache::builder().build().await.unwrap();
cache
.set(&"key".to_string(), &"value".to_string())
.await
.unwrap();
let _ = cache.is_empty().await.unwrap();
}
#[tokio::test]
async fn test_cache_exists() {
let cache: Cache<String, String> = Cache::builder().build().await.unwrap();
assert!(!cache.exists(&"key".to_string()).await.unwrap());
cache
.set(&"key".to_string(), &"value".to_string())
.await
.unwrap();
assert!(cache.exists(&"key".to_string()).await.unwrap());
}
#[tokio::test]
async fn test_cache_delete() {
let cache: Cache<String, String> = Cache::builder().build().await.unwrap();
cache
.set(&"key".to_string(), &"value".to_string())
.await
.unwrap();
cache.delete(&"key".to_string()).await.unwrap();
assert!(cache.get(&"key".to_string()).await.unwrap().is_none());
}
#[tokio::test]
async fn test_cache_get_or() {
let cache: Cache<String, String> = Cache::builder().build().await.unwrap();
let value = cache
.get_or(&"key".to_string(), || async { Ok("computed".to_string()) })
.await
.unwrap();
assert_eq!(value, "computed");
let cached = cache.get(&"key".to_string()).await.unwrap().unwrap();
assert_eq!(cached, "computed");
}
#[tokio::test]
async fn test_cache_health_check() {
let cache: Cache<String, String> = Cache::builder().build().await.unwrap();
assert!(cache.health_check().await.is_ok());
}
#[tokio::test]
async fn test_cache_stats() {
let cache: Cache<String, String> = Cache::builder().build().await.unwrap();
let stats = cache.stats().await.unwrap();
assert!(stats.contains_key("type"));
}
#[tokio::test]
async fn test_cache_get_miss_returns_none() {
let cache: Cache<String, String> = Cache::builder().build().await.unwrap();
let result = cache.get(&"missing".to_string()).await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn test_cache_set_overwrite() {
let cache: Cache<String, String> = Cache::builder().build().await.unwrap();
cache
.set(&"k".to_string(), &"v1".to_string())
.await
.unwrap();
assert_eq!(
cache.get(&"k".to_string()).await.unwrap().unwrap(),
"v1".to_string()
);
cache
.set(&"k".to_string(), &"v2".to_string())
.await
.unwrap();
assert_eq!(
cache.get(&"k".to_string()).await.unwrap().unwrap(),
"v2".to_string()
);
}
#[tokio::test]
async fn test_cache_delete_missing_key_no_error() {
let cache: Cache<String, String> = Cache::builder().build().await.unwrap();
assert!(cache.delete(&"never".to_string()).await.is_ok());
}
#[tokio::test]
async fn test_cache_exists_after_delete() {
let cache: Cache<String, String> = Cache::builder().build().await.unwrap();
cache.set(&"k".to_string(), &"v".to_string()).await.unwrap();
assert!(cache.exists(&"k".to_string()).await.unwrap());
cache.delete(&"k".to_string()).await.unwrap();
assert!(!cache.exists(&"k".to_string()).await.unwrap());
}
#[tokio::test]
async fn test_cache_set_with_ttl() {
let cache: Cache<String, String> = Cache::builder().build().await.unwrap();
cache
.set_with_ttl(
&"k".to_string(),
&"v".to_string(),
Some(Duration::from_secs(60)),
)
.await
.unwrap();
assert_eq!(
cache.get(&"k".to_string()).await.unwrap().unwrap(),
"v".to_string()
);
}
#[tokio::test]
async fn test_cache_set_with_ttl_none() {
let cache: Cache<String, i32> = Cache::builder().build().await.unwrap();
cache
.set_with_ttl(&"k".to_string(), &42, None)
.await
.unwrap();
assert_eq!(cache.get(&"k".to_string()).await.unwrap().unwrap(), 42);
}
#[tokio::test]
async fn test_cache_get_set_integer_type() {
let cache: Cache<String, i64> = Cache::builder().build().await.unwrap();
cache.set(&"count".to_string(), &12345).await.unwrap();
assert_eq!(
cache.get(&"count".to_string()).await.unwrap().unwrap(),
12345
);
}
#[tokio::test]
async fn test_cache_get_set_struct_type() {
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct User {
id: u64,
name: String,
}
let cache: Cache<String, User> = Cache::builder().build().await.unwrap();
let user = User {
id: 1,
name: "alice".to_string(),
};
cache.set(&"user:1".to_string(), &user).await.unwrap();
let result = cache.get(&"user:1".to_string()).await.unwrap().unwrap();
assert_eq!(result, user);
}
#[tokio::test]
async fn test_cache_get_or_cache_hit_fast_path() {
let cache: Cache<String, String> = Cache::builder().build().await.unwrap();
cache
.set(&"k".to_string(), &"cached".to_string())
.await
.unwrap();
let value = cache
.get_or(&"k".to_string(), || async {
Err(OxCacheError::Operation(
"fallback should not be called".to_string(),
))
})
.await
.unwrap();
assert_eq!(value, "cached");
}
#[tokio::test]
async fn test_cache_get_or_fallback_error_propagates() {
let cache: Cache<String, String> = Cache::builder().build().await.unwrap();
let result: OxCacheResult<String> = cache
.get_or(&"missing".to_string(), || async {
Err(OxCacheError::Operation("db down".to_string()))
})
.await;
assert!(result.is_err());
match result {
Err(OxCacheError::Operation(msg)) => assert_eq!(msg, "db down"),
_ => panic!("expected OxCacheError::Operation"),
}
}
#[tokio::test]
async fn test_cache_get_or_writes_to_cache() {
let cache: Cache<String, i32> = Cache::builder().build().await.unwrap();
let v1 = cache
.get_or(&"k".to_string(), || async { Ok(99) })
.await
.unwrap();
assert_eq!(v1, 99);
let cached = cache.get(&"k".to_string()).await.unwrap().unwrap();
assert_eq!(cached, 99);
}
#[tokio::test]
async fn test_cache_capacity() {
let cache: Cache<String, String> = Cache::builder().capacity(500).build().await.unwrap();
let capacity = cache.capacity().await.unwrap();
assert_eq!(capacity, 500);
}
#[tokio::test]
async fn test_cache_shutdown() {
let cache: Cache<String, String> = Cache::builder().build().await.unwrap();
cache.set(&"k".to_string(), &"v".to_string()).await.unwrap();
cache.shutdown().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn get_by_str_semantics_and_throughput() {
let cache: Cache<String, String> = Cache::builder().build().await.unwrap();
cache
.set(&"hit".to_string(), &"v".to_string())
.await
.unwrap();
assert_eq!(
cache.get_by_str("hit").await.unwrap(),
Some("v".to_string())
);
assert_eq!(cache.get_by_str("miss").await.unwrap(), None);
cache
.set_by_str("via-str", &"w".to_string(), None)
.await
.unwrap();
assert_eq!(
cache.get(&"via-str".to_string()).await.unwrap(),
Some("w".to_string())
);
const ITER: u32 = 50_000;
let measure_owned = || async {
let mut total = Duration::ZERO;
for round in 0..2 {
let t = std::time::Instant::now();
for i in 0..ITER {
let key = format!("hit{}", (i + round) % 100);
let _: Option<String> = cache.get(&key).await.unwrap();
}
if round == 1 {
total = t.elapsed();
}
}
total
};
let measure_borrowed = || async {
let mut total = Duration::ZERO;
for round in 0..2 {
let t = std::time::Instant::now();
for i in 0..ITER {
let key = format!("hit{}", (i + round) % 100);
let _: Option<String> = cache.get_by_str(&key).await.unwrap();
}
if round == 1 {
total = t.elapsed();
}
}
total
};
let owned_total = measure_owned().await;
let borrowed_total = measure_borrowed().await;
let owned_us = owned_total.as_micros();
let borrowed_us = borrowed_total.as_micros();
println!(
"hot path ({} iters, debug profile): get(owned)={}us get_by_str(borrowed)={}us",
ITER, owned_us, borrowed_us
);
assert!(
borrowed_total <= owned_total.saturating_mul(3),
"borrowed 路径不应慢于 owned 3 倍以上: {owned_us}us vs {borrowed_us}us"
);
}
#[tokio::test]
async fn test_deserialize_value_valid() {
let cache: Cache<String, i32> = Cache::builder().build().await.unwrap();
cache.set(&"k".to_string(), &42).await.unwrap();
let v = cache.get(&"k".to_string()).await.unwrap().unwrap();
assert_eq!(v, 42);
}
#[tokio::test]
async fn test_deserialize_value_invalid_json() {
let cache: Cache<String, i32> = Cache::builder().build().await.unwrap();
cache
.backend
.set(Arc::from("bad"), Arc::new(b"not json".to_vec()), None)
.await
.unwrap();
let result = cache.get(&"bad".to_string()).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_deserialize_value_depth_exceeded() {
let mut json_str = String::new();
for _ in 0..(MAX_JSON_DEPTH + 5) {
json_str.push('[');
}
for _ in 0..(MAX_JSON_DEPTH + 5) {
json_str.push(']');
}
let cache: Cache<String, serde_json::Value> = Cache::builder().build().await.unwrap();
cache
.backend
.set(Arc::from("deep"), Arc::new(json_str.into_bytes()), None)
.await
.unwrap();
let result = cache.get(&"deep".to_string()).await;
assert!(result.is_err());
match result {
Err(OxCacheError::Serialization(msg)) => {
assert!(msg.contains("深度") || msg.contains("depth"));
}
_ => panic!("expected OxCacheError::Serialization"),
}
}
#[tokio::test]
async fn test_cache_keys_returns_matching() {
let cache: Cache<String, String> = Cache::builder().build().await.unwrap();
cache
.set(&"user:1".to_string(), &"a".to_string())
.await
.unwrap();
cache
.set(&"user:2".to_string(), &"b".to_string())
.await
.unwrap();
cache
.set(&"session:1".to_string(), &"c".to_string())
.await
.unwrap();
let all = cache.keys("*").await.unwrap();
assert_eq!(all.len(), 3);
let users = cache.keys("user:*").await.unwrap();
assert_eq!(users.len(), 2);
let none = cache.keys("nope:*").await.unwrap();
assert!(none.is_empty());
}
#[tokio::test]
async fn test_cache_ttl_returns_remaining() {
let cache: Cache<String, String> = Cache::builder().ttl_jitter(0.0).build().await.unwrap();
cache
.set_with_ttl(
&"k".to_string(),
&"v".to_string(),
Some(Duration::from_secs(60)),
)
.await
.unwrap();
let ttl = cache
.ttl(&"k".to_string())
.await
.unwrap()
.expect("ttl should be Some");
assert!(ttl > Duration::from_secs(58));
assert!(ttl <= Duration::from_secs(60));
assert_eq!(cache.ttl(&"missing".to_string()).await.unwrap(), None);
}
#[tokio::test]
async fn test_cache_expire_extends_ttl() {
let cache: Cache<String, String> = Cache::builder().build().await.unwrap();
cache
.set_with_ttl(
&"k".to_string(),
&"v".to_string(),
Some(Duration::from_secs(60)),
)
.await
.unwrap();
let ok = cache
.expire(&"k".to_string(), Duration::from_secs(120))
.await
.unwrap();
assert!(ok);
let ttl = cache
.ttl(&"k".to_string())
.await
.unwrap()
.expect("ttl should be Some");
assert!(ttl > Duration::from_secs(118));
let ok = cache
.expire(&"missing".to_string(), Duration::from_secs(60))
.await
.unwrap();
assert!(!ok);
}
#[tokio::test]
async fn test_get_or_follower_not_hung_when_leader_set_fails() {
use crate::testing::MockBackend;
let backend: Arc<dyn crate::backend::CacheBackend> =
Arc::new(MockBackend::new("mock", 50, false).with_fail_set());
let cache: Arc<Cache<String, f64>> = Arc::new(Cache::new_with_backend(backend));
let (leader_registered_tx, leader_registered_rx) = tokio::sync::oneshot::channel();
let (leader_go_tx, leader_go_rx) = tokio::sync::oneshot::channel();
let cache_leader = cache.clone();
let leader = tokio::spawn(async move {
cache_leader
.get_or(&"k".to_string(), || async {
let _ = leader_registered_tx.send(());
let _ = leader_go_rx.await;
Ok(1.0f64)
})
.await
});
let _ = leader_registered_rx.await;
let cache_follower = cache.clone();
let follower = tokio::spawn(async move {
tokio::time::timeout(
Duration::from_secs(5),
cache_follower.get_or(&"k".to_string(), || async { Ok(2.0f64) }),
)
.await
});
tokio::time::sleep(Duration::from_millis(50)).await;
let _ = leader_go_tx.send(());
let _ = leader.await;
let follower_result = follower.await.unwrap();
assert!(
follower_result.is_ok(),
"follower must resolve (timeout indicates hang): {:?}",
follower_result
);
}
}
#[cfg(all(test, feature = "memory"))]
mod sync_tests {
use super::*;
use crate::backend::MokaMemoryBackend;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::thread;
use std::time::Duration;
fn make_sync_cache() -> Cache<String, String> {
let moka = Arc::new(MokaMemoryBackend::new());
let mut cache: Cache<String, String> = Cache::new_with_backend(moka.clone());
cache.set_sync_backend(moka);
cache.set_ttl_jitter_factor(0.0);
cache
}
#[test]
fn test_cache_get_sync_set_sync_basic() {
let cache = make_sync_cache();
cache.set_sync(&"k".to_string(), &"v".to_string()).unwrap();
let v = cache.get_sync(&"k".to_string()).unwrap();
assert_eq!(v, Some("v".to_string()));
}
#[test]
fn test_cache_get_sync_without_sync_mode_returns_err() {
let cache: Cache<String, String> = Cache::new();
let result = cache.get_sync(&"k".to_string());
assert!(
matches!(result, Err(OxCacheError::NotSupported(_))),
"expected Err(NotSupported), got {:?}",
result
);
}
#[test]
fn test_cache_get_or_sync_cache_hit() {
let cache = make_sync_cache();
cache
.set_sync(&"k".to_string(), &"cached".to_string())
.unwrap();
let v = cache
.get_or_sync(&"k".to_string(), || {
Err(OxCacheError::Operation(
"fallback should not run".to_string(),
))
})
.unwrap();
assert_eq!(v, "cached");
}
#[test]
fn test_cache_get_or_sync_single_flight_prevents_duplicate_fallback() {
let cache = Arc::new(make_sync_cache());
let counter = Arc::new(AtomicU32::new(0));
let cache_a = cache.clone();
let counter_a = counter.clone();
let handle_a = thread::spawn(move || {
cache_a
.get_or_sync(&"k".to_string(), || {
counter_a.fetch_add(1, Ordering::SeqCst);
thread::sleep(Duration::from_millis(120));
Ok("v".to_string())
})
.unwrap()
});
thread::sleep(Duration::from_millis(20));
let cache_b = cache.clone();
let counter_b = counter.clone();
let handle_b = thread::spawn(move || {
cache_b
.get_or_sync(&"k".to_string(), || {
counter_b.fetch_add(1, Ordering::SeqCst);
Ok("should_not_run".to_string())
})
.unwrap()
});
let v_a = handle_a.join().expect("thread A panicked");
let v_b = handle_b.join().expect("thread B panicked");
assert_eq!(v_a, "v");
assert_eq!(v_b, "v");
assert_eq!(
counter.load(Ordering::SeqCst),
1,
"fallback must run exactly once under single-flight"
);
}
#[test]
fn test_cache_set_with_ttl_sync_expires() {
let cache = make_sync_cache();
cache
.set_with_ttl_sync(
&"k".to_string(),
&"v".to_string(),
Some(Duration::from_millis(50)),
)
.unwrap();
assert_eq!(
cache.get_sync(&"k".to_string()).unwrap(),
Some("v".to_string())
);
thread::sleep(Duration::from_millis(120));
assert_eq!(cache.get_sync(&"k".to_string()).unwrap(), None);
}
#[test]
fn test_cache_delete_sync() {
let cache = make_sync_cache();
cache.set_sync(&"k".to_string(), &"v".to_string()).unwrap();
cache.delete_sync(&"k".to_string()).unwrap();
assert_eq!(cache.get_sync(&"k".to_string()).unwrap(), None);
}
#[test]
fn test_cache_exists_sync() {
let cache = make_sync_cache();
assert!(!cache.exists_sync(&"k".to_string()).unwrap());
cache.set_sync(&"k".to_string(), &"v".to_string()).unwrap();
assert!(cache.exists_sync(&"k".to_string()).unwrap());
}
#[test]
fn test_cache_ttl_sync() {
let cache = make_sync_cache();
cache
.set_with_ttl_sync(
&"k".to_string(),
&"v".to_string(),
Some(Duration::from_secs(60)),
)
.unwrap();
let ttl = cache
.ttl_sync(&"k".to_string())
.unwrap()
.expect("ttl should be Some");
assert!(ttl > Duration::from_secs(58));
assert!(ttl <= Duration::from_secs(60));
assert_eq!(cache.ttl_sync(&"missing".to_string()).unwrap(), None);
}
#[test]
fn test_cache_expire_sync() {
let cache = make_sync_cache();
cache
.set_with_ttl_sync(
&"k".to_string(),
&"v".to_string(),
Some(Duration::from_secs(60)),
)
.unwrap();
let ok = cache
.expire_sync(&"k".to_string(), Duration::from_secs(120))
.unwrap();
assert!(ok);
let ttl = cache
.ttl_sync(&"k".to_string())
.unwrap()
.expect("ttl should be Some");
assert!(ttl > Duration::from_secs(118));
let ok = cache
.expire_sync(&"missing".to_string(), Duration::from_secs(60))
.unwrap();
assert!(!ok);
}
#[test]
fn test_cache_sync_methods_without_sync_mode_returns_err() {
let cache: Cache<String, String> = Cache::new();
assert!(cache.delete_sync(&"k".to_string()).is_err());
assert!(cache.exists_sync(&"k".to_string()).is_err());
assert!(cache.ttl_sync(&"k".to_string()).is_err());
assert!(
cache
.expire_sync(&"k".to_string(), Duration::from_secs(1))
.is_err()
);
}
struct FailSetBackend {
inner: MokaMemoryBackend,
}
impl FailSetBackend {
fn new() -> Self {
Self {
inner: MokaMemoryBackend::new(),
}
}
}
impl crate::backend::SyncCacheReader for FailSetBackend {
fn get(&self, key: &str) -> OxCacheResult<Option<Vec<u8>>> {
self.inner.get(key)
}
fn exists(&self, key: &str) -> OxCacheResult<bool> {
self.inner.exists(key)
}
fn ttl(&self, key: &str) -> OxCacheResult<Option<Duration>> {
self.inner.ttl(key)
}
fn len(&self) -> OxCacheResult<u64> {
self.inner.len()
}
fn capacity(&self) -> OxCacheResult<u64> {
Ok(self.inner.capacity())
}
fn stats(&self) -> OxCacheResult<std::collections::HashMap<String, String>> {
self.inner.stats()
}
}
impl crate::backend::SyncCacheWriter for FailSetBackend {
fn set(
&self,
_key: Arc<str>,
_value: Arc<Vec<u8>>,
_ttl: Option<Duration>,
) -> OxCacheResult<()> {
Err(OxCacheError::Operation(
"FailSetBackend: injected set failure".to_string(),
))
}
fn delete(&self, key: &str) -> OxCacheResult<()> {
self.inner.delete(key)
}
fn clear(&self) -> OxCacheResult<()> {
self.inner.clear()
}
fn expire(&self, key: &str, ttl: Duration) -> OxCacheResult<bool> {
self.inner.expire(key, ttl)
}
}
impl crate::backend::SyncCacheConnector for FailSetBackend {
fn health_check(&self) -> OxCacheResult<()> {
self.inner.health_check()
}
fn shutdown(&self) {}
fn backend_kind(&self) -> crate::backend::BackendKind {
self.inner.backend_kind()
}
}
fn make_failing_sync_cache() -> Cache<String, String> {
let moka = Arc::new(MokaMemoryBackend::new());
let mut cache: Cache<String, String> = Cache::new_with_backend(moka);
cache.set_sync_backend(Arc::new(FailSetBackend::new()));
cache
}
#[test]
fn test_get_or_option_sync_propagates_value_set_failure() {
let cache = make_failing_sync_cache();
let result = cache.get_or_option_sync(&"k-value-set-fail".to_string(), || {
Ok(Some("v".to_string()))
});
assert!(
result.is_err(),
"leader set failure must propagate, got {:?}",
result
);
}
#[test]
fn test_get_or_option_sync_propagates_sentinel_set_failure() {
let mut cache = make_failing_sync_cache();
cache.set_null_cache_ttl(Some(Duration::from_secs(60)));
let result = cache.get_or_option_sync(&"k-sentinel-set-fail".to_string(), || Ok(None));
assert!(
result.is_err(),
"sentinel set failure must propagate, got {:?}",
result
);
}
}
#[cfg(all(test, feature = "memory"))]
mod sentinel_race_tests {
use super::*;
use crate::backend::memory::MokaMemoryBackend;
use crate::backend::{CacheConnector, CacheReader, CacheWriter};
use std::sync::atomic::AtomicBool;
struct FirstGetMissBackend {
inner: Arc<MokaMemoryBackend>,
key: &'static str,
swallowed: AtomicBool,
}
impl FirstGetMissBackend {
fn new(key: &'static str) -> Self {
Self {
inner: Arc::new(MokaMemoryBackend::new()),
key,
swallowed: AtomicBool::new(false),
}
}
}
#[async_trait::async_trait]
impl CacheReader for FirstGetMissBackend {
async fn get(&self, key: &str) -> OxCacheResult<Option<Vec<u8>>> {
if key == self.key
&& !self
.swallowed
.swap(true, std::sync::atomic::Ordering::SeqCst)
{
return Ok(None);
}
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>> {
CacheReader::ttl(&*self.inner, key).await
}
async fn len(&self) -> OxCacheResult<u64> {
self.inner.len().await
}
async fn capacity(&self) -> OxCacheResult<u64> {
Ok(self.inner.capacity())
}
async fn stats(&self) -> OxCacheResult<HashMap<String, String>> {
self.inner.stats().await
}
}
#[async_trait::async_trait]
impl CacheWriter for FirstGetMissBackend {
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 FirstGetMissBackend {
async fn health_check(&self) -> OxCacheResult<()> {
self.inner.health_check().await
}
async fn shutdown(&self) {
self.inner.shutdown().await
}
fn backend_kind(&self) -> crate::backend::BackendKind {
self.inner.backend_kind()
}
}
#[tokio::test]
async fn get_or_option_returns_real_value_written_after_fast_path() {
let backend: Arc<dyn crate::backend::CacheBackend> =
Arc::new(FirstGetMissBackend::new("race-real"));
let mut cache: Cache<String, String> = Cache::new_with_backend(backend);
cache.set_null_cache_ttl(Some(Duration::from_secs(60)));
cache
.backend
.set(
Arc::from("race-real"),
Arc::new(b"\"real-value\"".to_vec()),
None,
)
.await
.unwrap();
let got = cache
.get_or_option(&"race-real".to_string(), || async {
Err(OxCacheError::Operation("fallback must not run".into()))
})
.await
.unwrap();
assert_eq!(
got,
Some("real-value".to_string()),
"真实值不得被 exists 判定误判为空值哨兵"
);
}
}
#[cfg(test)]
mod jitter_tests {
use super::*;
fn jitter_cache(factor: f64) -> Cache<String, String> {
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.unwrap();
let mut cache: Cache<String, String> = rt
.block_on(async { Cache::builder().build().await })
.unwrap();
cache.set_ttl_jitter_factor(factor);
cache
}
#[test]
fn jitter_samples_within_bounds_and_non_degenerate() {
let cache = jitter_cache(0.1);
let base = Duration::from_secs(60);
let low = base.mul_f64(0.9).as_millis() as u64;
let high = base.mul_f64(1.1).as_millis() as u64;
let mut samples: Vec<u64> = (0..10_000)
.map(|_| cache.apply_jitter(base).as_millis() as u64)
.collect();
assert!(
samples.iter().all(|&s| s >= low && s <= high),
"采样必须落在 ±factor 区间内 [{low}, {high}]"
);
samples.sort_unstable();
assert!(
samples[0] < samples[5_000] && samples[5_000] < samples[9_999],
"分布非退化:min < median < max,实际 {} / {} / {}",
samples[0],
samples[5_000],
samples[9_999]
);
}
#[test]
fn zero_factor_is_identity() {
let cache = jitter_cache(0.0);
let base = Duration::from_secs(60);
for _ in 0..100 {
assert_eq!(cache.apply_jitter(base), base);
}
}
}
#[cfg(all(test, feature = "memory"))]
mod get_or_with_ttl_tests {
use super::*;
use crate::backend::memory::MokaMemoryBackend;
use std::sync::Arc as StdArc;
#[tokio::test]
async fn get_or_with_ttl_caches_with_ttl() {
let cache: Cache<String, String> = Cache::builder().build().await.unwrap();
let v = cache
.get_or_with_ttl(
&"ttl-or".to_string(),
Some(Duration::from_secs(60)),
|| async { Ok("v".to_string()) },
)
.await
.unwrap();
assert_eq!(v, "v");
let ttl = cache
.ttl(&"ttl-or".to_string())
.await
.unwrap()
.expect("ttl 应存在");
assert!(
ttl >= Duration::from_secs(54) && ttl < Duration::from_secs(66),
"ttl {ttl:?} 应在抖动区间 [54s, 66s)"
);
let cache2: Cache<String, String> = Cache::builder().build().await.unwrap();
cache2
.get_or(&"no-ttl-or".to_string(), || async { Ok("v".to_string()) })
.await
.unwrap();
assert_eq!(
cache2.ttl(&"no-ttl-or".to_string()).await.unwrap(),
None,
"get_or 旧路径必须保持无 TTL 语义"
);
}
#[tokio::test]
async fn get_or_option_with_ttl_jitters_sentinel() {
let mut cache: Cache<String, String> = Cache::builder().build().await.unwrap();
cache.set_null_cache_ttl(Some(Duration::from_secs(30)));
let got = cache
.get_or_option_with_ttl(
&"sentinel-ttl".to_string(),
Some(Duration::from_secs(60)),
|| async { Ok(None) },
)
.await
.unwrap();
assert_eq!(got, None);
let ttl = cache
.ttl(&"sentinel-ttl".to_string())
.await
.unwrap()
.expect("哨兵应存在");
assert!(
ttl >= Duration::from_secs(27) && ttl < Duration::from_secs(33),
"哨兵 ttl {ttl:?} 应在抖动区间 [27s, 33s)"
);
let got2 = cache
.get_or_option_with_ttl(
&"sentinel-ttl".to_string(),
Some(Duration::from_secs(60)),
|| async { Err(OxCacheError::Operation("must not run".into())) },
)
.await
.unwrap();
assert_eq!(got2, None);
}
#[test]
fn get_or_with_ttl_sync_caches_with_ttl() {
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.unwrap();
let mut cache: Cache<String, String> = rt
.block_on(async { Cache::builder().build().await })
.unwrap();
cache.set_sync_backend(StdArc::new(MokaMemoryBackend::new()));
cache
.get_or_with_ttl_sync(
&"ttl-sync".to_string(),
Some(Duration::from_secs(60)),
|| Ok("v".to_string()),
)
.unwrap();
let ttl = cache
.ttl_sync(&"ttl-sync".to_string())
.unwrap()
.expect("ttl 应存在");
assert!(
ttl >= Duration::from_secs(54) && ttl < Duration::from_secs(66),
"ttl {ttl:?} 应在抖动区间 [54s, 66s)"
);
}
}
#[cfg(test)]
mod default_jitter_tests {
use super::*;
#[tokio::test]
async fn default_jitter_is_on_and_can_be_disabled() {
let cache: Cache<String, String> = Cache::builder().build().await.unwrap();
cache
.set_with_ttl(
&"dj-on".to_string(),
&"v".to_string(),
Some(Duration::from_secs(60)),
)
.await
.unwrap();
let ttl = cache.ttl(&"dj-on".to_string()).await.unwrap().unwrap();
assert!(
ttl >= Duration::from_secs(54) && ttl < Duration::from_secs(66),
"默认抖动应使 ttl ∈ [54s, 66s),实际 {ttl:?}"
);
let cache2: Cache<String, String> = Cache::builder().ttl_jitter(0.0).build().await.unwrap();
cache2
.set_with_ttl(
&"dj-off".to_string(),
&"v".to_string(),
Some(Duration::from_secs(60)),
)
.await
.unwrap();
let ttl2 = cache2.ttl(&"dj-off".to_string()).await.unwrap().unwrap();
assert!(
ttl2 > Duration::from_secs(58),
"关闭抖动后 ttl 应 ≈ 60s,实际 {ttl2:?}"
);
}
}