From 73bd5d9d956fcc4780e3380d737711d63b09d209 Mon Sep 17 00:00:00 2001 From: houseme Date: Thu, 13 Aug 2026 03:07:59 +0800 Subject: [PATCH] perf(get): reduce request entry allocations (#6029) Co-authored-by: heihutu --- crates/ecstore/src/store/object.rs | 6 ++--- crates/utils/src/path.rs | 23 +++++++++++++--- rustfs/src/app/object_usecase.rs | 42 ++++++++++++++++++------------ 3 files changed, 49 insertions(+), 22 deletions(-) diff --git a/crates/ecstore/src/store/object.rs b/crates/ecstore/src/store/object.rs index 85d01d249..a9358ed6b 100644 --- a/crates/ecstore/src/store/object.rs +++ b/crates/ecstore/src/store/object.rs @@ -1585,7 +1585,7 @@ impl ECStore { ) -> Result { check_get_obj_args(bucket, object)?; - let object = encode_dir_object(object); + let object = rustfs_utils::path::encode_dir_object_ref(object); let mut opts = opts.clone(); let read_lock_guard = self .acquire_object_read_lock_if_needed("get_object", bucket, &object, &mut opts) @@ -1593,14 +1593,14 @@ impl ECStore { let reader = if self.single_pool() { self.pools[0] - .get_object_reader(bucket, object.as_str(), range, h, &opts) + .get_object_reader(bucket, object.as_ref(), range, h, &opts) .await? } else { let (_, idx) = self .get_latest_accessible_object_info_with_idx(bucket, &object, &opts) .await?; self.pools[idx] - .get_object_reader(bucket, object.as_str(), range, h, &opts) + .get_object_reader(bucket, object.as_ref(), range, h, &opts) .await? }; diff --git a/crates/utils/src/path.rs b/crates/utils/src/path.rs index 138ed4ecd..d672302d4 100644 --- a/crates/utils/src/path.rs +++ b/crates/utils/src/path.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::borrow::Cow; use std::path::Component; use std::path::Path; use std::path::PathBuf; @@ -44,14 +45,19 @@ pub fn has_suffix(s: &str, suffix: &str) -> bool { /// If the object name ends with a slash, it is considered a directory object. /// The trailing slash is removed and `GLOBAL_DIR_SUFFIX` is appended. /// If it does not end with a slash, the name is returned as is. -pub fn encode_dir_object(object: &str) -> String { +pub fn encode_dir_object_ref(object: &str) -> Cow<'_, str> { if has_suffix(object, SLASH_SEPARATOR) { - format!("{}{}", object.trim_end_matches(SLASH_SEPARATOR), GLOBAL_DIR_SUFFIX) + Cow::Owned(format!("{}{}", object.trim_end_matches(SLASH_SEPARATOR), GLOBAL_DIR_SUFFIX)) } else { - object.to_string() + Cow::Borrowed(object) } } +/// Owned compatibility wrapper for callers that retain or mutate the encoded name. +pub fn encode_dir_object(object: &str) -> String { + encode_dir_object_ref(object).into_owned() +} + /// Checks if the given object name represents a directory object. /// /// Returns true if the object name ends with `GLOBAL_DIR_SUFFIX`. @@ -602,6 +608,17 @@ mod tests { use super::*; use proptest::prelude::*; + #[test] + fn encode_dir_object_ref_borrows_objects_and_encodes_directories() { + let object = "prefix/object"; + let encoded = encode_dir_object_ref(object); + assert!(matches!(encoded, Cow::Borrowed(value) if value == object)); + + let encoded = encode_dir_object_ref("prefix/directory/"); + assert!(matches!(encoded, Cow::Owned(ref value) if value == "prefix/directory__XLDIR__")); + assert_eq!(encode_dir_object("prefix/directory/"), encoded); + } + #[test] fn test_trim_etag() { // Test with quoted ETag diff --git a/rustfs/src/app/object_usecase.rs b/rustfs/src/app/object_usecase.rs index fde4e9924..a6a656ac1 100644 --- a/rustfs/src/app/object_usecase.rs +++ b/rustfs/src/app/object_usecase.rs @@ -2044,6 +2044,18 @@ struct GetObjectResumeContext { identity: GetObjectResumeIdentity, } +fn get_object_store_headers(request_headers: &HeaderMap) -> HeaderMap { + let mut headers = HeaderMap::new(); + for name in [SSEC_ALGORITHM_HEADER, SSEC_KEY_HEADER, SSEC_KEY_MD5_HEADER] { + if let Some(value) = request_headers.get(name) { + let mut value = value.clone(); + value.set_sensitive(true); + headers.insert(name, value); + } + } + headers +} + impl GetObjectResumeContext { #[allow(clippy::too_many_arguments)] fn new( @@ -2061,17 +2073,9 @@ impl GetObjectResumeContext { { opts.version_id = Some(version_id.to_string()); } - let mut ssec_headers = HeaderMap::new(); - for name in [SSEC_ALGORITHM_HEADER, SSEC_KEY_HEADER, SSEC_KEY_MD5_HEADER] { - if let Some(value) = request_headers.get(name) { - // The store's instrumented spans record the header argument at - // debug level; mark the replayed values sensitive so the SSE-C - // key is redacted there on every resume attempt. - let mut value = value.clone(); - value.set_sensitive(true); - ssec_headers.insert(name, value); - } - } + // Store spans record their header argument at debug level. Retain only + // the SSE-C inputs needed to reopen the reader and keep them redacted. + let ssec_headers = get_object_store_headers(request_headers); Self { store, bucket: bucket.to_string(), @@ -4455,6 +4459,7 @@ impl DefaultObjectUsecase { ) -> S3Result { let read_start = std::time::Instant::now(); let read_stage_start = rustfs_io_metrics::get_stage_metrics_enabled().then_some(read_start); + let store_headers = get_object_store_headers(&req.headers); let cache_adapter = self.object_data_cache(); if cache_adapter.is_disabled() || !cache_adapter.materialize_fill_enabled() { let io_planning = Self::acquire_get_object_io_planning( @@ -4469,7 +4474,7 @@ impl DefaultObjectUsecase { .await?; let reader = track_object_read_setup( object_traffic_health.as_deref(), - store.get_object_reader(bucket, key, rs.clone(), req.headers.clone(), opts), + store.get_object_reader(bucket, key, rs.clone(), store_headers.clone(), opts), ) .await .map_err(map_get_object_reader_error)?; @@ -4596,7 +4601,7 @@ impl DefaultObjectUsecase { drop(metadata_admission.take()); let outcome = coordinate_cold_fill(&coordinator, cache_key, waiter_deadline, Some(proposed_producer_deadline), { let adapter = &cache_adapter; - let headers = &req.headers; + let headers = &store_headers; let store = &store; let range = &rs; let object_traffic_health = &object_traffic_health; @@ -4760,7 +4765,7 @@ impl DefaultObjectUsecase { .ok_or_else(|| s3_error!(InternalError, "prepared metadata admission is unavailable"))?; let reader = track_object_read_setup( object_traffic_health.as_deref(), - prepared.with_headers(req.headers.clone()).into_reader(), + prepared.with_headers(store_headers.clone()).into_reader(), ) .await .map_err(map_get_object_reader_error)?; @@ -4785,14 +4790,14 @@ impl DefaultObjectUsecase { .map_err(map_get_object_reader_error)?; track_object_read_setup( object_traffic_health.as_deref(), - prepared.with_headers(req.headers.clone()).into_reader(), + prepared.with_headers(store_headers.clone()).into_reader(), ) .await .map_err(map_get_object_reader_error)? } else { track_object_read_setup( object_traffic_health.as_deref(), - store.get_object_reader(bucket, key, rs.clone(), req.headers.clone(), opts), + store.get_object_reader(bucket, key, rs.clone(), store_headers, opts), ) .await .map_err(map_get_object_reader_error)? @@ -13722,6 +13727,11 @@ mod tests { request_headers.insert(SSEC_KEY_MD5_HEADER, HeaderValue::from_static("bWQ1")); request_headers.insert(http::header::AUTHORIZATION, HeaderValue::from_static("AWS4-HMAC-SHA256 Credential=test")); request_headers.insert("x-amz-security-token", HeaderValue::from_static("session-token")); + let store_headers = get_object_store_headers(&request_headers); + assert_eq!(store_headers.len(), 3, "only store-consumed SSE-C headers are forwarded"); + assert!(store_headers.values().all(HeaderValue::is_sensitive)); + assert!(store_headers.get(http::header::AUTHORIZATION).is_none()); + assert!(store_headers.get("x-amz-security-token").is_none()); let plain_info = ObjectInfo { size: 11, ..Default::default()