mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-18 10:43:15 +00:00
2416 lines
96 KiB
Rust
2416 lines
96 KiB
Rust
// 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.
|
|
|
|
//! I/O scheduling types for adaptive buffer sizing and load management.
|
|
//!
|
|
//! This is the live scheduling implementation. `rustfs_io_core` supplies the
|
|
//! shared config shapes (`IoSchedulerConfig`, `IoPriorityQueueConfig`) that the
|
|
//! types here project into through `to_core_config`, plus the `io_profile`
|
|
//! storage-media model; bandwidth samples come from `rustfs_io_metrics`.
|
|
//! Same-named io-core types are those config shapes, not a backing
|
|
//! implementation this module delegates to.
|
|
|
|
use rustfs_config::{KI_B, MI_B};
|
|
use rustfs_io_core::io_profile::{AccessPattern, StorageMedia, StorageProfile};
|
|
use rustfs_io_core::{IoPriorityQueueConfig as CoreIoPriorityQueueConfig, IoSchedulerConfig as CoreIoSchedulerConfig};
|
|
use rustfs_io_metrics::bandwidth::{BandwidthSnapshot, BandwidthTier};
|
|
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
|
use std::time::Duration;
|
|
|
|
/// Global concurrent request counter for adaptive buffer sizing.
|
|
pub(crate) static ACTIVE_GET_REQUESTS: AtomicUsize = AtomicUsize::new(0);
|
|
pub(crate) static ACTIVE_PUT_REQUESTS: AtomicUsize = AtomicUsize::new(0);
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub enum IoLoadLevel {
|
|
/// Low load: wait time < 10ms. System has ample I/O capacity.
|
|
Low,
|
|
/// Medium load: wait time 10-50ms. System is moderately loaded.
|
|
Medium,
|
|
/// High load: wait time 50-200ms. System is under significant load.
|
|
High,
|
|
/// Critical load: wait time > 200ms. System is heavily congested.
|
|
Critical,
|
|
}
|
|
|
|
impl IoLoadLevel {
|
|
/// Determine load level from disk permit wait duration.
|
|
pub fn from_wait_duration(wait: Duration) -> Self {
|
|
Self::from_wait_duration_with_thresholds(
|
|
wait,
|
|
rustfs_config::DEFAULT_OBJECT_IO_LOAD_LOW_THRESHOLD_MS,
|
|
rustfs_config::DEFAULT_OBJECT_IO_LOAD_HIGH_THRESHOLD_MS,
|
|
)
|
|
}
|
|
|
|
pub fn from_wait_duration_with_thresholds(wait: Duration, low_threshold_ms: u64, high_threshold_ms: u64) -> Self {
|
|
let wait_ms = wait.as_millis() as u64;
|
|
let low_threshold_ms = low_threshold_ms.max(1);
|
|
let high_threshold_ms = high_threshold_ms.max(low_threshold_ms + 1);
|
|
let critical_threshold_ms = high_threshold_ms.saturating_mul(4);
|
|
|
|
if wait_ms < low_threshold_ms {
|
|
IoLoadLevel::Low
|
|
} else if wait_ms < high_threshold_ms {
|
|
IoLoadLevel::Medium
|
|
} else if wait_ms < critical_threshold_ms {
|
|
IoLoadLevel::High
|
|
} else {
|
|
IoLoadLevel::Critical
|
|
}
|
|
}
|
|
|
|
/// Get the load level as a string for metrics labels.
|
|
#[allow(dead_code)]
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
IoLoadLevel::Low => "low",
|
|
IoLoadLevel::Medium => "medium",
|
|
IoLoadLevel::High => "high",
|
|
IoLoadLevel::Critical => "critical",
|
|
}
|
|
}
|
|
|
|
/// Get the load level as a numeric index (0=Low, 1=Medium, 2=High, 3=Critical).
|
|
#[allow(dead_code)]
|
|
pub fn level_index(&self) -> u8 {
|
|
match self {
|
|
IoLoadLevel::Low => 0,
|
|
IoLoadLevel::Medium => 1,
|
|
IoLoadLevel::High => 2,
|
|
IoLoadLevel::Critical => 3,
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// Priority-Based I/O Scheduling
|
|
// ============================================
|
|
|
|
/// I/O request priority level.
|
|
///
|
|
/// Requests are classified by size to prevent large requests
|
|
/// from starving small requests. This is especially important
|
|
/// under high concurrency where many range reads compete for I/O.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
pub enum IoPriority {
|
|
/// High priority: small requests (< 1MB).
|
|
/// These should complete quickly to maintain responsiveness.
|
|
High,
|
|
/// Normal priority: medium requests (1MB - 10MB).
|
|
/// Standard processing with fair resource allocation.
|
|
Normal,
|
|
/// Low priority: large requests (> 10MB).
|
|
/// These can tolerate longer wait times.
|
|
Low,
|
|
}
|
|
|
|
impl IoPriority {
|
|
/// Determine priority from request size using scheduler config thresholds.
|
|
#[allow(dead_code)]
|
|
pub fn from_size(size: i64) -> Self {
|
|
Self::from_size_with_thresholds(
|
|
size,
|
|
rustfs_config::DEFAULT_OBJECT_IO_HIGH_PRIORITY_SIZE_THRESHOLD,
|
|
rustfs_config::DEFAULT_OBJECT_IO_LOW_PRIORITY_SIZE_THRESHOLD,
|
|
)
|
|
}
|
|
|
|
pub fn from_size_with_thresholds(size: i64, high_priority_size_threshold: usize, low_priority_size_threshold: usize) -> Self {
|
|
if size < 0 {
|
|
return IoPriority::Normal;
|
|
}
|
|
|
|
let size = size as usize;
|
|
if size < high_priority_size_threshold {
|
|
IoPriority::High
|
|
} else if size > low_priority_size_threshold {
|
|
IoPriority::Low
|
|
} else {
|
|
IoPriority::Normal
|
|
}
|
|
}
|
|
|
|
/// Get the priority as a string for metrics labels.
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
IoPriority::High => "high",
|
|
IoPriority::Normal => "normal",
|
|
IoPriority::Low => "low",
|
|
}
|
|
}
|
|
|
|
/// Check if this is high priority.
|
|
#[allow(dead_code)]
|
|
pub fn is_high(&self) -> bool {
|
|
matches!(self, IoPriority::High)
|
|
}
|
|
|
|
/// Check if this is normal priority.
|
|
#[allow(dead_code)]
|
|
pub fn is_normal(&self) -> bool {
|
|
matches!(self, IoPriority::Normal)
|
|
}
|
|
|
|
/// Check if this is low priority.
|
|
#[allow(dead_code)]
|
|
pub fn is_low(&self) -> bool {
|
|
matches!(self, IoPriority::Low)
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for IoPriority {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{}", self.as_str())
|
|
}
|
|
}
|
|
|
|
/// I/O scheduler configuration.
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct IoSchedulerConfig {
|
|
/// Maximum concurrent disk reads.
|
|
pub max_concurrent_reads: usize,
|
|
/// High priority size threshold in bytes.
|
|
pub high_priority_size_threshold: usize,
|
|
/// Low priority size threshold in bytes.
|
|
pub low_priority_size_threshold: usize,
|
|
/// High priority queue capacity.
|
|
pub queue_high_capacity: usize,
|
|
/// Normal priority queue capacity.
|
|
pub queue_normal_capacity: usize,
|
|
/// Low priority queue capacity.
|
|
pub queue_low_capacity: usize,
|
|
/// Starvation prevention check interval in milliseconds.
|
|
pub starvation_prevention_interval_ms: u64,
|
|
/// Starvation threshold in seconds.
|
|
pub starvation_threshold_secs: u64,
|
|
/// Load sampling window size.
|
|
pub load_sample_window: usize,
|
|
/// High load wait time threshold in milliseconds.
|
|
pub load_high_threshold_ms: u64,
|
|
/// Low load wait time threshold in milliseconds.
|
|
pub load_low_threshold_ms: u64,
|
|
/// Whether priority scheduling is enabled.
|
|
pub enable_priority: bool,
|
|
|
|
// Enhanced scheduling configuration fields
|
|
/// Storage media detection enabled.
|
|
pub storage_detection_enabled: bool,
|
|
/// Storage media override string.
|
|
pub storage_media_override: String,
|
|
/// Pattern detection history size.
|
|
pub pattern_history_size: usize,
|
|
/// Sequential step tolerance in bytes.
|
|
pub sequential_step_tolerance_bytes: u64,
|
|
/// Bandwidth EMA beta (smoothing factor).
|
|
pub bandwidth_ema_beta: f64,
|
|
/// Bandwidth low threshold in bytes per second.
|
|
pub bandwidth_low_threshold_bps: u64,
|
|
/// Bandwidth high threshold in bytes per second.
|
|
pub bandwidth_high_threshold_bps: u64,
|
|
/// NVMe buffer capacity in bytes.
|
|
pub nvme_buffer_cap: usize,
|
|
/// SSD buffer capacity in bytes.
|
|
pub ssd_buffer_cap: usize,
|
|
/// HDD buffer capacity in bytes.
|
|
pub hdd_buffer_cap: usize,
|
|
/// Concurrency threshold to disable random readahead.
|
|
pub random_readahead_disable_concurrency: usize,
|
|
/// High concurrency threshold.
|
|
pub high_concurrency_threshold: usize,
|
|
/// Medium concurrency threshold.
|
|
pub medium_concurrency_threshold: usize,
|
|
}
|
|
|
|
impl Default for IoSchedulerConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
max_concurrent_reads: rustfs_config::DEFAULT_OBJECT_MAX_CONCURRENT_DISK_READS,
|
|
enable_priority: rustfs_config::DEFAULT_OBJECT_PRIORITY_SCHEDULING_ENABLE,
|
|
high_priority_size_threshold: rustfs_config::DEFAULT_OBJECT_IO_HIGH_PRIORITY_SIZE_THRESHOLD,
|
|
low_priority_size_threshold: rustfs_config::DEFAULT_OBJECT_IO_LOW_PRIORITY_SIZE_THRESHOLD,
|
|
queue_high_capacity: rustfs_config::DEFAULT_OBJECT_IO_QUEUE_HIGH_CAPACITY,
|
|
queue_normal_capacity: rustfs_config::DEFAULT_OBJECT_IO_QUEUE_NORMAL_CAPACITY,
|
|
queue_low_capacity: rustfs_config::DEFAULT_OBJECT_IO_QUEUE_LOW_CAPACITY,
|
|
starvation_prevention_interval_ms: rustfs_config::DEFAULT_OBJECT_IO_STARVATION_PREVENTION_INTERVAL,
|
|
starvation_threshold_secs: rustfs_config::DEFAULT_OBJECT_IO_STARVATION_THRESHOLD_SECS,
|
|
load_sample_window: rustfs_config::DEFAULT_OBJECT_IO_LOAD_SAMPLE_WINDOW,
|
|
load_high_threshold_ms: rustfs_config::DEFAULT_OBJECT_IO_LOAD_HIGH_THRESHOLD_MS,
|
|
load_low_threshold_ms: rustfs_config::DEFAULT_OBJECT_IO_LOAD_LOW_THRESHOLD_MS,
|
|
// Enhanced config defaults
|
|
storage_detection_enabled: rustfs_config::DEFAULT_OBJECT_IO_STORAGE_DETECTION_ENABLE,
|
|
storage_media_override: rustfs_config::DEFAULT_OBJECT_IO_STORAGE_MEDIA_OVERRIDE.to_string(),
|
|
pattern_history_size: rustfs_config::DEFAULT_OBJECT_IO_PATTERN_HISTORY_SIZE,
|
|
sequential_step_tolerance_bytes: rustfs_config::DEFAULT_OBJECT_IO_SEQUENTIAL_STEP_TOLERANCE_BYTES,
|
|
bandwidth_ema_beta: rustfs_config::DEFAULT_OBJECT_IO_BANDWIDTH_EMA_BETA,
|
|
bandwidth_low_threshold_bps: rustfs_config::DEFAULT_OBJECT_IO_BANDWIDTH_LOW_THRESHOLD_BPS,
|
|
bandwidth_high_threshold_bps: rustfs_config::DEFAULT_OBJECT_IO_BANDWIDTH_HIGH_THRESHOLD_BPS,
|
|
nvme_buffer_cap: rustfs_config::DEFAULT_OBJECT_IO_NVME_BUFFER_CAP,
|
|
ssd_buffer_cap: rustfs_config::DEFAULT_OBJECT_IO_SSD_BUFFER_CAP,
|
|
hdd_buffer_cap: rustfs_config::DEFAULT_OBJECT_IO_HDD_BUFFER_CAP,
|
|
random_readahead_disable_concurrency: rustfs_config::DEFAULT_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY,
|
|
high_concurrency_threshold: rustfs_config::DEFAULT_OBJECT_HIGH_CONCURRENCY_THRESHOLD,
|
|
medium_concurrency_threshold: rustfs_config::DEFAULT_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl IoSchedulerConfig {
|
|
/// Load configuration from environment.
|
|
pub fn from_env() -> Self {
|
|
Self {
|
|
max_concurrent_reads: rustfs_utils::get_env_usize(
|
|
rustfs_config::ENV_OBJECT_MAX_CONCURRENT_DISK_READS,
|
|
rustfs_config::DEFAULT_OBJECT_MAX_CONCURRENT_DISK_READS,
|
|
),
|
|
enable_priority: rustfs_utils::get_env_bool(
|
|
rustfs_config::ENV_OBJECT_PRIORITY_SCHEDULING_ENABLE,
|
|
rustfs_config::DEFAULT_OBJECT_PRIORITY_SCHEDULING_ENABLE,
|
|
),
|
|
high_priority_size_threshold: rustfs_utils::get_env_usize(
|
|
rustfs_config::ENV_OBJECT_IO_HIGH_PRIORITY_SIZE_THRESHOLD,
|
|
rustfs_config::DEFAULT_OBJECT_IO_HIGH_PRIORITY_SIZE_THRESHOLD,
|
|
),
|
|
low_priority_size_threshold: rustfs_utils::get_env_usize(
|
|
rustfs_config::ENV_OBJECT_IO_LOW_PRIORITY_SIZE_THRESHOLD,
|
|
rustfs_config::DEFAULT_OBJECT_IO_LOW_PRIORITY_SIZE_THRESHOLD,
|
|
),
|
|
queue_high_capacity: rustfs_utils::get_env_usize(
|
|
rustfs_config::ENV_OBJECT_IO_QUEUE_HIGH_CAPACITY,
|
|
rustfs_config::DEFAULT_OBJECT_IO_QUEUE_HIGH_CAPACITY,
|
|
),
|
|
queue_normal_capacity: rustfs_utils::get_env_usize(
|
|
rustfs_config::ENV_OBJECT_IO_QUEUE_NORMAL_CAPACITY,
|
|
rustfs_config::DEFAULT_OBJECT_IO_QUEUE_NORMAL_CAPACITY,
|
|
),
|
|
queue_low_capacity: rustfs_utils::get_env_usize(
|
|
rustfs_config::ENV_OBJECT_IO_QUEUE_LOW_CAPACITY,
|
|
rustfs_config::DEFAULT_OBJECT_IO_QUEUE_LOW_CAPACITY,
|
|
),
|
|
starvation_prevention_interval_ms: rustfs_utils::get_env_u64(
|
|
rustfs_config::ENV_OBJECT_IO_STARVATION_PREVENTION_INTERVAL,
|
|
rustfs_config::DEFAULT_OBJECT_IO_STARVATION_PREVENTION_INTERVAL,
|
|
),
|
|
starvation_threshold_secs: rustfs_utils::get_env_u64(
|
|
rustfs_config::ENV_OBJECT_IO_STARVATION_THRESHOLD_SECS,
|
|
rustfs_config::DEFAULT_OBJECT_IO_STARVATION_THRESHOLD_SECS,
|
|
),
|
|
load_sample_window: rustfs_utils::get_env_usize(
|
|
rustfs_config::ENV_OBJECT_IO_LOAD_SAMPLE_WINDOW,
|
|
rustfs_config::DEFAULT_OBJECT_IO_LOAD_SAMPLE_WINDOW,
|
|
),
|
|
load_high_threshold_ms: rustfs_utils::get_env_u64(
|
|
rustfs_config::ENV_OBJECT_IO_LOAD_HIGH_THRESHOLD_MS,
|
|
rustfs_config::DEFAULT_OBJECT_IO_LOAD_HIGH_THRESHOLD_MS,
|
|
),
|
|
load_low_threshold_ms: rustfs_utils::get_env_u64(
|
|
rustfs_config::ENV_OBJECT_IO_LOAD_LOW_THRESHOLD_MS,
|
|
rustfs_config::DEFAULT_OBJECT_IO_LOAD_LOW_THRESHOLD_MS,
|
|
),
|
|
// Enhanced config from environment
|
|
storage_detection_enabled: rustfs_utils::get_env_bool(
|
|
rustfs_config::ENV_OBJECT_IO_STORAGE_DETECTION_ENABLE,
|
|
rustfs_config::DEFAULT_OBJECT_IO_STORAGE_DETECTION_ENABLE,
|
|
),
|
|
storage_media_override: rustfs_utils::get_env_str(
|
|
rustfs_config::ENV_OBJECT_IO_STORAGE_MEDIA_OVERRIDE,
|
|
rustfs_config::DEFAULT_OBJECT_IO_STORAGE_MEDIA_OVERRIDE,
|
|
),
|
|
pattern_history_size: rustfs_utils::get_env_usize(
|
|
rustfs_config::ENV_OBJECT_IO_PATTERN_HISTORY_SIZE,
|
|
rustfs_config::DEFAULT_OBJECT_IO_PATTERN_HISTORY_SIZE,
|
|
),
|
|
sequential_step_tolerance_bytes: rustfs_utils::get_env_u64(
|
|
rustfs_config::ENV_OBJECT_IO_SEQUENTIAL_STEP_TOLERANCE_BYTES,
|
|
rustfs_config::DEFAULT_OBJECT_IO_SEQUENTIAL_STEP_TOLERANCE_BYTES,
|
|
),
|
|
bandwidth_ema_beta: rustfs_utils::get_env_f64(
|
|
rustfs_config::ENV_OBJECT_IO_BANDWIDTH_EMA_BETA,
|
|
rustfs_config::DEFAULT_OBJECT_IO_BANDWIDTH_EMA_BETA,
|
|
),
|
|
bandwidth_low_threshold_bps: rustfs_utils::get_env_u64(
|
|
rustfs_config::ENV_OBJECT_IO_BANDWIDTH_LOW_THRESHOLD_BPS,
|
|
rustfs_config::DEFAULT_OBJECT_IO_BANDWIDTH_LOW_THRESHOLD_BPS,
|
|
),
|
|
bandwidth_high_threshold_bps: rustfs_utils::get_env_u64(
|
|
rustfs_config::ENV_OBJECT_IO_BANDWIDTH_HIGH_THRESHOLD_BPS,
|
|
rustfs_config::DEFAULT_OBJECT_IO_BANDWIDTH_HIGH_THRESHOLD_BPS,
|
|
),
|
|
nvme_buffer_cap: rustfs_utils::get_env_usize(
|
|
rustfs_config::ENV_OBJECT_IO_NVME_BUFFER_CAP,
|
|
rustfs_config::DEFAULT_OBJECT_IO_NVME_BUFFER_CAP,
|
|
),
|
|
ssd_buffer_cap: rustfs_utils::get_env_usize(
|
|
rustfs_config::ENV_OBJECT_IO_SSD_BUFFER_CAP,
|
|
rustfs_config::DEFAULT_OBJECT_IO_SSD_BUFFER_CAP,
|
|
),
|
|
hdd_buffer_cap: rustfs_utils::get_env_usize(
|
|
rustfs_config::ENV_OBJECT_IO_HDD_BUFFER_CAP,
|
|
rustfs_config::DEFAULT_OBJECT_IO_HDD_BUFFER_CAP,
|
|
),
|
|
random_readahead_disable_concurrency: rustfs_utils::get_env_usize(
|
|
rustfs_config::ENV_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY,
|
|
rustfs_config::DEFAULT_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY,
|
|
),
|
|
high_concurrency_threshold: rustfs_utils::get_env_usize(
|
|
rustfs_config::ENV_OBJECT_HIGH_CONCURRENCY_THRESHOLD,
|
|
rustfs_config::DEFAULT_OBJECT_HIGH_CONCURRENCY_THRESHOLD,
|
|
),
|
|
medium_concurrency_threshold: rustfs_utils::get_env_usize(
|
|
rustfs_config::ENV_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD,
|
|
rustfs_config::DEFAULT_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD,
|
|
),
|
|
}
|
|
}
|
|
|
|
/// Convert storage-layer scheduler config to io-core scheduler config.
|
|
///
|
|
/// This keeps storage-specific policy loading in place while allowing
|
|
/// core scheduling components to consume a normalized shared config shape.
|
|
pub fn to_core_config(&self) -> CoreIoSchedulerConfig {
|
|
CoreIoSchedulerConfig {
|
|
max_concurrent_reads: self.max_concurrent_reads,
|
|
high_priority_size_threshold: self.high_priority_size_threshold,
|
|
low_priority_size_threshold: self.low_priority_size_threshold,
|
|
queue_high_capacity: self.queue_high_capacity,
|
|
queue_normal_capacity: self.queue_normal_capacity,
|
|
queue_low_capacity: self.queue_low_capacity,
|
|
starvation_prevention_interval_ms: self.starvation_prevention_interval_ms,
|
|
starvation_threshold_secs: self.starvation_threshold_secs,
|
|
load_sample_window: self.load_sample_window,
|
|
load_high_threshold_ms: self.load_high_threshold_ms,
|
|
load_low_threshold_ms: self.load_low_threshold_ms,
|
|
enable_priority: self.enable_priority,
|
|
storage_detection_enabled: self.storage_detection_enabled,
|
|
base_buffer_size: rustfs_config::DEFAULT_OBJECT_IO_BUFFER_SIZE,
|
|
max_buffer_size: MI_B,
|
|
min_buffer_size: 32 * KI_B,
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// I/O queue status for monitoring.
|
|
#[derive(Debug, Clone, Default)]
|
|
#[allow(dead_code)]
|
|
pub struct IoQueueStatus {
|
|
/// Total permits available.
|
|
pub total_permits: usize,
|
|
/// Permits currently in use.
|
|
pub permits_in_use: usize,
|
|
/// Number of high priority requests waiting.
|
|
pub high_priority_waiting: usize,
|
|
/// Number of normal priority requests waiting.
|
|
pub normal_priority_waiting: usize,
|
|
/// Number of low priority requests waiting.
|
|
pub low_priority_waiting: usize,
|
|
/// Number of high priority requests processed.
|
|
pub high_priority_processed: u64,
|
|
/// Number of normal priority requests processed.
|
|
pub normal_priority_processed: u64,
|
|
/// Number of low priority requests processed.
|
|
pub low_priority_processed: u64,
|
|
/// Number of starvation events (low priority requests boosted).
|
|
pub starvation_events: u64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct IoSchedulingContext {
|
|
pub file_size: i64,
|
|
pub base_buffer_size: usize,
|
|
pub permit_wait_duration: Duration,
|
|
pub is_sequential_hint: bool,
|
|
pub access_pattern: AccessPattern,
|
|
pub storage_media: StorageMedia,
|
|
pub observed_bandwidth_bps: Option<u64>,
|
|
pub concurrent_requests: usize,
|
|
}
|
|
|
|
impl IoSchedulingContext {
|
|
pub fn from_wait_duration(permit_wait_duration: Duration, base_buffer_size: usize) -> Self {
|
|
Self {
|
|
file_size: -1,
|
|
base_buffer_size,
|
|
permit_wait_duration,
|
|
is_sequential_hint: false,
|
|
access_pattern: AccessPattern::Unknown,
|
|
storage_media: StorageMedia::Unknown,
|
|
observed_bandwidth_bps: None,
|
|
concurrent_requests: ACTIVE_GET_REQUESTS.load(Ordering::Relaxed),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Performance-critical I/O strategy with minimal footprint.
|
|
///
|
|
/// This structure contains only the essential runtime fields needed for I/O operations,
|
|
/// optimized for cache performance and memory efficiency.
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct IoStrategyCore {
|
|
// ===== Basic Configuration =====
|
|
/// Detected storage media type (NVMe/SSD/HDD)
|
|
pub storage_media: StorageMedia,
|
|
/// Detected access pattern (Sequential/Random/Mixed)
|
|
pub access_pattern: AccessPattern,
|
|
/// Request size in bytes (-1 if unknown)
|
|
pub request_size: i64,
|
|
/// Base buffer size before adjustments
|
|
pub base_buffer_size: usize,
|
|
/// Maximum buffer size allowed by storage media
|
|
pub buffer_cap: usize,
|
|
|
|
// ===== Runtime Decisions =====
|
|
/// Recommended buffer size for I/O operations (in bytes)
|
|
pub buffer_size: usize,
|
|
/// Buffer size multiplier (0.0 - 1.0) applied to base buffer size
|
|
pub buffer_multiplier: f64,
|
|
/// Whether sequential read-ahead should be enabled
|
|
pub enable_readahead: bool,
|
|
/// Whether tokio BufReader should be used
|
|
pub use_buffered_io: bool,
|
|
|
|
// ===== Performance State =====
|
|
/// Current concurrent request count
|
|
pub concurrent_requests: usize,
|
|
/// Observed bandwidth (if available)
|
|
pub observed_bandwidth_bps: Option<u64>,
|
|
/// Bandwidth tier (Low/Medium/High/Unknown)
|
|
pub bandwidth_tier: BandwidthTier,
|
|
/// Whether I/O is bandwidth-limited
|
|
pub bandwidth_limited: bool,
|
|
/// Whether sequential access was detected
|
|
pub sequential_detected: bool,
|
|
|
|
// ===== Decision Flags =====
|
|
/// Storage profile preferences
|
|
pub storage_profile: StorageProfile,
|
|
/// Scheduling context for this request
|
|
pub scheduling_context: IoSchedulingContext,
|
|
/// Current I/O load level
|
|
pub load_level: IoLoadLevel,
|
|
/// Time spent waiting for disk permit
|
|
pub permit_wait_duration: Duration,
|
|
|
|
// ===== Tuning Multipliers =====
|
|
pub final_multiplier: f64,
|
|
pub should_throttle_random_io: bool,
|
|
pub should_expand_for_sequential: bool,
|
|
pub should_reduce_for_concurrency: bool,
|
|
pub should_reduce_for_bandwidth: bool,
|
|
pub should_disable_readahead: bool,
|
|
|
|
// ===== Priority Scheduling =====
|
|
pub priority_enabled: bool,
|
|
pub priority: IoPriority,
|
|
|
|
// ===== Bandwidth Snapshot =====
|
|
pub bandwidth_snapshot: Option<BandwidthSnapshot>,
|
|
}
|
|
|
|
impl IoStrategyCore {
|
|
/// Create a minimal IoStrategyCore with essential fields only.
|
|
#[allow(dead_code)]
|
|
pub fn new(storage_media: StorageMedia, access_pattern: AccessPattern, buffer_size: usize) -> Self {
|
|
Self {
|
|
storage_media,
|
|
access_pattern,
|
|
request_size: -1,
|
|
base_buffer_size: buffer_size,
|
|
buffer_cap: buffer_size,
|
|
buffer_size,
|
|
buffer_multiplier: 1.0,
|
|
enable_readahead: false,
|
|
use_buffered_io: true,
|
|
concurrent_requests: 1,
|
|
observed_bandwidth_bps: None,
|
|
bandwidth_tier: BandwidthTier::Unknown,
|
|
bandwidth_limited: false,
|
|
sequential_detected: false,
|
|
storage_profile: StorageProfile::for_media(
|
|
storage_media,
|
|
256 * 1024, // default NVMe cap
|
|
128 * 1024, // default SSD cap
|
|
64 * 1024, // default HDD cap
|
|
),
|
|
scheduling_context: IoSchedulingContext::from_wait_duration(Duration::ZERO, buffer_size),
|
|
load_level: IoLoadLevel::Low,
|
|
permit_wait_duration: Duration::ZERO,
|
|
final_multiplier: 1.0,
|
|
should_throttle_random_io: false,
|
|
should_expand_for_sequential: false,
|
|
should_reduce_for_concurrency: false,
|
|
should_reduce_for_bandwidth: false,
|
|
should_disable_readahead: false,
|
|
priority_enabled: false,
|
|
priority: IoPriority::Normal,
|
|
bandwidth_snapshot: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Debug information for I/O strategy decisions (feature-gated).
|
|
///
|
|
/// This structure contains detailed debugging, tracing, and observability fields
|
|
/// that are only needed during development and troubleshooting.
|
|
/// Disabled in production to reduce memory footprint.
|
|
#[cfg(feature = "io-scheduler-debug")]
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct IoStrategyDebugInfo {
|
|
// ===== Decision Labels =====
|
|
/// Reason for readahead enable/disable decision
|
|
pub readahead_reason: &'static str,
|
|
/// Strategy calculation version
|
|
pub strategy_version: &'static str,
|
|
/// High-level reason for strategy selection
|
|
pub strategy_reason: &'static str,
|
|
/// Source of this strategy (e.g., "from_wait_duration")
|
|
pub strategy_source: &'static str,
|
|
/// Additional notes about this strategy
|
|
pub notes: &'static str,
|
|
|
|
// ===== Request Classification =====
|
|
pub request_class: &'static str, // "small" | "medium" | "large"
|
|
pub io_path_kind: &'static str, // "sequential" | "random"
|
|
pub queue_mode: &'static str, // "high-priority" | "normal-priority" | "low-priority"
|
|
|
|
// ===== State Labels =====
|
|
pub load_level_label: &'static str,
|
|
pub pattern_label: &'static str,
|
|
pub media_label: &'static str,
|
|
pub bandwidth_label: &'static str,
|
|
pub storage_profile_buffer_cap_source: &'static str,
|
|
|
|
// ===== Decision Flags =====
|
|
pub is_large_request: bool,
|
|
pub is_small_request: bool,
|
|
pub storage_detection_enabled: bool,
|
|
pub storage_media_override_applied: bool,
|
|
pub used_compatibility_path: bool,
|
|
pub sequential_hint_applied: bool,
|
|
pub observed_bandwidth_available: bool,
|
|
pub read_size_known: bool,
|
|
|
|
// ===== Decision Tracking =====
|
|
pub random_penalty_applied: bool,
|
|
pub sequential_boost_applied: bool,
|
|
pub buffer_cap_applied: bool,
|
|
pub clamp_min_applied: bool,
|
|
pub clamp_max_applied: bool,
|
|
|
|
// ===== Readahead Decisions =====
|
|
pub readahead_disabled_by_concurrency: bool,
|
|
pub readahead_disabled_by_pattern: bool,
|
|
pub readahead_disabled_by_load: bool,
|
|
pub readahead_disabled_by_bandwidth: bool,
|
|
|
|
// ===== Threshold Snapshots =====
|
|
pub final_buffer_floor: usize,
|
|
pub queue_depth_hint: usize,
|
|
pub permit_wait_ms: u64,
|
|
|
|
// ===== Configuration Thresholds (for debugging) =====
|
|
pub high_concurrency_threshold: usize,
|
|
pub medium_concurrency_threshold: usize,
|
|
pub low_bandwidth_threshold_bps: u64,
|
|
pub high_bandwidth_threshold_bps: u64,
|
|
pub random_readahead_disable_concurrency: usize,
|
|
pub low_priority_size_threshold: usize,
|
|
pub high_priority_size_threshold: usize,
|
|
|
|
// ===== Multiplier Breakdown =====
|
|
pub effective_multiplier_stage_concurrency: f64,
|
|
pub effective_multiplier_stage_pattern: f64,
|
|
pub effective_multiplier_stage_bandwidth: f64,
|
|
|
|
// ===== Extended Config =====
|
|
pub pattern_history_size: usize,
|
|
pub sequential_step_tolerance_bytes: u64,
|
|
pub bandwidth_ema_beta: f64,
|
|
pub nvme_buffer_cap: usize,
|
|
pub ssd_buffer_cap: usize,
|
|
pub hdd_buffer_cap: usize,
|
|
pub is_range_request: bool,
|
|
pub target_read_size: i64,
|
|
pub source_request_size: i64,
|
|
}
|
|
|
|
/// Adaptive I/O strategy calculated from current system load.
|
|
///
|
|
/// This structure provides optimized I/O parameters based on the observed
|
|
/// disk permit wait times. It helps balance throughput vs. latency and
|
|
/// prevents I/O saturation under high load.
|
|
///
|
|
/// # Architecture
|
|
///
|
|
/// `IoStrategy` now wraps `IoStrategyCore` for better performance and memory efficiency:
|
|
/// - **Core fields**: Only runtime-essential data (~20 fields vs 100+ before)
|
|
/// - **Debug info**: Optional feature-gated debugging details (~40 fields)
|
|
///
|
|
/// # Usage Example
|
|
///
|
|
/// ```ignore
|
|
/// let strategy = manager.calculate_io_strategy(permit_wait_duration);
|
|
///
|
|
/// // Apply strategy to I/O operations
|
|
/// let buffer_size = strategy.buffer_size;
|
|
/// let enable_readahead = strategy.enable_readahead;
|
|
/// ```
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct IoStrategy {
|
|
/// Core strategy with runtime-essential fields
|
|
pub core: IoStrategyCore,
|
|
|
|
/// Optional debug information (only available with io-scheduler-debug feature)
|
|
#[cfg(feature = "io-scheduler-debug")]
|
|
pub debug_info: IoStrategyDebugInfo,
|
|
}
|
|
|
|
// Implement Deref for transparent access to core fields
|
|
impl std::ops::Deref for IoStrategy {
|
|
type Target = IoStrategyCore;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.core
|
|
}
|
|
}
|
|
|
|
impl std::ops::DerefMut for IoStrategy {
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
&mut self.core
|
|
}
|
|
}
|
|
|
|
impl IoStrategy {
|
|
/// Create a new IoStrategy from disk permit wait time and base buffer size.
|
|
///
|
|
/// This analyzes the wait duration to determine the current I/O load level
|
|
/// and calculates appropriate I/O parameters.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `permit_wait_duration` - Time spent waiting for disk read permit
|
|
/// * `base_buffer_size` - Base buffer size from workload configuration
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// An IoStrategy with optimized parameters for the current load level.
|
|
pub fn from_wait_duration(permit_wait_duration: Duration, base_buffer_size: usize) -> Self {
|
|
let load_level = IoLoadLevel::from_wait_duration(permit_wait_duration);
|
|
|
|
// Calculate buffer multiplier based on load level
|
|
let buffer_multiplier = match load_level {
|
|
IoLoadLevel::Low => 1.0,
|
|
IoLoadLevel::Medium => 0.75,
|
|
IoLoadLevel::High => 0.5,
|
|
IoLoadLevel::Critical => 0.4,
|
|
};
|
|
|
|
// Calculate actual buffer size
|
|
let buffer_size = ((base_buffer_size as f64) * buffer_multiplier) as usize;
|
|
let buffer_size = buffer_size.clamp(32 * KI_B, MI_B);
|
|
|
|
// Determine feature toggles based on load
|
|
let enable_readahead = match load_level {
|
|
IoLoadLevel::Low | IoLoadLevel::Medium => true,
|
|
IoLoadLevel::High | IoLoadLevel::Critical => false,
|
|
};
|
|
|
|
// Build minimal scheduling context for compatibility path
|
|
let scheduling_context = IoSchedulingContext::from_wait_duration(permit_wait_duration, base_buffer_size);
|
|
#[cfg(feature = "io-scheduler-debug")]
|
|
let load_level_clone = load_level.clone();
|
|
// Build core strategy
|
|
let core = IoStrategyCore {
|
|
// Basic configuration
|
|
storage_media: StorageMedia::Unknown,
|
|
access_pattern: AccessPattern::Unknown,
|
|
request_size: -1,
|
|
base_buffer_size,
|
|
buffer_cap: buffer_size,
|
|
|
|
// Runtime decisions
|
|
buffer_size,
|
|
buffer_multiplier,
|
|
enable_readahead,
|
|
use_buffered_io: true,
|
|
|
|
// Performance state
|
|
concurrent_requests: ACTIVE_GET_REQUESTS.load(Ordering::Relaxed),
|
|
observed_bandwidth_bps: None,
|
|
bandwidth_tier: BandwidthTier::Unknown,
|
|
bandwidth_limited: false,
|
|
sequential_detected: false,
|
|
|
|
// Decision flags
|
|
storage_profile: StorageProfile::for_media(
|
|
StorageMedia::Unknown,
|
|
rustfs_config::DEFAULT_OBJECT_IO_NVME_BUFFER_CAP,
|
|
rustfs_config::DEFAULT_OBJECT_IO_SSD_BUFFER_CAP,
|
|
rustfs_config::DEFAULT_OBJECT_IO_HDD_BUFFER_CAP,
|
|
),
|
|
scheduling_context,
|
|
load_level,
|
|
permit_wait_duration,
|
|
|
|
// Tuning multipliers
|
|
final_multiplier: buffer_multiplier,
|
|
should_throttle_random_io: false,
|
|
should_expand_for_sequential: false,
|
|
should_reduce_for_concurrency: false,
|
|
should_reduce_for_bandwidth: false,
|
|
should_disable_readahead: !enable_readahead,
|
|
|
|
// Priority scheduling
|
|
priority_enabled: false,
|
|
priority: IoPriority::Normal,
|
|
|
|
// Bandwidth snapshot
|
|
bandwidth_snapshot: None,
|
|
};
|
|
|
|
#[cfg(feature = "io-scheduler-debug")]
|
|
let debug_info = IoStrategyDebugInfo {
|
|
readahead_reason: if enable_readahead { "load-based" } else { "high-load" },
|
|
strategy_version: "1.0-compat",
|
|
strategy_reason: "compatibility-path",
|
|
strategy_source: "from_wait_duration",
|
|
notes: "legacy compatibility mode",
|
|
request_class: "unknown",
|
|
io_path_kind: "compat",
|
|
queue_mode: "standard",
|
|
load_level_label: load_level_clone.as_str(),
|
|
pattern_label: "unknown",
|
|
media_label: "unknown",
|
|
bandwidth_label: "unknown",
|
|
storage_profile_buffer_cap_source: "compat",
|
|
is_large_request: false,
|
|
is_small_request: false,
|
|
storage_detection_enabled: rustfs_config::DEFAULT_OBJECT_IO_STORAGE_DETECTION_ENABLE,
|
|
storage_media_override_applied: false,
|
|
used_compatibility_path: true,
|
|
sequential_hint_applied: false,
|
|
observed_bandwidth_available: false,
|
|
read_size_known: false,
|
|
random_penalty_applied: false,
|
|
sequential_boost_applied: false,
|
|
buffer_cap_applied: false,
|
|
clamp_min_applied: buffer_size <= 32 * KI_B,
|
|
clamp_max_applied: buffer_size >= MI_B,
|
|
readahead_disabled_by_concurrency: false,
|
|
readahead_disabled_by_pattern: false,
|
|
readahead_disabled_by_load: !enable_readahead,
|
|
readahead_disabled_by_bandwidth: false,
|
|
final_buffer_floor: 32 * KI_B,
|
|
queue_depth_hint: 0,
|
|
permit_wait_ms: permit_wait_duration.as_millis() as u64,
|
|
high_concurrency_threshold: rustfs_config::DEFAULT_OBJECT_HIGH_CONCURRENCY_THRESHOLD,
|
|
medium_concurrency_threshold: rustfs_config::DEFAULT_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD,
|
|
low_bandwidth_threshold_bps: rustfs_config::DEFAULT_OBJECT_IO_BANDWIDTH_LOW_THRESHOLD_BPS,
|
|
high_bandwidth_threshold_bps: rustfs_config::DEFAULT_OBJECT_IO_BANDWIDTH_HIGH_THRESHOLD_BPS,
|
|
random_readahead_disable_concurrency: rustfs_config::DEFAULT_OBJECT_IO_RANDOM_READAHEAD_DISABLE_CONCURRENCY,
|
|
low_priority_size_threshold: rustfs_config::DEFAULT_OBJECT_IO_LOW_PRIORITY_SIZE_THRESHOLD,
|
|
high_priority_size_threshold: rustfs_config::DEFAULT_OBJECT_IO_HIGH_PRIORITY_SIZE_THRESHOLD,
|
|
// queue_capacity_hint: 0,
|
|
// load_sample_window: rustfs_config::DEFAULT_OBJECT_IO_LOAD_SAMPLE_WINDOW,
|
|
// load_high_threshold_ms: rustfs_config::DEFAULT_OBJECT_IO_LOAD_HIGH_THRESHOLD_MS,
|
|
// load_low_threshold_ms: rustfs_config::DEFAULT_OBJECT_IO_LOAD_LOW_THRESHOLD_MS,
|
|
// starvation_prevention_interval_ms: rustfs_config::DEFAULT_OBJECT_IO_STARVATION_PREVENTION_INTERVAL,
|
|
// starvation_threshold_secs: rustfs_config::DEFAULT_OBJECT_IO_STARVATION_THRESHOLD_SECS,
|
|
// max_concurrent_reads: rustfs_config::DEFAULT_OBJECT_MAX_CONCURRENT_DISK_READS,
|
|
// priority_queue_high_capacity: rustfs_config::DEFAULT_OBJECT_IO_QUEUE_HIGH_CAPACITY,
|
|
// priority_queue_normal_capacity: rustfs_config::DEFAULT_OBJECT_IO_QUEUE_NORMAL_CAPACITY,
|
|
// priority_queue_low_capacity: rustfs_config::DEFAULT_OBJECT_IO_QUEUE_LOW_CAPACITY,
|
|
pattern_history_size: rustfs_config::DEFAULT_OBJECT_IO_PATTERN_HISTORY_SIZE,
|
|
sequential_step_tolerance_bytes: rustfs_config::DEFAULT_OBJECT_IO_SEQUENTIAL_STEP_TOLERANCE_BYTES,
|
|
bandwidth_ema_beta: rustfs_config::DEFAULT_OBJECT_IO_BANDWIDTH_EMA_BETA,
|
|
nvme_buffer_cap: rustfs_config::DEFAULT_OBJECT_IO_NVME_BUFFER_CAP,
|
|
ssd_buffer_cap: rustfs_config::DEFAULT_OBJECT_IO_SSD_BUFFER_CAP,
|
|
hdd_buffer_cap: rustfs_config::DEFAULT_OBJECT_IO_HDD_BUFFER_CAP,
|
|
is_range_request: false,
|
|
target_read_size: -1,
|
|
source_request_size: -1,
|
|
// profile_prefers_readahead: false,
|
|
// fallback_to_unknown_media: true,
|
|
effective_multiplier_stage_concurrency: buffer_multiplier,
|
|
effective_multiplier_stage_pattern: 1.0,
|
|
effective_multiplier_stage_bandwidth: 1.0,
|
|
};
|
|
|
|
#[cfg(not(feature = "io-scheduler-debug"))]
|
|
{
|
|
Self { core }
|
|
}
|
|
|
|
#[cfg(feature = "io-scheduler-debug")]
|
|
{
|
|
Self { core, debug_info }
|
|
}
|
|
}
|
|
|
|
/// Create a new IoStrategy from enhanced scheduling context and configuration.
|
|
///
|
|
/// This is the comprehensive multi-factor strategy calculation that integrates:
|
|
/// - Base buffer size from workload configuration
|
|
/// - Permit wait time and load level
|
|
/// - Concurrent request count
|
|
/// - Storage media profile (NVMe/SSD/HDD)
|
|
/// - Access pattern (sequential/random/mixed)
|
|
/// - Observed bandwidth
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `context` - Scheduling context with all runtime factors
|
|
/// * `config` - Scheduler configuration with thresholds and caps
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// An IoStrategy with optimized parameters based on all factors.
|
|
pub fn from_context_with_config(context: &IoSchedulingContext, config: &IoSchedulerConfig) -> Self {
|
|
// Stage 1: Start with base buffer size
|
|
let mut buffer_size;
|
|
let mut buffer_multiplier = 1.0;
|
|
let effective_access_pattern = if context.is_sequential_hint {
|
|
AccessPattern::Sequential
|
|
} else {
|
|
context.access_pattern
|
|
};
|
|
|
|
// Stage 2: Apply load level reduction based on permit wait
|
|
let load_level = IoLoadLevel::from_wait_duration_with_thresholds(
|
|
context.permit_wait_duration,
|
|
config.load_low_threshold_ms,
|
|
config.load_high_threshold_ms,
|
|
);
|
|
|
|
let load_multiplier = match load_level {
|
|
IoLoadLevel::Low => 1.0,
|
|
IoLoadLevel::Medium => 0.75,
|
|
IoLoadLevel::High => 0.5,
|
|
IoLoadLevel::Critical => 0.4,
|
|
};
|
|
buffer_multiplier *= load_multiplier;
|
|
|
|
// Stage 3: Apply concurrency-based reduction
|
|
let concurrency_multiplier = if context.concurrent_requests >= config.high_concurrency_threshold {
|
|
0.5
|
|
} else if context.concurrent_requests >= config.medium_concurrency_threshold {
|
|
0.75
|
|
} else {
|
|
1.0
|
|
};
|
|
buffer_multiplier *= concurrency_multiplier;
|
|
|
|
// Stage 4: Get storage profile for buffer cap and preferences
|
|
let storage_profile = StorageProfile::for_media(
|
|
context.storage_media,
|
|
config.nvme_buffer_cap,
|
|
config.ssd_buffer_cap,
|
|
config.hdd_buffer_cap,
|
|
);
|
|
|
|
// Stage 5: Apply access pattern adjustments
|
|
let pattern_multiplier = match effective_access_pattern {
|
|
AccessPattern::Sequential => storage_profile.sequential_boost_multiplier,
|
|
AccessPattern::Random => storage_profile.random_penalty_multiplier,
|
|
AccessPattern::Mixed => 1.0,
|
|
AccessPattern::Unknown => 1.0,
|
|
};
|
|
buffer_multiplier *= pattern_multiplier;
|
|
|
|
// Stage 6: Apply bandwidth-based reduction
|
|
let (bandwidth_tier, bandwidth_multiplier, bandwidth_limited) = match context.observed_bandwidth_bps {
|
|
Some(bps) if bps < config.bandwidth_low_threshold_bps => {
|
|
// Low bandwidth: reduce buffer size
|
|
(BandwidthTier::Low, 0.6, true)
|
|
}
|
|
Some(bps) if bps < config.bandwidth_high_threshold_bps => {
|
|
// Medium bandwidth: no change
|
|
(BandwidthTier::Medium, 1.0, false)
|
|
}
|
|
Some(_) => {
|
|
// High bandwidth: can use larger buffers
|
|
(BandwidthTier::High, 1.1, false)
|
|
}
|
|
None => {
|
|
// Unknown bandwidth: conservative
|
|
(BandwidthTier::Unknown, 0.9, false)
|
|
}
|
|
};
|
|
buffer_multiplier *= bandwidth_multiplier;
|
|
|
|
// Calculate final buffer size with all multipliers applied
|
|
buffer_size = ((context.base_buffer_size as f64) * buffer_multiplier) as usize;
|
|
|
|
// Apply storage media cap
|
|
let buffer_cap = storage_profile.buffer_cap;
|
|
#[cfg(feature = "io-scheduler-debug")]
|
|
let buffer_cap_applied = buffer_size > buffer_cap;
|
|
buffer_size = buffer_size.min(buffer_cap);
|
|
|
|
// Apply final clamp (safety bounds)
|
|
let clamp_min = 32 * KI_B;
|
|
let clamp_max = buffer_cap.max(MI_B);
|
|
#[cfg(feature = "io-scheduler-debug")]
|
|
let clamp_min_applied = buffer_size < clamp_min;
|
|
#[cfg(feature = "io-scheduler-debug")]
|
|
let clamp_max_applied = buffer_size > clamp_max;
|
|
buffer_size = buffer_size.clamp(clamp_min, clamp_max);
|
|
|
|
// Start with storage profile preference
|
|
let mut should_enable_readahead = storage_profile.prefers_readahead;
|
|
// Determine readahead preference
|
|
#[cfg(feature = "io-scheduler-debug")]
|
|
let mut readahead_reason = if storage_profile.prefers_readahead {
|
|
"media-pref"
|
|
} else {
|
|
"media-no-pref"
|
|
};
|
|
|
|
// Apply access pattern override
|
|
let readahead_disabled_by_pattern = matches!(effective_access_pattern, AccessPattern::Random);
|
|
if readahead_disabled_by_pattern {
|
|
should_enable_readahead = false;
|
|
#[cfg(feature = "io-scheduler-debug")]
|
|
{
|
|
readahead_reason = "random-pattern";
|
|
}
|
|
}
|
|
|
|
// Apply concurrency override
|
|
let readahead_disabled_by_concurrency = context.concurrent_requests >= config.random_readahead_disable_concurrency;
|
|
if readahead_disabled_by_concurrency && matches!(effective_access_pattern, AccessPattern::Random) {
|
|
should_enable_readahead = false;
|
|
#[cfg(feature = "io-scheduler-debug")]
|
|
{
|
|
readahead_reason = "high-concurrency-random";
|
|
}
|
|
}
|
|
|
|
// Apply load override
|
|
let readahead_disabled_by_load = matches!(load_level, IoLoadLevel::High | IoLoadLevel::Critical);
|
|
if readahead_disabled_by_load {
|
|
should_enable_readahead = false;
|
|
#[cfg(feature = "io-scheduler-debug")]
|
|
{
|
|
readahead_reason = "high-load";
|
|
}
|
|
}
|
|
|
|
// Apply bandwidth override
|
|
let readahead_disabled_by_bandwidth = bandwidth_limited;
|
|
if readahead_disabled_by_bandwidth {
|
|
should_enable_readahead = false;
|
|
#[cfg(feature = "io-scheduler-debug")]
|
|
{
|
|
readahead_reason = "low-bandwidth";
|
|
}
|
|
}
|
|
|
|
let enable_readahead = should_enable_readahead;
|
|
|
|
// Calculate priority based on request size
|
|
let priority = if context.file_size > 0 {
|
|
IoPriority::from_size_with_thresholds(
|
|
context.file_size,
|
|
config.high_priority_size_threshold,
|
|
config.low_priority_size_threshold,
|
|
)
|
|
} else {
|
|
IoPriority::Normal
|
|
};
|
|
#[cfg(feature = "io-scheduler-debug")]
|
|
let load_level_clone = load_level.clone();
|
|
// Build core strategy with essential runtime fields
|
|
let core = IoStrategyCore {
|
|
// ===== Basic Configuration =====
|
|
storage_media: context.storage_media,
|
|
access_pattern: effective_access_pattern,
|
|
request_size: context.file_size,
|
|
base_buffer_size: context.base_buffer_size,
|
|
buffer_cap,
|
|
|
|
// ===== Runtime Decisions =====
|
|
buffer_size,
|
|
buffer_multiplier,
|
|
enable_readahead,
|
|
use_buffered_io: true,
|
|
|
|
// ===== Performance State =====
|
|
concurrent_requests: context.concurrent_requests,
|
|
observed_bandwidth_bps: context.observed_bandwidth_bps,
|
|
bandwidth_tier,
|
|
bandwidth_limited,
|
|
sequential_detected: matches!(effective_access_pattern, AccessPattern::Sequential),
|
|
|
|
// ===== Decision Flags =====
|
|
storage_profile,
|
|
scheduling_context: context.clone(),
|
|
load_level,
|
|
permit_wait_duration: context.permit_wait_duration,
|
|
|
|
// ===== Tuning Multipliers =====
|
|
final_multiplier: buffer_multiplier,
|
|
should_throttle_random_io: matches!(effective_access_pattern, AccessPattern::Random),
|
|
should_expand_for_sequential: matches!(effective_access_pattern, AccessPattern::Sequential),
|
|
should_reduce_for_concurrency: concurrency_multiplier < 1.0,
|
|
should_reduce_for_bandwidth: bandwidth_limited,
|
|
should_disable_readahead: !enable_readahead,
|
|
|
|
// ===== Priority Scheduling =====
|
|
priority_enabled: config.enable_priority,
|
|
priority,
|
|
|
|
// ===== Bandwidth Snapshot =====
|
|
bandwidth_snapshot: context.observed_bandwidth_bps.map(|bps| BandwidthSnapshot {
|
|
bytes_per_second: bps,
|
|
tier: bandwidth_tier,
|
|
}),
|
|
};
|
|
|
|
#[cfg(feature = "io-scheduler-debug")]
|
|
let debug_info = IoStrategyDebugInfo {
|
|
// ===== Decision Labels =====
|
|
readahead_reason,
|
|
strategy_version: "2.0-multi-factor",
|
|
strategy_reason: "multi-factor",
|
|
strategy_source: "from_context_with_config",
|
|
notes: "Multi-factor strategy with media, pattern, and bandwidth awareness",
|
|
|
|
// ===== Request Classification =====
|
|
request_class: if context.file_size > 0 {
|
|
if context.file_size < config.high_priority_size_threshold as i64 {
|
|
"small"
|
|
} else if context.file_size < config.low_priority_size_threshold as i64 {
|
|
"medium"
|
|
} else {
|
|
"large"
|
|
}
|
|
} else {
|
|
"unknown"
|
|
},
|
|
io_path_kind: if context.is_sequential_hint { "sequential" } else { "random" },
|
|
queue_mode: match priority {
|
|
IoPriority::High => "high-priority",
|
|
IoPriority::Normal => "normal-priority",
|
|
IoPriority::Low => "low-priority",
|
|
},
|
|
|
|
// ===== State Labels =====
|
|
load_level_label: load_level_clone.as_str(),
|
|
pattern_label: effective_access_pattern.as_str(),
|
|
media_label: match context.storage_media {
|
|
StorageMedia::Nvme => "nvme",
|
|
StorageMedia::Ssd => "ssd",
|
|
StorageMedia::Hdd => "hdd",
|
|
StorageMedia::Unknown => "unknown",
|
|
},
|
|
bandwidth_label: match bandwidth_tier {
|
|
BandwidthTier::Low => "low",
|
|
BandwidthTier::Medium => "medium",
|
|
BandwidthTier::High => "high",
|
|
BandwidthTier::Unknown => "unknown",
|
|
},
|
|
storage_profile_buffer_cap_source: match context.storage_media {
|
|
StorageMedia::Nvme => "nvme-cap",
|
|
StorageMedia::Ssd => "ssd-cap",
|
|
StorageMedia::Hdd => "hdd-cap",
|
|
StorageMedia::Unknown => "unknown-cap",
|
|
},
|
|
|
|
// ===== Decision Flags =====
|
|
is_large_request: context.file_size > config.low_priority_size_threshold as i64,
|
|
is_small_request: context.file_size > 0 && context.file_size < config.high_priority_size_threshold as i64,
|
|
storage_detection_enabled: config.storage_detection_enabled,
|
|
storage_media_override_applied: !config.storage_media_override.is_empty(),
|
|
used_compatibility_path: false,
|
|
sequential_hint_applied: context.is_sequential_hint,
|
|
observed_bandwidth_available: context.observed_bandwidth_bps.is_some(),
|
|
read_size_known: context.file_size > 0,
|
|
|
|
// ===== Decision Tracking =====
|
|
random_penalty_applied: matches!(effective_access_pattern, AccessPattern::Random),
|
|
sequential_boost_applied: matches!(effective_access_pattern, AccessPattern::Sequential),
|
|
buffer_cap_applied,
|
|
clamp_min_applied,
|
|
clamp_max_applied,
|
|
|
|
// ===== Readahead Decisions =====
|
|
readahead_disabled_by_concurrency,
|
|
readahead_disabled_by_pattern,
|
|
readahead_disabled_by_load,
|
|
readahead_disabled_by_bandwidth,
|
|
|
|
// ===== Threshold Snapshots =====
|
|
final_buffer_floor: clamp_min,
|
|
queue_depth_hint: context.concurrent_requests,
|
|
permit_wait_ms: context.permit_wait_duration.as_millis() as u64,
|
|
|
|
// ===== Configuration Thresholds =====
|
|
high_concurrency_threshold: config.high_concurrency_threshold,
|
|
medium_concurrency_threshold: config.medium_concurrency_threshold,
|
|
low_bandwidth_threshold_bps: config.bandwidth_low_threshold_bps,
|
|
high_bandwidth_threshold_bps: config.bandwidth_high_threshold_bps,
|
|
random_readahead_disable_concurrency: config.random_readahead_disable_concurrency,
|
|
low_priority_size_threshold: config.low_priority_size_threshold,
|
|
high_priority_size_threshold: config.high_priority_size_threshold,
|
|
|
|
// ===== Multiplier Breakdown =====
|
|
effective_multiplier_stage_concurrency: concurrency_multiplier,
|
|
effective_multiplier_stage_pattern: pattern_multiplier,
|
|
effective_multiplier_stage_bandwidth: bandwidth_multiplier,
|
|
|
|
// ===== Extended Config =====
|
|
pattern_history_size: config.pattern_history_size,
|
|
sequential_step_tolerance_bytes: config.sequential_step_tolerance_bytes,
|
|
bandwidth_ema_beta: config.bandwidth_ema_beta,
|
|
nvme_buffer_cap: config.nvme_buffer_cap,
|
|
ssd_buffer_cap: config.ssd_buffer_cap,
|
|
hdd_buffer_cap: config.hdd_buffer_cap,
|
|
is_range_request: context.file_size > 0 && !context.is_sequential_hint,
|
|
target_read_size: context.file_size,
|
|
source_request_size: context.file_size,
|
|
};
|
|
|
|
#[cfg(not(feature = "io-scheduler-debug"))]
|
|
{
|
|
Self { core }
|
|
}
|
|
|
|
#[cfg(feature = "io-scheduler-debug")]
|
|
{
|
|
Self { core, debug_info }
|
|
}
|
|
}
|
|
|
|
/// Get a human-readable description of the current I/O strategy.
|
|
#[allow(dead_code)]
|
|
pub fn description(&self) -> String {
|
|
format!(
|
|
"IoStrategy[{:?}]: buffer={}KB, multiplier={:.2}, readahead={}, wait={:?}",
|
|
self.load_level,
|
|
self.buffer_size / 1024,
|
|
self.buffer_multiplier,
|
|
self.enable_readahead,
|
|
self.permit_wait_duration
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Rolling window metrics for I/O load tracking.
|
|
///
|
|
/// This structure maintains a sliding window of recent disk permit wait times
|
|
/// to provide smoothed load level estimates. This helps prevent strategy
|
|
/// oscillation from transient load spikes.
|
|
#[derive(Debug)]
|
|
pub(crate) struct IoLoadMetrics {
|
|
/// Recent permit wait durations (sliding window)
|
|
recent_waits: Vec<Duration>,
|
|
/// Maximum samples to keep in the window
|
|
max_samples: usize,
|
|
/// The earliest record index in the recent_waits vector
|
|
earliest_index: usize,
|
|
/// Total wait time observed (for averaging)
|
|
total_wait_ns: AtomicU64,
|
|
/// Total number of observations
|
|
observation_count: AtomicU64,
|
|
}
|
|
|
|
impl IoLoadMetrics {
|
|
pub(crate) fn new(max_samples: usize) -> Self {
|
|
Self {
|
|
recent_waits: Vec::with_capacity(max_samples),
|
|
max_samples,
|
|
earliest_index: 0,
|
|
total_wait_ns: AtomicU64::new(0),
|
|
observation_count: AtomicU64::new(0),
|
|
}
|
|
}
|
|
|
|
/// Record a new permit wait observation
|
|
pub(crate) fn record(&mut self, wait: Duration) {
|
|
// Add to recent waits (with eviction if full)
|
|
if self.recent_waits.len() < self.max_samples {
|
|
self.recent_waits.push(wait);
|
|
} else {
|
|
self.recent_waits[self.earliest_index] = wait;
|
|
self.earliest_index = (self.earliest_index + 1) % self.max_samples;
|
|
}
|
|
|
|
// Update totals for overall statistics
|
|
self.total_wait_ns.fetch_add(wait.as_nanos() as u64, Ordering::Relaxed);
|
|
self.observation_count.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
|
|
/// Get the average wait duration over the recent window
|
|
pub(crate) fn average_wait(&self) -> Duration {
|
|
if self.recent_waits.is_empty() {
|
|
return Duration::ZERO;
|
|
}
|
|
let total: Duration = self.recent_waits.iter().sum();
|
|
total / self.recent_waits.len() as u32
|
|
}
|
|
|
|
/// Get the maximum wait duration in the recent window
|
|
pub(crate) fn max_wait(&self) -> Duration {
|
|
self.recent_waits.iter().copied().max().unwrap_or(Duration::ZERO)
|
|
}
|
|
|
|
/// Get the P95 wait duration from the recent window
|
|
pub(crate) fn p95_wait(&self) -> Duration {
|
|
if self.recent_waits.is_empty() {
|
|
return Duration::ZERO;
|
|
}
|
|
let mut sorted = self.recent_waits.clone();
|
|
sorted.sort();
|
|
let p95_idx = ((sorted.len() as f64) * 0.95) as usize;
|
|
sorted.get(p95_idx.min(sorted.len() - 1)).copied().unwrap_or(Duration::ZERO)
|
|
}
|
|
|
|
/// Get the smoothed load level based on recent observations
|
|
pub(crate) fn smoothed_load_level(&self) -> IoLoadLevel {
|
|
IoLoadLevel::from_wait_duration(self.average_wait())
|
|
}
|
|
|
|
/// Get the overall average wait since startup
|
|
#[allow(dead_code)]
|
|
pub(crate) fn lifetime_average_wait(&self) -> Duration {
|
|
let total = self.total_wait_ns.load(Ordering::Relaxed);
|
|
let count = self.observation_count.load(Ordering::Relaxed);
|
|
total.checked_div(count).map(Duration::from_nanos).unwrap_or(Duration::ZERO)
|
|
}
|
|
|
|
/// Get the total observation count
|
|
pub(crate) fn observation_count(&self) -> u64 {
|
|
self.observation_count.load(Ordering::Relaxed)
|
|
}
|
|
}
|
|
#[derive(Debug, Clone, Copy)]
|
|
struct ConcurrencyThresholds {
|
|
medium: usize,
|
|
high: usize,
|
|
}
|
|
|
|
fn load_concurrency_thresholds() -> ConcurrencyThresholds {
|
|
ConcurrencyThresholds {
|
|
medium: rustfs_utils::get_env_usize(
|
|
rustfs_config::ENV_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD,
|
|
rustfs_config::DEFAULT_OBJECT_MEDIUM_CONCURRENCY_THRESHOLD,
|
|
),
|
|
high: rustfs_utils::get_env_usize(
|
|
rustfs_config::ENV_OBJECT_HIGH_CONCURRENCY_THRESHOLD,
|
|
rustfs_config::DEFAULT_OBJECT_HIGH_CONCURRENCY_THRESHOLD,
|
|
),
|
|
}
|
|
}
|
|
|
|
fn compute_concurrency_aware_buffer_size(
|
|
file_size: i64,
|
|
base_buffer_size: usize,
|
|
concurrent_requests: usize,
|
|
thresholds: ConcurrencyThresholds,
|
|
) -> usize {
|
|
let medium_threshold = thresholds.medium;
|
|
let high_threshold = thresholds.high;
|
|
|
|
// For low concurrency, use the base buffer size for maximum throughput
|
|
if concurrent_requests <= 1 {
|
|
return base_buffer_size;
|
|
}
|
|
|
|
// Calculate adaptive multiplier based on concurrency level
|
|
let adaptive_multiplier = if concurrent_requests <= 2 {
|
|
// Low concurrency (1-2): use full buffer for maximum throughput
|
|
1.0
|
|
} else if concurrent_requests <= medium_threshold {
|
|
// Medium concurrency (3-4): slightly reduce buffer size (75% of base)
|
|
0.75
|
|
} else if concurrent_requests <= high_threshold {
|
|
// Higher concurrency (5-8): more aggressive reduction (50% of base)
|
|
0.5
|
|
} else {
|
|
// Very high concurrency (>8): minimize memory per request (40% of base)
|
|
0.4
|
|
};
|
|
|
|
// Calculate the adjusted buffer size
|
|
let adjusted_size = (base_buffer_size as f64 * adaptive_multiplier) as usize;
|
|
|
|
// Ensure we stay within reasonable bounds
|
|
let min_buffer = if file_size > 0 && file_size < 100 * KI_B as i64 {
|
|
32 * KI_B // For very small files, use minimum buffer
|
|
} else {
|
|
64 * KI_B // Standard minimum buffer size
|
|
};
|
|
|
|
let max_buffer = if concurrent_requests > high_threshold {
|
|
256 * KI_B // Cap at 256KB for high concurrency
|
|
} else {
|
|
MI_B // Cap at 1MB for lower concurrency
|
|
};
|
|
|
|
adjusted_size.clamp(min_buffer, max_buffer)
|
|
}
|
|
|
|
pub fn get_concurrency_aware_buffer_size(file_size: i64, base_buffer_size: usize) -> usize {
|
|
let concurrent_requests = ACTIVE_GET_REQUESTS.load(Ordering::Relaxed);
|
|
|
|
// Record concurrent request metrics
|
|
{
|
|
use metrics::gauge;
|
|
gauge!("rustfs_concurrent_get_requests").set(concurrent_requests as f64);
|
|
}
|
|
|
|
compute_concurrency_aware_buffer_size(file_size, base_buffer_size, concurrent_requests, load_concurrency_thresholds())
|
|
}
|
|
|
|
pub fn get_put_concurrency_aware_buffer_size(file_size: i64, base_buffer_size: usize) -> usize {
|
|
let concurrent_requests = ACTIVE_PUT_REQUESTS.load(Ordering::Relaxed);
|
|
|
|
{
|
|
use metrics::gauge;
|
|
gauge!("rustfs_concurrent_put_requests").set(concurrent_requests as f64);
|
|
}
|
|
|
|
compute_concurrency_aware_buffer_size(file_size, base_buffer_size, concurrent_requests, load_concurrency_thresholds())
|
|
}
|
|
|
|
/// Advanced concurrency-aware buffer sizing with file size optimization
|
|
///
|
|
/// This enhanced version considers both concurrency level and file size patterns
|
|
/// to provide even better performance characteristics.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `file_size` - The size of the file being read, or -1 if unknown
|
|
/// * `base_buffer_size` - The baseline buffer size from workload profile
|
|
/// * `is_sequential` - Whether this is a sequential read (hint for optimization)
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// Optimized buffer size in bytes
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```ignore
|
|
/// let buffer_size = get_advanced_buffer_size(
|
|
/// 32 * 1024 * 1024, // 32MB file
|
|
/// 256 * 1024, // 256KB base buffer
|
|
/// true // sequential read
|
|
/// );
|
|
/// ```
|
|
pub fn get_advanced_buffer_size(file_size: i64, base_buffer_size: usize, is_sequential: bool) -> usize {
|
|
let concurrent_requests = ACTIVE_GET_REQUESTS.load(Ordering::Relaxed);
|
|
let thresholds = load_concurrency_thresholds();
|
|
|
|
// For very small files, use smaller buffers regardless of concurrency
|
|
// Replace manual max/min chain with clamp
|
|
if file_size > 0 && file_size < 256 * KI_B as i64 {
|
|
return (file_size as usize / 4).clamp(16 * KI_B, 64 * KI_B);
|
|
}
|
|
|
|
// Base calculation from standard function
|
|
let standard_size = compute_concurrency_aware_buffer_size(file_size, base_buffer_size, concurrent_requests, thresholds);
|
|
|
|
let medium_threshold = thresholds.medium;
|
|
let high_threshold = thresholds.high;
|
|
// For sequential reads, we can be more aggressive with buffer sizes
|
|
if is_sequential && concurrent_requests <= medium_threshold {
|
|
return ((standard_size as f64 * 1.5) as usize).min(2 * MI_B);
|
|
}
|
|
|
|
// For high concurrency with large files, optimize for parallel processing
|
|
if concurrent_requests > high_threshold && file_size > 10 * MI_B as i64 {
|
|
// Use smaller, more numerous buffers for better parallelism
|
|
return (standard_size as f64 * 0.8) as usize;
|
|
}
|
|
|
|
standard_size
|
|
}
|
|
|
|
// ============================================
|
|
// I/O Priority Queue Implementation
|
|
// ============================================
|
|
|
|
use std::collections::VecDeque;
|
|
use std::sync::Arc;
|
|
use std::time::Instant;
|
|
use tokio::sync::Mutex;
|
|
use tracing::warn;
|
|
|
|
/// Queued I/O request with metadata.
|
|
#[derive(Debug)]
|
|
#[allow(dead_code)]
|
|
struct QueuedRequest<T> {
|
|
/// The actual request payload.
|
|
request: T,
|
|
/// Time when the request was enqueued.
|
|
enqueue_time: Instant,
|
|
/// Original priority assigned to the request.
|
|
original_priority: IoPriority,
|
|
/// Current priority (may be boosted for starvation prevention).
|
|
current_priority: IoPriority,
|
|
/// Whether this request was boosted due to starvation prevention.
|
|
starvation_boosted: bool,
|
|
}
|
|
|
|
/// Queue statistics for monitoring.
|
|
#[derive(Debug, Clone, Default)]
|
|
#[allow(dead_code)]
|
|
struct QueueStats {
|
|
/// Number of high priority requests processed.
|
|
high_processed: u64,
|
|
/// Number of normal priority requests processed.
|
|
normal_processed: u64,
|
|
/// Number of low priority requests processed.
|
|
low_processed: u64,
|
|
/// Number of starvation events (low priority requests boosted).
|
|
starvation_events: u64,
|
|
/// Total wait time in nanoseconds.
|
|
total_wait_time_ns: u64,
|
|
}
|
|
|
|
/// I/O Priority Queue with starvation prevention.
|
|
///
|
|
/// This structure manages three priority queues (high, normal, low) and implements
|
|
/// starvation prevention by promoting low-priority requests that have been waiting
|
|
/// too long.
|
|
///
|
|
/// # Type Parameters
|
|
///
|
|
/// * `T` - The type of requests being queued.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```ignore
|
|
/// let config = IoSchedulerConfig::default();
|
|
/// let queue = IoPriorityQueue::new(config);
|
|
///
|
|
/// // Enqueue requests
|
|
/// queue.enqueue(IoPriority::High, request1).await;
|
|
/// queue.enqueue(IoPriority::Low, request2).await;
|
|
///
|
|
/// // Dequeue (prioritizes high priority)
|
|
/// if let Some((request, priority)) = queue.dequeue().await {
|
|
/// // Process request
|
|
/// }
|
|
/// ```
|
|
pub struct IoPriorityQueue<T> {
|
|
/// Configuration for the priority queue.
|
|
config: IoPriorityQueueConfig,
|
|
|
|
/// High priority queue.
|
|
high_queue: Arc<Mutex<VecDeque<QueuedRequest<T>>>>,
|
|
/// Normal priority queue.
|
|
normal_queue: Arc<Mutex<VecDeque<QueuedRequest<T>>>>,
|
|
/// Low priority queue.
|
|
low_queue: Arc<Mutex<VecDeque<QueuedRequest<T>>>>,
|
|
|
|
/// Queue statistics.
|
|
stats: Arc<Mutex<QueueStats>>,
|
|
|
|
/// Last time starvation check was performed.
|
|
last_starvation_check: Arc<Mutex<Instant>>,
|
|
}
|
|
|
|
/// Configuration for IoPriorityQueue.
|
|
#[derive(Debug, Clone)]
|
|
pub struct IoPriorityQueueConfig {
|
|
/// High priority queue capacity.
|
|
pub queue_high_capacity: usize,
|
|
/// Normal priority queue capacity.
|
|
pub queue_normal_capacity: usize,
|
|
/// Low priority queue capacity.
|
|
pub queue_low_capacity: usize,
|
|
/// Starvation prevention check interval in milliseconds.
|
|
pub starvation_prevention_interval_ms: u64,
|
|
/// Starvation threshold in seconds (how long before boosting).
|
|
pub starvation_threshold_secs: u64,
|
|
}
|
|
|
|
impl Default for IoPriorityQueueConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
queue_high_capacity: rustfs_config::DEFAULT_OBJECT_IO_QUEUE_HIGH_CAPACITY,
|
|
queue_normal_capacity: rustfs_config::DEFAULT_OBJECT_IO_QUEUE_NORMAL_CAPACITY,
|
|
queue_low_capacity: rustfs_config::DEFAULT_OBJECT_IO_QUEUE_LOW_CAPACITY,
|
|
starvation_prevention_interval_ms: rustfs_config::DEFAULT_OBJECT_IO_STARVATION_PREVENTION_INTERVAL,
|
|
starvation_threshold_secs: rustfs_config::DEFAULT_OBJECT_IO_STARVATION_THRESHOLD_SECS,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl IoPriorityQueueConfig {
|
|
/// Load configuration from environment.
|
|
#[allow(dead_code)]
|
|
pub fn from_env() -> Self {
|
|
Self {
|
|
queue_high_capacity: rustfs_utils::get_env_usize(
|
|
rustfs_config::ENV_OBJECT_IO_QUEUE_HIGH_CAPACITY,
|
|
rustfs_config::DEFAULT_OBJECT_IO_QUEUE_HIGH_CAPACITY,
|
|
),
|
|
queue_normal_capacity: rustfs_utils::get_env_usize(
|
|
rustfs_config::ENV_OBJECT_IO_QUEUE_NORMAL_CAPACITY,
|
|
rustfs_config::DEFAULT_OBJECT_IO_QUEUE_NORMAL_CAPACITY,
|
|
),
|
|
queue_low_capacity: rustfs_utils::get_env_usize(
|
|
rustfs_config::ENV_OBJECT_IO_QUEUE_LOW_CAPACITY,
|
|
rustfs_config::DEFAULT_OBJECT_IO_QUEUE_LOW_CAPACITY,
|
|
),
|
|
starvation_prevention_interval_ms: rustfs_utils::get_env_u64(
|
|
rustfs_config::ENV_OBJECT_IO_STARVATION_PREVENTION_INTERVAL,
|
|
rustfs_config::DEFAULT_OBJECT_IO_STARVATION_PREVENTION_INTERVAL,
|
|
),
|
|
starvation_threshold_secs: rustfs_utils::get_env_u64(
|
|
rustfs_config::ENV_OBJECT_IO_STARVATION_THRESHOLD_SECS,
|
|
rustfs_config::DEFAULT_OBJECT_IO_STARVATION_THRESHOLD_SECS,
|
|
),
|
|
}
|
|
}
|
|
|
|
/// Convert storage-layer queue config to io-core queue config.
|
|
pub fn to_core_config(&self) -> CoreIoPriorityQueueConfig {
|
|
CoreIoPriorityQueueConfig {
|
|
high_capacity: self.queue_high_capacity,
|
|
normal_capacity: self.queue_normal_capacity,
|
|
low_capacity: self.queue_low_capacity,
|
|
starvation_interval: Duration::from_millis(self.starvation_prevention_interval_ms),
|
|
starvation_threshold: Duration::from_secs(self.starvation_threshold_secs),
|
|
}
|
|
}
|
|
|
|
/// Build queue config directly from a scheduler config.
|
|
pub fn from_scheduler_config(config: &IoSchedulerConfig) -> Self {
|
|
Self {
|
|
queue_high_capacity: config.queue_high_capacity,
|
|
queue_normal_capacity: config.queue_normal_capacity,
|
|
queue_low_capacity: config.queue_low_capacity,
|
|
starvation_prevention_interval_ms: config.starvation_prevention_interval_ms,
|
|
starvation_threshold_secs: config.starvation_threshold_secs,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T> IoPriorityQueue<T> {
|
|
/// Create a new priority queue with the given configuration.
|
|
#[allow(dead_code)]
|
|
pub fn new(config: IoPriorityQueueConfig) -> Self {
|
|
let config_clone = config.clone();
|
|
Self {
|
|
config,
|
|
high_queue: Arc::new(Mutex::new(VecDeque::with_capacity(config_clone.queue_high_capacity))),
|
|
normal_queue: Arc::new(Mutex::new(VecDeque::with_capacity(config_clone.queue_normal_capacity))),
|
|
low_queue: Arc::new(Mutex::new(VecDeque::with_capacity(config_clone.queue_low_capacity))),
|
|
stats: Arc::new(Mutex::new(QueueStats::default())),
|
|
last_starvation_check: Arc::new(Mutex::new(Instant::now())),
|
|
}
|
|
}
|
|
|
|
/// Enqueue a request with the given priority.
|
|
#[allow(dead_code)]
|
|
pub async fn enqueue(&self, priority: IoPriority, request: T) {
|
|
let queued = QueuedRequest {
|
|
request,
|
|
enqueue_time: Instant::now(),
|
|
original_priority: priority,
|
|
current_priority: priority,
|
|
starvation_boosted: false,
|
|
};
|
|
|
|
match priority {
|
|
IoPriority::High => self.high_queue.lock().await.push_back(queued),
|
|
IoPriority::Normal => self.normal_queue.lock().await.push_back(queued),
|
|
IoPriority::Low => self.low_queue.lock().await.push_back(queued),
|
|
}
|
|
}
|
|
|
|
/// Dequeue a request, prioritizing high priority requests.
|
|
///
|
|
/// This method performs starvation prevention checks before dequeuing.
|
|
/// Returns `None` if all queues are empty.
|
|
#[allow(dead_code)]
|
|
pub async fn dequeue(&self) -> Option<(T, IoPriority)> {
|
|
// 1. Check for starvation prevention
|
|
self.check_starvation().await;
|
|
|
|
// 2. Dequeue in priority order
|
|
if let Some(queued) = self.high_queue.lock().await.pop_front() {
|
|
self.record_dequeue(&queued).await;
|
|
return Some((queued.request, queued.current_priority));
|
|
}
|
|
|
|
if let Some(queued) = self.normal_queue.lock().await.pop_front() {
|
|
self.record_dequeue(&queued).await;
|
|
return Some((queued.request, queued.current_priority));
|
|
}
|
|
|
|
if let Some(queued) = self.low_queue.lock().await.pop_front() {
|
|
self.record_dequeue(&queued).await;
|
|
return Some((queued.request, queued.current_priority));
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Check for starving low-priority requests and boost them.
|
|
async fn check_starvation(&self) {
|
|
let mut last_check = self.last_starvation_check.lock().await;
|
|
let now = Instant::now();
|
|
|
|
// Only check at the configured interval
|
|
if now.duration_since(*last_check) < Duration::from_millis(self.config.starvation_prevention_interval_ms) {
|
|
return;
|
|
}
|
|
|
|
*last_check = now;
|
|
|
|
// Check if low priority queue has requests waiting too long
|
|
let mut low_queue = self.low_queue.lock().await;
|
|
let mut normal_queue = self.normal_queue.lock().await;
|
|
|
|
let starvation_threshold = Duration::from_secs(self.config.starvation_threshold_secs);
|
|
let mut starvation_count = 0;
|
|
|
|
while let Some(queued) = low_queue.front() {
|
|
if now.duration_since(queued.enqueue_time) > starvation_threshold {
|
|
let mut queued = low_queue.pop_front().unwrap();
|
|
queued.current_priority = IoPriority::Normal;
|
|
queued.starvation_boosted = true;
|
|
normal_queue.push_back(queued);
|
|
starvation_count += 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if starvation_count > 0 {
|
|
self.stats.lock().await.starvation_events += starvation_count;
|
|
warn!(starvation_count, "Starvation prevention: boosted low priority requests to normal");
|
|
}
|
|
}
|
|
|
|
/// Record dequeue statistics.
|
|
async fn record_dequeue(&self, queued: &QueuedRequest<T>) {
|
|
let mut stats = self.stats.lock().await;
|
|
|
|
// Record processing count
|
|
match queued.current_priority {
|
|
IoPriority::High => stats.high_processed += 1,
|
|
IoPriority::Normal => stats.normal_processed += 1,
|
|
IoPriority::Low => stats.low_processed += 1,
|
|
}
|
|
|
|
// Record wait time
|
|
let wait_time = Instant::now().duration_since(queued.enqueue_time);
|
|
stats.total_wait_time_ns += wait_time.as_nanos() as u64;
|
|
}
|
|
|
|
/// Get current queue status for monitoring.
|
|
#[allow(dead_code)]
|
|
pub async fn status(&self) -> IoQueueStatus {
|
|
let high_queue = self.high_queue.lock().await;
|
|
let normal_queue = self.normal_queue.lock().await;
|
|
let low_queue = self.low_queue.lock().await;
|
|
let stats = self.stats.lock().await;
|
|
|
|
IoQueueStatus {
|
|
total_permits: 0, // Not applicable for priority queue
|
|
permits_in_use: 0, // Not applicable for priority queue
|
|
high_priority_waiting: high_queue.len(),
|
|
normal_priority_waiting: normal_queue.len(),
|
|
low_priority_waiting: low_queue.len(),
|
|
high_priority_processed: stats.high_processed,
|
|
normal_priority_processed: stats.normal_processed,
|
|
low_priority_processed: stats.low_processed,
|
|
starvation_events: stats.starvation_events,
|
|
}
|
|
}
|
|
|
|
/// Get the total number of queued requests.
|
|
#[allow(dead_code)]
|
|
pub async fn len(&self) -> usize {
|
|
let high_queue = self.high_queue.lock().await;
|
|
let normal_queue = self.normal_queue.lock().await;
|
|
let low_queue = self.low_queue.lock().await;
|
|
|
|
high_queue.len() + normal_queue.len() + low_queue.len()
|
|
}
|
|
|
|
/// Check if all queues are empty.
|
|
#[allow(dead_code)]
|
|
pub async fn is_empty(&self) -> bool {
|
|
self.len().await == 0
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// Unit Tests
|
|
// ============================================
|
|
|
|
#[cfg(test)]
|
|
#[allow(unused_imports)]
|
|
mod tests {
|
|
use super::{
|
|
IoLoadLevel, IoPriority, IoPriorityQueue, IoPriorityQueueConfig, IoSchedulerConfig, IoSchedulingContext, IoStrategy,
|
|
get_advanced_buffer_size, get_concurrency_aware_buffer_size,
|
|
};
|
|
use rustfs_io_core::io_profile::{AccessPattern, StorageMedia};
|
|
use rustfs_io_metrics::bandwidth::{BandwidthSnapshot, BandwidthTier};
|
|
use serial_test::serial;
|
|
use std::time::Duration;
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_io_priority_queue_basic() {
|
|
let config = IoPriorityQueueConfig::default();
|
|
let queue = IoPriorityQueue::new(config);
|
|
|
|
// Test empty queue
|
|
assert!(queue.is_empty().await);
|
|
assert_eq!(queue.len().await, 0);
|
|
|
|
// Test enqueue
|
|
queue.enqueue(IoPriority::High, ()).await;
|
|
queue.enqueue(IoPriority::Normal, ()).await;
|
|
queue.enqueue(IoPriority::Low, ()).await;
|
|
|
|
assert!(!queue.is_empty().await);
|
|
assert_eq!(queue.len().await, 3);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_io_priority_queue_dequeue_order() {
|
|
let config = IoPriorityQueueConfig::default();
|
|
let queue = IoPriorityQueue::new(config);
|
|
|
|
// Enqueue in reverse priority order
|
|
queue.enqueue(IoPriority::Low, "low").await;
|
|
queue.enqueue(IoPriority::Normal, "normal").await;
|
|
queue.enqueue(IoPriority::High, "high").await;
|
|
|
|
// Dequeue should return in priority order
|
|
let (req1, pri1) = queue.dequeue().await.unwrap();
|
|
assert_eq!(req1, "high");
|
|
assert_eq!(pri1, IoPriority::High);
|
|
|
|
let (req2, pri2) = queue.dequeue().await.unwrap();
|
|
assert_eq!(req2, "normal");
|
|
assert_eq!(pri2, IoPriority::Normal);
|
|
|
|
let (req3, pri3) = queue.dequeue().await.unwrap();
|
|
assert_eq!(req3, "low");
|
|
assert_eq!(pri3, IoPriority::Low);
|
|
|
|
// Queue should be empty now
|
|
assert!(queue.is_empty().await);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_io_priority_queue_status() {
|
|
let config = IoPriorityQueueConfig::default();
|
|
let queue = IoPriorityQueue::new(config);
|
|
|
|
// Enqueue some requests
|
|
queue.enqueue(IoPriority::High, ()).await;
|
|
queue.enqueue(IoPriority::High, ()).await;
|
|
queue.enqueue(IoPriority::Normal, ()).await;
|
|
queue.enqueue(IoPriority::Low, ()).await;
|
|
|
|
let status = queue.status().await;
|
|
assert_eq!(status.high_priority_waiting, 2);
|
|
assert_eq!(status.normal_priority_waiting, 1);
|
|
assert_eq!(status.low_priority_waiting, 1);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_io_priority_queue_starvation_prevention() {
|
|
let config = IoPriorityQueueConfig {
|
|
starvation_threshold_secs: 1,
|
|
starvation_prevention_interval_ms: 100,
|
|
..Default::default()
|
|
};
|
|
|
|
let queue = IoPriorityQueue::new(config);
|
|
|
|
// Enqueue a low priority request
|
|
queue.enqueue(IoPriority::Low, "low").await;
|
|
|
|
// Wait for starvation threshold
|
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
|
|
|
// Dequeue should return the boosted request as Normal priority
|
|
let (req, priority) = queue.dequeue().await.unwrap();
|
|
assert_eq!(req, "low");
|
|
// The request should be boosted to Normal due to starvation prevention
|
|
assert_eq!(priority, IoPriority::Normal);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_io_priority_from_size() {
|
|
// High priority: < 1MB
|
|
assert_eq!(IoPriority::from_size(100 * 1024), IoPriority::High);
|
|
assert_eq!(IoPriority::from_size(512 * 1024), IoPriority::High);
|
|
|
|
// Normal priority: 1MB - 10MB
|
|
assert_eq!(IoPriority::from_size(2 * 1024 * 1024), IoPriority::Normal);
|
|
assert_eq!(IoPriority::from_size(5 * 1024 * 1024), IoPriority::Normal);
|
|
|
|
// Low priority: > 10MB
|
|
assert_eq!(IoPriority::from_size(20 * 1024 * 1024), IoPriority::Low);
|
|
assert_eq!(IoPriority::from_size(100 * 1024 * 1024), IoPriority::Low);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_io_load_level_from_wait_duration() {
|
|
use std::time::Duration;
|
|
|
|
// Low load: < 10ms
|
|
assert_eq!(IoLoadLevel::from_wait_duration(Duration::from_millis(5)), IoLoadLevel::Low);
|
|
|
|
// Medium load: 10-50ms
|
|
assert_eq!(IoLoadLevel::from_wait_duration(Duration::from_millis(20)), IoLoadLevel::Medium);
|
|
|
|
// High load: 50-200ms
|
|
assert_eq!(IoLoadLevel::from_wait_duration(Duration::from_millis(100)), IoLoadLevel::High);
|
|
|
|
// Critical load: > 200ms
|
|
assert_eq!(IoLoadLevel::from_wait_duration(Duration::from_millis(300)), IoLoadLevel::Critical);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_io_scheduler_config_default() {
|
|
let config = IoSchedulerConfig::default();
|
|
|
|
assert!(config.enable_priority);
|
|
assert_eq!(config.high_priority_size_threshold, 1024 * 1024); // 1MB
|
|
assert_eq!(config.low_priority_size_threshold, 10 * 1024 * 1024); // 10MB
|
|
assert_eq!(config.queue_high_capacity, 32);
|
|
assert_eq!(config.queue_normal_capacity, 64);
|
|
assert_eq!(config.queue_low_capacity, 16);
|
|
assert_eq!(config.starvation_threshold_secs, 5);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_io_scheduler_config_to_core_config() {
|
|
let config = IoSchedulerConfig::default();
|
|
let core = config.to_core_config();
|
|
assert_eq!(core.max_concurrent_reads, config.max_concurrent_reads);
|
|
assert_eq!(core.high_priority_size_threshold, config.high_priority_size_threshold);
|
|
assert_eq!(core.low_priority_size_threshold, config.low_priority_size_threshold);
|
|
assert_eq!(core.queue_high_capacity, config.queue_high_capacity);
|
|
assert_eq!(core.queue_normal_capacity, config.queue_normal_capacity);
|
|
assert_eq!(core.queue_low_capacity, config.queue_low_capacity);
|
|
assert_eq!(core.load_high_threshold_ms, config.load_high_threshold_ms);
|
|
assert_eq!(core.load_low_threshold_ms, config.load_low_threshold_ms);
|
|
assert_eq!(core.enable_priority, config.enable_priority);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_io_priority_queue_config_to_core_config() {
|
|
let config = IoPriorityQueueConfig::default();
|
|
let core = config.to_core_config();
|
|
assert_eq!(core.high_capacity, config.queue_high_capacity);
|
|
assert_eq!(core.normal_capacity, config.queue_normal_capacity);
|
|
assert_eq!(core.low_capacity, config.queue_low_capacity);
|
|
assert_eq!(core.starvation_interval, Duration::from_millis(config.starvation_prevention_interval_ms));
|
|
assert_eq!(core.starvation_threshold, Duration::from_secs(config.starvation_threshold_secs));
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_io_priority_queue_config_from_scheduler_config() {
|
|
let scheduler_config = IoSchedulerConfig {
|
|
queue_high_capacity: 128,
|
|
queue_normal_capacity: 256,
|
|
queue_low_capacity: 512,
|
|
starvation_prevention_interval_ms: 2000,
|
|
starvation_threshold_secs: 120,
|
|
..Default::default()
|
|
};
|
|
let config = IoPriorityQueueConfig::from_scheduler_config(&scheduler_config);
|
|
assert_eq!(config.queue_high_capacity, 128);
|
|
assert_eq!(config.queue_normal_capacity, 256);
|
|
assert_eq!(config.queue_low_capacity, 512);
|
|
assert_eq!(config.starvation_prevention_interval_ms, 2000);
|
|
assert_eq!(config.starvation_threshold_secs, 120);
|
|
}
|
|
|
|
// ============================================
|
|
// Multi-Factor Strategy Tests
|
|
// ============================================
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_multi_factor_strategy_nvme_sequential_low_load() {
|
|
// NVMe + Sequential + Low load = maximum buffer size
|
|
let context = IoSchedulingContext {
|
|
file_size: 100 * 1024 * 1024, // 100MB
|
|
base_buffer_size: 256 * 1024, // 256KB
|
|
permit_wait_duration: Duration::from_millis(5), // Low load
|
|
is_sequential_hint: true,
|
|
access_pattern: AccessPattern::Sequential,
|
|
storage_media: StorageMedia::Nvme,
|
|
observed_bandwidth_bps: Some(600 * 1024 * 1024), // 600MB/s (High, > 512MB/s threshold)
|
|
concurrent_requests: 2, // Low concurrency
|
|
};
|
|
|
|
let config = IoSchedulerConfig::default();
|
|
let strategy = IoStrategy::from_context_with_config(&context, &config);
|
|
|
|
// Should get large buffer due to NVMe + Sequential + High bandwidth
|
|
assert!(strategy.buffer_size > 256 * 1024, "NVMe sequential should get larger buffer");
|
|
assert!(strategy.enable_readahead, "Sequential reads should enable readahead");
|
|
assert_eq!(strategy.load_level, IoLoadLevel::Low);
|
|
assert_eq!(strategy.storage_media, StorageMedia::Nvme);
|
|
assert_eq!(strategy.access_pattern, AccessPattern::Sequential);
|
|
assert_eq!(strategy.bandwidth_tier, BandwidthTier::High);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_multi_factor_strategy_hdd_random_high_load() {
|
|
// HDD + Random + High load = conservative buffer size
|
|
let context = IoSchedulingContext {
|
|
file_size: 100 * 1024 * 1024,
|
|
base_buffer_size: 256 * 1024,
|
|
permit_wait_duration: Duration::from_millis(100), // High load
|
|
is_sequential_hint: false,
|
|
access_pattern: AccessPattern::Random,
|
|
storage_media: StorageMedia::Hdd,
|
|
observed_bandwidth_bps: Some(10 * 1024 * 1024), // 10MB/s (Low)
|
|
concurrent_requests: 16, // High concurrency
|
|
};
|
|
|
|
let config = IoSchedulerConfig::default();
|
|
let strategy = IoStrategy::from_context_with_config(&context, &config);
|
|
|
|
// Should get small buffer due to HDD + Random + High load + Low bandwidth
|
|
assert!(strategy.buffer_size < 256 * 1024, "HDD random high load should get smaller buffer");
|
|
assert!(!strategy.enable_readahead, "Random reads should disable readahead");
|
|
assert_eq!(strategy.load_level, IoLoadLevel::High);
|
|
assert_eq!(strategy.storage_media, StorageMedia::Hdd);
|
|
assert_eq!(strategy.access_pattern, AccessPattern::Random);
|
|
assert!(strategy.bandwidth_limited, "Low bandwidth should be marked");
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_multi_factor_strategy_ssd_mixed_medium_load() {
|
|
// SSD + Mixed + Medium load = moderate buffer
|
|
let context = IoSchedulingContext {
|
|
file_size: 50 * 1024 * 1024,
|
|
base_buffer_size: 256 * 1024,
|
|
permit_wait_duration: Duration::from_millis(30), // Medium load
|
|
is_sequential_hint: false,
|
|
access_pattern: AccessPattern::Mixed,
|
|
storage_media: StorageMedia::Ssd,
|
|
observed_bandwidth_bps: Some(100 * 1024 * 1024), // 100MB/s (Medium)
|
|
concurrent_requests: 6, // Medium concurrency
|
|
};
|
|
|
|
let config = IoSchedulerConfig::default();
|
|
let strategy = IoStrategy::from_context_with_config(&context, &config);
|
|
|
|
// Should get moderate buffer
|
|
assert!(
|
|
strategy.buffer_size >= 128 * 1024 && strategy.buffer_size <= 256 * 1024,
|
|
"SSD mixed medium load should get moderate buffer"
|
|
);
|
|
assert_eq!(strategy.load_level, IoLoadLevel::Medium);
|
|
assert_eq!(strategy.storage_media, StorageMedia::Ssd);
|
|
assert_eq!(strategy.access_pattern, AccessPattern::Mixed);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_multi_factor_strategy_critical_load_disables_features() {
|
|
// Any media + Critical load = minimal features
|
|
let context = IoSchedulingContext {
|
|
file_size: 10 * 1024 * 1024,
|
|
base_buffer_size: 256 * 1024,
|
|
permit_wait_duration: Duration::from_millis(300), // Critical load
|
|
is_sequential_hint: true,
|
|
access_pattern: AccessPattern::Sequential,
|
|
storage_media: StorageMedia::Nvme,
|
|
observed_bandwidth_bps: Some(200 * 1024 * 1024),
|
|
concurrent_requests: 1,
|
|
};
|
|
|
|
let config = IoSchedulerConfig::default();
|
|
let strategy = IoStrategy::from_context_with_config(&context, &config);
|
|
|
|
// Critical load should disable readahead
|
|
assert_eq!(strategy.load_level, IoLoadLevel::Critical);
|
|
assert!(!strategy.enable_readahead, "Critical load should disable readahead");
|
|
// Buffer: 256KB * 0.4 (critical) * 1.35 (sequential) ≈ 138KB
|
|
assert!(strategy.buffer_size < 200 * 1024, "Critical load should reduce buffer");
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_multi_factor_strategy_buffer_cap_enforcement() {
|
|
// Test that storage media caps are enforced
|
|
let context = IoSchedulingContext {
|
|
file_size: 1000 * 1024 * 1024, // 1GB
|
|
base_buffer_size: 16 * 1024 * 1024, // 16MB (very large)
|
|
permit_wait_duration: Duration::from_millis(1), // Low load
|
|
is_sequential_hint: true,
|
|
access_pattern: AccessPattern::Sequential,
|
|
storage_media: StorageMedia::Nvme,
|
|
observed_bandwidth_bps: Some(1000 * 1024 * 1024), // Very high
|
|
concurrent_requests: 1,
|
|
};
|
|
|
|
let config = IoSchedulerConfig::default();
|
|
let strategy = IoStrategy::from_context_with_config(&context, &config);
|
|
|
|
// Should be capped at the configured NVMe media cap.
|
|
assert_eq!(strategy.buffer_size, rustfs_config::DEFAULT_OBJECT_IO_NVME_BUFFER_CAP);
|
|
|
|
#[cfg(feature = "io-scheduler-debug")]
|
|
assert!(strategy.debug_info.buffer_cap_applied, "Buffer cap should be applied");
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_multi_factor_strategy_applies_sequential_hint_when_pattern_unknown() {
|
|
let context = IoSchedulingContext {
|
|
file_size: 2 * 1024 * 1024 * 1024, // 2GiB
|
|
base_buffer_size: 1024 * 1024, // 1MiB base
|
|
permit_wait_duration: Duration::from_millis(1), // Low load
|
|
is_sequential_hint: true,
|
|
access_pattern: AccessPattern::Unknown,
|
|
storage_media: StorageMedia::Nvme,
|
|
observed_bandwidth_bps: Some(1000 * 1024 * 1024),
|
|
concurrent_requests: 1,
|
|
};
|
|
|
|
let config = IoSchedulerConfig::default();
|
|
let strategy = IoStrategy::from_context_with_config(&context, &config);
|
|
|
|
assert_eq!(strategy.access_pattern, AccessPattern::Sequential);
|
|
assert!(strategy.enable_readahead, "Sequential hint should keep readahead enabled");
|
|
assert!(
|
|
strategy.buffer_size > context.base_buffer_size,
|
|
"Sequential hint should allow a larger buffer for large GETs"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_multi_factor_strategy_bandwidth_low_reduces_buffer() {
|
|
// Low bandwidth should reduce buffer
|
|
let context = IoSchedulingContext {
|
|
file_size: 50 * 1024 * 1024,
|
|
base_buffer_size: 512 * 1024,
|
|
permit_wait_duration: Duration::from_millis(10),
|
|
is_sequential_hint: true,
|
|
access_pattern: AccessPattern::Sequential,
|
|
storage_media: StorageMedia::Ssd,
|
|
observed_bandwidth_bps: Some(5 * 1024 * 1024), // 5MB/s (Low)
|
|
concurrent_requests: 2,
|
|
};
|
|
|
|
let config = IoSchedulerConfig::default();
|
|
let strategy = IoStrategy::from_context_with_config(&context, &config);
|
|
|
|
assert_eq!(strategy.bandwidth_tier, BandwidthTier::Low);
|
|
assert!(strategy.bandwidth_limited, "Low bandwidth should be flagged");
|
|
assert!(!strategy.enable_readahead, "Low bandwidth should disable readahead");
|
|
assert!(strategy.buffer_size < context.base_buffer_size, "Low bandwidth should reduce buffer");
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_multi_factor_strategy_high_concurrency_reduction() {
|
|
// High concurrency should reduce buffer
|
|
let context = IoSchedulingContext {
|
|
file_size: 100 * 1024 * 1024,
|
|
base_buffer_size: 512 * 1024,
|
|
permit_wait_duration: Duration::from_millis(10),
|
|
is_sequential_hint: true,
|
|
access_pattern: AccessPattern::Sequential,
|
|
storage_media: StorageMedia::Nvme,
|
|
observed_bandwidth_bps: Some(200 * 1024 * 1024),
|
|
concurrent_requests: 20, // High concurrency (> 16)
|
|
};
|
|
|
|
let config = IoSchedulerConfig::default();
|
|
let strategy = IoStrategy::from_context_with_config(&context, &config);
|
|
|
|
assert!(strategy.concurrent_requests >= config.high_concurrency_threshold);
|
|
assert!(strategy.should_reduce_for_concurrency, "Should mark concurrency reduction");
|
|
assert!(strategy.buffer_size < context.base_buffer_size, "High concurrency should reduce buffer");
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_multi_factor_strategy_sequential_boost() {
|
|
// Sequential reads should get boost
|
|
let sequential_context = IoSchedulingContext {
|
|
file_size: 50 * 1024 * 1024,
|
|
base_buffer_size: 256 * 1024,
|
|
permit_wait_duration: Duration::from_millis(10),
|
|
is_sequential_hint: true,
|
|
access_pattern: AccessPattern::Sequential,
|
|
storage_media: StorageMedia::Ssd,
|
|
observed_bandwidth_bps: Some(100 * 1024 * 1024),
|
|
concurrent_requests: 2,
|
|
};
|
|
|
|
let random_context = IoSchedulingContext {
|
|
file_size: 50 * 1024 * 1024,
|
|
base_buffer_size: 256 * 1024,
|
|
permit_wait_duration: Duration::from_millis(10),
|
|
is_sequential_hint: false,
|
|
access_pattern: AccessPattern::Random,
|
|
storage_media: StorageMedia::Ssd,
|
|
observed_bandwidth_bps: Some(100 * 1024 * 1024),
|
|
concurrent_requests: 2,
|
|
};
|
|
|
|
let config = IoSchedulerConfig::default();
|
|
let sequential_strategy = IoStrategy::from_context_with_config(&sequential_context, &config);
|
|
let random_strategy = IoStrategy::from_context_with_config(&random_context, &config);
|
|
|
|
assert!(
|
|
sequential_strategy.buffer_size > random_strategy.buffer_size,
|
|
"Sequential should get larger buffer than random"
|
|
);
|
|
|
|
#[cfg(feature = "io-scheduler-debug")]
|
|
{
|
|
assert!(sequential_strategy.debug_info.sequential_boost_applied, "Should mark sequential boost");
|
|
assert!(random_strategy.debug_info.random_penalty_applied, "Should mark random penalty");
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_multi_factor_strategy_unknown_media_conservative() {
|
|
// Unknown media should be conservative
|
|
let context = IoSchedulingContext {
|
|
file_size: 50 * 1024 * 1024,
|
|
base_buffer_size: 256 * 1024,
|
|
permit_wait_duration: Duration::from_millis(10),
|
|
is_sequential_hint: true,
|
|
access_pattern: AccessPattern::Sequential,
|
|
storage_media: StorageMedia::Unknown,
|
|
observed_bandwidth_bps: None,
|
|
concurrent_requests: 2,
|
|
};
|
|
|
|
let config = IoSchedulerConfig::default();
|
|
let strategy = IoStrategy::from_context_with_config(&context, &config);
|
|
|
|
assert_eq!(strategy.storage_media, StorageMedia::Unknown);
|
|
assert_eq!(strategy.bandwidth_tier, BandwidthTier::Unknown);
|
|
assert!(
|
|
strategy.buffer_size <= context.base_buffer_size,
|
|
"Unknown media should not exceed base buffer"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_multi_factor_strategy_priority_classification() {
|
|
// Test priority classification based on file size
|
|
let small_context = IoSchedulingContext {
|
|
file_size: 500 * 1024, // 500KB (High priority)
|
|
base_buffer_size: 256 * 1024,
|
|
permit_wait_duration: Duration::from_millis(10),
|
|
is_sequential_hint: false,
|
|
access_pattern: AccessPattern::Unknown,
|
|
storage_media: StorageMedia::Ssd,
|
|
observed_bandwidth_bps: Some(100 * 1024 * 1024),
|
|
concurrent_requests: 2,
|
|
};
|
|
|
|
let medium_context = IoSchedulingContext {
|
|
file_size: 5 * 1024 * 1024, // 5MB (Normal priority)
|
|
base_buffer_size: 256 * 1024,
|
|
permit_wait_duration: Duration::from_millis(10),
|
|
is_sequential_hint: false,
|
|
access_pattern: AccessPattern::Unknown,
|
|
storage_media: StorageMedia::Ssd,
|
|
observed_bandwidth_bps: Some(100 * 1024 * 1024),
|
|
concurrent_requests: 2,
|
|
};
|
|
|
|
let large_context = IoSchedulingContext {
|
|
file_size: 50 * 1024 * 1024, // 50MB (Low priority)
|
|
base_buffer_size: 256 * 1024,
|
|
permit_wait_duration: Duration::from_millis(10),
|
|
is_sequential_hint: false,
|
|
access_pattern: AccessPattern::Unknown,
|
|
storage_media: StorageMedia::Ssd,
|
|
observed_bandwidth_bps: Some(100 * 1024 * 1024),
|
|
concurrent_requests: 2,
|
|
};
|
|
|
|
let config = IoSchedulerConfig::default();
|
|
let small_strategy = IoStrategy::from_context_with_config(&small_context, &config);
|
|
let medium_strategy = IoStrategy::from_context_with_config(&medium_context, &config);
|
|
let large_strategy = IoStrategy::from_context_with_config(&large_context, &config);
|
|
|
|
assert_eq!(small_strategy.priority, IoPriority::High);
|
|
assert_eq!(medium_strategy.priority, IoPriority::Normal);
|
|
assert_eq!(large_strategy.priority, IoPriority::Low);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_multi_factor_strategy_readahead_decision_matrix() {
|
|
// Test readahead enable/disable logic
|
|
let configs = vec![
|
|
// (media, pattern, load, bandwidth, concurrency, expected_readahead, reason)
|
|
(
|
|
StorageMedia::Nvme,
|
|
AccessPattern::Sequential,
|
|
IoLoadLevel::Low,
|
|
BandwidthTier::High,
|
|
1,
|
|
true,
|
|
"all-favorable",
|
|
),
|
|
(
|
|
StorageMedia::Hdd,
|
|
AccessPattern::Random,
|
|
IoLoadLevel::Low,
|
|
BandwidthTier::Medium,
|
|
1,
|
|
false,
|
|
"random-pattern",
|
|
),
|
|
(
|
|
StorageMedia::Ssd,
|
|
AccessPattern::Sequential,
|
|
IoLoadLevel::High,
|
|
BandwidthTier::Medium,
|
|
1,
|
|
false,
|
|
"high-load",
|
|
),
|
|
(
|
|
StorageMedia::Nvme,
|
|
AccessPattern::Sequential,
|
|
IoLoadLevel::Low,
|
|
BandwidthTier::Low,
|
|
1,
|
|
false,
|
|
"low-bandwidth",
|
|
),
|
|
(
|
|
StorageMedia::Ssd,
|
|
AccessPattern::Random,
|
|
IoLoadLevel::Low,
|
|
BandwidthTier::High,
|
|
20,
|
|
false,
|
|
"high-concurrency-random",
|
|
),
|
|
];
|
|
|
|
for (media, pattern, load, bandwidth, concurrency, expected, reason) in configs {
|
|
let context = IoSchedulingContext {
|
|
file_size: 10 * 1024 * 1024,
|
|
base_buffer_size: 256 * 1024,
|
|
permit_wait_duration: match load {
|
|
IoLoadLevel::Low => Duration::from_millis(5),
|
|
IoLoadLevel::Medium => Duration::from_millis(30),
|
|
IoLoadLevel::High => Duration::from_millis(100),
|
|
IoLoadLevel::Critical => Duration::from_millis(300),
|
|
},
|
|
is_sequential_hint: matches!(pattern, AccessPattern::Sequential),
|
|
access_pattern: pattern,
|
|
storage_media: media,
|
|
observed_bandwidth_bps: match bandwidth {
|
|
BandwidthTier::Low => Some(5 * 1024 * 1024),
|
|
BandwidthTier::Medium => Some(100 * 1024 * 1024),
|
|
BandwidthTier::High => Some(500 * 1024 * 1024),
|
|
BandwidthTier::Unknown => None,
|
|
},
|
|
concurrent_requests: concurrency,
|
|
};
|
|
|
|
let config = IoSchedulerConfig::default();
|
|
let strategy = IoStrategy::from_context_with_config(&context, &config);
|
|
|
|
assert_eq!(
|
|
strategy.enable_readahead, expected,
|
|
"Readahead mismatch for case: {}, expected={}, got={}",
|
|
reason, expected, strategy.enable_readahead
|
|
);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_multi_factor_strategy_buffer_multiplier_stages() {
|
|
// Test that all multiplier stages are applied
|
|
let context = IoSchedulingContext {
|
|
file_size: 100 * 1024 * 1024,
|
|
base_buffer_size: 1024 * 1024, // 1MB base
|
|
permit_wait_duration: Duration::from_millis(100), // High load (0.5x)
|
|
is_sequential_hint: false,
|
|
access_pattern: AccessPattern::Random, // Penalty (0.8x)
|
|
storage_media: StorageMedia::Ssd,
|
|
observed_bandwidth_bps: Some(5 * 1024 * 1024), // Low bandwidth (0.6x)
|
|
concurrent_requests: 12, // High concurrency (0.75x)
|
|
};
|
|
|
|
let config = IoSchedulerConfig::default();
|
|
let strategy = IoStrategy::from_context_with_config(&context, &config);
|
|
|
|
// Expected multiplier: 1.0 * 0.5 (load) * 0.5 (concurrency, 12>=8) * 0.8 (random) * 0.6 (bandwidth)
|
|
// = 0.12x
|
|
let expected_min = (1024_f64 * 1024_f64) * 0.10_f64; // ~100KB
|
|
let expected_max = (1024_f64 * 1024_f64) * 0.15_f64; // ~150KB
|
|
|
|
assert!(
|
|
strategy.buffer_size >= expected_min as usize && strategy.buffer_size <= expected_max as usize,
|
|
"Buffer size {} should be in range [{}, {}] based on combined multipliers",
|
|
strategy.buffer_size,
|
|
expected_min,
|
|
expected_max
|
|
);
|
|
|
|
assert!(strategy.should_reduce_for_concurrency);
|
|
assert!(strategy.should_reduce_for_bandwidth);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn test_multi_factor_strategy_compatibility_path() {
|
|
// Test that compatibility path (from_wait_duration) still works
|
|
let wait_duration = Duration::from_millis(50);
|
|
let base_buffer = 256 * 1024;
|
|
|
|
let compat_strategy = IoStrategy::from_wait_duration(wait_duration, base_buffer);
|
|
|
|
// 50ms is >= high_threshold (50ms), so it's High load
|
|
assert_eq!(compat_strategy.load_level, IoLoadLevel::High);
|
|
assert!(compat_strategy.buffer_size > 0);
|
|
assert_eq!(compat_strategy.storage_media, StorageMedia::Unknown);
|
|
assert_eq!(compat_strategy.access_pattern, AccessPattern::Unknown);
|
|
}
|
|
}
|