perf(ecstore): use AHashMap for FileInfo metadata fields (#6738)

This commit is contained in:
houseme
2026-08-27 18:34:10 +08:00
committed by GitHub
parent 94a6da6e83
commit 13a2ae212e
26 changed files with 141 additions and 87 deletions
@@ -69,6 +69,7 @@ use crate::storage_api_contracts::{
range::HTTPRangeSpec,
};
use crate::store::ECStore;
use ahash::AHashMap;
use async_channel::{Receiver as A_Receiver, Sender as A_Sender, bounded};
use http::HeaderMap;
use rand::RngExt as _;
@@ -2870,7 +2871,7 @@ fn spawn_transition_transaction_recovery_once(api: Arc<ECStore>) {
struct StaleMultipartUploadCandidate {
path: String,
initiated: OffsetDateTime,
metadata: Option<HashMap<String, String>>,
metadata: Option<AHashMap<String, String>>,
}
fn parse_stale_uploads_duration(env_key: &str, default: StdDuration) -> StdDuration {
@@ -2915,9 +2916,9 @@ async fn stale_upload_current_size(set: &Arc<SetDisks>, metadata: &HashMap<Strin
stale_upload_current_size_with_opts(set, metadata, upload_dir, false).await
}
async fn stale_upload_current_size_with_opts(
async fn stale_upload_current_size_with_opts<S: std::hash::BuildHasher>(
set: &Arc<SetDisks>,
metadata: &HashMap<String, String>,
metadata: &HashMap<String, String, S>,
upload_dir: &str,
no_lock: bool,
) -> Option<usize> {
@@ -2950,9 +2951,9 @@ async fn stale_upload_current_size_with_opts(
)
}
async fn stale_upload_lifecycle_due(
async fn stale_upload_lifecycle_due<S: std::hash::BuildHasher>(
set: &Arc<SetDisks>,
metadata: &HashMap<String, String>,
metadata: &HashMap<String, String, S>,
initiated: OffsetDateTime,
upload_dir: &str,
no_lock: bool,
@@ -2978,7 +2979,7 @@ async fn stale_upload_lifecycle_due(
.unwrap_or_default(),
is_latest: true,
delete_marker: false,
user_defined: metadata.clone(),
user_defined: metadata.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
..Default::default()
};
@@ -11616,7 +11617,7 @@ mod tests {
// Persist the durable backend identity on the transitioned version so the
// recovered free version carries it (matching a registered mock tier);
// free-version remote cleanup fails closed without it.
let mut transitioned_metadata = HashMap::new();
let mut transitioned_metadata = AHashMap::new();
if let Some(identity) = backend_identity {
rustfs_utils::http::metadata_compat::insert_str(
&mut transitioned_metadata,
@@ -12353,7 +12354,7 @@ mod tests {
StaleMultipartUploadCandidate {
path: "sha/upload".to_string(),
initiated: OffsetDateTime::UNIX_EPOCH,
metadata: Some(HashMap::from([("k".to_string(), "v".to_string())])),
metadata: Some(AHashMap::from([("k".to_string(), "v".to_string())])),
},
);
+2 -2
View File
@@ -5815,7 +5815,7 @@ fn decommission_remote_tiered_opts(
versioned: version_id.is_some(),
version_id,
mod_time: version.mod_time,
user_defined: version.metadata.clone(),
user_defined: version.metadata.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
src_pool_idx,
data_movement: true,
incl_free_versions: version.tier_free_version(),
@@ -11894,7 +11894,7 @@ mod tests {
let mod_time = OffsetDateTime::now_utc();
let version = rustfs_filemeta::FileInfo {
mod_time: Some(mod_time),
metadata: HashMap::from([("x-amz-meta-key".to_string(), "value".to_string())]),
metadata: AHashMap::from([("x-amz-meta-key".to_string(), "value".to_string())]),
..Default::default()
};
+2 -2
View File
@@ -1877,7 +1877,7 @@ mod tests {
mod_time: Some(OffsetDateTime::UNIX_EPOCH),
data_dir: Some(Uuid::from_u128(100)),
checksum: Some(Bytes::from_static(b"object-checksum")),
metadata: HashMap::from([
metadata: AHashMap::from([
("etag".to_string(), "etag-value".to_string()),
("x-amz-meta-key".to_string(), metadata_value.to_string()),
]),
@@ -1887,7 +1887,7 @@ mod tests {
size: 128,
actual_size: 128,
mod_time: Some(OffsetDateTime::UNIX_EPOCH),
checksums: Some(HashMap::from([(ChecksumType::CRC32C.to_string(), "part-checksum".to_string())])),
checksums: Some(AHashMap::from([(ChecksumType::CRC32C.to_string(), "part-checksum".to_string())])),
..Default::default()
}],
..Default::default()
+5 -1
View File
@@ -64,7 +64,11 @@ pub(crate) const ENCRYPTED_FRAME_LAYOUT_FIXED8K_SUFFIX: &str = "encrypted-frame-
pub(crate) const ENV_RUSTFS_ENCRYPTED_RANGE_SEEK: &str = "RUSTFS_ENCRYPTED_RANGE_SEEK";
pub(crate) const DEFAULT_RUSTFS_ENCRYPTED_RANGE_SEEK: bool = true;
pub(crate) fn has_encrypted_part_layout_marker(metadata: &HashMap<String, String>, suffix: &str, expected: &str) -> bool {
pub(crate) fn has_encrypted_part_layout_marker<S: std::hash::BuildHasher>(
metadata: &HashMap<String, String, S>,
suffix: &str,
expected: &str,
) -> bool {
let mut value = None;
for (key, candidate) in metadata {
if !rustfs_utils::http::has_internal_suffix(key, suffix) {
@@ -168,7 +168,7 @@ pub fn to_s3s_etag(etag: &str) -> ETag {
ETag::Strong(etag.to_string())
}
pub fn get_raw_etag(metadata: &HashMap<String, String>) -> String {
pub fn get_raw_etag<S: std::hash::BuildHasher>(metadata: &HashMap<String, String, S>) -> String {
metadata
.get("etag")
.cloned()
+3 -3
View File
@@ -1008,7 +1008,7 @@ impl ObjectInfo {
successor_mod_time: fi.successor_mod_time,
etag,
inlined,
user_defined: Arc::new(metadata),
user_defined: Arc::new(metadata.iter().map(|(k, v)| (k.clone(), v.clone())).collect()),
transitioned_object,
transition_version_state: fi.transition_version_state,
checksum: fi.checksum.clone(),
@@ -1316,7 +1316,7 @@ impl ObjectInfo {
if part > 0
&& let Some(checksums) = self.parts.iter().find(|p| p.number == part).and_then(|p| p.checksums.clone())
{
return Ok((checksums, true));
return Ok((checksums.iter().map(|(k, v)| (k.clone(), v.clone())).collect(), true));
}
if let Some(data) = &self.checksum {
@@ -1812,7 +1812,7 @@ mod tests {
storageclass::GLACIER,
] {
let fi = FileInfo {
metadata: HashMap::from([(AMZ_STORAGE_CLASS.to_string(), legacy_label.to_string())]),
metadata: AHashMap::from([(AMZ_STORAGE_CLASS.to_string(), legacy_label.to_string())]),
..Default::default()
};
@@ -57,7 +57,7 @@ fn rebalance_remote_tiered_opts(
versioned: version_id.is_some(),
version_id,
mod_time: version.mod_time,
user_defined: version.metadata.clone(),
user_defined: version.metadata.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
src_pool_idx,
data_movement: true,
include_part_checksums: true,
+3 -1
View File
@@ -1454,7 +1454,9 @@ fn tier_backend_identity(config: &TierConfig) -> io::Result<TierDestinationId> {
encode_tier_backend_identity(tier_type, endpoint, bucket, prefix, region, routing_account)
}
pub(crate) fn tier_destination_id_from_metadata(metadata: &HashMap<String, String>) -> io::Result<Option<TierDestinationId>> {
pub(crate) fn tier_destination_id_from_metadata<S: std::hash::BuildHasher>(
metadata: &HashMap<String, String, S>,
) -> io::Result<Option<TierDestinationId>> {
let Some(encoded) = rustfs_utils::http::metadata_compat::get_consistent_str(
metadata,
rustfs_utils::http::metadata_compat::SUFFIX_TRANSITION_TIER_DESTINATION_ID,
+5 -2
View File
@@ -609,7 +609,7 @@ impl SetDisks {
|| Self::starts_with_ignore_ascii_case(suffix, http::SUFFIX_REPLICATION_DELETE_MARKER_VERSION_ARN_PREFIX)
}
fn update_hash_quorum_metadata_map(hasher: &mut Sha256, entries: &HashMap<String, String>) {
fn update_hash_quorum_metadata_map<S: std::hash::BuildHasher>(hasher: &mut Sha256, entries: &HashMap<String, String, S>) {
let mut entries = entries
.iter()
.filter(|(name, _)| !Self::is_replication_quorum_metadata_key(name))
@@ -635,7 +635,10 @@ impl SetDisks {
/// so the dual internal prefixes carrying the same mapping share one
/// identity, while a genuine disagreement between disks still changes the
/// hash and surfaces as a quorum difference.
fn update_hash_target_delete_marker_versions(hasher: &mut Sha256, metadata: &HashMap<String, String>) {
fn update_hash_target_delete_marker_versions<S: std::hash::BuildHasher>(
hasher: &mut Sha256,
metadata: &HashMap<String, String, S>,
) {
let (versions, corrupt) = http::target_delete_marker_versions(metadata);
hasher.update([u8::from(corrupt)]);
let mut versions = versions.iter().collect::<Vec<_>>();
+13 -6
View File
@@ -190,7 +190,9 @@ use tracing::error;
use tracing::{Instrument, debug, info, warn};
use uuid::Uuid;
pub(super) fn restore_operation_id_from_metadata(metadata: &HashMap<String, String>) -> Result<Option<Uuid>> {
pub(super) fn restore_operation_id_from_metadata<S: std::hash::BuildHasher>(
metadata: &HashMap<String, String, S>,
) -> Result<Option<Uuid>> {
let Some(value) = rustfs_utils::http::metadata_compat::get_consistent_str(metadata, SUFFIX_RESTORE_OPERATION_ID) else {
if rustfs_utils::http::metadata_compat::contains_key_str(metadata, SUFFIX_RESTORE_OPERATION_ID) {
return Err(Error::other("invalid restore operation id metadata".to_string()));
@@ -204,14 +206,19 @@ pub(super) fn restore_operation_id_from_metadata(metadata: &HashMap<String, Stri
Ok(Some(id))
}
pub(super) fn require_restore_operation_id(metadata: &HashMap<String, String>, expected: Uuid) -> Result<()> {
pub(super) fn require_restore_operation_id<S: std::hash::BuildHasher>(
metadata: &HashMap<String, String, S>,
expected: Uuid,
) -> Result<()> {
match restore_operation_id_from_metadata(metadata)? {
Some(actual) if actual == expected => Ok(()),
_ => Err(Error::other("restore operation id changed before copy-back".to_string())),
}
}
pub(super) fn restore_commit_operation_id_from_metadata(metadata: &HashMap<String, String>) -> Result<Option<Uuid>> {
pub(super) fn restore_commit_operation_id_from_metadata<S: std::hash::BuildHasher>(
metadata: &HashMap<String, String, S>,
) -> Result<Option<Uuid>> {
if !metadata.contains_key(X_AMZ_RESTORE.as_str()) {
return Ok(None);
}
@@ -466,13 +473,13 @@ fn release_materialized_read_lock(bucket: &str, object: &str, read_lock_guard: O
drop(read_lock_guard);
}
pub(crate) fn strip_internal_multipart_metadata(metadata: &mut HashMap<String, String>) {
pub(crate) fn strip_internal_multipart_metadata<S: std::hash::BuildHasher>(metadata: &mut HashMap<String, String, S>) {
metadata.remove(RUSTFS_MULTIPART_BUCKET_KEY);
metadata.remove(RUSTFS_MULTIPART_OBJECT_KEY);
rustfs_utils::http::metadata_compat::remove_str(metadata, SUFFIX_BUCKET_INCARNATION_ID);
}
fn should_persist_encryption_original_size(metadata: &HashMap<String, String>) -> bool {
fn should_persist_encryption_original_size<S: std::hash::BuildHasher>(metadata: &HashMap<String, String, S>) -> bool {
metadata.keys().any(|key| is_object_encryption_marker(key))
}
@@ -8795,7 +8802,7 @@ mod tests {
#[test]
fn test_list_object_etags() {
// Test extracting etags from file info metadata
let mut metadata = HashMap::new();
let mut metadata = AHashMap::new();
metadata.insert("etag".to_string(), "test-etag".to_string());
let file_info = FileInfo {
+11 -8
View File
@@ -449,7 +449,7 @@ fn fence_commit_on_lock_loss(guard: Option<&ObjectLockDiagGuard>, mode: &'static
Ok(())
}
fn multipart_bucket_incarnation_id(metadata: &HashMap<String, String>) -> Result<Option<Uuid>> {
fn multipart_bucket_incarnation_id<S: std::hash::BuildHasher>(metadata: &HashMap<String, String, S>) -> Result<Option<Uuid>> {
let Some(value) = rustfs_utils::http::metadata_compat::get_consistent_str(metadata, SUFFIX_BUCKET_INCARNATION_ID) else {
if rustfs_utils::http::metadata_compat::contains_key_str(metadata, SUFFIX_BUCKET_INCARNATION_ID) {
return Err(Error::other("invalid multipart bucket incarnation metadata"));
@@ -463,12 +463,15 @@ fn multipart_bucket_incarnation_id(metadata: &HashMap<String, String>) -> Result
Ok(Some(incarnation))
}
fn multipart_bucket_incarnation_matches(metadata: &HashMap<String, String>, expected: Uuid) -> bool {
fn multipart_bucket_incarnation_matches<S: std::hash::BuildHasher>(
metadata: &HashMap<String, String, S>,
expected: Uuid,
) -> bool {
matches!(multipart_bucket_incarnation_id(metadata), Ok(Some(actual)) if actual == expected)
}
fn validate_multipart_bucket_incarnation(
metadata: &HashMap<String, String>,
fn validate_multipart_bucket_incarnation<S: std::hash::BuildHasher>(
metadata: &HashMap<String, String, S>,
bucket: &str,
object: &str,
upload_id: &str,
@@ -1521,7 +1524,7 @@ impl crate::storage_api_contracts::multipart::MultipartOperations for SetDisks {
mod_time: Some(OffsetDateTime::now_utc()),
actual_size,
index: index_op,
checksums: if checksums.is_empty() { None } else { Some(checksums) },
checksums: if checksums.is_empty() { None } else { Some(checksums.iter().map(|(k, v)| (k.clone(), v.clone())).collect()) },
..Default::default()
};
@@ -1716,7 +1719,7 @@ impl crate::storage_api_contracts::multipart::MultipartOperations for SetDisks {
max_parts,
part_number_marker,
user_defined: {
let mut metadata = fi.metadata.clone();
let mut metadata: HashMap<String, String> = fi.metadata.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
strip_internal_multipart_metadata(&mut metadata);
metadata
},
@@ -1975,7 +1978,7 @@ impl crate::storage_api_contracts::multipart::MultipartOperations for SetDisks {
let mod_time = opts.mod_time.unwrap_or_else(OffsetDateTime::now_utc);
for f in parts_metadatas.iter_mut() {
f.metadata = user_defined.clone();
f.metadata = user_defined.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
f.mod_time = Some(mod_time);
f.fresh = true;
}
@@ -2064,7 +2067,7 @@ impl crate::storage_api_contracts::multipart::MultipartOperations for SetDisks {
upload_id: upload_id.to_owned(),
user_defined: {
strip_internal_multipart_metadata(&mut fi.metadata);
fi.metadata.clone()
fi.metadata.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
},
..Default::default()
})
+13 -12
View File
@@ -72,11 +72,11 @@ use crate::set_disk::runtime_sources;
use crate::storage_api_contracts::multipart::MultipartOperations;
use crate::storage_api_contracts::object::ObjectIO;
use crate::storage_api_contracts::object::ObjectOperations;
use ahash::AHashMap;
use rustfs_lock::LockManager;
use rustfs_rio::EtagResolvable;
use rustfs_rio::HashReaderMut;
use rustfs_rio::TryGetIndex;
use rustfs_utils::http::HeaderExt;
use tokio::io::AsyncWriteExt;
#[cfg(all(test, feature = "test-util"))]
@@ -1107,13 +1107,13 @@ fn is_restore_control_metadata(key: &str) -> bool {
.is_some_and(|remainder| remainder.is_empty())
}
fn restore_metadata_update_preserves_protected_metadata(
existing: &HashMap<String, String>,
replacement: &HashMap<String, String>,
fn restore_metadata_update_preserves_protected_metadata<S1: std::hash::BuildHasher, S2: std::hash::BuildHasher>(
existing: &HashMap<String, String, S1>,
replacement: &HashMap<String, String, S2>,
) -> bool {
let mut existing = existing.clone();
let mut existing: HashMap<String, String> = existing.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
clean_metadata(&mut existing);
let mut replacement = replacement.clone();
let mut replacement: HashMap<String, String> = replacement.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
clean_metadata(&mut replacement);
let existing_count = existing.keys().filter(|key| !is_restore_control_metadata(key)).count();
let replacement_count = replacement.keys().filter(|key| !is_restore_control_metadata(key)).count();
@@ -2211,9 +2211,9 @@ pub(in crate::set_disk) fn stored_replication_category_metadata(existing: &Objec
///
/// Returns whether `inbound` was modified. Callers must hold the object write
/// lock so the stored values compared here are the ones being replaced.
pub(in crate::set_disk) fn merge_replication_metadata_lww(
inbound: &mut HashMap<String, String>,
existing: &HashMap<String, String>,
pub(in crate::set_disk) fn merge_replication_metadata_lww<S1: std::hash::BuildHasher, S2: std::hash::BuildHasher>(
inbound: &mut HashMap<String, String, S1>,
existing: &HashMap<String, String, S2>,
opts: &ObjectOptions,
) -> bool {
use rustfs_utils::http::headers::{
@@ -2844,7 +2844,7 @@ impl SetDisks {
)));
}
fi.metadata = user_defined;
fi.metadata = user_defined.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
fi.mod_time = mod_time;
fi.size = w_size as i64;
fi.versioned = opts.versioned || opts.version_suspended;
@@ -6051,7 +6051,8 @@ impl crate::storage_api_contracts::object::ObjectOperations for SetDisks {
} else {
None
};
let mut replacement_metadata = (*src_info.user_defined).clone();
let mut replacement_metadata: AHashMap<String, String> =
(*src_info.user_defined).iter().map(|(k, v)| (k.clone(), v.clone())).collect();
if let Some(part_checksums) = preserved_part_checksums {
rustfs_utils::http::insert_str(&mut replacement_metadata, rustfs_utils::http::SUFFIX_PART_CHECKSUMS, part_checksums);
}
@@ -7493,7 +7494,7 @@ impl crate::storage_api_contracts::object::ObjectOperations for SetDisks {
X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str(),
X_AMZ_OBJECT_LOCK_LEGAL_HOLD.as_str(),
] {
if let Some(value) = fi.metadata.lookup(header).filter(|value| !value.is_empty()) {
if let Some(value) = fi.metadata.get(header).filter(|value| !value.is_empty()) {
transition_meta.insert(header.to_ascii_lowercase(), value.to_string());
}
}
+2 -2
View File
@@ -4780,7 +4780,7 @@ mod tests {
purge_targets: version_purge_statuses_map("arn:minio:replication:target=PENDING;"),
..Default::default()
})),
metadata: HashMap::from([
metadata: AHashMap::from([
("etag".to_string(), "etag-value".to_string()),
("x-amz-meta-key".to_string(), "metadata-value".to_string()),
(rustfs_utils::http::AMZ_OBJECT_TAGGING.to_string(), "tag=value".to_string()),
@@ -4864,7 +4864,7 @@ mod tests {
source.parts = vec![rustfs_filemeta::ObjectPartInfo {
number: 1,
mod_time: Some(OffsetDateTime::UNIX_EPOCH + time::Duration::SECOND),
checksums: Some(HashMap::from([("CRC32C".to_string(), "AAAAAA==".to_string())])),
checksums: Some(AHashMap::from([("CRC32C".to_string(), "AAAAAA==".to_string())])),
..Default::default()
}];
rustfs_utils::http::insert_str(
+3 -3
View File
@@ -26,18 +26,18 @@ static STRICT_BUCKET_NAME_REGEX: LazyLock<Regex> =
static NON_STRICT_BUCKET_NAME_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[A-Za-z0-9][A-Za-z0-9\.\-_:]{1,61}[A-Za-z0-9]$").expect("valid non-strict bucket name regex"));
pub fn clean_metadata(metadata: &mut HashMap<String, String>) {
pub fn clean_metadata<S: std::hash::BuildHasher>(metadata: &mut HashMap<String, String, S>) {
remove_standard_storage_class(metadata);
clean_metadata_keys(metadata, &["md5Sum", "etag", "expires", AMZ_OBJECT_TAGGING, "last-modified"]);
}
pub fn remove_standard_storage_class(metadata: &mut HashMap<String, String>) {
pub fn remove_standard_storage_class<S: std::hash::BuildHasher>(metadata: &mut HashMap<String, String, S>) {
if metadata.get(AMZ_STORAGE_CLASS) == Some(&STANDARD.to_string()) {
metadata.remove(AMZ_STORAGE_CLASS);
}
}
pub fn clean_metadata_keys(metadata: &mut HashMap<String, String>, key_names: &[&str]) {
pub fn clean_metadata_keys<S: std::hash::BuildHasher>(metadata: &mut HashMap<String, String, S>, key_names: &[&str]) {
for key in key_names {
metadata.remove(key.to_owned());
}