diff --git a/Cargo.lock b/Cargo.lock index 45fef954d..fafbc34ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,6 +91,7 @@ dependencies = [ "const-random", "getrandom 0.3.4", "once_cell", + "serde", "version_check", "zerocopy", ] @@ -9716,6 +9717,7 @@ dependencies = [ name = "rustfs-filemeta" version = "1.0.0-rc.3" dependencies = [ + "ahash", "arc-swap", "byteorder", "bytes", @@ -10751,6 +10753,7 @@ dependencies = [ name = "rustfs-utils" version = "1.0.0-rc.3" dependencies = [ + "ahash", "base64-simd", "blake2", "brotli", diff --git a/Cargo.toml b/Cargo.toml index 3400d9309..60a9f4182 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -371,6 +371,9 @@ hotpath = { version = "0.24.0", default-features = false } # Snapshot testing for output format regression detection insta = { version = "1.48" } +# High-performance hashing +ahash = { version = "0.8", default-features = false, features = ["std", "runtime-rng", "serde"] } + [workspace.metadata.cargo-shear] ignored = ["hotpath", "rustfs"] diff --git a/crates/filemeta/Cargo.toml b/crates/filemeta/Cargo.toml index ef6095d65..101dc325e 100644 --- a/crates/filemeta/Cargo.toml +++ b/crates/filemeta/Cargo.toml @@ -51,6 +51,9 @@ s3s = { workspace = true, features = ["minio"] } regex.workspace = true arc-swap.workspace = true +# High-performance hashing +ahash = { workspace = true, features = ["serde"] } + [dev-dependencies] criterion = { workspace = true, features = ["html_reports"] } tempfile = { workspace = true } diff --git a/crates/filemeta/src/fileinfo.rs b/crates/filemeta/src/fileinfo.rs index 42613185e..b3e57026b 100644 --- a/crates/filemeta/src/fileinfo.rs +++ b/crates/filemeta/src/fileinfo.rs @@ -22,6 +22,7 @@ use rustfs_utils::http::{ contains_key_str, get_consistent_str, get_str, has_internal_suffix, insert_str, is_encryption_metadata_key, starts_with_ignore_ascii_case, }; +use ahash::AHashMap; use s3s::dto::{RestoreStatus, Timestamp}; use s3s::header::X_AMZ_RESTORE; use serde::de::{self, MapAccess, SeqAccess, Visitor, value::MapAccessDeserializer}; @@ -67,7 +68,7 @@ pub struct ObjectPartInfo { // Index holds the index of the part in the erasure coding pub index: Option, // Checksums holds checksums of the part - pub checksums: Option>, + pub checksums: Option>, pub error: Option, } @@ -268,7 +269,7 @@ pub struct FileInfo { pub mode: Option, // WrittenByVersion is the unix time stamp of the version that created this version of the object pub written_by_version: Option, - pub metadata: HashMap, + pub metadata: AHashMap, pub parts: Vec, pub erasure: ErasureInfo, // MarkDeleted marks this version as deleted @@ -301,7 +302,7 @@ fn is_sensitive_metadata_key(key: &str) -> bool { .any(|prefix| starts_with_ignore_ascii_case(key, prefix)) } -struct RedactedMetadata<'a>(&'a HashMap); +struct RedactedMetadata<'a>(&'a AHashMap); impl std::fmt::Debug for RedactedMetadata<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -425,7 +426,7 @@ struct FileInfoMapDef { size: i64, mode: Option, written_by_version: Option, - metadata: HashMap, + metadata: AHashMap, parts: Vec, erasure: ErasureInfo, mark_deleted: bool, @@ -1079,7 +1080,7 @@ impl FileInfo { mod_time: Option, actual_size: i64, index: Option, - checksums: Option>, + checksums: Option>, ) { let part = ObjectPartInfo { etag, @@ -1457,7 +1458,7 @@ pub fn parse_restore_obj_status(restore_hdr: &str) -> Result { Err(Error::other(ERR_RESTORE_HDR_MALFORMED)) } -pub fn is_restored_object_on_disk(meta: &HashMap) -> bool { +pub fn is_restored_object_on_disk(meta: &HashMap) -> bool { if let Some(restore_hdr) = meta.get(X_AMZ_RESTORE.as_str()) && let Ok(restore_status) = parse_restore_obj_status(restore_hdr) { diff --git a/crates/filemeta/src/filemeta.rs b/crates/filemeta/src/filemeta.rs index 5ce22b3c7..0ef17adcb 100644 --- a/crates/filemeta/src/filemeta.rs +++ b/crates/filemeta/src/filemeta.rs @@ -174,10 +174,10 @@ fn valid_target_delete_marker_version(arn: &str, version_id: &str) -> bool { /// included in the quorum hash, so such a divergence does surface — but as a /// quorum failure on an otherwise healthy object, which is not a state worth /// reaching. Merge the RPC metadata carrier instead, and only ever insert. -fn persist_target_delete_marker_versions( +fn persist_target_delete_marker_versions( meta_sys: &mut HashMap>, versions: &HashMap, - transport_metadata: &HashMap, + transport_metadata: &HashMap, ) { let mut bounded = BTreeMap::new(); // A corrupt carrier means the dual internal prefixes disagreed. Do not merge diff --git a/crates/filemeta/src/filemeta/version.rs b/crates/filemeta/src/filemeta/version.rs index 034e76017..e79cc0031 100644 --- a/crates/filemeta/src/filemeta/version.rs +++ b/crates/filemeta/src/filemeta/version.rs @@ -26,7 +26,7 @@ use super::msgp_decode::{ PrependByteReader, prealloc_hint, read_exact_vec, read_nil_or_array_len, read_nil_or_map_len, skip_msgp_value, }; use super::*; -use crate::{ChecksumInfo, TransitionVersionState}; +use crate::{AHashMap, ChecksumInfo, TransitionVersionState}; use rustfs_utils::HashAlgorithm; use rustfs_utils::http::{ RUSTFS_INTERNAL_PREFIX, SUFFIX_CRC, SUFFIX_FREE_VERSION, SUFFIX_INLINE_DATA, SUFFIX_PART_CHECKSUMS, SUFFIX_PURGESTATUS, @@ -377,7 +377,7 @@ impl<'a> DerivedInternalMetadata<'a> { } } -struct UniquePartChecksums(HashMap); +struct UniquePartChecksums(AHashMap); impl<'de> serde::Deserialize<'de> for UniquePartChecksums { fn deserialize(deserializer: D) -> std::result::Result @@ -397,7 +397,7 @@ impl<'de> serde::Deserialize<'de> for UniquePartChecksums { where A: serde::de::SeqAccess<'de>, { - let mut checksums = HashMap::with_capacity(seq.size_hint().unwrap_or_default()); + let mut checksums = AHashMap::with_capacity(seq.size_hint().unwrap_or_default()); while let Some((key, value)) = seq.next_element::<(String, String)>()? { if checksums.insert(key, value).is_some() { return Err(serde::de::Error::custom("duplicate part checksum name")); @@ -1477,7 +1477,7 @@ pub struct MetaObjectV1 { #[serde(rename = "Erasure")] pub erasure: MetaObjectV1Erasure, #[serde(rename = "Meta")] - pub meta: HashMap, + pub meta: AHashMap, #[serde(rename = "Parts")] pub parts: Vec, #[serde(rename = "VersionID")] @@ -1543,7 +1543,7 @@ pub struct MetaObjectV1Part { #[serde(rename = "i")] pub index: Option, #[serde(rename = "crc")] - pub checksums: Option>, + pub checksums: Option>, #[serde(rename = "err")] pub error: Option, } @@ -1887,7 +1887,7 @@ impl MetaObjectV1Part { "i" => self.index = Some(Bytes::from(read_msgp_bin(rd)?)), "crc" => { let len = rmp::decode::read_map_len(rd)? as usize; - let mut checksums = HashMap::with_capacity(prealloc_hint(len)); + let mut checksums = AHashMap::with_capacity(prealloc_hint(len)); for _ in 0..len { checksums.insert(read_msgp_string(rd)?, read_msgp_string(rd)?); } @@ -2570,7 +2570,7 @@ impl MetaObject { Vec::new() }; - let mut metadata = HashMap::with_capacity(self.meta_user.len() + self.meta_sys.len()); + let mut metadata = AHashMap::with_capacity(self.meta_user.len() + self.meta_sys.len()); for (k, v) in &self.meta_user { if k == AMZ_META_UNENCRYPTED_CONTENT_LENGTH || k == AMZ_META_UNENCRYPTED_CONTENT_MD5 { continue; @@ -2861,7 +2861,7 @@ impl From for MetaObject { } } -fn get_internal_replication_state(metadata: &HashMap) -> Option { +fn get_internal_replication_state(metadata: &HashMap) -> Option { let mut rs = ReplicationState::default(); let mut has = false; @@ -2942,7 +2942,7 @@ impl MetaDeleteMarker { } pub fn into_fileinfo(&self, volume: &str, path: &str, _all_parts: bool) -> Result { - let metadata = self + let metadata: AHashMap = self .meta_sys .clone() .into_iter() diff --git a/crates/filemeta/src/lib.rs b/crates/filemeta/src/lib.rs index 3fca95ea6..06c38d326 100644 --- a/crates/filemeta/src/lib.rs +++ b/crates/filemeta/src/lib.rs @@ -22,6 +22,12 @@ mod replication; pub mod test_data; +/// High-performance HashMap type alias using ahash instead of SipHash. +pub type AHashMap = ahash::AHashMap; + +/// High-performance HashSet type alias using ahash. +pub type AHashSet = ahash::AHashSet; + pub use error::*; pub use fileinfo::*; pub use filemeta::*; diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 2e3c7c6d8..8f0a184d3 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -58,6 +58,9 @@ transform-stream = { workspace = true, optional = true } url = { workspace = true, optional = true } zstd = { workspace = true, optional = true } +# High-performance hashing +ahash = { workspace = true, optional = true } + [dev-dependencies] criterion = { workspace = true, features = ["html_reports"] } tempfile = { workspace = true } diff --git a/crates/utils/src/http/metadata_compat.rs b/crates/utils/src/http/metadata_compat.rs index aa403d108..c5ad466fa 100644 --- a/crates/utils/src/http/metadata_compat.rs +++ b/crates/utils/src/http/metadata_compat.rs @@ -182,13 +182,13 @@ pub fn internal_key_rustfs(suffix: &str) -> String { // === String type (FileInfo.metadata, user_defined) === -pub fn insert_str(map: &mut HashMap, suffix: &str, value: String) { +pub fn insert_str(map: &mut HashMap, suffix: &str, value: String) { let (k1, k2) = both_keys(suffix); map.insert(k1, value.clone()); map.insert(k2, value); } -pub fn get_str(map: &HashMap, suffix: &str) -> Option { +pub fn get_str(map: &HashMap, suffix: &str) -> Option { if let Some(v) = with_internal_key(RUSTFS_INTERNAL_PREFIX, suffix, |k1| map.get(k1).cloned()) { return Some(v); } @@ -202,7 +202,7 @@ pub fn get_str(map: &HashMap, suffix: &str) -> Option { .map(|(_, value)| value.clone()) } -fn get_consistent_value<'a, V: AsRef<[u8]>>(map: &'a HashMap, suffix: &str) -> Option<&'a V> { +fn get_consistent_value<'a, V: AsRef<[u8]>, S: std::hash::BuildHasher>(map: &'a HashMap, suffix: &str) -> Option<&'a V> { let (rustfs_key, minio_key) = both_keys(suffix); let mut value = None; for (key, candidate) in map { @@ -220,11 +220,11 @@ fn get_consistent_value<'a, V: AsRef<[u8]>>(map: &'a HashMap, suffix: /// Returns a non-empty value when every compatibility key present for `suffix` agrees. /// A single RustFS or MinIO key is accepted for backward compatibility; conflicting or empty /// values return `None` so callers at destructive boundaries can fail closed. -pub fn get_consistent_str<'a>(map: &'a HashMap, suffix: &str) -> Option<&'a str> { +pub fn get_consistent_str<'a, S: std::hash::BuildHasher>(map: &'a HashMap, suffix: &str) -> Option<&'a str> { get_consistent_value(map, suffix).map(String::as_str) } -pub fn contains_key_str(map: &HashMap, suffix: &str) -> bool { +pub fn contains_key_str(map: &HashMap, suffix: &str) -> bool { if with_internal_key(RUSTFS_INTERNAL_PREFIX, suffix, |k1| map.contains_key(k1)) { return true; } @@ -236,7 +236,7 @@ pub fn contains_key_str(map: &HashMap, suffix: &str) -> bool { .any(|key| key.eq_ignore_ascii_case(&k1) || key.eq_ignore_ascii_case(&k2)) } -pub fn remove_str(map: &mut HashMap, suffix: &str) { +pub fn remove_str(map: &mut HashMap, suffix: &str) { with_internal_key(RUSTFS_INTERNAL_PREFIX, suffix, |k1| map.remove(k1)); with_internal_key(MINIO_INTERNAL_PREFIX, suffix, |k2| map.remove(k2)); let (k1, k2) = both_keys(suffix); @@ -285,7 +285,7 @@ pub fn strip_internal_prefix_preserving_case(key: &str) -> Option<&str> { /// Reads the bounded per-target delete-marker version map in one metadata scan. /// The boolean is set when matching metadata is malformed or compatibility keys disagree. -pub fn target_delete_marker_versions(map: &HashMap) -> (HashMap, bool) { +pub fn target_delete_marker_versions(map: &HashMap) -> (HashMap, bool) { const MAX_ENTRIES: usize = 1_000; const MAX_ARN_LEN: usize = 1_024; const MAX_VERSION_ID_LEN: usize = 1_024;