pub const MAX_TTL_TIER: u8 = 63;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct CacheTier(u8);
impl CacheTier {
#[must_use]
pub const fn new(value: u8) -> Self {
Self(value)
}
#[must_use]
pub const fn get(self) -> u8 {
self.0
}
}
impl From<u8> for CacheTier {
fn from(value: u8) -> Self {
Self::new(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct CacheTimestampMillis(u64);
impl CacheTimestampMillis {
#[must_use]
pub const fn new(value: u64) -> Self {
Self(value)
}
#[must_use]
pub const fn get(self) -> u64 {
self.0
}
#[must_use]
pub fn now() -> Self {
Self(now_millis())
}
}
impl From<u64> for CacheTimestampMillis {
fn from(value: u64) -> Self {
Self::new(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct CacheTtlMillis(u64);
impl CacheTtlMillis {
#[must_use]
pub const fn new(value: u64) -> Self {
Self(value)
}
#[must_use]
pub const fn get(self) -> u64 {
self.0
}
#[must_use]
pub fn from_duration(duration: std::time::Duration) -> Self {
Self(duration_millis_u64(duration))
}
}
impl From<u64> for CacheTtlMillis {
fn from(value: u64) -> Self {
Self::new(value)
}
}
#[inline]
#[must_use]
pub const fn effective_ttl(base_ttl: CacheTtlMillis, tier: CacheTier) -> CacheTtlMillis {
let tier = tier.get();
let capped_tier = if tier > MAX_TTL_TIER {
MAX_TTL_TIER
} else {
tier
};
CacheTtlMillis::new(base_ttl.get().saturating_mul(1u64 << capped_tier))
}
#[inline]
#[must_use]
pub fn now_millis() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, duration_millis_u64)
}
#[allow(clippy::cast_possible_truncation)] fn duration_millis_u64(duration: std::time::Duration) -> u64 {
u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
}
#[inline]
#[must_use]
pub fn is_expired(
inserted_at: CacheTimestampMillis,
base_ttl: CacheTtlMillis,
tier: CacheTier,
) -> bool {
let elapsed = now_millis().saturating_sub(inserted_at.get());
elapsed >= effective_ttl(base_ttl, tier).get()
}
#[inline]
#[must_use]
pub const fn ttl_multiplier(tier: CacheTier) -> u64 {
let tier = tier.get();
let capped = if tier > MAX_TTL_TIER {
MAX_TTL_TIER
} else {
tier
};
1u64 << capped
}
#[cfg(test)]
mod tests {
use super::*;
const fn ttl(value: u64) -> CacheTtlMillis {
CacheTtlMillis::new(value)
}
const fn timestamp(value: u64) -> CacheTimestampMillis {
CacheTimestampMillis::new(value)
}
const fn effective_ttl_ms(base_ttl_millis: u64, tier: CacheTier) -> u64 {
effective_ttl(ttl(base_ttl_millis), tier).get()
}
fn is_expired_ms(inserted_at_millis: u64, base_ttl_millis: u64, tier: CacheTier) -> bool {
is_expired(timestamp(inserted_at_millis), ttl(base_ttl_millis), tier)
}
#[test]
fn effective_ttl_tier_0_is_base() {
assert_eq!(effective_ttl_ms(1000, CacheTier::new(0)), 1000);
assert_eq!(effective_ttl_ms(0, CacheTier::new(0)), 0);
assert_eq!(effective_ttl_ms(1, CacheTier::new(0)), 1);
}
#[test]
fn effective_ttl_tier_1_is_2x() {
assert_eq!(effective_ttl_ms(1000, CacheTier::new(1)), 2000);
assert_eq!(effective_ttl_ms(500, CacheTier::new(1)), 1000);
}
#[test]
fn effective_ttl_tier_2_is_4x() {
assert_eq!(effective_ttl_ms(1000, CacheTier::new(2)), 4000);
}
#[test]
fn effective_ttl_tier_3_is_8x() {
assert_eq!(effective_ttl_ms(1000, CacheTier::new(3)), 8000);
}
#[test]
fn effective_ttl_tier_7_is_128x() {
assert_eq!(effective_ttl_ms(1000, CacheTier::new(7)), 128_000);
}
#[test]
fn effective_ttl_tier_10_is_1024x() {
assert_eq!(effective_ttl_ms(1000, CacheTier::new(10)), 1_024_000);
}
#[test]
fn effective_ttl_caps_at_tier_63() {
let tier_63_result = effective_ttl_ms(1, CacheTier::new(63));
assert_eq!(tier_63_result, 1u64 << 63);
assert_eq!(effective_ttl_ms(1, CacheTier::new(64)), tier_63_result);
assert_eq!(effective_ttl_ms(1, CacheTier::new(100)), tier_63_result);
assert_eq!(effective_ttl_ms(1, CacheTier::new(255)), tier_63_result);
}
#[test]
fn effective_ttl_saturates_on_overflow() {
assert_eq!(effective_ttl_ms(u64::MAX, CacheTier::new(0)), u64::MAX);
assert_eq!(effective_ttl_ms(u64::MAX, CacheTier::new(1)), u64::MAX);
assert_eq!(effective_ttl_ms(u64::MAX, CacheTier::new(63)), u64::MAX);
assert_eq!(
effective_ttl_ms(u64::MAX / 2 + 1, CacheTier::new(1)),
u64::MAX
);
}
#[test]
fn effective_ttl_zero_base() {
assert_eq!(effective_ttl_ms(0, CacheTier::new(0)), 0);
assert_eq!(effective_ttl_ms(0, CacheTier::new(1)), 0);
assert_eq!(effective_ttl_ms(0, CacheTier::new(63)), 0);
assert_eq!(effective_ttl_ms(0, CacheTier::new(255)), 0);
}
#[test]
fn ttl_multiplier_values() {
assert_eq!(ttl_multiplier(CacheTier::new(0)), 1);
assert_eq!(ttl_multiplier(CacheTier::new(1)), 2);
assert_eq!(ttl_multiplier(CacheTier::new(2)), 4);
assert_eq!(ttl_multiplier(CacheTier::new(3)), 8);
assert_eq!(ttl_multiplier(CacheTier::new(4)), 16);
assert_eq!(ttl_multiplier(CacheTier::new(5)), 32);
assert_eq!(ttl_multiplier(CacheTier::new(6)), 64);
assert_eq!(ttl_multiplier(CacheTier::new(7)), 128);
assert_eq!(ttl_multiplier(CacheTier::new(10)), 1024);
assert_eq!(ttl_multiplier(CacheTier::new(63)), 1u64 << 63);
}
#[test]
fn ttl_multiplier_caps_at_tier_63() {
let max = 1u64 << 63;
assert_eq!(ttl_multiplier(CacheTier::new(64)), max);
assert_eq!(ttl_multiplier(CacheTier::new(100)), max);
assert_eq!(ttl_multiplier(CacheTier::new(255)), max);
}
#[test]
fn is_expired_fresh_entry() {
let now = now_millis();
assert!(!is_expired_ms(now, 1000, CacheTier::new(0))); assert!(!is_expired_ms(now, 100, CacheTier::new(0))); }
#[test]
fn is_expired_old_entry() {
let old = now_millis().saturating_sub(2000);
assert!(is_expired_ms(old, 1000, CacheTier::new(0))); assert!(is_expired_ms(old, 1999, CacheTier::new(0))); }
#[test]
fn is_expired_boundary() {
let inserted = now_millis().saturating_sub(1000);
assert!(is_expired_ms(inserted, 1000, CacheTier::new(0)));
assert!(!is_expired_ms(inserted, 2000, CacheTier::new(0)));
}
#[test]
fn is_expired_respects_tier() {
let inserted = now_millis().saturating_sub(1500);
assert!(is_expired_ms(inserted, 1000, CacheTier::new(0))); assert!(!is_expired_ms(inserted, 1000, CacheTier::new(1))); assert!(!is_expired_ms(inserted, 1000, CacheTier::new(2))); }
#[test]
fn is_expired_high_tier_extends_ttl() {
let inserted = now_millis().saturating_sub(100_000); assert!(is_expired_ms(inserted, 1000, CacheTier::new(0))); assert!(is_expired_ms(inserted, 1000, CacheTier::new(1))); assert!(is_expired_ms(inserted, 1000, CacheTier::new(5))); assert!(is_expired_ms(inserted, 1000, CacheTier::new(6))); assert!(!is_expired_ms(inserted, 1000, CacheTier::new(7))); assert!(!is_expired_ms(inserted, 1000, CacheTier::new(10))); }
#[test]
fn is_expired_zero_ttl() {
let now = now_millis();
assert!(is_expired_ms(now, 0, CacheTier::new(0)));
assert!(is_expired_ms(now, 0, CacheTier::new(63))); }
#[test]
fn is_expired_future_timestamp() {
let future = now_millis().saturating_add(10000);
assert!(!is_expired_ms(future, 1000, CacheTier::new(0)));
}
#[test]
fn now_millis_is_reasonable() {
let now = now_millis();
assert!(now > 1_700_000_000_000);
}
#[test]
fn now_millis_is_monotonic() {
let t1 = now_millis();
let t2 = now_millis();
assert!(t2 >= t1);
}
}