mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-29 08:27:06 +00:00
chore(storage): drop dead backpressure and lock-optimizer wrappers (#6195)
Neither rustfs/src/storage/backpressure.rs nor rustfs/src/storage/lock_optimizer.rs had a production caller: their only non-self references were the pub mod lines in storage/mod.rs and a cfg(test) module, so the object transfer path never applied this backpressure and never took these lock shortcuts. The six removed tests in concurrent_fix_test.rs duplicated tests that lived inside the deleted files; the shared primitives they shadowed keep their own coverage in rustfs-io-core.
This commit is contained in:
@@ -14,9 +14,8 @@
|
|||||||
|
|
||||||
//! Shared backpressure policy type.
|
//! Shared backpressure policy type.
|
||||||
//!
|
//!
|
||||||
//! The runtime backpressure implementation (byte-watermark pipes and
|
//! This module only carries the watermark policy; the admission primitive it
|
||||||
//! monitors) lives in `rustfs/src/storage/backpressure.rs`; this module only
|
//! projects into lives in `rustfs-io-core`.
|
||||||
//! carries the watermark policy type that implementation shares.
|
|
||||||
|
|
||||||
use rustfs_io_core::BackpressureConfig as CoreBackpressureConfig;
|
use rustfs_io_core::BackpressureConfig as CoreBackpressureConfig;
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,6 @@ inventory. Generic function-local names such as `CACHE`, `LOCK`, `INIT`, and
|
|||||||
| `USE_STARSHARD_CACHE`, `BUCKET_CACHE_SMALL`, `BUCKET_CACHE_LARGE` | `rustfs/src/storage/ecfs_extend.rs` | Cache or constant / owner-local cache | Bucket validation cache backend selection and cache storage stay private to the ECFS extension owner. |
|
| `USE_STARSHARD_CACHE`, `BUCKET_CACHE_SMALL`, `BUCKET_CACHE_LARGE` | `rustfs/src/storage/ecfs_extend.rs` | Cache or constant / owner-local cache | Bucket validation cache backend selection and cache storage stay private to the ECFS extension owner. |
|
||||||
| `GLOBAL_SSE_DEK_PROVIDER`, `SSE_TEST_LOCK` | `rustfs/src/storage/sse.rs` | Owner-local cache / test state | SSE DEK provider cache and test serialization lock stay private to the SSE owner. |
|
| `GLOBAL_SSE_DEK_PROVIDER`, `SSE_TEST_LOCK` | `rustfs/src/storage/sse.rs` | Owner-local cache / test state | SSE DEK provider cache and test serialization lock stay private to the SSE owner. |
|
||||||
| `AUTH_FS` | `rustfs/src/storage/access.rs` | Cache or constant / owner-local cache | Authorization tag-condition lookup keeps its filesystem helper private to the access owner. |
|
| `AUTH_FS` | `rustfs/src/storage/access.rs` | Cache or constant / owner-local cache | Authorization tag-condition lookup keeps its filesystem helper private to the access owner. |
|
||||||
| `LOCK_STATS` | `rustfs/src/storage/lock_optimizer.rs` | Process-global owner-local metrics | Lock optimization statistics stay private behind lock optimizer helper APIs. |
|
|
||||||
| `DEADLOCK_DETECTOR` | `rustfs/src/storage/deadlock_detector.rs` | Process-global owner-local state | Deadlock detector lifecycle state stays private to the storage deadlock detector owner. |
|
| `DEADLOCK_DETECTOR` | `rustfs/src/storage/deadlock_detector.rs` | Process-global owner-local state | Deadlock detector lifecycle state stays private to the storage deadlock detector owner. |
|
||||||
| `CONCURRENCY_MANAGER`, `ACTIVE_GET_REQUESTS`, `ACTIVE_PUT_REQUESTS` | `rustfs/src/storage/concurrency/*` | Process-global owner-local scheduler state | Storage concurrency manager and request counters remain inside the storage concurrency owner boundary. |
|
| `CONCURRENCY_MANAGER`, `ACTIVE_GET_REQUESTS`, `ACTIVE_PUT_REQUESTS` | `rustfs/src/storage/concurrency/*` | Process-global owner-local scheduler state | Storage concurrency manager and request counters remain inside the storage concurrency owner boundary. |
|
||||||
| `GET_OBJECT_BUFFER_THRESHOLD_WARNED`, `GET_READER_STREAM_BUFFER_SIZE_OVERRIDE`, function-local `ENABLED`, `OBJECT_SEEK_SUPPORT_THRESHOLD`, `OBJECT_SEEK_SUPPORT_CONCURRENCY_THRESHOLDS` | `rustfs/src/app/object_usecase.rs` | Cache or constant / owner-local cache | Object GET/seek tuning caches and warning guards stay private to object usecase helpers. |
|
| `GET_OBJECT_BUFFER_THRESHOLD_WARNED`, `GET_READER_STREAM_BUFFER_SIZE_OVERRIDE`, function-local `ENABLED`, `OBJECT_SEEK_SUPPORT_THRESHOLD`, `OBJECT_SEEK_SUPPORT_CONCURRENCY_THRESHOLDS` | `rustfs/src/app/object_usecase.rs` | Cache or constant / owner-local cache | Object GET/seek tuning caches and warning guards stay private to object usecase helpers. |
|
||||||
|
|||||||
@@ -1,618 +0,0 @@
|
|||||||
// 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.
|
|
||||||
|
|
||||||
//! Backpressure Management for Object Data Transfer.
|
|
||||||
//!
|
|
||||||
//! This module provides backpressure-aware pipes for object data transfer,
|
|
||||||
//! preventing buffer overflow and memory exhaustion under high concurrency.
|
|
||||||
|
|
||||||
//! # Key Features
|
|
||||||
//!
|
|
||||||
//! - Configurable buffer size with high/low watermarks
|
|
||||||
//! - Backpressure state monitoring and events
|
|
||||||
//! - Backpressure metrics emitted through the shared metrics pipeline
|
|
||||||
//! - Graceful handling of slow consumers
|
|
||||||
//!
|
|
||||||
//! # Architecture
|
|
||||||
//!
|
|
||||||
//! ```text
|
|
||||||
//! [Disk Reader] --> [BackpressurePipe] --> [HTTP Response]
|
|
||||||
//! |
|
|
||||||
//! v
|
|
||||||
//! [Buffer Monitor]
|
|
||||||
//! |
|
|
||||||
//! v
|
|
||||||
//! [High Watermark?] --> Apply Backpressure
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
use tokio::io::{DuplexStream, duplex};
|
|
||||||
use tracing::{debug, warn};
|
|
||||||
|
|
||||||
use metrics::counter;
|
|
||||||
use rustfs_concurrency::PipeBackpressurePolicy;
|
|
||||||
use rustfs_io_core::BackpressureConfig as CoreBackpressureConfig;
|
|
||||||
|
|
||||||
/// Object-transfer duplex pipe backpressure policy.
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct ObjectPipeBackpressurePolicy {
|
|
||||||
/// Buffer size in bytes (default 4MB).
|
|
||||||
pub buffer_size: usize,
|
|
||||||
/// High watermark percentage (default 80%).
|
|
||||||
/// When buffer usage exceeds this, backpressure is applied.
|
|
||||||
pub high_watermark: u32,
|
|
||||||
/// Low watermark percentage (default 50%).
|
|
||||||
/// When buffer usage drops below this after high watermark, backpressure is released.
|
|
||||||
pub low_watermark: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ObjectPipeBackpressurePolicy {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
buffer_size: rustfs_config::DEFAULT_OBJECT_DUPLEX_BUFFER_SIZE,
|
|
||||||
high_watermark: rustfs_config::DEFAULT_OBJECT_BACKPRESSURE_HIGH_WATERMARK,
|
|
||||||
low_watermark: rustfs_config::DEFAULT_OBJECT_BACKPRESSURE_LOW_WATERMARK,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ObjectPipeBackpressurePolicy {
|
|
||||||
/// Load configuration from environment variables.
|
|
||||||
pub fn from_env() -> Self {
|
|
||||||
let buffer_size = rustfs_utils::get_env_usize(
|
|
||||||
rustfs_config::ENV_OBJECT_DUPLEX_BUFFER_SIZE,
|
|
||||||
rustfs_config::DEFAULT_OBJECT_DUPLEX_BUFFER_SIZE,
|
|
||||||
);
|
|
||||||
let high_watermark = rustfs_utils::get_env_u32(
|
|
||||||
rustfs_config::ENV_OBJECT_BACKPRESSURE_HIGH_WATERMARK,
|
|
||||||
rustfs_config::DEFAULT_OBJECT_BACKPRESSURE_HIGH_WATERMARK,
|
|
||||||
);
|
|
||||||
let low_watermark = rustfs_utils::get_env_u32(
|
|
||||||
rustfs_config::ENV_OBJECT_BACKPRESSURE_LOW_WATERMARK,
|
|
||||||
rustfs_config::DEFAULT_OBJECT_BACKPRESSURE_LOW_WATERMARK,
|
|
||||||
);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
buffer_size,
|
|
||||||
high_watermark,
|
|
||||||
low_watermark,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculate high watermark threshold in bytes.
|
|
||||||
pub fn high_watermark_bytes(&self) -> usize {
|
|
||||||
(self.buffer_size as u64 * self.high_watermark as u64 / 100) as usize
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculate low watermark threshold in bytes.
|
|
||||||
pub fn low_watermark_bytes(&self) -> usize {
|
|
||||||
(self.buffer_size as u64 * self.low_watermark as u64 / 100) as usize
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Project this object-transfer policy into the shared concurrency facade policy.
|
|
||||||
pub fn to_concurrency_policy(&self) -> PipeBackpressurePolicy {
|
|
||||||
PipeBackpressurePolicy {
|
|
||||||
buffer_size: self.buffer_size,
|
|
||||||
high_watermark: self.high_watermark,
|
|
||||||
low_watermark: self.low_watermark,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Project this object-transfer policy into the reusable io-core admission config.
|
|
||||||
pub fn to_core_config(&self) -> CoreBackpressureConfig {
|
|
||||||
self.to_concurrency_policy().to_core_config()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Backpressure state.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum BackpressureState {
|
|
||||||
/// Normal operation, buffer usage is below high watermark.
|
|
||||||
Normal,
|
|
||||||
/// Buffer usage is above high watermark, backpressure should be applied.
|
|
||||||
HighWatermark,
|
|
||||||
/// Backpressure is actively being applied to the producer.
|
|
||||||
BackpressureApplied,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for BackpressureState {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
BackpressureState::Normal => write!(f, "normal"),
|
|
||||||
BackpressureState::HighWatermark => write!(f, "high_watermark"),
|
|
||||||
BackpressureState::BackpressureApplied => write!(f, "backpressure_applied"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compact metadata snapshot for object-transfer backpressure pipes.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct BackpressurePipeMeta {
|
|
||||||
/// Buffer capacity in bytes.
|
|
||||||
pub buffer_capacity: usize,
|
|
||||||
/// Current backpressure state.
|
|
||||||
pub state: BackpressureState,
|
|
||||||
/// Age of the pipe since creation.
|
|
||||||
pub age: Duration,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compact metadata snapshot for the lightweight backpressure monitor.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub struct BackpressureMonitorMeta {
|
|
||||||
/// Buffer capacity in bytes.
|
|
||||||
pub buffer_capacity: usize,
|
|
||||||
/// Current buffer usage percentage.
|
|
||||||
pub usage_percent: f32,
|
|
||||||
/// Current backpressure state.
|
|
||||||
pub state: BackpressureState,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn calculate_usage_percent(usage: usize, capacity: usize) -> f32 {
|
|
||||||
if capacity > 0 {
|
|
||||||
(usage as f32 / capacity as f32) * 100.0
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_watermark_transition(
|
|
||||||
in_high_watermark: &AtomicBool,
|
|
||||||
usage: usize,
|
|
||||||
high: usize,
|
|
||||||
low: usize,
|
|
||||||
) -> (BackpressureState, bool) {
|
|
||||||
let current = in_high_watermark.load(Ordering::Acquire);
|
|
||||||
let next_state = if usage >= high {
|
|
||||||
BackpressureState::HighWatermark
|
|
||||||
} else if usage <= low {
|
|
||||||
BackpressureState::Normal
|
|
||||||
} else if current {
|
|
||||||
BackpressureState::HighWatermark
|
|
||||||
} else {
|
|
||||||
BackpressureState::Normal
|
|
||||||
};
|
|
||||||
let next_is_high = matches!(next_state, BackpressureState::HighWatermark);
|
|
||||||
let changed = in_high_watermark.swap(next_is_high, Ordering::AcqRel) != next_is_high;
|
|
||||||
(next_state, changed)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn saturating_sub_atomic(value: &AtomicUsize, delta: usize) {
|
|
||||||
value
|
|
||||||
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| Some(current.saturating_sub(delta)))
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A backpressure-aware pipe wrapping tokio's duplex.
|
|
||||||
///
|
|
||||||
/// This provides monitoring and events for backpressure conditions
|
|
||||||
/// while maintaining compatibility with the standard duplex interface.
|
|
||||||
pub struct BackpressurePipe {
|
|
||||||
/// Reader end of the duplex pipe.
|
|
||||||
reader: DuplexStream,
|
|
||||||
/// Writer end of the duplex pipe.
|
|
||||||
writer: DuplexStream,
|
|
||||||
/// Configuration.
|
|
||||||
config: ObjectPipeBackpressurePolicy,
|
|
||||||
/// Current buffer usage (approximate, updated on write).
|
|
||||||
buffer_usage: AtomicUsize,
|
|
||||||
/// Current backpressure state.
|
|
||||||
state: AtomicBool, // true = in high watermark state
|
|
||||||
/// Total bytes written.
|
|
||||||
total_written: AtomicUsize,
|
|
||||||
/// Total bytes read.
|
|
||||||
total_read: AtomicUsize,
|
|
||||||
/// Cached high watermark threshold in bytes.
|
|
||||||
high_watermark_bytes: usize,
|
|
||||||
/// Cached low watermark threshold in bytes.
|
|
||||||
low_watermark_bytes: usize,
|
|
||||||
/// Pipe creation timestamp.
|
|
||||||
created_at: Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BackpressurePipe {
|
|
||||||
/// Create a new backpressure-aware pipe with default configuration.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::with_config(ObjectPipeBackpressurePolicy::from_env())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new backpressure-aware pipe with custom configuration.
|
|
||||||
pub fn with_config(config: ObjectPipeBackpressurePolicy) -> Self {
|
|
||||||
let policy = config.to_concurrency_policy();
|
|
||||||
let (reader, writer) = duplex(policy.buffer_size);
|
|
||||||
let high_watermark_bytes = policy.high_watermark_bytes();
|
|
||||||
let low_watermark_bytes = policy.low_watermark_bytes();
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
buffer_size = config.buffer_size,
|
|
||||||
high_watermark = config.high_watermark,
|
|
||||||
low_watermark = config.low_watermark,
|
|
||||||
high_watermark_bytes,
|
|
||||||
low_watermark_bytes,
|
|
||||||
"Created backpressure pipe"
|
|
||||||
);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
reader,
|
|
||||||
writer,
|
|
||||||
config,
|
|
||||||
buffer_usage: AtomicUsize::new(0),
|
|
||||||
state: AtomicBool::new(false),
|
|
||||||
total_written: AtomicUsize::new(0),
|
|
||||||
total_read: AtomicUsize::new(0),
|
|
||||||
high_watermark_bytes,
|
|
||||||
low_watermark_bytes,
|
|
||||||
created_at: Instant::now(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Take the reader end of the pipe (consumes self).
|
|
||||||
pub fn into_reader(self) -> DuplexStream {
|
|
||||||
self.reader
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Take the writer end of the pipe (consumes self).
|
|
||||||
pub fn into_writer(self) -> DuplexStream {
|
|
||||||
self.writer
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Split into reader and writer (consumes self).
|
|
||||||
pub fn split(self) -> (DuplexStream, DuplexStream) {
|
|
||||||
(self.reader, self.writer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get current backpressure state.
|
|
||||||
pub fn state(&self) -> BackpressureState {
|
|
||||||
if self.state.load(Ordering::Acquire) {
|
|
||||||
BackpressureState::BackpressureApplied
|
|
||||||
} else {
|
|
||||||
BackpressureState::Normal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a compact metadata snapshot for the pipe.
|
|
||||||
pub fn meta(&self) -> BackpressurePipeMeta {
|
|
||||||
BackpressurePipeMeta {
|
|
||||||
buffer_capacity: self.config.buffer_size,
|
|
||||||
state: self.state(),
|
|
||||||
age: self.age(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the age of this pipe.
|
|
||||||
pub fn age(&self) -> Duration {
|
|
||||||
self.created_at.elapsed()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get current buffer usage.
|
|
||||||
pub fn usage(&self) -> usize {
|
|
||||||
self.buffer_usage.load(Ordering::Acquire)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Record bytes written (call after successful write).
|
|
||||||
pub fn record_write(&self, bytes: usize) {
|
|
||||||
self.total_written.fetch_add(bytes, Ordering::Relaxed);
|
|
||||||
self.buffer_usage.fetch_add(bytes, Ordering::Release);
|
|
||||||
self.update_watermark_state();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Record bytes read (call after successful read).
|
|
||||||
pub fn record_read(&self, bytes: usize) {
|
|
||||||
self.total_read.fetch_add(bytes, Ordering::Relaxed);
|
|
||||||
saturating_sub_atomic(&self.buffer_usage, bytes);
|
|
||||||
self.update_watermark_state();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update watermark state and emit transition signals.
|
|
||||||
fn update_watermark_state(&self) {
|
|
||||||
let usage = self.buffer_usage.load(Ordering::Acquire);
|
|
||||||
let usage_percent = calculate_usage_percent(usage, self.config.buffer_size) as u32;
|
|
||||||
let (next_state, changed) =
|
|
||||||
apply_watermark_transition(&self.state, usage, self.high_watermark_bytes, self.low_watermark_bytes);
|
|
||||||
|
|
||||||
if changed {
|
|
||||||
match next_state {
|
|
||||||
BackpressureState::HighWatermark => {
|
|
||||||
counter!("rustfs_backpressure_events_total", "state" => "high_watermark").increment(1);
|
|
||||||
|
|
||||||
warn!(
|
|
||||||
buffer_usage = usage,
|
|
||||||
buffer_capacity = self.config.buffer_size,
|
|
||||||
usage_percent,
|
|
||||||
high_watermark = self.config.high_watermark,
|
|
||||||
"Backpressure: high watermark reached"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
BackpressureState::Normal => {
|
|
||||||
counter!("rustfs_backpressure_events_total", "state" => "normal").increment(1);
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
buffer_usage = usage,
|
|
||||||
buffer_capacity = self.config.buffer_size,
|
|
||||||
usage_percent,
|
|
||||||
low_watermark = self.config.low_watermark,
|
|
||||||
"Backpressure: returned to normal"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
BackpressureState::BackpressureApplied => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get total bytes written.
|
|
||||||
pub fn total_written(&self) -> usize {
|
|
||||||
self.total_written.load(Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get total bytes read.
|
|
||||||
pub fn total_read(&self) -> usize {
|
|
||||||
self.total_read.load(Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get buffer capacity.
|
|
||||||
pub fn capacity(&self) -> usize {
|
|
||||||
self.config.buffer_size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for BackpressurePipe {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A simple wrapper that provides backpressure monitoring for duplex streams.
|
|
||||||
///
|
|
||||||
/// This is a lighter-weight alternative to `BackpressurePipe` that doesn't
|
|
||||||
/// wrap the streams but provides monitoring capabilities.
|
|
||||||
pub struct BackpressureMonitor {
|
|
||||||
/// Configuration.
|
|
||||||
config: ObjectPipeBackpressurePolicy,
|
|
||||||
/// Current buffer usage.
|
|
||||||
buffer_usage: AtomicUsize,
|
|
||||||
/// In high watermark state.
|
|
||||||
in_high_watermark: AtomicBool,
|
|
||||||
/// Cached high watermark threshold in bytes.
|
|
||||||
high_watermark_bytes: usize,
|
|
||||||
/// Cached low watermark threshold in bytes.
|
|
||||||
low_watermark_bytes: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BackpressureMonitor {
|
|
||||||
/// Create a new monitor with default configuration.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::with_config(ObjectPipeBackpressurePolicy::from_env())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new monitor with custom configuration.
|
|
||||||
pub fn with_config(config: ObjectPipeBackpressurePolicy) -> Self {
|
|
||||||
let policy = config.to_concurrency_policy();
|
|
||||||
let high_watermark_bytes = policy.high_watermark_bytes();
|
|
||||||
let low_watermark_bytes = policy.low_watermark_bytes();
|
|
||||||
Self {
|
|
||||||
config,
|
|
||||||
buffer_usage: AtomicUsize::new(0),
|
|
||||||
in_high_watermark: AtomicBool::new(false),
|
|
||||||
high_watermark_bytes,
|
|
||||||
low_watermark_bytes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Record bytes added to buffer.
|
|
||||||
pub fn on_write(&self, bytes: usize) -> BackpressureState {
|
|
||||||
self.buffer_usage.fetch_add(bytes, Ordering::Release);
|
|
||||||
self.update_state()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Record bytes removed from buffer.
|
|
||||||
pub fn on_read(&self, bytes: usize) -> BackpressureState {
|
|
||||||
saturating_sub_atomic(&self.buffer_usage, bytes);
|
|
||||||
self.update_state()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get current state.
|
|
||||||
pub fn state(&self) -> BackpressureState {
|
|
||||||
if self.in_high_watermark.load(Ordering::Acquire) {
|
|
||||||
BackpressureState::HighWatermark
|
|
||||||
} else {
|
|
||||||
BackpressureState::Normal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get current buffer usage.
|
|
||||||
pub fn usage(&self) -> usize {
|
|
||||||
self.buffer_usage.load(Ordering::Acquire)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get usage percentage.
|
|
||||||
pub fn usage_percent(&self) -> f32 {
|
|
||||||
let usage = self.buffer_usage.load(Ordering::Acquire);
|
|
||||||
calculate_usage_percent(usage, self.config.buffer_size)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a compact metadata snapshot for the monitor.
|
|
||||||
pub fn meta(&self) -> BackpressureMonitorMeta {
|
|
||||||
let usage = self.buffer_usage.load(Ordering::Acquire);
|
|
||||||
BackpressureMonitorMeta {
|
|
||||||
buffer_capacity: self.config.buffer_size,
|
|
||||||
usage_percent: calculate_usage_percent(usage, self.config.buffer_size),
|
|
||||||
state: self.state(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update state based on current usage.
|
|
||||||
fn update_state(&self) -> BackpressureState {
|
|
||||||
let usage = self.buffer_usage.load(Ordering::Acquire);
|
|
||||||
let usage_percent = calculate_usage_percent(usage, self.config.buffer_size) as u32;
|
|
||||||
let (next_state, changed) =
|
|
||||||
apply_watermark_transition(&self.in_high_watermark, usage, self.high_watermark_bytes, self.low_watermark_bytes);
|
|
||||||
|
|
||||||
if matches!(next_state, BackpressureState::HighWatermark) {
|
|
||||||
if changed {
|
|
||||||
counter!("rustfs_backpressure_events_total", "state" => "high_watermark").increment(1);
|
|
||||||
|
|
||||||
debug!(usage_percent, "Backpressure: entered high watermark");
|
|
||||||
}
|
|
||||||
BackpressureState::HighWatermark
|
|
||||||
} else {
|
|
||||||
if changed {
|
|
||||||
counter!("rustfs_backpressure_events_total", "state" => "normal").increment(1);
|
|
||||||
|
|
||||||
debug!(usage_percent, "Backpressure: returned to normal");
|
|
||||||
}
|
|
||||||
BackpressureState::Normal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for BackpressureMonitor {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[allow(unused_imports)]
|
|
||||||
mod tests {
|
|
||||||
use super::{BackpressureMonitor, BackpressurePipe, BackpressureState, ObjectPipeBackpressurePolicy};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_backpressure_config_default() {
|
|
||||||
let config = ObjectPipeBackpressurePolicy::default();
|
|
||||||
assert_eq!(config.buffer_size, 4 * 1024 * 1024);
|
|
||||||
assert_eq!(config.high_watermark, 80);
|
|
||||||
assert_eq!(config.low_watermark, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_backpressure_config_watermarks() {
|
|
||||||
let config = ObjectPipeBackpressurePolicy {
|
|
||||||
buffer_size: 1000,
|
|
||||||
high_watermark: 80,
|
|
||||||
low_watermark: 50,
|
|
||||||
};
|
|
||||||
assert_eq!(config.high_watermark_bytes(), 800);
|
|
||||||
assert_eq!(config.low_watermark_bytes(), 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_backpressure_policy_projects_to_concurrency_and_core_config() {
|
|
||||||
let config = ObjectPipeBackpressurePolicy {
|
|
||||||
buffer_size: 2000,
|
|
||||||
high_watermark: 75,
|
|
||||||
low_watermark: 40,
|
|
||||||
};
|
|
||||||
let concurrency = config.to_concurrency_policy();
|
|
||||||
let core = config.to_core_config();
|
|
||||||
|
|
||||||
assert_eq!(concurrency.buffer_size, config.buffer_size);
|
|
||||||
assert_eq!(concurrency.high_watermark, config.high_watermark);
|
|
||||||
assert_eq!(concurrency.low_watermark, config.low_watermark);
|
|
||||||
assert_eq!(core.high_water_mark, 0.75);
|
|
||||||
assert_eq!(core.low_water_mark, 0.40);
|
|
||||||
assert!(core.enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_backpressure_pipe_consumes_concurrency_policy_thresholds() {
|
|
||||||
let config = ObjectPipeBackpressurePolicy {
|
|
||||||
buffer_size: 2000,
|
|
||||||
high_watermark: 75,
|
|
||||||
low_watermark: 40,
|
|
||||||
};
|
|
||||||
let concurrency = config.to_concurrency_policy();
|
|
||||||
let pipe = BackpressurePipe::with_config(config);
|
|
||||||
|
|
||||||
assert_eq!(pipe.capacity(), concurrency.buffer_size);
|
|
||||||
assert_eq!(pipe.high_watermark_bytes, concurrency.high_watermark_bytes());
|
|
||||||
assert_eq!(pipe.low_watermark_bytes, concurrency.low_watermark_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_backpressure_monitor_consumes_concurrency_policy_thresholds() {
|
|
||||||
let config = ObjectPipeBackpressurePolicy {
|
|
||||||
buffer_size: 2000,
|
|
||||||
high_watermark: 75,
|
|
||||||
low_watermark: 40,
|
|
||||||
};
|
|
||||||
let concurrency = config.to_concurrency_policy();
|
|
||||||
let monitor = BackpressureMonitor::with_config(config);
|
|
||||||
|
|
||||||
assert_eq!(monitor.meta().buffer_capacity, concurrency.buffer_size);
|
|
||||||
assert_eq!(monitor.high_watermark_bytes, concurrency.high_watermark_bytes());
|
|
||||||
assert_eq!(monitor.low_watermark_bytes, concurrency.low_watermark_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_backpressure_state_display() {
|
|
||||||
assert_eq!(format!("{}", BackpressureState::Normal), "normal");
|
|
||||||
assert_eq!(format!("{}", BackpressureState::HighWatermark), "high_watermark");
|
|
||||||
assert_eq!(format!("{}", BackpressureState::BackpressureApplied), "backpressure_applied");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_backpressure_monitor() {
|
|
||||||
let config = ObjectPipeBackpressurePolicy {
|
|
||||||
buffer_size: 1000,
|
|
||||||
high_watermark: 80,
|
|
||||||
low_watermark: 50,
|
|
||||||
};
|
|
||||||
let monitor = BackpressureMonitor::with_config(config);
|
|
||||||
|
|
||||||
// Initially normal
|
|
||||||
assert_eq!(monitor.state(), BackpressureState::Normal);
|
|
||||||
assert_eq!(monitor.meta().buffer_capacity, 1000);
|
|
||||||
assert_eq!(monitor.meta().usage_percent, 0.0);
|
|
||||||
|
|
||||||
// Write to reach high watermark
|
|
||||||
let state = monitor.on_write(850);
|
|
||||||
assert_eq!(state, BackpressureState::HighWatermark);
|
|
||||||
assert_eq!(monitor.meta().usage_percent, 85.0);
|
|
||||||
|
|
||||||
// Read to go below low watermark
|
|
||||||
let state = monitor.on_read(400);
|
|
||||||
assert_eq!(state, BackpressureState::Normal);
|
|
||||||
assert_eq!(monitor.meta().usage_percent, 45.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_backpressure_pipe_creation() {
|
|
||||||
let pipe = BackpressurePipe::new();
|
|
||||||
assert_eq!(pipe.capacity(), 4 * 1024 * 1024);
|
|
||||||
assert_eq!(pipe.state(), BackpressureState::Normal);
|
|
||||||
assert_eq!(pipe.meta().buffer_capacity, 4 * 1024 * 1024);
|
|
||||||
assert!(pipe.meta().age <= pipe.age());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_backpressure_pipe_state_transitions() {
|
|
||||||
let config = ObjectPipeBackpressurePolicy {
|
|
||||||
buffer_size: 1000,
|
|
||||||
high_watermark: 80,
|
|
||||||
low_watermark: 50,
|
|
||||||
};
|
|
||||||
let pipe = BackpressurePipe::with_config(config);
|
|
||||||
|
|
||||||
assert_eq!(pipe.state(), BackpressureState::Normal);
|
|
||||||
assert_eq!(pipe.meta().state, BackpressureState::Normal);
|
|
||||||
|
|
||||||
pipe.record_write(850);
|
|
||||||
assert_eq!(pipe.state(), BackpressureState::BackpressureApplied);
|
|
||||||
assert_eq!(pipe.meta().state, BackpressureState::BackpressureApplied);
|
|
||||||
|
|
||||||
pipe.record_read(400);
|
|
||||||
assert_eq!(pipe.state(), BackpressureState::Normal);
|
|
||||||
assert_eq!(pipe.meta().state, BackpressureState::Normal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,17 +14,15 @@
|
|||||||
|
|
||||||
//! Integration tests for concurrent request fix.
|
//! Integration tests for concurrent request fix.
|
||||||
//!
|
//!
|
||||||
//! These tests verify that the timeout, backpressure, and deadlock detection
|
//! These tests verify that the timeout and deadlock detection mechanisms work
|
||||||
//! mechanisms work correctly under high concurrency scenarios.
|
//! correctly under high concurrency scenarios.
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::storage::backpressure::{BackpressureMonitor, BackpressureState, ObjectPipeBackpressurePolicy};
|
|
||||||
use crate::storage::concurrency::{IoLoadLevel, IoPriority};
|
use crate::storage::concurrency::{IoLoadLevel, IoPriority};
|
||||||
use crate::storage::deadlock_detector::{
|
use crate::storage::deadlock_detector::{
|
||||||
DeadlockDetector, LockInfo, LockType, RequestHangDetectionPolicy, RequestResourceTracker,
|
DeadlockDetector, LockInfo, LockType, RequestHangDetectionPolicy, RequestResourceTracker,
|
||||||
};
|
};
|
||||||
use crate::storage::lock_optimizer::{LockOptimizeConfig, LockOptimizer, LockStats};
|
|
||||||
use crate::storage::timeout_wrapper::{GetObjectTimeoutPolicy, RequestTimeoutWrapper, TimedGetObjectResult};
|
use crate::storage::timeout_wrapper::{GetObjectTimeoutPolicy, RequestTimeoutWrapper, TimedGetObjectResult};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -114,82 +112,6 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// Backpressure Tests
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_backpressure_config_defaults() {
|
|
||||||
let config = ObjectPipeBackpressurePolicy::default();
|
|
||||||
assert_eq!(config.buffer_size, 4 * 1024 * 1024); // 4MB
|
|
||||||
assert_eq!(config.high_watermark, 80);
|
|
||||||
assert_eq!(config.low_watermark, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_backpressure_monitor_state_transitions() {
|
|
||||||
let config = ObjectPipeBackpressurePolicy {
|
|
||||||
buffer_size: 1000,
|
|
||||||
high_watermark: 80,
|
|
||||||
low_watermark: 50,
|
|
||||||
};
|
|
||||||
let monitor = BackpressureMonitor::with_config(config);
|
|
||||||
|
|
||||||
// Initially normal
|
|
||||||
assert_eq!(monitor.state(), BackpressureState::Normal);
|
|
||||||
|
|
||||||
// Write to reach high watermark
|
|
||||||
let state = monitor.on_write(850);
|
|
||||||
assert_eq!(state, BackpressureState::HighWatermark);
|
|
||||||
|
|
||||||
// Read to go below low watermark
|
|
||||||
let state = monitor.on_read(400);
|
|
||||||
assert_eq!(state, BackpressureState::Normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_backpressure_usage_percent() {
|
|
||||||
let config = ObjectPipeBackpressurePolicy {
|
|
||||||
buffer_size: 1000,
|
|
||||||
high_watermark: 80,
|
|
||||||
low_watermark: 50,
|
|
||||||
};
|
|
||||||
let monitor = BackpressureMonitor::with_config(config);
|
|
||||||
|
|
||||||
monitor.on_write(500);
|
|
||||||
assert!((monitor.usage_percent() - 50.0).abs() < 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// Lock Optimizer Tests
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_lock_optimize_config_defaults() {
|
|
||||||
let config = LockOptimizeConfig::default();
|
|
||||||
assert!(config.enabled);
|
|
||||||
assert_eq!(config.acquire_timeout, Duration::from_secs(5));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_lock_stats_tracking() {
|
|
||||||
let stats = LockStats::new();
|
|
||||||
|
|
||||||
stats.record_acquire();
|
|
||||||
stats.record_early_release(Duration::from_millis(100));
|
|
||||||
stats.record_early_release(Duration::from_millis(200));
|
|
||||||
|
|
||||||
assert_eq!(stats.locks_acquired.load(std::sync::atomic::Ordering::Relaxed), 1);
|
|
||||||
assert_eq!(stats.locks_released_early.load(std::sync::atomic::Ordering::Relaxed), 2);
|
|
||||||
assert_eq!(stats.max_hold_time(), Duration::from_millis(200));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_lock_optimizer_creation() {
|
|
||||||
let optimizer = LockOptimizer::new();
|
|
||||||
assert!(optimizer.is_enabled());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// I/O Priority Tests
|
// I/O Priority Tests
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -1,458 +0,0 @@
|
|||||||
// 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.
|
|
||||||
|
|
||||||
//! Lock Optimization for GetObject Operations.
|
|
||||||
//!
|
|
||||||
//! 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
|
|
||||||
//! # Key Features
|
|
||||||
//!
|
|
||||||
//! - Early lock release after metadata read
|
|
||||||
//! - Lock hold time monitoring
|
|
||||||
//! - Configurable optimization (can be disabled for debugging)
|
|
||||||
//! - Lock contention metrics emitted through the shared metrics pipeline
|
|
||||||
//!
|
|
||||||
//! # Architecture
|
|
||||||
//!
|
|
||||||
//! ```text
|
|
||||||
//! Traditional: [Acquire Lock] --> [Read Metadata] --> [Transfer Data] --> [Release Lock]
|
|
||||||
//! |<------------------ Lock Held ------------------>|
|
|
||||||
//!
|
|
||||||
//! Optimized: [Acquire Lock] --> [Read Metadata] --> [Release Lock] --> [Transfer Data]
|
|
||||||
//! |<- Lock Held ->|
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
use tracing::debug;
|
|
||||||
|
|
||||||
use metrics::histogram;
|
|
||||||
|
|
||||||
/// Lock optimization configuration.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct LockOptimizeConfig {
|
|
||||||
/// Whether to enable lock optimization.
|
|
||||||
/// When enabled, read locks are released after metadata read.
|
|
||||||
/// When disabled, locks are held for the entire operation (traditional behavior).
|
|
||||||
pub enabled: bool,
|
|
||||||
/// Lock acquisition timeout.
|
|
||||||
pub acquire_timeout: Duration,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for LockOptimizeConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
enabled: rustfs_config::DEFAULT_OBJECT_LOCK_OPTIMIZATION_ENABLE,
|
|
||||||
acquire_timeout: Duration::from_secs(rustfs_config::DEFAULT_OBJECT_LOCK_ACQUIRE_TIMEOUT),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LockOptimizeConfig {
|
|
||||||
/// Load configuration from environment variables.
|
|
||||||
pub fn from_env() -> Self {
|
|
||||||
let enabled = rustfs_utils::get_env_bool(
|
|
||||||
rustfs_config::ENV_OBJECT_LOCK_OPTIMIZATION_ENABLE,
|
|
||||||
rustfs_config::DEFAULT_OBJECT_LOCK_OPTIMIZATION_ENABLE,
|
|
||||||
);
|
|
||||||
let acquire_timeout = Duration::from_secs(rustfs_utils::get_env_u64(
|
|
||||||
rustfs_config::ENV_OBJECT_LOCK_ACQUIRE_TIMEOUT,
|
|
||||||
rustfs_config::DEFAULT_OBJECT_LOCK_ACQUIRE_TIMEOUT,
|
|
||||||
));
|
|
||||||
|
|
||||||
Self {
|
|
||||||
enabled,
|
|
||||||
acquire_timeout,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Statistics for lock optimization monitoring.
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct LockStats {
|
|
||||||
/// Total locks acquired.
|
|
||||||
pub locks_acquired: AtomicU64,
|
|
||||||
/// Total locks released early.
|
|
||||||
pub locks_released_early: AtomicU64,
|
|
||||||
/// Total lock hold time in microseconds.
|
|
||||||
pub total_hold_time_us: AtomicU64,
|
|
||||||
/// Maximum lock hold time in microseconds.
|
|
||||||
pub max_hold_time_us: AtomicU64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LockStats {
|
|
||||||
/// Create new lock statistics.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Record a lock acquisition.
|
|
||||||
pub fn record_acquire(&self) {
|
|
||||||
self.locks_acquired.fetch_add(1, Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Record an early lock release.
|
|
||||||
pub fn record_early_release(&self, hold_time: Duration) {
|
|
||||||
self.locks_released_early.fetch_add(1, Ordering::Relaxed);
|
|
||||||
self.record_hold_time(hold_time);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Record lock hold time.
|
|
||||||
fn record_hold_time(&self, hold_time: Duration) {
|
|
||||||
let hold_time_us = hold_time.as_micros() as u64;
|
|
||||||
self.total_hold_time_us.fetch_add(hold_time_us, Ordering::Relaxed);
|
|
||||||
|
|
||||||
// Update max hold time
|
|
||||||
let mut current_max = self.max_hold_time_us.load(Ordering::Relaxed);
|
|
||||||
while hold_time_us > current_max {
|
|
||||||
match self
|
|
||||||
.max_hold_time_us
|
|
||||||
.compare_exchange_weak(current_max, hold_time_us, Ordering::Relaxed, Ordering::Relaxed)
|
|
||||||
{
|
|
||||||
Ok(_) => break,
|
|
||||||
Err(actual) => current_max = actual,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get average hold time.
|
|
||||||
pub fn avg_hold_time(&self) -> Duration {
|
|
||||||
let total = self.total_hold_time_us.load(Ordering::Relaxed);
|
|
||||||
let count = self.locks_released_early.load(Ordering::Relaxed);
|
|
||||||
total.checked_div(count).map(Duration::from_micros).unwrap_or(Duration::ZERO)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get maximum hold time.
|
|
||||||
pub fn max_hold_time(&self) -> Duration {
|
|
||||||
Duration::from_micros(self.max_hold_time_us.load(Ordering::Relaxed))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Global lock statistics.
|
|
||||||
static LOCK_STATS: std::sync::OnceLock<Arc<LockStats>> = std::sync::OnceLock::new();
|
|
||||||
|
|
||||||
/// Get global lock statistics.
|
|
||||||
pub fn get_lock_stats() -> Arc<LockStats> {
|
|
||||||
LOCK_STATS.get_or_init(|| Arc::new(LockStats::new())).clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An optimized lock guard that supports early release.
|
|
||||||
///
|
|
||||||
/// This wraps the actual lock guard and provides:
|
|
||||||
/// - Early release capability (before drop)
|
|
||||||
/// - Hold time tracking
|
|
||||||
/// - Metrics reporting
|
|
||||||
pub struct OptimizedLockGuard<G> {
|
|
||||||
/// The underlying lock guard.
|
|
||||||
guard: Option<G>,
|
|
||||||
/// When the lock was acquired.
|
|
||||||
acquire_time: Instant,
|
|
||||||
/// Whether the lock has been released.
|
|
||||||
released: bool,
|
|
||||||
/// Lock resource name (for logging).
|
|
||||||
resource: String,
|
|
||||||
/// Statistics reference.
|
|
||||||
stats: Arc<LockStats>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<G> OptimizedLockGuard<G> {
|
|
||||||
/// Create a new optimized lock guard.
|
|
||||||
pub fn new(guard: G, resource: impl Into<String>) -> Self {
|
|
||||||
let stats = get_lock_stats();
|
|
||||||
stats.record_acquire();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
guard: Some(guard),
|
|
||||||
acquire_time: Instant::now(),
|
|
||||||
released: false,
|
|
||||||
resource: resource.into(),
|
|
||||||
stats,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the lock hold time so far.
|
|
||||||
pub fn hold_time(&self) -> Duration {
|
|
||||||
self.acquire_time.elapsed()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if the lock has been released.
|
|
||||||
pub fn is_released(&self) -> bool {
|
|
||||||
self.released
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Release the lock early (before drop).
|
|
||||||
///
|
|
||||||
/// This is the key optimization: releasing the lock after
|
|
||||||
/// metadata read rather than waiting for the entire operation.
|
|
||||||
pub fn early_release(&mut self) {
|
|
||||||
if self.released {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let hold_time = self.hold_time();
|
|
||||||
self.guard.take();
|
|
||||||
self.released = true;
|
|
||||||
|
|
||||||
self.stats.record_early_release(hold_time);
|
|
||||||
|
|
||||||
histogram!("rustfs_lock_hold_duration_seconds").record(hold_time.as_secs_f64());
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
resource = %self.resource,
|
|
||||||
hold_time_ms = hold_time.as_millis(),
|
|
||||||
"Lock released early (optimization active)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a reference to the underlying guard.
|
|
||||||
pub fn as_ref(&self) -> Option<&G> {
|
|
||||||
if self.released { None } else { self.guard.as_ref() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<G> Drop for OptimizedLockGuard<G> {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if !self.released {
|
|
||||||
let hold_time = self.hold_time();
|
|
||||||
self.guard.take();
|
|
||||||
self.released = true;
|
|
||||||
|
|
||||||
self.stats.record_early_release(hold_time);
|
|
||||||
|
|
||||||
histogram!("rustfs_lock_hold_duration_seconds").record(hold_time.as_secs_f64());
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
resource = %self.resource,
|
|
||||||
hold_time_ms = hold_time.as_millis(),
|
|
||||||
"Lock released on drop (normal release)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A scope guard that releases a lock when it goes out of scope.
|
|
||||||
///
|
|
||||||
/// This is a simpler version of OptimizedLockGuard for cases
|
|
||||||
/// where we just need RAII semantics without tracking.
|
|
||||||
pub struct LockScopeGuard<G> {
|
|
||||||
guard: Option<G>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<G> LockScopeGuard<G> {
|
|
||||||
/// Create a new scope guard.
|
|
||||||
pub fn new(guard: G) -> Self {
|
|
||||||
Self { guard: Some(guard) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Release the lock early.
|
|
||||||
pub fn release(&mut self) {
|
|
||||||
self.guard.take();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<G> Drop for LockScopeGuard<G> {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
self.guard.take();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper for managing lock optimization in GetObject operations.
|
|
||||||
///
|
|
||||||
/// This provides a clean interface for the common pattern:
|
|
||||||
/// 1. Acquire lock
|
|
||||||
/// 2. Read metadata
|
|
||||||
/// 3. Release lock (if optimization enabled)
|
|
||||||
/// 4. Transfer data (without lock)
|
|
||||||
pub struct LockOptimizer {
|
|
||||||
/// Configuration.
|
|
||||||
config: LockOptimizeConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LockOptimizer {
|
|
||||||
/// Create a new lock optimizer with default configuration.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
config: LockOptimizeConfig::from_env(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new lock optimizer with custom configuration.
|
|
||||||
pub fn with_config(config: LockOptimizeConfig) -> Self {
|
|
||||||
Self { config }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if lock optimization is enabled.
|
|
||||||
pub fn is_enabled(&self) -> bool {
|
|
||||||
self.config.enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the lock acquisition timeout.
|
|
||||||
pub fn acquire_timeout(&self) -> Duration {
|
|
||||||
self.config.acquire_timeout
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wrap a lock guard for optimization.
|
|
||||||
pub fn wrap_guard<G>(&self, guard: G, resource: impl Into<String>) -> OptimizedLockGuard<G> {
|
|
||||||
OptimizedLockGuard::new(guard, resource)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Execute a metadata read operation with lock optimization.
|
|
||||||
///
|
|
||||||
/// This is the main entry point for optimized lock usage:
|
|
||||||
/// - If optimization is enabled: lock is released after metadata_fn completes
|
|
||||||
/// - If optimization is disabled: lock is held until the returned guard is dropped
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `guard` - The lock guard to optimize
|
|
||||||
/// * `resource` - Resource name for logging
|
|
||||||
/// * `metadata_fn` - Function to read metadata while holding lock
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// A tuple of (metadata result, optional guard to hold for later release)
|
|
||||||
pub async fn with_optimized_lock<G, F, Fut, T>(
|
|
||||||
&self,
|
|
||||||
guard: G,
|
|
||||||
resource: impl Into<String>,
|
|
||||||
metadata_fn: F,
|
|
||||||
) -> (T, Option<OptimizedLockGuard<G>>)
|
|
||||||
where
|
|
||||||
F: FnOnce() -> Fut,
|
|
||||||
Fut: std::future::Future<Output = T>,
|
|
||||||
{
|
|
||||||
let resource = resource.into();
|
|
||||||
let mut optimized = OptimizedLockGuard::new(guard, &resource);
|
|
||||||
|
|
||||||
// Execute metadata read while holding lock
|
|
||||||
let result = metadata_fn().await;
|
|
||||||
|
|
||||||
if self.config.enabled {
|
|
||||||
// Release lock early
|
|
||||||
optimized.early_release();
|
|
||||||
(result, None)
|
|
||||||
} else {
|
|
||||||
// Keep lock for caller to release
|
|
||||||
(result, Some(optimized))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for LockOptimizer {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if lock optimization is enabled globally.
|
|
||||||
pub fn is_lock_optimization_enabled() -> bool {
|
|
||||||
rustfs_utils::get_env_bool(
|
|
||||||
rustfs_config::ENV_OBJECT_LOCK_OPTIMIZATION_ENABLE,
|
|
||||||
rustfs_config::DEFAULT_OBJECT_LOCK_OPTIMIZATION_ENABLE,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[allow(unused_imports)]
|
|
||||||
mod tests {
|
|
||||||
use super::{LockOptimizeConfig, LockOptimizer, LockStats, OptimizedLockGuard};
|
|
||||||
use std::sync::Mutex;
|
|
||||||
use std::sync::atomic::Ordering;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_lock_optimize_config_default() {
|
|
||||||
let config = LockOptimizeConfig::default();
|
|
||||||
assert!(config.enabled);
|
|
||||||
assert_eq!(config.acquire_timeout, Duration::from_secs(5));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_lock_stats() {
|
|
||||||
let stats = LockStats::new();
|
|
||||||
|
|
||||||
stats.record_acquire();
|
|
||||||
stats.record_early_release(Duration::from_millis(100));
|
|
||||||
stats.record_early_release(Duration::from_millis(200));
|
|
||||||
|
|
||||||
assert_eq!(stats.locks_acquired.load(Ordering::Relaxed), 1);
|
|
||||||
assert_eq!(stats.locks_released_early.load(Ordering::Relaxed), 2);
|
|
||||||
assert_eq!(stats.max_hold_time(), Duration::from_millis(200));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_optimized_lock_guard() {
|
|
||||||
let guard = Mutex::new(42);
|
|
||||||
let locked = guard.lock().unwrap();
|
|
||||||
let mut optimized = OptimizedLockGuard::new(locked, "test-resource");
|
|
||||||
|
|
||||||
assert!(!optimized.is_released());
|
|
||||||
assert!(optimized.hold_time() < Duration::from_secs(1));
|
|
||||||
|
|
||||||
optimized.early_release();
|
|
||||||
assert!(optimized.is_released());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_lock_optimizer() {
|
|
||||||
let optimizer = LockOptimizer::new();
|
|
||||||
assert!(optimizer.is_enabled());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_with_optimized_lock_enabled() {
|
|
||||||
let optimizer = LockOptimizer::new();
|
|
||||||
let guard = Mutex::new(42);
|
|
||||||
let locked = guard.lock().unwrap();
|
|
||||||
|
|
||||||
let (result, returned_guard) = optimizer.with_optimized_lock(locked, "test-resource", || async { 100 }).await;
|
|
||||||
|
|
||||||
assert_eq!(result, 100);
|
|
||||||
// With optimization enabled, guard should be None (released early)
|
|
||||||
assert!(returned_guard.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_with_optimized_lock_disabled() {
|
|
||||||
let config = LockOptimizeConfig {
|
|
||||||
enabled: false,
|
|
||||||
acquire_timeout: Duration::from_secs(5),
|
|
||||||
};
|
|
||||||
let optimizer = LockOptimizer::with_config(config);
|
|
||||||
let guard = Mutex::new(42);
|
|
||||||
let locked = guard.lock().unwrap();
|
|
||||||
|
|
||||||
let (result, returned_guard) = optimizer.with_optimized_lock(locked, "test-resource", || async { 100 }).await;
|
|
||||||
|
|
||||||
assert_eq!(result, 100);
|
|
||||||
// With optimization disabled, guard should be Some (held for later)
|
|
||||||
assert!(returned_guard.is_some());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,12 +13,10 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
pub mod access;
|
pub mod access;
|
||||||
pub mod backpressure;
|
|
||||||
pub mod concurrency;
|
pub mod concurrency;
|
||||||
pub mod deadlock_detector;
|
pub mod deadlock_detector;
|
||||||
pub mod ecfs;
|
pub mod ecfs;
|
||||||
pub(crate) mod helper;
|
pub(crate) mod helper;
|
||||||
pub mod lock_optimizer;
|
|
||||||
pub mod options;
|
pub mod options;
|
||||||
pub mod request_context;
|
pub mod request_context;
|
||||||
pub mod rpc;
|
pub mod rpc;
|
||||||
|
|||||||
Reference in New Issue
Block a user