fix: address correctness, safety, and concurrency issues (#2327)

Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
houseme
2026-03-30 00:30:57 +08:00
committed by GitHub
parent 860a37d3a8
commit 7172e151de
90 changed files with 20397 additions and 1228 deletions
+21 -4
View File
@@ -20,6 +20,7 @@ license.workspace = true
repository.workspace = true
rust-version.workspace = true
homepage.workspace = true
default-run = "rustfs"
description = "RustFS is a high-performance, distributed file system designed for modern cloud-native applications, providing efficient data storage and retrieval with advanced features like S3 Select, IAM, and policy management."
keywords.workspace = true
categories.workspace = true
@@ -30,15 +31,24 @@ documentation = "https://docs.rustfs.com/"
name = "rustfs"
path = "src/main.rs"
[[bin]]
name = "manual-test-dial9"
path = "tests/manual/test_dial9.rs"
test = false
bench = false
required-features = ["manual-test-runners"]
[features]
default = ["metrics"]
metrics = []
metrics-gpu = ["metrics", "rustfs-metrics/gpu"]
default = ["direct-io"]
metrics-gpu = ["rustfs-metrics/gpu"]
ftps = ["rustfs-protocols/ftps"]
swift = ["rustfs-protocols/swift"]
webdav = ["rustfs-protocols/webdav"]
license = []
full = ["metrics", "metrics-gpu", "ftps", "swift", "webdav"]
direct-io = [] # Aligned direct I/O reader support (uses aligned pread, does not set O_DIRECT)
io-scheduler-debug = [] # Enable debug information in I/O scheduler
full = ["metrics-gpu", "ftps", "swift", "webdav", "direct-io"]
manual-test-runners = []
[lints]
workspace = true
@@ -73,6 +83,9 @@ rustfs-targets = { workspace = true }
rustfs-trusted-proxies = { workspace = true }
rustfs-utils = { workspace = true, features = ["full"] }
rustfs-zip = { workspace = true }
rustfs-io-core = { workspace = true }
rustfs-io-metrics = { workspace = true }
rustfs-concurrency = { workspace = true }
rustfs-scanner = { workspace = true }
# Async Runtime and Networking
@@ -147,6 +160,8 @@ aes-gcm = { workspace = true }
metrics = { workspace = true }
opentelemetry = { workspace = true }
tracing-opentelemetry = { workspace = true }
# Data structures
hashbrown = { workspace = true }
[target.'cfg(target_os = "linux")'.dependencies]
libsystemd.workspace = true
@@ -154,6 +169,8 @@ libsystemd.workspace = true
[target.'cfg(not(all(target_os = "linux", target_env = "gnu", target_arch = "x86_64")))'.dependencies]
mimalloc = { workspace = true }
# Only enable pprof-based profiling on non-Windows targets.
[target.'cfg(all(not(target_os = "windows"), not(all(target_os = "linux", target_env = "gnu", target_arch = "x86_64"))))'.dependencies]
starshard = { workspace = true }
File diff suppressed because it is too large Load Diff
+120 -50
View File
@@ -582,6 +582,82 @@ struct FeatureInfoJson {
description: &'static str,
}
struct FeatureSpec {
name: &'static str,
enabled: bool,
description: &'static str,
dependencies: &'static str,
default_enabled: bool,
}
fn feature_specs() -> [FeatureSpec; 9] {
[
FeatureSpec {
name: "direct-io",
enabled: cfg!(feature = "direct-io"),
description: "Aligned pread-based direct I/O reader support",
dependencies: "(none)",
default_enabled: true,
},
FeatureSpec {
name: "metrics-gpu",
enabled: cfg!(feature = "metrics-gpu"),
description: "Metrics GPU support",
dependencies: "rustfs-metrics/gpu",
default_enabled: false,
},
FeatureSpec {
name: "ftps",
enabled: cfg!(feature = "ftps"),
description: "FTPS protocol support",
dependencies: "rustfs-protocols/ftps",
default_enabled: false,
},
FeatureSpec {
name: "swift",
enabled: cfg!(feature = "swift"),
description: "Swift storage backend",
dependencies: "rustfs-protocols/swift",
default_enabled: false,
},
FeatureSpec {
name: "webdav",
enabled: cfg!(feature = "webdav"),
description: "WebDAV protocol support",
dependencies: "rustfs-protocols/webdav",
default_enabled: false,
},
FeatureSpec {
name: "license",
enabled: cfg!(feature = "license"),
description: "License validation",
dependencies: "(none)",
default_enabled: false,
},
FeatureSpec {
name: "io-scheduler-debug",
enabled: cfg!(feature = "io-scheduler-debug"),
description: "Enable debug information in I/O scheduler",
dependencies: "(none)",
default_enabled: false,
},
FeatureSpec {
name: "manual-test-runners",
enabled: cfg!(feature = "manual-test-runners"),
description: "Enable manual test binaries",
dependencies: "(none)",
default_enabled: false,
},
FeatureSpec {
name: "full",
enabled: cfg!(feature = "full"),
description: "All features enabled",
dependencies: "metrics-gpu + ftps + swift + webdav + direct-io",
default_enabled: false,
},
]
}
/// Dependency information for JSON output
#[derive(Serialize)]
struct DepsInfoJson {
@@ -591,38 +667,14 @@ struct DepsInfoJson {
}
fn collect_deps_info_json() -> DepsInfoJson {
let features = vec![
FeatureInfoJson {
name: "metrics",
enabled: cfg!(feature = "metrics"),
description: "Metrics collection and reporting",
},
FeatureInfoJson {
name: "ftps",
enabled: cfg!(feature = "ftps"),
description: "FTPS protocol support",
},
FeatureInfoJson {
name: "swift",
enabled: cfg!(feature = "swift"),
description: "Swift storage backend",
},
FeatureInfoJson {
name: "webdav",
enabled: cfg!(feature = "webdav"),
description: "WebDAV protocol support",
},
FeatureInfoJson {
name: "license",
enabled: cfg!(feature = "license"),
description: "License validation",
},
FeatureInfoJson {
name: "full",
enabled: cfg!(feature = "full"),
description: "All features enabled",
},
];
let features: Vec<FeatureInfoJson> = feature_specs()
.into_iter()
.map(|feature| FeatureInfoJson {
name: feature.name,
enabled: feature.enabled,
description: feature.description,
})
.collect();
let enabled_count = features.iter().filter(|f| f.enabled).count();
let total_count = features.len();
@@ -758,17 +810,9 @@ fn get_workload_profile_info() -> String {
/// Dependency information
fn format_deps_info() -> String {
// Check which features are enabled at compile time
let features = [
("metrics", cfg!(feature = "metrics"), "Metrics collection and reporting"),
("ftps", cfg!(feature = "ftps"), "FTPS protocol support"),
("swift", cfg!(feature = "swift"), "Swift storage backend"),
("webdav", cfg!(feature = "webdav"), "WebDAV protocol support"),
("license", cfg!(feature = "license"), "License validation"),
("full", cfg!(feature = "full"), "All features enabled"),
];
let features = feature_specs();
let enabled_count = features.iter().filter(|(_, enabled, _)| *enabled).count();
let enabled_count = features.iter().filter(|feature| feature.enabled).count();
let mut output = format!(
"## Build Features\n\n\
@@ -782,23 +826,24 @@ fn format_deps_info() -> String {
output.push_str("### Feature Status\n\n");
output.push_str("| Feature | Status | Description |\n");
output.push_str("|---------|--------|-------------|\n");
for (name, enabled, description) in features {
let status = if enabled { "" } else { "" };
output.push_str(&format!("| {} | {} | {} |\n", name, status, description));
for feature in &features {
let status = if feature.enabled { "" } else { "" };
output.push_str(&format!("| {} | {} | {} |\n", feature.name, status, feature.description));
}
output.push_str("\n### Default Features\n\n");
output.push_str("| Feature | Note |\n");
output.push_str("|---------|------|\n");
output.push_str("| metrics | enabled by default |\n");
for feature in features.iter().filter(|feature| feature.default_enabled) {
output.push_str(&format!("| {} | enabled by default |\n", feature.name));
}
output.push_str("\n### Feature Dependencies\n\n");
output.push_str("| Feature | Dependencies |\n");
output.push_str("|---------|-------------|\n");
output.push_str("| full | metrics + ftps + swift + webdav |\n");
output.push_str("| ftps | rustfs-protocols/ftps |\n");
output.push_str("| swift | rustfs-protocols/swift |\n");
output.push_str("| webdav | rustfs-protocols/webdav |\n");
for feature in &features {
output.push_str(&format!("| {} | {} |\n", feature.name, feature.dependencies));
}
output
}
@@ -875,4 +920,29 @@ mod tests {
let info = RuntimeInfo::collect();
assert!(info.process_id > 0);
}
#[test]
fn test_collect_deps_info_json_matches_cargo_features() {
let info = collect_deps_info_json();
let feature_names: Vec<_> = info.features.iter().map(|feature| feature.name).collect();
assert_eq!(info.total_count, 9);
assert_eq!(info.features.len(), 9);
assert!(feature_names.contains(&"direct-io"));
assert!(feature_names.contains(&"metrics-gpu"));
assert!(feature_names.contains(&"io-scheduler-debug"));
assert!(feature_names.contains(&"manual-test-runners"));
assert!(!feature_names.contains(&"metrics"));
}
#[test]
fn test_format_deps_info_matches_cargo_feature_output() {
let output = format_deps_info();
assert!(output.contains("| metrics-gpu |"));
assert!(output.contains("| io-scheduler-debug |"));
assert!(output.contains("| manual-test-runners |"));
assert!(output.contains("| direct-io | enabled by default |"));
assert!(output.contains("| full | metrics-gpu + ftps + swift + webdav + direct-io |"));
}
}
+46
View File
@@ -414,6 +414,52 @@ where
shutdown_tx
}
/// Starts the auto-tuner for performance optimization if enabled via environment variable.
///
/// The auto-tuner reads `RUSTFS_AUTOTUNER_ENABLED` to decide whether to run.
/// When enabled, it spawns a background task that tunes concurrency settings
/// every 60 seconds.
pub async fn init_auto_tuner(ctx: tokio_util::sync::CancellationToken) {
use crate::storage::concurrency::get_concurrency_manager;
use rustfs_io_metrics::AutoTuner;
use rustfs_io_metrics::TunerConfig;
use tracing::{debug, error, info};
let autotuner_enabled = rustfs_utils::get_env_bool("RUSTFS_AUTOTUNER_ENABLED", false);
if autotuner_enabled {
info!(target: "rustfs::main::run", "Starting auto-tuner for performance optimization");
let config = TunerConfig::default();
let manager = get_concurrency_manager();
let performance_metrics = manager.performance_metrics();
tokio::spawn(async move {
let mut tuner = AutoTuner::with_config(config).with_metrics(performance_metrics);
loop {
tokio::select! {
_ = ctx.cancelled() => {
info!(target: "rustfs::autotuner", "Auto-tuner shutting down");
break;
}
_ = tokio::time::sleep(tokio::time::Duration::from_secs(60)) => {
if let Err(e) = tuner.tune().await {
error!(target: "rustfs::autotuner", "Auto-tuner iteration failed: {}", e);
} else {
debug!(target: "rustfs::autotuner", "Auto-tuner iteration completed");
}
}
}
}
});
info!(target: "rustfs::main::run", "Auto-tuner started successfully");
} else {
info!(target: "rustfs::main::run", "Auto-tuner disabled (set RUSTFS_AUTOTUNER_ENABLED=true to enable)");
}
}
/// Initialize the FTP system
///
/// This function initializes the FTP server (non-encrypted) if enabled in the configuration.
+3
View File
@@ -564,6 +564,9 @@ async fn run(config: config::Config) -> Result<()> {
if rustfs_obs::observability_metric_enabled() {
// Initialize metrics system
init_metrics_system(ctx.clone());
// Initialize auto-tuner for performance optimization (optional)
crate::init::init_auto_tuner(ctx.clone()).await;
}
info!(
-5
View File
@@ -45,7 +45,6 @@ use std::time::Instant;
use tokio::io::{DuplexStream, duplex};
use tracing::{debug, warn};
#[cfg(feature = "metrics")]
use metrics::counter;
/// Backpressure pipe configuration.
@@ -281,7 +280,6 @@ impl BackpressurePipe {
if usage >= threshold && !self.state.load(Ordering::Relaxed) {
self.state.store(true, Ordering::Relaxed);
#[cfg(feature = "metrics")]
counter!("rustfs.backpressure.events.total", "state" => "high_watermark").increment(1);
warn!(
@@ -302,7 +300,6 @@ impl BackpressurePipe {
if usage <= threshold && self.state.load(Ordering::Relaxed) {
self.state.store(false, Ordering::Relaxed);
#[cfg(feature = "metrics")]
counter!("rustfs.backpressure.events.total", "state" => "normal").increment(1);
debug!(
@@ -409,7 +406,6 @@ impl BackpressureMonitor {
if usage >= high {
if !self.in_high_watermark.swap(true, Ordering::Relaxed) {
#[cfg(feature = "metrics")]
counter!("rustfs.backpressure.events.total", "state" => "high_watermark").increment(1);
debug!(usage_percent = self.usage_percent() as u32, "Backpressure: entered high watermark");
@@ -417,7 +413,6 @@ impl BackpressureMonitor {
BackpressureState::HighWatermark
} else if usage <= low {
if self.in_high_watermark.swap(false, Ordering::Relaxed) {
#[cfg(feature = "metrics")]
counter!("rustfs.backpressure.events.total", "state" => "normal").increment(1);
debug!(usage_percent = self.usage_percent() as u32, "Backpressure: returned to normal");
File diff suppressed because it is too large Load Diff
+578 -43
View File
@@ -15,12 +15,18 @@
//! Concurrency manager for coordinating concurrent GetObject requests.
use super::io_schedule::{
IoLoadLevel, IoLoadMetrics, IoPriority, IoPriorityQueue, IoPriorityQueueConfig, IoQueueStatus, IoStrategy,
IoLoadLevel, IoLoadMetrics, IoPriority, IoPriorityQueue, IoPriorityQueueConfig, IoQueueStatus, IoSchedulerConfig, IoStrategy,
get_advanced_buffer_size,
};
use super::object_cache::{CacheStats, CachedGetObject, CachedObject, HotObjectCache};
use super::object_cache::{CacheStats, CachedGetObject, TieredObjectCache, WarmupPattern};
use super::request_guard::GetObjectGuard;
use rustfs_concurrency::{GetObjectCacheEligibility, 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};
use rustfs_io_metrics::bandwidth::{BandwidthMonitor, BandwidthSnapshot};
use rustfs_io_metrics::global_metrics::get_global_metrics;
use rustfs_io_metrics::{MetricsCollector, PerformanceMetrics};
use std::sync::{Arc, LazyLock, Mutex};
use std::time::Duration;
use tokio::sync::Semaphore;
@@ -31,8 +37,8 @@ pub(crate) static CONCURRENCY_MANAGER: LazyLock<ConcurrencyManager> = LazyLock::
#[derive(Clone)]
pub struct ConcurrencyManager {
/// Hot object cache for frequently accessed objects
cache: Arc<HotObjectCache>,
/// 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)
@@ -42,6 +48,19 @@ pub struct ConcurrencyManager {
/// I/O priority queue for request scheduling
#[allow(dead_code)]
priority_queue: Arc<IoPriorityQueue<()>>,
/// Bytes pool for buffer allocation and reuse
bytes_pool: Arc<BytesPool>,
// Enhanced scheduler state
/// I/O scheduler configuration (cached at initialization)
scheduler_config: IoSchedulerConfig,
/// Detected storage media type
storage_media: StorageMedia,
/// I/O pattern detector for sequential/random access tracking
pattern_detector: Arc<Mutex<IoPatternDetector>>,
/// Bandwidth monitor for adaptive I/O sizing
bandwidth_monitor: Arc<Mutex<BandwidthMonitor>>,
/// Metrics collector for I/O latency tracking (P50, P95, P99)
metrics_collector: Arc<MetricsCollector>,
}
impl std::fmt::Debug for ConcurrencyManager {
@@ -52,10 +71,21 @@ impl std::fmt::Debug for ConcurrencyManager {
} else {
"locked".to_string()
};
let bandwidth_info = if let Ok(monitor) = self.bandwidth_monitor.lock() {
format!("{:?}", monitor.snapshot())
} else {
"locked".to_string()
};
f.debug_struct("ConcurrencyManager")
.field("active_requests", &super::io_schedule::ACTIVE_GET_REQUESTS.load(Ordering::Relaxed))
.field(
"active_requests",
&crate::storage::concurrency::io_schedule::ACTIVE_GET_REQUESTS.load(Ordering::Relaxed),
)
.field("disk_read_permits", &self.disk_read_semaphore.available_permits())
.field("io_metrics", &io_metrics_info)
.field("storage_media", &self.storage_media)
.field("bandwidth", &bandwidth_info)
.field("bytes_pool", &self.bytes_pool)
.finish()
}
}
@@ -64,22 +94,78 @@ impl ConcurrencyManager {
/// Create a new concurrency manager with default settings
///
/// Reads configuration from environment variables:
/// - `RUSTFS_OBJECT_CACHE_ENABLE`: Enable/disable object caching (default: false)
/// - `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 max_disk_reads = rustfs_utils::get_env_usize(
rustfs_config::ENV_OBJECT_MAX_CONCURRENT_DISK_READS,
rustfs_config::DEFAULT_OBJECT_MAX_CONCURRENT_DISK_READS,
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,
scheduler_config.sequential_step_tolerance_bytes,
)));
// Initialize bandwidth monitor
let bandwidth_monitor = Arc::new(Mutex::new(BandwidthMonitor::new(
scheduler_config.bandwidth_ema_beta,
scheduler_config.bandwidth_low_threshold_bps,
scheduler_config.bandwidth_high_threshold_bps,
)));
// Use global performance metrics instance for consistent metrics tracking
// This allows AutoTuner and other components to access the same metrics data
let performance_metrics = get_global_metrics();
// Initialize metrics collector for I/O latency tracking
// Keep 1000 samples for P95/P99 calculation
let metrics_collector = Arc::new(MetricsCollector::new(performance_metrics.clone(), 1000));
// Build priority queue config
let queue_config = IoPriorityQueueConfig {
queue_high_capacity: scheduler_config.queue_high_capacity,
queue_normal_capacity: scheduler_config.queue_normal_capacity,
queue_low_capacity: scheduler_config.queue_low_capacity,
starvation_prevention_interval_ms: scheduler_config.starvation_prevention_interval_ms,
starvation_threshold_secs: scheduler_config.starvation_threshold_secs,
};
Self {
cache: Arc::new(HotObjectCache::new()),
cache,
disk_read_semaphore: Arc::new(Semaphore::new(max_disk_reads)),
cache_enabled,
io_metrics: Arc::new(Mutex::new(IoLoadMetrics::new(100))), // Keep last 100 observations
priority_queue: Arc::new(IoPriorityQueue::new(IoPriorityQueueConfig::default())),
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()),
scheduler_config,
storage_media,
pattern_detector,
bandwidth_monitor,
metrics_collector,
}
}
@@ -104,14 +190,25 @@ impl ConcurrencyManager {
/// Try to get an object from cache
pub async fn get_cached(&self, key: &str) -> Option<Arc<Vec<u8>>> {
self.cache.get(key).await
self.cache.get_bytes(key).await
}
/// Cache an object for future retrievals
pub async fn cache_object(&self, key: String, data: Vec<u8>) {
let size = data.len();
let cached_obj = Arc::new(CachedObject::new_with_size(data, size));
self.cache.put(key, cached_obj).await;
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
/// reusable buffers for I/O operations, reducing allocation overhead.
///
/// # Returns
///
/// Arc-wrapped BytesPool instance
pub fn bytes_pool(&self) -> Arc<BytesPool> {
self.bytes_pool.clone()
}
/// Acquire a permit to perform a disk read operation
@@ -138,13 +235,57 @@ impl ConcurrencyManager {
if let Ok(mut metrics) = self.io_metrics.lock() {
metrics.record(wait_duration);
}
}
// Record histogram metric for Prometheus
#[cfg(all(feature = "metrics", not(test)))]
{
use metrics::histogram;
histogram!("rustfs.disk.permit.wait.duration.seconds").record(wait_duration.as_secs_f64());
}
// ============================================
// Metrics Collection Methods
// ============================================
/// Record a disk I/O operation for latency tracking.
///
/// This method delegates to MetricsCollector which:
/// 1. Updates atomic counters in PerformanceMetrics
/// 2. Records latency for P95/P99 calculation
/// 3. Reports to metrics crate (which exports to OTEL)
///
/// # Arguments
///
/// * `bytes` - Number of bytes transferred
/// * `duration` - Duration of the I/O operation
/// * `is_read` - true for read operations, false for writes
///
/// # Example
///
/// ```rust,ignore
/// let manager = get_concurrency_manager();
/// let start = Instant::now();
/// // ... perform disk I/O ...
/// let duration = start.elapsed();
/// manager.record_disk_operation(1024 * 1024, duration, true).await;
/// ```
pub async fn record_disk_operation(&self, bytes: u64, duration: Duration, is_read: bool) {
self.metrics_collector.record_io_operation(bytes, duration, is_read).await;
}
/// Get a reference to the metrics collector for external use.
///
/// # Returns
///
/// Arc-wrapped MetricsCollector instance
pub fn metrics_collector(&self) -> &Arc<MetricsCollector> {
&self.metrics_collector
}
/// Get the global performance metrics instance.
///
/// This provides access to the shared PerformanceMetrics that is used
/// across all components, including AutoTuner.
///
/// # Returns
///
/// Arc-wrapped PerformanceMetrics instance
pub fn performance_metrics(&self) -> Arc<PerformanceMetrics> {
get_global_metrics()
}
/// Calculate an adaptive I/O strategy based on disk permit wait time.
@@ -179,6 +320,85 @@ impl ConcurrencyManager {
IoStrategy::from_wait_duration(permit_wait_duration, base_buffer_size)
}
/// Calculate I/O strategy with enhanced multi-factor context.
///
/// This method integrates storage media, access patterns, bandwidth observations,
/// and concurrent request count to provide a more sophisticated I/O strategy.
///
/// # Arguments
///
/// * `file_size` - Size of the file/object being read (-1 if unknown)
/// * `base_buffer_size` - Base buffer size from workload configuration
/// * `permit_wait_duration` - Time spent waiting for disk read permit
/// * `is_sequential_hint` - Whether the access pattern is known to be sequential
///
/// # Returns
///
/// An `IoStrategy` with optimized parameters based on all available factors.
///
/// # Example
///
/// ```ignore
/// let strategy = manager.calculate_io_strategy_with_context(
/// file_size,
/// 256 * 1024,
/// permit_wait_duration,
/// false,
/// );
/// let optimal_buffer = strategy.buffer_size;
/// let enable_readahead = strategy.enable_readahead;
/// ```
pub fn calculate_io_strategy_with_context(
&self,
file_size: i64,
base_buffer_size: usize,
permit_wait_duration: Duration,
is_sequential_hint: bool,
) -> IoStrategy {
use crate::storage::concurrency::io_schedule::IoSchedulingContext;
// Record the observation for future smoothing
self.record_permit_wait(permit_wait_duration);
// Get current access pattern
let access_pattern = if let Ok(detector) = self.pattern_detector.lock() {
detector.current_pattern()
} else {
AccessPattern::Unknown
};
// Get current bandwidth snapshot
let observed_bandwidth_bps = if let Ok(monitor) = self.bandwidth_monitor.lock() {
let snapshot = monitor.snapshot();
if snapshot.tier == rustfs_io_metrics::bandwidth::BandwidthTier::Unknown {
None
} else {
Some(snapshot.bytes_per_second)
}
} else {
None
};
// Get concurrent request count
let concurrent_requests =
crate::storage::concurrency::io_schedule::ACTIVE_GET_REQUESTS.load(std::sync::atomic::Ordering::Relaxed);
// Build scheduling context
let context = IoSchedulingContext {
file_size,
base_buffer_size,
permit_wait_duration,
is_sequential_hint,
access_pattern,
storage_media: self.storage_media,
observed_bandwidth_bps,
concurrent_requests,
};
// Calculate strategy using multi-factor approach
IoStrategy::from_context_with_config(&context, &self.scheduler_config)
}
/// Get the smoothed I/O load level based on recent observations.
///
/// This uses the rolling window of permit wait times to provide a more
@@ -242,9 +462,78 @@ impl ConcurrencyManager {
buffer_size.clamp(32 * KI_B, MI_B)
}
// ============================================
// Enhanced I/O Scheduling Methods
// ============================================
/// Record an I/O access for pattern detection.
///
/// This updates the pattern detector with the offset and size of an access,
/// allowing it to distinguish between sequential and random access patterns.
///
/// # Arguments
///
/// * `offset` - File offset being accessed
/// * `len` - Length of the access
pub fn record_access(&self, offset: u64, len: u64) {
if let Ok(mut detector) = self.pattern_detector.lock() {
detector.record(offset, len);
}
}
/// Get the current access pattern.
///
/// Returns the detected access pattern (Sequential, Random, Mixed, or Unknown).
pub fn current_access_pattern(&self) -> AccessPattern {
if let Ok(detector) = self.pattern_detector.lock() {
detector.current_pattern()
} else {
AccessPattern::Unknown
}
}
/// Record a data transfer for bandwidth monitoring.
///
/// This updates the bandwidth monitor with the bytes transferred and duration,
/// allowing it to maintain an EMA (Exponential Moving Average) of the observed bandwidth.
///
/// # Arguments
///
/// * `bytes` - Number of bytes transferred
/// * `duration` - Duration of the transfer
pub fn record_transfer(&self, bytes: u64, duration: Duration) {
if let Ok(mut monitor) = self.bandwidth_monitor.lock() {
monitor.record_transfer(bytes, duration);
}
}
/// Get the current bandwidth snapshot.
///
/// Returns a snapshot of the current bandwidth including bytes per second and tier.
pub fn current_bandwidth_snapshot(&self) -> BandwidthSnapshot {
if let Ok(monitor) = self.bandwidth_monitor.lock() {
monitor.snapshot()
} else {
BandwidthSnapshot {
bytes_per_second: 0,
tier: rustfs_io_metrics::bandwidth::BandwidthTier::Unknown,
}
}
}
/// Get the detected storage media type.
pub fn storage_media(&self) -> StorageMedia {
self.storage_media
}
/// Get the scheduler configuration.
pub fn scheduler_config(&self) -> &IoSchedulerConfig {
&self.scheduler_config
}
/// Get cache statistics
pub async fn cache_stats(&self) -> CacheStats {
self.cache.stats().await
self.cache.stats_as_hot_cache().await
}
/// Clear all cached objects
@@ -252,6 +541,13 @@ impl ConcurrencyManager {
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
@@ -259,17 +555,18 @@ impl ConcurrencyManager {
/// 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(keys).await
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
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)> {
self.cache.get_hot_keys(limit).await
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
@@ -282,7 +579,55 @@ impl ConcurrencyManager {
/// 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>)>) {
self.cache.warm(objects).await;
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
@@ -459,31 +804,32 @@ impl ConcurrencyManager {
// Unknown size, use normal priority
IoPriority::Normal
} else {
IoPriority::from_size(request_size)
// Use cached scheduler config thresholds
IoPriority::from_size_with_thresholds(
request_size,
self.scheduler_config.high_priority_size_threshold,
self.scheduler_config.low_priority_size_threshold,
)
}
}
/// Check if priority scheduling is enabled.
pub fn is_priority_scheduling_enabled(&self) -> bool {
rustfs_utils::get_env_bool(
rustfs_config::ENV_OBJECT_PRIORITY_SCHEDULING_ENABLE,
rustfs_config::DEFAULT_OBJECT_PRIORITY_SCHEDULING_ENABLE,
)
self.scheduler_config.enable_priority
}
/// Get current I/O queue status for monitoring.
///
/// Returns information about permit usage and waiting requests.
pub fn io_queue_status(&self) -> IoQueueStatus {
let total_permits = rustfs_utils::get_env_usize(
rustfs_config::ENV_OBJECT_MAX_CONCURRENT_DISK_READS,
rustfs_config::DEFAULT_OBJECT_MAX_CONCURRENT_DISK_READS,
let snapshot = GetObjectQueueSnapshot::from_available_permits(
self.scheduler_config.max_concurrent_reads,
self.disk_read_semaphore.available_permits(),
);
let permits_in_use = total_permits.saturating_sub(self.disk_read_semaphore.available_permits());
IoQueueStatus {
total_permits,
permits_in_use,
total_permits: snapshot.total_permits,
permits_in_use: snapshot.permits_in_use,
high_priority_waiting: 0, // Would need additional tracking
normal_priority_waiting: 0,
low_priority_waiting: 0,
@@ -511,11 +857,7 @@ impl ConcurrencyManager {
&self,
priority: IoPriority,
) -> Result<tokio::sync::SemaphorePermit<'_>, tokio::sync::AcquireError> {
#[cfg(feature = "metrics")]
{
use metrics::counter;
counter!("rustfs.disk.read.queue.total", "priority" => priority.as_str()).increment(1);
}
rustfs_io_metrics::record_io_priority_assignment(priority.as_str());
debug!(
priority = %priority,
@@ -526,6 +868,26 @@ 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
@@ -714,4 +1076,177 @@ mod integration_tests {
assert!(size1 > 0);
assert!(size1 <= 2 * 1024 * 1024); // Not more than 2MB
}
// ============================================
// Multi-Factor Strategy Integration Tests
// ============================================
#[tokio::test]
#[serial]
async fn test_concurrency_manager_multi_factor_strategy_nvme_optimal() {
let manager = ConcurrencyManager::new();
// Simulate optimal conditions: Unknown/SSD + Sequential + Low load
let file_size = 100 * 1024 * 1024; // 100MB
let base_buffer = 256 * 1024;
let permit_wait = Duration::from_millis(5); // Low load
let is_sequential = true;
let strategy = manager.calculate_io_strategy_with_context(file_size, base_buffer, permit_wait, is_sequential);
let media = manager.storage_media();
// Verify basic optimizations work
assert_eq!(strategy.storage_media, media);
assert!(strategy.buffer_size >= base_buffer * 8 / 10, "Sequential should maintain or boost buffer");
let expected_readahead = !matches!(media, StorageMedia::Hdd);
assert_eq!(
strategy.enable_readahead, expected_readahead,
"Readahead should follow storage profile preference under low load"
);
assert_eq!(strategy.load_level, IoLoadLevel::Low);
}
#[tokio::test]
#[serial]
async fn test_concurrency_manager_multi_factor_strategy_access_pattern_tracking() {
let manager = ConcurrencyManager::new();
// Record sequential accesses
for offset in [0, 1024, 2048, 3072, 4096] {
manager.record_access(offset, 1024);
}
// Check pattern detection
let pattern = manager.current_access_pattern();
assert_eq!(pattern, AccessPattern::Sequential);
// Record random accesses
for offset in [0, 10 * 1024, 100 * 1024, 5 * 1024 * 1024] {
manager.record_access(offset, 1024);
}
// Pattern should change to mixed or random
let pattern_after = manager.current_access_pattern();
assert!(!matches!(pattern_after, AccessPattern::Sequential));
}
#[tokio::test]
#[serial]
async fn test_concurrency_manager_multi_factor_strategy_bandwidth_recording() {
let manager = ConcurrencyManager::new();
// Simulate transfer
let bytes = 10 * 1024 * 1024; // 10MB
let duration = Duration::from_millis(100); // 100ms = 100MB/s
manager.record_transfer(bytes, duration);
// Check bandwidth snapshot (returns BandwidthSnapshot directly)
let snapshot = manager.current_bandwidth_snapshot();
assert!(snapshot.bytes_per_second > 0, "Should have bandwidth data after recording");
}
#[tokio::test]
#[serial]
async fn test_concurrency_manager_multi_factor_strategy_compatibility() {
let manager = ConcurrencyManager::new();
// Test that old API still works
let old_strategy = manager.calculate_io_strategy(Duration::from_millis(50), 256 * 1024);
assert!(old_strategy.buffer_size > 0);
// New API with context should also work
let new_strategy =
manager.calculate_io_strategy_with_context(50 * 1024 * 1024, 256 * 1024, Duration::from_millis(50), false);
assert!(new_strategy.buffer_size > 0);
}
#[tokio::test]
#[serial]
async fn test_concurrency_manager_multi_factor_strategy_high_concurrency() {
let manager = ConcurrencyManager::new();
// Simulate high concurrent requests by keeping guards alive
let _guards: Vec<_> = (0..20).map(|_| GetObjectGuard::new()).collect();
let strategy = manager.calculate_io_strategy_with_context(100 * 1024 * 1024, 512 * 1024, Duration::from_millis(10), true);
// High concurrency should reduce buffer
assert!(strategy.concurrent_requests >= manager.scheduler_config().high_concurrency_threshold);
assert!(strategy.buffer_size < 512 * 1024, "High concurrency should reduce buffer");
}
#[tokio::test]
#[serial]
async fn test_concurrency_manager_multi_factor_strategy_buffer_clamp() {
let manager = ConcurrencyManager::new();
let media = manager.storage_media();
let config = manager.scheduler_config();
// Request very large base buffer
let large_base = 16 * 1024 * 1024; // 16MB
let strategy = manager.calculate_io_strategy_with_context(
1024 * 1024, // 1GB file
large_base,
Duration::from_millis(1),
true,
);
let media_cap = match media {
StorageMedia::Nvme => config.nvme_buffer_cap,
StorageMedia::Ssd => config.ssd_buffer_cap,
StorageMedia::Hdd => config.hdd_buffer_cap,
StorageMedia::Unknown => config.ssd_buffer_cap,
};
let expected_max = media_cap.min(MI_B);
// Large base buffer should be constrained by storage cap first, then global clamp.
assert_eq!(
strategy.buffer_size, expected_max,
"Buffer should be capped by media profile and global clamp"
);
}
#[tokio::test]
#[serial]
async fn test_concurrency_manager_multi_factor_strategy_storage_media_detection() {
let manager = ConcurrencyManager::new();
// Check storage media was detected at initialization
let media = manager.storage_media();
// Should be one of the known types (not Unknown unless detection failed)
// We accept Unknown if detection wasn't configured
assert!(matches!(
media,
StorageMedia::Nvme | StorageMedia::Ssd | StorageMedia::Hdd | StorageMedia::Unknown
));
}
#[tokio::test]
#[serial]
async fn test_concurrency_manager_multi_factor_strategy_priority_with_context() {
let manager = ConcurrencyManager::new();
// Test priority is correctly calculated in multi-factor strategy
let small_file_strategy = manager.calculate_io_strategy_with_context(
500 * 1024, // 500KB
256 * 1024,
Duration::from_millis(10),
false,
);
let large_file_strategy = manager.calculate_io_strategy_with_context(
50 * 1024 * 1024, // 50MB
256 * 1024,
Duration::from_millis(10),
false,
);
assert_eq!(small_file_strategy.priority, IoPriority::High);
assert_eq!(large_file_strategy.priority, IoPriority::Low);
}
}
+64 -2
View File
@@ -13,8 +13,28 @@
// limitations under the License.
//! Concurrency optimization module for high-performance object retrieval.
//!
//! This module provides concurrency management, I/O scheduling, and object caching
//! 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
//!
//! # Migration Note
//!
//! Core algorithms have been migrated to `rustfs-io-core` and metrics to
//! `rustfs-io-metrics`. This module maintains API compatibility while
//! delegating to the new implementations.
// Sub-modules
// pub mod bandwidth_monitor; // Migrated to rustfs-io-metrics
// pub mod global_metrics; // Migrated to rustfs-io-metrics
// pub mod io_profile; // Migrated to rustfs-io-core
pub mod io_schedule;
pub mod manager;
pub mod object_cache;
@@ -24,11 +44,11 @@ pub mod request_guard;
// Public API Re-exports
// ============================================
// I/O scheduling types
// I/O scheduling types (from io_schedule.rs for backward compatibility)
#[allow(unused_imports)]
pub use io_schedule::{
IO_PRIORITY_METRICS, IoLoadLevel, IoPriority, IoPriorityMetrics, IoPriorityQueue, IoPriorityQueueConfig, IoQueueStatus,
IoStrategy, get_advanced_buffer_size, get_concurrency_aware_buffer_size,
IoSchedulerConfig, IoStrategy, get_advanced_buffer_size, get_buffer_size_opt_in, get_concurrency_aware_buffer_size,
};
// Request tracking
@@ -41,6 +61,24 @@ pub use object_cache::{CacheHealthStatus, CacheStats, CachedGetObject};
// Concurrency manager
pub use manager::ConcurrencyManager;
// ============================================
// New Module Re-exports (for gradual migration)
// ============================================
// Re-export types from rustfs-io-core for convenience
pub use rustfs_io_core::{
// Backpressure types
BackpressureMonitor,
// Deadlock detection types
DeadlockDetector,
// Scheduler types
IoScheduler,
// Lock optimization types
LockOptimizer,
};
// Re-export types from rustfs-io-metrics for convenience
// ============================================
// Helper Functions
// ============================================
@@ -55,3 +93,27 @@ pub fn get_concurrency_manager() -> &'static ConcurrencyManager {
pub fn reset_active_get_requests() {
io_schedule::ACTIVE_GET_REQUESTS.store(0, std::sync::atomic::Ordering::Relaxed);
}
/// Create a new I/O scheduler with default configuration.
#[allow(dead_code)]
pub fn create_io_scheduler() -> IoScheduler {
IoScheduler::with_defaults()
}
/// Create a new backpressure monitor with default configuration.
#[allow(dead_code)]
pub fn create_backpressure_monitor() -> BackpressureMonitor {
BackpressureMonitor::with_defaults()
}
/// Create a new deadlock detector with default configuration.
#[allow(dead_code)]
pub fn create_deadlock_detector() -> DeadlockDetector {
DeadlockDetector::with_defaults()
}
/// Create a new lock optimizer with default configuration.
#[allow(dead_code)]
pub fn create_lock_optimizer() -> LockOptimizer {
LockOptimizer::with_defaults()
}
File diff suppressed because it is too large Load Diff
+36 -12
View File
@@ -18,11 +18,14 @@ use std::sync::atomic::Ordering;
use std::time::Instant;
use super::io_schedule::ACTIVE_GET_REQUESTS;
use rustfs_io_metrics::{record_get_object_request_result, record_get_object_request_start};
/// RAII guard for tracking active GetObject requests.
#[derive(Debug)]
pub struct GetObjectGuard {
start_time: Instant,
/// Final status set by the caller; if None when dropped, reported as "unknown".
result: Option<&'static str>,
}
impl GetObjectGuard {
@@ -30,18 +33,37 @@ impl GetObjectGuard {
pub fn new() -> Self {
ACTIVE_GET_REQUESTS.fetch_add(1, Ordering::Relaxed);
#[cfg(all(feature = "metrics", not(test)))]
if !std::thread::panicking() {
use metrics::counter;
counter!("rustfs.get.object.requests.started").increment(1);
}
// Record metrics for a started GetObject request. Capture the
// concurrent request count AFTER increment to reflect the current
// active requests.
let concurrent = ACTIVE_GET_REQUESTS.load(Ordering::Relaxed);
record_get_object_request_start(concurrent);
Self {
start_time: Instant::now(),
result: None,
}
}
/// Mark the request as completed successfully.
///
/// Call this before the guard is dropped to record the correct status.
pub fn finish_ok(&mut self) {
self.result = Some("ok");
}
/// Mark the request as failed.
///
/// Call this before the guard is dropped to record the correct status.
pub fn finish_err(&mut self) {
self.result = Some("error");
}
/// Get the elapsed time since this guard was created.
#[allow(dead_code)]
// This helper is primarily used by unit tests to assert timing.
// It's intentionally kept public for callers that may want to inspect
// a guard's duration without dropping it.
pub fn elapsed(&self) -> std::time::Duration {
self.start_time.elapsed()
}
@@ -65,6 +87,15 @@ impl Default for GetObjectGuard {
impl Drop for GetObjectGuard {
fn drop(&mut self) {
// Record duration of this request before decrementing the global
// counter. This ensures `start_time` is actually used and the
// `elapsed()` method remains meaningful for tests and callers.
let duration_secs = self.start_time.elapsed().as_secs_f64();
// Use the caller-set status, or "unknown" if the result was never set
// (e.g., the future was cancelled or the guard dropped without explicit completion).
let status = self.result.unwrap_or("unknown");
record_get_object_request_result(status, duration_secs);
if let Err(previous) =
ACTIVE_GET_REQUESTS.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| current.checked_sub(1))
{
@@ -74,13 +105,6 @@ impl Drop for GetObjectGuard {
previous
);
}
#[cfg(all(feature = "metrics", not(test)))]
if !std::thread::panicking() {
use metrics::{counter, histogram};
counter!("rustfs.get.object.requests.completed").increment(1);
histogram!("rustfs.get.object.duration.seconds").record(self.elapsed().as_secs_f64());
}
}
}
@@ -367,9 +367,14 @@ mod tests {
async fn test_moka_cache_eviction() {
let manager = ConcurrencyManager::new();
// Clear cache for clean test state
manager.clear_cache().await;
manager.reset_cache_metrics();
// Cache multiple objects to exceed the limit
let object_size = 6 * MI_B; // 6MB each
let num_objects = 20; // Total 120MB > 100MB limit
// Tiered cache has L1 (50MB) + L2 (200MB) = 250MB total
let object_size = 15 * MI_B; // 15MB each
let num_objects = 20; // Total 300MB > 250MB limit
for i in 0..num_objects {
let key = format!("test/object{i}");
@@ -383,6 +388,7 @@ mod tests {
// Verify cache size is within limit (Moka manages this automatically)
let stats = manager.cache_stats().await;
eprintln!("DEBUG: size={}, max_size={}, entries={}", stats.size, stats.max_size, stats.entries);
assert!(
stats.size <= stats.max_size,
"Moka should keep cache size {} within max {}",
@@ -628,6 +634,10 @@ mod tests {
async fn test_cache_hit_rate() {
let manager = ConcurrencyManager::new();
// Reset metrics for clean test
manager.reset_cache_metrics();
manager.clear_cache().await;
// Cache some objects
for i in 0..5 {
let key = format!("hitrate/object{i}");
@@ -637,6 +647,12 @@ mod tests {
sleep(Duration::from_millis(100)).await;
// Verify objects are cached
for i in 0..5 {
let key = format!("hitrate/object{i}");
assert!(manager.is_cached(&key).await, "Object {} should be cached", key);
}
// Mix of hits and misses
for i in 0..10 {
let key = if i < 5 {
@@ -647,9 +663,9 @@ mod tests {
let _ = manager.get_cached(&key).await;
}
// Hit rate should be around 50%
// Hit rate should be around 50% (0.5 on 0.0-1.0 scale)
let hit_rate = manager.cache_hit_rate();
assert!((40.0..=60.0).contains(&hit_rate), "Hit rate should be ~50%, got {hit_rate:.1}%");
assert!((0.4..=0.6).contains(&hit_rate), "Hit rate should be ~50% (0.5), got {hit_rate:.3}");
}
/// Test TTL expiration (Moka automatic cleanup)
@@ -1029,6 +1045,9 @@ mod tests {
async fn test_cache_invalidation_versioned() {
let manager = ConcurrencyManager::new();
// Clear cache for clean test state
manager.clear_cache().await;
let bucket = "bucket";
let key = "object";
let version_id = "v123";
+12 -2
View File
@@ -17,6 +17,18 @@
//! This module provides deadlock detection capabilities for diagnosing
//! hanging requests and lock contention issues in production systems.
//!
//! # Migration Note
//!
//! This module extends `rustfs_io_core::DeadlockDetector` with request-level
//! resource tracking (memory, file handles). For basic deadlock detection,
//! consider using the io-core version directly:
//!
//! ```ignore
//! // Basic deadlock detection
//! use rustfs_io_core::DeadlockDetector;
//! let detector = DeadlockDetector::with_defaults();
//! ```
//!
//! # Key Features
//!
//! - Request resource tracking (locks, memory, file handles)
@@ -53,7 +65,6 @@ use std::time::{Duration, Instant};
use tokio::sync::broadcast;
use tracing::{debug, error, warn};
#[cfg(feature = "metrics")]
use metrics::counter;
/// Request identifier type.
@@ -453,7 +464,6 @@ impl DeadlockDetector {
if let Some(cycle) = Self::find_cycle(&wait_graph) {
deadlocks_detected.fetch_add(1, Ordering::Relaxed);
#[cfg(feature = "metrics")]
counter!("rustfs.deadlock.detected.total").increment(1);
// Log detailed deadlock information
-1
View File
@@ -187,7 +187,6 @@ pub(crate) fn get_buffer_size_opt_in(file_size: i64) -> usize {
};
// Optional performance metrics collection for monitoring and optimization
#[cfg(feature = "metrics")]
{
use metrics::histogram;
histogram!("rustfs.buffer.size.bytes").record(buffer_size as f64);
+12 -3
View File
@@ -17,6 +17,18 @@
//! This module provides optimized lock management for read operations,
//! reducing lock contention by releasing locks early (after metadata read)
//! rather than holding them for the entire data transfer duration.
//!
//! # Migration Note
//!
//! For new code, consider using `rustfs_io_core::LockOptimizer` which provides
//! the same core functionality with better separation of concerns. This module
//! remains for backward compatibility and storage-specific configuration.
//!
//! ```ignore
//! // Recommended: Use io-core directly
//! use rustfs_io_core::LockOptimizer;
//! let optimizer = LockOptimizer::with_defaults();
//! ```
// Allow dead_code for public API that may be used by external modules or future features
#![allow(dead_code)]
@@ -42,7 +54,6 @@ use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use tracing::debug;
#[cfg(feature = "metrics")]
use metrics::histogram;
/// Lock optimization configuration.
@@ -216,7 +227,6 @@ impl<G> OptimizedLockGuard<G> {
self.stats.record_early_release(hold_time);
#[cfg(feature = "metrics")]
histogram!("rustfs.lock.hold.duration.seconds").record(hold_time.as_secs_f64());
debug!(
@@ -241,7 +251,6 @@ impl<G> Drop for OptimizedLockGuard<G> {
self.stats.record_early_release(hold_time);
#[cfg(feature = "metrics")]
histogram!("rustfs.lock.hold.duration.seconds").record(hold_time.as_secs_f64());
debug!(
+2
View File
@@ -37,6 +37,8 @@ mod ecfs_extend;
mod ecfs_test;
pub(crate) mod head_prefix;
#[cfg(test)]
mod multi_factor_scheduler_integration_test;
#[cfg(test)]
mod sse_test;
pub(crate) use ecfs_extend::*;
@@ -0,0 +1,213 @@
// 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.
//! Integration tests for multi-factor I/O scheduler.
//!
//! These tests verify the enhanced scheduler behavior in realistic scenarios
//! combining storage media, access patterns, bandwidth, and concurrency.
#[cfg(test)]
mod tests {
use crate::storage::concurrency::ConcurrencyManager;
use serial_test::serial;
use std::time::Duration;
/// Test scenario: NVMe sequential read with low load
///
/// Expected behavior: Maximum buffer size, readahead enabled
#[tokio::test]
#[serial]
async fn test_scenario_nvme_sequential_low_load() {
let manager = ConcurrencyManager::new();
let strategy = manager.calculate_io_strategy_with_context(
5 * 1024 * 1024, // 5MB file
256 * 1024, // 256KB base buffer
Duration::from_millis(5), // Low load
true, // Sequential
);
// Verify basic strategy properties
assert!(strategy.buffer_size > 0);
assert_eq!(strategy.load_level.level_index(), 0); // Low
}
/// Test scenario: High concurrency reduces buffer
#[tokio::test]
#[serial]
async fn test_scenario_high_concurrency() {
let manager = ConcurrencyManager::new();
// Low concurrency
let low_strategy = {
let _g1 = ConcurrencyManager::track_request();
let _g2 = ConcurrencyManager::track_request();
manager.calculate_io_strategy_with_context(50 * 1024 * 1024, 512 * 1024, Duration::from_millis(10), true)
};
// High concurrency
let high_strategy = {
let _guards: Vec<_> = (0..16).map(|_| ConcurrencyManager::track_request()).collect();
manager.calculate_io_strategy_with_context(50 * 1024 * 1024, 512 * 1024, Duration::from_millis(10), true)
};
// Buffer should decrease with higher concurrency
assert!(high_strategy.concurrent_requests >= low_strategy.concurrent_requests);
}
/// Test scenario: Progressive load increase
#[tokio::test]
#[serial]
async fn test_scenario_progressive_load() {
let manager = ConcurrencyManager::new();
let file_size = 50 * 1024 * 1024;
let base_buffer = 512 * 1024;
// Low load
let low_strategy = manager.calculate_io_strategy_with_context(file_size, base_buffer, Duration::from_millis(5), true);
// High load
let high_strategy = manager.calculate_io_strategy_with_context(file_size, base_buffer, Duration::from_millis(100), true);
// Critical load
let critical_strategy =
manager.calculate_io_strategy_with_context(file_size, base_buffer, Duration::from_millis(300), true);
// Load levels should increase
assert!(low_strategy.load_level.level_index() < high_strategy.load_level.level_index());
assert!(high_strategy.load_level.level_index() < critical_strategy.load_level.level_index());
// Readahead should be disabled at critical load
assert!(!critical_strategy.enable_readahead);
}
/// Test scenario: Small file gets high priority
#[tokio::test]
#[serial]
async fn test_scenario_small_file_priority() {
let manager = ConcurrencyManager::new();
let strategy = manager.calculate_io_strategy_with_context(
100 * 1024, // 100KB (small)
256 * 1024,
Duration::from_millis(100), // Even under high load
false,
);
// Should be high priority due to size
assert!(strategy.priority.is_high());
}
/// Test scenario: Large file gets low priority
#[tokio::test]
#[serial]
async fn test_scenario_large_file_priority() {
let manager = ConcurrencyManager::new();
let strategy = manager.calculate_io_strategy_with_context(
100 * 1024 * 1024, // 100MB (large)
256 * 1024,
Duration::from_millis(5), // Even under low load
false,
);
// Should be low priority due to size
assert!(strategy.priority.is_low());
}
/// Test scenario: Access pattern tracking
#[tokio::test]
#[serial]
async fn test_scenario_access_pattern_tracking() {
let manager = ConcurrencyManager::new();
// Record sequential accesses
for offset in [0, 1024, 2048, 3072, 4096] {
manager.record_access(offset, 1024);
}
// Should detect sequential pattern
let pattern = manager.current_access_pattern();
assert!(pattern.is_sequential() || pattern.is_unknown());
}
/// Test scenario: Bandwidth recording
#[tokio::test]
#[serial]
async fn test_scenario_bandwidth_recording() {
let manager = ConcurrencyManager::new();
// Record transfer
manager.record_transfer(10 * 1024 * 1024, Duration::from_millis(100));
// Bandwidth snapshot should be available (returns BandwidthSnapshot directly)
let snapshot = manager.current_bandwidth_snapshot();
assert!(snapshot.bytes_per_second > 0);
}
/// Test scenario: Sequential vs random comparison
#[tokio::test]
#[serial]
async fn test_scenario_sequential_vs_random() {
let manager = ConcurrencyManager::new();
let file_size = 50 * 1024 * 1024;
let base_buffer = 512 * 1024;
let wait = Duration::from_millis(20);
let sequential_strategy = manager.calculate_io_strategy_with_context(file_size, base_buffer, wait, true);
let random_strategy = manager.calculate_io_strategy_with_context(file_size, base_buffer, wait, false);
// Sequential should get better (or equal) treatment
assert!(sequential_strategy.buffer_size >= random_strategy.buffer_size);
}
/// Test scenario: Real-world video streaming
#[tokio::test]
#[serial]
async fn test_real_world_video_streaming() {
let manager = ConcurrencyManager::new();
let strategy = manager.calculate_io_strategy_with_context(
500 * 1024 * 1024, // 500MB video
512 * 1024,
Duration::from_millis(25),
true, // Sequential streaming
);
// Should be optimized for streaming
assert!(strategy.buffer_size > 0);
assert_eq!(strategy.load_level.level_index(), 1); // Medium load
}
/// Test scenario: Real-world API config files
#[tokio::test]
#[serial]
async fn test_real_world_api_configs() {
let manager = ConcurrencyManager::new();
let strategy = manager.calculate_io_strategy_with_context(
100 * 1024, // 100KB JSON
256 * 1024,
Duration::from_millis(5),
false, // Random access to different files
);
// Should optimize for low latency
assert!(strategy.priority.is_high());
assert_eq!(strategy.load_level.level_index(), 0); // Low load
}
}
+35 -235
View File
@@ -16,6 +16,25 @@
//!
//! This module provides timeout protection for GetObject requests to prevent
//! indefinite hangs caused by deadlocks, resource exhaustion, or slow I/O.
//!
//! # Migration Note
//!
//! This module extends `rustfs_io_core::RequestTimeoutWrapper` with Tokio
//! cancellation token support. For basic timeout handling without async
//! cancellation, consider using the io-core version:
//!
//! ```ignore
//! // Basic timeout handling
//! use rustfs_io_core::RequestTimeoutWrapper;
//! let wrapper = RequestTimeoutWrapper::new(config);
//! ```
//!
//! # Key Features
//!
//! - Configurable request-level timeout (default 30 seconds)
//! - Automatic cancellation of sub-tasks on timeout
//! - Resource cleanup on timeout (locks, memory, file handles)
//! - Prometheus metrics for timeout monitoring
// Allow dead_code for public API that may be used by external modules or future features
#![allow(dead_code)]
@@ -47,8 +66,7 @@ use std::time::{Duration, Instant};
use tokio_util::sync::CancellationToken;
use tracing::{debug, warn};
#[cfg(feature = "metrics")]
use metrics::{counter, histogram};
// Re-export types from rustfs_io_core for convenience
/// Timeout configuration for GetObject requests.
#[derive(Debug, Clone)]
@@ -190,66 +208,6 @@ pub struct TimeoutInfo {
pub progress_percent: Option<f32>,
}
/// Progress tracking for long-running operations
#[derive(Debug, Clone)]
pub struct OperationProgress {
/// Start time
start_time: Instant,
/// Last progress update time
last_update: Instant,
/// Bytes transferred so far
bytes_transferred: u64,
/// Total object size (if known)
total_size: Option<u64>,
/// Stale timeout - if no progress for this duration, consider stuck
stale_timeout: Duration,
}
impl OperationProgress {
/// Create a new progress tracker
pub fn new(total_size: Option<u64>, stale_timeout: Duration) -> Self {
Self {
start_time: Instant::now(),
last_update: Instant::now(),
bytes_transferred: 0,
total_size,
stale_timeout,
}
}
/// Update progress with new bytes transferred
pub fn update(&mut self, bytes: u64) {
self.bytes_transferred = bytes;
self.last_update = Instant::now();
}
/// Check if progress is stale (no updates for stale_timeout)
pub fn is_stale(&self) -> bool {
self.last_update.elapsed() > self.stale_timeout
}
/// Get progress percentage (0-100)
pub fn progress_percent(&self) -> Option<f32> {
self.total_size.map(|total| {
if total == 0 {
100.0
} else {
(self.bytes_transferred as f32 / total as f32 * 100.0).min(100.0)
}
})
}
/// Get transfer rate in bytes per second
pub fn transfer_rate(&self) -> u64 {
let elapsed = self.start_time.elapsed().as_secs_f64();
if elapsed > 0.0 {
(self.bytes_transferred as f64 / elapsed) as u64
} else {
0
}
}
}
/// Result of a timed GetObject operation.
#[derive(Debug)]
pub enum TimedGetObjectResult<T, E> {
@@ -405,8 +363,7 @@ impl RequestTimeoutWrapper {
);
// Record start time for metrics
#[cfg(feature = "metrics")]
counter!("rustfs.get.object.requests.started").increment(1);
rustfs_io_metrics::record_get_object_request_started();
// Clone cancel_token for the operation, keep original for potential cancellation
let cancel_token_for_op = self.cancel_token.clone();
@@ -416,11 +373,7 @@ impl RequestTimeoutWrapper {
// Operation completed successfully
let elapsed = start_time.elapsed();
#[cfg(feature = "metrics")]
{
counter!("rustfs.get.object.requests.completed").increment(1);
histogram!("rustfs.get.object.duration.seconds").record(elapsed.as_secs_f64());
}
rustfs_io_metrics::record_get_object_request_result("success", elapsed.as_secs_f64());
debug!(
request_id = %request_id,
@@ -434,11 +387,7 @@ impl RequestTimeoutWrapper {
// Operation failed before timeout
let elapsed = start_time.elapsed();
#[cfg(feature = "metrics")]
{
counter!("rustfs.get.object.requests.failed").increment(1);
histogram!("rustfs.get.object.duration.seconds").record(elapsed.as_secs_f64());
}
rustfs_io_metrics::record_get_object_request_result("error", elapsed.as_secs_f64());
debug!(
request_id = %request_id,
@@ -455,11 +404,8 @@ impl RequestTimeoutWrapper {
// Cancel the operation
self.cancel_token.cancel();
#[cfg(feature = "metrics")]
{
counter!("rustfs.get.object.timeout.total").increment(1);
histogram!("rustfs.get.object.duration.seconds").record(elapsed.as_secs_f64());
}
rustfs_io_metrics::record_get_object_timeout(None, Some(elapsed.as_secs_f64()));
rustfs_io_metrics::record_get_object_request_result("timeout", elapsed.as_secs_f64());
warn!(
request_id = %request_id,
@@ -527,8 +473,7 @@ impl RequestTimeoutWrapper {
"Starting timed operation"
);
#[cfg(feature = "metrics")]
counter!("rustfs.get.object.requests.started").increment(1);
rustfs_io_metrics::record_get_object_request_started();
// Clone cancel_token for the operation, keep original for potential cancellation
let cancel_token_for_op = self.cancel_token.clone();
@@ -537,11 +482,7 @@ impl RequestTimeoutWrapper {
Ok(Ok(result)) => {
let elapsed = start_time.elapsed();
#[cfg(feature = "metrics")]
{
counter!("rustfs.get.object.requests.completed").increment(1);
histogram!("rustfs.get.object.duration.seconds").record(elapsed.as_secs_f64());
}
rustfs_io_metrics::record_get_object_request_result("success", elapsed.as_secs_f64());
debug!(
request_id = %request_id,
@@ -556,11 +497,7 @@ impl RequestTimeoutWrapper {
Ok(Err(e)) => {
let elapsed = start_time.elapsed();
#[cfg(feature = "metrics")]
{
counter!("rustfs.get.object.requests.failed").increment(1);
histogram!("rustfs.get.object.duration.seconds").record(elapsed.as_secs_f64());
}
rustfs_io_metrics::record_get_object_request_result("error", elapsed.as_secs_f64());
debug!(
request_id = %request_id,
@@ -576,11 +513,8 @@ impl RequestTimeoutWrapper {
let elapsed = start_time.elapsed();
self.cancel_token.cancel();
#[cfg(feature = "metrics")]
{
counter!("rustfs.get.object.timeout.total").increment(1);
histogram!("rustfs.get.object.duration.seconds").record(elapsed.as_secs_f64());
}
rustfs_io_metrics::record_get_object_timeout(None, Some(elapsed.as_secs_f64()));
rustfs_io_metrics::record_get_object_request_result("timeout", elapsed.as_secs_f64());
warn!(
request_id = %request_id,
@@ -622,130 +556,6 @@ pub fn get_io_buffer_size() -> usize {
rustfs_utils::get_env_usize(rustfs_config::ENV_OBJECT_IO_BUFFER_SIZE, rustfs_config::DEFAULT_OBJECT_IO_BUFFER_SIZE)
}
/// Calculate adaptive timeout based on historical performance
///
/// This function adjusts timeout based on:
/// - Historical transfer rates
/// - Recent timeout occurrences
/// - System load indicators
pub fn calculate_adaptive_timeout(
base_timeout: Duration,
historical_rate_bps: Option<u64>,
recent_timeout_count: u32,
object_size: u64,
) -> Duration {
// If we have recent timeouts, increase timeout
let timeout_multiplier = if recent_timeout_count > 3 {
2.0 // Double timeout if many recent timeouts
} else if recent_timeout_count > 1 {
1.5 // 50% increase if some timeouts
} else {
1.0 // No adjustment
};
// If we have historical rate data, use it for estimation
let estimated_duration = if let Some(rate) = historical_rate_bps {
if rate > 0 {
let estimated_secs = (object_size as f64 / rate as f64) * 1.2; // 20% buffer
Duration::from_secs_f64(estimated_secs)
} else {
base_timeout
}
} else {
base_timeout
};
// Apply timeout multiplier but clamp to reasonable bounds
let adaptive_duration = Duration::from_secs_f64(estimated_duration.as_secs_f64() * timeout_multiplier);
// Clamp to 5 seconds minimum and 10 minutes maximum
adaptive_duration.max(Duration::from_secs(5)).min(Duration::from_secs(600))
}
/// Estimate bytes per second for timeout calculation
///
/// Uses a conservative estimate to avoid premature timeouts
pub fn estimate_bytes_per_second(object_size: u64, expected_duration: Duration) -> u64 {
let secs = expected_duration.as_secs_f64();
if secs > 0.0 {
(object_size as f64 / secs) as u64
} else {
rustfs_config::DEFAULT_OBJECT_BYTES_PER_SECOND
}
}
#[cfg(test)]
mod adaptive_timeout_tests {
use super::*;
#[test]
fn test_calculate_adaptive_timeout_basic() {
let base_timeout = Duration::from_secs(30);
let adaptive = calculate_adaptive_timeout(base_timeout, None, 0, 1024 * 1024);
// Should return base timeout when no historical data
assert_eq!(adaptive, base_timeout);
}
#[test]
fn test_calculate_adaptive_timeout_with_history() {
let base_timeout = Duration::from_secs(30);
let historical_rate = 2 * 1024 * 1024; // 2 MB/s
let object_size = 10 * 1024 * 1024; // 10 MB
let adaptive = calculate_adaptive_timeout(base_timeout, Some(historical_rate), 0, object_size);
// With 2 MB/s, 10 MB should take ~5 seconds + 20% buffer = 6 seconds
assert!(adaptive >= Duration::from_secs(5));
assert!(adaptive <= Duration::from_secs(10));
}
#[test]
fn test_calculate_adaptive_timeout_with_recent_timeouts() {
let base_timeout = Duration::from_secs(30);
// No timeouts
let adaptive1 = calculate_adaptive_timeout(base_timeout, None, 0, 1024 * 1024);
assert_eq!(adaptive1, base_timeout);
// Some timeouts (2 timeouts -> 1.5x multiplier -> 30 * 1.5 = 45 seconds)
let adaptive2 = calculate_adaptive_timeout(base_timeout, None, 2, 1024 * 1024);
assert!(adaptive2 > base_timeout);
assert!(adaptive2 <= Duration::from_secs(45)); // Changed from < to <=
// Many timeouts
let adaptive3 = calculate_adaptive_timeout(base_timeout, None, 5, 1024 * 1024);
assert!(adaptive3 >= base_timeout * 2);
}
#[test]
fn test_calculate_adaptive_timeout_clamping() {
let base_timeout = Duration::from_secs(1);
let adaptive = calculate_adaptive_timeout(base_timeout, None, 10, 1024 * 1024);
// Should clamp to minimum of 5 seconds
assert!(adaptive >= Duration::from_secs(5));
}
#[test]
fn test_estimate_bytes_per_second() {
let object_size = 10 * 1024 * 1024; // 10 MB
let duration = Duration::from_secs(10);
let bps = estimate_bytes_per_second(object_size, duration);
assert_eq!(bps, 1024 * 1024); // 1 MB/s
}
#[test]
fn test_estimate_bytes_per_second_zero_duration() {
let object_size = 1024;
let duration = Duration::from_secs(0);
let bps = estimate_bytes_per_second(object_size, duration);
assert_eq!(bps, rustfs_config::DEFAULT_OBJECT_BYTES_PER_SECOND);
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -908,32 +718,23 @@ mod tests {
assert_eq!(timeout1, config.get_object_timeout);
assert_eq!(timeout2, config.get_object_timeout);
}
use rustfs_concurrency::OperationProgress;
#[test]
fn test_operation_progress_new() {
let progress = OperationProgress::new(Some(1000), Duration::from_secs(5));
assert_eq!(progress.bytes_transferred, 0);
assert_eq!(progress.total_size, Some(1000));
assert!(!progress.is_stale());
}
#[test]
fn test_operation_progress_update() {
let mut progress = OperationProgress::new(Some(1000), Duration::from_secs(5));
assert_eq!(progress.current(), 0);
progress.update(500);
assert_eq!(progress.bytes_transferred, 500);
assert_eq!(progress.current(), 500);
assert!(!progress.is_stale());
// Simulate time passing
std::thread::sleep(Duration::from_millis(100));
progress.update(1000);
assert_eq!(progress.bytes_transferred, 1000);
assert_eq!(progress.current(), 1000);
}
#[test]
fn test_operation_progress_stale() {
let mut progress = OperationProgress::new(Some(1000), Duration::from_millis(100));
let progress = OperationProgress::new(Some(1000), Duration::from_millis(100));
progress.update(500);
assert!(!progress.is_stale());
@@ -953,7 +754,6 @@ mod tests {
assert_eq!(progress.progress_percent(), Some(0.0));
let mut progress = progress;
progress.update(500);
assert_eq!(progress.progress_percent(), Some(50.0));
@@ -0,0 +1,65 @@
# Concurrent Download Tool (tests)
This tool downloads multiple URLs concurrently and saves files to a target directory.
Saved filename format:
`<nanoseconds>_<index>_<original_filename>`
All downloaded files are written into one output directory.
## Environment variables
- `DOWNLOAD_URLS` (required): comma-separated URLs.
- `DOWNLOAD_OUTPUT_DIR` (optional): output directory, default `target/tmp/concurrent_downloads`.
- `DOWNLOAD_CONCURRENCY` (optional): max concurrent downloads, default `8`.
- `DOWNLOAD_REPEAT` (optional): repeat count per URL, default `1`.
- `DOWNLOAD_MAX_RETRIES` (optional): retry count per task after first failure, default `0`.
- `DOWNLOAD_RETRY_BACKOFF_MS` (optional): fixed backoff between retries, default `200`.
## Statistics output
After run, the tool prints:
- total tasks
- succeeded
- failed
- total bytes
- elapsed ms
- throughput bps
- total attempts
- retried tasks
- retry attempts
- latency p50 ms
- latency p95 ms
- failure details (`[index] url => error`) when failures exist
If any task fails, the test returns error after printing the summary.
Retry is triggered only for recoverable cases:
- network/request timeout/connect errors
- HTTP `429`
- HTTP `5xx`
## Compile check
```bash
cargo test -p rustfs --test concurrent_download_tool --no-run
```
## Manual run example
The commands below are for manual execution only.
They are not part of automated test runs.
```bash
DOWNLOAD_URLS="http://127.0.0.1:9001/demo/google-cloud-aiplugin-1.46.1-253.zip?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=HAXVOTZK9MLBJT8KWI4E%2F20260329%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260329T105159Z&X-Amz-Expires=86400&X-Amz-Security-Token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJwYXJlbnQiOiJydXN0ZnNhZG1pbiIsImV4cCI6MTc3NDgyMDgyMX0.tYhQoPRcg0Ysx4KVw9ez7ZpYxsqGgqomtsP_iaeTsKzoii8EVNt74BZm2wbUjXW-FbGXc1pqEYX6wZ5Ncpk9Iw&X-Amz-Signature=15f47b19832f53b34f9e0fe1862d53d71660bbf8f1a512669bb2d041ac8d0697&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject" \
DOWNLOAD_OUTPUT_DIR="/Users/zhi/Documents/code/rust/rustfs/rustfs/target/tmp/concurrent_downloads" \
DOWNLOAD_CONCURRENCY="40" \
DOWNLOAD_REPEAT="40" \
DOWNLOAD_MAX_RETRIES="2" \
DOWNLOAD_RETRY_BACKOFF_MS="300" \
cargo test -p rustfs --test concurrent_download_tool -- --ignored --nocapture
```
+407
View File
@@ -0,0 +1,407 @@
// 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.
use anyhow::{Context, Result, anyhow};
use futures::stream::{self, StreamExt};
use reqwest::{Client, Url};
use std::env;
use std::path::{Path, PathBuf};
use std::time::{Instant, SystemTime, UNIX_EPOCH};
use tokio::time::{Duration, sleep};
#[derive(Debug)]
struct DownloadSettings {
urls: Vec<String>,
output_dir: PathBuf,
concurrency: usize,
repeat: usize,
max_retries: usize,
retry_backoff_ms: u64,
}
#[derive(Debug)]
struct DownloadSuccess {
path: PathBuf,
bytes: usize,
attempts_used: usize,
elapsed_ms: u128,
}
#[derive(Debug)]
struct DownloadAttemptError {
attempts_used: usize,
error: String,
elapsed_ms: u128,
}
#[derive(Debug)]
struct DownloadFailure {
index: usize,
url: String,
attempts_used: usize,
error: String,
}
#[derive(Debug)]
struct DownloadSummary {
saved_files: Vec<PathBuf>,
total_tasks: usize,
succeeded: usize,
failed: usize,
total_bytes: usize,
elapsed_ms: u128,
throughput_bps: f64,
total_attempts: usize,
retried_tasks: usize,
retry_attempts: usize,
latency_p50_ms: u128,
latency_p95_ms: u128,
failures: Vec<DownloadFailure>,
}
fn should_retry_status(status: reqwest::StatusCode) -> bool {
status.as_u16() == 429 || status.is_server_error()
}
fn should_retry_reqwest_error(err: &reqwest::Error) -> bool {
if err.is_timeout() || err.is_connect() || err.is_request() {
return true;
}
match err.status() {
Some(status) => should_retry_status(status),
None => false,
}
}
fn percentile(values: &[u128], p: f64) -> u128 {
if values.is_empty() {
return 0;
}
let mut sorted = values.to_vec();
sorted.sort_unstable();
let rank = ((sorted.len() as f64 - 1.0) * p).round() as usize;
sorted[rank]
}
impl DownloadSettings {
fn from_env() -> Result<Self> {
let urls_raw = env::var("DOWNLOAD_URLS").context("missing DOWNLOAD_URLS, expected comma-separated URLs")?;
let urls: Vec<String> = urls_raw
.split(',')
.map(str::trim)
.filter(|v| !v.is_empty())
.map(ToString::to_string)
.collect();
if urls.is_empty() {
return Err(anyhow!("DOWNLOAD_URLS is empty, expected comma-separated URLs"));
}
let output_dir = env::var("DOWNLOAD_OUTPUT_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("target/tmp/concurrent_downloads"));
let concurrency = env::var("DOWNLOAD_CONCURRENCY")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.filter(|v| *v > 0)
.unwrap_or(8);
let repeat = env::var("DOWNLOAD_REPEAT")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.filter(|v| *v > 0)
.unwrap_or(1);
let max_retries = env::var("DOWNLOAD_MAX_RETRIES")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(0);
let retry_backoff_ms = env::var("DOWNLOAD_RETRY_BACKOFF_MS")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.filter(|v| *v > 0)
.unwrap_or(200);
Ok(Self {
urls,
output_dir,
concurrency,
repeat,
max_retries,
retry_backoff_ms,
})
}
}
fn original_filename(url: &str) -> String {
Url::parse(url)
.ok()
.and_then(|parsed| {
parsed
.path_segments()
.and_then(|mut segments| segments.rfind(|s| !s.is_empty()))
.map(ToString::to_string)
})
.filter(|name| !name.is_empty())
.unwrap_or_else(|| "download.bin".to_string())
}
fn nanos_prefix() -> Result<u128> {
Ok(SystemTime::now()
.duration_since(UNIX_EPOCH)
.context("system clock is before UNIX_EPOCH")?
.as_nanos())
}
async fn download_one(
client: &Client,
output_dir: &Path,
index: usize,
url: String,
max_retries: usize,
retry_backoff_ms: u64,
) -> std::result::Result<DownloadSuccess, DownloadAttemptError> {
let task_started_at = Instant::now();
let mut attempt = 0usize;
let mut last_error = String::new();
let mut retryable = false;
while attempt <= max_retries {
attempt += 1;
let response = match client.get(&url).send().await {
Ok(resp) => resp,
Err(err) => {
retryable = should_retry_reqwest_error(&err);
last_error = format!("failed request: {url}, error: {err}");
if retryable && attempt <= max_retries {
sleep(Duration::from_millis(retry_backoff_ms)).await;
continue;
}
break;
}
};
let status = response.status();
if !status.is_success() {
retryable = should_retry_status(status);
last_error = format!("non-success status for URL: {url}, status: {status}");
if retryable && attempt <= max_retries {
sleep(Duration::from_millis(retry_backoff_ms)).await;
continue;
}
break;
}
let body = match response.bytes().await {
Ok(bytes) => bytes,
Err(err) => {
retryable = should_retry_reqwest_error(&err);
last_error = format!("failed to read response body: {url}, error: {err}");
if retryable && attempt <= max_retries {
sleep(Duration::from_millis(retry_backoff_ms)).await;
continue;
}
break;
}
};
let source_name = original_filename(&url);
let nanos = match nanos_prefix() {
Ok(v) => v,
Err(err) => {
last_error = err.to_string();
retryable = false;
break;
}
};
let target_name = format!("{}_{}_{}", nanos, index, source_name);
let target_path = output_dir.join(target_name);
let result: Result<DownloadSuccess> = async {
tokio::fs::write(&target_path, &body)
.await
.with_context(|| format!("failed to write file: {}", target_path.display()))?;
Ok(DownloadSuccess {
path: target_path,
bytes: body.len(),
attempts_used: attempt,
elapsed_ms: task_started_at.elapsed().as_millis(),
})
}
.await;
match result {
Ok(success) => return Ok(success),
Err(err) => {
last_error = err.to_string();
retryable = false;
if retryable && attempt <= max_retries {
sleep(Duration::from_millis(retry_backoff_ms)).await;
}
break;
}
}
}
Err(DownloadAttemptError {
attempts_used: attempt,
error: if retryable {
last_error
} else {
format!("{} (non-retryable)", last_error)
},
elapsed_ms: task_started_at.elapsed().as_millis(),
})
}
async fn run_concurrent_downloads(settings: DownloadSettings) -> Result<DownloadSummary> {
let started_at = Instant::now();
tokio::fs::create_dir_all(&settings.output_dir)
.await
.with_context(|| format!("failed to create output dir: {}", settings.output_dir.display()))?;
let client = Client::new();
let tasks = settings
.urls
.into_iter()
.flat_map(|url| (0..settings.repeat).map(move |_| url.clone()))
.enumerate();
let results = stream::iter(tasks)
.map(|(index, url)| {
let client = client.clone();
let output_dir = settings.output_dir.clone();
let max_retries = settings.max_retries;
let retry_backoff_ms = settings.retry_backoff_ms;
async move {
let current_url = url.clone();
let result = download_one(&client, &output_dir, index, url, max_retries, retry_backoff_ms).await;
(index, current_url, result)
}
})
.buffer_unordered(settings.concurrency)
.collect::<Vec<(usize, String, std::result::Result<DownloadSuccess, DownloadAttemptError>)>>()
.await;
let mut saved_files = Vec::new();
let mut total_bytes = 0usize;
let mut total_attempts = 0usize;
let mut retried_tasks = 0usize;
let mut latencies_ms = Vec::new();
let mut failures = Vec::new();
for (index, url, item) in results {
match item {
Ok(success) => {
total_bytes += success.bytes;
total_attempts += success.attempts_used;
if success.attempts_used > 1 {
retried_tasks += 1;
}
latencies_ms.push(success.elapsed_ms);
saved_files.push(success.path);
}
Err(err) => {
total_attempts += err.attempts_used;
if err.attempts_used > 1 {
retried_tasks += 1;
}
latencies_ms.push(err.elapsed_ms);
failures.push(DownloadFailure {
index,
url,
attempts_used: err.attempts_used,
error: err.error,
});
}
}
}
let total_tasks = saved_files.len() + failures.len();
let retry_attempts = total_attempts.saturating_sub(total_tasks);
let elapsed_ms = started_at.elapsed().as_millis();
let throughput_bps = if elapsed_ms == 0 {
0.0
} else {
(total_bytes as f64) / ((elapsed_ms as f64) / 1000.0)
};
let latency_p50_ms = percentile(&latencies_ms, 0.50);
let latency_p95_ms = percentile(&latencies_ms, 0.95);
Ok(DownloadSummary {
total_tasks,
succeeded: saved_files.len(),
failed: failures.len(),
total_bytes,
elapsed_ms,
throughput_bps,
total_attempts,
retried_tasks,
retry_attempts,
latency_p50_ms,
latency_p95_ms,
saved_files,
failures,
})
}
#[tokio::test]
#[ignore]
async fn concurrent_download_tool() -> Result<()> {
let settings = DownloadSettings::from_env()?;
let summary = run_concurrent_downloads(settings).await?;
for path in &summary.saved_files {
println!("saved: {}", path.display());
}
println!("download complete");
println!("total tasks: {}", summary.total_tasks);
println!("succeeded: {}", summary.succeeded);
println!("failed: {}", summary.failed);
println!("total bytes: {}", summary.total_bytes);
println!("elapsed ms: {}", summary.elapsed_ms);
println!("throughput bps: {:.2}", summary.throughput_bps);
println!("total attempts: {}", summary.total_attempts);
println!("retried tasks: {}", summary.retried_tasks);
println!("retry attempts: {}", summary.retry_attempts);
println!("latency p50 ms: {}", summary.latency_p50_ms);
println!("latency p95 ms: {}", summary.latency_p95_ms);
if !summary.failures.is_empty() {
println!("failure details:");
for failure in &summary.failures {
println!(
" [{}] attempts={} {} => {}",
failure.index, failure.attempts_used, failure.url, failure.error
);
}
return Err(anyhow!("download finished with {} failures", summary.failures.len()));
}
Ok(())
}
+19
View File
@@ -0,0 +1,19 @@
# Manual test runners
Files in this directory are for manual execution only.
They are not auto-discovered as integration tests by `cargo test`.
## Dial9 runner
Build:
```bash
cargo build -p rustfs --features manual-test-runners --bin manual-test-dial9
```
Run:
```bash
cargo run -p rustfs --features manual-test-runners --bin manual-test-dial9
```
+81
View File
@@ -0,0 +1,81 @@
// Manual Dial9 integration runner.
//
// Run with:
// `cargo run -p rustfs --features manual-test-runners --bin manual-test-dial9`
//
// This file lives under `rustfs/tests/manual` and is registered explicitly in
// `rustfs/Cargo.toml` so it stays out of `cargo test` auto-discovery.
use rustfs_obs::dial9::{Dial9Config, Dial9SessionGuard};
use tokio::time::{Duration, sleep};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Dial9 Integration Test ===\n");
// Test 1: Check default dial9 configuration
println!("Test 1: Default configuration");
let default_config = Dial9Config::default();
println!(" default enabled: {}", default_config.enabled);
println!(" default output_dir: {}", default_config.output_dir);
println!(" default file_prefix: {}", default_config.file_prefix);
println!(" ✓ PASS: Default configuration loaded\n");
// Test 2: Create explicit dial9 configuration
println!("Test 2: Explicit dial9 configuration");
let config = Dial9Config {
enabled: true,
output_dir: "/tmp/rustfs-test-telemetry".to_string(),
sampling_rate: 0.5,
..Dial9Config::default()
};
println!(" config.enabled: {}", config.enabled);
println!(" config.output_dir: {}", config.output_dir);
println!(" config.file_prefix: {}", config.file_prefix);
println!(" config.sampling_rate: {}", config.sampling_rate);
assert!(config.enabled);
assert_eq!(config.output_dir, "/tmp/rustfs-test-telemetry");
assert_eq!(config.sampling_rate, 0.5);
println!(" ✓ PASS: Configuration loaded correctly\n");
// Test 3: Initialize dial9 session
println!("Test 3: Initialize dial9 session");
match Dial9SessionGuard::new(config.clone()).await {
Ok(Some(guard)) => {
println!(" Dial9 session initialized successfully");
println!(" guard.is_active(): {}", guard.is_active());
println!(" ✓ PASS: Session initialized\n");
// Test 4: Generate some async activity
println!("Test 4: Generate async activity for tracing");
let handle = tokio::spawn(async {
for i in 1..=5 {
println!(" Task iteration {}", i);
sleep(Duration::from_millis(50)).await;
}
});
handle.await?;
println!(" ✓ PASS: Async activity completed\n");
// Test 5: Session shutdown
println!("Test 5: Session cleanup");
drop(guard);
println!(" ✓ PASS: Session cleaned up\n");
}
Ok(None) => {
println!(" ⚠ SKIP: Dial9 session not created (configuration validation may have failed)\n");
}
Err(e) => {
println!(" ✗ FAIL: {:?}", e);
return Err(e.into());
}
}
// Cleanup
if let Err(err) = tokio::fs::remove_dir_all(&config.output_dir).await {
println!(" ⚠ SKIP: Failed to remove output directory: {}", err);
}
println!("=== All Tests Passed! ===");
Ok(())
}