// Copyright 2024 RustFS Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Cache configuration and adaptive TTL for object caching. //! //! This module provides cache configuration types and adaptive TTL //! algorithms for optimizing cache behavior based on access patterns. use std::time::Duration; /// Cache configuration. #[derive(Debug, Clone)] pub struct CacheConfig { /// Maximum cache capacity (number of entries). pub max_capacity: u64, /// Default TTL in seconds. pub default_ttl_seconds: u64, /// Maximum memory usage in bytes. pub max_memory_bytes: u64, /// Number of concurrent shards. pub concurrency_shards: usize, /// Whether adaptive TTL is enabled. pub adaptive_ttl_enabled: bool, /// Minimum TTL in seconds. pub min_ttl_seconds: u64, /// Maximum TTL in seconds. pub max_ttl_seconds: u64, /// TTL extension factor for hot items. pub ttl_extension_factor: f64, /// TTL reduction factor for cold items. pub ttl_reduction_factor: f64, } impl Default for CacheConfig { fn default() -> Self { Self { max_capacity: 10_000, default_ttl_seconds: 300, // 5 minutes max_memory_bytes: 100 * 1024 * 1024, // 100 MB concurrency_shards: num_cpus::get(), adaptive_ttl_enabled: true, min_ttl_seconds: 60, // 1 minute max_ttl_seconds: 3600, // 1 hour ttl_extension_factor: 1.5, ttl_reduction_factor: 0.7, } } } impl CacheConfig { /// Create a new cache configuration with default values. pub fn new() -> Self { Self::default() } /// Validate the configuration. pub fn validate(&self) -> Result<(), CacheConfigError> { if self.max_capacity == 0 { return Err(CacheConfigError::InvalidValue("max_capacity must be > 0".to_string())); } if self.min_ttl_seconds >= self.max_ttl_seconds { return Err(CacheConfigError::InvalidValue("min_ttl_seconds must be < max_ttl_seconds".to_string())); } if self.default_ttl_seconds < self.min_ttl_seconds || self.default_ttl_seconds > self.max_ttl_seconds { return Err(CacheConfigError::InvalidValue( "default_ttl_seconds must be between min_ttl_seconds and max_ttl_seconds".to_string(), )); } if self.ttl_extension_factor <= 1.0 { return Err(CacheConfigError::InvalidValue("ttl_extension_factor must be > 1.0".to_string())); } if self.ttl_reduction_factor >= 1.0 || self.ttl_reduction_factor <= 0.0 { return Err(CacheConfigError::InvalidValue( "ttl_reduction_factor must be between 0.0 and 1.0".to_string(), )); } Ok(()) } /// Get the default TTL as a Duration. pub fn default_ttl(&self) -> Duration { Duration::from_secs(self.default_ttl_seconds) } /// Get the minimum TTL as a Duration. pub fn min_ttl(&self) -> Duration { Duration::from_secs(self.min_ttl_seconds) } /// Get the maximum TTL as a Duration. pub fn max_ttl(&self) -> Duration { Duration::from_secs(self.max_ttl_seconds) } /// Builder pattern: set max capacity. pub fn with_max_capacity(mut self, value: u64) -> Self { self.max_capacity = value; self } /// Builder pattern: set TTL range. pub fn with_ttl_range(mut self, min: u64, default: u64, max: u64) -> Self { self.min_ttl_seconds = min; self.default_ttl_seconds = default; self.max_ttl_seconds = max; self } /// Builder pattern: enable/disable adaptive TTL. pub fn with_adaptive_ttl(mut self, enabled: bool) -> Self { self.adaptive_ttl_enabled = enabled; self } } /// Cache configuration error. #[derive(Debug, Clone, thiserror::Error)] pub enum CacheConfigError { /// Invalid configuration value. #[error("Invalid cache configuration: {0}")] InvalidValue(String), } /// Adaptive TTL calculator. #[derive(Debug, Clone)] pub struct AdaptiveTTL { /// Cache configuration. config: CacheConfig, /// Access count threshold for hot items. hot_threshold: u64, /// Access count threshold for cold items. cold_threshold: u64, /// Time window for access counting. access_window: Duration, } impl Default for AdaptiveTTL { fn default() -> Self { Self { config: CacheConfig::default(), hot_threshold: 10, cold_threshold: 2, access_window: Duration::from_secs(60), } } } impl AdaptiveTTL { /// Create a new adaptive TTL calculator. pub fn new(config: CacheConfig) -> Self { Self { config, hot_threshold: 10, cold_threshold: 2, access_window: Duration::from_secs(60), } } /// Create with custom thresholds. pub fn with_thresholds(mut self, hot: u64, cold: u64) -> Self { self.hot_threshold = hot; self.cold_threshold = cold; self } /// Create with custom access window. pub fn with_access_window(mut self, window: Duration) -> Self { self.access_window = window; self } /// Get the configuration. pub fn config(&self) -> &CacheConfig { &self.config } /// Calculate adjusted TTL based on access pattern. /// /// # Arguments /// /// * `base_ttl` - The base TTL value /// * `access_count` - Number of accesses in the window /// * `cache_hit_rate` - Overall cache hit rate (0.0 to 1.0) /// /// # Returns /// /// The adjusted TTL value. pub fn calculate_ttl(&self, base_ttl: Duration, access_count: u64, cache_hit_rate: f64) -> Duration { if !self.config.adaptive_ttl_enabled { return base_ttl; } let mut adjusted_ttl = base_ttl; // Adjust based on access count if access_count >= self.hot_threshold { // Hot item: extend TTL adjusted_ttl = Duration::from_secs_f64(adjusted_ttl.as_secs_f64() * self.config.ttl_extension_factor); } else if access_count <= self.cold_threshold { // Cold item: reduce TTL adjusted_ttl = Duration::from_secs_f64(adjusted_ttl.as_secs_f64() * self.config.ttl_reduction_factor); } // Adjust based on cache hit rate if cache_hit_rate > 0.8 { // High hit rate: extend TTL adjusted_ttl = Duration::from_secs_f64(adjusted_ttl.as_secs_f64() * 1.2); } else if cache_hit_rate < 0.3 { // Low hit rate: reduce TTL adjusted_ttl = Duration::from_secs_f64(adjusted_ttl.as_secs_f64() * 0.8); } // Clamp to configured range adjusted_ttl.clamp(self.config.min_ttl(), self.config.max_ttl()) } /// Determine if an item should be evicted early. /// /// # Arguments /// /// * `access_count` - Number of accesses since insertion /// * `age` - Time since insertion /// * `current_ttl` - Current TTL value /// /// # Returns /// /// True if the item should be evicted early. pub fn should_evict_early(&self, access_count: u64, age: Duration, current_ttl: Duration) -> bool { // Evict early if: // 1. Item is cold (low access count) // 2. Age is significant (> 50% of TTL) // 3. No recent accesses if access_count <= self.cold_threshold && age > current_ttl / 2 { return true; } false } /// Calculate priority score for an item. /// /// Higher score = higher priority to keep in cache. pub fn calculate_priority(&self, access_count: u64, age: Duration, size: usize) -> f64 { // Priority = access_frequency * recency_factor / size_factor let access_frequency = access_count as f64 / self.access_window.as_secs_f64().max(1.0); // Recency factor: newer items have higher priority let recency_factor = 1.0 / (1.0 + age.as_secs_f64() / 60.0); // Size factor: smaller items have higher priority (more items can fit) let size_factor = (size as f64 / 1024.0).max(1.0); access_frequency * recency_factor / size_factor } } /// Cache statistics. #[derive(Debug, Clone, Default)] pub struct CacheStats { /// Number of cache hits. pub hits: u64, /// Number of cache misses. pub misses: u64, /// Number of entries in the cache. pub entries: u64, /// Total memory used in bytes. pub memory_bytes: u64, /// Number of evictions. pub evictions: u64, /// Number of TTL expirations. pub ttl_expirations: u64, } impl CacheStats { /// Create new cache statistics. pub fn new() -> Self { Self::default() } /// Get the hit rate (0.0 to 1.0). pub fn hit_rate(&self) -> f64 { let total = self.hits + self.misses; if total == 0 { 0.0 } else { self.hits as f64 / total as f64 } } /// Get the miss rate (0.0 to 1.0). pub fn miss_rate(&self) -> f64 { 1.0 - self.hit_rate() } /// Get the total number of lookups. pub fn total_lookups(&self) -> u64 { self.hits + self.misses } /// Record a cache hit. pub fn record_hit(&mut self) { self.hits += 1; } /// Record a cache miss. pub fn record_miss(&mut self) { self.misses += 1; } /// Record an eviction. pub fn record_eviction(&mut self) { self.evictions += 1; } /// Record a TTL expiration. pub fn record_ttl_expiration(&mut self) { self.ttl_expirations += 1; } /// Reset all statistics. pub fn reset(&mut self) { *self = Self::default(); } } /// Cache health status. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CacheHealthStatus { /// Cache is healthy (high hit rate). Healthy, /// Cache is degraded (medium hit rate). Degraded, /// Cache is unhealthy (low hit rate). Unhealthy, /// Cache status is unknown. Unknown, } impl CacheHealthStatus { /// Determine health status from hit rate. pub fn from_hit_rate(hit_rate: f64) -> Self { if hit_rate >= 0.8 { CacheHealthStatus::Healthy } else if hit_rate >= 0.5 { CacheHealthStatus::Degraded } else if hit_rate >= 0.0 { CacheHealthStatus::Unhealthy } else { CacheHealthStatus::Unknown } } /// Get the status as a string. pub fn as_str(&self) -> &'static str { match self { CacheHealthStatus::Healthy => "healthy", CacheHealthStatus::Degraded => "degraded", CacheHealthStatus::Unhealthy => "unhealthy", CacheHealthStatus::Unknown => "unknown", } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_cache_config_default() { let config = CacheConfig::default(); assert!(config.validate().is_ok()); assert!(config.adaptive_ttl_enabled); } #[test] fn test_cache_config_validation() { let config = CacheConfig::new().with_max_capacity(0); assert!(config.validate().is_err()); let config = CacheConfig::new().with_ttl_range(100, 50, 10); assert!(config.validate().is_err()); } #[test] fn test_adaptive_ttl() { let ttl = AdaptiveTTL::default(); // Hot item let base = Duration::from_secs(300); let adjusted = ttl.calculate_ttl(base, 15, 0.5); assert!(adjusted > base); // Cold item let adjusted = ttl.calculate_ttl(base, 1, 0.5); assert!(adjusted < base); } #[test] fn test_cache_stats() { let mut stats = CacheStats::new(); stats.record_hit(); stats.record_hit(); stats.record_miss(); assert_eq!(stats.hits, 2); assert_eq!(stats.misses, 1); assert!((stats.hit_rate() - 0.6666666666666666).abs() < 0.01); } #[test] fn test_cache_health_status() { assert_eq!(CacheHealthStatus::from_hit_rate(0.9), CacheHealthStatus::Healthy); assert_eq!(CacheHealthStatus::from_hit_rate(0.6), CacheHealthStatus::Degraded); assert_eq!(CacheHealthStatus::from_hit_rate(0.2), CacheHealthStatus::Unhealthy); } #[test] fn test_should_evict_early() { let ttl = AdaptiveTTL::default(); // Cold item with significant age assert!(ttl.should_evict_early(1, Duration::from_secs(200), Duration::from_secs(300))); // Hot item assert!(!ttl.should_evict_early(20, Duration::from_secs(200), Duration::from_secs(300))); } #[test] fn test_calculate_priority() { let ttl = AdaptiveTTL::default(); // High access count = high priority let high_priority = ttl.calculate_priority(100, Duration::from_secs(10), 1024); let low_priority = ttl.calculate_priority(1, Duration::from_secs(100), 1024); assert!(high_priority > low_priority); // Smaller size = higher priority let small_priority = ttl.calculate_priority(10, Duration::from_secs(10), 1024); let large_priority = ttl.calculate_priority(10, Duration::from_secs(10), 10240); assert!(small_priority > large_priority); } }