use std::time::Duration;
pub const DEFAULT_BASE_TTL: Duration = Duration::from_secs(300);
pub const DEFAULT_MIN_TTL: Duration = Duration::from_secs(60);
pub const DEFAULT_MAX_TTL: Duration = Duration::from_secs(3600);
pub const DEFAULT_HOT_THRESHOLD: u64 = 10;
pub const DEFAULT_COLD_THRESHOLD: u64 = 2;
pub const DEFAULT_ADAPTATION_RATE: f64 = 0.25;
pub const DEFAULT_CLEANUP_INTERVAL: Duration = Duration::from_secs(60);
#[derive(Debug, Clone)]
pub struct TTLConfig {
pub base_ttl: Duration,
pub min_ttl: Duration,
pub max_ttl: Duration,
pub hot_threshold: u64,
pub cold_threshold: u64,
pub adaptation_rate: f64,
pub enable_background_cleanup: bool,
pub cleanup_interval: Duration,
pub max_entries: usize,
pub memory_pressure_threshold: f64,
pub enable_adaptive_ttl: bool,
pub access_window_secs: u64,
}
impl Default for TTLConfig {
fn default() -> Self {
Self {
base_ttl: DEFAULT_BASE_TTL,
min_ttl: DEFAULT_MIN_TTL,
max_ttl: DEFAULT_MAX_TTL,
hot_threshold: DEFAULT_HOT_THRESHOLD,
cold_threshold: DEFAULT_COLD_THRESHOLD,
adaptation_rate: DEFAULT_ADAPTATION_RATE,
enable_background_cleanup: true,
cleanup_interval: DEFAULT_CLEANUP_INTERVAL,
max_entries: 10_000,
memory_pressure_threshold: 0.8,
enable_adaptive_ttl: true,
access_window_secs: 300, }
}
}
impl TTLConfig {
pub fn new() -> Self {
Self::default()
}
pub fn high_hit_rate() -> Self {
Self {
base_ttl: Duration::from_secs(600), min_ttl: Duration::from_secs(120), max_ttl: Duration::from_secs(7200), hot_threshold: 5, cold_threshold: 1,
adaptation_rate: 0.35, enable_background_cleanup: true,
cleanup_interval: Duration::from_secs(30),
max_entries: 20_000,
memory_pressure_threshold: 0.85,
enable_adaptive_ttl: true,
access_window_secs: 180, }
}
pub fn memory_constrained() -> Self {
Self {
base_ttl: Duration::from_secs(180), min_ttl: Duration::from_secs(30), max_ttl: Duration::from_secs(1800), hot_threshold: 15,
cold_threshold: 3,
adaptation_rate: 0.2,
enable_background_cleanup: true,
cleanup_interval: Duration::from_secs(15),
max_entries: 1_000,
memory_pressure_threshold: 0.6, enable_adaptive_ttl: true,
access_window_secs: 120, }
}
pub fn write_heavy() -> Self {
Self {
base_ttl: Duration::from_secs(120), min_ttl: Duration::from_secs(30), max_ttl: Duration::from_secs(600), hot_threshold: 20,
cold_threshold: 5,
adaptation_rate: 0.15, enable_background_cleanup: true,
cleanup_interval: Duration::from_secs(20),
max_entries: 5_000,
memory_pressure_threshold: 0.75,
enable_adaptive_ttl: true,
access_window_secs: 60, }
}
pub fn validate(&self) -> Result<(), TTLConfigError> {
if self.min_ttl > self.base_ttl {
return Err(TTLConfigError::InvalidBounds(
"min_ttl cannot be greater than base_ttl".to_string(),
));
}
if self.base_ttl > self.max_ttl {
return Err(TTLConfigError::InvalidBounds(
"base_ttl cannot be greater than max_ttl".to_string(),
));
}
if !(0.0..=1.0).contains(&self.adaptation_rate) {
return Err(TTLConfigError::InvalidAdaptationRate(self.adaptation_rate));
}
if !(0.0..=1.0).contains(&self.memory_pressure_threshold) {
return Err(TTLConfigError::InvalidThreshold(
self.memory_pressure_threshold,
));
}
if self.hot_threshold <= self.cold_threshold {
return Err(TTLConfigError::InvalidThresholds {
hot: self.hot_threshold,
cold: self.cold_threshold,
});
}
if self.max_entries == 0 {
return Err(TTLConfigError::InvalidMaxEntries);
}
Ok(())
}
pub fn calculate_ttl(&self, current_ttl: Duration, access_count: u64) -> Duration {
if !self.enable_adaptive_ttl {
return self.base_ttl;
}
let new_ttl = if access_count >= self.hot_threshold {
let extension = current_ttl.mul_f64(self.adaptation_rate);
current_ttl + extension
} else if access_count <= self.cold_threshold {
let reduction = current_ttl.mul_f64(self.adaptation_rate);
current_ttl.saturating_sub(reduction)
} else {
current_ttl
};
new_ttl.clamp(self.min_ttl, self.max_ttl)
}
pub fn with_base_ttl(mut self, ttl: Duration) -> Self {
self.base_ttl = ttl;
self
}
pub fn with_min_ttl(mut self, ttl: Duration) -> Self {
self.min_ttl = ttl;
self
}
pub fn with_max_ttl(mut self, ttl: Duration) -> Self {
self.max_ttl = ttl;
self
}
pub fn with_hot_threshold(mut self, threshold: u64) -> Self {
self.hot_threshold = threshold;
self
}
pub fn with_cold_threshold(mut self, threshold: u64) -> Self {
self.cold_threshold = threshold;
self
}
pub fn with_adaptation_rate(mut self, rate: f64) -> Self {
self.adaptation_rate = rate.clamp(0.0, 1.0);
self
}
pub fn with_max_entries(mut self, max: usize) -> Self {
self.max_entries = max;
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum TTLConfigError {
InvalidBounds(String),
InvalidAdaptationRate(f64),
InvalidThreshold(f64),
InvalidThresholds { hot: u64, cold: u64 },
InvalidMaxEntries,
}
impl std::fmt::Display for TTLConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidBounds(msg) => write!(f, "Invalid TTL bounds: {msg}"),
Self::InvalidAdaptationRate(rate) => {
write!(f, "Invalid adaptation rate: {rate} (must be 0.0 - 1.0)")
}
Self::InvalidThreshold(threshold) => {
write!(f, "Invalid threshold: {threshold} (must be 0.0 - 1.0)")
}
Self::InvalidThresholds { hot, cold } => {
write!(f, "Invalid thresholds: hot ({hot}) must be > cold ({cold})")
}
Self::InvalidMaxEntries => write!(f, "Invalid max entries: must be > 0"),
}
}
}
impl std::error::Error for TTLConfigError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = TTLConfig::default();
assert_eq!(config.base_ttl, DEFAULT_BASE_TTL);
assert_eq!(config.min_ttl, DEFAULT_MIN_TTL);
assert_eq!(config.max_ttl, DEFAULT_MAX_TTL);
assert!(config.validate().is_ok());
}
#[test]
fn test_high_hit_rate_config() {
let config = TTLConfig::high_hit_rate();
assert!(config.validate().is_ok());
assert_eq!(config.base_ttl, Duration::from_secs(600));
assert!(config.max_entries > 10_000);
}
#[test]
fn test_memory_constrained_config() {
let config = TTLConfig::memory_constrained();
assert!(config.validate().is_ok());
assert!(config.max_entries < 2_000);
assert!(config.memory_pressure_threshold < 0.7);
}
#[test]
fn test_write_heavy_config() {
let config = TTLConfig::write_heavy();
assert!(config.validate().is_ok());
assert!(config.base_ttl < Duration::from_secs(300));
}
#[test]
fn test_invalid_bounds() {
let config = TTLConfig {
min_ttl: Duration::from_secs(600),
base_ttl: Duration::from_secs(300),
..Default::default()
};
assert!(matches!(
config.validate(),
Err(TTLConfigError::InvalidBounds(_))
));
}
#[test]
fn test_invalid_adaptation_rate() {
let config = TTLConfig {
adaptation_rate: 1.5,
..Default::default()
};
assert!(matches!(
config.validate(),
Err(TTLConfigError::InvalidAdaptationRate(1.5))
));
}
#[test]
fn test_invalid_thresholds() {
let config = TTLConfig {
hot_threshold: 2,
cold_threshold: 5,
..Default::default()
};
assert!(matches!(
config.validate(),
Err(TTLConfigError::InvalidThresholds { hot: 2, cold: 5 })
));
}
#[test]
fn test_calculate_ttl_hot() {
let config = TTLConfig::default();
let current = Duration::from_secs(300);
let new_ttl = config.calculate_ttl(current, 15); assert!(new_ttl > current);
assert!(new_ttl <= config.max_ttl);
}
#[test]
fn test_calculate_ttl_cold() {
let config = TTLConfig::default();
let current = Duration::from_secs(300);
let new_ttl = config.calculate_ttl(current, 1); assert!(new_ttl < current);
assert!(new_ttl >= config.min_ttl);
}
#[test]
fn test_calculate_ttl_neutral() {
let config = TTLConfig::default();
let current = Duration::from_secs(300);
let new_ttl = config.calculate_ttl(current, 5); assert_eq!(new_ttl, current);
}
#[test]
fn test_builder_methods() {
let config = TTLConfig::new()
.with_base_ttl(Duration::from_secs(600))
.with_min_ttl(Duration::from_secs(120))
.with_max_ttl(Duration::from_secs(7200))
.with_hot_threshold(20)
.with_cold_threshold(3)
.with_adaptation_rate(0.5)
.with_max_entries(5000);
assert_eq!(config.base_ttl, Duration::from_secs(600));
assert_eq!(config.min_ttl, Duration::from_secs(120));
assert_eq!(config.max_ttl, Duration::from_secs(7200));
assert_eq!(config.hot_threshold, 20);
assert_eq!(config.cold_threshold, 3);
assert_eq!(config.adaptation_rate, 0.5);
assert_eq!(config.max_entries, 5000);
assert!(config.validate().is_ok());
}
#[test]
fn test_adaptation_rate_clamping() {
let config = TTLConfig::new().with_adaptation_rate(1.5);
assert_eq!(config.adaptation_rate, 1.0);
let config = TTLConfig::new().with_adaptation_rate(-0.5);
assert_eq!(config.adaptation_rate, 0.0);
}
}