use std::collections::HashSet;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Mutex, PoisonError};
use std::time::Duration;
use cached::macros::{cached, concurrent_cached};
use cached::{CloneCached, ConcurrentCloneCached};
static COMPUTES: AtomicUsize = AtomicUsize::new(0);
fn compute_count() -> usize {
COMPUTES.load(Ordering::SeqCst)
}
#[cached(ttl_secs = 1, key = "String", convert = r#"{ id.to_string() }"#)]
fn sync_lookup(id: &str) -> String {
let n = COMPUTES.fetch_add(1, Ordering::SeqCst) + 1;
std::thread::sleep(Duration::from_millis(200));
format!("{id}-v{n}")
}
fn sync_lookup_swr(id: &str) -> String {
let (value, expired) = {
let cache = SYNC_LOOKUP.read();
cache.cache_peek_with_expiry_status(&id.to_string())
};
match value {
Some(v) if !expired => v,
Some(stale) => {
let owned = id.to_string();
std::thread::spawn(move || {
sync_lookup_prime_cache(&owned);
});
stale
}
None => sync_lookup(id),
}
}
#[cached(ttl_secs = 1, key = "String", convert = r#"{ id.to_string() }"#)]
async fn async_lookup(id: &str) -> String {
let n = COMPUTES.fetch_add(1, Ordering::SeqCst) + 1;
tokio::time::sleep(Duration::from_millis(200)).await;
format!("{id}-v{n}")
}
async fn async_lookup_swr(id: &str) -> String {
let (value, expired) = {
let cache = ASYNC_LOOKUP.read().await;
cache.cache_peek_with_expiry_status(&id.to_string())
};
match value {
Some(v) if !expired => v,
Some(stale) => {
let owned = id.to_string();
tokio::spawn(async move {
async_lookup_prime_cache(&owned).await;
});
stale
}
None => async_lookup(id).await,
}
}
#[concurrent_cached(ttl_secs = 1, key = "String", convert = r#"{ id.to_string() }"#)]
async fn sharded_lookup(id: &str) -> String {
let n = COMPUTES.fetch_add(1, Ordering::SeqCst) + 1;
tokio::time::sleep(Duration::from_millis(200)).await;
format!("{id}-v{n}")
}
async fn sharded_lookup_swr(id: &str) -> String {
let peeked = SHARDED_LOOKUP
.get()
.map(|cache| cache.cache_peek_with_expiry_status(&id.to_string()));
match peeked {
Some((Some(v), false)) => v,
Some((Some(stale), true)) => {
let owned = id.to_string();
tokio::spawn(async move {
sharded_lookup_prime_cache(&owned).await;
});
stale
}
_ => sharded_lookup(id).await,
}
}
static REFRESHING: Mutex<Option<HashSet<String>>> = Mutex::new(None);
struct RefreshClaim {
key: String,
}
impl RefreshClaim {
fn key(&self) -> &str {
&self.key
}
}
impl Drop for RefreshClaim {
fn drop(&mut self) {
release_refresh(&self.key);
}
}
fn claim_refresh(key: &str) -> Option<RefreshClaim> {
let mut guard = REFRESHING.lock().expect("refresh set poisoned");
if guard
.get_or_insert_with(HashSet::new)
.insert(key.to_string())
{
Some(RefreshClaim {
key: key.to_string(),
})
} else {
None
}
}
fn release_refresh(key: &str) {
let mut guard = REFRESHING.lock().unwrap_or_else(PoisonError::into_inner);
if let Some(set) = guard.as_mut() {
set.remove(key);
}
}
async fn async_lookup_swr_deduped(id: &str) -> String {
let (value, expired) = {
let cache = ASYNC_LOOKUP.read().await;
cache.cache_peek_with_expiry_status(&id.to_string())
};
match value {
Some(v) if !expired => v,
Some(stale) => {
if let Some(claim) = claim_refresh(id) {
tokio::spawn(async move {
async_lookup_prime_cache(claim.key()).await;
});
}
stale
}
None => async_lookup(id).await,
}
}
type PanicHook = dyn Fn(&std::panic::PanicHookInfo<'_>) + Sync + Send;
struct SilencedPanic {
previous: Option<Box<PanicHook>>,
}
impl SilencedPanic {
fn new() -> Self {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
Self {
previous: Some(previous),
}
}
}
impl Drop for SilencedPanic {
fn drop(&mut self) {
if let Some(previous) = self.previous.take() {
std::panic::set_hook(previous);
}
}
}
static PANIC_NEXT_REFRESH: AtomicBool = AtomicBool::new(false);
#[cached(ttl_secs = 2, key = "String", convert = r#"{ id.to_string() }"#)]
async fn flaky_lookup(id: &str) -> String {
let n = COMPUTES.fetch_add(1, Ordering::SeqCst) + 1;
assert!(
!PANIC_NEXT_REFRESH.swap(false, Ordering::SeqCst),
"simulated refresh failure"
);
format!("{id}-v{n}")
}
fn spawn_refresh(id: &str) -> Option<tokio::task::JoinHandle<()>> {
let claim = claim_refresh(id)?;
Some(tokio::spawn(async move {
flaky_lookup_prime_cache(claim.key()).await;
}))
}
#[cached(ttl_secs = 2, key = "String", convert = r#"{ id.to_string() }"#)]
async fn stuck_lookup(id: &str) -> String {
let n = COMPUTES.fetch_add(1, Ordering::SeqCst) + 1;
tokio::time::sleep(Duration::from_secs(5)).await;
format!("{id}-v{n}")
}
fn spawn_stuck_refresh(id: &str) -> Option<tokio::task::JoinHandle<()>> {
let claim = claim_refresh(id)?;
Some(tokio::spawn(async move {
stuck_lookup_prime_cache(claim.key()).await;
}))
}
#[cached(
ttl_secs = 1,
key = "String",
convert = r#"{ id.to_string() }"#,
sync_writes = "by_key"
)]
async fn single_flight(id: &str) -> String {
let n = COMPUTES.fetch_add(1, Ordering::SeqCst) + 1;
tokio::time::sleep(Duration::from_millis(200)).await;
format!("{id}-v{n}")
}
async fn single_flight_swr(id: &str) -> String {
let (value, expired) = {
let cache = SINGLE_FLIGHT.read().await;
cache.cache_peek_with_expiry_status(&id.to_string())
};
match value {
Some(v) if !expired => v,
Some(stale) => {
if let Some(claim) = claim_refresh(id) {
tokio::spawn(async move {
single_flight_prime_cache(claim.key()).await;
});
}
stale
}
None => single_flight(id).await,
}
}
static PANIC_NEXT_SINGLE_FLIGHT_REFRESH: AtomicBool = AtomicBool::new(false);
#[cached(
ttl_secs = 2,
key = "String",
convert = r#"{ id.to_string() }"#,
sync_writes = "by_key"
)]
async fn flaky_single_flight(id: &str) -> String {
let n = COMPUTES.fetch_add(1, Ordering::SeqCst) + 1;
assert!(
!PANIC_NEXT_SINGLE_FLIGHT_REFRESH.swap(false, Ordering::SeqCst),
"simulated single-flight refresh failure"
);
format!("{id}-v{n}")
}
fn spawn_flaky_single_flight_refresh(id: &str) -> Option<tokio::task::JoinHandle<()>> {
let claim = claim_refresh(id)?;
Some(tokio::spawn(async move {
flaky_single_flight_prime_cache(claim.key()).await;
}))
}
#[tokio::main]
async fn main() {
println!("sync `#[cached]`");
let first = sync_lookup_swr("a");
println!(" cold miss -> {first} ({} computes)", compute_count());
let hit = sync_lookup_swr("a");
println!(" fresh hit -> {hit} ({} computes)", compute_count());
std::thread::sleep(Duration::from_millis(1_100));
let started = std::time::Instant::now();
let stale = sync_lookup_swr("a");
println!(
" stale hit -> {stale} returned in {}ms, refresh spawned",
started.elapsed().as_millis()
);
assert_eq!(stale, first, "the stale hit must serve the previous value");
std::thread::sleep(Duration::from_millis(400));
let refreshed = sync_lookup_swr("a");
println!(" after bg -> {refreshed} ({} computes)", compute_count());
assert_ne!(
refreshed, first,
"the background refresh must have replaced it"
);
println!("\nasync `#[cached]`");
let first = async_lookup_swr("b").await;
println!(" cold miss -> {first}");
tokio::time::sleep(Duration::from_millis(1_100)).await;
let started = std::time::Instant::now();
let stale = async_lookup_swr("b").await;
println!(
" stale hit -> {stale} returned in {}ms, refresh spawned",
started.elapsed().as_millis()
);
assert_eq!(stale, first);
tokio::time::sleep(Duration::from_millis(400)).await;
println!(" after bg -> {}", async_lookup_swr("b").await);
println!("\nasync `#[concurrent_cached]`");
let first = sharded_lookup_swr("c").await;
println!(" cold miss -> {first}");
tokio::time::sleep(Duration::from_millis(1_100)).await;
let stale = sharded_lookup_swr("c").await;
println!(" stale hit -> {stale}, refresh spawned");
assert_eq!(stale, first);
tokio::time::sleep(Duration::from_millis(400)).await;
println!(" after bg -> {}", sharded_lookup_swr("c").await);
println!("\ncollapsing concurrent refreshes");
tokio::time::sleep(Duration::from_millis(1_100)).await;
let before = compute_count();
let mut handles = Vec::new();
for _ in 0..8 {
handles.push(tokio::spawn(async { async_lookup_swr_deduped("b").await }));
}
for handle in handles {
handle.await.expect("refresh task panicked");
}
tokio::time::sleep(Duration::from_millis(400)).await;
println!(
" 8 concurrent stale readers -> {} recompute(s)",
compute_count() - before
);
assert_eq!(
compute_count() - before,
1,
"the in-flight guard must collapse the refreshes to one"
);
let cold = flaky_lookup("g").await;
PANIC_NEXT_REFRESH.store(true, Ordering::SeqCst);
let outcome = {
let _silence = SilencedPanic::new(); let failing = spawn_refresh("g").expect("the first caller must claim the refresh");
failing.await
};
assert!(outcome.is_err(), "the refresh task must have panicked");
let retry = spawn_refresh("g").expect("a panicking refresh must not wedge the key");
retry.await.expect("the retry must not panic");
let stored = {
let cache = FLAKY_LOOKUP.read().await;
cache.cache_peek_with_expiry_status(&"g".to_string()).0
};
println!(" a panicking refresh released its claim, retry stored {stored:?}");
assert!(
stored.is_some(),
"the retried refresh must have stored a value"
);
assert_ne!(
stored.as_deref(),
Some(cold.as_str()),
"the retry must have replaced the value the panicking refresh failed to"
);
let stuck = spawn_stuck_refresh("h").expect("the first caller must claim the refresh");
tokio::time::sleep(Duration::from_millis(50)).await;
stuck.abort();
let cancelled = stuck.await;
assert!(
cancelled.as_ref().is_err_and(|e| e.is_cancelled()),
"the task must have been cancelled, not merely finished on its own: {cancelled:?}"
);
let retry_claim = claim_refresh("h");
println!(
" an aborted refresh released its claim: retry claim succeeded {}",
retry_claim.is_some()
);
assert!(
retry_claim.is_some(),
"an aborted refresh must release its claim, not wedge the key forever"
);
drop(retry_claim);
println!("\nsingle-flight revalidation");
let before = compute_count();
let started = std::time::Instant::now();
let mut handles = Vec::new();
for _ in 0..8 {
handles.push(tokio::spawn(async { single_flight_swr("d").await }));
}
let mut cold = Vec::new();
for handle in handles {
cold.push(handle.await.expect("cold task panicked"));
}
println!(
" cold : 8 callers -> {} compute(s) in {}ms, all callers agree: {}",
compute_count() - before,
started.elapsed().as_millis(),
cold.iter().all(|v| *v == cold[0])
);
assert_eq!(
compute_count() - before,
1,
"`by_key` must dedupe the cold path"
);
assert!(cold.iter().all(|v| *v == cold[0]));
tokio::time::sleep(Duration::from_millis(1_100)).await;
let before = compute_count();
let started = std::time::Instant::now();
let mut handles = Vec::new();
for _ in 0..8 {
handles.push(tokio::spawn(async { single_flight_swr("d").await }));
}
let mut served = Vec::new();
for handle in handles {
served.push(handle.await.expect("stale task panicked"));
}
let stale_ms = started.elapsed().as_millis();
println!(
" stale: 8 callers -> served {} in {}ms without waiting",
served[0], stale_ms
);
assert!(stale_ms < 100, "a stale read must not wait for the refresh");
assert_eq!(
served[0], cold[0],
"the stale reads must serve the old value"
);
tokio::time::sleep(Duration::from_millis(400)).await;
println!(
" after: {} refresh(es), next read -> {}",
compute_count() - before,
single_flight_swr("d").await
);
assert_eq!(
compute_count() - before,
1,
"exactly one caller may revalidate"
);
let cold_flaky = flaky_single_flight("i").await;
PANIC_NEXT_SINGLE_FLIGHT_REFRESH.store(true, Ordering::SeqCst);
let outcome = {
let _silence = SilencedPanic::new(); let failing = spawn_flaky_single_flight_refresh("i")
.expect("the first caller must claim the refresh");
failing.await
};
assert!(
outcome.is_err(),
"the single-flight refresh task must have panicked"
);
let retry = spawn_flaky_single_flight_refresh("i")
.expect("a panicking single-flight refresh must not wedge the key");
retry.await.expect("the retry must not panic");
let stored = {
let cache = FLAKY_SINGLE_FLIGHT.read().await;
cache.cache_peek_with_expiry_status(&"i".to_string()).0
};
println!(" a panicking single-flight refresh released its claim, retry stored {stored:?}");
assert!(
stored.is_some(),
"the retried single-flight refresh must have stored a value"
);
assert_ne!(
stored.as_deref(),
Some(cold_flaky.as_str()),
"the retry must have replaced the value the panicking refresh failed to"
);
let claim = claim_refresh("poison-me").expect("the first claim on a fresh key must succeed");
let poisoned = {
let _silence = SilencedPanic::new(); std::panic::catch_unwind(|| {
let _guard = REFRESHING.lock().expect("not yet poisoned");
panic!("deliberately poison REFRESHING while a guard is held");
})
};
assert!(
poisoned.is_err(),
"the deliberate panic must have propagated"
);
assert!(REFRESHING.is_poisoned(), "REFRESHING must now be poisoned");
drop(claim);
let recovered = REFRESHING.lock().unwrap_or_else(PoisonError::into_inner);
let still_claimed = recovered
.as_ref()
.is_some_and(|set| set.contains("poison-me"));
println!(" release_refresh recovered a poisoned lock: still claimed = {still_claimed}");
assert!(
!still_claimed,
"release_refresh must remove the key even from a poisoned set"
);
println!("\nstale-while-revalidate ok");
}