mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-29 08:27:06 +00:00
perf(get,heal): fix GET hot-path overhead and heal checkpoint scaling (#4237)
perf(get,heal): land verified fixes for backlog #800-#804 Five fixes from the GET-path performance audit and scanner/heal completeness audit (rustfs/backlog#800..#804), each verified locally: - backlog#800 (heal checkpoint O(N^2)): ResumeCheckpoint object sets are now HashSet (Vec::contains was O(n) per healed object; 1.5ms at n=1M vs 11ns measured), and per-object checkpoint/resume-state persistence is batched (1000 mutations / 5s) instead of rewriting the whole file per object. complete_page() prunes the sets at page boundaries so memory stays bounded; positions still persist unconditionally and legacy Vec-format checkpoints still deserialize. - backlog#801 (DiskInfo.healing never set): erasure-set heal now writes a healing marker (.rustfs.sys/healing.bin) on the disks it rebuilds (endpoints plumbed via HealRequest/HealTask.heal_endpoints from the auto disk scanner) and clears it on success. LocalDisk::disk_info surfaces the marker, so scanner heal coordination, lock selection and admin/metrics healing counts see the rebuild. - backlog#802 (cache probe after data read): new GetObjectBodyCacheHook in ecstore lets the app-layer object data cache serve the body inside get_object_reader, after metadata quorum resolution (etag known) but before the erasure shard read/decode. Previously a hit still paid the full disk read. Hook is None/no-op when the cache is disabled. - backlog#803 (GET hot-path redundant work): ObjectInfo is cloned for event notification only when an event will actually be built (GET and HEAD paths; events are currently suppressed so the clone was pure waste); get_opts/put_opts/del_opts resolve bucket versioning with one metadata-sys lookup instead of two; skip_verify_bitrot and get_lock_acquire_timeout env reads are cached via OnceLock; the io-priority metric is no longer double-counted; GetObject input fields are cloned selectively instead of cloning the whole input. - backlog#804 (disk permit starvation): the disk-read permit wait is now bounded (RUSTFS_OBJECT_DISK_PERMIT_WAIT_TIMEOUT, default 5s, 0 = previous unbounded behavior); on timeout the GET proceeds without a permit and the bypass is counted. DiskReadPermitReader also releases the permit at body EOF instead of holding it until the client drops the stream. Verification: make pre-commit; cargo clippy -D warnings on the four changed crates; full rustfs lib suite (2096 tests) green; rustfs-heal lib suite green with new unit tests for checkpoint pruning/legacy format/throttle, permit EOF release, and the cache hook (hit + SSE skip). The heal_integration_test and one set_disk listing test fail identically on unmodified main (pre-existing global-state ordering flakes, verified via git stash A/B). Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
@@ -79,6 +79,9 @@ pub struct AppContext {
|
||||
impl AppContext {
|
||||
pub fn new(object_store: Arc<ECStore>, iam: Arc<dyn IamInterface>, kms: Arc<dyn KmsInterface>) -> Self {
|
||||
let object_data_cache = ObjectDataCacheAdapter::from_env_or_disabled();
|
||||
// Let ecstore probe this cache inside get_object_reader, after
|
||||
// metadata resolution but before the erasure data read (backlog#802).
|
||||
crate::app::object_data_cache::register_object_data_cache_body_hook(Arc::clone(&object_data_cache));
|
||||
|
||||
Self {
|
||||
object_store,
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
// 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.
|
||||
|
||||
//! ecstore-facing GET body cache hook.
|
||||
//!
|
||||
//! Registered into ecstore so the cache probe runs inside `get_object_reader`
|
||||
//! right after metadata resolution: probing earlier needs a second metadata
|
||||
//! fan-out, probing later (after the reader is built) means a hit no longer
|
||||
//! saves the erasure read/decode.
|
||||
|
||||
use crate::app::object_data_cache::{
|
||||
GetObjectBodyCacheLookup, GetObjectBodyCacheRequest, ObjectDataCacheAdapter, build_get_object_body_cache_plan,
|
||||
lookup_get_object_body_cache_hit,
|
||||
};
|
||||
use crate::app::storage_api::object_usecase::StorageObjectInfo;
|
||||
use crate::storage::sse::contains_managed_encryption_metadata;
|
||||
use crate::storage::storage_api::ecstore_bucket::lifecycle::bucket_lifecycle_ops::LifecycleOps as _;
|
||||
use crate::storage::storage_api::ecstore_object::{GetObjectBodyCacheHook, register_get_object_body_cache_hook};
|
||||
use bytes::Bytes;
|
||||
use rustfs_utils::http::headers::SSEC_ALGORITHM_HEADER;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Adapter-backed implementation of ecstore's GET body cache hook.
|
||||
pub(crate) struct ObjectDataCacheBodyHook {
|
||||
adapter: Arc<ObjectDataCacheAdapter>,
|
||||
}
|
||||
|
||||
/// Registers the body-cache hook into ecstore. No-op for a disabled cache so
|
||||
/// the hot path keeps a single `None` branch when the feature is off.
|
||||
pub(crate) fn register_object_data_cache_body_hook(adapter: Arc<ObjectDataCacheAdapter>) {
|
||||
if adapter.is_disabled() {
|
||||
return;
|
||||
}
|
||||
register_get_object_body_cache_hook(Arc::new(ObjectDataCacheBodyHook { adapter }));
|
||||
}
|
||||
|
||||
fn object_metadata_indicates_encryption(metadata: &std::collections::HashMap<String, String>) -> bool {
|
||||
// SSE-C or managed SSE bodies are decrypted on the normal read path, so
|
||||
// the cached plaintext identity used by the planner would not match what
|
||||
// ecstore returns here (pre-decryption). Skip them entirely.
|
||||
metadata.contains_key(SSEC_ALGORITHM_HEADER) || contains_managed_encryption_metadata(metadata)
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl GetObjectBodyCacheHook for ObjectDataCacheBodyHook {
|
||||
async fn lookup(&self, bucket: &str, object: &str, info: &StorageObjectInfo) -> Option<Bytes> {
|
||||
if info.is_remote() || object_metadata_indicates_encryption(&info.user_defined) {
|
||||
return None;
|
||||
}
|
||||
let response_content_length = info.get_actual_size().ok()?;
|
||||
let request = GetObjectBodyCacheRequest {
|
||||
bucket,
|
||||
key: object,
|
||||
info,
|
||||
response_content_length,
|
||||
has_range: false,
|
||||
part_number: None,
|
||||
encryption_applied: false,
|
||||
};
|
||||
let plan = build_get_object_body_cache_plan(&self.adapter, request);
|
||||
match lookup_get_object_body_cache_hit(&self.adapter, &plan).await {
|
||||
GetObjectBodyCacheLookup::Hit(bytes) => Some(bytes),
|
||||
GetObjectBodyCacheLookup::Disabled | GetObjectBodyCacheLookup::Skip | GetObjectBodyCacheLookup::Miss => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rustfs_object_data_cache::{ObjectDataCacheConfig, ObjectDataCacheMode};
|
||||
|
||||
fn hit_only_adapter() -> Arc<ObjectDataCacheAdapter> {
|
||||
Arc::new(
|
||||
ObjectDataCacheAdapter::new(ObjectDataCacheConfig {
|
||||
mode: ObjectDataCacheMode::FillBufferedOnly,
|
||||
max_bytes: 4 * 1024 * 1024,
|
||||
..ObjectDataCacheConfig::default()
|
||||
})
|
||||
.expect("adapter"),
|
||||
)
|
||||
}
|
||||
|
||||
fn plain_info(size: i64) -> StorageObjectInfo {
|
||||
StorageObjectInfo {
|
||||
bucket: "b".to_string(),
|
||||
name: "k".to_string(),
|
||||
etag: Some("etag-1".to_string()),
|
||||
size,
|
||||
actual_size: size,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hook_lookup_returns_cached_body_after_fill() {
|
||||
let adapter = hit_only_adapter();
|
||||
let info = plain_info(5);
|
||||
let request = GetObjectBodyCacheRequest {
|
||||
bucket: "b",
|
||||
key: "k",
|
||||
info: &info,
|
||||
response_content_length: 5,
|
||||
has_range: false,
|
||||
part_number: None,
|
||||
encryption_applied: false,
|
||||
};
|
||||
let plan = build_get_object_body_cache_plan(&adapter, request);
|
||||
let body = Bytes::from_static(b"hello");
|
||||
let _ = crate::app::object_data_cache::fill_get_object_body_cache_from_buffered_body(&adapter, &plan, &body).await;
|
||||
|
||||
let hook = ObjectDataCacheBodyHook {
|
||||
adapter: Arc::clone(&adapter),
|
||||
};
|
||||
let hit = hook.lookup("b", "k", &info).await.expect("cache hit");
|
||||
assert_eq!(hit, body);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hook_lookup_skips_encrypted_objects() {
|
||||
let adapter = hit_only_adapter();
|
||||
let mut info = plain_info(5);
|
||||
info.user_defined = std::sync::Arc::new(
|
||||
[(SSEC_ALGORITHM_HEADER.to_string(), "AES256".to_string())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
);
|
||||
|
||||
let hook = ObjectDataCacheBodyHook { adapter };
|
||||
assert!(hook.lookup("b", "k", &info).await.is_none());
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
mod adapter;
|
||||
mod body;
|
||||
mod hook;
|
||||
mod invalidation;
|
||||
mod planner;
|
||||
|
||||
@@ -24,6 +25,7 @@ pub(crate) use body::{
|
||||
GetObjectBodyCacheLookup, fill_get_object_body_cache_from_buffered_body, fill_get_object_body_cache_from_materialized_body,
|
||||
lookup_get_object_body_cache_hit,
|
||||
};
|
||||
pub(crate) use hook::register_object_data_cache_body_hook;
|
||||
pub(crate) use invalidation::{
|
||||
invalidate_object_data_cache_after_complete_multipart_success, invalidate_object_data_cache_after_copy_success,
|
||||
invalidate_object_data_cache_after_delete_success, invalidate_object_data_cache_after_put_success,
|
||||
|
||||
@@ -330,7 +330,6 @@ struct GetObjectRequestContext {
|
||||
|
||||
struct GetObjectReadSetup {
|
||||
info: ObjectInfo,
|
||||
event_info: ObjectInfo,
|
||||
final_stream: DynReader,
|
||||
buffered_body: Option<Bytes>,
|
||||
rs: Option<HTTPRangeSpec>,
|
||||
@@ -361,7 +360,7 @@ struct GetObjectStrategyContext {
|
||||
|
||||
struct GetObjectOutputContext {
|
||||
output: GetObjectOutput,
|
||||
event_info: ObjectInfo,
|
||||
event_info: Option<ObjectInfo>,
|
||||
response_content_length: i64,
|
||||
optimal_buffer_size: usize,
|
||||
}
|
||||
@@ -522,7 +521,7 @@ pin_project! {
|
||||
struct DiskReadPermitReader<R> {
|
||||
#[pin]
|
||||
inner: R,
|
||||
_disk_permit: OwnedSemaphorePermit,
|
||||
disk_permit: Option<OwnedSemaphorePermit>,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,7 +529,7 @@ impl<R> DiskReadPermitReader<R> {
|
||||
fn new(inner: R, disk_permit: OwnedSemaphorePermit) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
_disk_permit: disk_permit,
|
||||
disk_permit: Some(disk_permit),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -540,7 +539,16 @@ where
|
||||
R: AsyncRead,
|
||||
{
|
||||
fn poll_read(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<std::io::Result<()>> {
|
||||
self.project().inner.poll_read(cx, buf)
|
||||
let this = self.project();
|
||||
let had_capacity = buf.remaining() > 0;
|
||||
let filled_before = buf.filled().len();
|
||||
let poll = this.inner.poll_read(cx, buf);
|
||||
// EOF: no more disk reads can happen through this stream, so release
|
||||
// the permit instead of holding it until the client drops the body.
|
||||
if had_capacity && matches!(poll, Poll::Ready(Ok(()))) && buf.filled().len() == filled_before {
|
||||
this.disk_permit.take();
|
||||
}
|
||||
poll
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2208,6 +2216,18 @@ impl DefaultObjectUsecase {
|
||||
})
|
||||
}
|
||||
|
||||
/// How long a GET waits for a disk read permit before degrading to a
|
||||
/// permit-less read. Cached: consulted per GET. Zero disables the bound.
|
||||
fn disk_permit_wait_timeout() -> Duration {
|
||||
static CACHED: std::sync::OnceLock<Duration> = std::sync::OnceLock::new();
|
||||
*CACHED.get_or_init(|| {
|
||||
Duration::from_secs(rustfs_utils::get_env_u64(
|
||||
rustfs_config::ENV_OBJECT_DISK_PERMIT_WAIT_TIMEOUT,
|
||||
rustfs_config::DEFAULT_OBJECT_DISK_PERMIT_WAIT_TIMEOUT,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
async fn acquire_get_object_io_planning(
|
||||
manager: &ConcurrencyManager,
|
||||
wrapper: &RequestTimeoutWrapper,
|
||||
@@ -2216,10 +2236,32 @@ impl DefaultObjectUsecase {
|
||||
key: &str,
|
||||
) -> S3Result<GetObjectIoPlanning> {
|
||||
let permit_wait_start = std::time::Instant::now();
|
||||
let disk_permit = manager
|
||||
.acquire_owned_disk_read_permit()
|
||||
.await
|
||||
.map_err(|_| s3_error!(InternalError, "disk read semaphore closed"))?;
|
||||
let permit_wait_timeout = Self::disk_permit_wait_timeout();
|
||||
// Permits are held for the whole body transfer, so slow clients can
|
||||
// pin all of them while disks are idle. Bound the wait and degrade to
|
||||
// a permit-less read instead of stalling into the request timeout.
|
||||
let disk_permit = if permit_wait_timeout.is_zero() {
|
||||
Some(
|
||||
manager
|
||||
.acquire_owned_disk_read_permit()
|
||||
.await
|
||||
.map_err(|_| s3_error!(InternalError, "disk read semaphore closed"))?,
|
||||
)
|
||||
} else {
|
||||
match tokio::time::timeout(permit_wait_timeout, manager.acquire_owned_disk_read_permit()).await {
|
||||
Ok(permit) => Some(permit.map_err(|_| s3_error!(InternalError, "disk read semaphore closed"))?),
|
||||
Err(_) => {
|
||||
metrics::counter!("rustfs.get_object.disk_permit.bypass.total").increment(1);
|
||||
warn!(
|
||||
bucket = %bucket,
|
||||
key = %key,
|
||||
wait_ms = permit_wait_start.elapsed().as_millis() as u64,
|
||||
"GetObject proceeding without disk read permit after bounded wait"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
let permit_wait_duration = permit_wait_start.elapsed();
|
||||
|
||||
Self::ensure_get_object_not_timed_out(
|
||||
@@ -2253,7 +2295,7 @@ impl DefaultObjectUsecase {
|
||||
Self::ensure_get_object_not_timed_out(wrapper, timeout_config, bucket, key, GetObjectTimeoutStage::BeforeRead)?;
|
||||
|
||||
Ok(GetObjectIoPlanning {
|
||||
disk_permit: Some(disk_permit),
|
||||
disk_permit,
|
||||
permit_wait_duration,
|
||||
queue_status,
|
||||
queue_utilization,
|
||||
@@ -2261,14 +2303,12 @@ impl DefaultObjectUsecase {
|
||||
}
|
||||
|
||||
async fn prepare_get_object_request_context(req: &S3Request<GetObjectInput>) -> S3Result<GetObjectRequestContext> {
|
||||
let GetObjectInput {
|
||||
bucket,
|
||||
key,
|
||||
version_id,
|
||||
part_number,
|
||||
range,
|
||||
..
|
||||
} = req.input.clone();
|
||||
// Clone only the fields this path needs instead of the whole input.
|
||||
let bucket = req.input.bucket.clone();
|
||||
let key = req.input.key.clone();
|
||||
let version_id = req.input.version_id.clone();
|
||||
let part_number = req.input.part_number;
|
||||
let range = req.input.range;
|
||||
|
||||
validate_object_key(&key, "GET")?;
|
||||
|
||||
@@ -2420,7 +2460,6 @@ impl DefaultObjectUsecase {
|
||||
);
|
||||
}
|
||||
|
||||
let event_info = info.clone();
|
||||
let content_type = if let Some(content_type) = &info.content_type {
|
||||
match ContentType::from_str(content_type) {
|
||||
Ok(res) => Some(res),
|
||||
@@ -2510,7 +2549,6 @@ impl DefaultObjectUsecase {
|
||||
|
||||
Ok(GetObjectReadSetup {
|
||||
info,
|
||||
event_info,
|
||||
final_stream,
|
||||
buffered_body,
|
||||
rs,
|
||||
@@ -2595,8 +2633,6 @@ impl DefaultObjectUsecase {
|
||||
request_size = response_content_length,
|
||||
"I/O priority assigned (based on actual request size)"
|
||||
);
|
||||
|
||||
rustfs_io_metrics::record_io_priority_assignment(io_priority.as_str());
|
||||
}
|
||||
|
||||
rustfs_io_metrics::record_get_object_io_state(
|
||||
@@ -3609,11 +3645,15 @@ impl DefaultObjectUsecase {
|
||||
bucket: &str,
|
||||
method: &hyper::Method,
|
||||
headers: &HeaderMap,
|
||||
event_info: ObjectInfo,
|
||||
event_info: Option<ObjectInfo>,
|
||||
version_id_for_event: String,
|
||||
output: GetObjectOutput,
|
||||
) -> S3Result<S3Response<GetObjectOutput>> {
|
||||
let helper = helper.object(event_info).version_id(version_id_for_event);
|
||||
let helper = match event_info {
|
||||
Some(event_info) => helper.object(event_info),
|
||||
None => helper,
|
||||
};
|
||||
let helper = helper.version_id(version_id_for_event);
|
||||
let response = wrap_response_with_cors(bucket, method, headers, output).await;
|
||||
let result = Ok(response);
|
||||
let _ = helper.complete(&result);
|
||||
@@ -3627,7 +3667,7 @@ impl DefaultObjectUsecase {
|
||||
bucket: &str,
|
||||
key: &str,
|
||||
info: ObjectInfo,
|
||||
event_info: ObjectInfo,
|
||||
event_info: Option<ObjectInfo>,
|
||||
final_stream: DynReader,
|
||||
buffered_body: Option<Bytes>,
|
||||
rs: Option<HTTPRangeSpec>,
|
||||
@@ -3831,7 +3871,6 @@ impl DefaultObjectUsecase {
|
||||
|
||||
let GetObjectReadSetup {
|
||||
info,
|
||||
event_info,
|
||||
final_stream,
|
||||
buffered_body,
|
||||
rs,
|
||||
@@ -3852,6 +3891,10 @@ impl DefaultObjectUsecase {
|
||||
final_stream
|
||||
};
|
||||
|
||||
// Clone ObjectInfo for event notification only when an event will
|
||||
// actually be built — the clone is expensive for multipart objects.
|
||||
let event_info = helper.wants_object_info().then(|| info.clone());
|
||||
|
||||
let output_build_start = rustfs_io_metrics::get_stage_metrics_enabled().then(std::time::Instant::now);
|
||||
let output_context = self
|
||||
.build_get_object_output_context(
|
||||
@@ -5225,7 +5268,9 @@ impl DefaultObjectUsecase {
|
||||
|
||||
// Compute x-amz-expiration header from lifecycle prediction (before info is partially moved)
|
||||
let expiration_header = resolve_put_object_expiration(&bucket, &info).await;
|
||||
let event_info = info.clone();
|
||||
// Clone ObjectInfo for event notification only when an event will
|
||||
// actually be built — the clone is expensive for multipart objects.
|
||||
let event_info = helper.wants_object_info().then(|| info.clone());
|
||||
let content_type = {
|
||||
if let Some(content_type) = &info.content_type {
|
||||
match ContentType::from_str(content_type) {
|
||||
@@ -5344,7 +5389,10 @@ impl DefaultObjectUsecase {
|
||||
};
|
||||
|
||||
let version_id = req.input.version_id.clone().unwrap_or_default();
|
||||
helper = helper.object(event_info).version_id(version_id);
|
||||
if let Some(event_info) = event_info {
|
||||
helper = helper.object(event_info);
|
||||
}
|
||||
helper = helper.version_id(version_id);
|
||||
|
||||
// NOTE ON CORS:
|
||||
// Bucket-level CORS headers are intentionally applied only for object retrieval
|
||||
@@ -7401,6 +7449,26 @@ mod tests {
|
||||
assert_eq!(body, b"hello");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn disk_read_permit_reader_releases_permit_at_eof() {
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
let semaphore = Arc::new(tokio::sync::Semaphore::new(1));
|
||||
let permit = semaphore.clone().acquire_owned().await.expect("acquire permit");
|
||||
assert_eq!(semaphore.available_permits(), 0);
|
||||
|
||||
let mut reader = DiskReadPermitReader::new(std::io::Cursor::new(b"hello".to_vec()), permit);
|
||||
let mut body = Vec::new();
|
||||
reader.read_to_end(&mut body).await.expect("read body");
|
||||
assert_eq!(body, b"hello");
|
||||
|
||||
// The reader is still alive (client hasn't dropped the body), but EOF
|
||||
// was observed, so the permit must already be back in the semaphore.
|
||||
assert_eq!(semaphore.available_permits(), 1);
|
||||
drop(reader);
|
||||
assert_eq!(semaphore.available_permits(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pooled_buffer_reader_keeps_buffer_alive_until_consumed() {
|
||||
use tokio::io::AsyncReadExt;
|
||||
@@ -7783,7 +7851,7 @@ mod tests {
|
||||
"test-bucket",
|
||||
"path/raw",
|
||||
info.clone(),
|
||||
info,
|
||||
Some(info),
|
||||
wrap_reader(tokio::io::empty()),
|
||||
None,
|
||||
None,
|
||||
|
||||
@@ -237,6 +237,13 @@ impl OperationHelper {
|
||||
}))
|
||||
}
|
||||
|
||||
/// True when a pending event notification still needs the final ObjectInfo.
|
||||
/// Callers can use this to avoid cloning ObjectInfo when the event chain is
|
||||
/// disabled or suppressed.
|
||||
pub fn wants_object_info(&self) -> bool {
|
||||
matches!(self, Self::Enabled(state) if state.event_builder.is_some())
|
||||
}
|
||||
|
||||
/// Sets the ObjectInfo for event notification.
|
||||
pub fn object(mut self, object_info: ObjectInfo) -> Self {
|
||||
if let Self::Enabled(state) = &mut self
|
||||
|
||||
@@ -45,7 +45,44 @@ use crate::auth::AuthType;
|
||||
use crate::auth::get_query_param;
|
||||
use crate::auth::get_request_auth_type_with_query;
|
||||
use crate::auth::is_request_presigned_signature_v4_with_query;
|
||||
use crate::storage::storage_api::ecstore_bucket::versioning::VersioningApi as _;
|
||||
use crate::storage::storage_api::options_consumer::StorageObjectOptions as ObjectOptions;
|
||||
use s3s::dto::VersioningConfiguration;
|
||||
|
||||
/// Fetch the bucket's versioning configuration once so callers can derive
|
||||
/// enabled/suspended state without repeated metadata-sys lookups per request.
|
||||
async fn bucket_versioning_config(bucket: &str) -> VersioningConfiguration {
|
||||
match BucketVersioningSys::get(bucket).await {
|
||||
Ok(cfg) => cfg,
|
||||
Err(err) => {
|
||||
tracing::warn!("{:?}", err);
|
||||
VersioningConfiguration::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether GET should skip per-shard bitrot verification. Read once: the env
|
||||
/// flag is consulted on every GET and `std::env::var` takes a process-global
|
||||
/// lock. In tests the env is read directly so `temp_env` overrides apply.
|
||||
fn get_skip_verify_bitrot() -> bool {
|
||||
#[cfg(test)]
|
||||
{
|
||||
rustfs_utils::get_env_bool(
|
||||
rustfs_config::ENV_OBJECT_GET_SKIP_BITROT_VERIFY,
|
||||
rustfs_config::DEFAULT_OBJECT_GET_SKIP_BITROT_VERIFY,
|
||||
)
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
{
|
||||
static CACHED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*CACHED.get_or_init(|| {
|
||||
rustfs_utils::get_env_bool(
|
||||
rustfs_config::ENV_OBJECT_GET_SKIP_BITROT_VERIFY,
|
||||
rustfs_config::DEFAULT_OBJECT_GET_SKIP_BITROT_VERIFY,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates options for deleting an object in a bucket.
|
||||
pub async fn del_opts(
|
||||
@@ -55,8 +92,9 @@ pub async fn del_opts(
|
||||
headers: &HeaderMap<HeaderValue>,
|
||||
metadata: HashMap<String, String>,
|
||||
) -> Result<ObjectOptions> {
|
||||
let versioned = BucketVersioningSys::prefix_enabled(bucket, object).await;
|
||||
let version_suspended = BucketVersioningSys::suspended(bucket).await;
|
||||
let versioning_cfg = bucket_versioning_config(bucket).await;
|
||||
let versioned = versioning_cfg.prefix_enabled(object);
|
||||
let version_suspended = versioning_cfg.suspended();
|
||||
|
||||
let vid = if vid.is_none() {
|
||||
get_header(headers, SUFFIX_SOURCE_VERSION_ID).map(|s| s.into_owned())
|
||||
@@ -120,8 +158,9 @@ pub async fn get_opts(
|
||||
part_num: Option<usize>,
|
||||
headers: &HeaderMap<HeaderValue>,
|
||||
) -> Result<ObjectOptions> {
|
||||
let versioned = BucketVersioningSys::prefix_enabled(bucket, object).await;
|
||||
let version_suspended = BucketVersioningSys::prefix_suspended(bucket, object).await;
|
||||
let versioning_cfg = bucket_versioning_config(bucket).await;
|
||||
let versioned = versioning_cfg.prefix_enabled(object);
|
||||
let version_suspended = versioning_cfg.prefix_suspended(object);
|
||||
|
||||
let vid = vid.map(|v| v.as_str().trim().to_owned());
|
||||
|
||||
@@ -159,10 +198,7 @@ pub async fn get_opts(
|
||||
|
||||
// Optionally skip per-shard bitrot hash verification on reads to save CPU.
|
||||
// Background scanner still performs full integrity checks asynchronously.
|
||||
opts.skip_verify_bitrot = rustfs_utils::get_env_bool(
|
||||
rustfs_config::ENV_OBJECT_GET_SKIP_BITROT_VERIFY,
|
||||
rustfs_config::DEFAULT_OBJECT_GET_SKIP_BITROT_VERIFY,
|
||||
);
|
||||
opts.skip_verify_bitrot = get_skip_verify_bitrot();
|
||||
|
||||
fill_conditional_writes_opts_from_header(headers, &mut opts)?;
|
||||
|
||||
@@ -213,8 +249,9 @@ pub async fn put_opts(
|
||||
headers: &HeaderMap<HeaderValue>,
|
||||
metadata: HashMap<String, String>,
|
||||
) -> Result<ObjectOptions> {
|
||||
let versioned = BucketVersioningSys::prefix_enabled(bucket, object).await;
|
||||
let version_suspended = BucketVersioningSys::prefix_suspended(bucket, object).await;
|
||||
let versioning_cfg = bucket_versioning_config(bucket).await;
|
||||
let versioned = versioning_cfg.prefix_enabled(object);
|
||||
let version_suspended = versioning_cfg.prefix_suspended(object);
|
||||
|
||||
let vid = if vid.is_none() {
|
||||
get_header(headers, SUFFIX_SOURCE_VERSION_ID).map(|s| s.into_owned())
|
||||
|
||||
@@ -2208,7 +2208,7 @@ pub fn mark_encrypted_multipart_metadata(metadata: &mut HashMap<String, String>)
|
||||
metadata.insert(MINIO_INTERNAL_ENCRYPTION_MULTIPART_HEADER.to_string(), String::new());
|
||||
}
|
||||
|
||||
fn contains_managed_encryption_metadata(metadata: &HashMap<String, String>) -> bool {
|
||||
pub(crate) fn contains_managed_encryption_metadata(metadata: &HashMap<String, String>) -> bool {
|
||||
metadata.contains_key(INTERNAL_ENCRYPTION_KEY_HEADER)
|
||||
|| metadata.contains_key(MINIO_INTERNAL_ENCRYPTION_S3_SEALED_KEY_HEADER)
|
||||
|| metadata.contains_key(MINIO_INTERNAL_ENCRYPTION_KMS_SEALED_KEY_HEADER)
|
||||
|
||||
@@ -431,6 +431,10 @@ pub(crate) mod ecstore_rpc {
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) mod ecstore_object {
|
||||
pub(crate) use rustfs_ecstore::api::object::{GetObjectBodyCacheHook, register_get_object_body_cache_hook};
|
||||
}
|
||||
|
||||
pub(crate) mod ecstore_set_disk {
|
||||
pub(crate) use rustfs_ecstore::api::set_disk::{DEFAULT_READ_BUFFER_SIZE, get_lock_acquire_timeout, is_valid_storage_class};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user