Files
rustfs/crates/io-metrics/src/cache_config.rs
T
2026-03-30 00:30:57 +08:00

450 lines
14 KiB
Rust

// 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);
}
}