#![cfg(feature = "full")]
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;
use oxcache::Cache;
use oxcache::features::stale::StalePolicy;
#[tokio::test(flavor = "multi_thread")]
async fn sync_misc_surface() {
let cache: Cache<String, String> = Cache::builder().sync_mode(true).build().await.unwrap();
cache.set_sync(&"a".to_string(), &"1".to_string()).unwrap();
cache.set_sync(&"b".to_string(), &"2".to_string()).unwrap();
let _ = cache.len_sync().unwrap();
let _cap = cache.capacity_sync().unwrap();
let stats = cache.stats_sync().unwrap();
let _ = stats;
cache.health_check_sync().unwrap();
cache.clear_sync().unwrap();
let _ = cache.len_sync().unwrap();
cache.shutdown_sync();
}
#[tokio::test(flavor = "multi_thread")]
async fn get_or_option_sync_value_none_and_error() {
let cache: Cache<String, String> = Cache::builder().sync_mode(true).build().await.unwrap();
let calls = AtomicU32::new(0);
let v = cache
.get_or_option_sync(&"k:1".to_string(), || {
calls.fetch_add(1, Ordering::SeqCst);
Ok(Some("v1".to_string()))
})
.unwrap();
assert_eq!(v.as_deref(), Some("v1"));
let v2 = cache
.get_or_option_sync(&"k:1".to_string(), || {
calls.fetch_add(1, Ordering::SeqCst);
Ok(Some("other".to_string()))
})
.unwrap();
assert_eq!(v2.as_deref(), Some("v1"));
assert_eq!(calls.load(Ordering::SeqCst), 1);
let n = cache
.get_or_option_sync(&"k:2".to_string(), || {
Ok::<Option<String>, oxcache::OxCacheError>(None)
})
.unwrap();
assert!(n.is_none());
let err = cache
.get_or_option_sync(&"k:3".to_string(), || {
Err(oxcache::OxCacheError::L1Error("boom".to_string()))
})
.unwrap_err();
assert!(err.to_string().contains("boom"));
}
#[tokio::test(flavor = "multi_thread")]
async fn get_or_option_sync_null_sentinel_short_circuit() {
let cache: Cache<String, String> = Cache::builder()
.sync_mode(true)
.null_cache_ttl(Duration::from_secs(30))
.build()
.await
.unwrap();
let calls = AtomicU32::new(0);
let first = cache
.get_or_option_sync(&"nk".to_string(), || {
calls.fetch_add(1, Ordering::SeqCst);
Ok::<Option<String>, oxcache::OxCacheError>(None)
})
.unwrap();
assert!(first.is_none());
assert_eq!(calls.load(Ordering::SeqCst), 1);
let second = cache
.get_or_option_sync(&"nk".to_string(), || {
calls.fetch_add(1, Ordering::SeqCst);
Ok::<Option<String>, oxcache::OxCacheError>(None)
})
.unwrap();
assert!(second.is_none());
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[tokio::test(flavor = "multi_thread")]
async fn get_or_option_with_ttl_sync_variants() {
let cache: Cache<String, String> = Cache::builder()
.sync_mode(true)
.null_cache_ttl(Duration::from_secs(30))
.build()
.await
.unwrap();
let v = cache
.get_or_option_with_ttl_sync(&"tk".to_string(), Some(Duration::from_secs(60)), || {
Ok(Some("tv".to_string()))
})
.unwrap();
assert_eq!(v.as_deref(), Some("tv"));
let n = cache
.get_or_option_with_ttl_sync(&"tn".to_string(), Some(Duration::from_secs(60)), || {
Ok::<Option<String>, oxcache::OxCacheError>(None)
})
.unwrap();
assert!(n.is_none());
let n2 = cache
.get_or_option_with_ttl_sync(&"tn".to_string(), Some(Duration::from_secs(60)), || {
Ok::<Option<String>, oxcache::OxCacheError>(None)
})
.unwrap();
assert!(n2.is_none());
}
#[tokio::test(flavor = "multi_thread")]
async fn sync_api_without_sync_mode_fails_explicitly() {
let cache: Cache<String, String> = Cache::builder().build().await.unwrap();
let err = cache
.get_or_sync(&"k".to_string(), || Ok("v".to_string()))
.unwrap_err();
assert!(!err.to_string().is_empty());
let err = cache.get_sync(&"k".to_string()).unwrap_err();
assert!(!err.to_string().is_empty());
}
#[tokio::test(flavor = "multi_thread")]
async fn get_or_sync_concurrent_leader_follower() {
let cache: Arc<Cache<String, String>> =
Arc::new(Cache::builder().sync_mode(true).build().await.unwrap());
let calls = Arc::new(AtomicU32::new(0));
let mut handles = Vec::new();
for _ in 0..4 {
let cache = Arc::clone(&cache);
let calls = Arc::clone(&calls);
handles.push(tokio::task::spawn_blocking(move || {
cache
.get_or_sync(&"contended".to_string(), || {
calls.fetch_add(1, Ordering::SeqCst);
std::thread::sleep(Duration::from_millis(120));
Ok("computed".to_string())
})
.unwrap()
}));
}
for h in handles {
assert_eq!(h.await.unwrap(), "computed");
}
let executed = calls.load(Ordering::SeqCst);
assert!((1..=4).contains(&executed), "executed={executed}");
let again = cache
.get_or_sync(&"contended".to_string(), || {
calls.fetch_add(1, Ordering::SeqCst);
Ok("should-not-run".to_string())
})
.unwrap();
assert_eq!(again, "computed");
}
async fn build_stale_cache(policy: StalePolicy) -> Cache<String, String> {
Cache::builder()
.stale_ttl(Duration::from_secs(2))
.stale_policy(policy)
.build()
.await
.unwrap()
}
#[tokio::test(flavor = "multi_thread")]
async fn stale_return_policy_serves_old_value() {
let cache = build_stale_cache(StalePolicy::Return).await;
let calls = AtomicU32::new(0);
let v = cache
.get_or_with_ttl(&"s:1".to_string(), Some(Duration::from_millis(80)), || {
calls.fetch_add(1, Ordering::SeqCst);
async { Ok("fresh1".to_string()) }
})
.await
.unwrap();
assert_eq!(v, "fresh1");
tokio::time::sleep(Duration::from_millis(200)).await;
let v2 = cache
.get_or_with_ttl(&"s:1".to_string(), Some(Duration::from_millis(80)), || {
calls.fetch_add(1, Ordering::SeqCst);
async { Ok("fresh2".to_string()) }
})
.await
.unwrap();
assert_eq!(v2, "fresh1", "Return 策略:stale 命中返回旧值");
assert_eq!(calls.load(Ordering::SeqCst), 1, "stale 命中不执行 fallback");
}
#[tokio::test(flavor = "multi_thread")]
async fn stale_revalidate_policy_treats_as_miss() {
let cache = build_stale_cache(StalePolicy::Revalidate).await;
let calls = AtomicU32::new(0);
let v = cache
.get_or_with_ttl(&"s:2".to_string(), Some(Duration::from_millis(80)), || {
calls.fetch_add(1, Ordering::SeqCst);
async { Ok("v1".to_string()) }
})
.await
.unwrap();
assert_eq!(v, "v1");
tokio::time::sleep(Duration::from_millis(200)).await;
let v2 = cache
.get_or_with_ttl(&"s:2".to_string(), Some(Duration::from_millis(80)), || {
calls.fetch_add(1, Ordering::SeqCst);
async { Ok("v2".to_string()) }
})
.await
.unwrap();
assert_eq!(v2, "v2", "Revalidate 策略:stale 视同 miss 并刷新");
assert_eq!(calls.load(Ordering::SeqCst), 2);
}
#[tokio::test(flavor = "multi_thread")]
async fn stale_offload_policy_returns_old_and_refreshes_background() {
let cache = build_stale_cache(StalePolicy::OffloadRevalidate).await;
let calls = Arc::new(AtomicU32::new(0));
let v = cache
.get_or_refresh(&"s:3".to_string(), Some(Duration::from_millis(80)), {
let calls = Arc::clone(&calls);
move || {
calls.fetch_add(1, Ordering::SeqCst);
async move { Ok("gen1".to_string()) }
}
})
.await
.unwrap();
assert_eq!(v, "gen1");
tokio::time::sleep(Duration::from_millis(200)).await;
let v2 = cache
.get_or_refresh(&"s:3".to_string(), Some(Duration::from_millis(80)), {
let calls = Arc::clone(&calls);
move || {
calls.fetch_add(1, Ordering::SeqCst);
async move { Ok("gen2".to_string()) }
}
})
.await
.unwrap();
assert_eq!(v2, "gen1", "OffloadRevalidate:立即返回旧值");
assert_eq!(calls.load(Ordering::SeqCst), 1);
for _ in 0..20 {
tokio::time::sleep(Duration::from_millis(100)).await;
if let Ok(Some(v)) = cache.get(&"s:3".to_string()).await
&& v == "gen2"
{
return;
}
}
panic!("offload 后台刷新未落盘");
}
#[tokio::test(flavor = "multi_thread")]
async fn stale_get_or_refresh_revalidate_and_return_paths() {
let cache = build_stale_cache(StalePolicy::Return).await;
let calls = Arc::new(AtomicU32::new(0));
let v = cache
.get_or_refresh(&"r:1".to_string(), Some(Duration::from_millis(80)), {
let calls = Arc::clone(&calls);
move || {
calls.fetch_add(1, Ordering::SeqCst);
async move { Ok("old".to_string()) }
}
})
.await
.unwrap();
assert_eq!(v, "old");
tokio::time::sleep(Duration::from_millis(200)).await;
let v2 = cache
.get_or_refresh(&"r:1".to_string(), Some(Duration::from_millis(80)), {
let calls = Arc::clone(&calls);
move || {
calls.fetch_add(1, Ordering::SeqCst);
async move { Ok("new".to_string()) }
}
})
.await
.unwrap();
assert_eq!(v2, "old");
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[tokio::test(flavor = "multi_thread")]
async fn stale_miss_falls_through_to_normal_get_or() {
let cache = build_stale_cache(StalePolicy::Return).await;
let v = cache
.get_or_with_ttl(&"s:none".to_string(), None, || async {
Ok("plain".to_string())
})
.await
.unwrap();
assert_eq!(v, "plain");
}
#[tokio::test(flavor = "multi_thread")]
async fn get_or_async_concurrent_follower_and_double_check() {
let cache: Arc<Cache<String, String>> = Arc::new(Cache::builder().build().await.unwrap());
let calls = Arc::new(AtomicU32::new(0));
let mut handles = Vec::new();
for i in 0..8 {
let cache = Arc::clone(&cache);
let calls = Arc::clone(&calls);
handles.push(tokio::spawn(async move {
cache
.get_or(&"hot".to_string(), move || {
let calls = Arc::clone(&calls);
async move {
calls.fetch_add(1, Ordering::SeqCst);
tokio::time::sleep(Duration::from_millis(250)).await;
let _ = i;
Ok("computed".to_string())
}
})
.await
}));
}
for h in handles {
assert_eq!(h.await.unwrap().unwrap(), "computed");
}
let executed = calls.load(Ordering::SeqCst);
assert!((1..=8).contains(&executed), "executed={executed}");
let again = cache
.get_or(&"hot".to_string(), || async { Ok("nope".to_string()) })
.await
.unwrap();
assert_eq!(again, "computed");
}
#[tokio::test(flavor = "multi_thread")]
async fn get_or_option_async_follower_value_and_leader_failure() {
let cache: Arc<Cache<String, String>> = Arc::new(Cache::builder().build().await.unwrap());
let c1 = Arc::clone(&cache);
let leader = tokio::spawn(async move {
c1.get_or_option(&"fv".to_string(), || async {
tokio::time::sleep(Duration::from_millis(200)).await;
Ok(Some("lv".to_string()))
})
.await
});
tokio::time::sleep(Duration::from_millis(50)).await;
let follower_val = cache
.get_or_option(&"fv".to_string(), || async {
Ok(Some("ignored".to_string()))
})
.await
.unwrap();
assert_eq!(follower_val.as_deref(), Some("lv"));
assert_eq!(leader.await.unwrap().unwrap().as_deref(), Some("lv"));
let c2 = Arc::clone(&cache);
let failing = tokio::spawn(async move {
c2.get_or_option(&"ff".to_string(), || async {
tokio::time::sleep(Duration::from_millis(200)).await;
Err(oxcache::OxCacheError::L1Error("boom".to_string()))
})
.await
});
tokio::time::sleep(Duration::from_millis(50)).await;
let err = cache
.get_or_option(&"ff".to_string(), || async {
Ok(Some("ignored".to_string()))
})
.await
.unwrap_err();
assert!(!err.to_string().is_empty());
let _ = failing.await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn get_or_option_with_ttl_async_value_path() {
let cache: Cache<String, String> = Cache::builder().build().await.unwrap();
let v = cache
.get_or_option_with_ttl(
&"ttl-obj".to_string(),
Some(Duration::from_secs(30)),
|| async { Ok(Some("tv".to_string())) },
)
.await
.unwrap();
assert_eq!(v.as_deref(), Some("tv"));
}
#[tokio::test(flavor = "multi_thread")]
async fn get_or_option_sync_concurrent_follower_paths() {
let cache: Arc<Cache<String, String>> =
Arc::new(Cache::builder().sync_mode(true).build().await.unwrap());
let mut handles = Vec::new();
for _ in 0..6 {
let cache = Arc::clone(&cache);
handles.push(tokio::task::spawn_blocking(move || {
cache
.get_or_option_sync(&"of".to_string(), || {
std::thread::sleep(Duration::from_millis(200));
Ok(Some("sv".to_string()))
})
.unwrap()
}));
}
for h in handles {
assert_eq!(h.await.unwrap().as_deref(), Some("sv"));
}
let sentinel_cache: Arc<Cache<String, String>> = Arc::new(
Cache::builder()
.sync_mode(true)
.null_cache_ttl(Duration::from_secs(30))
.build()
.await
.unwrap(),
);
let mut handles = Vec::new();
for _ in 0..4 {
let cache = Arc::clone(&sentinel_cache);
handles.push(tokio::task::spawn_blocking(move || {
cache
.get_or_option_sync(&"ns".to_string(), || {
std::thread::sleep(Duration::from_millis(200));
Ok::<Option<String>, oxcache::OxCacheError>(None)
})
.unwrap()
}));
}
for h in handles {
assert!(h.await.unwrap().is_none());
}
}
#[tokio::test(flavor = "multi_thread")]
async fn stale_sentinel_payload_treated_as_miss() {
let cache: Cache<String, String> = Cache::builder()
.stale_ttl(Duration::from_secs(2))
.stale_policy(StalePolicy::Return)
.null_cache_ttl(Duration::from_millis(70))
.build()
.await
.unwrap();
let none = cache
.get_or_option(&"sn".to_string(), || async {
Ok::<Option<String>, oxcache::OxCacheError>(None)
})
.await
.unwrap();
assert!(none.is_none());
tokio::time::sleep(Duration::from_millis(150)).await;
let calls = AtomicU32::new(0);
let v = cache
.get_or(&"sn".to_string(), || {
calls.fetch_add(1, Ordering::SeqCst);
async { Ok("recomputed".to_string()) }
})
.await
.unwrap();
assert_eq!(v, "recomputed", "哨兵 stale 命中按 miss 重新回源");
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[tokio::test(flavor = "multi_thread")]
async fn get_or_refresh_revalidate_deletes_stale_entry() {
let cache: Cache<String, String> = Cache::builder()
.stale_ttl(Duration::from_secs(2))
.stale_policy(StalePolicy::Revalidate)
.build()
.await
.unwrap();
let calls = Arc::new(AtomicU32::new(0));
let v = cache
.get_or_refresh(&"rr".to_string(), Some(Duration::from_millis(70)), {
let calls = Arc::clone(&calls);
move || {
calls.fetch_add(1, Ordering::SeqCst);
async move { Ok("gen1".to_string()) }
}
})
.await
.unwrap();
assert_eq!(v, "gen1");
tokio::time::sleep(Duration::from_millis(120)).await;
let v2 = cache
.get_or_refresh(&"rr".to_string(), Some(Duration::from_millis(70)), {
let calls = Arc::clone(&calls);
move || {
calls.fetch_add(1, Ordering::SeqCst);
async move { Ok("gen2".to_string()) }
}
})
.await
.unwrap();
assert_eq!(v2, "gen2");
assert_eq!(calls.load(Ordering::SeqCst), 2);
}