mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 16:28:15 +00:00
refactor(storage): remove object cache plumbing (#2422)
Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
Generated
-4
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<f64>) {
|
||||
@@ -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<usize>) {
|
||||
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);
|
||||
|
||||
@@ -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
|
||||
|
||||
+28
-513
@@ -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<Bytes>;
|
||||
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<i32>;
|
||||
fn user_metadata(&self) -> &HashMap<String, String>;
|
||||
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<dyn Reader>),
|
||||
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<Bytes>,
|
||||
pub content_length: i64,
|
||||
pub content_type: Option<String>,
|
||||
pub content_encoding: Option<String>,
|
||||
pub cache_control: Option<String>,
|
||||
pub content_disposition: Option<String>,
|
||||
pub content_language: Option<String>,
|
||||
pub expires: Option<String>,
|
||||
pub storage_class: Option<String>,
|
||||
pub version_id: Option<String>,
|
||||
pub delete_marker: bool,
|
||||
pub user_metadata: HashMap<String, String>,
|
||||
pub e_tag: Option<String>,
|
||||
pub last_modified: Option<String>,
|
||||
pub checksum_crc32: Option<String>,
|
||||
pub checksum_crc32c: Option<String>,
|
||||
pub checksum_sha1: Option<String>,
|
||||
pub checksum_sha256: Option<String>,
|
||||
pub checksum_crc64nvme: Option<String>,
|
||||
pub checksum_type: Option<ChecksumType>,
|
||||
}
|
||||
|
||||
pub struct GetObjectBodyMaterialization {
|
||||
pub body: Option<StreamingBlob>,
|
||||
pub cache_writeback: Option<GetObjectCacheWriteback>,
|
||||
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<T>(
|
||||
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<T>(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<T>(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<String, String>,
|
||||
) -> 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<R>(
|
||||
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<Bytes>,
|
||||
content_length: i64,
|
||||
content_type: Option<String>,
|
||||
e_tag: Option<String>,
|
||||
last_modified: Option<String>,
|
||||
expires: Option<String>,
|
||||
cache_control: Option<String>,
|
||||
content_disposition: Option<String>,
|
||||
content_encoding: Option<String>,
|
||||
content_language: Option<String>,
|
||||
storage_class: Option<String>,
|
||||
version_id: Option<String>,
|
||||
delete_marker: bool,
|
||||
tag_count: Option<i32>,
|
||||
user_metadata: HashMap<String, String>,
|
||||
checksum_crc32: Option<String>,
|
||||
checksum_crc32c: Option<String>,
|
||||
checksum_sha1: Option<String>,
|
||||
checksum_sha256: Option<String>,
|
||||
checksum_crc64nvme: Option<String>,
|
||||
checksum_type: Option<ChecksumType>,
|
||||
}
|
||||
|
||||
impl CachedGetObjectSource for MockCachedSource {
|
||||
fn body(&self) -> &Arc<Bytes> {
|
||||
&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<i32> {
|
||||
self.tag_count
|
||||
}
|
||||
|
||||
fn user_metadata(&self) -> &HashMap<String, String> {
|
||||
&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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<String>) {
|
||||
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<PutObjectInput>) -> S3Result<S3Response<PutObjectOutput>> {
|
||||
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
|
||||
|
||||
@@ -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<GetObject
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
Ok(GetObjectRequestContext {
|
||||
cache_key: ConcurrencyManager::make_cache_key(&bucket, &key, version_id.as_deref()),
|
||||
version_id_for_event: version_id.unwrap_or_default(),
|
||||
bucket,
|
||||
key,
|
||||
@@ -83,92 +80,6 @@ pub(super) async fn prepare_get_object_request_context(req: &S3Request<GetObject
|
||||
})
|
||||
}
|
||||
|
||||
impl ObjectIoCachedGetObjectSource for CachedGetObject {
|
||||
fn body(&self) -> &std::sync::Arc<bytes::Bytes> {
|
||||
&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<i32> {
|
||||
self.tag_count
|
||||
}
|
||||
|
||||
fn user_metadata(&self) -> &std::collections::HashMap<String, String> {
|
||||
&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<GetObjectBootstrap> {
|
||||
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<usize>,
|
||||
rs: Option<&HTTPRangeSpec>,
|
||||
request_start: std::time::Instant,
|
||||
) -> Option<GetObjectFlowResult> {
|
||||
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<StreamingBlob>,
|
||||
pub(super) body_plan: ObjectIoGetObjectBodyPlan,
|
||||
pub(super) cache_writeback: Option<GetObjectCacheWriteback>,
|
||||
}
|
||||
|
||||
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<R>(
|
||||
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<GetObjectBodyAdapterOutput>
|
||||
planning_inputs: ObjectIoGetObjectBodyPlanningInputs,
|
||||
) -> S3Result<Option<StreamingBlob>>
|
||||
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<PutObjectInput>) -> PutObjectRequestContext {
|
||||
@@ -525,29 +376,19 @@ pub(super) async fn complete_get_flow_result(
|
||||
request_context: &GetObjectRequestContext,
|
||||
flow_result: GetObjectFlowResult,
|
||||
) -> S3Result<S3Response<GetObjectOutput>> {
|
||||
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<S3Response<PutObjectOutput>> {
|
||||
|
||||
@@ -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<usize>,
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<usize>,
|
||||
pub(super) rs: Option<HTTPRangeSpec>,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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<ConcurrencyManager> = LazyLock::
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ConcurrencyManager {
|
||||
/// Tiered object cache (L1 + L2) for frequently accessed objects
|
||||
cache: Arc<TieredObjectCache>,
|
||||
/// Semaphore to limit concurrent disk reads
|
||||
disk_read_semaphore: Arc<Semaphore>,
|
||||
/// 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<Mutex<IoLoadMetrics>>,
|
||||
/// 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<Arc<Vec<u8>>> {
|
||||
self.cache.get_bytes(key).await
|
||||
}
|
||||
|
||||
/// Cache an object for future retrievals
|
||||
pub async fn cache_object(&self, key: String, data: Vec<u8>) {
|
||||
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<Option<Arc<Vec<u8>>>> {
|
||||
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<u8>)>) {
|
||||
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<CachedGetObject>)` - 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<Arc<CachedGetObject>> {
|
||||
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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user