#![cfg(all(
feature = "macros",
feature = "l1",
not(feature = "unsync"),
not(target_arch = "wasm32")
))]
mod common;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::{Duration, Instant};
use common::MockBackend;
use cachekit::interop::{interop_key, InteropValue};
use cachekit::{cachekit, CacheKit, CachekitError};
use tokio::sync::Notify;
const ORIGIN_DELAY: Duration = Duration::from_millis(400);
fn client(backend: cachekit::SharedBackend) -> CacheKit {
CacheKit::builder()
.backend(backend)
.swr_threshold_ratio(0.25)
.build()
.expect("client builds")
}
fn key(operation: &str, id: u64) -> String {
interop_key("swrtest", operation, &[InteropValue::from(id)]).expect("test key is valid")
}
static SWR_CALLS: AtomicU32 = AtomicU32::new(0);
#[cachekit(client = cache, ttl = 4, interop = "swr_probe", namespace = "swrtest")]
async fn swr_probe(cache: &CacheKit, id: u64) -> Result<String, CachekitError> {
let n = SWR_CALLS.fetch_add(1, Ordering::SeqCst) + 1;
tokio::time::sleep(ORIGIN_DELAY).await;
Ok(format!("u{id}-c{n}"))
}
#[tokio::test]
async fn stale_reads_serve_immediately_and_refresh_exactly_once() {
let cache = client(MockBackend::shared());
assert_eq!(swr_probe(&cache, 7).await.unwrap(), "u7-c1");
assert_eq!(SWR_CALLS.load(Ordering::SeqCst), 1);
tokio::time::sleep(Duration::from_millis(1400)).await;
let started = Instant::now();
let mut set = tokio::task::JoinSet::new();
for _ in 0..8 {
let clone = cache.clone();
set.spawn(async move { swr_probe(&clone, 7).await });
}
let mut reads = Vec::new();
while let Some(joined) = set.join_next().await {
reads.push(joined.expect("reader task panicked"));
}
let elapsed = started.elapsed();
assert_eq!(reads.len(), 8);
for read in reads {
assert_eq!(read.unwrap(), "u7-c1", "stale value is served as-is");
}
assert!(
elapsed < Duration::from_millis(300),
"stale reads must not block on the origin: took {elapsed:?}"
);
tokio::time::sleep(ORIGIN_DELAY + Duration::from_millis(800)).await;
assert_eq!(
SWR_CALLS.load(Ordering::SeqCst),
2,
"8 concurrent stale readers must trigger exactly one refresh"
);
assert_eq!(swr_probe(&cache, 7).await.unwrap(), "u7-c2");
assert_eq!(SWR_CALLS.load(Ordering::SeqCst), 2);
}
static DELETE_RACE_CALLS: AtomicU32 = AtomicU32::new(0);
static DELETE_REFRESH_STARTED: Notify = Notify::const_new();
static DELETE_REFRESH_RELEASE: Notify = Notify::const_new();
#[cachekit(
client = cache,
ttl = 4,
interop = "swr_delete_race",
namespace = "swrtest"
)]
async fn swr_delete_race(cache: &CacheKit, id: u64) -> Result<String, CachekitError> {
let n = DELETE_RACE_CALLS.fetch_add(1, Ordering::SeqCst) + 1;
if n == 2 {
DELETE_REFRESH_STARTED.notify_one();
DELETE_REFRESH_RELEASE.notified().await;
}
Ok(format!("d{id}-c{n}"))
}
#[tokio::test]
async fn concurrent_delete_wins_over_an_older_refresh() {
let cache = client(MockBackend::shared());
let storage_key = key("swr_delete_race", 11);
assert_eq!(swr_delete_race(&cache, 11).await.unwrap(), "d11-c1");
tokio::time::sleep(Duration::from_millis(1400)).await;
assert_eq!(swr_delete_race(&cache, 11).await.unwrap(), "d11-c1");
tokio::time::timeout(Duration::from_secs(2), DELETE_REFRESH_STARTED.notified())
.await
.expect("refresh origin started");
assert!(cache.delete(&storage_key).await.unwrap());
DELETE_REFRESH_RELEASE.notify_one();
let flight = cache.single_flight(&storage_key).await;
flight.release().await;
let value: Option<String> = cache.interop_get(&storage_key).await.unwrap();
assert_eq!(value, None, "completed refresh must not resurrect a delete");
assert_eq!(DELETE_RACE_CALLS.load(Ordering::SeqCst), 2);
}
#[cfg(feature = "encryption")]
static SECURE_DELETE_RACE_CALLS: AtomicU32 = AtomicU32::new(0);
#[cfg(feature = "encryption")]
static SECURE_DELETE_REFRESH_STARTED: Notify = Notify::const_new();
#[cfg(feature = "encryption")]
static SECURE_DELETE_REFRESH_RELEASE: Notify = Notify::const_new();
#[cfg(feature = "encryption")]
#[cachekit(
client = cache,
ttl = 4,
interop = "swr_secure_delete_race",
namespace = "swrtest",
secure
)]
async fn swr_secure_delete_race(cache: &CacheKit, id: u64) -> Result<String, CachekitError> {
let n = SECURE_DELETE_RACE_CALLS.fetch_add(1, Ordering::SeqCst) + 1;
if n == 2 {
SECURE_DELETE_REFRESH_STARTED.notify_one();
SECURE_DELETE_REFRESH_RELEASE.notified().await;
}
Ok(format!("sd{id}-c{n}"))
}
#[cfg(feature = "encryption")]
#[tokio::test]
async fn concurrent_delete_wins_over_an_older_secure_refresh() {
let cache = CacheKit::builder()
.backend(MockBackend::shared())
.swr_threshold_ratio(0.25)
.encryption_from_bytes(&[7_u8; 32], "swr-race-tenant")
.expect("encryption configures")
.build()
.expect("client builds");
let storage_key = key("swr_secure_delete_race", 13);
assert_eq!(swr_secure_delete_race(&cache, 13).await.unwrap(), "sd13-c1");
tokio::time::sleep(Duration::from_millis(1400)).await;
assert_eq!(swr_secure_delete_race(&cache, 13).await.unwrap(), "sd13-c1");
tokio::time::timeout(
Duration::from_secs(2),
SECURE_DELETE_REFRESH_STARTED.notified(),
)
.await
.expect("secure refresh origin started");
assert!(cache.secure().unwrap().delete(&storage_key).await.unwrap());
SECURE_DELETE_REFRESH_RELEASE.notify_one();
let flight = cache.single_flight(&storage_key).await;
flight.release().await;
let value: Option<String> = cache
.secure()
.unwrap()
.interop_get(&storage_key)
.await
.unwrap();
assert_eq!(value, None, "secure refresh must not resurrect a delete");
assert_eq!(SECURE_DELETE_RACE_CALLS.load(Ordering::SeqCst), 2);
}
static SET_RACE_CALLS: AtomicU32 = AtomicU32::new(0);
static SET_REFRESH_STARTED: Notify = Notify::const_new();
static SET_REFRESH_RELEASE: Notify = Notify::const_new();
#[cachekit(
client = cache,
ttl = 4,
interop = "swr_set_race",
namespace = "swrtest"
)]
async fn swr_set_race(cache: &CacheKit, id: u64) -> Result<String, CachekitError> {
let n = SET_RACE_CALLS.fetch_add(1, Ordering::SeqCst) + 1;
if n == 2 {
SET_REFRESH_STARTED.notify_one();
SET_REFRESH_RELEASE.notified().await;
}
Ok(format!("s{id}-c{n}"))
}
#[tokio::test]
async fn concurrent_set_wins_over_an_older_refresh() {
let cache = client(MockBackend::shared());
let storage_key = key("swr_set_race", 12);
assert_eq!(swr_set_race(&cache, 12).await.unwrap(), "s12-c1");
tokio::time::sleep(Duration::from_millis(1400)).await;
assert_eq!(swr_set_race(&cache, 12).await.unwrap(), "s12-c1");
tokio::time::timeout(Duration::from_secs(2), SET_REFRESH_STARTED.notified())
.await
.expect("refresh origin started");
cache
.clone()
.set_with_ttl(
&storage_key,
&"manual-write".to_owned(),
Duration::from_secs(4),
)
.await
.unwrap();
SET_REFRESH_RELEASE.notify_one();
let flight = cache.single_flight(&storage_key).await;
flight.release().await;
let value: Option<String> = cache.interop_get(&storage_key).await.unwrap();
assert_eq!(value.as_deref(), Some("manual-write"));
assert_eq!(SET_RACE_CALLS.load(Ordering::SeqCst), 2);
}
static RENEW_CALLS: AtomicU32 = AtomicU32::new(0);
static RENEW_REFRESH_STARTED: Notify = Notify::const_new();
#[cachekit(
client = cache,
ttl = 2,
interop = "swr_expiry_renewal",
namespace = "swrtest"
)]
async fn swr_expiry_renewal(cache: &CacheKit, id: u64) -> Result<String, CachekitError> {
let n = RENEW_CALLS.fetch_add(1, Ordering::SeqCst) + 1;
if n == 2 {
RENEW_REFRESH_STARTED.notify_one();
}
Ok(format!("r{id}-c{n}"))
}
#[tokio::test]
async fn refresh_renews_the_full_l1_ttl() {
let (backend, handle) = MockBackend::new_with_handle();
let cache = client(backend);
let storage_key = key("swr_expiry_renewal", 14);
assert_eq!(swr_expiry_renewal(&cache, 14).await.unwrap(), "r14-c1");
tokio::time::sleep(Duration::from_millis(700)).await;
assert_eq!(swr_expiry_renewal(&cache, 14).await.unwrap(), "r14-c1");
tokio::time::timeout(Duration::from_secs(2), RENEW_REFRESH_STARTED.notified())
.await
.expect("refresh origin started");
let flight = cache.single_flight(&storage_key).await;
flight.release().await;
assert_eq!(RENEW_CALLS.load(Ordering::SeqCst), 2);
handle.store.lock().await.clear();
tokio::time::sleep(Duration::from_millis(1500)).await;
let value: Option<String> = cache.interop_get(&storage_key).await.unwrap();
assert_eq!(value.as_deref(), Some("r14-c2"));
}
static SLOW_RENEW_CALLS: AtomicU32 = AtomicU32::new(0);
static SLOW_RENEW_STARTED: Notify = Notify::const_new();
static SLOW_RENEW_RELEASE: Notify = Notify::const_new();
#[cachekit(
client = cache,
ttl = 2,
interop = "swr_slow_expiry_renewal",
namespace = "swrtest"
)]
async fn swr_slow_expiry_renewal(cache: &CacheKit, id: u64) -> Result<String, CachekitError> {
let n = SLOW_RENEW_CALLS.fetch_add(1, Ordering::SeqCst) + 1;
if n == 2 {
SLOW_RENEW_STARTED.notify_one();
SLOW_RENEW_RELEASE.notified().await;
}
Ok(format!("sr{id}-c{n}"))
}
#[tokio::test]
async fn slow_refresh_can_commit_after_the_original_entry_expires() {
let (backend, handle) = MockBackend::new_with_handle();
let cache = client(backend);
let storage_key = key("swr_slow_expiry_renewal", 15);
assert_eq!(
swr_slow_expiry_renewal(&cache, 15).await.unwrap(),
"sr15-c1"
);
tokio::time::sleep(Duration::from_millis(700)).await;
assert_eq!(
swr_slow_expiry_renewal(&cache, 15).await.unwrap(),
"sr15-c1"
);
tokio::time::timeout(Duration::from_secs(2), SLOW_RENEW_STARTED.notified())
.await
.expect("slow refresh origin started");
tokio::time::sleep(Duration::from_millis(1500)).await;
SLOW_RENEW_RELEASE.notify_one();
let flight = cache.single_flight(&storage_key).await;
flight.release().await;
assert_eq!(SLOW_RENEW_CALLS.load(Ordering::SeqCst), 2);
handle.store.lock().await.clear();
let value: Option<String> = cache.interop_get(&storage_key).await.unwrap();
assert_eq!(value.as_deref(), Some("sr15-c2"));
}
static EXPIRY_CALLS: AtomicU32 = AtomicU32::new(0);
#[cachekit(client = cache, ttl = 2, interop = "swr_expiry", namespace = "swrtest")]
async fn swr_expiry_probe(cache: &CacheKit, id: u64) -> Result<String, CachekitError> {
let n = EXPIRY_CALLS.fetch_add(1, Ordering::SeqCst) + 1;
tokio::time::sleep(ORIGIN_DELAY).await;
Ok(format!("e{id}-c{n}"))
}
#[tokio::test]
async fn hard_expired_entry_takes_the_blocking_miss_path() {
let (backend, handle) = MockBackend::new_with_handle();
let cache = client(backend);
assert_eq!(swr_expiry_probe(&cache, 3).await.unwrap(), "e3-c1");
tokio::time::sleep(Duration::from_millis(2500)).await;
handle.store.lock().await.clear();
let started = Instant::now();
let value = swr_expiry_probe(&cache, 3).await.unwrap();
let elapsed = started.elapsed();
assert_eq!(value, "e3-c2", "hard-expired read must recompute");
assert!(
elapsed >= ORIGIN_DELAY,
"hard-expired read must block on the origin: took {elapsed:?}"
);
assert_eq!(EXPIRY_CALLS.load(Ordering::SeqCst), 2);
}
static FRESH_CALLS: AtomicU32 = AtomicU32::new(0);
#[cachekit(client = cache, ttl = 60, interop = "swr_fresh", namespace = "swrtest")]
async fn swr_fresh_probe(cache: &CacheKit, id: u64) -> Result<String, CachekitError> {
let n = FRESH_CALLS.fetch_add(1, Ordering::SeqCst) + 1;
Ok(format!("f{id}-c{n}"))
}
#[tokio::test]
async fn fresh_read_does_not_schedule_a_refresh() {
let cache = client(MockBackend::shared());
assert_eq!(swr_fresh_probe(&cache, 1).await.unwrap(), "f1-c1");
assert_eq!(swr_fresh_probe(&cache, 1).await.unwrap(), "f1-c1");
tokio::time::sleep(Duration::from_millis(300)).await;
assert_eq!(
FRESH_CALLS.load(Ordering::SeqCst),
1,
"a fresh hit must not spawn background work"
);
}
static OFF_CALLS: AtomicU32 = AtomicU32::new(0);
#[cachekit(client = cache, ttl = 4, interop = "swr_off", namespace = "swrtest")]
async fn swr_off_probe(cache: &CacheKit, id: u64) -> Result<String, CachekitError> {
let n = OFF_CALLS.fetch_add(1, Ordering::SeqCst) + 1;
Ok(format!("o{id}-c{n}"))
}
#[tokio::test]
async fn disabled_swr_serves_until_hard_expiry_without_refreshing() {
let cache = CacheKit::builder()
.backend(MockBackend::shared())
.swr_enabled(false)
.swr_threshold_ratio(0.25)
.build()
.unwrap();
assert_eq!(swr_off_probe(&cache, 5).await.unwrap(), "o5-c1");
tokio::time::sleep(Duration::from_millis(1500)).await;
assert_eq!(swr_off_probe(&cache, 5).await.unwrap(), "o5-c1");
tokio::time::sleep(Duration::from_millis(300)).await;
assert_eq!(
OFF_CALLS.load(Ordering::SeqCst),
1,
"SWR off must mean zero background refreshes"
);
}
static STR_CALLS: AtomicU32 = AtomicU32::new(0);
#[cachekit(client = cache, ttl = 4, interop = "swr_str", namespace = "swrtest")]
async fn swr_str_probe(cache: &CacheKit, name: &str) -> Result<String, CachekitError> {
let n = STR_CALLS.fetch_add(1, Ordering::SeqCst) + 1;
Ok(format!("{name}-c{n}"))
}
#[tokio::test]
async fn str_argument_refreshes_in_the_background() {
let cache = client(MockBackend::shared());
assert_eq!(swr_str_probe(&cache, "ada").await.unwrap(), "ada-c1");
tokio::time::sleep(Duration::from_millis(1400)).await;
assert_eq!(swr_str_probe(&cache, "ada").await.unwrap(), "ada-c1");
tokio::time::sleep(Duration::from_millis(500)).await;
assert_eq!(STR_CALLS.load(Ordering::SeqCst), 2, "one refresh ran");
assert_eq!(swr_str_probe(&cache, "ada").await.unwrap(), "ada-c2");
}
#[tokio::test]
async fn threshold_ratio_is_validated_at_build() {
for bad in [0.0, -0.5, 1.5, f64::NAN] {
let Err(err) = CacheKit::builder()
.backend(MockBackend::shared())
.swr_threshold_ratio(bad)
.build()
else {
panic!("out-of-range ratio {bad} must fail at build");
};
assert!(matches!(err, CachekitError::Config(_)), "got {err:?}");
}
assert!(
CacheKit::builder()
.backend(MockBackend::shared())
.swr_threshold_ratio(1.0)
.build()
.is_ok(),
"ratio 1.0 is legal"
);
}
#[tokio::test]
async fn clones_share_l1_state() {
let (backend, handle) = MockBackend::new_with_handle();
let cache = CacheKit::builder().backend(backend).build().unwrap();
let clone = cache.clone();
cache.set("shared", &"value".to_owned()).await.unwrap();
handle.store.lock().await.clear();
let via_clone: Option<String> = clone.get("shared").await.unwrap();
assert_eq!(via_clone.as_deref(), Some("value"));
}