#![cfg(feature = "proc_macro")]
use std::sync::atomic::{AtomicUsize, Ordering};
use cached::Expires;
use cached::macros::concurrent_cached;
#[derive(Clone, Debug, PartialEq, Eq)]
struct Stamped {
generation: usize,
expired: bool,
}
impl Expires for Stamped {
fn is_expired(&self) -> bool {
self.expired
}
}
static UNBOUNDED_CALLS: AtomicUsize = AtomicUsize::new(0);
#[concurrent_cached(expires = true, result_fallback = true)]
fn expiring_fallback(key: u32) -> Result<Stamped, String> {
let n = UNBOUNDED_CALLS.fetch_add(1, Ordering::SeqCst);
let _ = key;
if n == 0 {
Ok(Stamped {
generation: 1,
expired: true,
})
} else {
Err("refresh failed".to_string())
}
}
#[test]
fn expires_result_fallback_serves_last_ok_on_err() {
UNBOUNDED_CALLS.store(0, Ordering::SeqCst);
let first = expiring_fallback(1).expect("first call computes and caches an `Ok`");
assert_eq!(first.generation, 1);
assert_eq!(UNBOUNDED_CALLS.load(Ordering::SeqCst), 1);
let second = expiring_fallback(1).expect("`Err` refresh falls back to the last cached `Ok`");
assert_eq!(
second.generation, 1,
"fallback returns the value cached by the first call"
);
assert_eq!(
UNBOUNDED_CALLS.load(Ordering::SeqCst),
2,
"the expired entry is recomputed rather than served as a hit"
);
let third = expiring_fallback(1).expect("fallback keeps serving the last cached `Ok`");
assert_eq!(third.generation, 1);
assert_eq!(UNBOUNDED_CALLS.load(Ordering::SeqCst), 3);
assert!(
expiring_fallback(2).is_err(),
"a key with no cached `Ok` propagates the error"
);
}
static LIVE_CALLS: AtomicUsize = AtomicUsize::new(0);
#[concurrent_cached(expires = true, result_fallback = true)]
fn expiring_fallback_live(key: u32) -> Result<Stamped, String> {
let n = LIVE_CALLS.fetch_add(1, Ordering::SeqCst);
let _ = key;
if n == 0 {
Ok(Stamped {
generation: 1,
expired: false,
})
} else {
Err("should not be reached".to_string())
}
}
#[test]
fn expires_result_fallback_hits_live_entry_without_running_body() {
LIVE_CALLS.store(0, Ordering::SeqCst);
assert_eq!(expiring_fallback_live(7).unwrap().generation, 1);
assert_eq!(expiring_fallback_live(7).unwrap().generation, 1);
assert_eq!(expiring_fallback_live(7).unwrap().generation, 1);
assert_eq!(
LIVE_CALLS.load(Ordering::SeqCst),
1,
"an unexpired entry is returned from the cache; the body runs once"
);
}
static LRU_CALLS: AtomicUsize = AtomicUsize::new(0);
#[concurrent_cached(expires = true, result_fallback = true, max_size = 8)]
fn expiring_lru_fallback(key: u32) -> Result<Stamped, String> {
let n = LRU_CALLS.fetch_add(1, Ordering::SeqCst);
let _ = key;
if n == 0 {
Ok(Stamped {
generation: 42,
expired: true,
})
} else {
Err("refresh failed".to_string())
}
}
#[test]
fn expires_max_size_result_fallback_serves_last_ok_on_err() {
LRU_CALLS.store(0, Ordering::SeqCst);
assert_eq!(expiring_lru_fallback(3).unwrap().generation, 42);
let stale =
expiring_lru_fallback(3).expect("`Err` refresh falls back on the LRU expiring store");
assert_eq!(stale.generation, 42);
assert_eq!(LRU_CALLS.load(Ordering::SeqCst), 2);
}
static FORCED_CALLS: AtomicUsize = AtomicUsize::new(0);
#[concurrent_cached(
expires = true,
result_fallback = true,
key = "u32",
convert = "{ key }",
force_refresh = "{ bypass }"
)]
fn expiring_forced_fallback(key: u32, bypass: bool) -> Result<Stamped, String> {
let _ = bypass; let _ = key;
let n = FORCED_CALLS.fetch_add(1, Ordering::SeqCst);
if n == 0 {
Ok(Stamped {
generation: 9,
expired: false,
})
} else {
Err("refresh failed".to_string())
}
}
#[test]
fn expires_result_fallback_with_force_refresh_recovers_stale_ok() {
FORCED_CALLS.store(0, Ordering::SeqCst);
assert_eq!(
expiring_forced_fallback(9, false).unwrap().generation,
9,
"first call computes and caches"
);
assert_eq!(
expiring_forced_fallback(9, true).unwrap().generation,
9,
"force_refresh bypass still falls back to the cached `Ok`"
);
assert_eq!(FORCED_CALLS.load(Ordering::SeqCst), 2);
}
#[cfg(feature = "async")]
mod async_tests {
use super::*;
static ASYNC_CALLS: AtomicUsize = AtomicUsize::new(0);
#[concurrent_cached(expires = true, result_fallback = true)]
async fn expiring_fallback_async(key: u32) -> Result<Stamped, String> {
let n = ASYNC_CALLS.fetch_add(1, Ordering::SeqCst);
let _ = key;
if n == 0 {
Ok(Stamped {
generation: 5,
expired: true,
})
} else {
Err("refresh failed".to_string())
}
}
#[tokio::test]
async fn async_expires_result_fallback_serves_last_ok_on_err() {
ASYNC_CALLS.store(0, Ordering::SeqCst);
assert_eq!(expiring_fallback_async(1).await.unwrap().generation, 5);
let stale = expiring_fallback_async(1)
.await
.expect("`Err` refresh falls back to the last cached `Ok`");
assert_eq!(stale.generation, 5);
assert_eq!(ASYNC_CALLS.load(Ordering::SeqCst), 2);
}
}