refactor(storage): remove object cache plumbing (#2422)

Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
houseme
2026-04-07 22:00:53 +08:00
committed by GitHub
parent e3000f16e0
commit 79ffecbf14
22 changed files with 188 additions and 5011 deletions
+1 -1
View File
@@ -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 {
-46
View File
@@ -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();
-229
View File
@@ -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;
-4
View File
@@ -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,
+4 -87
View File
@@ -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);
+2 -3
View File
@@ -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
View File
@@ -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");
}