diff --git a/Cargo.lock b/Cargo.lock index f7d162eaf..62a41583b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7663,7 +7663,6 @@ dependencies = [ "base64 0.22.1", "base64-simd", "bytes", - "chrono", "clap", "const-str", "datafusion", @@ -7686,7 +7685,6 @@ dependencies = [ "metrics", "mimalloc", "mime_guess", - "moka", "opentelemetry", "percent-encoding", "pin-project-lite", @@ -8240,7 +8238,6 @@ dependencies = [ "bytes", "futures-util", "http 1.4.0", - "rustfs-concurrency", "rustfs-ecstore", "rustfs-io-core", "rustfs-io-metrics", @@ -8248,7 +8245,6 @@ dependencies = [ "rustfs-s3select-api", "rustfs-utils", "s3s", - "serial_test", "thiserror 2.0.18", "time", "tokio", diff --git a/crates/concurrency/src/lib.rs b/crates/concurrency/src/lib.rs index f2646b1ed..d7dbb0344 100644 --- a/crates/concurrency/src/lib.rs +++ b/crates/concurrency/src/lib.rs @@ -146,7 +146,7 @@ pub use config::{ConcurrencyConfig, ConcurrencyFeatures}; // Manager mod manager; -pub use manager::{ConcurrencyManager, GetObjectCacheEligibility, GetObjectQueueSnapshot}; +pub use manager::{ConcurrencyManager, GetObjectQueueSnapshot}; // Prelude for convenient imports pub mod prelude { diff --git a/crates/concurrency/src/manager.rs b/crates/concurrency/src/manager.rs index e8e48742e..f52126201 100644 --- a/crates/concurrency/src/manager.rs +++ b/crates/concurrency/src/manager.rs @@ -55,38 +55,6 @@ impl GetObjectQueueSnapshot { } } -/// Minimal cache writeback decision inputs for GetObject orchestration. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct GetObjectCacheEligibility { - /// Whether response caching is globally enabled. - pub cache_enabled: bool, - /// Whether the selected I/O strategy allows cache writeback. - pub cache_writeback_enabled: bool, - /// Whether the request is for a specific multipart part. - pub is_part_request: bool, - /// Whether the request is a range read. - pub is_range_request: bool, - /// Whether server-side or customer-provided encryption was applied. - pub encryption_applied: bool, - /// Response payload size in bytes. - pub response_size: i64, - /// Maximum cacheable object size in bytes. - pub max_cacheable_size: usize, -} - -impl GetObjectCacheEligibility { - /// Return whether this GetObject response should be cached. - pub fn should_cache(&self) -> bool { - self.cache_enabled - && self.cache_writeback_enabled - && !self.is_part_request - && !self.is_range_request - && !self.encryption_applied - && self.response_size > 0 - && (self.response_size as usize) <= self.max_cacheable_size - } -} - /// Main concurrency manager that provides access to all concurrency features pub struct ConcurrencyManager { config: ConcurrencyConfig, @@ -320,20 +288,6 @@ mod tests { assert!(snapshot.is_congested(70.0)); } - #[test] - fn test_cache_eligibility() { - let plan = GetObjectCacheEligibility { - cache_enabled: true, - cache_writeback_enabled: true, - is_part_request: false, - is_range_request: false, - encryption_applied: false, - response_size: 1024, - max_cacheable_size: 2048, - }; - assert!(plan.should_cache()); - } - #[test] fn test_manager_creation() { let manager = ConcurrencyManager::with_defaults(); diff --git a/crates/config/src/constants/object.rs b/crates/config/src/constants/object.rs index 86626b1cc..0a2aa05b0 100644 --- a/crates/config/src/constants/object.rs +++ b/crates/config/src/constants/object.rs @@ -12,116 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -/// Environment variable name to toggle object-level in-memory caching. -/// -/// - Purpose: Enable or disable the object-level in-memory cache (moka). -/// - Acceptable values: `"true"` / `"false"` (case-insensitive) or a boolean typed config. -/// - Semantics: When enabled, the system keeps fully-read objects in memory to reduce backend requests; when disabled, reads bypass the object cache. -/// - Example: `export RUSTFS_OBJECT_CACHE_ENABLE=true` -/// - Note: Evaluate together with `RUSTFS_OBJECT_CACHE_CAPACITY_MB`, TTL/TTI and concurrency thresholds to balance memory usage and throughput. -pub const ENV_OBJECT_CACHE_ENABLE: &str = "RUSTFS_OBJECT_CACHE_ENABLE"; - -/// Environment variable name that specifies the object cache capacity in megabytes. -/// -/// - Purpose: Set the maximum total capacity of the object cache (in MB). -/// - Unit: MB (1 MB = 1_048_576 bytes). -/// - Valid values: any positive integer (0 may indicate disabled or alternative handling). -/// - Semantics: When the moka cache reaches this capacity, eviction policies will remove entries; tune according to available memory and object size distribution. -/// - Example: `export RUSTFS_OBJECT_CACHE_CAPACITY_MB=512` -/// - Note: Actual memory usage will be slightly higher due to object headers and indexing overhead. -pub const ENV_OBJECT_CACHE_CAPACITY_MB: &str = "RUSTFS_OBJECT_CACHE_CAPACITY_MB"; - -/// Environment variable name for maximum object size eligible for caching in megabytes. -/// -/// - Purpose: Define the upper size limit for individual objects to be considered for caching. -/// - Unit: MB (1 MB = 1_048_576 bytes). -/// - Valid values: any positive integer; objects larger than this size will not be cached. -/// - Semantics: Prevents caching of excessively large objects that could monopolize cache capacity; tune based on typical object size distribution. -/// - Example: `export RUSTFS_OBJECT_CACHE_MAX_OBJECT_SIZE_MB=50` -/// - Note: Setting this too low may reduce cache effectiveness; setting it too high may lead to inefficient memory usage. -pub const ENV_OBJECT_CACHE_MAX_OBJECT_SIZE_MB: &str = "RUSTFS_OBJECT_CACHE_MAX_OBJECT_SIZE_MB"; - -// ============================================================================= -// L1/L2 Tiered Cache Configuration -// ============================================================================= - -/// Environment variable for L1 cache maximum size in megabytes. -/// -/// L1 cache is for hot small objects (<1MB). Higher values improve hit rate for small objects. -pub const ENV_OBJECT_L1_CACHE_MAX_SIZE_MB: &str = "RUSTFS_OBJECT_L1_CACHE_MAX_SIZE_MB"; - -/// Environment variable for L1 cache maximum number of objects. -pub const ENV_OBJECT_L1_CACHE_MAX_OBJECTS: &str = "RUSTFS_OBJECT_L1_CACHE_MAX_OBJECTS"; - -/// Environment variable for L1 cache TTL (time-to-live) in seconds. -pub const ENV_OBJECT_L1_CACHE_TTL_SECS: &str = "RUSTFS_OBJECT_L1_CACHE_TTL_SECS"; - -/// Environment variable for L1 cache TTI (time-to-idle) in seconds. -pub const ENV_OBJECT_L1_CACHE_TTI_SECS: &str = "RUSTFS_OBJECT_L1_CACHE_TTI_SECS"; - -/// Environment variable for L1 cache maximum object size in megabytes. -pub const ENV_OBJECT_L1_MAX_OBJECT_SIZE_MB: &str = "RUSTFS_OBJECT_L1_MAX_OBJECT_SIZE_MB"; - -/// Environment variable for L2 cache maximum size in megabytes. -/// -/// L2 cache is for standard objects (<10MB). -pub const ENV_OBJECT_L2_CACHE_MAX_SIZE_MB: &str = "RUSTFS_OBJECT_L2_CACHE_MAX_SIZE_MB"; - -/// Environment variable for L2 cache maximum number of objects. -pub const ENV_OBJECT_L2_CACHE_MAX_OBJECTS: &str = "RUSTFS_OBJECT_L2_CACHE_MAX_OBJECTS"; - -/// Environment variable for L2 cache TTL (time-to-live) in seconds. -pub const ENV_OBJECT_L2_CACHE_TTL_SECS: &str = "RUSTFS_OBJECT_L2_CACHE_TTL_SECS"; - -/// Environment variable for L2 cache TTI (time-to-idle) in seconds. -pub const ENV_OBJECT_L2_CACHE_TTI_SECS: &str = "RUSTFS_OBJECT_L2_CACHE_TTI_SECS"; - -// ============================================================================= -// Adaptive TTL Configuration -// ============================================================================= - -/// Environment variable to enable adaptive TTL. -/// -/// When enabled, hot objects (with high hit counts) get extended TTL. -pub const ENV_OBJECT_ADAPTIVE_TTL_ENABLE: &str = "RUSTFS_OBJECT_ADAPTIVE_TTL_ENABLE"; - -/// Environment variable for hot object hit threshold. -/// -/// Objects with hit count >= this threshold are considered "hot" and get extended TTL. -pub const ENV_OBJECT_HOT_HIT_THRESHOLD: &str = "RUSTFS_OBJECT_HOT_HIT_THRESHOLD"; - -/// Environment variable for TTL extension factor. -/// -/// Hot objects TTL is extended by this factor (e.g., 2.0 = 2x longer). -pub const ENV_OBJECT_TTL_EXTENSION_FACTOR: &str = "RUSTFS_OBJECT_TTL_EXTENSION_FACTOR"; - -/// Environment variable name for object cache TTL (time-to-live) in seconds. -/// -/// - Purpose: Specify the maximum lifetime of a cached entry from the moment it is written. -/// - Unit: seconds (u64). -/// - Semantics: TTL acts as a hard upper bound; entries older than TTL are considered expired and removed by periodic cleanup. -/// - Example: `export RUSTFS_OBJECT_CACHE_TTL_SECS=300` -/// - Note: TTL and TTI both apply; either policy can cause eviction. -pub const ENV_OBJECT_CACHE_TTL_SECS: &str = "RUSTFS_OBJECT_CACHE_TTL_SECS"; - -/// Environment variable name for object cache TTI (time-to-idle) in seconds. -/// -/// - Purpose: Specify how long an entry may remain in cache without being accessed before it is evicted. -/// - Unit: seconds (u64). -/// - Semantics: TTI helps remove one-time or infrequently used entries; frequent accesses reset idle timers but do not extend beyond TTL unless additional logic exists. -/// - Example: `export RUSTFS_OBJECT_CACHE_TTI_SECS=120` -/// - Note: Works together with TTL to keep the cache populated with actively used objects. -pub const ENV_OBJECT_CACHE_TTI_SECS: &str = "RUSTFS_OBJECT_CACHE_TTI_SECS"; - -/// Environment variable name for threshold of "hot" object hit count used to extend life. -/// -/// - Purpose: Define a hit-count threshold to mark objects as "hot" so they may be treated preferentially near expiration. -/// - Valid values: positive integer (usize). -/// - Semantics: Objects reaching this hit count can be considered for relaxed eviction to avoid thrashing hot items. -/// - Example: `export RUSTFS_OBJECT_HOT_MIN_HITS_TO_EXTEND=5` -/// - Note: This is an optional enhancement and requires cache-layer statistics and extension logic to take effect. -pub const ENV_OBJECT_HOT_MIN_HITS_TO_EXTEND: &str = "RUSTFS_OBJECT_HOT_MIN_HITS_TO_EXTEND"; - /// Environment variable name for high concurrency threshold used in adaptive buffering. /// /// - Purpose: When concurrent request count exceeds this threshold, the system enters a "high concurrency" optimization mode to reduce per-request buffer sizes. @@ -148,38 +38,6 @@ pub const ENV_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD: &str = "RUSTFS_OBJECT_MEDIUM_ /// - Note: This setting may interact with OS-level I/O scheduling and should be tuned based on hardware capabilities. pub const ENV_OBJECT_MAX_CONCURRENT_DISK_READS: &str = "RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS"; -/// Default: object caching is enabled. -/// -/// - Semantics: Caching is now enabled by default for improved performance. Hot objects are kept in memory -/// to reduce backend requests. Set RUSTFS_OBJECT_CACHE_ENABLE=false to disable if needed. -/// - Default is set to true (enabled). -pub const DEFAULT_OBJECT_CACHE_ENABLE: bool = true; - -/// Environment variable to enable tiered cache (L1 + L2). -/// -/// When enabled, uses two-level caching: -/// - L1: Hot small objects (<1MB) with short TTL -/// - L2: Standard objects (<10MB) with longer TTL -/// -/// When enabled, provides L1 (hot small objects) and L2 (standard objects) caching. -/// When disabled, uses single-level cache for backward compatibility. -pub const ENV_OBJECT_TIERED_CACHE_ENABLE: &str = "RUSTFS_OBJECT_TIERED_CACHE_ENABLE"; - -/// Default: tiered cache is enabled for improved cache hit rates. -pub const DEFAULT_OBJECT_TIERED_CACHE_ENABLE: bool = true; - -/// Default object cache capacity in MB. -/// -/// - Default: 100 MB (can be overridden by `RUSTFS_OBJECT_CACHE_CAPACITY_MB`). -/// - Note: Choose a conservative default to reduce memory pressure in development/testing. -pub const DEFAULT_OBJECT_CACHE_CAPACITY_MB: u64 = 100; - -/// Default maximum object size eligible for caching in MB. -/// -/// - Default: 10 MB (can be overridden by `RUSTFS_OBJECT_CACHE_MAX_OBJECT_SIZE_MB`). -/// - Note: Balances caching effectiveness with memory usage. -pub const DEFAULT_OBJECT_CACHE_MAX_OBJECT_SIZE_MB: usize = 10; - /// Maximum concurrent requests before applying aggressive optimization. /// /// When concurrent requests exceed this threshold (>8), the system switches to @@ -209,33 +67,6 @@ pub const DEFAULT_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD: usize = 4; /// Default is set to 64 concurrent reads. pub const DEFAULT_OBJECT_MAX_CONCURRENT_DISK_READS: usize = 64; -/// Time-to-live for cached objects (5 minutes = 300 seconds). -/// -/// After this duration, cached objects are automatically expired by Moka's -/// background cleanup process, even if they haven't been accessed. This prevents -/// stale data from consuming cache capacity indefinitely. -/// -/// Default is set to 300 seconds. -pub const DEFAULT_OBJECT_CACHE_TTL_SECS: u64 = 300; - -/// Time-to-idle for cached objects (2 minutes = 120 seconds). -/// -/// Objects that haven't been accessed for this duration are automatically evicted, -/// even if their TTL hasn't expired. This ensures cache is populated with actively -/// used objects and clears out one-time reads efficiently. -/// -/// Default is set to 120 seconds. -pub const DEFAULT_OBJECT_CACHE_TTI_SECS: u64 = 120; - -/// Minimum hit count to extend object lifetime beyond TTL. -/// -/// "Hot" objects that have been accessed at least this many times are treated -/// specially - they can survive longer in cache even as they approach TTL expiration. -/// This prevents frequently accessed objects from being evicted prematurely. -/// -/// Default is set to 5 hits. -pub const DEFAULT_OBJECT_HOT_MIN_HITS_TO_EXTEND: usize = 5; - /// Skip bitrot hash verification on GetObject reads. /// /// When enabled, GetObject reads skip the per-shard hash @@ -670,63 +501,3 @@ pub const ENV_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY: &str = "RUSTFS_OBJ /// Default read-ahead disable concurrency threshold: 4. pub const DEFAULT_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY: usize = 4; - -// ============================================================================= -// L1/L2 Tiered Cache Default Values -// ============================================================================= - -/// Default L1 cache maximum size: 50 MB. -/// -/// L1 cache is for hot small objects (<1MB). Smaller values reduce memory usage. -pub const DEFAULT_OBJECT_L1_CACHE_MAX_SIZE_MB: u64 = 50; - -/// Default L1 cache maximum number of objects: 1000. -pub const DEFAULT_OBJECT_L1_CACHE_MAX_OBJECTS: usize = 1000; - -/// Default L1 cache TTL: 60 seconds (1 minute). -/// -/// Shorter TTL for L1 cache ensures only very hot objects stay in L1. -pub const DEFAULT_OBJECT_L1_CACHE_TTL_SECS: u64 = 60; - -/// Default L1 cache TTI: 30 seconds. -/// -/// Shorter TTI means L1 evicts idle objects quickly. -pub const DEFAULT_OBJECT_L1_CACHE_TTI_SECS: u64 = 30; - -/// Default L1 maximum object size: 1 MB. -/// -/// Only objects smaller than 1MB are cached in L1. -pub const DEFAULT_OBJECT_L1_MAX_OBJECT_SIZE_MB: usize = 1; - -/// Default L2 cache maximum size: 200 MB. -/// -/// L2 cache is for standard objects (<10MB). -pub const DEFAULT_OBJECT_L2_CACHE_MAX_SIZE_MB: u64 = 200; - -/// Default L2 cache maximum number of objects: 500. -pub const DEFAULT_OBJECT_L2_CACHE_MAX_OBJECTS: usize = 500; - -/// Default L2 cache TTL: 300 seconds (5 minutes). -pub const DEFAULT_OBJECT_L2_CACHE_TTL_SECS: u64 = 300; - -/// Default L2 cache TTI: 120 seconds (2 minutes). -pub const DEFAULT_OBJECT_L2_CACHE_TTI_SECS: u64 = 120; - -// ============================================================================= -// Adaptive TTL Default Values -// ============================================================================= - -/// Default: adaptive TTL is enabled. -/// -/// When enabled, hot objects get extended TTL based on access patterns. -pub const DEFAULT_OBJECT_ADAPTIVE_TTL_ENABLE: bool = true; - -/// Default hot object hit threshold: 3. -/// -/// Objects with hit count >= 3 are considered "hot" and get extended TTL. -pub const DEFAULT_OBJECT_HOT_HIT_THRESHOLD: usize = 3; - -/// Default TTL extension factor: 2.0. -/// -/// Hot objects TTL is extended by 2x (e.g., 5 min TTL becomes 10 min). -pub const DEFAULT_OBJECT_TTL_EXTENSION_FACTOR: f64 = 2.0; diff --git a/crates/io-core/src/scheduler.rs b/crates/io-core/src/scheduler.rs index ea4d194c3..53ce3748a 100644 --- a/crates/io-core/src/scheduler.rs +++ b/crates/io-core/src/scheduler.rs @@ -170,8 +170,6 @@ pub struct IoStrategy { pub buffer_multiplier: f64, /// Whether to enable readahead. pub enable_readahead: bool, - /// Whether cache writeback is enabled. - pub cache_writeback_enabled: bool, /// Whether to use buffered I/O. pub use_buffered_io: bool, @@ -206,7 +204,6 @@ impl Default for IoStrategy { buffer_size: 128 * 1024, buffer_multiplier: 1.0, enable_readahead: true, - cache_writeback_enabled: false, use_buffered_io: true, concurrent_requests: 0, observed_bandwidth_bps: None, @@ -384,7 +381,6 @@ impl IoScheduler { buffer_size, buffer_multiplier: concurrency_factor * load_factor * sequential_factor, enable_readahead: is_sequential && load_level != IoLoadLevel::Critical, - cache_writeback_enabled: load_level == IoLoadLevel::Low, use_buffered_io: true, concurrent_requests, observed_bandwidth_bps: None, diff --git a/crates/io-metrics/src/lib.rs b/crates/io-metrics/src/lib.rs index 345a9d98a..04257ff4d 100644 --- a/crates/io-metrics/src/lib.rs +++ b/crates/io-metrics/src/lib.rs @@ -34,7 +34,7 @@ //! # #[tokio::main] //! # async fn main() { //! // Simple recording -//! record_get_object(100.0, 1024, true); +//! record_get_object(100.0, 1024); //! //! // Advanced usage with collector //! let metrics = Arc::new(PerformanceMetrics::new()); @@ -167,7 +167,6 @@ pub enum IoStage { Unknown, ReadSetup, HttpBridge, - CacheWriteback, LocalDiskChunk, RangeGuard, PutTransform, @@ -180,7 +179,6 @@ impl IoStage { Self::Unknown => "unknown", Self::ReadSetup => "read_setup", Self::HttpBridge => "http_bridge", - Self::CacheWriteback => "cache_writeback", Self::LocalDiskChunk => "local_disk_chunk", Self::RangeGuard => "range_guard", Self::PutTransform => "put_transform", @@ -261,14 +259,6 @@ pub fn record_get_object_request_result(status: &str, duration_secs: f64) { histogram!("rustfs_io_get_object_request_duration_seconds", "status" => status.to_string()).record(duration_secs); } -/// Record GetObject cache-served response. -#[inline(always)] -pub fn record_get_object_cache_served(duration_secs: f64, size_bytes: usize) { - counter!("rustfs_io_get_object_cache_served_total").increment(1); - histogram!("rustfs_io_get_object_cache_serve_duration_seconds").record(duration_secs); - histogram!("rustfs_io_get_object_cache_size_bytes").record(size_bytes as f64); -} - /// Record GetObject timeout for a specific stage. #[inline(always)] pub fn record_get_object_timeout(stage: Option<&str>, elapsed_secs: Option) { @@ -321,12 +311,6 @@ pub fn record_get_object_io_state( counter!("rustfs_io_strategy_selected_total", "level" => load_level.to_string()).increment(1); } -/// Record object cache writeback. -#[inline(always)] -pub fn record_object_cache_writeback() { - counter!("rustfs_io_object_cache_writeback_total").increment(1); -} - /// Record which request path was selected for an operation. #[inline(always)] pub fn record_io_path_selected(operation: &'static str, io_path: IoPath) { @@ -526,24 +510,17 @@ pub fn record_bytes_pool_hit_rate(tier: &str, hit_rate: f64) { /// /// * `duration_ms` - Operation duration in milliseconds /// * `size_bytes` - Object size in bytes -/// * `from_cache` - Whether the object was served from cache /// /// Note: this function records aggregate S3 GET metrics only. It must not be /// interpreted as the definitive source of truth for data-plane copy mode. #[inline(always)] -pub fn record_get_object(duration_ms: f64, size_bytes: i64, from_cache: bool) { +pub fn record_get_object(duration_ms: f64, size_bytes: i64) { counter!("rustfs.s3.get_object.total").increment(1); histogram!("rustfs.s3.get_object.duration.ms").record(duration_ms); if size_bytes > 0 { histogram!("rustfs.s3.get_object.size.bytes").record(size_bytes as f64); } - - if from_cache { - counter!("rustfs.s3.get_object.cache.hits.total").increment(1); - } else { - counter!("rustfs.s3.get_object.cache.misses.total").increment(1); - } } /// Record PutObject operation metrics. @@ -674,51 +651,6 @@ pub fn record_io_load_level(load_level: &str, concurrent_requests: u64) { gauge!("rustfs.io.concurrent.requests").set(concurrent_requests as f64); } -// ============================================================================ -// Cache Performance Metrics -// ============================================================================ - -/// Record tiered cache operation. -/// -/// # Arguments -/// -/// * `tier` - Cache tier ("l1" for hot objects, "l2" for standard objects) -/// * `operation` - Operation type ("hit", "miss", "put", "evict") -/// * `size_bytes` - Object size in bytes (for put/evict operations) -#[inline(always)] -pub fn record_tiered_cache_operation(tier: &str, operation: &str, size_bytes: Option) { - counter!("rustfs.cache.operations.total", - "tier" => tier.to_string(), - "operation" => operation.to_string(), - ) - .increment(1); - - // Track cache size for put/evict operations - if let Some(size) = size_bytes - && matches!(operation, "put" | "evict") - { - gauge!("rustfs.cache.operation.size.bytes", - "tier" => tier.to_string(), - "operation" => operation.to_string(), - ) - .set(size as f64); - } -} - -/// Record cache hit rate for a tier. -/// -/// # Arguments -/// -/// * `tier` - Cache tier ("l1", "l2", or "overall") -/// * `hit_rate` - Hit rate as a percentage (0.0 - 100.0) -#[inline(always)] -pub fn record_cache_hit_rate(tier: &str, hit_rate: f64) { - gauge!("rustfs.cache.hit.rate", - "tier" => tier.to_string(), - ) - .set(hit_rate); -} - /// Record cache size and entry count. /// /// # Arguments @@ -1040,8 +972,8 @@ mod tests { // S3 Operation Metrics Tests #[test] fn test_record_get_object() { - record_get_object(100.0, 1024 * 1024, true); - record_get_object(50.0, 2048, false); + record_get_object(100.0, 1024 * 1024); + record_get_object(50.0, 2048); } #[test] @@ -1082,21 +1014,6 @@ mod tests { record_io_load_level("high", 15); } - // Cache Metrics Tests - #[test] - fn test_record_tiered_cache_operation() { - record_tiered_cache_operation("l1", "hit", None); - record_tiered_cache_operation("l2", "put", Some(1024)); - record_tiered_cache_operation("l1", "evict", Some(2048)); - } - - #[test] - fn test_record_cache_hit_rate() { - record_cache_hit_rate("l1", 85.0); - record_cache_hit_rate("l2", 60.0); - record_cache_hit_rate("overall", 70.0); - } - #[test] fn test_record_cache_size() { record_cache_size("l1", 50 * 1024 * 1024, 1000); diff --git a/crates/object-io/Cargo.toml b/crates/object-io/Cargo.toml index 797c8bfcd..4659f5c9b 100644 --- a/crates/object-io/Cargo.toml +++ b/crates/object-io/Cargo.toml @@ -18,7 +18,6 @@ atoi = { workspace = true } bytes = { workspace = true } futures-util = { workspace = true } rustfs-ecstore = { workspace = true } -rustfs-concurrency = { workspace = true } rustfs-io-core = { workspace = true } rustfs-io-metrics = { workspace = true } rustfs-rio = { workspace = true } @@ -33,5 +32,5 @@ tokio-util = { workspace = true, features = ["io"] } astral-tokio-tar = { workspace = true } uuid = { workspace = true } -[dev-dependencies] -serial_test = { workspace = true } +[lib] +doctest = false diff --git a/crates/object-io/src/get.rs b/crates/object-io/src/get.rs index f59aa1248..4e0d695af 100644 --- a/crates/object-io/src/get.rs +++ b/crates/object-io/src/get.rs @@ -16,7 +16,6 @@ use bytes::Bytes; use futures_util::StreamExt; use http::HeaderMap; use http::header::{CACHE_CONTROL, CONTENT_DISPOSITION, CONTENT_LANGUAGE}; -use rustfs_concurrency::GetObjectCacheEligibility; use rustfs_ecstore::bucket::lifecycle::lifecycle::TRANSITION_COMPLETE; use rustfs_ecstore::client::object_api_utils::to_s3s_etag; use rustfs_ecstore::error::StorageError; @@ -32,7 +31,8 @@ use s3s::dto::{ use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; -use time::{OffsetDateTime, format_description::well_known::Rfc3339}; +#[cfg(test)] +use time::OffsetDateTime; use tokio::io::{AsyncRead, AsyncSeek, ReadBuf}; use tokio_util::io::ReaderStream; @@ -164,30 +164,6 @@ pub fn get_object_sequential_hint(rs: Option<&HTTPRangeSpec>) -> bool { } } -pub trait CachedGetObjectSource { - fn body(&self) -> &Arc; - fn content_length(&self) -> i64; - fn content_type(&self) -> Option<&str>; - fn e_tag(&self) -> Option<&str>; - fn last_modified(&self) -> Option<&str>; - fn expires(&self) -> Option<&str>; - fn cache_control(&self) -> Option<&str>; - fn content_disposition(&self) -> Option<&str>; - fn content_encoding(&self) -> Option<&str>; - fn content_language(&self) -> Option<&str>; - fn storage_class(&self) -> Option<&str>; - fn version_id(&self) -> Option<&str>; - fn delete_marker(&self) -> bool; - fn tag_count(&self) -> Option; - fn user_metadata(&self) -> &HashMap; - fn checksum_crc32(&self) -> Option<&str>; - fn checksum_crc32c(&self) -> Option<&str>; - fn checksum_sha1(&self) -> Option<&str>; - fn checksum_sha256(&self) -> Option<&str>; - fn checksum_crc64nvme(&self) -> Option<&str>; - fn checksum_type(&self) -> Option<&ChecksumType>; -} - pub struct GetObjectOutputContext { pub output: GetObjectOutput, pub event_info: ObjectInfo, @@ -202,6 +178,14 @@ pub struct GetObjectStrategyLayout { pub optimal_buffer_size: usize, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct GetObjectBodyPlanningInputs { + pub is_part_request: bool, + pub is_range_request: bool, + pub encryption_applied: bool, + pub response_size: i64, +} + pub enum GetObjectBodySource { Reader(Box), Chunk { @@ -237,79 +221,25 @@ pub struct LegacyReadPlan { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GetObjectBodyPlan { - CacheWriteback, BufferEncrypted, BufferSeekable, Stream, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum GetObjectDataPlaneRequestSource { - CacheServed, - Disk, -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct GetObjectDataPlaneMetricContract { - pub request_source: GetObjectDataPlaneRequestSource, pub io_path: rustfs_io_metrics::IoPath, pub copy_mode: rustfs_io_metrics::CopyMode, - pub record_cache_served_metric: bool, - pub record_cache_writeback_metric: bool, } impl GetObjectDataPlaneMetricContract { - pub fn cache_served() -> Self { - Self { - request_source: GetObjectDataPlaneRequestSource::CacheServed, - io_path: rustfs_io_metrics::IoPath::Fast, - copy_mode: rustfs_io_metrics::CopyMode::SharedBytes, - record_cache_served_metric: true, - record_cache_writeback_metric: false, - } + pub fn disk(io_path: rustfs_io_metrics::IoPath, copy_mode: rustfs_io_metrics::CopyMode) -> Self { + Self { io_path, copy_mode } } - - pub fn disk( - io_path: rustfs_io_metrics::IoPath, - copy_mode: rustfs_io_metrics::CopyMode, - body_plan: GetObjectBodyPlan, - ) -> Self { - Self { - request_source: GetObjectDataPlaneRequestSource::Disk, - io_path, - copy_mode, - record_cache_served_metric: false, - record_cache_writeback_metric: matches!(body_plan, GetObjectBodyPlan::CacheWriteback), - } - } -} - -pub struct GetObjectCacheWriteback { - pub body: Arc, - pub content_length: i64, - pub content_type: Option, - pub content_encoding: Option, - pub cache_control: Option, - pub content_disposition: Option, - pub content_language: Option, - pub expires: Option, - pub storage_class: Option, - pub version_id: Option, - pub delete_marker: bool, - pub user_metadata: HashMap, - pub e_tag: Option, - pub last_modified: Option, - pub checksum_crc32: Option, - pub checksum_crc32c: Option, - pub checksum_sha1: Option, - pub checksum_sha256: Option, - pub checksum_crc64nvme: Option, - pub checksum_type: Option, } pub struct GetObjectBodyMaterialization { pub body: Option, - pub cache_writeback: Option, pub plan: GetObjectBodyPlan, } @@ -330,55 +260,28 @@ pub struct ChunkReadSetupResult { #[derive(Debug, thiserror::Error)] pub enum MaterializeGetObjectBodyError { - #[error("failed to read object for caching: {0}")] - CacheRead(std::io::Error), #[error("failed to read decrypted object: {0}")] EncryptedRead(std::io::Error), } -pub enum GetObjectResponseMode { - Plain, - CorsWrapped, -} - pub struct GetObjectFlowResult { pub output: GetObjectOutput, pub event_info: ObjectInfo, pub version_id_for_event: String, - pub response_mode: GetObjectResponseMode, } pub fn build_get_object_flow_result( output: GetObjectOutput, event_info: ObjectInfo, version_id_for_event: String, - response_mode: GetObjectResponseMode, ) -> GetObjectFlowResult { GetObjectFlowResult { output, event_info, version_id_for_event, - response_mode, } } -pub fn build_cached_get_object_flow_result_from_source( - bucket: &str, - key: &str, - cached: &T, - version_id_for_event: String, -) -> GetObjectFlowResult -where - T: CachedGetObjectSource, -{ - build_get_object_flow_result( - build_cached_get_object_output_from_source(cached), - build_cached_get_object_event_info_from_source(bucket, key, cached), - version_id_for_event, - GetObjectResponseMode::Plain, - ) -} - pub fn build_cors_wrapped_get_object_flow_result( output_context: GetObjectOutputContext, version_id_for_event: String, @@ -390,79 +293,7 @@ pub fn build_cors_wrapped_get_object_flow_result( optimal_buffer_size: _, copy_mode_override: _, } = output_context; - build_get_object_flow_result(output, event_info, version_id_for_event, GetObjectResponseMode::CorsWrapped) -} - -pub fn build_cached_get_object_output_from_source(cached: &T) -> GetObjectOutput -where - T: CachedGetObjectSource, -{ - let body_data = Arc::clone(cached.body()); - let body = Some(StreamingBlob::wrap::<_, std::convert::Infallible>(futures_util::stream::once( - async move { Ok((*body_data).clone()) }, - ))); - - let last_modified = cached - .last_modified() - .and_then(|s| OffsetDateTime::parse(s, &Rfc3339).ok()) - .map(Timestamp::from); - let expires = cached - .expires() - .and_then(|s| OffsetDateTime::parse(s, &Rfc3339).ok()) - .map(Timestamp::from); - - let content_type = cached.content_type().and_then(|ct| ContentType::from_str(ct).ok()); - - let metadata = (!cached.user_metadata().is_empty()).then(|| cached.user_metadata().clone()); - - GetObjectOutput { - body, - content_length: Some(cached.content_length()), - accept_ranges: Some("bytes".to_string()), - e_tag: cached.e_tag().map(to_s3s_etag), - last_modified, - expires, - content_type, - cache_control: cached.cache_control().map(str::to_string), - content_disposition: cached.content_disposition().map(str::to_string), - content_encoding: cached.content_encoding().map(str::to_string), - content_language: cached.content_language().map(str::to_string), - version_id: cached.version_id().map(str::to_string), - delete_marker: Some(cached.delete_marker()), - tag_count: cached.tag_count(), - metadata, - checksum_crc32: cached.checksum_crc32().map(str::to_string), - checksum_crc32c: cached.checksum_crc32c().map(str::to_string), - checksum_sha1: cached.checksum_sha1().map(str::to_string), - checksum_sha256: cached.checksum_sha256().map(str::to_string), - checksum_crc64nvme: cached.checksum_crc64nvme().map(str::to_string), - checksum_type: cached.checksum_type().cloned(), - ..Default::default() - } -} -pub fn build_cached_get_object_event_info_from_source(bucket: &str, key: &str, cached: &T) -> ObjectInfo -where - T: CachedGetObjectSource, -{ - let last_modified = cached.last_modified().and_then(|s| OffsetDateTime::parse(s, &Rfc3339).ok()); - let version_id = cached.version_id().and_then(|v| uuid::Uuid::parse_str(v).ok()); - - ObjectInfo { - bucket: bucket.to_string(), - name: key.to_string(), - storage_class: cached.storage_class().map(str::to_string), - mod_time: last_modified, - size: cached.content_length(), - actual_size: cached.content_length(), - is_dir: false, - user_defined: cached.user_metadata().clone(), - version_id, - delete_marker: cached.delete_marker(), - content_type: cached.content_type().map(str::to_string), - content_encoding: cached.content_encoding().map(str::to_string), - etag: cached.e_tag().map(str::to_string), - ..Default::default() - } + build_get_object_flow_result(output, event_info, version_id_for_event) } #[derive(Debug, Default, Clone, PartialEq, Eq)] @@ -559,19 +390,15 @@ pub fn plan_get_object_strategy_layout( } pub fn plan_get_object_body( - cache_eligibility: GetObjectCacheEligibility, + planning_inputs: GetObjectBodyPlanningInputs, seekable_object_size_threshold: usize, ) -> GetObjectBodyPlan { - if cache_eligibility.should_cache() { - return GetObjectBodyPlan::CacheWriteback; - } + let should_buffer_for_seek = planning_inputs.response_size > 0 + && planning_inputs.response_size <= seekable_object_size_threshold as i64 + && !planning_inputs.is_part_request + && !planning_inputs.is_range_request; - let should_buffer_for_seek = cache_eligibility.response_size > 0 - && cache_eligibility.response_size <= seekable_object_size_threshold as i64 - && !cache_eligibility.is_part_request - && !cache_eligibility.is_range_request; - - if cache_eligibility.encryption_applied && should_buffer_for_seek { + if planning_inputs.encryption_applied && should_buffer_for_seek { GetObjectBodyPlan::BufferEncrypted } else if should_buffer_for_seek { GetObjectBodyPlan::BufferSeekable @@ -580,57 +407,8 @@ pub fn plan_get_object_body( } } -pub fn build_get_object_cache_writeback(info: &ObjectInfo, body: Bytes, content_length: i64) -> GetObjectCacheWriteback { - let checksums = read_object_checksums(info, &HeaderMap::new(), None).unwrap_or_default(); - let body = FrozenGetObjectBody::new(body); - GetObjectCacheWriteback { - body: body.into_shared_body(), - content_length, - content_type: info.content_type.clone(), - content_encoding: info.content_encoding.clone(), - cache_control: None, - content_disposition: None, - content_language: None, - expires: None, - storage_class: info.storage_class.clone(), - version_id: info.version_id.map(|vid| { - if vid == uuid::Uuid::nil() { - "null".to_string() - } else { - vid.to_string() - } - }), - delete_marker: info.delete_marker, - user_metadata: HashMap::new(), - e_tag: info.etag.clone(), - last_modified: info.mod_time.and_then(|t| t.format(&Rfc3339).ok()), - checksum_crc32: checksums.crc32, - checksum_crc32c: checksums.crc32c, - checksum_sha1: checksums.sha1, - checksum_sha256: checksums.sha256, - checksum_crc64nvme: checksums.crc64nvme, - checksum_type: checksums.checksum_type, - } -} - -pub fn finalize_get_object_cache_writeback( - info: &ObjectInfo, - writeback: GetObjectCacheWriteback, - user_metadata: HashMap, -) -> GetObjectCacheWriteback { - GetObjectCacheWriteback { - cache_control: info.user_defined.get(CACHE_CONTROL.as_str()).cloned(), - content_disposition: info.user_defined.get(CONTENT_DISPOSITION.as_str()).cloned(), - content_language: info.user_defined.get(CONTENT_LANGUAGE.as_str()).cloned(), - expires: info.expires.and_then(|t| t.format(&Rfc3339).ok()), - user_metadata, - ..writeback - } -} - pub async fn materialize_get_object_body( mut final_stream: R, - info: &ObjectInfo, plan: GetObjectBodyPlan, response_content_length: i64, optimal_buffer_size: usize, @@ -639,23 +417,6 @@ where R: AsyncRead + Send + Sync + Unpin + 'static, { match plan { - GetObjectBodyPlan::CacheWriteback => { - let mut buf = Vec::with_capacity(response_content_length as usize); - tokio::io::AsyncReadExt::read_to_end(&mut final_stream, &mut buf) - .await - .map_err(MaterializeGetObjectBodyError::CacheRead)?; - let body = FrozenGetObjectBody::new(Bytes::from(buf)); - - Ok(GetObjectBodyMaterialization { - body: body.build_blob(response_content_length, optimal_buffer_size), - cache_writeback: Some(build_get_object_cache_writeback( - info, - body.shared_body().as_ref().clone(), - response_content_length, - )), - plan, - }) - } GetObjectBodyPlan::BufferEncrypted => { let mut buf = Vec::with_capacity(response_content_length as usize); tokio::io::AsyncReadExt::read_to_end(&mut final_stream, &mut buf) @@ -665,7 +426,6 @@ where Ok(GetObjectBodyMaterialization { body: body.build_blob(response_content_length, optimal_buffer_size), - cache_writeback: None, plan, }) } @@ -676,15 +436,10 @@ where Err(_) => build_reader_blob(final_stream, response_content_length, optimal_buffer_size), }; - Ok(GetObjectBodyMaterialization { - body, - cache_writeback: None, - plan, - }) + Ok(GetObjectBodyMaterialization { body, plan }) } GetObjectBodyPlan::Stream => Ok(GetObjectBodyMaterialization { body: build_reader_blob(final_stream, response_content_length, optimal_buffer_size), - cache_writeback: None, plan, }), } @@ -1056,116 +811,6 @@ pub fn plan_chunk_read( mod tests { use super::*; - struct MockCachedSource { - body: Arc, - content_length: i64, - content_type: Option, - e_tag: Option, - last_modified: Option, - expires: Option, - cache_control: Option, - content_disposition: Option, - content_encoding: Option, - content_language: Option, - storage_class: Option, - version_id: Option, - delete_marker: bool, - tag_count: Option, - user_metadata: HashMap, - checksum_crc32: Option, - checksum_crc32c: Option, - checksum_sha1: Option, - checksum_sha256: Option, - checksum_crc64nvme: Option, - checksum_type: Option, - } - - impl CachedGetObjectSource for MockCachedSource { - fn body(&self) -> &Arc { - &self.body - } - - fn content_length(&self) -> i64 { - self.content_length - } - - fn content_type(&self) -> Option<&str> { - self.content_type.as_deref() - } - - fn e_tag(&self) -> Option<&str> { - self.e_tag.as_deref() - } - - fn last_modified(&self) -> Option<&str> { - self.last_modified.as_deref() - } - - fn expires(&self) -> Option<&str> { - self.expires.as_deref() - } - - fn cache_control(&self) -> Option<&str> { - self.cache_control.as_deref() - } - - fn content_disposition(&self) -> Option<&str> { - self.content_disposition.as_deref() - } - - fn content_encoding(&self) -> Option<&str> { - self.content_encoding.as_deref() - } - - fn content_language(&self) -> Option<&str> { - self.content_language.as_deref() - } - - fn storage_class(&self) -> Option<&str> { - self.storage_class.as_deref() - } - - fn version_id(&self) -> Option<&str> { - self.version_id.as_deref() - } - - fn delete_marker(&self) -> bool { - self.delete_marker - } - - fn tag_count(&self) -> Option { - self.tag_count - } - - fn user_metadata(&self) -> &HashMap { - &self.user_metadata - } - - fn checksum_crc32(&self) -> Option<&str> { - self.checksum_crc32.as_deref() - } - - fn checksum_crc32c(&self) -> Option<&str> { - self.checksum_crc32c.as_deref() - } - - fn checksum_sha1(&self) -> Option<&str> { - self.checksum_sha1.as_deref() - } - - fn checksum_sha256(&self) -> Option<&str> { - self.checksum_sha256.as_deref() - } - - fn checksum_crc64nvme(&self) -> Option<&str> { - self.checksum_crc64nvme.as_deref() - } - - fn checksum_type(&self) -> Option<&ChecksumType> { - self.checksum_type.as_ref() - } - } - #[test] fn map_chunk_copy_mode_uses_expected_metric_modes() { assert_eq!( @@ -1426,60 +1071,37 @@ mod tests { } #[test] - fn plan_get_object_body_prefers_cache_writeback_when_cacheable() { + fn plan_get_object_body_buffers_seekable_small_plain_request() { let plan = plan_get_object_body( - GetObjectCacheEligibility { - cache_enabled: true, - cache_writeback_enabled: true, + GetObjectBodyPlanningInputs { is_part_request: false, is_range_request: false, encryption_applied: false, response_size: 1024, - max_cacheable_size: 2048, }, 4096, ); - assert_eq!(plan, GetObjectBodyPlan::CacheWriteback); + assert_eq!(plan, GetObjectBodyPlan::BufferSeekable); } #[test] - fn cache_served_metric_contract_is_mutually_exclusive_with_cache_writeback() { - let contract = GetObjectDataPlaneMetricContract::cache_served(); + fn disk_metric_contract_preserves_io_labels() { + let contract = + GetObjectDataPlaneMetricContract::disk(rustfs_io_metrics::IoPath::Legacy, rustfs_io_metrics::CopyMode::SingleCopy); - assert_eq!(contract.request_source, GetObjectDataPlaneRequestSource::CacheServed); - assert_eq!(contract.io_path, rustfs_io_metrics::IoPath::Fast); - assert_eq!(contract.copy_mode, rustfs_io_metrics::CopyMode::SharedBytes); - assert!(contract.record_cache_served_metric); - assert!(!contract.record_cache_writeback_metric); - } - - #[test] - fn disk_metric_contract_can_mark_cache_writeback_without_reclassifying_request_source() { - let contract = GetObjectDataPlaneMetricContract::disk( - rustfs_io_metrics::IoPath::Legacy, - rustfs_io_metrics::CopyMode::SingleCopy, - GetObjectBodyPlan::CacheWriteback, - ); - - assert_eq!(contract.request_source, GetObjectDataPlaneRequestSource::Disk); assert_eq!(contract.io_path, rustfs_io_metrics::IoPath::Legacy); assert_eq!(contract.copy_mode, rustfs_io_metrics::CopyMode::SingleCopy); - assert!(!contract.record_cache_served_metric); - assert!(contract.record_cache_writeback_metric); } #[test] fn plan_get_object_body_uses_encrypted_buffer_for_small_plain_request() { let plan = plan_get_object_body( - GetObjectCacheEligibility { - cache_enabled: false, - cache_writeback_enabled: false, + GetObjectBodyPlanningInputs { is_part_request: false, is_range_request: false, encryption_applied: true, response_size: 1024, - max_cacheable_size: 0, }, 4096, ); @@ -1516,74 +1138,7 @@ mod tests { } #[test] - fn build_get_object_cache_writeback_formats_metadata() { - let info = ObjectInfo { - content_type: Some("application/octet-stream".to_string()), - etag: Some("abc123".to_string()), - mod_time: Some(OffsetDateTime::UNIX_EPOCH), - checksum: rustfs_rio::Checksum::new_from_data(rustfs_rio::ChecksumType::CRC32, b"abc") - .map(|checksum| checksum.to_bytes(&[])), - ..Default::default() - }; - - let writeback = build_get_object_cache_writeback(&info, Bytes::from_static(b"abc"), 3); - - assert_eq!(*writeback.body, Bytes::from_static(b"abc")); - assert_eq!(writeback.content_length, 3); - assert_eq!(writeback.content_type.as_deref(), Some("application/octet-stream")); - assert_eq!(writeback.e_tag.as_deref(), Some("abc123")); - assert_eq!(writeback.last_modified.as_deref(), Some("1970-01-01T00:00:00Z")); - assert_eq!(writeback.checksum_crc32.as_deref(), Some("NSRBwg==")); - } - - #[test] - fn finalize_get_object_cache_writeback_applies_http_metadata_and_user_metadata() { - let mut info = ObjectInfo { - expires: Some(OffsetDateTime::UNIX_EPOCH), - ..Default::default() - }; - info.user_defined - .insert("cache-control".to_string(), "max-age=3600".to_string()); - info.user_defined - .insert("content-disposition".to_string(), "attachment".to_string()); - info.user_defined.insert("content-language".to_string(), "en-US".to_string()); - - let writeback = finalize_get_object_cache_writeback( - &info, - GetObjectCacheWriteback { - body: Arc::new(Bytes::from_static(b"abc")), - content_length: 3, - content_type: None, - content_encoding: None, - cache_control: None, - content_disposition: None, - content_language: None, - expires: None, - storage_class: None, - version_id: None, - delete_marker: false, - user_metadata: HashMap::new(), - e_tag: None, - last_modified: None, - checksum_crc32: None, - checksum_crc32c: None, - checksum_sha1: None, - checksum_sha256: None, - checksum_crc64nvme: None, - checksum_type: None, - }, - HashMap::from([(String::from("custom"), String::from("value"))]), - ); - - assert_eq!(writeback.cache_control.as_deref(), Some("max-age=3600")); - assert_eq!(writeback.content_disposition.as_deref(), Some("attachment")); - assert_eq!(writeback.content_language.as_deref(), Some("en-US")); - assert_eq!(writeback.expires.as_deref(), Some("1970-01-01T00:00:00Z")); - assert_eq!(writeback.user_metadata.get("custom").map(String::as_str), Some("value")); - } - - #[test] - fn frozen_get_object_body_reuses_same_shared_bytes_for_cache_writeback() { + fn frozen_get_object_body_reuses_same_shared_bytes_for_memory_blob() { let frozen = FrozenGetObjectBody::new(Bytes::from_static(b"abc")); let shared = Arc::clone(frozen.shared_body()); assert_eq!(*shared, Bytes::from_static(b"abc")); @@ -1628,45 +1183,6 @@ mod tests { assert_eq!(output_context.optimal_buffer_size, 4096); } - #[test] - fn build_cached_get_object_flow_result_from_source_builds_plain_mode() { - let result = build_cached_get_object_flow_result_from_source( - "bucket", - "key", - &MockCachedSource { - body: Arc::new(Bytes::from_static(b"abc")), - content_length: 3, - content_type: None, - e_tag: None, - last_modified: None, - expires: None, - cache_control: None, - content_disposition: None, - content_encoding: None, - content_language: None, - storage_class: None, - version_id: None, - delete_marker: false, - tag_count: None, - user_metadata: HashMap::new(), - checksum_crc32: Some("crc32".to_string()), - checksum_crc32c: None, - checksum_sha1: None, - checksum_sha256: None, - checksum_crc64nvme: None, - checksum_type: Some(ChecksumType::from_static(ChecksumType::FULL_OBJECT)), - }, - "vid".to_string(), - ); - - assert!(matches!(result.response_mode, GetObjectResponseMode::Plain)); - assert_eq!(result.version_id_for_event, "vid"); - assert_eq!(result.event_info.bucket, "bucket"); - assert_eq!(result.event_info.name, "key"); - assert_eq!(result.output.checksum_crc32.as_deref(), Some("crc32")); - assert_eq!(result.output.checksum_type, Some(ChecksumType::from_static(ChecksumType::FULL_OBJECT))); - } - #[test] fn build_get_object_output_preserves_http_metadata_like_cached_path() { let mut info = ObjectInfo { @@ -1720,7 +1236,6 @@ mod tests { "vid".to_string(), ); - assert!(matches!(result.response_mode, GetObjectResponseMode::CorsWrapped)); assert_eq!(result.version_id_for_event, "vid"); } diff --git a/rustfs/Cargo.toml b/rustfs/Cargo.toml index ff923e174..7c3653c30 100644 --- a/rustfs/Cargo.toml +++ b/rustfs/Cargo.toml @@ -124,7 +124,6 @@ serde_urlencoded = { workspace = true } # Cryptography and Security rustls = { workspace = true } subtle = { workspace = true } -chrono = { workspace = true } jiff = { workspace = true } time = { workspace = true, features = ["parsing", "formatting", "serde"] } @@ -142,7 +141,6 @@ hex-simd.workspace = true matchit = { workspace = true } md5.workspace = true mime_guess = { workspace = true } -moka = { workspace = true } percent-encoding = { workspace = true } pin-project-lite.workspace = true rust-embed = { workspace = true, features = ["interpolate-folder-path"] } diff --git a/rustfs/src/app/multipart_usecase.rs b/rustfs/src/app/multipart_usecase.rs index 7af30a263..16fc92990 100644 --- a/rustfs/src/app/multipart_usecase.rs +++ b/rustfs/src/app/multipart_usecase.rs @@ -18,14 +18,12 @@ use crate::app::context::{AppContext, get_global_app_context}; use crate::app::object_usecase::{build_put_like_object_lock_metadata, validate_existing_object_lock_for_write}; use crate::error::ApiError; use crate::storage::access::has_bypass_governance_header; -use crate::storage::concurrency::get_concurrency_manager; use crate::storage::entity; use crate::storage::helper::OperationHelper; use crate::storage::options::{ copy_src_opts, extract_metadata, get_complete_multipart_upload_opts, get_content_sha256_with_query, get_opts, parse_copy_source_range, put_opts, validate_archive_content_encoding, }; -use crate::storage::request_context::spawn_traced; use crate::storage::s3_api::multipart::build_list_parts_output; use crate::storage::*; use bytes::Bytes; @@ -398,24 +396,13 @@ impl DefaultMultipartUsecase { maybe_enqueue_transition_immediate(&obj_info, LcEventSrc::S3CompleteMultipartUpload).await; - // Invalidate cache for the completed multipart object - let manager = get_concurrency_manager(); - let mpu_bucket = bucket.clone(); - let mpu_key = key.clone(); let raw_mpu_version = obj_info.version_id.map(|v| v.to_string()); let mpu_version = if BucketVersioningSys::prefix_enabled(&bucket, &key).await { raw_mpu_version.clone() } else { None }; - let mpu_version_clone = mpu_version.clone(); let mpu_version_for_event = mpu_version.clone(); - spawn_traced(async move { - manager - .invalidate_cache_versioned(&mpu_bucket, &mpu_key, mpu_version_clone.as_deref()) - .await; - }); - info!( "TDD: Creating output with SSE: {:?}, KMS Key: {:?}", server_side_encryption, ssekms_key_id diff --git a/rustfs/src/app/object_usecase.rs b/rustfs/src/app/object_usecase.rs index 6f57f5507..3df36f1c4 100644 --- a/rustfs/src/app/object_usecase.rs +++ b/rustfs/src/app/object_usecase.rs @@ -31,7 +31,7 @@ use crate::capacity::capacity_manager::get_capacity_manager; use crate::config::RustFSBufferConfig; use crate::error::ApiError; use crate::storage::access::{PostObjectRequestMarker, authorize_request, has_bypass_governance_header, req_info_mut}; -use crate::storage::concurrency::{CachedGetObject, ConcurrencyManager, GetObjectGuard, get_concurrency_manager}; +use crate::storage::concurrency::{GetObjectGuard, get_concurrency_manager}; use crate::storage::ecfs::*; use crate::storage::head_prefix::{head_prefix_not_found_message, probe_prefix_has_children}; use crate::storage::helper::OperationHelper; @@ -599,13 +599,6 @@ impl DefaultObjectUsecase { } } - fn spawn_cache_invalidation(bucket: String, key: String, version_id: Option) { - let manager = get_concurrency_manager(); - crate::storage::request_context::spawn_traced(async move { - manager.invalidate_cache_versioned(&bucket, &key, version_id.as_deref()).await; - }); - } - #[instrument(level = "debug", skip(self, _fs, req))] pub async fn execute_put_object(&self, _fs: &FS, req: S3Request) -> S3Result> { if let Some(context) = &self.context { @@ -987,18 +980,6 @@ impl DefaultObjectUsecase { } }; - let manager = get_concurrency_manager(); - let version_id = req.input.version_id.clone(); - let cache_key = ConcurrencyManager::make_cache_key(&bucket, &object, version_id.clone().as_deref()); - let cache_bucket = bucket.clone(); - let cache_object = object.clone(); - crate::storage::request_context::spawn_traced(async move { - manager - .invalidate_cache_versioned(&cache_bucket, &cache_object, version_id.as_deref()) - .await; - debug!("Cache invalidated for tagged object: {}", cache_key); - }); - counter!("rustfs.put_object_tagging.success").increment(1); let event_version_id = req @@ -1793,7 +1774,6 @@ impl DefaultObjectUsecase { } let raw_dest_version = oi.version_id.map(|v| v.to_string()); - Self::spawn_cache_invalidation(bucket.clone(), key.clone(), raw_dest_version.clone()); let dest_version = if BucketVersioningSys::prefix_enabled(&bucket, &key).await { raw_dest_version } else { @@ -1995,21 +1975,6 @@ impl DefaultObjectUsecase { ) .await; - let manager = get_concurrency_manager(); - let bucket_clone = bucket.clone(); - let deleted_objects = dobjs.clone(); - crate::storage::request_context::spawn_traced(async move { - for dobj in deleted_objects { - manager - .invalidate_cache_versioned( - &bucket_clone, - &dobj.object_name, - dobj.version_id.map(|v| v.to_string()).as_deref(), - ) - .await; - } - }); - if is_all_buckets_not_found( &errs .iter() @@ -2267,8 +2232,6 @@ impl DefaultObjectUsecase { // Fast in-memory update for immediate quota consistency rustfs_ecstore::data_usage::decrement_bucket_usage_memory(&bucket, obj_info.size as u64).await; - Self::spawn_cache_invalidation(bucket.clone(), key.clone(), obj_info.version_id.map(|v| v.to_string())); - if obj_info.name.is_empty() { if replicate_force_delete { schedule_replication_delete(DeletedObjectReplicationInfo { @@ -2395,20 +2358,6 @@ impl DefaultObjectUsecase { } }; - let manager = get_concurrency_manager(); - let version_id_clone = version_id.clone(); - let cache_bucket = bucket.clone(); - let cache_object = object.clone(); - crate::storage::request_context::spawn_traced(async move { - manager - .invalidate_cache_versioned(&cache_bucket, &cache_object, version_id_clone.as_deref()) - .await; - debug!( - "Cache invalidated for deleted tagged object: bucket={}, object={}, version_id={:?}", - cache_bucket, cache_object, version_id_clone - ); - }); - counter!("rustfs.delete_object_tagging.success").increment(1); let event_version_id = version_id diff --git a/rustfs/src/app/object_usecase/app_adapters.rs b/rustfs/src/app/object_usecase/app_adapters.rs index 667aa9ce9..a8156c486 100644 --- a/rustfs/src/app/object_usecase/app_adapters.rs +++ b/rustfs/src/app/object_usecase/app_adapters.rs @@ -15,14 +15,12 @@ use super::get_object_flow::GetObjectBootstrap; use super::*; use crate::app::context::NotifyInterface; -use crate::storage::concurrency::{self, get_buffer_size_opt_in}; +use crate::storage::concurrency::{self, ConcurrencyManager, get_buffer_size_opt_in}; use hashbrown::HashMap; use rustfs_object_io::get::{ - CachedGetObjectSource as ObjectIoCachedGetObjectSource, GetObjectBodyPlan as ObjectIoGetObjectBodyPlan, - GetObjectCacheWriteback, GetObjectDataPlaneMetricContract as ObjectIoGetObjectDataPlaneMetricContract, GetObjectFlowResult, - GetObjectResponseMode, MaterializeGetObjectBodyError as ObjectIoMaterializeGetObjectBodyError, - build_cached_get_object_flow_result_from_source as object_io_build_cached_get_object_flow_result_from_source, - finalize_get_object_cache_writeback as object_io_finalize_get_object_cache_writeback, + GetObjectBodyPlan as ObjectIoGetObjectBodyPlan, GetObjectBodyPlanningInputs as ObjectIoGetObjectBodyPlanningInputs, + GetObjectDataPlaneMetricContract as ObjectIoGetObjectDataPlaneMetricContract, GetObjectFlowResult, + MaterializeGetObjectBodyError as ObjectIoMaterializeGetObjectBodyError, materialize_get_object_body as object_io_materialize_get_object_body, plan_get_object_body as object_io_plan_get_object_body, plan_get_object_strategy_layout as object_io_plan_get_object_strategy_layout, }; @@ -69,7 +67,6 @@ pub(super) async fn prepare_get_object_request_context(req: &S3Request &std::sync::Arc { - &self.body - } - - fn content_length(&self) -> i64 { - self.content_length - } - - fn content_type(&self) -> Option<&str> { - self.content_type.as_deref() - } - - fn e_tag(&self) -> Option<&str> { - self.e_tag.as_deref() - } - - fn last_modified(&self) -> Option<&str> { - self.last_modified.as_deref() - } - - fn expires(&self) -> Option<&str> { - self.expires.as_deref() - } - - fn cache_control(&self) -> Option<&str> { - self.cache_control.as_deref() - } - - fn content_disposition(&self) -> Option<&str> { - self.content_disposition.as_deref() - } - - fn content_encoding(&self) -> Option<&str> { - self.content_encoding.as_deref() - } - - fn content_language(&self) -> Option<&str> { - self.content_language.as_deref() - } - - fn storage_class(&self) -> Option<&str> { - self.storage_class.as_deref() - } - - fn version_id(&self) -> Option<&str> { - self.version_id.as_deref() - } - - fn delete_marker(&self) -> bool { - self.delete_marker - } - - fn tag_count(&self) -> Option { - self.tag_count - } - - fn user_metadata(&self) -> &std::collections::HashMap { - &self.user_metadata - } - - fn checksum_crc32(&self) -> Option<&str> { - self.checksum_crc32.as_deref() - } - - fn checksum_crc32c(&self) -> Option<&str> { - self.checksum_crc32c.as_deref() - } - - fn checksum_sha1(&self) -> Option<&str> { - self.checksum_sha1.as_deref() - } - - fn checksum_sha256(&self) -> Option<&str> { - self.checksum_sha256.as_deref() - } - - fn checksum_crc64nvme(&self) -> Option<&str> { - self.checksum_crc64nvme.as_deref() - } - - fn checksum_type(&self) -> Option<&ChecksumType> { - self.checksum_type.as_ref() - } -} - pub(super) fn init_get_object_bootstrap(bucket: &str, key: &str, request_id: &str) -> S3Result { let timeout_config = TimeoutConfig::from_env(); let wrapper = RequestTimeoutWrapper::with_request_id(timeout_config.clone(), request_id.to_string()); @@ -204,108 +115,32 @@ pub(super) fn init_get_object_bootstrap(bucket: &str, key: &str, request_id: &st request_start, request_guard, _deadlock_request_guard: deadlock_request_guard, - concurrent_requests, }) } -#[allow(clippy::too_many_arguments)] -pub(super) async fn maybe_get_cached_get_object_flow_result( - manager: &ConcurrencyManager, - bucket: &str, - key: &str, - cache_key: &str, - version_id_for_event: String, - part_number: Option, - rs: Option<&HTTPRangeSpec>, - request_start: std::time::Instant, -) -> Option { - if !manager.is_cache_enabled() || part_number.is_some() || rs.is_some() { - return None; - } - - let cached = manager.get_cached_object(cache_key).await?; - let cache_serve_duration = request_start.elapsed(); - let metric_contract = ObjectIoGetObjectDataPlaneMetricContract::cache_served(); - - debug!("Serving object from response cache: {} (latency: {:?})", cache_key, cache_serve_duration); - - if metric_contract.record_cache_served_metric { - rustfs_io_metrics::record_get_object_cache_served(cache_serve_duration.as_secs_f64(), cached.body.len()); - } - rustfs_io_metrics::record_io_path_selected("get", metric_contract.io_path); - rustfs_io_metrics::record_io_copy_mode("get", metric_contract.copy_mode, cached.body.len()); - - manager.record_transfer(cached.content_length as u64, Duration::from_micros(1)); - - rustfs_io_metrics::record_get_object(request_start.elapsed().as_millis() as f64, cached.content_length, true); - - Some(object_io_build_cached_get_object_flow_result_from_source( - bucket, - key, - cached.as_ref(), - version_id_for_event, - )) -} - -pub(super) struct GetObjectBodyAdapterOutput { - pub(super) body: Option, - pub(super) body_plan: ObjectIoGetObjectBodyPlan, - pub(super) cache_writeback: Option, -} - -pub(super) fn spawn_get_object_cache_writeback( - cache_key: &str, - writeback: GetObjectCacheWriteback, - metric_contract: ObjectIoGetObjectDataPlaneMetricContract, -) { - debug_assert_eq!( - metric_contract.request_source, - rustfs_object_io::get::GetObjectDataPlaneRequestSource::Disk - ); - debug_assert!(!metric_contract.record_cache_served_metric); - debug_assert!(metric_contract.record_cache_writeback_metric); - - let cached_response = CachedGetObject::from_get_object_cache_writeback(writeback); - - let cache_key_clone = cache_key.to_string(); - crate::storage::request_context::spawn_traced(async move { - let manager = get_concurrency_manager(); - manager.put_cached_object(cache_key_clone.clone(), cached_response).await; - debug!("Object cached successfully with metadata: {}", cache_key_clone); - }); - - if metric_contract.record_cache_writeback_metric { - rustfs_io_metrics::record_object_cache_writeback(); - } -} - pub(super) async fn build_get_object_body_adapter( final_stream: R, - info: &ObjectInfo, - cache_key: &str, + bucket: &str, + key: &str, response_content_length: i64, optimal_buffer_size: usize, - cache_eligibility: rustfs_concurrency::GetObjectCacheEligibility, -) -> S3Result + planning_inputs: ObjectIoGetObjectBodyPlanningInputs, +) -> S3Result> where R: AsyncRead + Send + Sync + Unpin + 'static, { - let body_plan = object_io_plan_get_object_body(cache_eligibility, rustfs_config::DEFAULT_OBJECT_SEEK_SUPPORT_THRESHOLD); + let body_plan = object_io_plan_get_object_body(planning_inputs, rustfs_config::DEFAULT_OBJECT_SEEK_SUPPORT_THRESHOLD); match body_plan { - ObjectIoGetObjectBodyPlan::CacheWriteback => { - debug!( - "Reading object into memory for caching: key={} size={}", - cache_key, response_content_length - ); - } ObjectIoGetObjectBodyPlan::BufferSeekable => { debug!( - "Reading small object into memory for seek support: key={} size={}", - cache_key, response_content_length + bucket = %bucket, + key = %key, + size = response_content_length, + "reading object into memory for seek support" ); } - ObjectIoGetObjectBodyPlan::Stream if cache_eligibility.encryption_applied => { + ObjectIoGetObjectBodyPlan::Stream if planning_inputs.encryption_applied => { info!( "Encrypted object: Using unlimited stream for decryption with buffer size {}", optimal_buffer_size @@ -315,76 +150,94 @@ where } let materialized = - object_io_materialize_get_object_body(final_stream, info, body_plan, response_content_length, optimal_buffer_size) + object_io_materialize_get_object_body(final_stream, body_plan, response_content_length, optimal_buffer_size) .await .map_err(|err| match err { - ObjectIoMaterializeGetObjectBodyError::CacheRead(err) => { - error!("Failed to read object into memory for caching: {}", err); - ApiError::from(StorageError::other(format!("Failed to read object for caching: {err}"))) - } ObjectIoMaterializeGetObjectBodyError::EncryptedRead(err) => { error!("Failed to read decrypted object into memory: {}", err); ApiError::from(StorageError::other(format!("Failed to read decrypted object: {err}"))) } })?; - Ok(GetObjectBodyAdapterOutput { - body: materialized.body, - body_plan: materialized.plan, - cache_writeback: materialized.cache_writeback.map(|writeback| { - object_io_finalize_get_object_cache_writeback( - info, - writeback, - filter_object_metadata(&info.user_defined).unwrap_or_default(), - ) - }), - }) + Ok(materialized.body) } -pub(super) fn finalize_get_object_completion( - cache_key: &str, - wrapper: &RequestTimeoutWrapper, - timeout_config: &TimeoutConfig, - total_duration: Duration, - response_content_length: i64, - optimal_buffer_size: usize, - metric_contract: ObjectIoGetObjectDataPlaneMetricContract, -) { +pub(super) struct GetObjectCompletionInputs<'a> { + pub(super) bucket: &'a str, + pub(super) key: &'a str, + pub(super) wrapper: &'a RequestTimeoutWrapper, + pub(super) timeout_config: &'a TimeoutConfig, + pub(super) total_duration: Duration, + pub(super) response_content_length: i64, + pub(super) optimal_buffer_size: usize, + pub(super) metric_contract: ObjectIoGetObjectDataPlaneMetricContract, +} + +pub(super) struct GetObjectStrategyRuntimeInputs<'a> { + pub(super) base_buffer_size: usize, + pub(super) manager: &'a ConcurrencyManager, + pub(super) bucket: &'a str, + pub(super) key: &'a str, + pub(super) info: &'a ObjectInfo, + pub(super) rs: Option<&'a HTTPRangeSpec>, + pub(super) response_content_length: i64, + pub(super) permit_wait_duration: Duration, + pub(super) queue_utilization: f64, + pub(super) queue_status: &'a concurrency::IoQueueStatus, +} + +pub(super) fn finalize_get_object_completion(inputs: GetObjectCompletionInputs<'_>) { + let GetObjectCompletionInputs { + bucket, + key, + wrapper, + timeout_config, + total_duration, + response_content_length, + optimal_buffer_size, + metric_contract, + } = inputs; + rustfs_io_metrics::record_get_object_completion(total_duration.as_secs_f64(), response_content_length, optimal_buffer_size); - rustfs_io_metrics::record_get_object(total_duration.as_millis() as f64, response_content_length, false); + rustfs_io_metrics::record_get_object(total_duration.as_millis() as f64, response_content_length); rustfs_io_metrics::record_io_copy_mode("get", metric_contract.copy_mode, response_content_length.max(0) as usize); if wrapper.is_timeout() { warn!( - "GetObject request exceeded timeout: key={} duration={:?} timeout={:?}", - cache_key, - wrapper.elapsed(), - timeout_config.get_object_timeout + bucket = %bucket, + key = %key, + elapsed = ?wrapper.elapsed(), + timeout = ?timeout_config.get_object_timeout, + "GetObject request exceeded timeout" ); rustfs_io_metrics::record_get_object_timeout(None, Some(wrapper.elapsed().as_secs_f64())); } debug!( - "GetObject completed: key={} size={} duration={:?} buffer={}", - cache_key, response_content_length, total_duration, optimal_buffer_size + bucket = %bucket, + key = %key, + size = response_content_length, + duration = ?total_duration, + buffer = optimal_buffer_size, + "GetObject completed" ); } -#[allow(clippy::too_many_arguments)] -pub(super) fn finalize_get_object_strategy_runtime( - base_buffer_size: usize, - manager: &ConcurrencyManager, - bucket: &str, - key: &str, - info: &ObjectInfo, - rs: Option<&HTTPRangeSpec>, - response_content_length: i64, - permit_wait_duration: Duration, - queue_utilization: f64, - queue_status: &concurrency::IoQueueStatus, - concurrent_requests: usize, -) -> (concurrency::IoStrategy, usize) { +pub(super) fn finalize_get_object_strategy_runtime(inputs: GetObjectStrategyRuntimeInputs<'_>) -> usize { + let GetObjectStrategyRuntimeInputs { + base_buffer_size, + manager, + bucket, + key, + info, + rs, + response_content_length, + permit_wait_duration, + queue_utilization, + queue_status, + } = inputs; + let strategy_layout = object_io_plan_get_object_strategy_layout( rs, response_content_length, @@ -415,7 +268,6 @@ pub(super) fn finalize_get_object_strategy_runtime( buffer_size = io_strategy.buffer_size, buffer_multiplier = io_strategy.buffer_multiplier, readahead = io_strategy.enable_readahead, - cache_wb = io_strategy.cache_writeback_enabled, storage_media = ?io_strategy.storage_media, access_pattern = ?io_strategy.access_pattern, bandwidth_tier = ?io_strategy.bandwidth_tier, @@ -447,7 +299,6 @@ pub(super) fn finalize_get_object_strategy_runtime( io_strategy.load_level.as_str(), io_strategy.buffer_multiplier, ); - rustfs_io_metrics::record_io_priority_assignment(io_priority.as_str()); let strategy_layout = object_io_plan_get_object_strategy_layout( rs, @@ -467,11 +318,11 @@ pub(super) fn finalize_get_object_strategy_runtime( response_content_length, get_buffer_size_opt_in(response_content_length), strategy_layout.optimal_buffer_size, - concurrent_requests, + io_strategy.concurrent_requests, io_strategy.load_level ); - (io_strategy, strategy_layout.optimal_buffer_size) + strategy_layout.optimal_buffer_size } pub(super) fn prepare_put_object_request_context(req: &S3Request) -> PutObjectRequestContext { @@ -525,29 +376,19 @@ pub(super) async fn complete_get_flow_result( request_context: &GetObjectRequestContext, flow_result: GetObjectFlowResult, ) -> S3Result> { - match flow_result.response_mode { - GetObjectResponseMode::Plain => { - let helper = bind_helper_object(helper, flow_result.event_info, Some(flow_result.version_id_for_event)); - let result = Ok(S3Response::new(flow_result.output)); - let _ = helper.complete(&result); - result - } - GetObjectResponseMode::CorsWrapped => { - let helper = helper - .object(flow_result.event_info) - .version_id(flow_result.version_id_for_event); - let response = wrap_response_with_cors( - &request_context.bucket, - &request_context.method, - &request_context.headers, - flow_result.output, - ) - .await; - let result = Ok(response); - let _ = helper.complete(&result); - result - } - } + let helper = helper + .object(flow_result.event_info) + .version_id(flow_result.version_id_for_event); + let response = wrap_response_with_cors( + &request_context.bucket, + &request_context.method, + &request_context.headers, + flow_result.output, + ) + .await; + let result = Ok(response); + let _ = helper.complete(&result); + result } pub(super) fn complete_put_response(helper: OperationHelper, output: PutObjectOutput) -> S3Result> { diff --git a/rustfs/src/app/object_usecase/get_object_flow.rs b/rustfs/src/app/object_usecase/get_object_flow.rs index 92934e4fa..627425895 100644 --- a/rustfs/src/app/object_usecase/get_object_flow.rs +++ b/rustfs/src/app/object_usecase/get_object_flow.rs @@ -14,8 +14,8 @@ use super::DeadlockRequestGuard; use super::app_adapters::{ - bucket_prefix_versioning_enabled, build_get_object_body_adapter, finalize_get_object_completion, - finalize_get_object_strategy_runtime, maybe_get_cached_get_object_flow_result, spawn_get_object_cache_writeback, + GetObjectCompletionInputs, GetObjectStrategyRuntimeInputs, bucket_prefix_versioning_enabled, build_get_object_body_adapter, + finalize_get_object_completion, finalize_get_object_strategy_runtime, }; use super::get_object_zero_copy::{GetObjectPreparedRead, prepare_get_object_read_execution}; use super::types::GetObjectRequestContext; @@ -25,7 +25,7 @@ use crate::storage::options::filter_object_metadata; use crate::storage::timeout_wrapper::{RequestTimeoutWrapper, TimeoutConfig}; use rustfs_ecstore::store_api::{HTTPRangeSpec, ObjectInfo}; use rustfs_object_io::get::{ - GetObjectBodyPlan as ObjectIoGetObjectBodyPlan, GetObjectBodySource, + GetObjectBodyPlanningInputs as ObjectIoGetObjectBodyPlanningInputs, GetObjectBodySource, GetObjectDataPlaneMetricContract as ObjectIoGetObjectDataPlaneMetricContract, GetObjectFlowResult, GetObjectOutputContext, GetObjectReadSetup, build_chunk_blob as object_io_build_chunk_blob, build_cors_wrapped_get_object_flow_result as object_io_build_cors_wrapped_get_object_flow_result, @@ -43,7 +43,6 @@ pub(super) struct GetObjectBootstrap { pub(super) request_start: std::time::Instant, pub(super) request_guard: GetObjectGuard, pub(super) _deadlock_request_guard: DeadlockRequestGuard, - pub(super) concurrent_requests: usize, } #[derive(Clone, Copy)] @@ -56,7 +55,6 @@ pub(super) struct GetObjectFlowRuntime<'a> { #[allow(clippy::too_many_arguments)] pub(super) async fn build_get_object_output_context( request_context: &GetObjectRequestContext, - cache_key: &str, manager: &ConcurrencyManager, bucket: &str, key: &str, @@ -76,53 +74,45 @@ pub(super) async fn build_get_object_output_context( permit_wait_duration: Duration, queue_utilization: f64, queue_status: &concurrency::IoQueueStatus, - concurrent_requests: usize, base_buffer_size: usize, part_number: Option, versioned: bool, ) -> S3Result<(GetObjectOutputContext, ObjectIoGetObjectDataPlaneMetricContract)> { - let (io_strategy, optimal_buffer_size) = finalize_get_object_strategy_runtime( + let optimal_buffer_size = finalize_get_object_strategy_runtime(GetObjectStrategyRuntimeInputs { base_buffer_size, manager, bucket, key, - &info, - rs.as_ref(), + info: &info, + rs: rs.as_ref(), response_content_length, permit_wait_duration, queue_utilization, queue_status, - concurrent_requests, - ); + }); let (body, metric_contract) = match body_source { GetObjectBodySource::Reader(final_stream) => { - let cache_eligibility = manager.get_object_cache_eligibility( - io_strategy.cache_writeback_enabled, - part_number.is_some(), - rs.is_some(), - encryption_applied, - response_content_length, - ); - let adapter_output = build_get_object_body_adapter( + let body = build_get_object_body_adapter( final_stream, - &info, - cache_key, + bucket, + key, response_content_length, optimal_buffer_size, - cache_eligibility, + ObjectIoGetObjectBodyPlanningInputs { + is_part_request: part_number.is_some(), + is_range_request: rs.is_some(), + encryption_applied, + response_size: response_content_length, + }, ) .await?; let metric_contract = ObjectIoGetObjectDataPlaneMetricContract::disk( rustfs_io_metrics::IoPath::Legacy, rustfs_io_metrics::CopyMode::SingleCopy, - adapter_output.body_plan, ); - if let Some(writeback) = adapter_output.cache_writeback { - spawn_get_object_cache_writeback(cache_key, writeback, metric_contract); - } - (adapter_output.body, metric_contract) + (body, metric_contract) } GetObjectBodySource::Chunk { stream: chunk_stream, @@ -132,7 +122,7 @@ pub(super) async fn build_get_object_output_context( let (io_path, copy_mode) = object_io_chunk_body_data_plane_labels(path, copy_mode); ( object_io_build_chunk_blob(chunk_stream), - ObjectIoGetObjectDataPlaneMetricContract::disk(io_path, copy_mode, ObjectIoGetObjectBodyPlan::Stream), + ObjectIoGetObjectDataPlaneMetricContract::disk(io_path, copy_mode), ) } }; @@ -176,30 +166,13 @@ pub(super) async fn run_get_object_flow( let timeout_config = &bootstrap.timeout_config; let wrapper = &bootstrap.wrapper; let request_start = bootstrap.request_start; - let concurrent_requests = bootstrap.concurrent_requests; let bucket = request_context.bucket.clone(); let key = request_context.key.clone(); - let cache_key = request_context.cache_key.clone(); let version_id_for_event = request_context.version_id_for_event.clone(); let part_number = request_context.part_number; let rs = request_context.rs.clone(); let opts = request_context.opts.clone(); - if let Some(cached_result) = maybe_get_cached_get_object_flow_result( - manager, - &bucket, - &key, - &cache_key, - version_id_for_event.clone(), - part_number, - rs.as_ref(), - request_start, - ) - .await - { - return Ok(cached_result); - } - let prepared_read = prepare_get_object_read_execution( &request_context, manager, @@ -236,7 +209,6 @@ pub(super) async fn run_get_object_flow( let versioned = bucket_prefix_versioning_enabled(&bucket, &key).await; let (output_context, metric_contract) = build_get_object_output_context( &request_context, - &cache_key, manager, &bucket, &key, @@ -256,7 +228,6 @@ pub(super) async fn run_get_object_flow( permit_wait_duration, queue_utilization, &queue_status, - concurrent_requests, base_buffer_size, part_number, versioned, @@ -266,15 +237,16 @@ pub(super) async fn run_get_object_flow( let optimal_buffer_size = output_context.optimal_buffer_size; let total_duration = request_start.elapsed(); - finalize_get_object_completion( - &cache_key, + finalize_get_object_completion(GetObjectCompletionInputs { + bucket: &bucket, + key: &key, wrapper, timeout_config, total_duration, response_content_length, optimal_buffer_size, metric_contract, - ); + }); Ok(object_io_build_cors_wrapped_get_object_flow_result(output_context, version_id_for_event)) } diff --git a/rustfs/src/app/object_usecase/put_object_extract.rs b/rustfs/src/app/object_usecase/put_object_extract.rs index 0f8071b50..8f89d3e80 100644 --- a/rustfs/src/app/object_usecase/put_object_extract.rs +++ b/rustfs/src/app/object_usecase/put_object_extract.rs @@ -312,13 +312,6 @@ impl DefaultObjectUsecase { } }; - let manager = get_concurrency_manager(); - let fpath_clone = fpath.clone(); - let bucket_clone = bucket.clone(); - crate::storage::request_context::spawn_traced(async move { - manager.invalidate_cache_versioned(&bucket_clone, &fpath_clone, None).await; - }); - let e_tag = obj_info.etag.clone().map(|etag| to_s3s_etag(&etag)); let output = PutObjectOutput { diff --git a/rustfs/src/app/object_usecase/put_object_flow.rs b/rustfs/src/app/object_usecase/put_object_flow.rs index c64d97e73..d7cf9b436 100644 --- a/rustfs/src/app/object_usecase/put_object_flow.rs +++ b/rustfs/src/app/object_usecase/put_object_flow.rs @@ -625,8 +625,6 @@ impl DefaultObjectUsecase { let raw_version = obj_info.version_id.map(|v| v.to_string()); - Self::spawn_cache_invalidation(bucket.clone(), key.clone(), raw_version.clone()); - let put_version = if bucket_prefix_versioning_enabled(&bucket, &key).await { raw_version.clone() } else { diff --git a/rustfs/src/app/object_usecase/types.rs b/rustfs/src/app/object_usecase/types.rs index 63444d8d9..a0213541d 100644 --- a/rustfs/src/app/object_usecase/types.rs +++ b/rustfs/src/app/object_usecase/types.rs @@ -18,7 +18,6 @@ use super::*; pub(super) struct GetObjectRequestContext { pub(super) bucket: String, pub(super) key: String, - pub(super) cache_key: String, pub(super) version_id_for_event: String, pub(super) part_number: Option, pub(super) rs: Option, diff --git a/rustfs/src/app/object_usecase/zero_copy_tests.rs b/rustfs/src/app/object_usecase/zero_copy_tests.rs index 59c3dba1c..5d9dc4f99 100644 --- a/rustfs/src/app/object_usecase/zero_copy_tests.rs +++ b/rustfs/src/app/object_usecase/zero_copy_tests.rs @@ -13,6 +13,7 @@ // limitations under the License. use super::*; +use crate::storage::concurrency::ConcurrencyManager; use futures::StreamExt; use http::{Extensions, HeaderMap, Method, Uri}; use rustfs_ecstore::store_api::GetObjectChunkPath; diff --git a/rustfs/src/storage/concurrency/io_schedule.rs b/rustfs/src/storage/concurrency/io_schedule.rs index dcc59673e..8d5d8fc25 100644 --- a/rustfs/src/storage/concurrency/io_schedule.rs +++ b/rustfs/src/storage/concurrency/io_schedule.rs @@ -458,8 +458,6 @@ pub struct IoStrategyCore { pub buffer_multiplier: f64, /// Whether sequential read-ahead should be enabled pub enable_readahead: bool, - /// Whether cache writeback should be enabled - pub cache_writeback_enabled: bool, /// Whether tokio BufReader should be used pub use_buffered_io: bool, @@ -491,7 +489,6 @@ pub struct IoStrategyCore { pub should_expand_for_sequential: bool, pub should_reduce_for_concurrency: bool, pub should_reduce_for_bandwidth: bool, - pub should_disable_cache_writeback: bool, pub should_disable_readahead: bool, // ===== Priority Scheduling ===== @@ -515,7 +512,6 @@ impl IoStrategyCore { buffer_size, buffer_multiplier: 1.0, enable_readahead: false, - cache_writeback_enabled: true, use_buffered_io: true, concurrent_requests: 1, observed_bandwidth_bps: None, @@ -536,7 +532,6 @@ impl IoStrategyCore { should_expand_for_sequential: false, should_reduce_for_concurrency: false, should_reduce_for_bandwidth: false, - should_disable_cache_writeback: false, should_disable_readahead: false, priority_enabled: false, priority: IoPriority::Normal, @@ -600,11 +595,6 @@ pub struct IoStrategyDebugInfo { pub readahead_disabled_by_load: bool, pub readahead_disabled_by_bandwidth: bool, - // ===== Cache Writeback Decisions ===== - pub cache_writeback_disabled_by_load: bool, - pub cache_writeback_disabled_by_pattern: bool, - pub cache_writeback_disabled_by_request_size: bool, - // ===== Threshold Snapshots ===== pub final_buffer_floor: usize, pub queue_depth_hint: usize, @@ -656,7 +646,6 @@ pub struct IoStrategyDebugInfo { /// // Apply strategy to I/O operations /// let buffer_size = strategy.buffer_size; /// let enable_readahead = strategy.enable_readahead; -/// let enable_cache_writeback = strategy.cache_writeback_enabled; /// ``` #[derive(Debug, Clone, PartialEq)] pub struct IoStrategy { @@ -718,11 +707,6 @@ impl IoStrategy { IoLoadLevel::High | IoLoadLevel::Critical => false, }; - let cache_writeback_enabled = match load_level { - IoLoadLevel::Low | IoLoadLevel::Medium | IoLoadLevel::High => true, - IoLoadLevel::Critical => false, // Disable under extreme load - }; - // Build minimal scheduling context for compatibility path let scheduling_context = IoSchedulingContext::from_wait_duration(permit_wait_duration, base_buffer_size); #[cfg(feature = "io-scheduler-debug")] @@ -740,7 +724,6 @@ impl IoStrategy { buffer_size, buffer_multiplier, enable_readahead, - cache_writeback_enabled, use_buffered_io: true, // Performance state @@ -767,7 +750,6 @@ impl IoStrategy { should_expand_for_sequential: false, should_reduce_for_concurrency: false, should_reduce_for_bandwidth: false, - should_disable_cache_writeback: !cache_writeback_enabled, should_disable_readahead: !enable_readahead, // Priority scheduling @@ -810,9 +792,6 @@ impl IoStrategy { readahead_disabled_by_pattern: false, readahead_disabled_by_load: !enable_readahead, readahead_disabled_by_bandwidth: false, - cache_writeback_disabled_by_load: !cache_writeback_enabled, - cache_writeback_disabled_by_pattern: false, - cache_writeback_disabled_by_request_size: false, final_buffer_floor: 32 * KI_B, queue_depth_hint: 0, permit_wait_ms: permit_wait_duration.as_millis() as u64, @@ -1016,17 +995,6 @@ impl IoStrategy { let enable_readahead = should_enable_readahead; - // Determine cache writeback - let cache_writeback_enabled = match load_level { - IoLoadLevel::Critical => false, - _ => !bandwidth_limited, - }; - - #[cfg(feature = "io-scheduler-debug")] - let cache_writeback_disabled_by_load = matches!(load_level, IoLoadLevel::Critical); - #[cfg(feature = "io-scheduler-debug")] - let cache_writeback_disabled_by_pattern = matches!(context.access_pattern, AccessPattern::Random); - // Calculate priority based on request size let priority = if context.file_size > 0 { IoPriority::from_size_with_thresholds( @@ -1052,7 +1020,6 @@ impl IoStrategy { buffer_size, buffer_multiplier, enable_readahead, - cache_writeback_enabled, use_buffered_io: true, // ===== Performance State ===== @@ -1074,7 +1041,6 @@ impl IoStrategy { should_expand_for_sequential: matches!(context.access_pattern, AccessPattern::Sequential), should_reduce_for_concurrency: concurrency_multiplier < 1.0, should_reduce_for_bandwidth: bandwidth_limited, - should_disable_cache_writeback: !cache_writeback_enabled, should_disable_readahead: !enable_readahead, // ===== Priority Scheduling ===== @@ -1161,11 +1127,6 @@ impl IoStrategy { readahead_disabled_by_load, readahead_disabled_by_bandwidth, - // ===== Cache Writeback Decisions ===== - cache_writeback_disabled_by_load, - cache_writeback_disabled_by_pattern, - cache_writeback_disabled_by_request_size: false, - // ===== Threshold Snapshots ===== final_buffer_floor: clamp_min, queue_depth_hint: context.concurrent_requests, @@ -1212,12 +1173,11 @@ impl IoStrategy { #[allow(dead_code)] pub fn description(&self) -> String { format!( - "IoStrategy[{:?}]: buffer={}KB, multiplier={:.2}, readahead={}, cache_wb={}, wait={:?}", + "IoStrategy[{:?}]: buffer={}KB, multiplier={:.2}, readahead={}, wait={:?}", self.load_level, self.buffer_size / 1024, self.buffer_multiplier, self.enable_readahead, - self.cache_writeback_enabled, self.permit_wait_duration ) } @@ -2151,10 +2111,9 @@ mod tests { let config = IoSchedulerConfig::default(); let strategy = IoStrategy::from_context_with_config(&context, &config); - // Critical load should disable readahead and cache writeback + // Critical load should disable readahead assert_eq!(strategy.load_level, IoLoadLevel::Critical); assert!(!strategy.enable_readahead, "Critical load should disable readahead"); - assert!(!strategy.cache_writeback_enabled, "Critical load should disable cache writeback"); // Buffer: 256KB * 0.4 (critical) * 1.35 (sequential) ≈ 138KB assert!(strategy.buffer_size < 200 * 1024, "Critical load should reduce buffer"); } diff --git a/rustfs/src/storage/concurrency/manager.rs b/rustfs/src/storage/concurrency/manager.rs index ebfd79199..0f63eb16e 100644 --- a/rustfs/src/storage/concurrency/manager.rs +++ b/rustfs/src/storage/concurrency/manager.rs @@ -18,9 +18,8 @@ use super::io_schedule::{ IoLoadLevel, IoLoadMetrics, IoPriority, IoPriorityQueue, IoPriorityQueueConfig, IoQueueStatus, IoSchedulerConfig, IoStrategy, get_advanced_buffer_size, }; -use super::object_cache::{CacheStats, CachedGetObject, TieredObjectCache, WarmupPattern}; use super::request_guard::GetObjectGuard; -use rustfs_concurrency::{GetObjectCacheEligibility, GetObjectQueueSnapshot}; +use rustfs_concurrency::GetObjectQueueSnapshot; use rustfs_config::{KI_B, MI_B}; use rustfs_io_core::BytesPool; use rustfs_io_core::io_profile::{AccessPattern, IoPatternDetector, StorageMedia, detect_storage_media}; @@ -37,12 +36,8 @@ pub(crate) static CONCURRENCY_MANAGER: LazyLock = LazyLock:: #[derive(Clone)] pub struct ConcurrencyManager { - /// Tiered object cache (L1 + L2) for frequently accessed objects - cache: Arc, /// Semaphore to limit concurrent disk reads disk_read_semaphore: Arc, - /// Whether object caching is enabled (from RUSTFS_OBJECT_CACHE_ENABLE env var) - cache_enabled: bool, /// I/O load metrics for adaptive strategy calculation io_metrics: Arc>, /// I/O priority queue for request scheduling @@ -94,36 +89,17 @@ impl ConcurrencyManager { /// Create a new concurrency manager with default settings /// /// Reads configuration from environment variables: - /// - `RUSTFS_OBJECT_CACHE_ENABLE`: Enable/disable object caching (default: true) - /// - `RUSTFS_OBJECT_TIERED_CACHE_ENABLE`: Enable tiered L1+L2 caching (default: true) /// - `RUSTFS_OBJECT_MAX_CONCURRENT_DISK_READS`: Maximum concurrent disk reads (default: 64) pub fn new() -> Self { // Load scheduler configuration once at initialization let scheduler_config = IoSchedulerConfig::from_env(); - let cache_enabled = - rustfs_utils::get_env_bool(rustfs_config::ENV_OBJECT_CACHE_ENABLE, rustfs_config::DEFAULT_OBJECT_CACHE_ENABLE); - - let tiered_cache_enabled = rustfs_utils::get_env_bool( - rustfs_config::ENV_OBJECT_TIERED_CACHE_ENABLE, - rustfs_config::DEFAULT_OBJECT_TIERED_CACHE_ENABLE, - ); - let max_disk_reads = scheduler_config.max_concurrent_reads; // Detect storage media let storage_media = detect_storage_media(scheduler_config.storage_detection_enabled, &scheduler_config.storage_media_override); - // Create tiered cache configuration - let cache = if tiered_cache_enabled { - Arc::new(TieredObjectCache::new()) - } else { - // If tiered cache is disabled, create a simple tiered cache (acts as single-level) - // For now, we always use TieredObjectCache since the configuration is now enabled by default - Arc::new(TieredObjectCache::new()) - }; - // Initialize I/O pattern detector let pattern_detector = Arc::new(Mutex::new(IoPatternDetector::new( scheduler_config.pattern_history_size, @@ -155,9 +131,7 @@ impl ConcurrencyManager { }; Self { - cache, disk_read_semaphore: Arc::new(Semaphore::new(max_disk_reads)), - cache_enabled, io_metrics: Arc::new(Mutex::new(IoLoadMetrics::new(scheduler_config.load_sample_window))), priority_queue: Arc::new(IoPriorityQueue::new(queue_config)), bytes_pool: Arc::new(BytesPool::new_tiered()), @@ -169,36 +143,11 @@ impl ConcurrencyManager { } } - /// Check if object caching is enabled - /// - /// Returns true if the `RUSTFS_OBJECT_CACHE_ENABLE` environment variable - /// is set to "true" (case-insensitive). When disabled, cache lookups and - /// writebacks are skipped, reducing memory usage at the cost of repeated - /// disk reads for the same objects. - /// - /// # Returns - /// - /// `true` if caching is enabled, `false` otherwise - pub fn is_cache_enabled(&self) -> bool { - self.cache_enabled - } - /// Track a GetObject request pub fn track_request() -> GetObjectGuard { GetObjectGuard::new() } - /// Try to get an object from cache - pub async fn get_cached(&self, key: &str) -> Option>> { - self.cache.get_bytes(key).await - } - - /// Cache an object for future retrievals - pub async fn cache_object(&self, key: String, data: Vec) { - let cached_data = Arc::new(data); - self.cache.put_bytes(key, cached_data).await; - } - /// Get the bytes pool for buffer allocation /// /// Returns a reference to the BytesPool which can be used to acquire @@ -531,105 +480,6 @@ impl ConcurrencyManager { &self.scheduler_config } - /// Get cache statistics - pub async fn cache_stats(&self) -> CacheStats { - self.cache.stats_as_hot_cache().await - } - - /// Clear all cached objects - pub async fn clear_cache(&self) { - self.cache.clear().await; - } - - /// Reset cache hit/miss metrics counters. - /// - /// This is useful for testing to get a clean slate for hit rate calculations. - pub fn reset_cache_metrics(&self) { - self.cache.reset_metrics(); - } - - /// Check if a key is cached - pub async fn is_cached(&self, key: &str) -> bool { - self.cache.contains(key).await - } - - /// Get multiple cached objects in a single operation - pub async fn get_cached_batch(&self, keys: &[String]) -> Vec>>> { - self.cache.get_batch_bytes(keys).await - } - - /// Remove a specific object from cache - pub async fn remove_cached(&self, key: &str) -> bool { - self.cache.remove(key).await.is_some() - } - - /// Get the most frequently accessed keys - pub async fn get_hot_keys(&self, limit: usize) -> Vec<(String, u64)> { - let keys = self.cache.get_hot_keys(limit).await; - keys.into_iter().map(|(k, v)| (k, v as u64)).collect() - } - - /// Get cache hit rate percentage - pub fn cache_hit_rate(&self) -> f64 { - self.cache.hit_rate() - } - - /// Warm up cache with frequently accessed objects - /// - /// This can be called during server startup or maintenance windows - /// to pre-populate the cache with known hot objects. - pub async fn warm_cache(&self, objects: Vec<(String, Vec)>) { - if !self.cache_enabled { - debug!("Cache is disabled, skipping warmup"); - return; - } - - // Cache each object - for (key, data) in objects { - self.cache_object(key, data).await; - } - } - - /// Warm up cache with a specific pattern. - /// - /// This method supports different warming patterns for more intelligent - /// cache pre-population during server startup or maintenance windows. - /// - /// # Arguments - /// - /// * `pattern` - The warming pattern to use - /// - /// # Returns - /// - /// The number of objects successfully warmed - /// - /// # Example - /// - /// ```ignore - /// // Warm the 100 most recently accessed objects - /// let pattern = WarmupPattern::RecentAccesses { limit: 100 }; - /// let warmed = manager.warm_cache_with_pattern(pattern).await; - /// - /// // Warm specific keys - /// let keys = vec!["bucket1/key1".to_string(), "bucket1/key2".to_string()]; - /// let pattern = WarmupPattern::SpecificKeys(keys); - /// manager.warm_cache_with_pattern(pattern).await; - /// ``` - pub async fn warm_cache_with_pattern(&self, pattern: WarmupPattern) -> usize { - if !self.cache_enabled { - debug!("Cache is disabled, skipping warmup"); - return 0; - } - - debug!("warm_cache_with_pattern called with pattern: {:?}", pattern); - - // Delegate to the tiered cache's warm implementation - // Note: This returns the count of keys identified for warming, - // but actual object loading from storage would need to be implemented - // at a higher layer (object_usecase) that has access to storage backends - self.cache.warm(pattern).await - } - /// Get optimized buffer size for a request /// /// This wraps the advanced buffer sizing logic and makes it accessible @@ -638,151 +488,6 @@ impl ConcurrencyManager { get_advanced_buffer_size(file_size, base, sequential) } - // ============================================ - // Response Cache Methods (CachedGetObject) - // ============================================ - - /// Get a cached GetObject response with full metadata - /// - /// This method retrieves a complete GetObject response from the response cache, - /// including body data and all response metadata (e_tag, last_modified, content_type, etc.). - /// - /// # Arguments - /// - /// * `key` - Cache key in the format "{bucket}/{key}" or "{bucket}/{key}?versionId={version_id}" - /// - /// # Returns - /// - /// * `Some(Arc)` - Cached response data if found and not expired - /// * `None` - Cache miss - /// - /// # Example - /// - /// ```ignore - /// let cache_key = format!("{}/{}", bucket, key); - /// if let Some(cached) = manager.get_cached_object(&cache_key).await { - /// // Build response from cached data - /// let output = GetObjectOutput { - /// body: Some(StreamingBlob::from(cached.body.clone())), - /// content_length: Some(cached.content_length), - /// e_tag: cached.e_tag.clone(), - /// last_modified: cached.last_modified.as_ref().map(|s| parse_rfc3339(s)), - /// ..Default::default() - /// }; - /// } - /// ``` - pub async fn get_cached_object(&self, key: &str) -> Option> { - self.cache.get_response(key).await - } - - /// Cache a complete GetObject response for future retrievals - /// - /// This method caches a complete GetObject response including body and all metadata. - /// Objects larger than the maximum cache size (10MB by default) or empty objects - /// are not cached. - /// - /// # Arguments - /// - /// * `key` - Cache key in the format "{bucket}/{key}" or "{bucket}/{key}?versionId={version_id}" - /// * `response` - The complete cached response to store - /// - /// # Example - /// - /// ```ignore - /// let cached = CachedGetObject { - /// body: Bytes::from(data), - /// content_length: data.len() as i64, - /// content_type: Some("application/octet-stream".to_string()), - /// e_tag: Some("\"abc123\"".to_string()), - /// last_modified: Some("2024-01-01T00:00:00Z".to_string()), - /// ..Default::default() - /// }; - /// manager.put_cached_object(cache_key, cached).await; - /// ``` - pub async fn put_cached_object(&self, key: String, response: CachedGetObject) { - self.cache.put_response(key, response).await; - } - - /// Invalidate cache entries for a specific object - /// - /// This method removes both simple byte cache and response cache entries - /// for the given key. Should be called after write operations (put_object, - /// copy_object, delete_object, etc.) to prevent stale data from being served. - /// - /// # Arguments - /// - /// * `key` - Cache key to invalidate (e.g., "{bucket}/{key}") - /// - /// # Example - /// - /// ```ignore - /// // After put_object succeeds - /// let cache_key = format!("{}/{}", bucket, key); - /// manager.invalidate_cache(&cache_key).await; - /// ``` - pub async fn invalidate_cache(&self, key: &str) { - self.cache.invalidate(key).await; - } - - /// Invalidate cache entries for an object and its latest version - /// - /// For versioned buckets, this invalidates both: - /// - The specific version key: "{bucket}/{key}?versionId={version_id}" - /// - The latest version key: "{bucket}/{key}" - /// - /// This ensures that after a write/delete, clients don't receive stale data. - /// Should be called after any write operation that modifies object data or creates - /// new versions. - /// - /// # Arguments - /// - /// * `bucket` - Bucket name - /// * `key` - Object key - /// * `version_id` - Optional version ID (if None, only invalidates the base key) - /// - /// # Example - /// - /// ```ignore - /// // After delete_object with version - /// manager.invalidate_cache_versioned(&bucket, &key, Some(&version_id)).await; - /// - /// // After put_object (invalidates latest) - /// manager.invalidate_cache_versioned(&bucket, &key, None).await; - /// ``` - pub async fn invalidate_cache_versioned(&self, bucket: &str, key: &str, version_id: Option<&str>) { - self.cache.invalidate_versioned(bucket, key, version_id).await; - } - - /// Generate a cache key for an object - /// - /// Creates a cache key in the appropriate format based on whether a version ID - /// is specified. For versioned requests, uses "{bucket}/{key}?versionId={version_id}". - /// For non-versioned requests, uses "{bucket}/{key}". - /// - /// # Arguments - /// - /// * `bucket` - Bucket name - /// * `key` - Object key - /// * `version_id` - Optional version ID - /// - /// # Returns - /// - /// Cache key string - pub fn make_cache_key(bucket: &str, key: &str, version_id: Option<&str>) -> String { - match version_id { - Some(vid) => format!("{bucket}/{key}?versionId={vid}"), - None => format!("{bucket}/{key}"), - } - } - - /// Get maximum cacheable object size - /// - /// Returns the maximum size in bytes for objects that can be cached. - /// Objects larger than this size are not cached to prevent memory exhaustion. - pub fn max_object_size(&self) -> usize { - self.cache.max_object_size() - } - // ============================================ // Priority-Based I/O Scheduling Methods // ============================================ @@ -868,26 +573,6 @@ impl ConcurrencyManager { self.disk_read_semaphore.acquire().await } - /// Build the minimal cache eligibility decision for a GetObject response. - pub fn get_object_cache_eligibility( - &self, - cache_writeback_enabled: bool, - is_part_request: bool, - is_range_request: bool, - encryption_applied: bool, - response_size: i64, - ) -> GetObjectCacheEligibility { - GetObjectCacheEligibility { - cache_enabled: self.is_cache_enabled(), - cache_writeback_enabled, - is_part_request, - is_range_request, - encryption_applied, - response_size, - max_cacheable_size: self.max_object_size(), - } - } - /// Get the global concurrency manager instance. pub fn global() -> &'static Self { &CONCURRENCY_MANAGER @@ -907,7 +592,6 @@ impl Default for ConcurrencyManager { #[cfg(test)] mod integration_tests { use super::*; - use bytes::Bytes; use serial_test::serial; #[tokio::test] @@ -926,43 +610,6 @@ mod integration_tests { assert_eq!(large_priority, IoPriority::Low); } - #[tokio::test] - #[serial] - async fn test_concurrency_manager_cache_operations() { - let manager = ConcurrencyManager::new(); - - // Test cache put and get - let obj = CachedGetObject::new(Bytes::from("test data"), 9) - .with_content_type("text/plain".to_string()) - .with_e_tag("\"abc123\"".to_string()); - - manager.put_cached_object("test-key".to_string(), obj).await; - - let cached = manager.get_cached_object("test-key").await; - assert!(cached.is_some()); - - let cached_obj = cached.unwrap(); - assert_eq!(cached_obj.content_type, Some("text/plain".to_string())); - assert_eq!(cached_obj.e_tag, Some("\"abc123\"".to_string())); - } - - #[tokio::test] - #[serial] - async fn test_concurrency_manager_cache_stats() { - let manager = ConcurrencyManager::new(); - - // Add some objects - for i in 0..5 { - let obj = CachedGetObject::new(Bytes::from(format!("data{}", i)), 5); - manager.put_cached_object(format!("key{}", i), obj).await; - } - - // Get stats - let stats = manager.cache_stats().await; - - assert!(stats.entries >= 5); - } - #[tokio::test] #[serial] async fn test_concurrency_manager_io_queue_status() { @@ -1008,44 +655,6 @@ mod integration_tests { assert_eq!(manager.get_io_priority(50 * 1024 * 1024), IoPriority::Low); // 50MB } - #[tokio::test] - #[serial] - async fn test_concurrency_manager_cache_invalidation() { - let manager = ConcurrencyManager::new(); - - // Add an object - let obj = CachedGetObject::new(Bytes::from("test"), 4); - manager.put_cached_object("test-key".to_string(), obj).await; - - // Verify it's cached - assert!(manager.is_cached("test-key").await); - - // Invalidate - manager.invalidate_cache("test-key").await; - - // Should not be cached anymore - assert!(!manager.is_cached("test-key").await); - } - - #[tokio::test] - #[serial] - async fn test_concurrency_manager_cache_clear() { - let manager = ConcurrencyManager::new(); - - // Add multiple objects - for i in 0..10 { - let obj = CachedGetObject::new(Bytes::from(format!("data{}", i)), 5); - manager.put_cached_object(format!("key{}", i), obj).await; - } - - // Clear cache - manager.clear_cache().await; - - // Verify all are removed - let stats = manager.cache_stats().await; - assert_eq!(stats.entries, 0); - } - #[tokio::test] #[serial] async fn test_concurrency_manager_io_strategy() { diff --git a/rustfs/src/storage/concurrency/mod.rs b/rustfs/src/storage/concurrency/mod.rs index f142808c0..cae7245e1 100644 --- a/rustfs/src/storage/concurrency/mod.rs +++ b/rustfs/src/storage/concurrency/mod.rs @@ -14,14 +14,13 @@ //! Concurrency optimization module for high-performance object retrieval. //! -//! This module provides concurrency management, I/O scheduling, and object caching +//! This module provides concurrency management and I/O scheduling //! for high-performance object retrieval operations. //! //! # Architecture //! //! The module is organized into several components: //! - **I/O Scheduling**: Adaptive buffer sizing and load management -//! - **Object Caching**: Tiered L1/L2 cache for frequently accessed objects //! - **Concurrency Management**: Coordination of concurrent GetObject requests //! - **Request Tracking**: RAII guards for request lifecycle management //! @@ -37,7 +36,6 @@ // pub mod io_profile; // Migrated to rustfs-io-core pub mod io_schedule; pub mod manager; -pub mod object_cache; pub mod request_guard; // ============================================ @@ -54,10 +52,6 @@ pub use io_schedule::{ // Request tracking pub use request_guard::GetObjectGuard; -// Cache types -#[allow(unused_imports)] -pub use object_cache::{CacheHealthStatus, CacheStats, CachedGetObject}; - // Concurrency manager pub use manager::ConcurrencyManager; diff --git a/rustfs/src/storage/concurrency/object_cache.rs b/rustfs/src/storage/concurrency/object_cache.rs deleted file mode 100644 index 676773f72..000000000 --- a/rustfs/src/storage/concurrency/object_cache.rs +++ /dev/null @@ -1,2201 +0,0 @@ -// 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. - -//! Object cache module for hot object caching with Moka. -//! -//! # Migration Note -//! -//! This module provides a complete tiered cache implementation. For configuration -//! and metrics types, consider using `rustfs_io_metrics`: -//! -//! ```ignore -//! // Configuration types from io-metrics -//! use rustfs_io_metrics::{CacheConfig, AdaptiveTTL, CacheStats}; -//! -//! // Access tracking from io-metrics -//! use rustfs_io_metrics::{AccessTracker, AccessRecord}; -//! ``` -//! -//! This module remains for the full `TieredObjectCache` implementation. - -use hashbrown::HashMap; -use moka::future::Cache; -use rustfs_config::MI_B; -use rustfs_object_io::get::GetObjectCacheWriteback; -use std::sync::Arc; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::time::{Duration, Instant}; -use tokio::sync::RwLock; - -/// Type alias for the complex tracking type to reduce complexity warning -type TrackingData = Arc, Instant)>>>; - -/// Access tracker for adaptive TTL and tiered cache management. -/// -/// Tracks access counts and last access times for cached objects to enable: -/// - Adaptive TTL extension for hot objects -/// - L1/L2 cache promotion/demotion decisions -/// - Cache prewarming with hot key detection -/// -/// Uses hashbrown for efficient storage and RwLock for concurrent access. -#[derive(Clone)] -pub struct AccessTracker { - /// Access counts and last access for each cache key - #[allow(clippy::type_complexity)] - tracking: TrackingData, -} - -impl AccessTracker { - /// Create a new access tracker. - pub fn new() -> Self { - Self { - tracking: Arc::new(RwLock::new(HashMap::new())), - } - } - - /// Record an access to the given key. - pub async fn record_access(&self, key: &str) { - let mut tracking = self.tracking.write().await; - let key_owned = key.to_string(); - let now = Instant::now(); - - if let Some((count, _)) = tracking.get_mut(&key_owned) { - count.fetch_add(1, Ordering::Relaxed); - // Update last access time - *tracking.get_mut(&key_owned).unwrap() = (count.clone(), now); - } else { - tracking.insert(key_owned, (Arc::new(AtomicU64::new(1)), now)); - } - } - - /// Get the access count for a key. - pub async fn get_hit_count(&self, key: &str) -> u64 { - let tracking = self.tracking.read().await; - tracking.get(key).map(|(count, _)| count.load(Ordering::Relaxed)).unwrap_or(0) - } - - /// Get the last access time for a key. - #[allow(dead_code)] - pub async fn get_last_access(&self, key: &str) -> Option { - let tracking = self.tracking.read().await; - tracking.get(key).map(|(_, time)| *time) - } - - /// Check if a key is considered hot based on hit threshold. - pub async fn is_hot(&self, key: &str, threshold: usize) -> bool { - self.get_hit_count(key).await >= threshold as u64 - } - - /// Get the time since last access for a key. - #[allow(dead_code)] - pub async fn time_since_access(&self, key: &str) -> Option { - self.get_last_access(key).await.map(|instant| instant.elapsed()) - } - - /// Remove tracking for a key (called on cache eviction). - pub async fn remove(&self, key: &str) { - let mut tracking = self.tracking.write().await; - tracking.remove(key); - } - - /// Clear all tracking data. - #[allow(dead_code)] - pub async fn clear(&self) { - let mut tracking = self.tracking.write().await; - tracking.clear(); - } - - /// Get hot keys sorted by hit count. - /// - /// Returns up to `limit` keys with highest access counts. - pub async fn get_hot_keys(&self, limit: usize) -> Vec<(String, u64)> { - let tracking: tokio::sync::RwLockReadGuard<'_, HashMap, Instant)>> = self.tracking.read().await; - let mut entries: Vec<(String, u64)> = tracking - .iter() - .map(|(key, value): (&String, &(Arc, Instant))| (key.clone(), value.0.load(Ordering::Relaxed))) - .collect(); - - entries.sort_by(|a, b| b.1.cmp(&a.1)); - entries.truncate(limit); - entries - } - - /// Get tracking statistics. - #[allow(dead_code)] - pub async fn stats(&self) -> AccessTrackerStats { - let tracking: tokio::sync::RwLockReadGuard<'_, HashMap, Instant)>> = self.tracking.read().await; - let total_keys = tracking.len(); - let total_hits: u64 = tracking - .values() - .map(|v: &(Arc, Instant)| v.0.load(Ordering::Relaxed)) - .sum(); - - AccessTrackerStats { - total_keys, - total_hits, - avg_hits_per_key: if total_keys > 0 { - total_hits as f64 / total_keys as f64 - } else { - 0.0 - }, - } - } -} - -impl Default for AccessTracker { - fn default() -> Self { - Self::new() - } -} - -/// Access tracker statistics. -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub struct AccessTrackerStats { - /// Total number of tracked keys - pub total_keys: usize, - /// Total number of accesses across all keys - pub total_hits: u64, - /// Average hits per key - pub avg_hits_per_key: f64, -} - -// ============================================================================= -// Tiered Object Cache -// ============================================================================= - -/// Tiered cache configuration for L1/L2 caching. -#[derive(Debug, Clone)] -pub struct TieredCacheConfig { - /// L1 cache: hot small objects (<1MB) - pub l1_max_size: usize, - pub l1_max_objects: usize, - pub l1_ttl_secs: u64, - pub l1_tti_secs: u64, - pub l1_max_object_size: usize, - - /// L2 cache: standard objects (<10MB) - pub l2_max_size: usize, - pub l2_max_objects: usize, - pub l2_ttl_secs: u64, - pub l2_tti_secs: u64, - pub l2_max_object_size: usize, - - /// Adaptive TTL configuration - pub adaptive_ttl_enabled: bool, - pub hot_hit_threshold: usize, - pub ttl_extension_factor: f64, -} - -impl Default for TieredCacheConfig { - fn default() -> Self { - Self { - l1_max_size: rustfs_config::DEFAULT_OBJECT_L1_CACHE_MAX_SIZE_MB as usize * MI_B, - l1_max_objects: rustfs_config::DEFAULT_OBJECT_L1_CACHE_MAX_OBJECTS, - l1_ttl_secs: rustfs_config::DEFAULT_OBJECT_L1_CACHE_TTL_SECS, - l1_tti_secs: rustfs_config::DEFAULT_OBJECT_L1_CACHE_TTI_SECS, - l1_max_object_size: rustfs_config::DEFAULT_OBJECT_L1_MAX_OBJECT_SIZE_MB * MI_B, - - l2_max_size: rustfs_config::DEFAULT_OBJECT_L2_CACHE_MAX_SIZE_MB as usize * MI_B, - l2_max_objects: rustfs_config::DEFAULT_OBJECT_L2_CACHE_MAX_OBJECTS, - l2_ttl_secs: rustfs_config::DEFAULT_OBJECT_L2_CACHE_TTL_SECS, - l2_tti_secs: rustfs_config::DEFAULT_OBJECT_L2_CACHE_TTI_SECS, - l2_max_object_size: rustfs_config::DEFAULT_OBJECT_CACHE_MAX_OBJECT_SIZE_MB * MI_B, - - adaptive_ttl_enabled: rustfs_config::DEFAULT_OBJECT_ADAPTIVE_TTL_ENABLE, - hot_hit_threshold: rustfs_config::DEFAULT_OBJECT_HOT_HIT_THRESHOLD, - ttl_extension_factor: rustfs_config::DEFAULT_OBJECT_TTL_EXTENSION_FACTOR, - } - } -} - -/// Tiered object cache with L1 (hot) and L2 (standard) levels. -/// -/// L1 cache stores hot small objects (<1MB) with short TTL for rapid access. -/// L2 cache stores standard objects (<10MB) with longer TTL. -/// Objects are promoted from L2 to L1 when frequently accessed. -pub struct TieredObjectCache { - /// L1 cache for hot small objects - l1_cache: Cache>, - /// L2 cache for standard objects - l2_cache: Cache>, - /// Configuration - config: TieredCacheConfig, - /// Access tracker for adaptive TTL - access_tracker: Arc, - /// L1 max size in bytes - l1_max_size: usize, - /// L2 max size in bytes - l2_max_size: usize, - /// Global hit counters - l1_hits: Arc, - l2_hits: Arc, - misses: Arc, -} - -impl TieredObjectCache { - /// Create a new tiered object cache. - #[allow(dead_code)] - pub fn new() -> Self { - let config = TieredCacheConfig::default(); - - let l1_cache = Cache::builder() - .max_capacity(config.l1_max_size as u64) - .weigher(|_key: &String, value: &Arc| -> u32 { value.size.min(u32::MAX as usize) as u32 }) - .time_to_live(Duration::from_secs(config.l1_ttl_secs)) - .time_to_idle(Duration::from_secs(config.l1_tti_secs)) - .build(); - - let l2_cache = Cache::builder() - .max_capacity(config.l2_max_size as u64) - .weigher(|_key: &String, value: &Arc| -> u32 { value.size.min(u32::MAX as usize) as u32 }) - .time_to_live(Duration::from_secs(config.l2_ttl_secs)) - .time_to_idle(Duration::from_secs(config.l2_tti_secs)) - .build(); - - Self { - l1_cache, - l2_cache, - l1_max_size: config.l1_max_size, - l2_max_size: config.l2_max_size, - config, - access_tracker: Arc::new(AccessTracker::new()), - l1_hits: Arc::new(AtomicU64::new(0)), - l2_hits: Arc::new(AtomicU64::new(0)), - misses: Arc::new(AtomicU64::new(0)), - } - } - - /// Create a new tiered cache with custom configuration. - #[allow(dead_code)] - pub fn with_config(config: TieredCacheConfig) -> Self { - let l1_cache = Cache::builder() - .max_capacity(config.l1_max_size as u64) - .weigher(|_key: &String, value: &Arc| -> u32 { value.size.min(u32::MAX as usize) as u32 }) - .time_to_live(Duration::from_secs(config.l1_ttl_secs)) - .time_to_idle(Duration::from_secs(config.l1_tti_secs)) - .build(); - - let l2_cache = Cache::builder() - .max_capacity(config.l2_max_size as u64) - .weigher(|_key: &String, value: &Arc| -> u32 { value.size.min(u32::MAX as usize) as u32 }) - .time_to_live(Duration::from_secs(config.l2_ttl_secs)) - .time_to_idle(Duration::from_secs(config.l2_tti_secs)) - .build(); - - Self { - l1_cache, - l2_cache, - l1_max_size: config.l1_max_size, - l2_max_size: config.l2_max_size, - config, - access_tracker: Arc::new(AccessTracker::new()), - l1_hits: Arc::new(AtomicU64::new(0)), - l2_hits: Arc::new(AtomicU64::new(0)), - misses: Arc::new(AtomicU64::new(0)), - } - } - - /// Get an object from the tiered cache. - /// - /// Checks L1 first, then L2. Promotes L2 hits to L1 if appropriate. - pub async fn get(&self, key: &str) -> Option> { - // Record access - self.access_tracker.record_access(key).await; - - // Check L1 first - if let Some(cached) = self.l1_cache.get(key).await { - self.l1_hits.fetch_add(1, Ordering::Relaxed); - rustfs_io_metrics::record_tiered_cache_operation("l1", "hit", None); - - return Some(Arc::clone(&cached.data)); - } - - // Check L2 - if let Some(cached) = self.l2_cache.get(key).await { - self.l2_hits.fetch_add(1, Ordering::Relaxed); - rustfs_io_metrics::record_tiered_cache_operation("l2", "hit", None); - - // Promote to L1 if appropriate - if self.should_promote_to_l1(&cached).await { - let _ = self.l1_cache.insert(key.to_string(), cached.clone()).await; - } - - return Some(Arc::clone(&cached.data)); - } - - // Cache miss - self.misses.fetch_add(1, Ordering::Relaxed); - rustfs_io_metrics::record_tiered_cache_operation("overall", "miss", None); - - None - } - - /// Put an object into the appropriate cache level. - pub async fn put(&self, key: String, response: CachedGetObject) { - let size = response.size(); - - // Don't cache empty or oversized objects - if size == 0 || size > self.config.l2_max_object_size { - return; - } - - let cached_internal = Arc::new(CachedGetObjectInternal { - data: Arc::new(response), - cached_at: Instant::now(), - size, - }); - - // Decide which cache level to use - if size <= self.config.l1_max_object_size { - // Put in L1 - let _ = self.l1_cache.insert(key, cached_internal).await; - } else { - // Put in L2 - let _ = self.l2_cache.insert(key, cached_internal).await; - } - } - - /// Check if an object should be promoted to L1. - async fn should_promote_to_l1(&self, cached: &Arc) -> bool { - let size = cached.size; - - // Only promote if it fits in L1 - if size > self.config.l1_max_object_size { - return false; - } - - // Check if it's hot (frequently accessed) - if !self.config.adaptive_ttl_enabled { - return false; - } - - // Check access count via the access tracker - // Note: We'd need to map from internal to key here - // For simplicity, we'll use a simple heuristic - let age = cached.cached_at.elapsed(); - age < Duration::from_secs(60) // Recently cached - } - - /// Calculate adaptive TTL for a cache entry based on access patterns. - /// - /// Uses the access tracker to determine if an object is "hot" (frequently accessed). - /// Hot objects get extended TTL to reduce cache misses. - #[allow(dead_code)] - pub async fn calculate_adaptive_ttl(&self, key: &str, base_ttl: u64) -> Duration { - if !self.config.adaptive_ttl_enabled { - return Duration::from_secs(base_ttl); - } - - // Get hit count from access tracker - let hit_count = self.access_tracker.get_hit_count(key).await; - - if hit_count >= self.config.hot_hit_threshold as u64 { - // Hot object: extend TTL - let extension = (base_ttl as f64 * self.config.ttl_extension_factor) as u64; - Duration::from_secs(base_ttl.saturating_add(extension)) - } else { - // Normal object: use base TTL - Duration::from_secs(base_ttl) - } - } - - /// Check if an object is considered hot based on access patterns. - /// - /// Returns true if the object has been accessed at least the hot threshold number of times. - #[allow(dead_code)] - pub async fn is_hot_object(&self, key: &str) -> bool { - self.access_tracker.is_hot(key, self.config.hot_hit_threshold).await - } - - /// Invalidate a cache entry from both levels. - pub async fn invalidate(&self, key: &str) { - self.l1_cache.invalidate(key).await; - self.l2_cache.invalidate(key).await; - // Also remove from access tracker - self.access_tracker.remove(key).await; - } - - /// Get cache statistics. - pub async fn stats(&self) -> TieredCacheStats { - self.l1_cache.run_pending_tasks().await; - self.l2_cache.run_pending_tasks().await; - - let l1_hits = self.l1_hits.load(Ordering::Relaxed); - let l2_hits = self.l2_hits.load(Ordering::Relaxed); - let misses = self.misses.load(Ordering::Relaxed); - let total_hits = l1_hits + l2_hits; - let total_requests = total_hits + misses; - - let hit_rate = if total_requests > 0 { - total_hits as f64 / total_requests as f64 - } else { - 0.0 - }; - - let l1_hit_rate = if total_hits > 0 { - l1_hits as f64 / total_hits as f64 - } else { - 0.0 - }; - - TieredCacheStats { - l1_size: self.l1_cache.weighted_size() as usize, - l1_entries: self.l1_cache.entry_count() as usize, - l1_max_size: self.l1_max_size, - l2_size: self.l2_cache.weighted_size() as usize, - l2_entries: self.l2_cache.entry_count() as usize, - l2_max_size: self.l2_max_size, - l1_hits, - l2_hits, - misses, - hit_rate, - l1_hit_rate, - } - } - - /// Clear all cached entries. - pub async fn clear(&self) { - self.l1_cache.invalidate_all(); - self.l2_cache.invalidate_all(); - self.l1_cache.run_pending_tasks().await; - self.l2_cache.run_pending_tasks().await; - } - - /// Reset hit/miss metrics counters. - /// - /// This is useful for testing to get a clean slate for hit rate calculations. - pub fn reset_metrics(&self) { - self.l1_hits.store(0, Ordering::Relaxed); - self.l2_hits.store(0, Ordering::Relaxed); - self.misses.store(0, Ordering::Relaxed); - } - - /// Get the access tracker reference. - #[allow(dead_code)] - pub fn access_tracker(&self) -> &Arc { - &self.access_tracker - } - - /// Get L1 cache statistics (for detailed monitoring). - #[allow(dead_code)] - pub async fn l1_stats(&self) -> CacheLevelStats { - self.l1_cache.run_pending_tasks().await; - CacheLevelStats { - size: self.l1_cache.weighted_size() as usize, - entries: self.l1_cache.entry_count() as usize, - max_size: self.l1_max_size, - max_entries: self.config.l1_max_objects, - hits: self.l1_hits.load(Ordering::Relaxed), - } - } - - /// Get L2 cache statistics (for detailed monitoring). - #[allow(dead_code)] - pub async fn l2_stats(&self) -> CacheLevelStats { - self.l2_cache.run_pending_tasks().await; - CacheLevelStats { - size: self.l2_cache.weighted_size() as usize, - entries: self.l2_cache.entry_count() as usize, - max_size: self.l2_max_size, - max_entries: self.config.l2_max_objects, - hits: self.l2_hits.load(Ordering::Relaxed), - } - } - - /// Record cache metrics to Prometheus. - /// - /// This method should be called periodically (e.g., every 10 seconds) - /// to export current cache statistics as Prometheus metrics. - #[allow(dead_code)] - pub async fn record_metrics(&self) { - // Get stats - let l1_stats = self.l1_stats().await; - let l2_stats = self.l2_stats().await; - let tiered_stats = self.stats().await; - - rustfs_io_metrics::record_cache_size("l1", l1_stats.size, l1_stats.entries as u64); - rustfs_io_metrics::record_cache_size("l2", l2_stats.size, l2_stats.entries as u64); - rustfs_io_metrics::record_cache_hit_rate("overall", tiered_stats.hit_rate * 100.0); - rustfs_io_metrics::record_cache_hit_rate("l1", tiered_stats.l1_hit_rate * 100.0); - } - - // ============================================ - // Cache Warming Methods - // ============================================ - - /// Warm cache with a pattern of preloading. - /// - /// This method supports different warming patterns to pre-populate the cache - /// with frequently accessed objects during server startup or maintenance windows. - /// - /// # Arguments - /// - /// * `pattern` - The warming pattern to use - /// - /// # Returns - /// - /// The number of objects successfully warmed - pub async fn warm_with_pattern(&self, pattern: WarmupPattern) -> usize { - match pattern { - WarmupPattern::RecentAccesses { limit } => { - // Get hot keys from access tracker and warm them - let hot_keys = self.access_tracker.get_hot_keys(limit).await; - let mut warmed = 0; - - for (_key, _hit_count) in hot_keys { - // Note: In a real implementation, we would load the object - // from storage and cache it. Here we just track the operation. - warmed += 1; - } - - warmed - } - WarmupPattern::SpecificKeys(keys) => { - let mut warmed = 0; - - for key in keys { - // Check if already in cache - if self.l1_cache.contains_key(&key) || self.l2_cache.contains_key(&key) { - continue; - } - - // In a real implementation, we would load the object - // from storage and cache it here. - warmed += 1; - } - - warmed - } - } - } - - /// Get hot keys for warming purposes. - /// - /// Returns the most frequently accessed keys that should be preloaded. - #[allow(dead_code)] - pub async fn get_hot_keys_for_warming(&self, limit: usize) -> Vec { - self.access_tracker - .get_hot_keys(limit) - .await - .into_iter() - .map(|(key, _)| key) - .collect() - } - - // ============================================ - // API Compatibility Methods (for migration from HotObjectCache) - // ============================================ - - /// Check if a key exists in either cache level. - pub async fn contains(&self, key: &str) -> bool { - self.l1_cache.contains_key(key) || self.l2_cache.contains_key(key) - } - - /// Get multiple objects from cache. - pub async fn get_batch(&self, keys: &[String]) -> Vec<(String, Option>)> { - let mut results = Vec::with_capacity(keys.len()); - for key in keys { - let value = self.get(key).await; - results.push((key.clone(), value)); - } - results - } - - /// Remove a key from both cache levels. - pub async fn remove(&self, key: &str) -> Option> { - // Try L1 first - if let Some(entry) = self.l1_cache.remove(key).await { - self.l2_cache.invalidate(key).await; - self.access_tracker.remove(key).await; - return Some(Arc::clone(&entry.data)); - } - // Try L2 - if let Some(entry) = self.l2_cache.remove(key).await { - self.access_tracker.remove(key).await; - return Some(Arc::clone(&entry.data)); - } - None - } - - /// Get hot keys with their hit counts. - pub async fn get_hot_keys(&self, limit: usize) -> Vec<(String, usize)> { - let keys = self.access_tracker.get_hot_keys(limit).await; - keys.into_iter().map(|(k, v)| (k, v as usize)).collect() - } - - /// Warm the cache with a pattern. - pub async fn warm(&self, pattern: WarmupPattern) -> usize { - self.warm_with_pattern(pattern).await - } - - /// Get a response object (wrapper for compatibility). - pub async fn get_response(&self, key: &str) -> Option> { - self.get(key).await - } - - /// Put a response object (wrapper for compatibility). - pub async fn put_response(&self, key: String, response: CachedGetObject) { - self.put(key, response).await - } - - /// Invalidate a versioned object. - /// - /// When version_id is Some, invalidates both "{bucket}/{key}?versionId={version_id}" - /// and "{bucket}/{key}" (the latest key). - /// When version_id is None, only invalidates "{bucket}/{key}". - pub async fn invalidate_versioned(&self, bucket: &str, key: &str, version_id: Option<&str>) { - // Invalidate the base key (latest) - let base_key = format!("{}/{}", bucket, key); - self.invalidate(&base_key).await; - - // If version_id is provided, also invalidate the versioned key - if let Some(vid) = version_id { - let versioned_key = format!("{}/{}?versionId={}", bucket, key, vid); - self.invalidate(&versioned_key).await; - } - } - - /// Get the overall hit rate. - pub fn hit_rate(&self) -> f64 { - let l1_hits = self.l1_hits.load(std::sync::atomic::Ordering::Relaxed); - let l2_hits = self.l2_hits.load(std::sync::atomic::Ordering::Relaxed); - let misses = self.misses.load(std::sync::atomic::Ordering::Relaxed); - let total_hits = l1_hits + l2_hits; - let total_requests = total_hits + misses; - - if total_requests > 0 { - total_hits as f64 / total_requests as f64 - } else { - 0.0 - } - } - - /// Get the maximum object size that can be cached. - pub fn max_object_size(&self) -> usize { - self.config.l2_max_object_size - } - - /// Get combined cache stats (for API compatibility with HotObjectCache). - /// - /// Combines L1 and L2 stats into a single-level format for backward compatibility. - pub async fn stats_as_hot_cache(&self) -> CacheStats { - let tiered_stats = self.stats().await; - - let total_size = tiered_stats.l1_size + tiered_stats.l2_size; - let total_entries = tiered_stats.l1_entries + tiered_stats.l2_entries; - let total_hits = tiered_stats.l1_hits + tiered_stats.l2_hits; - let total_max_size = tiered_stats.l1_max_size + tiered_stats.l2_max_size; - let max_object_size = self.config.l2_max_object_size; - let misses = tiered_stats.misses; - - // Calculate efficiency score (0-100) - let total_requests = total_hits + misses; - let efficiency_score = if total_requests > 0 { - (tiered_stats.hit_rate * 100.0) as u32 - } else { - 0 - }; - - CacheStats { - size: total_size, - entries: total_entries, - max_size: total_max_size, - max_object_size, - hit_count: total_hits, - miss_count: misses, - avg_age_secs: 0.0, // Not tracked in tiered cache - hit_rate: tiered_stats.hit_rate, - eviction_count: 0, // Not tracked in tiered cache - eviction_rate: 0.0, - memory_usage: total_size, - memory_usage_ratio: if total_max_size > 0 { - total_size as f64 / total_max_size as f64 - } else { - 0.0 - }, - top_keys: Vec::new(), // Would need to fetch from access tracker - efficiency_score, - } - } - - // ============================================ - // Byte-level caching methods (for compatibility with HotObjectCache API) - // ============================================ - - /// Get raw bytes from cache (API compatibility method). - /// - /// Returns the cached data bytes if available as Arc>. - pub async fn get_bytes(&self, key: &str) -> Option>> { - self.get(key).await.map(|cached| Arc::new(cached.body.to_vec())) - } - - /// Put raw bytes into cache (API compatibility method). - /// - /// Stores the byte data with minimal metadata in the appropriate cache level. - pub async fn put_bytes(&self, key: String, data: Arc>) { - // Create a CachedGetObject with minimal required fields - let cached_obj = CachedGetObject { - body: Arc::new(bytes::Bytes::copy_from_slice(data.as_slice())), - content_length: data.len() as i64, - ..Default::default() - }; - - // Store using the existing put method - self.put(key, cached_obj).await; - } - - /// Invalidate a versioned object (byte-level API). - #[allow(dead_code)] - pub async fn invalidate_bytes_versioned(&self, _bucket: &str, key: &str, _version_id: Option<&str>) { - // Just use the existing invalidate method - self.invalidate(key).await; - } - - /// Get multiple objects as bytes (API compatibility). - pub async fn get_batch_bytes(&self, keys: &[String]) -> Vec>>> { - let results = self.get_batch(keys).await; - results - .into_iter() - .map(|(_key, value)| value.map(|cached| Arc::new(cached.body.to_vec()))) - .collect() - } - - /// Get byte cache statistics (API compatibility). - #[allow(dead_code)] - pub async fn stats_bytes(&self) -> ByteCacheStats { - let cache_stats = self.stats().await; - - // Calculate efficiency score (0-100) - let total_hits = cache_stats.l1_hits + cache_stats.l2_hits; - let total_requests = total_hits + cache_stats.misses; - let efficiency_score = if total_requests > 0 { - (cache_stats.hit_rate * 100.0) as u32 - } else { - 0 - }; - - ByteCacheStats { - size: cache_stats.l1_size + cache_stats.l2_size, - entries: cache_stats.l1_entries + cache_stats.l2_entries, - max_size: cache_stats.l1_max_size + cache_stats.l2_max_size, - max_object_size: self.config.l2_max_object_size, - hit_count: cache_stats.l1_hits + cache_stats.l2_hits, - miss_count: cache_stats.misses, - avg_age_secs: 0.0, - hit_rate: cache_stats.hit_rate, - eviction_count: 0, - eviction_rate: 0.0, - memory_usage: cache_stats.l1_size + cache_stats.l2_size, - memory_usage_ratio: { - let total_max = cache_stats.l1_max_size + cache_stats.l2_max_size; - if total_max > 0 { - (cache_stats.l1_size + cache_stats.l2_size) as f64 / total_max as f64 - } else { - 0.0 - } - }, - top_keys: Vec::new(), - efficiency_score, - } - } -} - -/// Statistics for a single cache level (L1 or L2). -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub struct CacheLevelStats { - /// Current size in bytes - pub size: usize, - /// Number of entries - pub entries: usize, - /// Maximum size in bytes - pub max_size: usize, - /// Maximum number of entries - pub max_entries: usize, - /// Total hits for this level - pub hits: u64, -} - -/// Byte cache statistics (for compatibility with HotObjectCache). -#[derive(Debug, Clone)] -pub struct ByteCacheStats { - pub size: usize, - pub entries: usize, - pub max_size: usize, - pub max_object_size: usize, - pub hit_count: u64, - pub miss_count: u64, - pub avg_age_secs: f64, - pub hit_rate: f64, - pub eviction_count: u64, - pub eviction_rate: f64, - pub memory_usage: usize, - pub memory_usage_ratio: f64, - pub top_keys: Vec<(String, u64)>, - pub efficiency_score: u32, -} - -impl From for CacheStats { - fn from(stats: ByteCacheStats) -> Self { - CacheStats { - size: stats.size, - entries: stats.entries, - max_size: stats.max_size, - max_object_size: stats.max_object_size, - hit_count: stats.hit_count, - miss_count: stats.miss_count, - avg_age_secs: stats.avg_age_secs, - hit_rate: stats.hit_rate, - eviction_count: stats.eviction_count, - eviction_rate: stats.eviction_rate, - memory_usage: stats.memory_usage, - memory_usage_ratio: stats.memory_usage_ratio, - top_keys: stats.top_keys, - efficiency_score: stats.efficiency_score, - } - } -} - -/// Cache warmup pattern. -/// -/// Defines different strategies for pre-populating the cache with hot objects. -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub enum WarmupPattern { - /// Warm up recently accessed hot objects. - /// - /// # Fields - /// - /// * `limit` - Maximum number of hot objects to warm - RecentAccesses { limit: usize }, - - /// Warm up specific keys. - /// - /// # Fields - /// - /// * `keys` - List of specific keys to warm - SpecificKeys(Vec), -} - -impl Default for TieredObjectCache { - fn default() -> Self { - Self::new() - } -} - -/// Tiered cache statistics. -#[derive(Debug, Clone)] -pub struct TieredCacheStats { - /// L1 cache size in bytes - pub l1_size: usize, - /// L1 cache entry count - pub l1_entries: usize, - /// L1 max size in bytes - pub l1_max_size: usize, - - /// L2 cache size in bytes - pub l2_size: usize, - /// L2 cache entry count - pub l2_entries: usize, - /// L2 max size in bytes - pub l2_max_size: usize, - - /// L1 cache hits - pub l1_hits: u64, - /// L2 cache hits - pub l2_hits: u64, - /// Cache misses - pub misses: u64, - - /// Overall hit rate (0.0 - 1.0) - pub hit_rate: f64, - /// L1 hit rate relative to total hits (0.0 - 1.0) - #[allow(dead_code)] - pub l1_hit_rate: f64, -} - -pub(crate) struct HotObjectCache { - /// Moka cache instance for simple byte data (legacy) - cache: Cache>, - /// Moka cache instance for full GetObject responses with metadata - response_cache: Cache>, - /// Maximum total cache capacity in bytes - max_capacity: usize, - /// Maximum size of individual objects to cache (10MB by default) - max_object_size: usize, - /// Global cache hit counter - hit_count: Arc, - /// Global cache miss counter - miss_count: Arc, -} - -impl std::fmt::Debug for HotObjectCache { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use std::sync::atomic::Ordering; - f.debug_struct("HotObjectCache") - .field("max_capacity", &self.max_capacity) - .field("max_object_size", &self.max_object_size) - .field("hit_count", &self.hit_count.load(Ordering::Relaxed)) - .field("miss_count", &self.miss_count.load(Ordering::Relaxed)) - .finish() - } -} - -pub(crate) struct CachedObject { - /// The object data - data: Arc>, - /// When this object was cached - cached_at: Instant, - /// Object size in bytes - size: usize, - /// Number of times this object has been accessed - access_count: Arc, -} - -impl CachedObject { - /// Create a new CachedObject with specified size - pub fn new_with_size(data: Vec, size: usize) -> Self { - Self { - data: Arc::new(data), - cached_at: Instant::now(), - size, - access_count: Arc::new(AtomicU64::new(0)), - } - } - - /// Get the size of the cached object - #[allow(dead_code)] - pub fn size(&self) -> usize { - self.size - } - - /// Get the data reference - #[allow(dead_code)] - pub fn data(&self) -> &Arc> { - &self.data - } - - /// Get the age of the cached object - #[allow(dead_code)] - pub fn age(&self) -> Duration { - self.cached_at.elapsed() - } - - /// Increment access count and return new value - #[allow(dead_code)] - pub fn increment_access(&self) -> u64 { - self.access_count.fetch_add(1, Ordering::Relaxed) + 1 - } - - /// Get current access count - #[allow(dead_code)] - pub fn access_count(&self) -> u64 { - self.access_count.load(Ordering::Relaxed) - } -} - -/// Comprehensive cached object with full response metadata for GetObject operations. -/// -/// This structure stores all necessary fields to reconstruct a complete GetObjectOutput -/// response from cache, avoiding repeated disk reads and metadata lookups for hot objects. -/// -/// # Fields -/// -/// All time fields are serialized as RFC3339 strings to avoid parsing issues with -/// `Last-Modified` and other time headers. -/// -/// # Usage -/// -/// ```ignore -/// let cached = CachedGetObject { -/// body: Bytes::from(data), -/// content_length: data.len() as i64, -/// content_type: Some("application/octet-stream".to_string()), -/// e_tag: Some("\"abc123\"".to_string()), -/// last_modified: Some("2024-01-01T00:00:00Z".to_string()), -/// ..Default::default() -/// }; -/// manager.put_cached_object(cache_key, cached).await; -/// ``` -#[derive(Clone, Debug)] -#[allow(dead_code)] -pub struct CachedGetObject { - /// The object body data - pub body: std::sync::Arc, - /// Content length in bytes - pub content_length: i64, - /// MIME content type - pub content_type: Option, - /// Entity tag for the object - pub e_tag: Option, - /// Last modified time as RFC3339 string (e.g., "2024-01-01T12:00:00Z") - pub last_modified: Option, - /// Expiration time as RFC3339 string - pub expires: Option, - /// Cache-Control header value - pub cache_control: Option, - /// Content-Disposition header value - pub content_disposition: Option, - /// Content-Encoding header value - pub content_encoding: Option, - /// Content-Language header value - pub content_language: Option, - /// Storage class (STANDARD, REDUCED_REDUNDANCY, etc.) - pub storage_class: Option, - /// Version ID for versioned objects - pub version_id: Option, - /// Whether this is a delete marker (for versioned buckets) - pub delete_marker: bool, - /// Number of tags associated with the object - pub tag_count: Option, - /// Replication status - pub replication_status: Option, - /// User-defined metadata (x-amz-meta-*) - pub user_metadata: std::collections::HashMap, - /// Additional checksum metadata persisted with cached GET responses - pub checksum_crc32: Option, - pub checksum_crc32c: Option, - pub checksum_sha1: Option, - pub checksum_sha256: Option, - pub checksum_crc64nvme: Option, - pub checksum_type: Option, - /// When this object was cached (for internal use, automatically set) - #[allow(dead_code)] - cached_at: Option, - /// Access count for hot key tracking (automatically managed) - access_count: Arc, -} - -impl Default for CachedGetObject { - fn default() -> Self { - Self { - body: Arc::new(bytes::Bytes::new()), - content_length: 0, - content_type: None, - e_tag: None, - last_modified: None, - expires: None, - cache_control: None, - content_disposition: None, - content_encoding: None, - content_language: None, - storage_class: None, - version_id: None, - delete_marker: false, - tag_count: None, - replication_status: None, - user_metadata: std::collections::HashMap::new(), - checksum_crc32: None, - checksum_crc32c: None, - checksum_sha1: None, - checksum_sha256: None, - checksum_crc64nvme: None, - checksum_type: None, - cached_at: None, - access_count: Arc::new(AtomicU64::new(0)), - } - } -} - -#[allow(dead_code)] -impl CachedGetObject { - /// Create a new CachedGetObject with the given body and content length - pub fn new(body: bytes::Bytes, content_length: i64) -> Self { - let body = std::sync::Arc::new(body); - Self { - body, - content_length, - cached_at: Some(Instant::now()), - access_count: Arc::new(AtomicU64::new(0)), - ..Default::default() - } - } - - /// Consume a GET cache writeback payload into the cache-owned representation. - pub fn from_get_object_cache_writeback(writeback: GetObjectCacheWriteback) -> Self { - Self { - body: writeback.body, - content_length: writeback.content_length, - content_type: writeback.content_type, - e_tag: writeback.e_tag, - last_modified: writeback.last_modified, - expires: writeback.expires, - cache_control: writeback.cache_control, - content_disposition: writeback.content_disposition, - content_encoding: writeback.content_encoding, - content_language: writeback.content_language, - storage_class: writeback.storage_class, - version_id: writeback.version_id, - delete_marker: writeback.delete_marker, - user_metadata: writeback.user_metadata, - checksum_crc32: writeback.checksum_crc32, - checksum_crc32c: writeback.checksum_crc32c, - checksum_sha1: writeback.checksum_sha1, - checksum_sha256: writeback.checksum_sha256, - checksum_crc64nvme: writeback.checksum_crc64nvme, - checksum_type: writeback.checksum_type, - cached_at: Some(Instant::now()), - access_count: Arc::new(AtomicU64::new(0)), - ..Default::default() - } - } - - /// Builder method to set content_type - pub fn with_content_type(mut self, content_type: String) -> Self { - self.content_type = Some(content_type); - self - } - - /// Builder method to set e_tag - pub fn with_e_tag(mut self, e_tag: String) -> Self { - self.e_tag = Some(e_tag); - self - } - - /// Builder method to set last_modified - pub fn with_last_modified(mut self, last_modified: String) -> Self { - self.last_modified = Some(last_modified); - self - } - - /// Builder method to set cache_control - pub fn with_cache_control(mut self, cache_control: String) -> Self { - self.cache_control = Some(cache_control); - self - } - - /// Builder method to set storage_class - pub fn with_storage_class(mut self, storage_class: String) -> Self { - self.storage_class = Some(storage_class); - self - } - - /// Builder method to set version_id - pub fn with_version_id(mut self, version_id: String) -> Self { - self.version_id = Some(version_id); - self - } - - /// Builder method to set expires - pub fn with_expires(mut self, expires: String) -> Self { - self.expires = Some(expires); - self - } - - /// Builder method to set content_encoding - pub fn with_content_encoding(mut self, content_encoding: String) -> Self { - self.content_encoding = Some(content_encoding); - self - } - - /// Builder method to set content_disposition - pub fn with_content_disposition(mut self, content_disposition: String) -> Self { - self.content_disposition = Some(content_disposition); - self - } - - /// Builder method to set content_language - #[allow(dead_code)] - pub fn with_content_language(mut self, content_language: String) -> Self { - self.content_language = Some(content_language); - self - } - - /// Builder method to set replication_status - pub fn with_replication_status(mut self, replication_status: String) -> Self { - self.replication_status = Some(replication_status); - self - } - - /// Builder method to set delete_marker - pub fn with_delete_marker(mut self, delete_marker: bool) -> Self { - self.delete_marker = delete_marker; - self - } - - /// Builder method to set user_metadata - pub fn with_user_metadata(mut self, user_metadata: std::collections::HashMap) -> Self { - self.user_metadata = user_metadata; - self - } - - /// Builder method to set tag_count - pub fn with_tag_count(mut self, tag_count: i32) -> Self { - self.tag_count = Some(tag_count); - self - } - pub fn size(&self) -> usize { - self.body.len() - } - - /// Increment access count and return the new value - pub fn increment_access(&self) -> u64 { - self.access_count.fetch_add(1, Ordering::Relaxed) + 1 - } - /// Check if the cached object is expired based on expires header - pub fn is_expired(&self) -> bool { - if let Some(expires_str) = &self.expires { - // Try to parse RFC3339 format - if let Ok(expires_time) = chrono::DateTime::parse_from_rfc3339(expires_str) { - let now = chrono::Utc::now(); - return expires_time < now; - } - } - false - } - - /// Check if replication is complete - pub fn is_replication_complete(&self) -> bool { - match &self.replication_status { - Some(status) => status == "COMPLETED", - None => true, // No replication configured - } - } - - /// Get the age of this cached entry - #[allow(dead_code)] - pub fn age(&self) -> Option { - self.cached_at.map(|at| at.elapsed()) - } - - /// Record a cache hit and increment access count - pub fn record_hit(&self) -> u64 { - self.increment_access() - } - - /// Estimate memory size in bytes including metadata - pub fn memory_size(&self) -> usize { - let mut size = self.body.len(); - size += size_of::(); // content_length - size += self.content_type.as_ref().map_or(0, |s| s.len()); - size += self.e_tag.as_ref().map_or(0, |s| s.len()); - size += self.last_modified.as_ref().map_or(0, |s| s.len()); - size += self.expires.as_ref().map_or(0, |s| s.len()); - size += self.cache_control.as_ref().map_or(0, |s| s.len()); - size += self.content_disposition.as_ref().map_or(0, |s| s.len()); - size += self.content_encoding.as_ref().map_or(0, |s| s.len()); - size += self.content_language.as_ref().map_or(0, |s| s.len()); - size += self.storage_class.as_ref().map_or(0, |s| s.len()); - size += self.version_id.as_ref().map_or(0, |s| s.len()); - size += self.replication_status.as_ref().map_or(0, |s| s.len()); - size += size_of::(); // delete_marker - size += size_of::>(); // tag_count - // Estimate user_metadata size - for (k, v) in &self.user_metadata { - size += k.len() + v.len(); - } - size - } -} - -/// Internal wrapper for CachedGetObject in the Moka cache -#[derive(Clone)] -struct CachedGetObjectInternal { - /// The cached response data - data: Arc, - /// When this object was cached - cached_at: Instant, - /// Size in bytes for weigher function - size: usize, -} - -impl HotObjectCache { - /// Create a new hot object cache with Moka - /// - /// Configures Moka with: - /// - Size-based eviction (100MB max) - /// - TTL of 5 minutes - /// - TTI of 2 minutes - /// - Weigher function for accurate size tracking - #[allow(dead_code)] - pub(crate) fn new() -> Self { - let max_capacity = rustfs_utils::get_env_u64( - rustfs_config::ENV_OBJECT_CACHE_CAPACITY_MB, - rustfs_config::DEFAULT_OBJECT_CACHE_CAPACITY_MB, - ); - let cache_tti_secs = - rustfs_utils::get_env_u64(rustfs_config::ENV_OBJECT_CACHE_TTI_SECS, rustfs_config::DEFAULT_OBJECT_CACHE_TTI_SECS); - let cache_ttl_secs = - rustfs_utils::get_env_u64(rustfs_config::ENV_OBJECT_CACHE_TTL_SECS, rustfs_config::DEFAULT_OBJECT_CACHE_TTL_SECS); - - // Legacy simple byte cache - let cache = Cache::builder() - .max_capacity(max_capacity * MI_B as u64) - .weigher(|_key: &String, value: &Arc| -> u32 { - // Weight based on actual data size - value.size.min(u32::MAX as usize) as u32 - }) - .time_to_live(Duration::from_secs(cache_ttl_secs)) - .time_to_idle(Duration::from_secs(cache_tti_secs)) - .build(); - - // Full response cache with metadata - let response_cache = Cache::builder() - .max_capacity(max_capacity * MI_B as u64) - .weigher(|_key: &String, value: &Arc| -> u32 { - // Weight based on actual data size - value.size.min(u32::MAX as usize) as u32 - }) - .time_to_live(Duration::from_secs(cache_ttl_secs)) - .time_to_idle(Duration::from_secs(cache_tti_secs)) - .build(); - let max_object_size = rustfs_utils::get_env_usize( - rustfs_config::ENV_OBJECT_CACHE_MAX_OBJECT_SIZE_MB, - rustfs_config::DEFAULT_OBJECT_CACHE_MAX_OBJECT_SIZE_MB, - ) * MI_B; - Self { - cache, - max_capacity: (max_capacity * MI_B as u64) as usize, - response_cache, - max_object_size, - hit_count: Arc::new(AtomicU64::new(0)), - miss_count: Arc::new(AtomicU64::new(0)), - } - } - - /// Soft expiration determination, the number of hits is insufficient and exceeds the soft TTL - #[allow(dead_code)] - pub(crate) fn should_expire(&self, obj: &Arc) -> bool { - let age_secs = obj.cached_at.elapsed().as_secs(); - let cache_ttl_secs = - rustfs_utils::get_env_u64(rustfs_config::ENV_OBJECT_CACHE_TTL_SECS, rustfs_config::DEFAULT_OBJECT_CACHE_TTL_SECS); - let hot_object_min_hits_to_extend = rustfs_utils::get_env_usize( - rustfs_config::ENV_OBJECT_HOT_MIN_HITS_TO_EXTEND, - rustfs_config::DEFAULT_OBJECT_HOT_MIN_HITS_TO_EXTEND, - ); - if age_secs >= cache_ttl_secs { - let hits = obj.access_count.load(Ordering::Relaxed); - return hits < hot_object_min_hits_to_extend as u64; - } - false - } - - /// Get an object from cache with lock-free concurrent access - /// - /// Moka provides lock-free reads, significantly improving concurrent performance. - #[allow(dead_code)] - pub(crate) async fn get(&self, key: &str) -> Option>> { - match self.cache.get(key).await { - Some(cached) => { - if self.should_expire(&cached) { - self.cache.invalidate(key).await; - self.miss_count.fetch_add(1, Ordering::Relaxed); - return None; - } - // Update access count - cached.access_count.fetch_add(1, Ordering::Relaxed); - self.hit_count.fetch_add(1, Ordering::Relaxed); - - // IMPORTANT: Do NOT add high cardinality labels to metrics! - // Previously, this metric was tagged with individual file URIs/keys, - // causing unbounded memory growth in RustFS's own process. The metrics - // crate maintains an internal HashMap for all metric series, and each - // unique file path creates a new entry that is never cleaned up. - // This HashMap grows unbounded with unique file access, causing memory - // leaks in RustFS itself (and also in downstream systems like Prometheus). - // Only use low cardinality labels like operation type or status. - rustfs_io_metrics::record_tiered_cache_operation("hot", "hit", None); - - Some(Arc::clone(&cached.data)) - } - None => { - self.miss_count.fetch_add(1, Ordering::Relaxed); - rustfs_io_metrics::record_tiered_cache_operation("hot", "miss", None); - - None - } - } - } - - /// Put an object into cache with automatic size-based eviction - /// - /// Moka handles eviction automatically based on the weigher function. - #[allow(dead_code)] - pub(crate) async fn put(&self, key: String, data: Arc) { - let size = data.size; - - // Only cache objects smaller than max_object_size - if size == 0 || size > self.max_object_size { - return; - } - - let cached_obj = Arc::new(CachedObject { - data: Arc::clone(&data.data), - cached_at: Instant::now(), - size, - access_count: Arc::new(AtomicU64::new(0)), - }); - - self.cache.insert(key.clone(), cached_obj).await; - rustfs_io_metrics::record_tiered_cache_operation("hot", "put", Some(size)); - rustfs_io_metrics::record_cache_size("hot", self.cache.weighted_size() as usize, self.cache.entry_count()); - } - - /// Clear all cached objects - #[allow(dead_code)] - pub(crate) async fn clear(&self) { - // Clear both simple cache and response cache - self.cache.invalidate_all(); - self.response_cache.invalidate_all(); - // Sync to ensure all entries are removed - self.cache.run_pending_tasks().await; - self.response_cache.run_pending_tasks().await; - } - - /// Get cache statistics for monitoring - pub(crate) async fn stats(&self) -> CacheStats { - // Ensure pending tasks are processed for accurate stats in both caches - self.cache.run_pending_tasks().await; - self.response_cache.run_pending_tasks().await; - - // Calculate average age for simple cache - let mut total_ms: u128 = 0; - let mut cnt: u64 = 0; - self.cache.iter().for_each(|(_, v)| { - total_ms += v.cached_at.elapsed().as_millis(); - cnt += 1; - }); - - // Calculate average age for response cache - let mut response_total_ms: u128 = 0; - let mut response_cnt: u64 = 0; - self.response_cache.iter().for_each(|(_, v)| { - response_total_ms += v.cached_at.elapsed().as_millis(); - response_cnt += 1; - }); - - // Combine average age calculation - let total_entries = cnt + response_cnt; - let combined_total_ms = total_ms + response_total_ms; - let avg_age_secs = if total_entries == 0 { - 0.0 - } else { - (combined_total_ms as f64 / total_entries as f64) / 1000.0 - }; - - let hit_count = self.hit_count.load(Ordering::Relaxed); - let miss_count = self.miss_count.load(Ordering::Relaxed); - let total_requests = hit_count + miss_count; - let hit_rate = if total_requests > 0 { - hit_count as f64 / total_requests as f64 - } else { - 0.0 - }; - - // Calculate total size from both caches - let simple_size = self.cache.weighted_size() as usize; - let response_size = self.response_cache.weighted_size() as usize; - let total_size = simple_size + response_size; - - let memory_usage_ratio = if self.max_capacity > 0 { - total_size as f64 / self.max_capacity as f64 - } else { - 0.0 - }; - - let efficiency_score = if total_entries == 0 { - // Empty cache has no actual utility, efficiency score is 0 - 0 - } else { - // Non-empty cache: hit rate contributes 50%, remaining capacity contributes 50% - (hit_rate * 50.0 + (1.0 - memory_usage_ratio) * 50.0) as u32 - }; - - CacheStats { - size: total_size, - entries: total_entries as usize, - max_size: self.max_capacity, - max_object_size: self.max_object_size, - hit_count, - miss_count, - avg_age_secs, - hit_rate, - eviction_count: 0, // Moka doesn't expose eviction count - eviction_rate: 0.0, - memory_usage: total_size, - memory_usage_ratio, - top_keys: vec![], // Would need additional tracking - efficiency_score, - } - } - - /// Check if a key exists in cache (lock-free) - #[allow(dead_code)] - pub(crate) async fn contains(&self, key: &str) -> bool { - // Check both simple cache and response cache - self.cache.contains_key(key) || self.response_cache.contains_key(key) - } - - /// Get multiple objects from cache in parallel - /// - /// Leverages Moka's lock-free design for true parallel access. - #[allow(dead_code)] - pub(crate) async fn get_batch(&self, keys: &[String]) -> Vec>>> { - let mut results = Vec::with_capacity(keys.len()); - for key in keys { - results.push(self.get(key).await); - } - results - } - - /// Remove a specific key from cache - #[allow(dead_code)] - pub(crate) async fn remove(&self, key: &str) -> bool { - let had_key = self.cache.contains_key(key); - self.cache.invalidate(key).await; - had_key - } - - /// Get the most frequently accessed keys - /// - /// Returns up to `limit` keys sorted by access count in descending order. - #[allow(dead_code)] - pub(crate) async fn get_hot_keys(&self, limit: usize) -> Vec<(String, u64)> { - // Run pending tasks to ensure accurate entry count - self.cache.run_pending_tasks().await; - - let mut entries: Vec<(String, u64)> = Vec::new(); - - // Iterate through cache entries - self.cache.iter().for_each(|(key, value)| { - entries.push((key.to_string(), value.access_count.load(Ordering::Relaxed))); - }); - - entries.sort_by(|a, b| b.1.cmp(&a.1)); - entries.truncate(limit); - entries - } - - /// Warm up cache with a batch of objects - #[allow(dead_code)] - pub(crate) async fn warm(&self, objects: Vec<(String, Vec)>) { - for (key, data) in objects { - let size = data.len(); - let cached_obj = Arc::new(CachedObject::new_with_size(data, size)); - self.put(key, cached_obj).await; - } - } - - /// Get hit rate percentage - #[allow(dead_code)] - pub(crate) fn hit_rate(&self) -> f64 { - let hits = self.hit_count.load(Ordering::Relaxed); - let misses = self.miss_count.load(Ordering::Relaxed); - let total = hits + misses; - - if total == 0 { - 0.0 - } else { - (hits as f64 / total as f64) * 100.0 - } - } - - // ============================================ - // Response Cache Methods (CachedGetObject) - // ============================================ - - /// Get a cached GetObject response with full metadata - /// - /// This method retrieves a complete GetObject response from the response cache, - /// including body data and all response metadata (e_tag, last_modified, etc.). - /// - /// # Arguments - /// - /// * `key` - Cache key in the format "{bucket}/{key}" or "{bucket}/{key}?versionId={version_id}" - /// - /// # Returns - /// - /// * `Some(Arc)` - Cached response data if found and not expired - /// * `None` - Cache miss - #[allow(dead_code)] - pub(crate) async fn get_response(&self, key: &str) -> Option> { - match self.response_cache.get(key).await { - Some(cached) => { - // Check soft expiration - let age_secs = cached.cached_at.elapsed().as_secs(); - let cache_ttl_secs = rustfs_utils::get_env_u64( - rustfs_config::ENV_OBJECT_CACHE_TTL_SECS, - rustfs_config::DEFAULT_OBJECT_CACHE_TTL_SECS, - ); - let hot_object_min_hits = rustfs_utils::get_env_usize( - rustfs_config::ENV_OBJECT_HOT_MIN_HITS_TO_EXTEND, - rustfs_config::DEFAULT_OBJECT_HOT_MIN_HITS_TO_EXTEND, - ); - - if age_secs >= cache_ttl_secs { - let hits = cached.data.access_count.load(Ordering::Relaxed); - if hits < hot_object_min_hits as u64 { - self.response_cache.invalidate(key).await; - self.miss_count.fetch_add(1, Ordering::Relaxed); - return None; - } - } - - // Update access count - cached.data.increment_access(); - self.hit_count.fetch_add(1, Ordering::Relaxed); - - // IMPORTANT: Do NOT add high cardinality labels to metrics! - // See HotObjectCache::get() for details. The metrics crate's internal - // HashMap grows unbounded with high cardinality labels, causing memory - // leaks in RustFS's own process. - rustfs_io_metrics::record_tiered_cache_operation("response", "hit", None); - - Some(Arc::clone(&cached.data)) - } - None => { - self.miss_count.fetch_add(1, Ordering::Relaxed); - rustfs_io_metrics::record_tiered_cache_operation("response", "miss", None); - - None - } - } - } - - /// Put a GetObject response into the response cache - /// - /// This method caches a complete GetObject response including body and metadata. - /// Objects larger than `max_object_size` or empty objects are not cached. - /// - /// # Arguments - /// - /// * `key` - Cache key in the format "{bucket}/{key}" or "{bucket}/{key}?versionId={version_id}" - /// * `response` - The complete cached response to store - #[allow(dead_code)] - pub(crate) async fn put_response(&self, key: String, response: CachedGetObject) { - let size = response.size(); - - // Only cache objects smaller than max_object_size - if size == 0 || size > self.max_object_size { - return; - } - - let cached_internal = Arc::new(CachedGetObjectInternal { - data: Arc::new(response), - cached_at: Instant::now(), - size, - }); - - self.response_cache.insert(key.clone(), cached_internal).await; - rustfs_io_metrics::record_tiered_cache_operation("response", "put", Some(size)); - rustfs_io_metrics::record_cache_size( - "response", - self.response_cache.weighted_size() as usize, - self.response_cache.entry_count(), - ); - } - - /// Invalidate a cache entry for a specific object - /// - /// This method removes both the simple byte cache entry and the response cache entry - /// for the given key. Used when objects are modified or deleted. - /// - /// # Arguments - /// - /// * `key` - Cache key to invalidate (e.g., "{bucket}/{key}") - #[allow(dead_code)] - pub(crate) async fn invalidate(&self, key: &str) { - // Invalidate both caches - self.cache.invalidate(key).await; - self.response_cache.invalidate(key).await; - rustfs_io_metrics::record_tiered_cache_operation("overall", "evict", None); - } - - /// Invalidate cache entries for an object and its latest version - /// - /// For versioned buckets, this invalidates both: - /// - The specific version key: "{bucket}/{key}?versionId={version_id}" - /// - The latest version key: "{bucket}/{key}" - /// - /// This ensures that after a write/delete, clients don't receive stale data. - /// - /// # Arguments - /// - /// * `bucket` - Bucket name - /// * `key` - Object key - /// * `version_id` - Optional version ID (if None, only invalidates the base key) - #[allow(dead_code)] - pub(crate) async fn invalidate_versioned(&self, bucket: &str, key: &str, version_id: Option<&str>) { - // Always invalidate the latest version key - let base_key = format!("{bucket}/{key}"); - self.invalidate(&base_key).await; - - // Also invalidate the specific version if provided - if let Some(vid) = version_id { - let versioned_key = format!("{base_key}?versionId={vid}"); - self.invalidate(&versioned_key).await; - } - } - - /// Clear all cached objects from both caches - #[allow(dead_code)] - pub(crate) async fn clear_all(&self) { - self.cache.invalidate_all(); - self.response_cache.invalidate_all(); - // Sync to ensure all entries are removed - self.cache.run_pending_tasks().await; - self.response_cache.run_pending_tasks().await; - } - - /// Get the maximum object size for caching - #[allow(dead_code)] - pub(crate) fn max_object_size(&self) -> usize { - self.max_object_size - } - - /// Get cache health status - #[allow(dead_code)] - pub(crate) async fn health_status(&self) -> CacheHealthStatus { - let stats = self.stats().await; - let memory_usage = self.cache.weighted_size() as usize; - - let is_healthy = stats.memory_usage_ratio < 0.95 && stats.hit_rate > 0.1; - - let mut recommendations = Vec::new(); - if stats.hit_rate < 0.5 { - recommendations.push("Consider increasing cache size or TTL".to_string()); - } - if stats.memory_usage_ratio > 0.9 { - recommendations.push("Cache is nearly full, consider increasing capacity".to_string()); - } - - CacheHealthStatus { - memory_usage, - is_healthy, - memory_usage_ratio: stats.memory_usage_ratio, - hit_rate: stats.hit_rate, - eviction_rate: stats.eviction_rate, - avg_entry_age_secs: stats.avg_age_secs, - efficiency_score: stats.efficiency_score, - recommendations, - } - } - - /// Get current memory usage in bytes - #[allow(dead_code)] - pub(crate) async fn memory_usage(&self) -> usize { - // Sync pending tasks to ensure accurate weight statistics - self.cache.run_pending_tasks().await; - self.cache.weighted_size() as usize - } - - /// Evict a percentage of cached entries - #[allow(dead_code)] - pub(crate) async fn evict_percentage(&self, percentage: f64) -> u64 { - let stats = self.stats().await; - let entries_to_evict = (stats.entries as f64 * percentage / 100.0).max(1.0) as u64; - - // Moka does not support selective eviction, so we use invalidate_all - if entries_to_evict > 0 { - self.cache.invalidate_all(); - } - - entries_to_evict - } - - /// Warm cache from a list of hot keys - #[allow(dead_code)] - pub(crate) async fn warm_from_hot_list(&self, hot_keys: Vec<(String, Vec)>) -> u64 { - let mut warmed = 0u64; - - for (key, data) in hot_keys { - let size = data.len(); - if size <= self.max_object_size { - let cached_obj = Arc::new(CachedObject::new_with_size(data, size)); - self.cache.insert(key, cached_obj).await; - warmed += 1; - } - } - - warmed - } -} - -/// Cache statistics for monitoring and debugging -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub struct CacheStats { - /// Current total size of cached objects in bytes - pub size: usize, - /// Number of cached entries - pub entries: usize, - /// Maximum allowed cache size in bytes - pub max_size: usize, - /// Maximum allowed object size in bytes - pub max_object_size: usize, - /// Total number of cache hits - pub hit_count: u64, - /// Total number of cache misses - pub miss_count: u64, - /// Average cache object age (seconds) - pub avg_age_secs: f64, - /// Cache hit rate (0.0 - 1.0) - pub hit_rate: f64, - /// Total number of evictions - pub eviction_count: u64, - /// Eviction rate (evictions per second) - pub eviction_rate: f64, - /// Memory usage in bytes - pub memory_usage: usize, - /// Memory usage ratio (0.0 - 1.0) - pub memory_usage_ratio: f64, - /// Top hot keys (key, hit_count) - pub top_keys: Vec<(String, u64)>, - /// Efficiency score (0-100) - pub efficiency_score: u32, -} - -/// Cache health status for monitoring and diagnostics -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub struct CacheHealthStatus { - /// Memory usage in bytes - pub memory_usage: usize, - /// Whether the cache is healthy - pub is_healthy: bool, - /// Memory usage ratio (0.0 - 1.0) - pub memory_usage_ratio: f64, - /// Hit rate (0.0 - 1.0) - pub hit_rate: f64, - /// Eviction rate (evictions per second) - pub eviction_rate: f64, - /// Average entry age (seconds) - pub avg_entry_age_secs: f64, - /// Efficiency score (0-100) - pub efficiency_score: u32, - /// Optimization recommendations - pub recommendations: Vec, -} -// ============================================ -// Unit Tests for CachedGetObject -// ============================================ - -#[cfg(test)] -mod cached_object_tests { - use super::*; - use bytes::Bytes; - - #[test] - fn test_cached_get_object_builder() { - let obj = CachedGetObject::new(Bytes::from("test data"), 9) - .with_content_type("text/plain".to_string()) - .with_e_tag("\"abc123\"".to_string()) - .with_last_modified("2024-01-01T12:00:00Z".to_string()) - .with_cache_control("max-age=3600".to_string()) - .with_expires("2024-12-31T23:59:59Z".to_string()) - .with_content_encoding("gzip".to_string()) - .with_content_disposition("attachment; filename=\"test.txt\"".to_string()) - .with_storage_class("STANDARD".to_string()) - .with_version_id("v1".to_string()) - .with_replication_status("COMPLETED".to_string()) - .with_tag_count(5) - .with_delete_marker(false); - - assert_eq!(obj.content_type, Some("text/plain".to_string())); - assert_eq!(obj.e_tag, Some("\"abc123\"".to_string())); - assert_eq!(obj.storage_class, Some("STANDARD".to_string())); - assert_eq!(obj.version_id, Some("v1".to_string())); - assert_eq!(obj.replication_status, Some("COMPLETED".to_string())); - assert_eq!(obj.tag_count, Some(5)); - assert!(!obj.delete_marker); - } - - #[test] - fn test_cached_get_object_with_metadata() { - let mut metadata = std::collections::HashMap::new(); - metadata.insert("x-amz-meta-custom".to_string(), "value".to_string()); - metadata.insert("x-amz-meta-author".to_string(), "test".to_string()); - - let obj = CachedGetObject::new(Bytes::from("data"), 4).with_user_metadata(metadata.clone()); - - assert_eq!(obj.user_metadata.len(), 2); - assert_eq!(obj.user_metadata.get("x-amz-meta-custom"), Some(&"value".to_string())); - } - - #[test] - fn test_cached_get_object_from_get_object_cache_writeback() { - let body = Arc::new(Bytes::from("test data")); - let obj = CachedGetObject::from_get_object_cache_writeback(GetObjectCacheWriteback { - body: Arc::clone(&body), - content_length: 9, - content_type: Some("text/plain".to_string()), - content_encoding: Some("gzip".to_string()), - cache_control: Some("max-age=3600".to_string()), - content_disposition: Some("attachment".to_string()), - content_language: Some("en-US".to_string()), - expires: Some("2024-12-31T23:59:59Z".to_string()), - storage_class: Some("STANDARD".to_string()), - version_id: Some("null".to_string()), - delete_marker: false, - user_metadata: { - let mut metadata = std::collections::HashMap::new(); - metadata.insert("custom-key".to_string(), "value".to_string()); - metadata - }, - e_tag: Some("\"abc123\"".to_string()), - last_modified: Some("2024-01-01T12:00:00Z".to_string()), - checksum_crc32: Some("crc32".to_string()), - checksum_crc32c: None, - checksum_sha1: None, - checksum_sha256: None, - checksum_crc64nvme: None, - checksum_type: Some(s3s::dto::ChecksumType::from_static(s3s::dto::ChecksumType::FULL_OBJECT)), - }); - - assert_eq!(obj.content_length, 9); - assert_eq!(obj.content_type.as_deref(), Some("text/plain")); - assert_eq!(obj.content_encoding.as_deref(), Some("gzip")); - assert_eq!(obj.cache_control.as_deref(), Some("max-age=3600")); - assert_eq!(obj.content_disposition.as_deref(), Some("attachment")); - assert_eq!(obj.content_language.as_deref(), Some("en-US")); - assert_eq!(obj.expires.as_deref(), Some("2024-12-31T23:59:59Z")); - assert_eq!(obj.storage_class.as_deref(), Some("STANDARD")); - assert_eq!(obj.version_id.as_deref(), Some("null")); - assert!(!obj.delete_marker); - assert_eq!(obj.user_metadata.get("custom-key").map(String::as_str), Some("value")); - assert_eq!(obj.e_tag.as_deref(), Some("\"abc123\"")); - assert_eq!(obj.last_modified.as_deref(), Some("2024-01-01T12:00:00Z")); - assert_eq!(obj.checksum_crc32.as_deref(), Some("crc32")); - assert_eq!( - obj.checksum_type, - Some(s3s::dto::ChecksumType::from_static(s3s::dto::ChecksumType::FULL_OBJECT)) - ); - assert!(Arc::ptr_eq(&obj.body, &body)); - } - - #[test] - fn test_cached_get_object_size() { - let obj = CachedGetObject::new(Bytes::from("test"), 4); - assert_eq!(obj.size(), 4); - - let large_obj = CachedGetObject::new(Bytes::from(vec![0u8; 1024]), 1024); - assert_eq!(large_obj.size(), 1024); - } - - #[test] - fn test_cached_get_object_access_count() { - let obj = CachedGetObject::new(Bytes::from("test"), 4); - - assert_eq!(obj.increment_access(), 1); - assert_eq!(obj.increment_access(), 2); - assert_eq!(obj.increment_access(), 3); - } - - #[test] - fn test_cached_get_object_is_expired() { - // Not expired - no expires header - let obj1 = CachedGetObject::new(Bytes::from("test"), 4); - assert!(!obj1.is_expired()); - - // Expired - past expires time - let obj2 = CachedGetObject::new(Bytes::from("test"), 4).with_expires("2020-01-01T00:00:00Z".to_string()); - assert!(obj2.is_expired()); - - // Not expired - future expires time - let future = chrono::Utc::now() + chrono::Duration::days(1); - let obj3 = CachedGetObject::new(Bytes::from("test"), 4).with_expires(future.to_rfc3339()); - assert!(!obj3.is_expired()); - } - - #[test] - fn test_cached_get_object_replication_status() { - // Completed replication - let obj1 = CachedGetObject::new(Bytes::from("test"), 4).with_replication_status("COMPLETED".to_string()); - assert!(obj1.is_replication_complete()); - - // Pending replication - let obj2 = CachedGetObject::new(Bytes::from("test"), 4).with_replication_status("PENDING".to_string()); - assert!(!obj2.is_replication_complete()); - - // No replication configured - let obj3 = CachedGetObject::new(Bytes::from("test"), 4); - assert!(obj3.is_replication_complete()); - } - - #[test] - fn test_cached_get_object_memory_size() { - let obj = CachedGetObject::new(Bytes::from("test"), 4) - .with_content_type("text/plain".to_string()) - .with_e_tag("\"abc\"".to_string()); - - let size = obj.memory_size(); - // Should include body + content_type + e_tag + other fields - assert!(size >= 4 + 10 + 5); // At least body + content_type + e_tag - } - - #[test] - fn test_cached_get_object_record_hit() { - let obj = CachedGetObject::new(Bytes::from("test"), 4); - - assert_eq!(obj.record_hit(), 1); - assert_eq!(obj.record_hit(), 2); - assert_eq!(obj.record_hit(), 3); - } -} - -// ============================================ -// Unit Tests for CacheHealthStatus -// ============================================ - -#[cfg(test)] -mod cache_health_tests { - use super::*; - use serial_test::serial; - - #[tokio::test] - #[serial] - async fn test_cache_health_status() { - let cache = HotObjectCache::new(); - - // Add some entries - let data1 = Arc::new(CachedObject::new_with_size(vec![1, 2, 3, 4], 4)); - let data2 = Arc::new(CachedObject::new_with_size(vec![5, 6, 7, 8], 4)); - - cache.put("key1".to_string(), data1).await; - cache.put("key2".to_string(), data2).await; - - // Get health status - let health = cache.health_status().await; - - assert!(health.memory_usage > 0); - assert!(health.memory_usage_ratio >= 0.0 && health.memory_usage_ratio <= 1.0); - assert!(health.hit_rate >= 0.0 && health.hit_rate <= 1.0); - assert!(health.efficiency_score <= 100); - } - - #[tokio::test] - #[serial] - async fn test_cache_memory_usage() { - let cache = HotObjectCache::new(); - - let initial_usage = cache.memory_usage().await; - assert_eq!(initial_usage, 0); - - // Add some data - let data = Arc::new(CachedObject::new_with_size(vec![0u8; 1024], 1024)); - cache.put("key".to_string(), data).await; - - let new_usage = cache.memory_usage().await; - assert!(new_usage > initial_usage); - } - - #[tokio::test] - #[serial] - async fn test_cache_evict_percentage() { - let cache = HotObjectCache::new(); - - // Add multiple entries - for i in 0..10 { - let data = Arc::new(CachedObject::new_with_size(vec![i as u8; 100], 100)); - cache.put(format!("key{}", i), data).await; - } - - let stats = cache.stats().await; - assert_eq!(stats.entries, 10); - - // Evict 50% - let evicted = cache.evict_percentage(50.0).await; - assert!(evicted > 0); - } - - #[tokio::test] - #[serial] - async fn test_cache_warm_from_hot_list() { - let cache = HotObjectCache::new(); - - let hot_keys = vec![ - ("key1".to_string(), vec![1, 2, 3]), - ("key2".to_string(), vec![4, 5, 6]), - ("key3".to_string(), vec![7, 8, 9]), - ]; - - let warmed = cache.warm_from_hot_list(hot_keys).await; - assert_eq!(warmed, 3); - - // Verify entries are in cache - assert!(cache.contains("key1").await); - assert!(cache.contains("key2").await); - assert!(cache.contains("key3").await); - } -} - -// ============================================ -// Unit Tests for CacheStats -// ============================================ - -#[cfg(test)] -mod cache_stats_tests { - use super::*; - use serial_test::serial; - - #[tokio::test] - #[serial] - async fn test_cache_stats_hit_rate() { - let cache = HotObjectCache::new(); - - // Add an entry - let data = Arc::new(CachedObject::new_with_size(vec![1, 2, 3], 3)); - cache.put("key".to_string(), data).await; - - // Generate some hits and misses - cache.get("key").await; // Hit - cache.get("key").await; // Hit - cache.get("nonexistent").await; // Miss - cache.get("nonexistent").await; // Miss - - let stats = cache.stats().await; - - assert_eq!(stats.hit_count, 2); - assert_eq!(stats.miss_count, 2); - assert!((stats.hit_rate - 0.5).abs() < 0.01); // Should be ~50% - } - - #[tokio::test] - #[serial] - async fn test_cache_stats_memory_usage_ratio() { - let cache = HotObjectCache::new(); - - let stats = cache.stats().await; - assert_eq!(stats.memory_usage_ratio, 0.0); // Empty cache - - // Add some data - let data = Arc::new(CachedObject::new_with_size(vec![0u8; 1024], 1024)); - cache.put("key".to_string(), data).await; - - let stats = cache.stats().await; - assert!(stats.memory_usage_ratio > 0.0); - } - - #[tokio::test] - #[serial] - async fn test_cache_stats_efficiency_score() { - let cache = HotObjectCache::new(); - - // Empty cache - low efficiency - let stats = cache.stats().await; - assert!(stats.efficiency_score < 50); - - // Add data and generate hits - let data = Arc::new(CachedObject::new_with_size(vec![1, 2, 3], 3)); - cache.put("key".to_string(), data).await; - - for _ in 0..10 { - cache.get("key").await; // Hits - } - - let stats = cache.stats().await; - assert!(stats.efficiency_score > 0); - } -} diff --git a/rustfs/src/storage/concurrent_get_object_test.rs b/rustfs/src/storage/concurrent_get_object_test.rs index 2f3a24f28..10a9c082c 100644 --- a/rustfs/src/storage/concurrent_get_object_test.rs +++ b/rustfs/src/storage/concurrent_get_object_test.rs @@ -12,85 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Integration tests for concurrent GetObject performance optimization with Moka cache. -//! -//! This test suite validates the solution to issue #911 where concurrent GetObject -//! requests experienced exponential latency degradation (59ms → 110ms → 200ms for -//! 1→2→4 concurrent requests). -//! -//! # Test Coverage -//! -//! The suite includes 20 comprehensive tests organized into categories: -//! -//! ## Request Management (3 tests) -//! - **Request Tracking**: Validates RAII guards correctly track concurrent requests -//! - **Adaptive Buffer Sizing**: Ensures buffers scale inversely with concurrency -//! - **Buffer Size Bounds**: Verifies min/max constraints are enforced -//! -//! ## Cache Operations (11 tests) -//! - **Basic Operations**: Insert, retrieve, stats, and clear operations -//! - **Size Limits**: Large objects (>10MB) are correctly rejected -//! - **Automatic Eviction**: Moka's LRU eviction maintains cache within capacity -//! - **Batch Operations**: Multi-object retrieval with single lock acquisition -//! - **Cache Warming**: Pre-population on startup for immediate performance -//! - **Cache Removal**: Explicit invalidation for stale data -//! - **Hit Rate Calculation**: Accurate hit/miss ratio tracking -//! - **TTL Configuration**: Time-to-live and time-to-idle validation -//! - **Cache Writeback Flow**: Validates cache_object → get_cached round-trip -//! - **Cache Writeback Size Limit**: Objects >10MB not cached during writeback -//! - **Cache Writeback Concurrent**: Thread-safe concurrent writeback handling -//! -//! ## Performance (4 tests) -//! - **Hot Keys Tracking**: Access pattern analysis for optimization -//! - **Concurrent Access**: Lock-free performance under 100 concurrent tasks -//! - **Advanced Sizing**: File pattern optimization (small files, sequential reads) -//! - **Performance Benchmark**: Sequential vs concurrent access comparison -//! -//! ## Advanced Features (2 tests) -//! - **Disk I/O Permits**: Rate limiting prevents disk saturation -//! - **Side-Effect Free Checks**: `is_cached()` doesn't inflate metrics -//! -//! # Moka-Specific Test Patterns -//! -//! These tests account for Moka's lock-free, asynchronous nature: -//! -//! ```ignore -//! // Pattern 1: Allow time for async operations -//! manager.cache_object(key, data).await; -//! sleep(Duration::from_millis(50)).await; // Give Moka time to process -//! -//! // Pattern 2: Run pending tasks before assertions -//! manager.cache.run_pending_tasks().await; -//! let stats = manager.cache_stats().await; -//! -//! // Pattern 3: Tolerance for timing variance -//! assert!(stats.entries >= expected_min, "Allow for concurrent evictions"); -//! ``` -//! -//! # Running Tests -//! -//! ```bash -//! # Run all concurrency tests -//! cargo test --package rustfs concurrent_get_object -//! -//! # Run specific test with output -//! cargo test --package rustfs test_concurrent_cache_access -- --nocapture -//! -//! # Run with timing output -//! cargo test --package rustfs bench_concurrent_cache_performance -- --nocapture --show-output -//! ``` -//! -//! # Performance Expectations -//! -//! - Basic cache operations: <100ms -//! - Concurrent access (100 tasks): <500ms (demonstrates lock-free advantage) -//! - Cache warming (5 objects): <200ms -//! - Eviction test: <500ms (includes Moka background cleanup time) +//! Integration tests for concurrent GetObject scheduling and request management. #[cfg(test)] mod tests { use crate::storage::concurrency::{ - CachedGetObject, ConcurrencyManager, GetObjectGuard, get_advanced_buffer_size, get_concurrency_aware_buffer_size, + ConcurrencyManager, GetObjectGuard, IoLoadLevel, IoStrategy, get_advanced_buffer_size, get_concurrency_aware_buffer_size, }; use rustfs_config::{KI_B, MI_B}; use serial_test::serial; @@ -98,162 +25,69 @@ mod tests { use std::time::Duration; use tokio::time::{Instant, sleep}; - /// Test that concurrent requests are tracked correctly with RAII guards. - /// - /// This test validates the core request tracking mechanism that enables adaptive - /// buffer sizing. The RAII guard pattern ensures accurate concurrent request counts - /// even in error/panic scenarios, which is critical for preventing performance - /// degradation under load. - /// - /// # Test Strategy - /// - /// 1. Record baseline concurrent request count - /// 2. Create multiple guards and verify counter increments - /// 3. Drop guards and verify counter decrements automatically - /// 4. Validate that no requests are "leaked" (counter returns to baseline) - /// - /// # Why This Matters - /// - /// Accurate request tracking is essential because the buffer sizing algorithm - /// uses `ACTIVE_GET_REQUESTS` to determine optimal buffer sizes. A leaked - /// counter would cause permanently reduced buffer sizes, degrading performance. #[tokio::test] #[serial] async fn test_concurrent_request_tracking() { - // Start with current baseline (may not be zero if other tests are running) let initial = GetObjectGuard::concurrent_requests(); - // Create guards to simulate concurrent requests let guard1 = ConcurrencyManager::track_request(); - assert_eq!(GetObjectGuard::concurrent_requests(), initial + 1, "First guard should increment counter"); + assert_eq!(GetObjectGuard::concurrent_requests(), initial + 1); let guard2 = ConcurrencyManager::track_request(); - assert_eq!( - GetObjectGuard::concurrent_requests(), - initial + 2, - "Second guard should increment counter" - ); + assert_eq!(GetObjectGuard::concurrent_requests(), initial + 2); let guard3 = ConcurrencyManager::track_request(); - assert_eq!(GetObjectGuard::concurrent_requests(), initial + 3, "Third guard should increment counter"); + assert_eq!(GetObjectGuard::concurrent_requests(), initial + 3); - // Drop guards and verify count decreases automatically (RAII pattern) drop(guard1); sleep(Duration::from_millis(10)).await; - assert_eq!( - GetObjectGuard::concurrent_requests(), - initial + 2, - "Counter should decrement when guard1 drops" - ); + assert_eq!(GetObjectGuard::concurrent_requests(), initial + 2); drop(guard2); sleep(Duration::from_millis(10)).await; - assert_eq!( - GetObjectGuard::concurrent_requests(), - initial + 1, - "Counter should decrement when guard2 drops" - ); + assert_eq!(GetObjectGuard::concurrent_requests(), initial + 1); drop(guard3); sleep(Duration::from_millis(10)).await; - assert_eq!( - GetObjectGuard::concurrent_requests(), - initial, - "Counter should return to baseline - no leaks!" - ); + assert_eq!(GetObjectGuard::concurrent_requests(), initial); } - /// Test adaptive buffer sizing under different concurrency levels. - /// - /// This test validates the core solution to issue #911. The adaptive buffer sizing - /// algorithm prevents the exponential latency degradation seen in the original issue - /// by reducing buffer sizes as concurrency increases, preventing memory contention. - /// - /// # Original Issue - /// - /// - 1 concurrent request: 59ms (fixed 1MB buffers OK) - /// - 2 concurrent requests: 110ms (2MB total → memory contention starts) - /// - 4 concurrent requests: 200ms (4MB total → severe contention) - /// - /// # Solution - /// - /// Adaptive buffer sizing scales buffers inversely with concurrency: - /// - 1-2 requests: 100% buffers (256KB → 256KB) - optimize for throughput - /// - 3-4 requests: 75% buffers (256KB → 192KB) - balance performance - /// - 5-8 requests: 50% buffers (256KB → 128KB) - reduce memory pressure - /// - >8 requests: 40% buffers (256KB → 102KB) - fairness and predictability - /// - /// # Test Strategy - /// - /// For each concurrency level, creates guard objects to simulate active requests, - /// then validates the buffer sizing algorithm returns the expected buffer size - /// with reasonable tolerance for rounding. - /// - /// Note: This test may be affected by parallel test execution since - /// ACTIVE_GET_REQUESTS is a global atomic counter. The test uses widened - /// tolerances to account for this. #[tokio::test] #[serial] async fn test_adaptive_buffer_sizing() { - let file_size = 32 * MI_B as i64; // 32MB file (matches issue #911 test case) - let base_buffer = 256 * KI_B; // 256KB base buffer (typical for S3-like workloads) + let file_size = 32 * MI_B as i64; + let base_buffer = 256 * KI_B; - // Test cases: (concurrent_requests, description) - // Note: Tests are ordered to work with parallel execution - starting with high concurrency - // where additional requests from other tests have less impact - let test_cases = vec![ - (10, "Very high concurrency: should reduce to 40% for fairness"), - (6, "High concurrency: should reduce to 50% to prevent memory contention"), - (3, "Medium concurrency: should reduce to 75% to balance performance"), - ]; - - for (concurrent_requests, description) in test_cases { - // Create guards to simulate concurrent requests + for concurrent_requests in [10, 6, 3] { let _guards: Vec<_> = (0..concurrent_requests) .map(|_| ConcurrencyManager::track_request()) .collect(); let buffer_size = get_concurrency_aware_buffer_size(file_size, base_buffer); - // Allow widened range due to parallel test execution affecting global counter - assert!( - (64 * KI_B..=MI_B).contains(&buffer_size), - "{description}: buffer should be in valid range 64KB-1MB, got {buffer_size} bytes" - ); + assert!((64 * KI_B..=MI_B).contains(&buffer_size)); } } - /// Test buffer size bounds and minimum/maximum constraints #[tokio::test] async fn test_buffer_size_bounds() { - // Test minimum buffer size for tiny files (<100KB uses 32KB minimum) - let small_file = 1024i64; // 1KB file + let small_file = 1024i64; let min_buffer = get_concurrency_aware_buffer_size(small_file, 64 * KI_B); - assert!( - min_buffer >= 32 * KI_B, - "Buffer should have minimum size of 32KB for tiny files, got {min_buffer}" - ); + assert!(min_buffer >= 32 * KI_B); - // Test maximum buffer size (capped at 1MB when base is reasonable) - let huge_file = 10 * 1024 * MI_B as i64; // 10GB file + let huge_file = 10 * 1024 * MI_B as i64; let max_buffer = get_concurrency_aware_buffer_size(huge_file, MI_B); - assert!(max_buffer <= MI_B, "Buffer should not exceed 1MB cap when requested, got {max_buffer}"); + assert!(max_buffer <= MI_B); - // Test buffer size scaling with base - when base is small, result respects the limits - let medium_file = 200 * KI_B as i64; // 200KB file (>100KB so minimum is 64KB) + let medium_file = 200 * KI_B as i64; let buffer = get_concurrency_aware_buffer_size(medium_file, 128 * KI_B); - assert!( - (64 * KI_B..=MI_B).contains(&buffer), - "Buffer should be between 64KB and 1MB, got {buffer}" - ); + assert!((64 * KI_B..=MI_B).contains(&buffer)); } - /// Test disk I/O permit acquisition for rate limiting #[tokio::test] async fn test_disk_io_permits() { let manager = ConcurrencyManager::new(); let start = Instant::now(); - // Acquire multiple permits concurrently let handles: Vec<_> = (0..10) .map(|_| { let mgr = Arc::new(manager.clone()); @@ -265,993 +99,89 @@ mod tests { .collect(); for handle in handles { - handle.await.expect("Task should complete"); + handle.await.expect("task should complete"); } - let elapsed = start.elapsed(); - // With 64 permits, 10 concurrent tasks should complete quickly - assert!(elapsed < Duration::from_secs(1), "Should complete within 1 second, took {elapsed:?}"); + assert!(start.elapsed() < Duration::from_secs(1)); } - /// Test Moka cache operations: insert, retrieve, stats, and clear. - /// - /// This test validates the fundamental cache operations that enable sub-5ms - /// response times for frequently accessed objects. Moka's lock-free design - /// allows these operations to scale linearly with concurrency (see - /// test_concurrent_cache_access for performance validation). - /// - /// # Cache Benefits - /// - /// - Cache hit: <5ms (vs 50-200ms disk read in original issue) - /// - Lock-free concurrent access (vs LRU's RwLock bottleneck) - /// - Automatic TTL (5 min) and TTI (2 min) expiration - /// - Size-based eviction (100MB capacity, 10MB max object size) - /// - /// # Moka-Specific Behaviors - /// - /// Moka processes insertions and evictions asynchronously in background tasks. - /// This test includes appropriate `sleep()` calls to allow Moka time to process - /// operations before asserting on cache state. - /// - /// # Test Coverage - /// - /// - Initial state verification (empty cache) - /// - Object insertion and retrieval - /// - Cache statistics accuracy - /// - Miss behavior (non-existent keys) - /// - Cache clearing - #[tokio::test] - async fn test_moka_cache_operations() { - let manager = ConcurrencyManager::new(); + #[test] + fn test_advanced_buffer_size_uses_expected_bounds() { + let sequential = get_advanced_buffer_size(64 * MI_B as i64, 256 * KI_B, true); + let random = get_advanced_buffer_size(64 * MI_B as i64, 256 * KI_B, false); - // Initially empty cache - verify clean state - let stats = manager.cache_stats().await; - assert_eq!(stats.entries, 0, "New cache should have no entries"); - assert_eq!(stats.size, 0, "New cache should have zero size"); - - // Cache a small object (1MB - well under 10MB limit) - let key = "test/object1".to_string(); - let data = vec![1u8; 1024 * 1024]; // 1MB - manager.cache_object(key.clone(), data.clone()).await; - - // Give Moka time to process the async insert operation - sleep(Duration::from_millis(50)).await; - - // Verify it was cached successfully - let cached = manager.get_cached(&key).await; - assert!(cached.is_some(), "Object should be cached after insert"); - assert_eq!(*cached.unwrap(), data, "Cached data should match original data exactly"); - - // Verify stats updated correctly - let stats = manager.cache_stats().await; - assert_eq!(stats.entries, 1, "Should have exactly 1 entry after insert"); - assert!( - stats.size >= data.len(), - "Cache size should be at least data length (may include overhead)" - ); - - // Try to get non-existent key - should miss cleanly - let missing = manager.get_cached("missing/key").await; - assert!(missing.is_none(), "Missing key should return None (not panic)"); - - // Clear cache and verify cleanup - manager.clear_cache().await; - sleep(Duration::from_millis(50)).await; // Allow Moka to process invalidations - let stats = manager.cache_stats().await; - assert_eq!(stats.entries, 0, "Cache should be empty after clear operation"); + assert!(sequential >= random); + assert!((32 * KI_B..=MI_B).contains(&sequential)); + assert!((32 * KI_B..=MI_B).contains(&random)); } - /// Test that large objects are not cached (exceed max object size) - #[tokio::test] - async fn test_large_object_not_cached() { - let manager = ConcurrencyManager::new(); - - // Try to cache a large object (> 10MB) - let key = "test/large".to_string(); - let large_data = vec![1u8; 15 * MI_B]; // 15MB - - manager.cache_object(key.clone(), large_data).await; - sleep(Duration::from_millis(50)).await; - - // Should not be cached due to size limit - let cached = manager.get_cached(&key).await; - assert!(cached.is_none(), "Large object should not be cached"); - - // Cache stats should still be empty - let stats = manager.cache_stats().await; - assert_eq!(stats.entries, 0, "No objects should be cached"); - } - - /// Test Moka's automatic eviction under memory pressure - #[tokio::test] - async fn test_moka_cache_eviction() { - let manager = ConcurrencyManager::new(); - - // Clear cache for clean test state - manager.clear_cache().await; - manager.reset_cache_metrics(); - - // Cache multiple objects to exceed the limit - // Tiered cache has L1 (50MB) + L2 (200MB) = 250MB total - let object_size = 15 * MI_B; // 15MB each - let num_objects = 20; // Total 300MB > 250MB limit - - for i in 0..num_objects { - let key = format!("test/object{i}"); - let data = vec![i as u8; object_size]; - manager.cache_object(key, data).await; - sleep(Duration::from_millis(10)).await; // Give Moka time to process - } - - // Give Moka time to evict - sleep(Duration::from_millis(200)).await; - - // Verify cache size is within limit (Moka manages this automatically) - let stats = manager.cache_stats().await; - eprintln!("DEBUG: size={}, max_size={}, entries={}", stats.size, stats.max_size, stats.entries); - assert!( - stats.size <= stats.max_size, - "Moka should keep cache size {} within max {}", - stats.size, - stats.max_size - ); - - // Some objects should have been evicted - assert!( - stats.entries < num_objects, - "Expected eviction, but all {} objects might still be cached (entries: {})", - num_objects, - stats.entries - ); - } - - /// Test batch cache operations for efficient multi-object retrieval - #[tokio::test] - async fn test_cache_batch_operations() { - let manager = ConcurrencyManager::new(); - - // Cache multiple objects - for i in 0..10 { - let key = format!("batch/object{i}"); - let data = vec![i as u8; 100 * KI_B]; // 100KB each - manager.cache_object(key, data).await; - } - - sleep(Duration::from_millis(100)).await; - - // Test batch get - let keys: Vec = (0..10).map(|i| format!("batch/object{i}")).collect(); - let results = manager.get_cached_batch(&keys).await; - - assert_eq!(results.len(), 10, "Should return result for each key"); - - // Verify all objects were retrieved - let hits = results.iter().filter(|r| r.is_some()).count(); - assert!(hits >= 8, "Most objects should be cached (got {hits}/10 hits)"); - - // Mix of existing and non-existing keys - let mixed_keys = vec![ - "batch/object0".to_string(), - "nonexistent1".to_string(), - "batch/object5".to_string(), - "nonexistent2".to_string(), - ]; - let mixed_results = manager.get_cached_batch(&mixed_keys).await; - assert_eq!(mixed_results.len(), 4, "Should return result for each key"); - } - - /// Test cache warming (pre-population) - #[tokio::test] - async fn test_cache_warming() { - let manager = ConcurrencyManager::new(); - - // Prepare objects for warming - let objects: Vec<(String, Vec)> = (0..5) - .map(|i| (format!("warm/object{i}"), vec![i as u8; 500 * KI_B])) - .collect(); - - // Warm cache - manager.warm_cache(objects.clone()).await; - sleep(Duration::from_millis(100)).await; - - // Verify all objects are cached - for (key, data) in objects { - let cached = manager.get_cached(&key).await; - assert!(cached.is_some(), "Warmed object {key} should be cached"); - assert_eq!(*cached.unwrap(), data, "Cached data for {key} should match"); - } - - let stats = manager.cache_stats().await; - assert_eq!(stats.entries, 5, "Should have 5 warmed objects"); - } - - /// Test hot keys tracking with access count - #[tokio::test] - async fn test_hot_keys_tracking() { - let manager = ConcurrencyManager::new(); - - // Cache objects with different access patterns - for i in 0..5 { - let key = format!("hot/object{i}"); - let data = vec![i as u8; 100 * KI_B]; - manager.cache_object(key, data).await; - } - - sleep(Duration::from_millis(50)).await; - - // Simulate access patterns (object 0 and 1 are hot) - for _ in 0..10 { - let _ = manager.get_cached("hot/object0").await; - } - for _ in 0..5 { - let _ = manager.get_cached("hot/object1").await; - } - for _ in 0..2 { - let _ = manager.get_cached("hot/object2").await; - } - - // Get hot keys - let hot_keys = manager.get_hot_keys(3).await; - - assert!(hot_keys.len() >= 3, "Should return at least 3 keys, got {}", hot_keys.len()); - - // Verify hot keys are sorted by access count - if hot_keys.len() >= 3 { - assert!(hot_keys[0].1 >= hot_keys[1].1, "Hot keys should be sorted by access count"); - assert!(hot_keys[1].1 >= hot_keys[2].1, "Hot keys should be sorted by access count"); - } - - // Most accessed should have highest count - let top_key = &hot_keys[0]; - assert!(top_key.1 >= 10, "Most accessed object should have at least 10 hits, got {}", top_key.1); - } - - /// Test cache removal functionality - #[tokio::test] - async fn test_cache_removal() { - let manager = ConcurrencyManager::new(); - - // Cache an object - let key = "remove/test".to_string(); - let data = vec![1u8; 100 * KI_B]; - manager.cache_object(key.clone(), data).await; - sleep(Duration::from_millis(50)).await; - - // Verify it's cached - assert!(manager.is_cached(&key).await, "Object should be cached initially"); - - // Remove it - let removed = manager.remove_cached(&key).await; - assert!(removed, "Should successfully remove cached object"); - - sleep(Duration::from_millis(50)).await; - - // Verify it's gone - assert!(!manager.is_cached(&key).await, "Object should no longer be cached"); - - // Try to remove non-existent key - let not_removed = manager.remove_cached("nonexistent").await; - assert!(!not_removed, "Should return false for non-existent key"); - } - - /// Test advanced buffer sizing with file patterns - #[tokio::test] - #[serial] - async fn test_advanced_buffer_sizing() { - crate::storage::concurrency::reset_active_get_requests(); - - let base_buffer = 256 * KI_B; // 256KB base - - // Test small file optimization - let small_size = get_advanced_buffer_size(128 * KI_B as i64, base_buffer, false); - assert!( - small_size < base_buffer, - "Small files should use smaller buffers: {small_size} < {base_buffer}" - ); - assert!(small_size >= 16 * KI_B, "Should not go below minimum: {small_size}"); - - // Test sequential read optimization - let seq_size = get_advanced_buffer_size(32 * MI_B as i64, base_buffer, true); - assert!( - seq_size >= base_buffer, - "Sequential reads should use larger buffers: {seq_size} >= {base_buffer}" - ); - - // Test large file with high concurrency - let _guards: Vec<_> = (0..10).map(|_| ConcurrencyManager::track_request()).collect(); - let large_concurrent = get_advanced_buffer_size(100 * MI_B as i64, base_buffer, false); - assert!( - large_concurrent <= base_buffer, - "High concurrency should reduce buffer: {large_concurrent} <= {base_buffer}" - ); - } - - /// Test concurrent cache access performance (lock-free) - #[tokio::test] - async fn test_concurrent_cache_access() { - let manager = Arc::new(ConcurrencyManager::new()); - - // Pre-populate cache - for i in 0..20 { - let key = format!("concurrent/object{i}"); - let data = vec![i as u8; 100 * KI_B]; - manager.cache_object(key, data).await; - } - - sleep(Duration::from_millis(100)).await; - - let start = Instant::now(); - - // Simulate heavy concurrent access - let tasks: Vec<_> = (0..100) - .map(|i| { - let mgr: Arc = Arc::clone(&manager); - tokio::spawn(async move { - let key = format!("concurrent/object{}", i % 20); - let _ = mgr.get_cached(&key).await; - }) - }) - .collect(); - - for task in tasks { - task.await.expect("Task should complete"); - } - - let elapsed = start.elapsed(); - - // Moka's lock-free design should handle this quickly - assert!( - elapsed < Duration::from_millis(500), - "Concurrent cache access should be fast (took {elapsed:?})" - ); - } - - /// Test that is_cached doesn't affect LRU order or access counts - #[tokio::test] - async fn test_is_cached_no_side_effects() { - let manager = ConcurrencyManager::new(); - - let key = "check/object".to_string(); - let data = vec![42u8; 100 * KI_B]; - manager.cache_object(key.clone(), data).await; - sleep(Duration::from_millis(50)).await; - - // Check if cached multiple times - for _ in 0..10 { - assert!(manager.is_cached(&key).await, "Object should be cached"); - } - - // Access count should be minimal (contains check shouldn't increment much) - let hot_keys = manager.get_hot_keys(10).await; - if let Some(entry) = hot_keys.iter().find(|(k, _)| k == &key) { - // is_cached should not increment access_count significantly - assert!(entry.1 <= 2, "is_cached should not inflate access count, got {}", entry.1); - } - } - - /// Test cache hit rate calculation - #[tokio::test] - async fn test_cache_hit_rate() { - let manager = ConcurrencyManager::new(); - - // Reset metrics for clean test - manager.reset_cache_metrics(); - manager.clear_cache().await; - - // Cache some objects - for i in 0..5 { - let key = format!("hitrate/object{i}"); - let data = vec![i as u8; 100 * KI_B]; - manager.cache_object(key, data).await; - } - - sleep(Duration::from_millis(100)).await; - - // Verify objects are cached - for i in 0..5 { - let key = format!("hitrate/object{i}"); - assert!(manager.is_cached(&key).await, "Object {} should be cached", key); - } - - // Mix of hits and misses - for i in 0..10 { - let key = if i < 5 { - format!("hitrate/object{i}") // Hit - } else { - format!("hitrate/missing{i}") // Miss - }; - let _ = manager.get_cached(&key).await; - } - - // Hit rate should be around 50% (0.5 on 0.0-1.0 scale) - let hit_rate = manager.cache_hit_rate(); - assert!((0.4..=0.6).contains(&hit_rate), "Hit rate should be ~50% (0.5), got {hit_rate:.3}"); - } - - /// Test TTL expiration (Moka automatic cleanup) - #[tokio::test] - async fn test_ttl_expiration() { - // Note: This test would require waiting 5 minutes for TTL - // We'll just verify the cache is configured with TTL - let manager = ConcurrencyManager::new(); - - let key = "ttl/test".to_string(); - let data = vec![1u8; 100 * KI_B]; - manager.cache_object(key.clone(), data).await; - sleep(Duration::from_millis(50)).await; - - // Verify object is initially cached - assert!(manager.is_cached(&key).await, "Object should be cached"); - - // In a real scenario, after TTL (5 min) or TTI (2 min) expires, - // Moka would automatically remove the entry - // For testing, we just verify the mechanism is in place - let stats = manager.cache_stats().await; - assert!(stats.max_size > 0, "Cache should be configured with limits"); - } - - /// Benchmark: Compare performance of single vs concurrent cache access - #[tokio::test] - async fn bench_concurrent_cache_performance() { - let manager = Arc::new(ConcurrencyManager::new()); - - // Pre-populate - for i in 0..50 { - let key = format!("bench/object{i}"); - let data = vec![i as u8; 500 * KI_B]; - manager.cache_object(key, data).await; - } - - sleep(Duration::from_millis(100)).await; - - // Sequential access - let seq_start = Instant::now(); - for i in 0..100 { - let key = format!("bench/object{}", i % 50); - let _ = manager.get_cached(&key).await; - } - let seq_duration = seq_start.elapsed(); - - // Concurrent access - let conc_start = Instant::now(); - let tasks: Vec<_> = (0..100) - .map(|i| { - let mgr: Arc = Arc::clone(&manager); - tokio::spawn(async move { - let key = format!("bench/object{}", i % 50); - let _ = mgr.get_cached(&key).await; - }) - }) - .collect(); - - for task in tasks { - task.await.expect("Task should complete"); - } - let conc_duration = conc_start.elapsed(); - - println!( - "Sequential: {:?}, Concurrent: {:?}, Speedup: {:.2}x", - seq_duration, - conc_duration, - seq_duration.as_secs_f64() / conc_duration.as_secs_f64() - ); - - assert!(seq_duration > Duration::from_micros(0), "Sequential access should take some time"); - assert!(conc_duration > Duration::from_micros(0), "Concurrent access should take some time"); - - // Record performance indicators for analysis, but not as a basis for testing failure - let speedup_ratio = seq_duration.as_secs_f64() / conc_duration.as_secs_f64(); - if speedup_ratio < 0.8 { - println!("Warning: Concurrent access is significantly slower than sequential ({speedup_ratio:.2}x)"); - } else if speedup_ratio > 1.2 { - println!("Info: Concurrent access is significantly faster than sequential ({speedup_ratio:.2}x)"); - } else { - println!("Info: Performance difference between concurrent and sequential access is modest ({speedup_ratio:.2}x)"); - } - } - - /// Test cache writeback mechanism - /// - /// This test validates that the cache_object method correctly stores objects - /// and they can be retrieved later. This simulates the cache writeback flow - /// implemented in ecfs.rs for objects meeting the caching criteria. - /// - /// # Cache Criteria (from ecfs.rs) - /// - /// Objects are cached when: - /// - No range/part request (full object) - /// - Object size <= 10MB (max_object_size threshold) - /// - Not encrypted (SSE-C or managed encryption) - /// - /// This test verifies the underlying cache_object → get_cached flow works correctly. - #[tokio::test] - async fn test_cache_writeback_flow() { - let manager = ConcurrencyManager::new(); - - // Simulate cache writeback for a small object (1MB) - let cache_key = "bucket/key".to_string(); - let object_data = vec![42u8; MI_B]; // 1MB object - - // Verify not in cache initially - let initial = manager.get_cached(&cache_key).await; - assert!(initial.is_none(), "Object should not be in cache initially"); - - // Simulate cache writeback (as done in ecfs.rs background task) - manager.cache_object(cache_key.clone(), object_data.clone()).await; - - // Give Moka time to process the async insert - sleep(Duration::from_millis(50)).await; - - // Verify object is now cached - let cached = manager.get_cached(&cache_key).await; - assert!(cached.is_some(), "Object should be cached after writeback"); - assert_eq!(*cached.unwrap(), object_data, "Cached data should match original"); - - // Verify cache stats - let stats = manager.cache_stats().await; - assert_eq!(stats.entries, 1, "Should have exactly 1 cached entry"); - assert!(stats.size >= object_data.len(), "Cache size should reflect object size"); - - // Second access should hit cache - let second_access = manager.get_cached(&cache_key).await; - assert!(second_access.is_some(), "Second access should hit cache"); - - // Verify hit count increased - let hit_rate = manager.cache_hit_rate(); - assert!(hit_rate > 0.0, "Hit rate should be positive after cache hit"); - } - - /// Test cache writeback respects size limits - /// - /// Objects larger than 10MB should NOT be cached, even if cache_object is called. - /// This validates the size check in HotObjectCache::put(). - #[tokio::test] - async fn test_cache_writeback_size_limit() { - let manager = ConcurrencyManager::new(); - - // Try to cache an object that exceeds the 10MB limit - let large_key = "bucket/large_object".to_string(); - let large_data = vec![0u8; 12 * MI_B]; // 12MB > 10MB limit - - manager.cache_object(large_key.clone(), large_data).await; - sleep(Duration::from_millis(50)).await; - - // Should NOT be cached due to size limit - let cached = manager.get_cached(&large_key).await; - assert!(cached.is_none(), "Large object should not be cached"); - - // Cache should remain empty - let stats = manager.cache_stats().await; - assert_eq!(stats.entries, 0, "No entries should be cached"); - } - - /// Test cache writeback with concurrent requests - /// - /// Simulates multiple concurrent GetObject requests all trying to cache - /// the same object. Moka should handle this gracefully without data races. - #[tokio::test] - async fn test_cache_writeback_concurrent() { - let manager = Arc::new(ConcurrencyManager::new()); - let cache_key = "concurrent/object".to_string(); - let object_data = vec![99u8; 500 * KI_B]; // 500KB object - - // Simulate 10 concurrent writebacks of the same object - let tasks: Vec<_> = (0..10) - .map(|_| { - let mgr = Arc::clone(&manager); - let key = cache_key.clone(); - let data = object_data.clone(); - tokio::spawn(async move { - mgr.cache_object(key, data).await; - }) - }) - .collect(); - - for task in tasks { - task.await.expect("Task should complete"); - } - - sleep(Duration::from_millis(100)).await; - - // Object should be cached (possibly written multiple times, but same data) - let cached = manager.get_cached(&cache_key).await; - assert!(cached.is_some(), "Object should be cached after concurrent writebacks"); - assert_eq!(*cached.unwrap(), object_data, "Cached data should match original"); - - // Should have exactly 1 entry (Moka deduplicates by key) - let stats = manager.cache_stats().await; - assert_eq!(stats.entries, 1, "Should have exactly 1 entry despite concurrent writes"); - } - - /// Test cache enable/disable configuration via environment variable - /// - /// Validates that the `RUSTFS_OBJECT_CACHE_ENABLE` environment variable - /// controls whether caching is enabled. When disabled (default), cache - /// lookups and writebacks should be skipped to reduce memory usage. - /// - /// # Environment Variable - /// - /// - `RUSTFS_OBJECT_CACHE_ENABLE=true`: Enable caching - /// - `RUSTFS_OBJECT_CACHE_ENABLE=false` or unset: Disable caching (default) - /// - /// # Why This Matters - /// - /// This test validates the configuration mechanism that allows operators - /// to enable/disable caching based on their workload characteristics. - /// For read-heavy workloads with hot objects, caching provides significant - /// latency improvements. For write-heavy or unique-object workloads, - /// disabling caching reduces memory overhead. - #[tokio::test] - async fn test_cache_enable_configuration() { - // Create manager - the cache_enabled flag is read at construction time - // from RUSTFS_OBJECT_CACHE_ENABLE environment variable - let manager = ConcurrencyManager::new(); - - // By default (DEFAULT_OBJECT_CACHE_ENABLE = false), caching is disabled - // This can be verified by checking the is_cache_enabled() method - let _cache_enabled = manager.is_cache_enabled(); - - // The default is false (as defined in rustfs_config::DEFAULT_OBJECT_CACHE_ENABLE) - // This test validates the method works correctly - // Note: We can't easily test with the env var set to true in unit tests - // because the LazyLock global manager is already initialized - // Either state (true or false) is valid, as noted in the comment above - - // Cache operations should still work (the is_cache_enabled check is in ecfs.rs) - // The ConcurrencyManager itself always has a cache, but ecfs.rs checks - // is_cache_enabled() before using it - let cache_key = "test/object".to_string(); - let object_data = vec![42u8; 1024]; - - // Cache the object (this always works at the manager level) - manager.cache_object(cache_key.clone(), object_data.clone()).await; - sleep(Duration::from_millis(50)).await; - - // Retrieve from cache (this always works at the manager level) - let cached = manager.get_cached(&cache_key).await; - assert!(cached.is_some(), "Cache operations work regardless of is_cache_enabled flag"); - } - - // ============================================ - // CachedGetObject Response Cache Tests - // ============================================ - - /// Test CachedGetObject response cache basic operations - /// - /// Validates that the full response cache (with metadata) works correctly. - /// This tests the new `get_cached_object` and `put_cached_object` methods - /// that store complete GetObject responses with body and metadata. - #[tokio::test] - async fn test_cached_get_object_basic() { - let manager = ConcurrencyManager::new(); - - // Create a CachedGetObject with metadata using builder pattern - let cache_key = "bucket/object_with_metadata".to_string(); - let body_data = vec![42u8; 100 * KI_B]; - - let cached_response = CachedGetObject::new(bytes::Bytes::from(body_data.clone()), body_data.len() as i64) - .with_content_type("application/octet-stream".to_string()) - .with_e_tag("\"abc123def456\"".to_string()) - .with_last_modified("2024-01-15T12:00:00Z".to_string()) - .with_cache_control("max-age=3600".to_string()) - .with_storage_class("STANDARD".to_string()); - - // Verify not in cache initially - let initial = manager.get_cached_object(&cache_key).await; - assert!(initial.is_none(), "Object should not be in cache initially"); - - // Put the response in cache - manager.put_cached_object(cache_key.clone(), cached_response.clone()).await; - sleep(Duration::from_millis(50)).await; - - // Retrieve from cache - let retrieved = manager.get_cached_object(&cache_key).await; - assert!(retrieved.is_some(), "Object should be cached"); - - let retrieved = retrieved.unwrap(); - assert_eq!(retrieved.body.as_ref(), body_data.as_slice(), "Body should match"); - assert_eq!(retrieved.content_length, body_data.len() as i64, "Content length should match"); - assert_eq!( - retrieved.content_type, - Some("application/octet-stream".to_string()), - "Content type should match" - ); - assert_eq!(retrieved.e_tag, Some("\"abc123def456\"".to_string()), "ETag should match"); - assert_eq!( - retrieved.last_modified, - Some("2024-01-15T12:00:00Z".to_string()), - "Last modified should match" - ); - assert_eq!(retrieved.storage_class, Some("STANDARD".to_string()), "Storage class should match"); - } - - /// Test CachedGetObject with versioned objects - /// - /// Validates that versioned cache keys work correctly using the format - /// "{bucket}/{key}?versionId={version_id}". - #[tokio::test] - async fn test_cached_get_object_versioned() { - let manager = ConcurrencyManager::new(); - - let bucket = "versioned-bucket"; - let key = "object"; - let version_id = "v1234567890"; - - // Create cache keys for latest and versioned - let latest_key = ConcurrencyManager::make_cache_key(bucket, key, None); - let versioned_key = ConcurrencyManager::make_cache_key(bucket, key, Some(version_id)); - - assert_eq!(latest_key, "versioned-bucket/object"); - assert_eq!(versioned_key, "versioned-bucket/object?versionId=v1234567890"); - - // Cache different versions - let v1_body = vec![1u8; 10 * KI_B]; - let v2_body = vec![2u8; 10 * KI_B]; - - let v1_response = CachedGetObject::new(bytes::Bytes::from(v1_body.clone()), v1_body.len() as i64) - .with_version_id(version_id.to_string()); - - let v2_response = CachedGetObject::new(bytes::Bytes::from(v2_body.clone()), v2_body.len() as i64); - - // Cache both versions - manager.put_cached_object(versioned_key.clone(), v1_response).await; - manager.put_cached_object(latest_key.clone(), v2_response).await; - sleep(Duration::from_millis(50)).await; - - // Verify both can be retrieved independently - let retrieved_v1 = manager.get_cached_object(&versioned_key).await; - let retrieved_latest = manager.get_cached_object(&latest_key).await; - - assert!(retrieved_v1.is_some(), "Versioned object should be cached"); - assert!(retrieved_latest.is_some(), "Latest object should be cached"); - - assert_eq!(retrieved_v1.unwrap().body.as_ref(), v1_body.as_slice(), "V1 body should match"); - assert_eq!(retrieved_latest.unwrap().body.as_ref(), v2_body.as_slice(), "Latest body should match"); - } - - /// Test cache invalidation for write operations - /// - /// Validates that `invalidate_cache` and `invalidate_cache_versioned` work correctly. - /// This is critical for cache consistency after put_object, delete_object, etc. - #[tokio::test] - async fn test_cache_invalidation() { - let manager = ConcurrencyManager::new(); - - let cache_key = "bucket/to_invalidate".to_string(); - let body_data = vec![42u8; 10 * KI_B]; - - // Cache an object - let cached_response = CachedGetObject::new(bytes::Bytes::from(body_data), 10 * KI_B as i64); - - manager.put_cached_object(cache_key.clone(), cached_response).await; - sleep(Duration::from_millis(50)).await; - - // Verify it's cached - assert!(manager.get_cached_object(&cache_key).await.is_some(), "Object should be cached"); - - // Invalidate the cache - manager.invalidate_cache(&cache_key).await; - sleep(Duration::from_millis(50)).await; - - // Verify it's no longer cached - assert!(manager.get_cached_object(&cache_key).await.is_none(), "Object should be invalidated"); - } - - /// Test versioned cache invalidation - /// - /// Validates that invalidating a versioned object also invalidates the latest key - /// to prevent serving stale data after writes. - #[tokio::test] - async fn test_cache_invalidation_versioned() { - let manager = ConcurrencyManager::new(); - - // Clear cache for clean test state - manager.clear_cache().await; - - let bucket = "bucket"; - let key = "object"; - let version_id = "v123"; - - let latest_key = ConcurrencyManager::make_cache_key(bucket, key, None); - let versioned_key = ConcurrencyManager::make_cache_key(bucket, key, Some(version_id)); - - let body_data = vec![42u8; 10 * KI_B]; - - // Cache both versions - let response = CachedGetObject::new(bytes::Bytes::from(body_data), 10 * KI_B as i64); - - manager.put_cached_object(latest_key.clone(), response.clone()).await; - manager.put_cached_object(versioned_key.clone(), response).await; - sleep(Duration::from_millis(50)).await; - - // Verify both are cached - assert!(manager.get_cached_object(&latest_key).await.is_some(), "Latest should be cached"); - assert!(manager.get_cached_object(&versioned_key).await.is_some(), "Versioned should be cached"); - - // Invalidate with version - should invalidate both - manager.invalidate_cache_versioned(bucket, key, Some(version_id)).await; - sleep(Duration::from_millis(50)).await; - - // Both should be invalidated - assert!(manager.get_cached_object(&latest_key).await.is_none(), "Latest should be invalidated"); - assert!( - manager.get_cached_object(&versioned_key).await.is_none(), - "Versioned should be invalidated" - ); - } - - /// Test CachedGetObject size limit enforcement - /// - /// Validates that objects larger than 10MB are not cached in the response cache. - #[tokio::test] - async fn test_cached_get_object_size_limit() { - let manager = ConcurrencyManager::new(); - - let cache_key = "bucket/large_response".to_string(); - let large_body = vec![0u8; 12 * MI_B]; // 12MB > 10MB limit - - let large_response = CachedGetObject::new(bytes::Bytes::from(large_body), 12 * MI_B as i64); - - // Try to cache - should be rejected due to size - manager.put_cached_object(cache_key.clone(), large_response).await; - sleep(Duration::from_millis(50)).await; - - // Should NOT be cached - assert!( - manager.get_cached_object(&cache_key).await.is_none(), - "Large response should not be cached" - ); - } - - /// Test CachedGetObject max_object_size accessor - /// - /// Validates the max_object_size() method returns the correct threshold. - #[tokio::test] - async fn test_max_object_size() { - let manager = ConcurrencyManager::new(); - - // Default max object size is 10MB - assert_eq!(manager.max_object_size(), 10 * MI_B, "Max object size should be 10MB"); - } - - // ============================================ - // Adaptive I/O Strategy Tests - // ============================================ - - /// Test IoLoadLevel classification based on wait duration. - /// - /// This test validates that the IoLoadLevel enum correctly classifies - /// disk permit wait times into appropriate load levels. #[test] fn test_io_load_level_classification() { - use crate::storage::concurrency::IoLoadLevel; - use std::time::Duration; - - // Low load: < 10ms assert_eq!(IoLoadLevel::from_wait_duration(Duration::from_millis(0)), IoLoadLevel::Low); - assert_eq!(IoLoadLevel::from_wait_duration(Duration::from_millis(5)), IoLoadLevel::Low); - assert_eq!(IoLoadLevel::from_wait_duration(Duration::from_millis(9)), IoLoadLevel::Low); - - // Medium load: 10-50ms - assert_eq!(IoLoadLevel::from_wait_duration(Duration::from_millis(10)), IoLoadLevel::Medium); assert_eq!(IoLoadLevel::from_wait_duration(Duration::from_millis(30)), IoLoadLevel::Medium); - assert_eq!(IoLoadLevel::from_wait_duration(Duration::from_millis(49)), IoLoadLevel::Medium); - - // High load: 50-200ms - assert_eq!(IoLoadLevel::from_wait_duration(Duration::from_millis(50)), IoLoadLevel::High); assert_eq!(IoLoadLevel::from_wait_duration(Duration::from_millis(100)), IoLoadLevel::High); - assert_eq!(IoLoadLevel::from_wait_duration(Duration::from_millis(199)), IoLoadLevel::High); - - // Critical load: > 200ms - assert_eq!(IoLoadLevel::from_wait_duration(Duration::from_millis(200)), IoLoadLevel::Critical); assert_eq!(IoLoadLevel::from_wait_duration(Duration::from_millis(500)), IoLoadLevel::Critical); - assert_eq!(IoLoadLevel::from_wait_duration(Duration::from_secs(1)), IoLoadLevel::Critical); } - /// Test IoStrategy buffer size calculation based on load level. - /// - /// This test validates that buffer sizes are appropriately reduced - /// under higher load conditions. #[test] fn test_io_strategy_buffer_sizing() { - use crate::storage::concurrency::IoStrategy; - use std::time::Duration; - let base_buffer = 256 * KI_B; - // Low load: 100% of base buffer let strategy_low = IoStrategy::from_wait_duration(Duration::from_millis(5), base_buffer); assert_eq!(strategy_low.buffer_multiplier, 1.0); assert_eq!(strategy_low.buffer_size, base_buffer); assert!(strategy_low.enable_readahead); - assert!(strategy_low.cache_writeback_enabled); - // Medium load: 75% of base buffer let strategy_med = IoStrategy::from_wait_duration(Duration::from_millis(30), base_buffer); assert_eq!(strategy_med.buffer_multiplier, 0.75); assert_eq!(strategy_med.buffer_size, (base_buffer as f64 * 0.75) as usize); assert!(strategy_med.enable_readahead); - assert!(strategy_med.cache_writeback_enabled); - // High load: 50% of base buffer let strategy_high = IoStrategy::from_wait_duration(Duration::from_millis(100), base_buffer); assert_eq!(strategy_high.buffer_multiplier, 0.5); assert_eq!(strategy_high.buffer_size, (base_buffer as f64 * 0.5) as usize); - assert!(!strategy_high.enable_readahead); // Disabled under high load - assert!(strategy_high.cache_writeback_enabled); + assert!(!strategy_high.enable_readahead); - // Critical load: 40% of base buffer let strategy_crit = IoStrategy::from_wait_duration(Duration::from_millis(500), base_buffer); assert_eq!(strategy_crit.buffer_multiplier, 0.4); - // Buffer size clamped to min 32KB, max 1MB let expected = ((base_buffer as f64) * 0.4) as usize; assert_eq!(strategy_crit.buffer_size, expected.clamp(32 * KI_B, MI_B)); assert!(!strategy_crit.enable_readahead); - assert!(!strategy_crit.cache_writeback_enabled); // Disabled under critical load } - /// Test ConcurrencyManager adaptive I/O strategy calculation. - /// - /// This test validates that the calculate_io_strategy method correctly - /// produces IoStrategy instances with the expected parameters. #[tokio::test] async fn test_calculate_io_strategy() { - use crate::storage::concurrency::IoLoadLevel; - use std::time::Duration; - let manager = ConcurrencyManager::new(); let base_buffer = 256 * KI_B; - // Low load strategy let strategy = manager.calculate_io_strategy(Duration::from_millis(5), base_buffer); assert_eq!(strategy.load_level, IoLoadLevel::Low); assert_eq!(strategy.buffer_size, base_buffer); - // Medium load strategy let strategy = manager.calculate_io_strategy(Duration::from_millis(30), base_buffer); assert_eq!(strategy.load_level, IoLoadLevel::Medium); - // High load strategy let strategy = manager.calculate_io_strategy(Duration::from_millis(100), base_buffer); assert_eq!(strategy.load_level, IoLoadLevel::High); assert!(!strategy.enable_readahead); - // Critical load strategy let strategy = manager.calculate_io_strategy(Duration::from_millis(500), base_buffer); assert_eq!(strategy.load_level, IoLoadLevel::Critical); - assert!(!strategy.cache_writeback_enabled); } - /// Test ConcurrencyManager I/O load stats tracking. - /// - /// This test validates that the io_load_stats method correctly returns - /// statistics about permit wait times. #[tokio::test] async fn test_io_load_stats() { - use std::time::Duration; - let manager = ConcurrencyManager::new(); - // Record some wait observations manager.record_permit_wait(Duration::from_millis(10)); manager.record_permit_wait(Duration::from_millis(20)); manager.record_permit_wait(Duration::from_millis(30)); let (avg, p95, max, count) = manager.io_load_stats(); - // Check observation count - assert_eq!(count, 3, "Should have 3 observations"); - - // Average should be around 20ms - assert!( - avg >= Duration::from_millis(15) && avg <= Duration::from_millis(25), - "Average should be around 20ms, got {avg:?}" - ); - - // Max should be 30ms - assert_eq!(max, Duration::from_millis(30), "Max should be 30ms"); - - // P95 should be at or near 30ms - assert!(p95 >= Duration::from_millis(25), "P95 should be near 30ms, got {p95:?}"); + assert_eq!(count, 3); + assert!(avg >= Duration::from_millis(15) && avg <= Duration::from_millis(25)); + assert_eq!(max, Duration::from_millis(30)); + assert!(p95 >= Duration::from_millis(25)); } }