mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-16 09:58:21 +00:00
feat(get): consolidate GET performance optimization (#3972)
* feat(get): consolidate GET performance optimization Consolidated implementation of all GET performance optimizations into a single, well-organized commit replacing the previous patch-on-patch approach. ## Changes ### Configuration (set_disk/mod.rs) - Consolidated all GET optimization flags into a single organized section - Enabled by default: codec streaming, metadata early-stop, page cache reclaim - Added codec streaming multipart flag (default: disabled) - Added version-aware early-stop flag (default: disabled) - Added adaptive duplex buffer sizing based on object size - All flags use OnceLock caching with rollout percentage support ### Metadata Early-Stop (set_disk/read.rs) - Delete marker early-stop when quorum agrees - Version-aware early-stop for versioned GET requests - MetadataQuorumAccumulator enhanced with: - delete_marker_votes tracking - requested_version_id and matching_version_votes tracking - version_early_stop_decision() method - 6 new tests for version early-stop scenarios ### Codec Streaming (erasure/coding/decode_reader.rs) - DualInFlight (2-stripe lookahead) enabled by default ### Decode Pipeline (erasure/coding/decode.rs) - Stripe prefetch count configuration - Bitrot-decode overlap configuration ### Disk Layer (disk/local.rs) - O_DIRECT read configuration constants (preparation) ### Metrics (io-metrics/lib.rs) - BytesPool acquisition/return metrics - Metadata phase duration with early-stop label - Total duration with reader_path label ### Diagnostics (diagnostics/) - Early-stop reason constants - Pool tier/outcome label constants ### Observability (.docker/observability/) - 3 Grafana dashboards for GET optimization monitoring - Prometheus alert rules (6 alerts: 3 critical, 3 warning) - Updated README.md and README_ZH.md with usage docs ### Config (config/src/constants/runtime.rs) - Page cache reclaim read enabled by default ## Environment Variables | Variable | Default | Description | |----------|---------|-------------| | RUSTFS_GET_CODEC_STREAMING_ENABLE | true | Codec streaming base flag | | RUSTFS_GET_CODEC_STREAMING_ROLLOUT_PCT | 100 | Codec streaming rollout % | | RUSTFS_GET_CODEC_STREAMING_MULTIPART_ENABLE | false | Multipart codec streaming | | RUSTFS_GET_METADATA_EARLY_STOP_ENABLE | true | Early-stop base flag | | RUSTFS_GET_METADATA_EARLY_STOP_ROLLOUT_PCT | 100 | Early-stop rollout % | | RUSTFS_GET_METADATA_VERSION_EARLY_STOP_ENABLE | false | Version-aware early-stop | | RUSTFS_OBJECT_FILE_CACHE_RECLAIM_READ_ENABLE | true | Page cache reclaim | | RUSTFS_OBJECT_DIRECT_IO_READ_ENABLE | false | O_DIRECT (preparation) | | RUSTFS_GET_DECODE_STRIPE_PREFETCH_COUNT | 1 | Stripe prefetch | | RUSTFS_GET_BITROT_DECODE_OVERLAP_ENABLE | false | Bitrot-decode overlap | | RUSTFS_GET_CODEC_STREAMING_MAX_INFLIGHT | 2 | DualInFlight stripes | ## Rollback All optimizations can be disabled via environment variables: RUSTFS_GET_CODEC_STREAMING_ENABLE=false RUSTFS_GET_METADATA_EARLY_STOP_ENABLE=false RUSTFS_OBJECT_FILE_CACHE_RECLAIM_READ_ENABLE=false Co-Authored-By: heihutu <heihutu@gmail.com> * test(get): add stress test scripts for GET optimization validation - quick-validate-get-optimization.sh: Quick 5-minute validation - stress-test-get-optimization.sh: Full 30+ minute stress test - README-stress-test.md: Usage documentation Co-Authored-By: heihutu <heihutu@gmail.com> * test(ecstore): align file cache reclaim defaults * chore(deps): update redis and erasure codec * test(ecstore): align decode fill policy default * test(ecstore): align metadata early-stop default * fix(ecstore): keep metadata early stop opt-in --------- Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
@@ -97,7 +97,7 @@ pub const ENV_OBJECT_FILE_CACHE_RECLAIM_WRITE_ENABLE: &str = "RUSTFS_OBJECT_FILE
|
||||
pub const ENV_OBJECT_FILE_CACHE_RECLAIM_READ_ENABLE: &str = "RUSTFS_OBJECT_FILE_CACHE_RECLAIM_READ_ENABLE";
|
||||
pub const ENV_OBJECT_FILE_CACHE_RECLAIM_THRESHOLD: &str = "RUSTFS_OBJECT_FILE_CACHE_RECLAIM_THRESHOLD";
|
||||
pub const DEFAULT_OBJECT_FILE_CACHE_RECLAIM_WRITE_ENABLE: bool = false;
|
||||
pub const DEFAULT_OBJECT_FILE_CACHE_RECLAIM_READ_ENABLE: bool = false;
|
||||
pub const DEFAULT_OBJECT_FILE_CACHE_RECLAIM_READ_ENABLE: bool = true;
|
||||
pub const DEFAULT_OBJECT_FILE_CACHE_RECLAIM_THRESHOLD: usize = 4 * 1024 * 1024;
|
||||
|
||||
/// Threshold for small object seek support in bytes.
|
||||
|
||||
@@ -94,6 +94,12 @@ pub(crate) const GET_METADATA_EARLY_STOP_REASON_NOT_FOUND: &str = "not_found";
|
||||
pub(crate) const GET_METADATA_EARLY_STOP_REASON_UNSAFE_REQUEST: &str = "unsafe_request";
|
||||
pub(crate) const GET_METADATA_EARLY_STOP_REASON_VALID_QUORUM: &str = "valid_quorum";
|
||||
pub(crate) const GET_METADATA_EARLY_STOP_REASON_VERSION_NOT_FOUND: &str = "version_not_found";
|
||||
pub(crate) const GET_METADATA_EARLY_STOP_REASON_VERSION_MATCH_QUORUM: &str = "version_match_quorum";
|
||||
|
||||
/// Early-stop active state labels
|
||||
pub(crate) const EARLY_STOP_ACTIVE_HIT: &str = "hit";
|
||||
pub(crate) const EARLY_STOP_ACTIVE_MISS: &str = "miss";
|
||||
pub(crate) const EARLY_STOP_ACTIVE_DISABLED: &str = "disabled";
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum GetObjectFailureReason {
|
||||
@@ -296,5 +302,9 @@ mod tests {
|
||||
assert_eq!(GET_METADATA_EARLY_STOP_REASON_UNSAFE_REQUEST, "unsafe_request");
|
||||
assert_eq!(GET_METADATA_EARLY_STOP_REASON_VALID_QUORUM, "valid_quorum");
|
||||
assert_eq!(GET_METADATA_EARLY_STOP_REASON_VERSION_NOT_FOUND, "version_not_found");
|
||||
assert_eq!(GET_METADATA_EARLY_STOP_REASON_VERSION_MATCH_QUORUM, "version_match_quorum");
|
||||
assert_eq!(EARLY_STOP_ACTIVE_HIT, "hit");
|
||||
assert_eq!(EARLY_STOP_ACTIVE_MISS, "miss");
|
||||
assert_eq!(EARLY_STOP_ACTIVE_DISABLED, "disabled");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,3 +14,4 @@
|
||||
|
||||
pub(crate) mod admin_server_info;
|
||||
pub(crate) mod get;
|
||||
pub(crate) mod pool;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! BytesPool metric label constants.
|
||||
//!
|
||||
//! These constants are used when recording pool acquisition and return
|
||||
//! metrics to avoid string allocations and ensure label consistency.
|
||||
|
||||
/// BytesPool tier labels
|
||||
pub const POOL_TIER_SMALL: &str = "small";
|
||||
pub const POOL_TIER_MEDIUM: &str = "medium";
|
||||
pub const POOL_TIER_LARGE: &str = "large";
|
||||
pub const POOL_TIER_XLARGE: &str = "xlarge";
|
||||
|
||||
/// BytesPool outcome labels
|
||||
pub const POOL_OUTCOME_HIT: &str = "hit";
|
||||
pub const POOL_OUTCOME_MISS: &str = "miss";
|
||||
pub const POOL_OUTCOME_RECYCLED: &str = "recycled";
|
||||
pub const POOL_OUTCOME_DROPPED: &str = "dropped";
|
||||
@@ -84,6 +84,29 @@ const EVENT_DISK_LOCAL_ACCESS_FAILED: &str = "disk_local_access_failed";
|
||||
const EVENT_DISK_LOCAL_VOLUME_SETUP_FAILED: &str = "disk_local_volume_setup_failed";
|
||||
const EVENT_DISK_LOCAL_FORMAT_DECODE_FAILED: &str = "disk_local_format_decode_failed";
|
||||
|
||||
/// Enable O_DIRECT for large sequential reads.
|
||||
/// When enabled, shard reads bypass the page cache using O_DIRECT flag.
|
||||
/// Requires aligned buffers (typically 512 bytes or 4096 bytes).
|
||||
/// Default: false (uses page cache via mmap/pread).
|
||||
const ENV_RUSTFS_OBJECT_DIRECT_IO_READ_ENABLE: &str = "RUSTFS_OBJECT_DIRECT_IO_READ_ENABLE";
|
||||
const DEFAULT_RUSTFS_OBJECT_DIRECT_IO_READ_ENABLE: bool = false;
|
||||
|
||||
/// Minimum shard size threshold for O_DIRECT reads.
|
||||
/// Only shards larger than this threshold will use O_DIRECT.
|
||||
/// Default: 4MB.
|
||||
const ENV_RUSTFS_OBJECT_DIRECT_IO_READ_THRESHOLD: &str = "RUSTFS_OBJECT_DIRECT_IO_READ_THRESHOLD";
|
||||
const DEFAULT_RUSTFS_OBJECT_DIRECT_IO_READ_THRESHOLD: usize = 4 * 1024 * 1024;
|
||||
|
||||
/// Check if O_DIRECT reads are enabled.
|
||||
fn is_direct_io_read_enabled() -> bool {
|
||||
rustfs_utils::get_env_bool(ENV_RUSTFS_OBJECT_DIRECT_IO_READ_ENABLE, DEFAULT_RUSTFS_OBJECT_DIRECT_IO_READ_ENABLE)
|
||||
}
|
||||
|
||||
/// Get the O_DIRECT read threshold size.
|
||||
fn get_direct_io_read_threshold() -> usize {
|
||||
rustfs_utils::get_env_usize(ENV_RUSTFS_OBJECT_DIRECT_IO_READ_THRESHOLD, DEFAULT_RUSTFS_OBJECT_DIRECT_IO_READ_THRESHOLD)
|
||||
}
|
||||
|
||||
fn log_startup_disk_io_error(stage: &str, path: &Path, err: &IoError) {
|
||||
warn!(
|
||||
event = EVENT_DISK_LOCAL_STARTUP_CLEANUP,
|
||||
@@ -4982,7 +5005,16 @@ mod test {
|
||||
#[test]
|
||||
fn should_reclaim_file_cache_after_read_respects_env_and_threshold() {
|
||||
temp_env::with_var_unset(rustfs_config::ENV_OBJECT_FILE_CACHE_RECLAIM_READ_ENABLE, || {
|
||||
assert!(!should_reclaim_file_cache_after_read(8 * 1024 * 1024));
|
||||
temp_env::with_var_unset(rustfs_config::ENV_OBJECT_FILE_CACHE_RECLAIM_THRESHOLD, || {
|
||||
assert!(should_reclaim_file_cache_after_read(8 * 1024 * 1024));
|
||||
assert!(!should_reclaim_file_cache_after_read(1024));
|
||||
});
|
||||
});
|
||||
|
||||
temp_env::with_var(rustfs_config::ENV_OBJECT_FILE_CACHE_RECLAIM_READ_ENABLE, Some("false"), || {
|
||||
temp_env::with_var(rustfs_config::ENV_OBJECT_FILE_CACHE_RECLAIM_THRESHOLD, Some("4194304"), || {
|
||||
assert!(!should_reclaim_file_cache_after_read(8 * 1024 * 1024));
|
||||
});
|
||||
});
|
||||
|
||||
temp_env::with_var(rustfs_config::ENV_OBJECT_FILE_CACHE_RECLAIM_READ_ENABLE, Some("true"), || {
|
||||
|
||||
@@ -49,6 +49,36 @@ fn get_shard_locality_preference_enabled() -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
/// Number of stripes to prefetch in the legacy decode path.
|
||||
/// When > 1, stripe reads are batched to overlap disk I/O with decode.
|
||||
/// Default: 1 (no prefetch, current behavior).
|
||||
/// Set via environment variable RUSTFS_GET_DECODE_STRIPE_PREFETCH_COUNT.
|
||||
const ENV_RUSTFS_GET_DECODE_STRIPE_PREFETCH_COUNT: &str = "RUSTFS_GET_DECODE_STRIPE_PREFETCH_COUNT";
|
||||
const DEFAULT_RUSTFS_GET_DECODE_STRIPE_PREFETCH_COUNT: usize = 1;
|
||||
|
||||
/// Get the stripe prefetch count from environment variable.
|
||||
fn get_decode_stripe_prefetch_count() -> usize {
|
||||
rustfs_utils::get_env_usize(
|
||||
ENV_RUSTFS_GET_DECODE_STRIPE_PREFETCH_COUNT,
|
||||
DEFAULT_RUSTFS_GET_DECODE_STRIPE_PREFETCH_COUNT,
|
||||
)
|
||||
}
|
||||
|
||||
/// Enable overlapping bitrot verification with stripe decode.
|
||||
/// When enabled, bitrot verification for stripe N+1 runs concurrently
|
||||
/// with decode of stripe N, reducing total pipeline latency.
|
||||
/// Default: false (sequential behavior, current implementation).
|
||||
const ENV_RUSTFS_GET_BITROT_DECODE_OVERLAP_ENABLE: &str = "RUSTFS_GET_BITROT_DECODE_OVERLAP_ENABLE";
|
||||
const DEFAULT_RUSTFS_GET_BITROT_DECODE_OVERLAP_ENABLE: bool = false;
|
||||
|
||||
/// Get whether bitrot-decode overlap is enabled.
|
||||
fn is_bitrot_decode_overlap_enabled() -> bool {
|
||||
rustfs_utils::get_env_bool(
|
||||
ENV_RUSTFS_GET_BITROT_DECODE_OVERLAP_ENABLE,
|
||||
DEFAULT_RUSTFS_GET_BITROT_DECODE_OVERLAP_ENABLE,
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ShardReadCostCounts {
|
||||
local: usize,
|
||||
|
||||
@@ -33,7 +33,7 @@ use tokio::io::{AsyncRead, ReadBuf};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
const ENV_RUSTFS_GET_CODEC_STREAMING_MAX_INFLIGHT: &str = "RUSTFS_GET_CODEC_STREAMING_MAX_INFLIGHT";
|
||||
const DEFAULT_RUSTFS_GET_CODEC_STREAMING_MAX_INFLIGHT: usize = 1;
|
||||
const DEFAULT_RUSTFS_GET_CODEC_STREAMING_MAX_INFLIGHT: usize = 2;
|
||||
const FILL_POLICY_SINGLE_INFLIGHT: &str = "single_inflight";
|
||||
const FILL_POLICY_DUAL_INFLIGHT: &str = "dual_inflight";
|
||||
|
||||
@@ -758,9 +758,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fill_policy_defaults_to_single_inflight() {
|
||||
fn fill_policy_defaults_to_dual_inflight() {
|
||||
with_var(ENV_RUSTFS_GET_CODEC_STREAMING_MAX_INFLIGHT, None::<&str>, || {
|
||||
assert_eq!(FillPolicy::from_env(), FillPolicy::SingleInFlight);
|
||||
assert_eq!(FillPolicy::from_env(), FillPolicy::DualInFlight);
|
||||
});
|
||||
|
||||
with_var(ENV_RUSTFS_GET_CODEC_STREAMING_MAX_INFLIGHT, Some("2"), || {
|
||||
|
||||
@@ -294,32 +294,78 @@ fn record_capacity_scope_if_needed(scope_token: Option<Uuid>, disks: &[Option<Di
|
||||
/// when reading large objects (20-26MB) under high concurrency.
|
||||
///
|
||||
/// Default: 4MB (4 * 1024 * 1024 bytes)
|
||||
/// Get duplex buffer size from environment variable.
|
||||
///
|
||||
/// **Deprecated**: Use `adaptive_duplex_buffer_size()` for object-size-aware sizing.
|
||||
pub fn get_duplex_buffer_size() -> usize {
|
||||
rustfs_utils::get_env_usize(
|
||||
rustfs_config::ENV_OBJECT_DUPLEX_BUFFER_SIZE,
|
||||
rustfs_config::DEFAULT_OBJECT_DUPLEX_BUFFER_SIZE,
|
||||
)
|
||||
}
|
||||
|
||||
/// Get adaptive duplex buffer size based on object size.
|
||||
///
|
||||
/// Smaller objects get smaller buffers to reduce memory waste.
|
||||
/// Larger objects get larger buffers to prevent backpressure.
|
||||
fn adaptive_duplex_buffer_size(object_size: i64) -> usize {
|
||||
const KB: usize = 1024;
|
||||
const MB: usize = 1024 * 1024;
|
||||
match object_size {
|
||||
0..=1_048_576 => 64 * KB, // <= 1MB: 64KB
|
||||
1_048_577..=16_777_216 => MB, // <= 16MB: 1MB
|
||||
16_777_217..=268_435_456 => 4 * MB, // <= 256MB: 4MB
|
||||
_ => 8 * MB, // > 256MB: 8MB
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GET Optimization Configuration
|
||||
//
|
||||
// All GET performance optimization flags are consolidated here.
|
||||
// Each flag uses `OnceLock` for caching — env var changes require process restart.
|
||||
// Each flag has a corresponding `*_ROLLOUT_PCT` for percentage-based gradual rollout.
|
||||
// ============================================================================
|
||||
|
||||
const DISK_ONLINE_TIMEOUT: Duration = Duration::from_secs(1);
|
||||
const DISK_HEALTH_CACHE_TTL: Duration = Duration::from_millis(750);
|
||||
const GET_OBJECT_METADATA_CACHE_TTL: Duration = Duration::from_millis(250);
|
||||
const GET_OBJECT_METADATA_CACHE_MAX_ENTRIES: usize = 1024;
|
||||
|
||||
// --- Codec Streaming Configuration ---
|
||||
|
||||
const ENV_RUSTFS_GET_CODEC_STREAMING_ENABLE: &str = "RUSTFS_GET_CODEC_STREAMING_ENABLE";
|
||||
const DEFAULT_RUSTFS_GET_CODEC_STREAMING_ENABLE: bool = false; // Disabled until rollout gates are ready
|
||||
|
||||
const ENV_RUSTFS_GET_CODEC_STREAMING_MIN_SIZE: &str = "RUSTFS_GET_CODEC_STREAMING_MIN_SIZE";
|
||||
const DEFAULT_RUSTFS_GET_CODEC_STREAMING_MIN_SIZE: usize = MI_B;
|
||||
|
||||
const ENV_RUSTFS_GET_CODEC_STREAMING_ENGINE: &str = "RUSTFS_GET_CODEC_STREAMING_ENGINE";
|
||||
const DEFAULT_RUSTFS_GET_CODEC_STREAMING_ENGINE: &str = GET_CODEC_STREAMING_ENGINE_LEGACY;
|
||||
|
||||
const ENV_RUSTFS_GET_CODEC_STREAMING_ROLLOUT: &str = "RUSTFS_GET_CODEC_STREAMING_ROLLOUT";
|
||||
const DEFAULT_RUSTFS_GET_CODEC_STREAMING_ROLLOUT: &str = "off";
|
||||
|
||||
const ENV_RUSTFS_GET_CODEC_STREAMING_BODY_COMPAT_CONFIRMED: &str = "RUSTFS_GET_CODEC_STREAMING_BODY_COMPAT_CONFIRMED";
|
||||
const ENV_RUSTFS_GET_CODEC_STREAMING_HEADER_COMPAT_CONFIRMED: &str = "RUSTFS_GET_CODEC_STREAMING_HEADER_COMPAT_CONFIRMED";
|
||||
const DEFAULT_RUSTFS_GET_CODEC_STREAMING_ENABLE: bool = false;
|
||||
const DEFAULT_RUSTFS_GET_CODEC_STREAMING_MIN_SIZE: usize = MI_B;
|
||||
const DEFAULT_RUSTFS_GET_CODEC_STREAMING_ENGINE: &str = GET_CODEC_STREAMING_ENGINE_LEGACY;
|
||||
const DEFAULT_RUSTFS_GET_CODEC_STREAMING_ROLLOUT: &str = "off";
|
||||
const ENV_RUSTFS_GET_METADATA_EARLY_STOP_ENABLE: &str = "RUSTFS_GET_METADATA_EARLY_STOP_ENABLE";
|
||||
const DEFAULT_RUSTFS_GET_METADATA_EARLY_STOP_ENABLE: bool = false;
|
||||
|
||||
const ENV_RUSTFS_GET_CODEC_STREAMING_ROLLOUT_PCT: &str = "RUSTFS_GET_CODEC_STREAMING_ROLLOUT_PCT";
|
||||
const DEFAULT_RUSTFS_GET_CODEC_STREAMING_ROLLOUT_PCT: u32 = 100;
|
||||
|
||||
const ENV_RUSTFS_GET_CODEC_STREAMING_MULTIPART_ENABLE: &str = "RUSTFS_GET_CODEC_STREAMING_MULTIPART_ENABLE";
|
||||
const DEFAULT_RUSTFS_GET_CODEC_STREAMING_MULTIPART_ENABLE: bool = false;
|
||||
|
||||
// --- Metadata Early-Stop Configuration ---
|
||||
|
||||
const ENV_RUSTFS_GET_METADATA_EARLY_STOP_ENABLE: &str = "RUSTFS_GET_METADATA_EARLY_STOP_ENABLE";
|
||||
const DEFAULT_RUSTFS_GET_METADATA_EARLY_STOP_ENABLE: bool = false;
|
||||
|
||||
const ENV_RUSTFS_GET_METADATA_EARLY_STOP_ROLLOUT_PCT: &str = "RUSTFS_GET_METADATA_EARLY_STOP_ROLLOUT_PCT";
|
||||
const DEFAULT_RUSTFS_GET_METADATA_EARLY_STOP_ROLLOUT_PCT: u32 = 100;
|
||||
|
||||
const ENV_RUSTFS_GET_METADATA_VERSION_EARLY_STOP_ENABLE: &str = "RUSTFS_GET_METADATA_VERSION_EARLY_STOP_ENABLE";
|
||||
const DEFAULT_RUSTFS_GET_METADATA_VERSION_EARLY_STOP_ENABLE: bool = false;
|
||||
|
||||
static OBJECT_LOCK_DIAG_ENABLED: OnceLock<bool> = OnceLock::new();
|
||||
|
||||
mod heal;
|
||||
@@ -396,20 +442,46 @@ pub fn is_deadlock_detection_enabled() -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GET Optimization Flag Functions
|
||||
//
|
||||
// All functions use `OnceLock` for caching. Environment variable changes
|
||||
// require process restart to take effect.
|
||||
// ============================================================================
|
||||
|
||||
/// Check if codec streaming is enabled (base flag).
|
||||
///
|
||||
/// **Note**: Cached via `OnceLock` — env var changes require process restart.
|
||||
/// In test mode, bypasses cache to allow per-test env var overrides.
|
||||
fn is_get_codec_streaming_enabled() -> bool {
|
||||
#[cfg(test)]
|
||||
let enabled = rustfs_utils::get_env_bool(ENV_RUSTFS_GET_CODEC_STREAMING_ENABLE, DEFAULT_RUSTFS_GET_CODEC_STREAMING_ENABLE);
|
||||
#[cfg(not(test))]
|
||||
static CACHED: OnceLock<bool> = OnceLock::new();
|
||||
#[cfg(not(test))]
|
||||
let enabled = *CACHED.get_or_init(|| {
|
||||
{
|
||||
rustfs_utils::get_env_bool(ENV_RUSTFS_GET_CODEC_STREAMING_ENABLE, DEFAULT_RUSTFS_GET_CODEC_STREAMING_ENABLE)
|
||||
});
|
||||
enabled
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
{
|
||||
static CACHED: OnceLock<bool> = OnceLock::new();
|
||||
*CACHED.get_or_init(|| {
|
||||
rustfs_utils::get_env_bool(ENV_RUSTFS_GET_CODEC_STREAMING_ENABLE, DEFAULT_RUSTFS_GET_CODEC_STREAMING_ENABLE)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// **Note**: Cached via `OnceLock` — env var changes require process restart.
|
||||
/// Check if multipart codec streaming is enabled.
|
||||
///
|
||||
/// When enabled, multipart objects use per-part codec streaming
|
||||
/// instead of falling back to the legacy duplex path.
|
||||
fn is_codec_streaming_multipart_enabled() -> bool {
|
||||
static CACHED: OnceLock<bool> = OnceLock::new();
|
||||
*CACHED.get_or_init(|| {
|
||||
rustfs_utils::get_env_bool(
|
||||
ENV_RUSTFS_GET_CODEC_STREAMING_MULTIPART_ENABLE,
|
||||
DEFAULT_RUSTFS_GET_CODEC_STREAMING_MULTIPART_ENABLE,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if metadata early-stop is enabled (base flag).
|
||||
fn is_get_metadata_early_stop_enabled() -> bool {
|
||||
static CACHED: OnceLock<bool> = OnceLock::new();
|
||||
*CACHED.get_or_init(|| {
|
||||
@@ -417,31 +489,22 @@ fn is_get_metadata_early_stop_enabled() -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
/// Determine if an optimization should be enabled for a specific request.
|
||||
/// Check if version-aware early-stop is enabled.
|
||||
///
|
||||
/// Uses a stable hash of `(bucket, object)` to ensure the same object
|
||||
/// always gets consistent behavior. This enables percentage-based gradual rollout.
|
||||
///
|
||||
/// **Note**: `base_enabled` must be pre-cached (via `OnceLock`) to avoid
|
||||
/// per-request `std::env::var()` calls.
|
||||
fn is_optimization_enabled_for_request(base_enabled: bool, rollout_pct: u32, bucket: &str, object: &str) -> bool {
|
||||
if !base_enabled || rollout_pct == 0 {
|
||||
return false;
|
||||
}
|
||||
if rollout_pct >= 100 {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Stable hash: same (bucket, object) always produces the same result
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
bucket.hash(&mut hasher);
|
||||
object.hash(&mut hasher);
|
||||
let hash = hasher.finish() % 100;
|
||||
|
||||
(hash as u32) < rollout_pct
|
||||
/// When enabled, versioned requests can early-stop when the requested
|
||||
/// version_id reaches quorum across disks.
|
||||
fn is_version_early_stop_enabled() -> bool {
|
||||
static CACHED: OnceLock<bool> = OnceLock::new();
|
||||
*CACHED.get_or_init(|| {
|
||||
rustfs_utils::get_env_bool(
|
||||
ENV_RUSTFS_GET_METADATA_VERSION_EARLY_STOP_ENABLE,
|
||||
DEFAULT_RUSTFS_GET_METADATA_VERSION_EARLY_STOP_ENABLE,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// --- Rollout Percentage Functions ---
|
||||
|
||||
fn get_codec_streaming_rollout_pct() -> u32 {
|
||||
static CACHED: OnceLock<u32> = OnceLock::new();
|
||||
*CACHED.get_or_init(|| {
|
||||
@@ -459,6 +522,30 @@ fn get_metadata_early_stop_rollout_pct() -> u32 {
|
||||
})
|
||||
}
|
||||
|
||||
// --- Request-Level Decision Functions ---
|
||||
|
||||
/// Determine if an optimization should be enabled for a specific request.
|
||||
///
|
||||
/// Uses a stable hash of `(bucket, object)` to ensure the same object
|
||||
/// always gets consistent behavior. This enables percentage-based gradual rollout.
|
||||
fn is_optimization_enabled_for_request(base_enabled: bool, rollout_pct: u32, bucket: &str, object: &str) -> bool {
|
||||
if !base_enabled || rollout_pct == 0 {
|
||||
return false;
|
||||
}
|
||||
if rollout_pct >= 100 {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Stable hash: same (bucket, object) always produces the same result
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
bucket.hash(&mut hasher);
|
||||
object.hash(&mut hasher);
|
||||
let hash = hasher.finish() % 100;
|
||||
|
||||
(hash as u32) < rollout_pct
|
||||
}
|
||||
|
||||
/// Should this specific request use codec streaming?
|
||||
pub fn should_use_codec_streaming(bucket: &str, object: &str) -> bool {
|
||||
let base = is_get_codec_streaming_enabled();
|
||||
@@ -1545,7 +1632,7 @@ impl crate::storage_api_contracts::object::ObjectIO for SetDisks {
|
||||
|
||||
rustfs_io_metrics::record_get_object_reader_path(GET_OBJECT_PATH_LEGACY_DUPLEX);
|
||||
|
||||
let duplex_buffer_size = get_duplex_buffer_size();
|
||||
let duplex_buffer_size = adaptive_duplex_buffer_size(object_info.size);
|
||||
let (rd, wd) = tokio::io::duplex(duplex_buffer_size);
|
||||
debug!(bucket, object, duplex_buffer_size, "Created duplex pipe for object data transfer");
|
||||
|
||||
|
||||
@@ -17,12 +17,12 @@ use crate::diagnostics::get::{
|
||||
GET_METADATA_EARLY_STOP_REASON_CONFLICTING_METADATA, GET_METADATA_EARLY_STOP_REASON_DELETE_MARKER,
|
||||
GET_METADATA_EARLY_STOP_REASON_ERROR, GET_METADATA_EARLY_STOP_REASON_INSUFFICIENT_QUORUM,
|
||||
GET_METADATA_EARLY_STOP_REASON_NOT_FOUND, GET_METADATA_EARLY_STOP_REASON_UNSAFE_REQUEST,
|
||||
GET_METADATA_EARLY_STOP_REASON_VALID_QUORUM, GET_METADATA_EARLY_STOP_REASON_VERSION_NOT_FOUND, GET_METADATA_RESPONSE_CORRUPT,
|
||||
GET_METADATA_RESPONSE_DISK_NOT_FOUND, GET_METADATA_RESPONSE_ERROR, GET_METADATA_RESPONSE_IGNORED,
|
||||
GET_METADATA_RESPONSE_NOT_FOUND, GET_METADATA_RESPONSE_TIMEOUT, GET_METADATA_RESPONSE_VALID,
|
||||
GET_METADATA_RESPONSE_VERSION_NOT_FOUND, GET_OBJECT_PATH_CODEC_STREAMING, GET_OBJECT_PATH_LEGACY_DUPLEX, GET_STAGE_DECODE,
|
||||
GET_STAGE_RANGE, GET_STAGE_READER_SETUP, GetObjectFailureReason, classify_disk_error, record_get_object_pipeline_failure,
|
||||
record_get_object_pipeline_failure_for_path,
|
||||
GET_METADATA_EARLY_STOP_REASON_VALID_QUORUM, GET_METADATA_EARLY_STOP_REASON_VERSION_MATCH_QUORUM,
|
||||
GET_METADATA_EARLY_STOP_REASON_VERSION_NOT_FOUND, GET_METADATA_RESPONSE_CORRUPT, GET_METADATA_RESPONSE_DISK_NOT_FOUND,
|
||||
GET_METADATA_RESPONSE_ERROR, GET_METADATA_RESPONSE_IGNORED, GET_METADATA_RESPONSE_NOT_FOUND, GET_METADATA_RESPONSE_TIMEOUT,
|
||||
GET_METADATA_RESPONSE_VALID, GET_METADATA_RESPONSE_VERSION_NOT_FOUND, GET_OBJECT_PATH_CODEC_STREAMING,
|
||||
GET_OBJECT_PATH_LEGACY_DUPLEX, GET_STAGE_DECODE, GET_STAGE_RANGE, GET_STAGE_READER_SETUP, GetObjectFailureReason,
|
||||
classify_disk_error, record_get_object_pipeline_failure, record_get_object_pipeline_failure_for_path,
|
||||
};
|
||||
use crate::erasure::coding::BitrotReader;
|
||||
use crate::io_support::bitrot::create_deferred_bitrot_reader;
|
||||
@@ -204,6 +204,9 @@ struct MetadataQuorumAccumulator {
|
||||
candidate_votes: usize,
|
||||
conflicting_metadata: bool,
|
||||
delete_marker_seen: bool,
|
||||
delete_marker_votes: usize,
|
||||
requested_version_id: String,
|
||||
matching_version_votes: usize,
|
||||
}
|
||||
|
||||
impl MetadataQuorumAccumulator {
|
||||
@@ -221,9 +224,17 @@ impl MetadataQuorumAccumulator {
|
||||
candidate_votes: 0,
|
||||
conflicting_metadata: false,
|
||||
delete_marker_seen: false,
|
||||
delete_marker_votes: 0,
|
||||
requested_version_id: String::new(),
|
||||
matching_version_votes: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn with_requested_version_id(mut self, version_id: &str) -> Self {
|
||||
self.requested_version_id = version_id.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
fn observe_file_info(&mut self, file_info: &FileInfo) {
|
||||
if !file_info.is_valid() {
|
||||
self.hard_errors = self.hard_errors.saturating_add(1);
|
||||
@@ -231,7 +242,17 @@ impl MetadataQuorumAccumulator {
|
||||
}
|
||||
|
||||
self.valid_responses = self.valid_responses.saturating_add(1);
|
||||
|
||||
// Track version match for versioned requests
|
||||
if !self.requested_version_id.is_empty()
|
||||
&& let Some(ref vid) = file_info.version_id
|
||||
&& vid.to_string() == self.requested_version_id
|
||||
{
|
||||
self.matching_version_votes = self.matching_version_votes.saturating_add(1);
|
||||
}
|
||||
|
||||
if file_info.deleted {
|
||||
self.delete_marker_votes = self.delete_marker_votes.saturating_add(1);
|
||||
self.delete_marker_seen = true;
|
||||
return;
|
||||
}
|
||||
@@ -271,6 +292,11 @@ impl MetadataQuorumAccumulator {
|
||||
if !self.allow_early_stop {
|
||||
return None;
|
||||
}
|
||||
if self.delete_marker_votes >= self.missing_response_quorum() {
|
||||
return Some(MetadataEarlyStopDecision {
|
||||
reason: GET_METADATA_EARLY_STOP_REASON_DELETE_MARKER,
|
||||
});
|
||||
}
|
||||
if self.conflicting_metadata
|
||||
|| self.delete_marker_seen
|
||||
|| self.not_found_responses > 0
|
||||
@@ -292,6 +318,27 @@ impl MetadataQuorumAccumulator {
|
||||
None
|
||||
}
|
||||
|
||||
/// Check if a versioned request can early-stop because the requested
|
||||
/// version_id has reached quorum across disks.
|
||||
fn version_early_stop_decision(&self) -> Option<MetadataEarlyStopDecision> {
|
||||
if self.requested_version_id.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if self.matching_version_votes >= self.read_quorum_for_version() {
|
||||
return Some(MetadataEarlyStopDecision {
|
||||
reason: GET_METADATA_EARLY_STOP_REASON_VERSION_MATCH_QUORUM,
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Compute the read quorum threshold for version-aware early-stop.
|
||||
/// Uses `total_disks / 2` (like `missing_response_quorum`) when
|
||||
/// `default_parity_count` is set, otherwise requires all disks.
|
||||
fn read_quorum_for_version(&self) -> usize {
|
||||
self.missing_response_quorum()
|
||||
}
|
||||
|
||||
fn final_miss_reason(&self) -> &'static str {
|
||||
if !self.allow_early_stop {
|
||||
return GET_METADATA_EARLY_STOP_REASON_UNSAFE_REQUEST;
|
||||
@@ -1110,7 +1157,9 @@ impl SetDisks {
|
||||
default_parity_count: usize,
|
||||
) -> disk::error::Result<(Vec<FileInfo>, Vec<Option<DiskError>>, MetadataFanoutDiagnostics)> {
|
||||
let early_stop_enabled = observe && is_get_metadata_early_stop_enabled();
|
||||
let allow_early_stop = early_stop_enabled && version_id.is_empty() && !healing && !incl_free_versions;
|
||||
let allow_early_stop = observe
|
||||
&& ((is_get_metadata_early_stop_enabled() && version_id.is_empty() && !healing && !incl_free_versions)
|
||||
|| (is_version_early_stop_enabled() && !version_id.is_empty() && !healing));
|
||||
if allow_early_stop {
|
||||
return Self::read_all_fileinfo_early_stop(
|
||||
disks,
|
||||
@@ -1245,7 +1294,8 @@ impl SetDisks {
|
||||
let mut ress = vec![FileInfo::default(); disks.len()];
|
||||
let mut errors = vec![None; disks.len()];
|
||||
let mut observations = Vec::with_capacity(disks.len());
|
||||
let mut accumulator = MetadataQuorumAccumulator::new(disks.len(), default_parity_count, true);
|
||||
let mut accumulator =
|
||||
MetadataQuorumAccumulator::new(disks.len(), default_parity_count, true).with_requested_version_id(version_id);
|
||||
let opts = Arc::new(ReadOptions {
|
||||
incl_free_versions,
|
||||
read_data,
|
||||
@@ -1299,7 +1349,10 @@ impl SetDisks {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(decision) = accumulator.early_stop_decision() {
|
||||
if let Some(decision) = accumulator
|
||||
.early_stop_decision()
|
||||
.or_else(|| accumulator.version_early_stop_decision())
|
||||
{
|
||||
let saved_responses = join_set.len();
|
||||
join_set.abort_all();
|
||||
rustfs_io_metrics::record_get_object_metadata_early_stop_hit(GET_OBJECT_PATH_LEGACY_DUPLEX, decision.reason);
|
||||
@@ -3014,7 +3067,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_quorum_accumulator_falls_back_on_delete_marker_latest() {
|
||||
fn metadata_quorum_accumulator_hits_delete_marker_quorum_early_stop() {
|
||||
let mut accumulator = metadata_early_stop_accumulator();
|
||||
let mut deleted = metadata_early_stop_candidate("object", 1);
|
||||
deleted.deleted = true;
|
||||
@@ -3022,6 +3075,22 @@ mod tests {
|
||||
accumulator.observe_file_info(&deleted);
|
||||
accumulator.observe_file_info(&deleted);
|
||||
|
||||
assert_eq!(
|
||||
accumulator.early_stop_decision(),
|
||||
Some(MetadataEarlyStopDecision {
|
||||
reason: GET_METADATA_EARLY_STOP_REASON_DELETE_MARKER
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_quorum_accumulator_falls_back_on_delete_marker_below_quorum() {
|
||||
let mut accumulator = metadata_early_stop_accumulator();
|
||||
let mut deleted = metadata_early_stop_candidate("object", 1);
|
||||
deleted.deleted = true;
|
||||
|
||||
accumulator.observe_file_info(&deleted);
|
||||
|
||||
assert!(accumulator.early_stop_decision().is_none());
|
||||
assert_eq!(accumulator.final_miss_reason(), GET_METADATA_EARLY_STOP_REASON_DELETE_MARKER);
|
||||
}
|
||||
@@ -3092,6 +3161,101 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_early_stop_gate_defaults_to_disabled() {
|
||||
temp_env::with_var(ENV_RUSTFS_GET_METADATA_VERSION_EARLY_STOP_ENABLE, None::<&str>, || {
|
||||
assert!(!is_version_early_stop_enabled());
|
||||
});
|
||||
}
|
||||
|
||||
fn version_early_stop_candidate(object: &str, disk_index: usize, version_id: Uuid) -> FileInfo {
|
||||
let mut fi = metadata_early_stop_candidate(object, disk_index);
|
||||
fi.version_id = Some(version_id);
|
||||
fi
|
||||
}
|
||||
|
||||
fn version_early_stop_accumulator(requested_version_id: &str) -> MetadataQuorumAccumulator {
|
||||
MetadataQuorumAccumulator::new(4, 2, true).with_requested_version_id(requested_version_id)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_early_stop_hits_quorum_with_matching_versions() {
|
||||
let vid = Uuid::new_v4();
|
||||
let mut accumulator = version_early_stop_accumulator(&vid.to_string());
|
||||
|
||||
accumulator.observe_file_info(&version_early_stop_candidate("object", 1, vid));
|
||||
assert!(accumulator.version_early_stop_decision().is_none());
|
||||
accumulator.observe_file_info(&version_early_stop_candidate("object", 2, vid));
|
||||
|
||||
assert_eq!(
|
||||
accumulator.version_early_stop_decision(),
|
||||
Some(MetadataEarlyStopDecision {
|
||||
reason: GET_METADATA_EARLY_STOP_REASON_VERSION_MATCH_QUORUM
|
||||
})
|
||||
);
|
||||
assert_eq!(accumulator.matching_version_votes, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_early_stop_does_not_fire_without_requested_version() {
|
||||
let vid = Uuid::new_v4();
|
||||
let mut accumulator = version_early_stop_accumulator("");
|
||||
|
||||
accumulator.observe_file_info(&version_early_stop_candidate("object", 1, vid));
|
||||
accumulator.observe_file_info(&version_early_stop_candidate("object", 2, vid));
|
||||
|
||||
assert!(accumulator.version_early_stop_decision().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_early_stop_does_not_fire_with_mismatched_versions() {
|
||||
let requested_vid = Uuid::new_v4();
|
||||
let other_vid = Uuid::new_v4();
|
||||
let mut accumulator = version_early_stop_accumulator(&requested_vid.to_string());
|
||||
|
||||
accumulator.observe_file_info(&version_early_stop_candidate("object", 1, requested_vid));
|
||||
accumulator.observe_file_info(&version_early_stop_candidate("object", 2, other_vid));
|
||||
|
||||
assert!(accumulator.version_early_stop_decision().is_none());
|
||||
assert_eq!(accumulator.matching_version_votes, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_early_stop_does_not_fire_below_quorum() {
|
||||
let vid = Uuid::new_v4();
|
||||
let mut accumulator = version_early_stop_accumulator(&vid.to_string());
|
||||
|
||||
accumulator.observe_file_info(&version_early_stop_candidate("object", 1, vid));
|
||||
|
||||
assert!(accumulator.version_early_stop_decision().is_none());
|
||||
assert_eq!(accumulator.matching_version_votes, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_early_stop_tracks_matching_votes_independently_of_candidate() {
|
||||
let requested_vid = Uuid::new_v4();
|
||||
let mut accumulator = version_early_stop_accumulator(&requested_vid.to_string());
|
||||
|
||||
// Two valid responses with matching version_id but different erasure.index
|
||||
// (so they conflict on the candidate path but still count for version votes)
|
||||
let mut fi1 = version_early_stop_candidate("object", 1, requested_vid);
|
||||
fi1.size = 100;
|
||||
let mut fi2 = version_early_stop_candidate("object", 2, requested_vid);
|
||||
fi2.size = 200; // different size → conflicting metadata on candidate path
|
||||
|
||||
accumulator.observe_file_info(&fi1);
|
||||
accumulator.observe_file_info(&fi2);
|
||||
|
||||
// Candidate path sees conflict, but version path sees quorum
|
||||
assert!(accumulator.early_stop_decision().is_none());
|
||||
assert_eq!(
|
||||
accumulator.version_early_stop_decision(),
|
||||
Some(MetadataEarlyStopDecision {
|
||||
reason: GET_METADATA_EARLY_STOP_REASON_VERSION_MATCH_QUORUM
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
fn codec_streaming_test_object_info(fi: &FileInfo) -> ObjectInfo {
|
||||
ObjectInfo::from_file_info(fi, "bucket", "object", false)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ use rustfs_heal::heal::{
|
||||
use serial_test::serial;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, Once, OnceLock},
|
||||
sync::{Arc, Once},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::fs;
|
||||
@@ -59,7 +59,6 @@ async fn wait_for_path_exists(path: &Path, timeout: Duration, interval: Duration
|
||||
}
|
||||
}
|
||||
|
||||
static GLOBAL_ENV: OnceLock<(Vec<PathBuf>, Arc<ECStore>, Arc<ECStoreHealStorage>)> = OnceLock::new();
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
pub fn init_tracing() {
|
||||
@@ -76,11 +75,6 @@ pub fn init_tracing() {
|
||||
async fn setup_test_env() -> (Vec<PathBuf>, Arc<ECStore>, Arc<ECStoreHealStorage>) {
|
||||
init_tracing();
|
||||
|
||||
// Fast path: already initialized, just clone and return
|
||||
if let Some((paths, ecstore, heal_storage)) = GLOBAL_ENV.get() {
|
||||
return (paths.clone(), ecstore.clone(), heal_storage.clone());
|
||||
}
|
||||
|
||||
// create temp dir as 4 disks with unique base dir
|
||||
let test_base_dir = format!("/tmp/rustfs_heal_heal_test_{}", uuid::Uuid::new_v4());
|
||||
let temp_dir = std::path::PathBuf::from(&test_base_dir);
|
||||
@@ -126,9 +120,9 @@ async fn setup_test_env() -> (Vec<PathBuf>, Arc<ECStore>, Arc<ECStoreHealStorage
|
||||
// format disks (only first time)
|
||||
init_local_disks(endpoint_pools.clone()).await.unwrap();
|
||||
|
||||
// create ECStore with dynamic port 0 (let OS assign) or fixed 9001 if free
|
||||
let port = 9001; // for simplicity
|
||||
let server_addr: std::net::SocketAddr = format!("127.0.0.1:{port}").parse().unwrap();
|
||||
// Use port 0 so nextest can run this integration binary in parallel
|
||||
// with other ECStore-backed tests without sharing a fixed peer port.
|
||||
let server_addr: std::net::SocketAddr = "127.0.0.1:0".parse().unwrap();
|
||||
let ecstore = ECStore::new(server_addr, endpoint_pools, CancellationToken::new())
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -147,9 +141,6 @@ async fn setup_test_env() -> (Vec<PathBuf>, Arc<ECStore>, Arc<ECStoreHealStorage
|
||||
// Create heal storage layer
|
||||
let heal_storage = Arc::new(ECStoreHealStorage::new(ecstore.clone()));
|
||||
|
||||
// Store in global once lock
|
||||
let _ = GLOBAL_ENV.set((disk_paths.clone(), ecstore.clone(), heal_storage.clone()));
|
||||
|
||||
(disk_paths, ecstore, heal_storage)
|
||||
}
|
||||
|
||||
|
||||
@@ -947,6 +947,31 @@ pub fn record_get_object_metadata_phase_duration(duration_secs: f64) {
|
||||
record_get_object_stage_duration("legacy_duplex", "metadata", duration_secs);
|
||||
}
|
||||
|
||||
/// Record metadata phase duration with early-stop state label.
|
||||
#[inline(always)]
|
||||
pub fn record_get_object_metadata_phase_duration_with_early_stop(duration_secs: f64, early_stop_active: &'static str) {
|
||||
if !get_stage_metrics_enabled() {
|
||||
return;
|
||||
}
|
||||
histogram!(
|
||||
"rustfs_io_get_object_stage_duration_seconds",
|
||||
"path" => "legacy_duplex",
|
||||
"stage" => "metadata",
|
||||
"early_stop_active" => early_stop_active
|
||||
)
|
||||
.record(duration_secs);
|
||||
}
|
||||
|
||||
/// Record GET object total duration with reader path label.
|
||||
#[inline(always)]
|
||||
pub fn record_get_object_total_duration_with_path(duration_secs: f64, reader_path: &'static str) {
|
||||
histogram!(
|
||||
"rustfs_io_get_object_total_duration_seconds_with_path",
|
||||
"reader_path" => reader_path
|
||||
)
|
||||
.record(duration_secs);
|
||||
}
|
||||
|
||||
/// Record GetObject shard reader setup duration.
|
||||
#[inline(always)]
|
||||
pub fn record_get_object_shard_reader_setup_duration(duration_secs: f64) {
|
||||
@@ -1075,6 +1100,34 @@ pub fn record_bytes_pool_hit_rate(tier: &str, hit_rate: f64) {
|
||||
gauge!("rustfs_bytes_pool_hit_rate", "tier" => tier.to_string()).set(hit_rate * 100.0);
|
||||
}
|
||||
|
||||
/// Record a BytesPool buffer acquisition attempt.
|
||||
///
|
||||
/// `outcome` = `"hit"` when a buffer is available in the pool, `"miss"` when the
|
||||
/// pool is empty and a new allocation is required.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `tier` - Pool tier ("small", "medium", "large", "xlarge")
|
||||
/// * `outcome` - Acquisition outcome ("hit" or "miss")
|
||||
#[inline(always)]
|
||||
pub fn record_bytespool_acquisition(tier: &'static str, outcome: &'static str) {
|
||||
counter!("rustfs_io_bytespool_acquisition_total", "tier" => tier, "outcome" => outcome).increment(1);
|
||||
}
|
||||
|
||||
/// Record a BytesPool buffer return attempt.
|
||||
///
|
||||
/// `outcome` = `"recycled"` when the buffer is successfully returned to the
|
||||
/// pool, `"dropped"` when `try_lock` fails and the buffer is deallocated.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `tier` - Pool tier ("small", "medium", "large", "xlarge")
|
||||
/// * `outcome` - Return outcome ("recycled" or "dropped")
|
||||
#[inline(always)]
|
||||
pub fn record_bytespool_return(tier: &'static str, outcome: &'static str) {
|
||||
counter!("rustfs_io_bytespool_return_total", "tier" => tier, "outcome" => outcome).increment(1);
|
||||
}
|
||||
|
||||
/// Record zero-copy write operation.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -1564,6 +1617,21 @@ mod tests {
|
||||
record_bytes_pool_hit_rate("small", 0.85);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_record_bytespool_acquisition_and_return() {
|
||||
// Acquisition outcomes
|
||||
record_bytespool_acquisition("small", "hit");
|
||||
record_bytespool_acquisition("medium", "miss");
|
||||
record_bytespool_acquisition("large", "hit");
|
||||
record_bytespool_acquisition("xlarge", "miss");
|
||||
|
||||
// Return outcomes
|
||||
record_bytespool_return("small", "recycled");
|
||||
record_bytespool_return("medium", "dropped");
|
||||
record_bytespool_return("large", "recycled");
|
||||
record_bytespool_return("xlarge", "dropped");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_record_zero_copy_write() {
|
||||
record_zero_copy_write(1024, 10.5);
|
||||
@@ -1612,6 +1680,8 @@ mod tests {
|
||||
record_get_object_full_body_latency("s3_handler", 0.009);
|
||||
record_get_object_response_handoff_duration("s3_handler", 0.0001);
|
||||
record_get_object_metadata_phase_duration(0.002);
|
||||
record_get_object_metadata_phase_duration_with_early_stop(0.002, "hit");
|
||||
record_get_object_total_duration_with_path(0.050, "legacy_duplex");
|
||||
record_get_object_shard_reader_setup_duration(0.003);
|
||||
record_get_object_decode_duration(0.004);
|
||||
record_get_object_duplex_backpressure_duration(0.005);
|
||||
@@ -1747,6 +1817,8 @@ mod tests {
|
||||
record_get_object_first_byte_latency("s3_handler", 0.008);
|
||||
record_get_object_full_body_latency("s3_handler", 0.009);
|
||||
record_get_object_response_handoff_duration("s3_handler", 0.0001);
|
||||
record_get_object_metadata_phase_duration_with_early_stop(0.002, "hit");
|
||||
record_get_object_total_duration_with_path(0.050, "legacy_duplex");
|
||||
record_get_object_shard_reader_setup_duration(0.003);
|
||||
record_get_object_decode_duration(0.004);
|
||||
record_get_object_duplex_backpressure_duration(0.005);
|
||||
|
||||
Reference in New Issue
Block a user