mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-01 09:48:20 +00:00
perf(s3): bound snowball member imports (#6945)
* fix(s3): harden Snowball extract error boundaries * fix(s3): close Snowball extract compatibility gaps * fix(s3): verify Snowball request body completion * test(s3): reject forged Snowball streaming signatures * build(deps): pin Snowball archive parser limits * fix(s3): preserve Snowball trailer and member errors * docs(architecture): register Snowball tar fork cleanup * refactor(s3): route Snowball errors through object boundary * ci(deps): allow pinned tokio-tar source * fix: align Snowball archive codec detection * fix(s3): harden Snowball codec compatibility * fix(s3): preserve Snowball codec compatibility * test(zip): align yield wake assertion with Tokio * fix(rio): preserve legacy large-block reads * fix(zip): accept blank tar numeric fields * fix(s3): align Snowball member import semantics * fix(s3): authorize PAX legal-hold conditions * refactor(s3): preserve Snowball error boundary * fix(iam): support legal-hold policy conditions * perf(s3): bound Snowball member imports * fix(s3): preserve bounded Snowball import contracts * fix(s3): preserve bounded import invariants * fix(s3): route Snowball limits through app boundary * refactor(s3): keep bounded import errors behind facade * fix(s3): satisfy Snowball feature lint gates * fix(s3): preserve Snowball safety guardrails
This commit is contained in:
+1422
-195
File diff suppressed because it is too large
Load Diff
@@ -308,14 +308,14 @@ pub(crate) fn guard_put_object_body_read_timeout(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PooledBufferReader {
|
pub(super) struct PooledBufferReader {
|
||||||
buffer: PooledBuffer,
|
buffer: PooledBuffer,
|
||||||
len: usize,
|
len: usize,
|
||||||
pos: usize,
|
pos: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PooledBufferReader {
|
impl PooledBufferReader {
|
||||||
fn new(buffer: PooledBuffer, len: usize) -> Self {
|
pub(super) fn new(buffer: PooledBuffer, len: usize) -> Self {
|
||||||
Self { buffer, len, pos: 0 }
|
Self { buffer, len, pos: 0 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -631,7 +631,7 @@ fn select_put_path_with_concurrency(
|
|||||||
/// where the allocation cost is negligible (≤4KiB memcpy).
|
/// where the allocation cost is negligible (≤4KiB memcpy).
|
||||||
const POOL_BYPASS_MAX_SIZE: usize = 4 * 1024;
|
const POOL_BYPASS_MAX_SIZE: usize = 4 * 1024;
|
||||||
|
|
||||||
async fn read_small_put_body_into<R, B>(body: &mut R, buf: &mut B, size: usize) -> S3Result<()>
|
pub(super) async fn read_small_put_body_into<R, B>(body: &mut R, buf: &mut B, size: usize) -> S3Result<()>
|
||||||
where
|
where
|
||||||
R: AsyncRead + Unpin,
|
R: AsyncRead + Unpin,
|
||||||
B: bytes::BufMut,
|
B: bytes::BufMut,
|
||||||
|
|||||||
@@ -978,9 +978,12 @@ pub(crate) mod bucket {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) mod concurrency {
|
pub(crate) mod concurrency {
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) use crate::storage::storage_api::concurrency_consumer::SNOWBALL_MEMBER_COMMIT_LIMIT;
|
||||||
pub(crate) use crate::storage::storage_api::concurrency_consumer::{
|
pub(crate) use crate::storage::storage_api::concurrency_consumer::{
|
||||||
ConcurrencyManager, DiskReadAdmission, ForegroundWriteAdmission, GetObjectGuard, IoQueueStatus, IoStrategy,
|
ConcurrencyManager, DiskReadAdmission, ForegroundWriteAdmission, GetObjectGuard, IoQueueStatus, IoStrategy,
|
||||||
PutObjectGuard, get_concurrency_aware_buffer_size, get_concurrency_manager, get_put_concurrency_aware_buffer_size,
|
PutObjectGuard, SNOWBALL_STAGING_BYTES_LIMIT, get_concurrency_aware_buffer_size, get_concurrency_manager,
|
||||||
|
get_put_concurrency_aware_buffer_size,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,10 +31,12 @@ use rustfs_io_metrics::bandwidth::{BandwidthMonitor, BandwidthSnapshot};
|
|||||||
use rustfs_io_metrics::{MetricsCollector, PerformanceMetrics};
|
use rustfs_io_metrics::{MetricsCollector, PerformanceMetrics};
|
||||||
use std::sync::{Arc, LazyLock, Mutex};
|
use std::sync::{Arc, LazyLock, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::sync::Semaphore;
|
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
const DERIVED_LARGE_PUT_ADMISSION_LIMIT_MAX: usize = 32;
|
const DERIVED_LARGE_PUT_ADMISSION_LIMIT_MAX: usize = 32;
|
||||||
|
pub(crate) const SNOWBALL_MEMBER_COMMIT_LIMIT: usize = 32;
|
||||||
|
pub(crate) const SNOWBALL_STAGING_BYTES_LIMIT: usize = 4 * MI_B;
|
||||||
|
|
||||||
/// Global concurrency manager instance
|
/// Global concurrency manager instance
|
||||||
pub(crate) static CONCURRENCY_MANAGER: LazyLock<ConcurrencyManager> = LazyLock::new(ConcurrencyManager::new);
|
pub(crate) static CONCURRENCY_MANAGER: LazyLock<ConcurrencyManager> = LazyLock::new(ConcurrencyManager::new);
|
||||||
@@ -69,6 +71,12 @@ pub struct ConcurrencyManager {
|
|||||||
metrics_collector: Arc<MetricsCollector>,
|
metrics_collector: Arc<MetricsCollector>,
|
||||||
/// Foreground write admission policy, resolved once at startup.
|
/// Foreground write admission policy, resolved once at startup.
|
||||||
foreground_write_admission_policy: ForegroundWriteAdmissionPolicy,
|
foreground_write_admission_policy: ForegroundWriteAdmissionPolicy,
|
||||||
|
/// Snowball members are internal PUTs, so they use a separate global gate
|
||||||
|
/// from preparation through the independently owned post-commit tail.
|
||||||
|
snowball_member_commit_semaphore: Arc<Semaphore>,
|
||||||
|
/// Bounds the owned member bodies and metadata retained between TAR parsing
|
||||||
|
/// and storage commit across all extract requests.
|
||||||
|
snowball_staging_bytes_semaphore: Arc<Semaphore>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for ConcurrencyManager {
|
impl std::fmt::Debug for ConcurrencyManager {
|
||||||
@@ -417,6 +425,8 @@ impl ConcurrencyManager {
|
|||||||
bandwidth_monitor,
|
bandwidth_monitor,
|
||||||
metrics_collector,
|
metrics_collector,
|
||||||
foreground_write_admission_policy,
|
foreground_write_admission_policy,
|
||||||
|
snowball_member_commit_semaphore: Arc::new(Semaphore::new(SNOWBALL_MEMBER_COMMIT_LIMIT)),
|
||||||
|
snowball_staging_bytes_semaphore: Arc::new(Semaphore::new(SNOWBALL_STAGING_BYTES_LIMIT)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -556,6 +566,40 @@ impl ConcurrencyManager {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Admit a Snowball member through the foreground PUT policy using the
|
||||||
|
/// member's logical size. The outer archive has a separate preflight, while
|
||||||
|
/// every member shares the ordinary PUT gate and its wait/rejection policy.
|
||||||
|
pub(crate) async fn admit_snowball_foreground_write(
|
||||||
|
&self,
|
||||||
|
member_size: i64,
|
||||||
|
) -> Result<ForegroundWriteAdmission, tokio::sync::AcquireError> {
|
||||||
|
self.foreground_write_admission_policy
|
||||||
|
.admit(ForegroundWriteAdmissionKind::PutObject, member_size)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Acquire one global Snowball member lifecycle slot.
|
||||||
|
pub(crate) async fn acquire_snowball_member_commit(&self) -> Result<OwnedSemaphorePermit, tokio::sync::AcquireError> {
|
||||||
|
self.snowball_member_commit_semaphore.clone().acquire_owned().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to acquire one global Snowball member lifecycle slot.
|
||||||
|
pub(crate) fn try_acquire_snowball_member_commit(&self) -> Option<OwnedSemaphorePermit> {
|
||||||
|
self.snowball_member_commit_semaphore.clone().try_acquire_owned().ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to reserve prepared-member bytes without waiting.
|
||||||
|
///
|
||||||
|
/// A producer holding a non-empty micro-batch must use this method and
|
||||||
|
/// flush before waiting, otherwise several archives can each retain part of
|
||||||
|
/// the global budget while waiting forever for the remainder.
|
||||||
|
pub(crate) fn try_acquire_snowball_staging_bytes(&self, bytes: u32) -> Option<OwnedSemaphorePermit> {
|
||||||
|
self.snowball_staging_bytes_semaphore
|
||||||
|
.clone()
|
||||||
|
.try_acquire_many_owned(bytes)
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
|
||||||
/// Admit a multipart UploadPart request under the configured write gate.
|
/// Admit a multipart UploadPart request under the configured write gate.
|
||||||
///
|
///
|
||||||
/// Multipart workloads can saturate memory and internode write streams with
|
/// Multipart workloads can saturate memory and internode write streams with
|
||||||
@@ -1050,13 +1094,138 @@ impl Default for ConcurrencyManager {
|
|||||||
mod integration_tests {
|
mod integration_tests {
|
||||||
use super::super::io_schedule::{IoLoadLevel, IoPriority};
|
use super::super::io_schedule::{IoLoadLevel, IoPriority};
|
||||||
use super::super::request_guard::GetObjectGuard;
|
use super::super::request_guard::GetObjectGuard;
|
||||||
use super::{ConcurrencyManager, ForegroundWriteAdmission, derive_large_put_admission_limit};
|
use super::{
|
||||||
|
ConcurrencyManager, ForegroundWriteAdmission, SNOWBALL_MEMBER_COMMIT_LIMIT, SNOWBALL_STAGING_BYTES_LIMIT,
|
||||||
|
derive_large_put_admission_limit,
|
||||||
|
};
|
||||||
use crate::storage::storage_api::concurrency_consumer::PutObjectGuard;
|
use crate::storage::storage_api::concurrency_consumer::PutObjectGuard;
|
||||||
use rustfs_concurrency::{AdmissionState, WorkloadAdmissionSnapshotProvider, WorkloadClass};
|
use rustfs_concurrency::{AdmissionState, WorkloadAdmissionSnapshotProvider, WorkloadClass};
|
||||||
use rustfs_io_core::io_profile::{AccessPattern, StorageMedia};
|
use rustfs_io_core::io_profile::{AccessPattern, StorageMedia};
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_snowball_gates_are_global_bounded_and_reusable() {
|
||||||
|
let manager = ConcurrencyManager::new();
|
||||||
|
let clone = manager.clone();
|
||||||
|
|
||||||
|
let commit_permits = manager
|
||||||
|
.snowball_member_commit_semaphore
|
||||||
|
.clone()
|
||||||
|
.try_acquire_many_owned(u32::try_from(SNOWBALL_MEMBER_COMMIT_LIMIT).expect("Snowball commit limit must fit into u32"))
|
||||||
|
.expect("the exact Snowball commit limit must be available");
|
||||||
|
assert!(
|
||||||
|
clone.snowball_member_commit_semaphore.clone().try_acquire_owned().is_err(),
|
||||||
|
"a cloned manager must share the global commit gate"
|
||||||
|
);
|
||||||
|
drop(commit_permits);
|
||||||
|
assert!(clone.snowball_member_commit_semaphore.clone().try_acquire_owned().is_ok());
|
||||||
|
|
||||||
|
let staging_bytes = u32::try_from(SNOWBALL_STAGING_BYTES_LIMIT).expect("Snowball staging limit must fit into u32");
|
||||||
|
let staging_permit = manager
|
||||||
|
.try_acquire_snowball_staging_bytes(staging_bytes)
|
||||||
|
.expect("the exact Snowball staging budget must be available");
|
||||||
|
assert!(
|
||||||
|
clone.try_acquire_snowball_staging_bytes(1).is_none(),
|
||||||
|
"a cloned manager must share the global staging budget"
|
||||||
|
);
|
||||||
|
drop(staging_permit);
|
||||||
|
assert!(clone.try_acquire_snowball_staging_bytes(1).is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_snowball_members_share_the_strict_foreground_put_gate() {
|
||||||
|
let manager = ConcurrencyManager::with_put_admission_for_test(true, 2, Duration::ZERO);
|
||||||
|
let outer = match manager
|
||||||
|
.admit_put_object(1)
|
||||||
|
.await
|
||||||
|
.expect("strict outer admission must remain open")
|
||||||
|
{
|
||||||
|
ForegroundWriteAdmission::Admitted(permit) => permit,
|
||||||
|
outcome => panic!("strict outer admission must return a permit: {outcome:?}"),
|
||||||
|
};
|
||||||
|
let member = match manager
|
||||||
|
.admit_snowball_foreground_write(1)
|
||||||
|
.await
|
||||||
|
.expect("strict member admission must remain open")
|
||||||
|
{
|
||||||
|
ForegroundWriteAdmission::Admitted(permit) => permit,
|
||||||
|
outcome => panic!("strict member admission must return a permit: {outcome:?}"),
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
matches!(
|
||||||
|
manager
|
||||||
|
.admit_snowball_foreground_write(1)
|
||||||
|
.await
|
||||||
|
.expect("strict member admission must remain open"),
|
||||||
|
ForegroundWriteAdmission::Rejected
|
||||||
|
),
|
||||||
|
"a saturated Snowball member admission must preserve the zero-wait rejection policy"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(
|
||||||
|
manager.admit_put_object(1).await.expect("strict gate must remain usable"),
|
||||||
|
ForegroundWriteAdmission::Rejected
|
||||||
|
),
|
||||||
|
"outer PUTs and Snowball members must exhaust the same strict gate"
|
||||||
|
);
|
||||||
|
|
||||||
|
drop(outer);
|
||||||
|
let replacement = match manager
|
||||||
|
.admit_snowball_foreground_write(1)
|
||||||
|
.await
|
||||||
|
.expect("released strict capacity must be reusable")
|
||||||
|
{
|
||||||
|
ForegroundWriteAdmission::Admitted(permit) => permit,
|
||||||
|
outcome => panic!("strict replacement admission must return a permit: {outcome:?}"),
|
||||||
|
};
|
||||||
|
drop((member, replacement));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_snowball_members_use_their_size_for_the_large_foreground_put_gate() {
|
||||||
|
let min_size = 16 * 1024 * 1024;
|
||||||
|
let manager = ConcurrencyManager::with_large_put_admission_for_test(true, 1, min_size, Duration::ZERO);
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
manager
|
||||||
|
.admit_put_object((min_size - 1) as i64)
|
||||||
|
.await
|
||||||
|
.expect("small outer archive admission must remain open"),
|
||||||
|
ForegroundWriteAdmission::Disabled
|
||||||
|
));
|
||||||
|
let large_member = match manager
|
||||||
|
.admit_snowball_foreground_write(min_size as i64)
|
||||||
|
.await
|
||||||
|
.expect("large Snowball member admission must remain open")
|
||||||
|
{
|
||||||
|
ForegroundWriteAdmission::Admitted(permit) => permit,
|
||||||
|
outcome => panic!("large Snowball member must consume the large PUT gate: {outcome:?}"),
|
||||||
|
};
|
||||||
|
assert!(matches!(
|
||||||
|
manager
|
||||||
|
.admit_snowball_foreground_write(min_size as i64)
|
||||||
|
.await
|
||||||
|
.expect("saturated Snowball member admission must remain open"),
|
||||||
|
ForegroundWriteAdmission::Rejected
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
manager
|
||||||
|
.admit_put_object(min_size as i64)
|
||||||
|
.await
|
||||||
|
.expect("ordinary large PUT admission must remain open"),
|
||||||
|
ForegroundWriteAdmission::Rejected
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
manager
|
||||||
|
.admit_snowball_foreground_write((min_size - 1) as i64)
|
||||||
|
.await
|
||||||
|
.expect("small Snowball member admission must remain open"),
|
||||||
|
ForegroundWriteAdmission::Disabled
|
||||||
|
));
|
||||||
|
drop(large_member);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[serial]
|
#[serial]
|
||||||
async fn test_concurrency_manager_priority_queue_integration() {
|
async fn test_concurrency_manager_priority_queue_integration() {
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ pub use io_schedule::{
|
|||||||
pub use request_guard::{GetObjectGuard, PutObjectGuard};
|
pub use request_guard::{GetObjectGuard, PutObjectGuard};
|
||||||
|
|
||||||
// Concurrency manager
|
// Concurrency manager
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) use manager::SNOWBALL_MEMBER_COMMIT_LIMIT;
|
||||||
|
pub(crate) use manager::SNOWBALL_STAGING_BYTES_LIMIT;
|
||||||
pub use manager::{ConcurrencyManager, DiskReadAdmission, ForegroundWriteAdmission};
|
pub use manager::{ConcurrencyManager, DiskReadAdmission, ForegroundWriteAdmission};
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -125,9 +125,12 @@ pub(crate) mod access_consumer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) mod concurrency_consumer {
|
pub(crate) mod concurrency_consumer {
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) use super::super::concurrency::SNOWBALL_MEMBER_COMMIT_LIMIT;
|
||||||
pub(crate) use super::super::concurrency::{
|
pub(crate) use super::super::concurrency::{
|
||||||
ConcurrencyManager, DiskReadAdmission, ForegroundWriteAdmission, GetObjectGuard, IoQueueStatus, IoStrategy,
|
ConcurrencyManager, DiskReadAdmission, ForegroundWriteAdmission, GetObjectGuard, IoQueueStatus, IoStrategy,
|
||||||
PutObjectGuard, get_concurrency_aware_buffer_size, get_concurrency_manager, get_put_concurrency_aware_buffer_size,
|
PutObjectGuard, SNOWBALL_STAGING_BYTES_LIMIT, get_concurrency_aware_buffer_size, get_concurrency_manager,
|
||||||
|
get_put_concurrency_aware_buffer_size,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16445,7 +16445,7 @@ fn object_mutation_entrypoints_call_reserved_prefix_guard() {
|
|||||||
"if let Err(err) = validate_table_catalog_object_mutation(&bucket, &obj_id.key).await",
|
"if let Err(err) = validate_table_catalog_object_mutation(&bucket, &obj_id.key).await",
|
||||||
"validate_table_catalog_object_mutation(&bucket, &object).await?;",
|
"validate_table_catalog_object_mutation(&bucket, &object).await?;",
|
||||||
"validate_object_key(&key, \"PUT\")?;\n validate_table_catalog_object_mutation(&bucket, &key).await?;",
|
"validate_object_key(&key, \"PUT\")?;\n validate_table_catalog_object_mutation(&bucket, &key).await?;",
|
||||||
"validate_table_catalog_object_mutation(&bucket, &fpath).await?;",
|
"extract_try!(validate_table_catalog_object_mutation(&bucket, &fpath).await);",
|
||||||
] {
|
] {
|
||||||
assert!(source.contains(expected), "missing object mutation guard: {expected}");
|
assert!(source.contains(expected), "missing object mutation guard: {expected}");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user