#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExtendedTtlPolicy {
pub idle_ttl_ms: Option<u64>,
pub stale_serve_ms: Option<u64>,
pub jitter_pct: u8,
}
impl ExtendedTtlPolicy {
pub fn off() -> Self {
Self {
idle_ttl_ms: None,
stale_serve_ms: None,
jitter_pct: 0,
}
}
pub fn is_active(&self) -> bool {
self.idle_ttl_ms.is_some() || self.stale_serve_ms.is_some() || self.jitter_pct > 0
}
}
impl Default for ExtendedTtlPolicy {
fn default() -> Self {
Self::off()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExpiryDecision {
Fresh,
Stale {
window_remaining_ms: u64,
},
Expired,
}
pub struct EffectiveExpiry;
impl EffectiveExpiry {
pub fn compute(
hard_expires_at_unix_ms: Option<u64>,
now_unix_ms: u64,
last_access_unix_ms: u64,
extended: &ExtendedTtlPolicy,
) -> ExpiryDecision {
if let Some(idle_ttl_ms) = extended.idle_ttl_ms {
let idle_ms = now_unix_ms.saturating_sub(last_access_unix_ms);
if idle_ms > idle_ttl_ms {
return ExpiryDecision::Expired;
}
}
let Some(hard) = hard_expires_at_unix_ms else {
return ExpiryDecision::Fresh;
};
if now_unix_ms <= hard {
return ExpiryDecision::Fresh;
}
let stale_window = extended.stale_serve_ms.unwrap_or(0);
if stale_window == 0 {
return ExpiryDecision::Expired;
}
let stale_deadline = hard.saturating_add(stale_window);
if now_unix_ms <= stale_deadline {
ExpiryDecision::Stale {
window_remaining_ms: stale_deadline - now_unix_ms,
}
} else {
ExpiryDecision::Expired
}
}
pub fn jittered_ttl_ms(base_ttl_ms: u64, jitter_pct: u8, seed: u64) -> u64 {
let pct = jitter_pct.min(100) as u64;
if pct == 0 || base_ttl_ms == 0 {
return base_ttl_ms;
}
let mixed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
let offset = mixed % (pct + 1);
let extra = base_ttl_ms.saturating_mul(offset) / 100;
base_ttl_ms.saturating_add(extra)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn off_is_inactive() {
let p = ExtendedTtlPolicy::off();
assert_eq!(p.idle_ttl_ms, None);
assert_eq!(p.stale_serve_ms, None);
assert_eq!(p.jitter_pct, 0);
assert!(!p.is_active());
}
#[test]
fn default_matches_off() {
assert_eq!(ExtendedTtlPolicy::default(), ExtendedTtlPolicy::off());
}
#[test]
fn any_field_set_makes_active() {
assert!(ExtendedTtlPolicy {
idle_ttl_ms: Some(1),
stale_serve_ms: None,
jitter_pct: 0,
}
.is_active());
assert!(ExtendedTtlPolicy {
idle_ttl_ms: None,
stale_serve_ms: Some(1),
jitter_pct: 0,
}
.is_active());
assert!(ExtendedTtlPolicy {
idle_ttl_ms: None,
stale_serve_ms: None,
jitter_pct: 1,
}
.is_active());
}
#[test]
fn hard_expiry_always_wins_proptest_style() {
let hards = [10u64, 100, 1_000, 10_000, u64::MAX / 2];
let stales = [0u64, 1, 50, 1_000, 1_000_000];
let idles = [None, Some(1u64), Some(10_000), Some(u64::MAX)];
let jitters = [0u8, 25, 50, 100, 250];
for &hard in &hards {
for &stale in &stales {
for &idle in &idles {
for &jitter in &jitters {
let ext = ExtendedTtlPolicy {
idle_ttl_ms: idle,
stale_serve_ms: Some(stale),
jitter_pct: jitter,
};
let now = hard.saturating_add(stale).saturating_add(1);
let decision = EffectiveExpiry::compute(Some(hard), now, now, &ext);
assert_eq!(
decision,
ExpiryDecision::Expired,
"hard={hard} stale={stale} idle={idle:?} jitter={jitter} now={now}",
);
}
}
}
}
}
#[test]
fn idle_ttl_kills_entry() {
let ext = ExtendedTtlPolicy {
idle_ttl_ms: Some(100),
stale_serve_ms: None,
jitter_pct: 0,
};
let d = EffectiveExpiry::compute(Some(10_000), 101, 0, &ext);
assert_eq!(d, ExpiryDecision::Expired);
}
#[test]
fn idle_ttl_resets_on_access() {
let ext = ExtendedTtlPolicy {
idle_ttl_ms: Some(100),
stale_serve_ms: None,
jitter_pct: 0,
};
let d = EffectiveExpiry::compute(Some(10_000), 1_000, 950, &ext);
assert_eq!(d, ExpiryDecision::Fresh);
}
#[test]
fn idle_ttl_at_boundary_is_fresh() {
let ext = ExtendedTtlPolicy {
idle_ttl_ms: Some(100),
stale_serve_ms: None,
jitter_pct: 0,
};
let d = EffectiveExpiry::compute(Some(10_000), 100, 0, &ext);
assert_eq!(d, ExpiryDecision::Fresh);
}
#[test]
fn idle_ttl_handles_clock_skew() {
let ext = ExtendedTtlPolicy {
idle_ttl_ms: Some(100),
stale_serve_ms: None,
jitter_pct: 0,
};
let d = EffectiveExpiry::compute(Some(10_000), 500, 510, &ext);
assert_eq!(d, ExpiryDecision::Fresh);
}
#[test]
fn stale_window_fires_after_hard() {
let ext = ExtendedTtlPolicy {
idle_ttl_ms: None,
stale_serve_ms: Some(50),
jitter_pct: 0,
};
let d = EffectiveExpiry::compute(Some(100), 120, 120, &ext);
assert_eq!(
d,
ExpiryDecision::Stale {
window_remaining_ms: 30
}
);
}
#[test]
fn stale_window_at_exact_boundary() {
let ext = ExtendedTtlPolicy {
idle_ttl_ms: None,
stale_serve_ms: Some(50),
jitter_pct: 0,
};
let d = EffectiveExpiry::compute(Some(100), 150, 150, &ext);
assert_eq!(
d,
ExpiryDecision::Stale {
window_remaining_ms: 0
}
);
}
#[test]
fn stale_window_expires() {
let ext = ExtendedTtlPolicy {
idle_ttl_ms: None,
stale_serve_ms: Some(50),
jitter_pct: 0,
};
let d = EffectiveExpiry::compute(Some(100), 151, 151, &ext);
assert_eq!(d, ExpiryDecision::Expired);
}
#[test]
fn no_stale_config_immediate_expired() {
let ext = ExtendedTtlPolicy {
idle_ttl_ms: None,
stale_serve_ms: None,
jitter_pct: 0,
};
let d = EffectiveExpiry::compute(Some(100), 101, 101, &ext);
assert_eq!(d, ExpiryDecision::Expired);
}
#[test]
fn stale_zero_acts_like_no_stale() {
let ext = ExtendedTtlPolicy {
idle_ttl_ms: None,
stale_serve_ms: Some(0),
jitter_pct: 0,
};
let d = EffectiveExpiry::compute(Some(100), 101, 101, &ext);
assert_eq!(d, ExpiryDecision::Expired);
}
#[test]
fn within_hard_is_fresh_even_with_stale_configured() {
let ext = ExtendedTtlPolicy {
idle_ttl_ms: None,
stale_serve_ms: Some(1_000),
jitter_pct: 0,
};
let d = EffectiveExpiry::compute(Some(100), 50, 50, &ext);
assert_eq!(d, ExpiryDecision::Fresh);
}
#[test]
fn hard_at_exact_boundary_is_fresh() {
let ext = ExtendedTtlPolicy::off();
let d = EffectiveExpiry::compute(Some(100), 100, 100, &ext);
assert_eq!(d, ExpiryDecision::Fresh);
}
#[test]
fn no_hard_expiry_is_fresh() {
let ext = ExtendedTtlPolicy::off();
let d = EffectiveExpiry::compute(None, u64::MAX, 0, &ext);
assert_eq!(d, ExpiryDecision::Fresh);
}
#[test]
fn no_hard_but_idle_still_kills() {
let ext = ExtendedTtlPolicy {
idle_ttl_ms: Some(50),
stale_serve_ms: None,
jitter_pct: 0,
};
let d = EffectiveExpiry::compute(None, 100, 0, &ext);
assert_eq!(d, ExpiryDecision::Expired);
}
#[test]
fn off_compute_never_returns_stale() {
let ext = ExtendedTtlPolicy::off();
for &hard in &[0u64, 1, 100, 10_000, u64::MAX] {
for &now in &[0u64, 1, 99, 100, 101, 10_001, u64::MAX] {
let d = EffectiveExpiry::compute(Some(hard), now, now, &ext);
assert!(
matches!(d, ExpiryDecision::Fresh | ExpiryDecision::Expired),
"off() must not produce Stale: hard={hard} now={now} got {d:?}",
);
}
}
}
#[test]
fn jitter_zero_is_identity() {
for base in [0u64, 1, 100, 1_000, 10_000_000] {
for seed in [0u64, 1, 42, u64::MAX] {
assert_eq!(
EffectiveExpiry::jittered_ttl_ms(base, 0, seed),
base,
"base={base} seed={seed}",
);
}
}
}
#[test]
fn jitter_zero_base_is_zero() {
for pct in [0u8, 25, 100] {
assert_eq!(EffectiveExpiry::jittered_ttl_ms(0, pct, 12345), 0);
}
}
#[test]
fn jitter_bound_1000_calls() {
let base = 1_000u64;
let pct = 20u8;
for seed in 0u64..1_000 {
let v = EffectiveExpiry::jittered_ttl_ms(base, pct, seed);
assert!(
(1_000..=1_200).contains(&v),
"seed={seed} v={v} out of [1000, 1200]",
);
}
}
#[test]
fn jitter_deterministic() {
let base = 5_000u64;
let pct = 50u8;
for seed in [0u64, 1, 42, 999, u64::MAX, 0xDEAD_BEEF] {
let a = EffectiveExpiry::jittered_ttl_ms(base, pct, seed);
let b = EffectiveExpiry::jittered_ttl_ms(base, pct, seed);
assert_eq!(a, b, "seed={seed}: {a} != {b}");
}
}
#[test]
fn jitter_pct_clamps_above_100() {
let base = 1_000u64;
for seed in 0u64..200 {
let v = EffectiveExpiry::jittered_ttl_ms(base, 250, seed);
assert!(
(1_000..=2_000).contains(&v),
"seed={seed} v={v} out of [1000, 2000] for clamped pct",
);
}
}
#[test]
fn jitter_distribution_covers_range() {
let base = 1_000u64;
let mut min_seen = u64::MAX;
let mut max_seen = 0u64;
for seed in 0u64..10_000 {
let v = EffectiveExpiry::jittered_ttl_ms(base, 100, seed);
min_seen = min_seen.min(v);
max_seen = max_seen.max(v);
}
assert!(min_seen <= 1_100, "min_seen={min_seen} too high");
assert!(max_seen >= 1_900, "max_seen={max_seen} too low");
}
#[test]
fn jitter_saturates_on_overflow() {
let v = EffectiveExpiry::jittered_ttl_ms(u64::MAX, 100, 1);
assert_eq!(v, u64::MAX);
}
fn reference_decision(
hard: Option<u64>,
now: u64,
last_access: u64,
ext: &ExtendedTtlPolicy,
) -> ExpiryDecision {
if let Some(idle) = ext.idle_ttl_ms {
let elapsed = if now >= last_access {
now - last_access
} else {
0
};
if elapsed > idle {
return ExpiryDecision::Expired;
}
}
let h = match hard {
None => return ExpiryDecision::Fresh,
Some(v) => v,
};
if now <= h {
return ExpiryDecision::Fresh;
}
let stale = ext.stale_serve_ms.unwrap_or(0);
if stale == 0 {
return ExpiryDecision::Expired;
}
let deadline = h.saturating_add(stale);
if now <= deadline {
ExpiryDecision::Stale {
window_remaining_ms: deadline - now,
}
} else {
ExpiryDecision::Expired
}
}
#[test]
fn combinatorial_matches_reference() {
let mut state: u64 = 0x1234_5678_9ABC_DEF0;
for _ in 0..5_000 {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1);
let hard = if state & 1 == 0 {
None
} else {
Some((state >> 1) % 10_000)
};
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1);
let now = state % 12_000;
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1);
let last_access = state % 12_000;
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1);
let idle = if state & 1 == 0 {
None
} else {
Some((state >> 1) % 5_000)
};
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1);
let stale = if state & 1 == 0 {
None
} else {
Some((state >> 1) % 5_000)
};
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1);
let jitter = (state % 256) as u8;
let ext = ExtendedTtlPolicy {
idle_ttl_ms: idle,
stale_serve_ms: stale,
jitter_pct: jitter,
};
let actual = EffectiveExpiry::compute(hard, now, last_access, &ext);
let expected = reference_decision(hard, now, last_access, &ext);
assert_eq!(
actual, expected,
"hard={hard:?} now={now} last={last_access} ext={ext:?}",
);
}
}
}