use std::any::Any;
use std::collections::HashMap;
use std::future::Future;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, LazyLock, Mutex, PoisonError};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use tokio::sync::{Semaphore, watch};
use super::{Cache, FillLockStatus, get_cached, insert_cached};
use crate::time::{ClockSource, SystemClock, clock_unix_duration};
#[derive(Clone)]
pub struct GetOrComputeOptions {
pub ttl: Option<Duration>,
pub distributed_fill_lock: bool,
pub lock_ttl: Duration,
pub lock_poll_interval: Duration,
pub lock_wait_timeout: Duration,
pub stale_while_revalidate: Option<Duration>,
clock: Arc<dyn ClockSource>,
}
impl GetOrComputeOptions {
#[must_use]
pub fn new() -> Self {
Self {
ttl: None,
distributed_fill_lock: false,
lock_ttl: Duration::from_secs(10),
lock_poll_interval: Duration::from_millis(50),
lock_wait_timeout: Duration::from_secs(5),
stale_while_revalidate: None,
clock: Arc::new(SystemClock),
}
}
#[must_use]
pub const fn ttl(mut self, ttl: Duration) -> Self {
self.ttl = Some(ttl);
self
}
#[must_use]
pub const fn distributed_fill_lock(mut self, enabled: bool) -> Self {
self.distributed_fill_lock = enabled;
self
}
#[must_use]
pub const fn lock_ttl(mut self, ttl: Duration) -> Self {
self.lock_ttl = ttl;
self
}
#[must_use]
pub const fn lock_poll_interval(mut self, interval: Duration) -> Self {
self.lock_poll_interval = interval;
self
}
#[must_use]
pub const fn lock_wait_timeout(mut self, timeout: Duration) -> Self {
self.lock_wait_timeout = timeout;
self
}
#[must_use]
pub const fn stale_while_revalidate(mut self, grace: Duration) -> Self {
self.stale_while_revalidate = Some(grace);
self
}
#[must_use]
pub fn with_clock(mut self, clock: Arc<dyn ClockSource>) -> Self {
self.clock = clock;
self
}
}
impl Default for GetOrComputeOptions {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for GetOrComputeOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GetOrComputeOptions")
.field("ttl", &self.ttl)
.field("distributed_fill_lock", &self.distributed_fill_lock)
.field("lock_ttl", &self.lock_ttl)
.field("lock_poll_interval", &self.lock_poll_interval)
.field("lock_wait_timeout", &self.lock_wait_timeout)
.field("stale_while_revalidate", &self.stale_while_revalidate)
.finish_non_exhaustive()
}
}
#[derive(Debug, thiserror::Error)]
pub enum CacheFillError<E> {
#[error("cache fill failed: {0}")]
Fill(E),
#[error("cache fill failed in concurrent caller: {0}")]
FillFailed(Arc<str>),
}
impl<E> CacheFillError<E> {
pub fn into_fill(self) -> Option<E> {
match self {
Self::Fill(e) => Some(e),
Self::FillFailed(_) => None,
}
}
}
#[derive(Debug, Default)]
pub struct ReadThroughMetrics {
hits: AtomicU64,
misses: AtomicU64,
coalesced_waits: AtomicU64,
fills: AtomicU64,
fill_failures: AtomicU64,
stale_serves: AtomicU64,
fill_lock_acquires: AtomicU64,
fill_lock_contended: AtomicU64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
pub struct ReadThroughMetricsSnapshot {
pub hits: u64,
pub misses: u64,
pub coalesced_waits: u64,
pub fills: u64,
pub fill_failures: u64,
pub stale_serves: u64,
pub fill_lock_acquires: u64,
pub fill_lock_contended: u64,
}
impl ReadThroughMetrics {
pub fn snapshot(&self) -> ReadThroughMetricsSnapshot {
ReadThroughMetricsSnapshot {
hits: self.hits.load(Ordering::Relaxed),
misses: self.misses.load(Ordering::Relaxed),
coalesced_waits: self.coalesced_waits.load(Ordering::Relaxed),
fills: self.fills.load(Ordering::Relaxed),
fill_failures: self.fill_failures.load(Ordering::Relaxed),
stale_serves: self.stale_serves.load(Ordering::Relaxed),
fill_lock_acquires: self.fill_lock_acquires.load(Ordering::Relaxed),
fill_lock_contended: self.fill_lock_contended.load(Ordering::Relaxed),
}
}
}
#[must_use]
pub fn read_through_metrics() -> &'static ReadThroughMetrics {
static METRICS: LazyLock<ReadThroughMetrics> = LazyLock::new(ReadThroughMetrics::default);
&METRICS
}
#[must_use]
pub fn jittered_ttl(base: Duration, fraction: f64) -> Duration {
let fraction = if fraction.is_finite() {
fraction.clamp(0.0, 1.0)
} else {
0.0
};
if fraction == 0.0 {
return base;
}
let mut buf = [0_u8; 4];
if getrandom::getrandom(&mut buf).is_err() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.subsec_nanos());
buf = nanos.to_le_bytes();
}
let unit = f64::from(u32::from_le_bytes(buf)) / f64::from(u32::MAX);
let factor = fraction.mul_add(2.0f64.mul_add(unit, -1.0), 1.0);
base.mul_f64(factor)
}
#[derive(Clone)]
enum FillState {
Pending,
Done(Arc<dyn Any + Send + Sync>),
Failed(Arc<str>),
}
type InFlightKey = (usize, String);
static IN_FLIGHT: LazyLock<Mutex<HashMap<InFlightKey, watch::Receiver<FillState>>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
struct InFlightGuard {
key: InFlightKey,
}
impl Drop for InFlightGuard {
fn drop(&mut self) {
IN_FLIGHT
.lock()
.unwrap_or_else(PoisonError::into_inner)
.remove(&self.key);
}
}
enum Role {
Leader(watch::Sender<FillState>, InFlightGuard),
Waiter(watch::Receiver<FillState>),
}
fn cache_identity(cache: &Arc<dyn Cache>) -> usize {
(Arc::as_ptr(cache).cast::<()>()) as usize
}
fn claim_role(cache: &Arc<dyn Cache>, key: &str) -> Role {
use std::collections::hash_map::Entry;
let map_key: InFlightKey = (cache_identity(cache), key.to_owned());
let mut in_flight = IN_FLIGHT.lock().unwrap_or_else(PoisonError::into_inner);
match in_flight.entry(map_key.clone()) {
Entry::Occupied(entry) => Role::Waiter(entry.get().clone()),
Entry::Vacant(entry) => {
let (tx, rx) = watch::channel(FillState::Pending);
entry.insert(rx);
Role::Leader(tx, InFlightGuard { key: map_key })
}
}
}
async fn await_result<V, E>(
mut rx: watch::Receiver<FillState>,
) -> Option<Result<V, CacheFillError<E>>>
where
V: Clone + Send + Sync + 'static,
{
let state = {
let waited = rx.wait_for(|s| !matches!(s, FillState::Pending)).await;
match waited {
Ok(state_ref) => state_ref.clone(),
Err(_closed) => return None,
}
};
match state {
FillState::Pending => unreachable!("wait_for guarantees a non-pending state"),
FillState::Done(value) => value.downcast_ref::<V>().cloned().map(Ok),
FillState::Failed(message) => Some(Err(CacheFillError::FillFailed(message))),
}
}
#[derive(Clone, serde::Serialize, serde::Deserialize)]
struct SwrEnvelope<V> {
value: V,
fresh_until_unix_ms: u64,
}
fn now_unix_ms(clock: &dyn ClockSource) -> u64 {
u64::try_from(clock_unix_duration(clock).as_millis()).unwrap_or(u64::MAX)
}
fn fast_path_value<V>(cache: &Arc<dyn Cache>, key: &str, options: &GetOrComputeOptions) -> Option<V>
where
V: Clone + serde::de::DeserializeOwned + Send + Sync + 'static,
{
if options.stale_while_revalidate.is_some() {
get_cached::<SwrEnvelope<V>>(cache.as_ref(), key)
.filter(|envelope| now_unix_ms(options.clock.as_ref()) < envelope.fresh_until_unix_ms)
.map(|envelope| envelope.value)
} else {
get_cached::<V>(cache.as_ref(), key)
}
}
const MAX_LOCK_POLL_INTERVAL: Duration = Duration::from_secs(1);
async fn run_fill_and_release<V, E, F, Fut>(
cache: &Arc<dyn Cache>,
key: &str,
options: &GetOrComputeOptions,
fill: F,
tx: &watch::Sender<FillState>,
token: &str,
) -> Result<V, CacheFillError<E>>
where
V: Clone + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static,
E: std::fmt::Display + Send + 'static,
F: FnOnce() -> Fut + Send,
Fut: Future<Output = Result<V, E>> + Send,
{
if let Some(value) = fast_path_value::<V>(cache, key, options) {
cache.release_fill_lock(key, token);
let _ = tx.send(FillState::Done(Arc::new(value.clone())));
return Ok(value);
}
read_through_metrics()
.fill_lock_acquires
.fetch_add(1, Ordering::Relaxed);
let result = fill().await;
let outcome = finish_fill(cache, key, options, result, tx);
cache.release_fill_lock(key, token);
outcome
}
async fn run_leader_fill<V, E, F, Fut>(
cache: &Arc<dyn Cache>,
key: &str,
options: &GetOrComputeOptions,
fill: F,
tx: watch::Sender<FillState>,
) -> Result<V, CacheFillError<E>>
where
V: Clone + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static,
E: std::fmt::Display + Send + 'static,
F: FnOnce() -> Fut + Send,
Fut: Future<Output = Result<V, E>> + Send,
{
if !options.distributed_fill_lock {
let result = fill().await;
return finish_fill(cache, key, options, result, &tx);
}
let token = uuid::Uuid::new_v4().to_string();
match cache.try_acquire_fill_lock(key, &token, options.lock_ttl) {
FillLockStatus::Unsupported => {
let result = fill().await;
finish_fill(cache, key, options, result, &tx)
}
FillLockStatus::Acquired => {
run_fill_and_release(cache, key, options, fill, &tx, &token).await
}
FillLockStatus::Held => {
read_through_metrics()
.fill_lock_contended
.fetch_add(1, Ordering::Relaxed);
let start = Instant::now();
let mut poll_interval = options.lock_poll_interval;
loop {
let remaining = options.lock_wait_timeout.saturating_sub(start.elapsed());
if remaining.is_zero() {
let result = fill().await;
return finish_fill(cache, key, options, result, &tx);
}
tokio::time::sleep(poll_interval.min(remaining)).await;
poll_interval = poll_interval.saturating_mul(2).min(MAX_LOCK_POLL_INTERVAL);
if let Some(value) = fast_path_value::<V>(cache, key, options) {
let _ = tx.send(FillState::Done(Arc::new(value.clone())));
return Ok(value);
}
if start.elapsed() >= options.lock_wait_timeout {
let result = fill().await;
return finish_fill(cache, key, options, result, &tx);
}
if cache.try_acquire_fill_lock(key, &token, options.lock_ttl)
== FillLockStatus::Acquired
{
return run_fill_and_release(cache, key, options, fill, &tx, &token).await;
}
}
}
}
}
fn finish_fill<V, E>(
cache: &Arc<dyn Cache>,
key: &str,
options: &GetOrComputeOptions,
result: Result<V, E>,
tx: &watch::Sender<FillState>,
) -> Result<V, CacheFillError<E>>
where
V: Clone + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static,
E: std::fmt::Display,
{
match result {
Ok(value) => {
if let Some(grace) = options.stale_while_revalidate {
let fresh_until_unix_ms = options.ttl.map_or(u64::MAX, |ttl| {
let ttl_ms = u64::try_from(ttl.as_millis()).unwrap_or(u64::MAX);
now_unix_ms(options.clock.as_ref()).saturating_add(ttl_ms)
});
let envelope = SwrEnvelope {
value: value.clone(),
fresh_until_unix_ms,
};
let physical_ttl = options.ttl.map(|ttl| ttl + grace);
insert_cached(cache.as_ref(), key, envelope, physical_ttl);
} else {
insert_cached(cache.as_ref(), key, value.clone(), options.ttl);
}
read_through_metrics().fills.fetch_add(1, Ordering::Relaxed);
let _ = tx.send(FillState::Done(Arc::new(value.clone())));
Ok(value)
}
Err(error) => {
let message: Arc<str> = error.to_string().into();
read_through_metrics()
.fill_failures
.fetch_add(1, Ordering::Relaxed);
let _ = tx.send(FillState::Failed(message));
Err(CacheFillError::Fill(error))
}
}
}
const MAX_CONCURRENT_BACKGROUND_REFRESHES: usize = 64;
static BACKGROUND_REFRESH_LIMIT: LazyLock<Semaphore> =
LazyLock::new(|| Semaphore::new(MAX_CONCURRENT_BACKGROUND_REFRESHES));
fn spawn_background_refresh<V, E, F, Fut>(
cache: Arc<dyn Cache>,
key: String,
options: GetOrComputeOptions,
fill: F,
) where
V: Clone + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static,
E: std::fmt::Display + Send + 'static,
F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = Result<V, E>> + Send + 'static,
{
match claim_role(&cache, &key) {
Role::Leader(tx, guard) => {
let Ok(permit) = BACKGROUND_REFRESH_LIMIT.try_acquire() else {
drop(guard);
return;
};
tokio::spawn(async move {
let _permit = permit;
let _result = run_leader_fill(&cache, &key, &options, fill, tx).await;
drop(guard);
});
}
Role::Waiter(_rx) => {
}
}
}
async fn simple_read_through<V, E, F, Fut>(
cache: &Arc<dyn Cache>,
key: &str,
options: &GetOrComputeOptions,
fill: F,
) -> Result<V, CacheFillError<E>>
where
V: Clone + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static,
E: std::fmt::Display + Send + 'static,
F: FnOnce() -> Fut + Send,
Fut: Future<Output = Result<V, E>> + Send,
{
loop {
if let Some(value) = get_cached::<V>(cache.as_ref(), key) {
read_through_metrics().hits.fetch_add(1, Ordering::Relaxed);
return Ok(value);
}
read_through_metrics()
.misses
.fetch_add(1, Ordering::Relaxed);
match claim_role(cache, key) {
Role::Leader(tx, guard) => {
if let Some(value) = get_cached::<V>(cache.as_ref(), key) {
let _ = tx.send(FillState::Done(Arc::new(value.clone())));
drop(guard);
return Ok(value);
}
let result = run_leader_fill(cache, key, options, fill, tx).await;
drop(guard);
return result;
}
Role::Waiter(rx) => {
read_through_metrics()
.coalesced_waits
.fetch_add(1, Ordering::Relaxed);
if let Some(result) = await_result::<V, E>(rx).await {
return result;
}
}
}
}
}
fn swr_within_grace(now_unix_ms: u64, fresh_until_unix_ms: u64, grace: Option<Duration>) -> bool {
let grace_ms = grace.map_or(0, |g| u64::try_from(g.as_millis()).unwrap_or(u64::MAX));
now_unix_ms < fresh_until_unix_ms.saturating_add(grace_ms)
}
async fn swr_read_through<V, E, F, Fut>(
cache: &Arc<dyn Cache>,
key: &str,
options: GetOrComputeOptions,
fill: F,
) -> Result<V, CacheFillError<E>>
where
V: Clone + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static,
E: std::fmt::Display + Send + 'static,
F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = Result<V, E>> + Send + 'static,
{
loop {
let envelope = get_cached::<SwrEnvelope<V>>(cache.as_ref(), key);
let now = now_unix_ms(options.clock.as_ref());
if let Some(envelope) = &envelope {
if now < envelope.fresh_until_unix_ms {
read_through_metrics().hits.fetch_add(1, Ordering::Relaxed);
return Ok(envelope.value.clone());
}
if swr_within_grace(
now,
envelope.fresh_until_unix_ms,
options.stale_while_revalidate,
) {
read_through_metrics()
.stale_serves
.fetch_add(1, Ordering::Relaxed);
spawn_background_refresh(cache.clone(), key.to_owned(), options.clone(), fill);
return Ok(envelope.value.clone());
}
}
read_through_metrics()
.misses
.fetch_add(1, Ordering::Relaxed);
match claim_role(cache, key) {
Role::Leader(tx, guard) => {
if let Some(envelope) = get_cached::<SwrEnvelope<V>>(cache.as_ref(), key)
&& swr_within_grace(
now_unix_ms(options.clock.as_ref()),
envelope.fresh_until_unix_ms,
options.stale_while_revalidate,
)
{
let _ = tx.send(FillState::Done(Arc::new(envelope.value.clone())));
drop(guard);
return Ok(envelope.value);
}
let result = run_leader_fill(cache, key, &options, fill, tx).await;
drop(guard);
return result;
}
Role::Waiter(rx) => {
read_through_metrics()
.coalesced_waits
.fetch_add(1, Ordering::Relaxed);
if let Some(result) = await_result::<V, E>(rx).await {
return result;
}
}
}
}
}
pub async fn get_or_compute<V, E, F, Fut>(
cache: &Arc<dyn Cache>,
key: &str,
ttl: Option<Duration>,
fill: F,
) -> Result<V, CacheFillError<E>>
where
V: Clone + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static,
E: std::fmt::Display + Send + 'static,
F: FnOnce() -> Fut + Send,
Fut: Future<Output = Result<V, E>> + Send,
{
let options = GetOrComputeOptions {
ttl,
..GetOrComputeOptions::new()
};
simple_read_through(cache, key, &options, fill).await
}
pub async fn get_or_compute_with<V, E, F, Fut>(
cache: &Arc<dyn Cache>,
key: &str,
options: GetOrComputeOptions,
fill: F,
) -> Result<V, CacheFillError<E>>
where
V: Clone + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static,
E: std::fmt::Display + Send + 'static,
F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = Result<V, E>> + Send + 'static,
{
if options.stale_while_revalidate.is_some() {
swr_read_through(cache, key, options, fill).await
} else {
simple_read_through(cache, key, &options, fill).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn jittered_ttl_within_bounds() {
let base = Duration::from_secs(100);
let mut distinct = std::collections::HashSet::new();
for _ in 0..100 {
let jittered = jittered_ttl(base, 0.2);
assert!(
jittered >= Duration::from_secs(80) && jittered <= Duration::from_secs(120),
"jittered TTL {jittered:?} outside [80s, 120s]"
);
distinct.insert(jittered.as_nanos());
}
assert!(
distinct.len() >= 2,
"expected at least 2 distinct jittered values, got {}",
distinct.len()
);
assert_eq!(jittered_ttl(base, 0.0), base, "fraction 0.0 must be exact");
}
#[test]
fn jittered_ttl_clamps_fraction() {
let base = Duration::from_secs(10);
for _ in 0..50 {
let jittered = jittered_ttl(base, 5.0);
assert!(
jittered <= Duration::from_secs(20),
"fraction must clamp to 1.0; got {jittered:?}"
);
}
assert_eq!(jittered_ttl(base, f64::NAN), base);
assert_eq!(jittered_ttl(base, f64::INFINITY), base);
}
#[cfg(feature = "cache-moka")]
#[test]
fn fill_lock_default_unsupported() {
use super::super::{FillLockStatus, MokaCache};
let cache = MokaCache::new(10, None);
assert_eq!(
Cache::try_acquire_fill_lock(&cache, "k", "token", Duration::from_secs(1)),
FillLockStatus::Unsupported
);
Cache::release_fill_lock(&cache, "k", "token");
}
#[cfg(feature = "cache-moka")]
#[test]
fn fast_path_value_ignores_stale_swr_envelope() {
use super::super::MokaCache;
let cache: Arc<dyn Cache> = Arc::new(MokaCache::new(10, None));
let key = "fast-path-swr-key";
let options = GetOrComputeOptions::new().stale_while_revalidate(Duration::from_secs(10));
let stale = SwrEnvelope {
value: "stale-value".to_string(),
fresh_until_unix_ms: 0,
};
insert_cached(cache.as_ref(), key, stale, None);
assert_eq!(
fast_path_value::<String>(&cache, key, &options),
None,
"a stale SWR envelope must not be reported as a completed fill"
);
let fresh = SwrEnvelope {
value: "fresh-value".to_string(),
fresh_until_unix_ms: u64::MAX,
};
insert_cached(cache.as_ref(), key, fresh, None);
assert_eq!(
fast_path_value::<String>(&cache, key, &options),
Some("fresh-value".to_string())
);
}
#[cfg(feature = "cache-moka")]
#[tokio::test]
async fn run_fill_and_release_skips_fill_if_value_already_present() {
use super::super::MokaCache;
let cache: Arc<dyn Cache> = Arc::new(MokaCache::new(10, None));
let key = "recheck-after-lock-key";
let options = GetOrComputeOptions::new();
let (tx, mut rx) = watch::channel(FillState::Pending);
insert_cached(cache.as_ref(), key, "already-written".to_string(), None);
let fill_count = Arc::new(AtomicU64::new(0));
let fc = fill_count.clone();
let result: Result<String, CacheFillError<String>> = run_fill_and_release(
&cache,
key,
&options,
move || async move {
fc.fetch_add(1, Ordering::Relaxed);
Ok::<String, String>("redundant-recompute".to_string())
},
&tx,
"token",
)
.await;
assert_eq!(result.unwrap(), "already-written");
assert_eq!(
fill_count.load(Ordering::Relaxed),
0,
"fill must not run when another replica already wrote the value"
);
assert!(matches!(*rx.borrow_and_update(), FillState::Done(_)));
}
#[test]
fn metrics_snapshot_starts_at_zero_and_counts() {
let metrics = ReadThroughMetrics::default();
let snap = metrics.snapshot();
assert_eq!(snap.hits, 0);
assert_eq!(snap.misses, 0);
assert_eq!(snap.coalesced_waits, 0);
assert_eq!(snap.fills, 0);
assert_eq!(snap.fill_failures, 0);
assert_eq!(snap.stale_serves, 0);
assert_eq!(snap.fill_lock_acquires, 0);
assert_eq!(snap.fill_lock_contended, 0);
metrics.hits.fetch_add(2, Ordering::Relaxed);
metrics.misses.fetch_add(1, Ordering::Relaxed);
metrics.coalesced_waits.fetch_add(3, Ordering::Relaxed);
metrics.fills.fetch_add(4, Ordering::Relaxed);
metrics.fill_failures.fetch_add(5, Ordering::Relaxed);
metrics.stale_serves.fetch_add(6, Ordering::Relaxed);
metrics.fill_lock_acquires.fetch_add(7, Ordering::Relaxed);
metrics.fill_lock_contended.fetch_add(8, Ordering::Relaxed);
let snap = metrics.snapshot();
assert_eq!(snap.hits, 2);
assert_eq!(snap.misses, 1);
assert_eq!(snap.coalesced_waits, 3);
assert_eq!(snap.fills, 4);
assert_eq!(snap.fill_failures, 5);
assert_eq!(snap.stale_serves, 6);
assert_eq!(snap.fill_lock_acquires, 7);
assert_eq!(snap.fill_lock_contended, 8);
}
#[test]
fn read_through_metrics_is_a_stable_singleton() {
let a: *const ReadThroughMetrics = read_through_metrics();
let b: *const ReadThroughMetrics = read_through_metrics();
assert_eq!(a, b);
}
}