feat: optimize small GET read paths (#4022)

This commit is contained in:
houseme
2026-07-01 15:40:00 +08:00
committed by GitHub
parent b0f491549a
commit b16120dbcc
25 changed files with 5318 additions and 502 deletions
Generated
+132 -46
View File
@@ -893,9 +893,9 @@ dependencies = [
[[package]]
name = "aws-lc-rs"
version = "1.17.0"
version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00"
checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad"
dependencies = [
"aws-lc-sys",
"untrusted 0.7.1",
@@ -904,14 +904,15 @@ dependencies = [
[[package]]
name = "aws-lc-sys"
version = "0.41.0"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4"
checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444"
dependencies = [
"cc",
"cmake",
"dunce",
"fs_extra",
"pkg-config",
]
[[package]]
@@ -1715,9 +1716,9 @@ dependencies = [
[[package]]
name = "camino"
version = "1.2.3"
version = "1.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ce8d3bd5823c7504d3f579f13e7b2f3da252fcb938c594d5680ee508bf846f"
checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0"
dependencies = [
"serde_core",
]
@@ -5001,9 +5002,9 @@ checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
[[package]]
name = "hybrid-array"
version = "0.4.12"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da"
checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c"
dependencies = [
"ctutils",
"subtle",
@@ -5332,9 +5333,9 @@ dependencies = [
[[package]]
name = "io-uring"
version = "0.7.12"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d09b98f7eace8982db770e4408e7470b028ce513ac28fecdc6bf4c30fe92b62"
checksum = "9080b15e63775b9a2ac7dca720f7050a8b955e092ea0f6020a4a80f69998cdc0"
dependencies = [
"bitflags 2.13.0",
"cfg-if",
@@ -5447,9 +5448,9 @@ dependencies = [
[[package]]
name = "jiff"
version = "0.2.29"
version = "0.2.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34f877a98676d2fb664698d74cc6a51ce6c484ce8c770f05d0108ec9090aeb46"
checksum = "ccfe6121cbe750cf81efa362d85c0bde7ea298ec43092d3a193baca59cdbd634"
dependencies = [
"defmt",
"jiff-static",
@@ -5463,9 +5464,9 @@ dependencies = [
[[package]]
name = "jiff-static"
version = "0.2.29"
version = "0.2.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0666b5ab5ecaca213fc2a85b8c0083d9004e84ee2d5f9a7e0017aaf50986f25f"
checksum = "e165e897f662d428f3cd3828a919dbe067c2d42bb1031eede74ef9d27ecdedd2"
dependencies = [
"proc-macro2",
"quote",
@@ -5789,18 +5790,18 @@ dependencies = [
[[package]]
name = "liblzma"
version = "0.4.6"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6033b77c21d1f56deeae8014eb9fbe7bdf1765185a6c508b5ca82eeaed7f899"
checksum = "45aec2360b3933207e27908049d8e4df4e476b58180afb1e56b2a4fb72efe4ba"
dependencies = [
"liblzma-sys",
]
[[package]]
name = "liblzma-sys"
version = "0.4.6"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a60851d15cd8c5346eca4ab8babff585be2ae4bc8097c067291d3ffe2add3b6"
checksum = "a046c7f353ba30f810545151e04f63545833803f5b86ee3ddf1517247fe560a5"
dependencies = [
"cc",
"libc",
@@ -7567,11 +7568,12 @@ dependencies = [
[[package]]
name = "pkcs5"
version = "0.8.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "279a91971a1d8eb1260a30938eae3be9cb67b472dffecb222fbbbe2fd2dc1453"
checksum = "63d440a804ec8d6fafbb6b84471e013286658d373248927692ab3366686220ca"
dependencies = [
"aes 0.9.1",
"aes-gcm",
"cbc 0.2.1",
"der 0.8.0",
"pbkdf2 0.13.0",
@@ -7598,7 +7600,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7"
dependencies = [
"der 0.8.0",
"pkcs5 0.8.0",
"pkcs5 0.8.1",
"rand_core 0.10.1",
"spki 0.8.0",
]
@@ -8227,6 +8229,16 @@ name = "quick-xml"
version = "0.40.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2474bd2e5029e7ccb6abb2ba48cf2383a333851dedf495901544281590c7da7f"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "quick-xml"
version = "0.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
dependencies = [
"encoding_rs",
"memchr",
@@ -8287,7 +8299,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -8418,9 +8430,9 @@ dependencies = [
[[package]]
name = "rapidhash"
version = "4.4.1"
version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59"
checksum = "32b266a82f4aa99bb5c25e28d11cc44ace63d91adbcbcee4d323e2ae3d49ef37"
dependencies = [
"rustversion",
]
@@ -8901,7 +8913,7 @@ dependencies = [
"pageant",
"pbkdf2 0.13.0",
"pkcs1 0.8.0-rc.4",
"pkcs5 0.8.0",
"pkcs5 0.8.1",
"pkcs8 0.11.0",
"polyval",
"rand 0.10.1",
@@ -9330,7 +9342,7 @@ dependencies = [
"path-absolutize",
"pin-project-lite",
"proptest",
"quick-xml 0.40.1",
"quick-xml 0.41.0",
"rand 0.10.1",
"ratelimit",
"rcgen",
@@ -9682,7 +9694,7 @@ dependencies = [
"hashbrown 0.17.1",
"metrics",
"percent-encoding",
"quick-xml 0.40.1",
"quick-xml 0.41.0",
"rayon",
"rustc-hash",
"rustfs-config",
@@ -9821,7 +9833,7 @@ dependencies = [
"md5",
"percent-encoding",
"proptest",
"quick-xml 0.40.1",
"quick-xml 0.41.0",
"regex",
"russh",
"russh-sftp",
@@ -10318,9 +10330,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.14.1"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
dependencies = [
"web-time",
"zeroize",
@@ -11353,9 +11365,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "suppaftp"
version = "9.0.0"
version = "10.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b367d8e84825dc6e858d2044f8cfff5e80f63b2aa0634c6382fc9a94b6926b22"
checksum = "81b8286a5639930b8fc0400f8a5111439ffb57e5fba6e8276a8bce857ec47b18"
dependencies = [
"async-trait",
"chrono",
@@ -11655,9 +11667,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.51"
version = "0.3.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327"
checksum = "0e48db7b415311b615f910b3dcaa4557bcd4bf1982379c95c223fd8c2a20e210"
dependencies = [
"deranged",
"js-sys",
@@ -11678,9 +11690,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
[[package]]
name = "time-macros"
version = "0.2.30"
version = "0.2.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935"
checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f"
dependencies = [
"num-conv",
"time-core",
@@ -12781,7 +12793,7 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
"windows-targets 0.52.6",
]
[[package]]
@@ -12790,7 +12802,16 @@ version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.5",
]
[[package]]
@@ -12808,14 +12829,31 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm 0.53.1",
"windows_aarch64_msvc 0.53.1",
"windows_i686_gnu 0.53.1",
"windows_i686_gnullvm 0.53.1",
"windows_i686_msvc 0.53.1",
"windows_x86_64_gnu 0.53.1",
"windows_x86_64_gnullvm 0.53.1",
"windows_x86_64_msvc 0.53.1",
]
[[package]]
@@ -12833,48 +12871,96 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
version = "1.0.3"
+7 -7
View File
@@ -168,7 +168,7 @@ byteorder = "1.5.0"
flatbuffers = "25.12.19"
form_urlencoded = "1.2.2"
prost = "0.14.4"
quick-xml = "0.40.1"
quick-xml = "0.41.0"
rmp = { version = "0.8.15" }
rmp-serde = { version = "1.3.1" }
serde = { version = "1.0.228", features = ["derive"] }
@@ -192,7 +192,7 @@ pbkdf2 = "0.13.0"
rsa = { version = "=0.10.0-rc.18" }
rustls = { version = "0.23.41", default-features = false, features = ["aws-lc-rs", "logging", "tls12", "prefer-post-quantum", "std"] }
rustls-native-certs = "0.8"
rustls-pki-types = "1.14.1"
rustls-pki-types = "1.15.0"
sha1 = "0.11.0"
sha2 = "0.11.0"
subtle = "2.6"
@@ -201,8 +201,8 @@ zeroize = { version = "1.9.0", features = ["derive"] }
# Time and Date
chrono = { version = "0.4.45", features = ["serde"] }
humantime = "2.3.0"
jiff = { version = "0.2.29", features = ["serde"] }
time = { version = "0.3.51", features = ["std", "parsing", "formatting", "macros", "serde"] }
jiff = { version = "0.2.31", features = ["serde"] }
time = { version = "0.3.52", features = ["std", "parsing", "formatting", "macros", "serde"] }
# Database
deadpool-postgres = { version = "0.14", features = ["rt_tokio_1"] }
@@ -211,7 +211,7 @@ tokio-postgres-rustls = "0.14.0"
# Utilities and Tools
anyhow = "1.0.103"
arc-swap = "1.9.1"
arc-swap = "1.9.2"
astral-tokio-tar = "0.6.3"
atoi = "3.1.0"
atomic_enum = "0.3.0"
@@ -323,7 +323,7 @@ pyroscope = { version = "=2.0.5", features = ["backend-pprof-rs", "backend-jemal
# FTP and SFTP
libunftp = { version = "0.23.0", features = ["experimental"] }
unftp-core = "0.1.0"
suppaftp = { version = "9.0.0", features = ["tokio", "tokio-rustls-aws-lc-rs"] }
suppaftp = { version = "10.0.0", features = ["tokio", "tokio-rustls-aws-lc-rs"] }
rcgen = "0.14.8"
russh = { version = "0.61.2", features = ["serde"] }
russh-sftp = "2.3.0"
@@ -338,7 +338,7 @@ tikv-jemallocator = { version = "0.6", features = ["profiling", "stats", "unpref
# Used to control and obtain statistics for jemalloc at runtime
tikv-jemalloc-ctl = { version = "0.6", features = ["use_std", "stats", "profiling"] }
# Snapshot testing for output format regression detection
insta = { version = "1.41", features = ["yaml", "json"] }
insta = { version = "1.48", features = ["yaml", "json"] }
# Used to generate pprof-compatible memory profiling data and support symbolization and flame graphs
jemalloc_pprof = { version = "0.8.2", features = ["symbolize", "flamegraph"] }
# Used to generate CPU performance analysis data and flame diagrams
@@ -149,6 +149,7 @@ pub fn new_getobjectreader<'a>(
let r = GetObjectReader {
object_info: oi.clone(),
stream: Box::new(input_reader),
buffered_body: None,
};
r
//})
+1
View File
@@ -1536,6 +1536,7 @@ mod tests {
_guard: guard,
}),
object_info: self.object_info(bucket, object),
buffered_body: None,
})
}
+1
View File
@@ -1149,6 +1149,7 @@ mod tests {
let rd = GetObjectReader {
stream: Box::new(Cursor::new(raw_payload.clone())),
object_info: object_info.clone(),
buffered_body: None,
};
let mut data = data_movement_put_object_reader("bucket-a", &object_info, rd, "test_migration")
+75
View File
@@ -21,9 +21,13 @@ pub(crate) const GET_OBJECT_PATH_CODEC_STREAMING: &str = "codec_streaming";
pub(crate) const GET_OBJECT_PATH_CODEC_STREAMING_LEGACY_ENGINE: &str = "codec_streaming_legacy_engine";
pub(crate) const GET_OBJECT_PATH_CODEC_STREAMING_RUSTFS_ENGINE: &str = "codec_streaming_rustfs_engine";
pub(crate) const GET_OBJECT_PATH_EMPTY: &str = "empty";
pub(crate) const GET_OBJECT_PATH_DIRECT_MEMORY: &str = "direct_memory";
pub(crate) const GET_OBJECT_PATH_INLINE_DIRECT: &str = "inline_direct";
pub(crate) const GET_OBJECT_PATH_LEGACY_DUPLEX: &str = "legacy_duplex";
pub(crate) const GET_OBJECT_PATH_REMOTE_TRANSITION: &str = "remote_transition";
pub(crate) const GET_OBJECT_PATH_SET_DISK: &str = "set_disk";
pub(crate) const GET_DIRECT_MEMORY_SUBPATH_DISK_DATA_BLOCKS: &str = "disk_data_blocks";
pub(crate) const GET_DIRECT_MEMORY_SUBPATH_INLINE_BUFFERED: &str = "inline_buffered";
pub(crate) const GET_CODEC_STREAMING_DECISION_USE: &str = "use";
pub(crate) const GET_CODEC_STREAMING_DECISION_FALLBACK: &str = "fallback";
pub(crate) const GET_CODEC_STREAMING_REASON_NONE: &str = "none";
@@ -42,13 +46,39 @@ pub(crate) const GET_STAGE_FIRST_METADATA_RESPONSE: &str = "first_metadata_respo
pub(crate) const GET_STAGE_FIRST_VALID_METADATA_RESPONSE: &str = "first_valid_metadata_response";
pub(crate) const GET_STAGE_FIRST_SHARD_READ: &str = "first_shard_read";
pub(crate) const GET_STAGE_FULL_BODY: &str = "full_body";
pub(crate) const GET_STAGE_INLINE_PREPARE: &str = "inline_prepare";
pub(crate) const GET_STAGE_LOCK_ACQUIRE: &str = "lock_acquire";
pub(crate) const GET_STAGE_METADATA: &str = "metadata";
pub(crate) const GET_STAGE_METADATA_CACHE_LOOKUP: &str = "metadata_cache_lookup";
pub(crate) const GET_STAGE_METADATA_FANOUT: &str = "metadata_fanout";
pub(crate) const GET_STAGE_METADATA_RESOLVE: &str = "metadata_resolve";
pub(crate) const GET_STAGE_OBJECT_INFO: &str = "object_info";
pub(crate) const GET_STAGE_OUTPUT_LOCK_WAIT: &str = "output_lock_wait";
pub(crate) const GET_STAGE_OUTPUT_POLL: &str = "output_poll";
pub(crate) const GET_STAGE_PATH_DECISION: &str = "path_decision";
pub(crate) const GET_STAGE_QUORUM_REACHED: &str = "quorum_reached";
pub(crate) const GET_STAGE_RANGE: &str = "range";
pub(crate) const GET_STAGE_READER_SETUP: &str = "reader_setup";
pub(crate) const GET_STAGE_READER_SETUP_DROP_PENDING: &str = "reader_setup_drop_pending";
pub(crate) const GET_STAGE_READER_SETUP_SCHEDULE: &str = "reader_setup_schedule";
pub(crate) const GET_STAGE_READER_SETUP_WAIT_QUORUM: &str = "reader_setup_wait_quorum";
pub(crate) const GET_STAGE_READER_OPEN_MMAP_COPY_FALLBACK: &str = "reader_open_mmap_copy_fallback";
pub(crate) const GET_STAGE_READER_OPEN_MMAP_COPY_SUCCESS: &str = "reader_open_mmap_copy_success";
pub(crate) const GET_STAGE_READER_OPEN_STREAM: &str = "reader_open_stream";
pub(crate) const GET_STAGE_READER_MMAP_ACCESS_CHECK: &str = "reader_mmap_access_check";
pub(crate) const GET_STAGE_READER_MMAP_BLOCKING_TASK: &str = "reader_mmap_blocking_task";
pub(crate) const GET_STAGE_READER_MMAP_BLOCKING_WAIT: &str = "reader_mmap_blocking_wait";
pub(crate) const GET_STAGE_READER_MMAP_COPY_BUFFER: &str = "reader_mmap_copy_buffer";
pub(crate) const GET_STAGE_READER_MMAP_DIRECT_READ_COPY: &str = "reader_mmap_direct_read_copy";
pub(crate) const GET_STAGE_READER_MMAP_FILE_OPEN: &str = "reader_mmap_file_open";
pub(crate) const GET_STAGE_READER_MMAP_MAP: &str = "reader_mmap_map";
pub(crate) const GET_STAGE_READER_MMAP_METADATA_LOOKUP: &str = "reader_mmap_metadata_lookup";
pub(crate) const GET_STAGE_READER_MMAP_METADATA_VALIDATE: &str = "reader_mmap_metadata_validate";
pub(crate) const GET_STAGE_READER_MMAP_PATH_RESOLVE: &str = "reader_mmap_path_resolve";
pub(crate) const GET_STAGE_READER_STREAM_FIRST_READ: &str = "reader_stream_first_read";
pub(crate) const GET_STAGE_READER_TASK_BITROT_READER_INIT: &str = "reader_task_bitrot_reader_init";
pub(crate) const GET_STAGE_READER_TASK_FILE_OPEN: &str = "reader_task_file_open";
pub(crate) const GET_STAGE_READER_TASK_READER_CONSTRUCTION: &str = "reader_task_reader_construction";
pub(crate) const GET_STAGE_RECONSTRUCT: &str = "reconstruct";
pub(crate) const GET_STAGE_RESPONSE_HANDOFF: &str = "response_handoff";
pub(crate) const GET_STAGE_SLOWEST_METADATA_RESPONSE: &str = "slowest_metadata_response";
@@ -88,6 +118,25 @@ pub(crate) const GET_METADATA_RESPONSE_NOT_FOUND: &str = "not_found";
pub(crate) const GET_METADATA_RESPONSE_TIMEOUT: &str = "timeout";
pub(crate) const GET_METADATA_RESPONSE_VALID: &str = "valid";
pub(crate) const GET_METADATA_RESPONSE_VERSION_NOT_FOUND: &str = "version_not_found";
pub(crate) const GET_METADATA_CACHE_DECISION_HIT: &str = "hit";
pub(crate) const GET_METADATA_CACHE_DECISION_MISS: &str = "miss";
pub(crate) const GET_METADATA_CACHE_DECISION_REJECT: &str = "reject";
pub(crate) const GET_METADATA_CACHE_DECISION_SKIP: &str = "skip";
pub(crate) const GET_METADATA_CACHE_REASON_DATA_MOVEMENT: &str = "data_movement";
pub(crate) const GET_METADATA_CACHE_REASON_DELETE_MARKER: &str = "delete_marker";
pub(crate) const GET_METADATA_CACHE_REASON_DIST_ERASURE: &str = "dist_erasure";
pub(crate) const GET_METADATA_CACHE_REASON_INCL_FREE_VERSIONS: &str = "incl_free_versions";
pub(crate) const GET_METADATA_CACHE_REASON_INSUFFICIENT_CACHED_QUORUM: &str = "insufficient_cached_quorum";
pub(crate) const GET_METADATA_CACHE_REASON_META_BUCKET: &str = "meta_bucket";
pub(crate) const GET_METADATA_CACHE_REASON_NO_LOCK: &str = "no_lock";
pub(crate) const GET_METADATA_CACHE_REASON_NOT_FOUND_OR_EXPIRED: &str = "not_found_or_expired";
pub(crate) const GET_METADATA_CACHE_REASON_NOT_READ_DATA: &str = "not_read_data";
pub(crate) const GET_METADATA_CACHE_REASON_PART_NUMBER: &str = "part_number";
pub(crate) const GET_METADATA_CACHE_REASON_RAW_DATA_MOVEMENT_READ: &str = "raw_data_movement_read";
pub(crate) const GET_METADATA_CACHE_REASON_USABLE: &str = "usable";
pub(crate) const GET_METADATA_CACHE_REASON_VERSION_ID: &str = "version_id";
pub(crate) const GET_METADATA_CACHE_REASON_VERSION_SUSPENDED: &str = "version_suspended";
pub(crate) const GET_METADATA_CACHE_REASON_VERSIONED: &str = "versioned";
pub(crate) const GET_METADATA_EARLY_STOP_REASON_CONFLICTING_METADATA: &str = "conflicting_metadata";
pub(crate) const GET_METADATA_EARLY_STOP_REASON_DELETE_MARKER: &str = "delete_marker";
pub(crate) const GET_METADATA_EARLY_STOP_REASON_ERROR: &str = "error";
@@ -258,6 +307,7 @@ mod tests {
assert_eq!(GET_CODEC_STREAMING_OBJECT_CLASS_COMPRESSED, "compressed");
assert_eq!(GET_CODEC_STREAMING_OBJECT_CLASS_REMOTE, "remote");
assert_eq!(GET_CODEC_STREAMING_OBJECT_CLASS_MULTIPART, "multipart");
assert_eq!(GET_OBJECT_PATH_SET_DISK, "set_disk");
assert_eq!(GET_READER_PREFETCH_DIRECT, "direct");
assert_eq!(GET_READER_PREFETCH_STORED, "stored");
assert_eq!(GET_READER_PREFETCH_EOF, "eof");
@@ -275,10 +325,16 @@ mod tests {
assert_eq!(GET_STAGE_FIRST_VALID_METADATA_RESPONSE, "first_valid_metadata_response");
assert_eq!(GET_STAGE_FIRST_SHARD_READ, "first_shard_read");
assert_eq!(GET_STAGE_FULL_BODY, "full_body");
assert_eq!(GET_STAGE_INLINE_PREPARE, "inline_prepare");
assert_eq!(GET_STAGE_LOCK_ACQUIRE, "lock_acquire");
assert_eq!(GET_STAGE_METADATA, "metadata");
assert_eq!(GET_STAGE_METADATA_CACHE_LOOKUP, "metadata_cache_lookup");
assert_eq!(GET_STAGE_METADATA_FANOUT, "metadata_fanout");
assert_eq!(GET_STAGE_METADATA_RESOLVE, "metadata_resolve");
assert_eq!(GET_STAGE_OBJECT_INFO, "object_info");
assert_eq!(GET_STAGE_OUTPUT_LOCK_WAIT, "output_lock_wait");
assert_eq!(GET_STAGE_OUTPUT_POLL, "output_poll");
assert_eq!(GET_STAGE_PATH_DECISION, "path_decision");
assert_eq!(GET_STAGE_QUORUM_REACHED, "quorum_reached");
assert_eq!(GET_STAGE_RANGE, "range");
assert_eq!(GET_STAGE_READER_SETUP, "reader_setup");
@@ -308,6 +364,25 @@ mod tests {
assert_eq!(GET_METADATA_RESPONSE_TIMEOUT, "timeout");
assert_eq!(GET_METADATA_RESPONSE_VALID, "valid");
assert_eq!(GET_METADATA_RESPONSE_VERSION_NOT_FOUND, "version_not_found");
assert_eq!(GET_METADATA_CACHE_DECISION_HIT, "hit");
assert_eq!(GET_METADATA_CACHE_DECISION_MISS, "miss");
assert_eq!(GET_METADATA_CACHE_DECISION_REJECT, "reject");
assert_eq!(GET_METADATA_CACHE_DECISION_SKIP, "skip");
assert_eq!(GET_METADATA_CACHE_REASON_DATA_MOVEMENT, "data_movement");
assert_eq!(GET_METADATA_CACHE_REASON_DELETE_MARKER, "delete_marker");
assert_eq!(GET_METADATA_CACHE_REASON_DIST_ERASURE, "dist_erasure");
assert_eq!(GET_METADATA_CACHE_REASON_INCL_FREE_VERSIONS, "incl_free_versions");
assert_eq!(GET_METADATA_CACHE_REASON_INSUFFICIENT_CACHED_QUORUM, "insufficient_cached_quorum");
assert_eq!(GET_METADATA_CACHE_REASON_META_BUCKET, "meta_bucket");
assert_eq!(GET_METADATA_CACHE_REASON_NO_LOCK, "no_lock");
assert_eq!(GET_METADATA_CACHE_REASON_NOT_FOUND_OR_EXPIRED, "not_found_or_expired");
assert_eq!(GET_METADATA_CACHE_REASON_NOT_READ_DATA, "not_read_data");
assert_eq!(GET_METADATA_CACHE_REASON_PART_NUMBER, "part_number");
assert_eq!(GET_METADATA_CACHE_REASON_RAW_DATA_MOVEMENT_READ, "raw_data_movement_read");
assert_eq!(GET_METADATA_CACHE_REASON_USABLE, "usable");
assert_eq!(GET_METADATA_CACHE_REASON_VERSION_ID, "version_id");
assert_eq!(GET_METADATA_CACHE_REASON_VERSION_SUSPENDED, "version_suspended");
assert_eq!(GET_METADATA_CACHE_REASON_VERSIONED, "versioned");
assert_eq!(GET_METADATA_EARLY_STOP_REASON_CONFLICTING_METADATA, "conflicting_metadata");
assert_eq!(GET_METADATA_EARLY_STOP_REASON_DELETE_MARKER, "delete_marker");
assert_eq!(GET_METADATA_EARLY_STOP_REASON_ERROR, "error");
+21 -2
View File
@@ -14,8 +14,8 @@
use crate::disk::{
CheckPartsResp, DeleteOptions, DiskAPI, DiskError, DiskInfo, DiskInfoOptions, DiskLocation, Endpoint, Error,
FileInfoVersions, ReadMultipleReq, ReadMultipleResp, ReadOptions, RenameDataResp, Result, UpdateMetadataOpts, VolumeInfo,
WalkDirOptions,
FileInfoVersions, MmapCopyStageMetrics, ReadMultipleReq, ReadMultipleResp, ReadOptions, RenameDataResp, Result,
UpdateMetadataOpts, VolumeInfo, WalkDirOptions,
health_state::{
RuntimeDriveHealthState, classify_drive_recovery, get_drive_returning_probe_interval,
get_drive_returning_success_threshold, get_drive_suspect_failure_threshold, record_drive_offline_duration,
@@ -1369,6 +1369,25 @@ impl DiskAPI for LocalDiskWrapper {
.await
}
async fn read_file_mmap_copy_with_metrics(
&self,
volume: &str,
path: &str,
offset: usize,
length: usize,
metrics: Option<MmapCopyStageMetrics>,
) -> Result<bytes::Bytes> {
self.track_disk_health(
|| async {
self.disk
.read_file_mmap_copy_with_metrics(volume, path, offset, length, metrics)
.await
},
get_max_timeout_duration(),
)
.await
}
async fn append_file(&self, volume: &str, path: &str) -> Result<crate::disk::FileWriter> {
self.track_disk_health(|| async { self.disk.append_file(volume, path).await }, Duration::ZERO)
.await
+639 -118
View File
@@ -18,14 +18,17 @@ use crate::disk::disk_store::get_object_disk_read_timeout;
use crate::disk::{
BUCKET_META_PREFIX, CHECK_PART_FILE_CORRUPT, CHECK_PART_FILE_NOT_FOUND, CHECK_PART_SUCCESS, CHECK_PART_UNKNOWN,
CHECK_PART_VOLUME_NOT_FOUND, CheckPartsResp, DeleteOptions, DiskAPI, DiskInfo, DiskInfoOptions, DiskLocation, DiskMetrics,
FileInfoVersions, FileReader, FileWriter, RUSTFS_META_BUCKET, RUSTFS_META_TMP_BUCKET, RUSTFS_META_TMP_DELETED_BUCKET,
ReadMultipleReq, ReadMultipleResp, ReadOptions, RenameDataResp, STORAGE_FORMAT_FILE, STORAGE_FORMAT_FILE_BACKUP,
UpdateMetadataOpts, VolumeInfo, WalkDirOptions, conv_part_err_to_int,
FileInfoVersions, FileReader, FileWriter, MmapCopyStageMetrics, RUSTFS_META_BUCKET, RUSTFS_META_TMP_BUCKET,
RUSTFS_META_TMP_DELETED_BUCKET, ReadMultipleReq, ReadMultipleResp, ReadOptions, RenameDataResp, STORAGE_FORMAT_FILE,
STORAGE_FORMAT_FILE_BACKUP, UpdateMetadataOpts, VolumeInfo, WalkDirOptions, conv_part_err_to_int,
endpoint::Endpoint,
error::{DiskError, Error, FileAccessDeniedWithContext, Result},
error_conv::{to_access_error, to_file_error, to_unformatted_disk_error, to_volume_error},
format::FormatV3,
fs::{O_APPEND, O_CREATE, O_RDONLY, O_TRUNC, O_WRONLY, access, lstat, lstat_std, remove, remove_all_std, remove_std, rename},
fs::{
O_APPEND, O_CREATE, O_RDONLY, O_TRUNC, O_WRONLY, access, access_std, lstat, lstat_std, remove, remove_all_std,
remove_std, rename,
},
os,
os::{check_path_length, is_empty_dir, is_root_disk, rename_all, rename_all_ignore_missing_source},
};
@@ -83,6 +86,130 @@ const EVENT_DISK_LOCAL_CHECK_PARTS: &str = "disk_local_check_parts";
const EVENT_DISK_LOCAL_ACCESS_FAILED: &str = "disk_local_access_failed";
const EVENT_DISK_LOCAL_VOLUME_SETUP_FAILED: &str = "disk_local_volume_setup_failed";
const EVENT_DISK_LOCAL_FORMAT_DECODE_FAILED: &str = "disk_local_format_decode_failed";
const METRIC_GET_OBJECT_MMAP_PAGE_FAULTS_TOTAL: &str = "rustfs_io_get_object_mmap_page_faults_total";
const METRIC_GET_OBJECT_DIRECT_READ_PAGE_FAULTS_TOTAL: &str = "rustfs_io_get_object_direct_read_page_faults_total";
#[inline(always)]
fn record_mmap_copy_stage(metrics: MmapCopyStageMetrics, stage: &'static str, started_at: Option<std::time::Instant>) {
if let Some(started_at) = started_at {
rustfs_io_metrics::record_get_object_stage_duration(metrics.path, stage, started_at.elapsed().as_secs_f64());
}
}
#[cfg(unix)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
struct MmapPageFaultCounts {
minor: libc::c_long,
major: libc::c_long,
}
#[cfg(unix)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
struct MmapPageFaultDelta {
minor: u64,
major: u64,
}
#[cfg(all(unix, any(target_os = "linux", target_os = "android")))]
fn mmap_rusage_who() -> libc::c_int {
libc::RUSAGE_THREAD
}
#[cfg(all(unix, not(any(target_os = "linux", target_os = "android"))))]
fn mmap_rusage_who() -> libc::c_int {
libc::RUSAGE_SELF
}
#[cfg(unix)]
// SAFETY: this allowance is limited to reading kernel-provided rusage data via
// libc; each unsafe operation below documents pointer validity and initialization.
#[allow(unsafe_code)]
fn read_mmap_page_fault_counts(enabled: bool) -> Option<MmapPageFaultCounts> {
if !enabled {
return None;
}
let mut usage = std::mem::MaybeUninit::<libc::rusage>::uninit();
// SAFETY: `getrusage` writes to the provided `rusage` pointer when it
// returns 0. The pointer is valid for writes and initialized only on success.
let rc = unsafe { libc::getrusage(mmap_rusage_who(), usage.as_mut_ptr()) };
if rc != 0 {
return None;
}
// SAFETY: `getrusage` returned success, so `usage` has been initialized.
let usage = unsafe { usage.assume_init() };
Some(MmapPageFaultCounts {
minor: usage.ru_minflt,
major: usage.ru_majflt,
})
}
#[cfg(unix)]
fn non_negative_fault_delta(before: libc::c_long, after: libc::c_long) -> u64 {
if after <= before {
return 0;
}
u64::try_from(after - before).unwrap_or(u64::MAX)
}
#[cfg(unix)]
fn mmap_page_fault_delta(before: Option<MmapPageFaultCounts>, after: Option<MmapPageFaultCounts>) -> MmapPageFaultDelta {
match (before, after) {
(Some(before), Some(after)) => MmapPageFaultDelta {
minor: non_negative_fault_delta(before.minor, after.minor),
major: non_negative_fault_delta(before.major, after.major),
},
_ => MmapPageFaultDelta::default(),
}
}
#[cfg(unix)]
fn record_mmap_page_fault_delta(path: &'static str, stage: &'static str, delta: MmapPageFaultDelta) {
if delta.minor > 0 {
counter!(
METRIC_GET_OBJECT_MMAP_PAGE_FAULTS_TOTAL,
"path" => path,
"stage" => stage,
"kind" => "minor",
)
.increment(delta.minor);
}
if delta.major > 0 {
counter!(
METRIC_GET_OBJECT_MMAP_PAGE_FAULTS_TOTAL,
"path" => path,
"stage" => stage,
"kind" => "major",
)
.increment(delta.major);
}
}
#[cfg(unix)]
fn record_direct_read_page_fault_delta(path: &'static str, stage: &'static str, delta: MmapPageFaultDelta) {
if delta.minor > 0 {
counter!(
METRIC_GET_OBJECT_DIRECT_READ_PAGE_FAULTS_TOTAL,
"path" => path,
"stage" => stage,
"kind" => "minor",
)
.increment(delta.minor);
}
if delta.major > 0 {
counter!(
METRIC_GET_OBJECT_DIRECT_READ_PAGE_FAULTS_TOTAL,
"path" => path,
"stage" => stage,
"kind" => "major",
)
.increment(delta.major);
}
}
/// Enable O_DIRECT for large sequential reads.
/// When enabled, shard reads bypass the page cache using O_DIRECT flag.
@@ -96,6 +223,17 @@ const DEFAULT_RUSTFS_OBJECT_DIRECT_IO_READ_ENABLE: bool = false;
/// Default: 4MB.
const ENV_RUSTFS_OBJECT_DIRECT_IO_READ_THRESHOLD: &str = "RUSTFS_OBJECT_DIRECT_IO_READ_THRESHOLD";
const DEFAULT_RUSTFS_OBJECT_DIRECT_IO_READ_THRESHOLD: usize = 4 * 1024 * 1024;
const ENV_RUSTFS_OBJECT_MMAP_POPULATE_ENABLE: &str = "RUSTFS_OBJECT_MMAP_POPULATE_ENABLE";
const DEFAULT_RUSTFS_OBJECT_MMAP_POPULATE_ENABLE: bool = false;
const ENV_RUSTFS_OBJECT_MMAP_READ_METHOD: &str = "RUSTFS_OBJECT_MMAP_READ_METHOD";
const RUSTFS_OBJECT_MMAP_READ_METHOD_MMAP_COPY: &str = "mmap_copy";
const RUSTFS_OBJECT_MMAP_READ_METHOD_DIRECT_READ_COPY: &str = "direct_read_copy";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum LocalReadCopyMethod {
MmapCopy,
DirectReadCopy,
}
/// Check if O_DIRECT reads are enabled.
fn is_direct_io_read_enabled() -> bool {
@@ -107,6 +245,36 @@ fn get_direct_io_read_threshold() -> usize {
rustfs_utils::get_env_usize(ENV_RUSTFS_OBJECT_DIRECT_IO_READ_THRESHOLD, DEFAULT_RUSTFS_OBJECT_DIRECT_IO_READ_THRESHOLD)
}
fn should_populate_mmap_read(length: usize) -> bool {
length > 0 && rustfs_utils::get_env_bool(ENV_RUSTFS_OBJECT_MMAP_POPULATE_ENABLE, DEFAULT_RUSTFS_OBJECT_MMAP_POPULATE_ENABLE)
}
fn local_read_copy_method() -> LocalReadCopyMethod {
let method = rustfs_utils::get_env_str(ENV_RUSTFS_OBJECT_MMAP_READ_METHOD, RUSTFS_OBJECT_MMAP_READ_METHOD_MMAP_COPY);
match method.as_str() {
RUSTFS_OBJECT_MMAP_READ_METHOD_DIRECT_READ_COPY => LocalReadCopyMethod::DirectReadCopy,
_ => LocalReadCopyMethod::MmapCopy,
}
}
#[cfg(unix)]
#[allow(unsafe_code)]
fn mmap_page_size() -> Result<u64> {
static PAGE_SIZE: OnceLock<Option<u64>> = OnceLock::new();
PAGE_SIZE
.get_or_init(|| {
// SAFETY: `sysconf(_SC_PAGESIZE)` has no pointer arguments and only
// queries process-global OS configuration.
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
if page_size <= 0 {
return None;
}
u64::try_from(page_size).ok()
})
.ok_or_else(|| DiskError::other("failed to determine system page size"))
}
#[cfg(test)]
static RENAME_DATA_FAIL_BEFORE_OLD_METADATA_BACKUP: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
@@ -1070,62 +1238,21 @@ impl LocalDisk {
// Get the absolute path of an object
pub fn get_object_path(&self, bucket: &str, key: &str) -> Result<PathBuf> {
// For high-frequency paths, use faster string concatenation
let cache_key = if key.is_empty() {
bucket.to_string()
} else {
path_join_buf(&[bucket, key])
};
#[cfg(windows)]
let path = self.root.join(cache_key.replace('/', "\\"));
#[cfg(not(windows))]
let path = self.root.join(cache_key);
self.check_valid_path(&path)?;
Ok(path)
local_disk_object_path(&self.root, bucket, key)
}
// Get the absolute path of a bucket
pub fn get_bucket_path(&self, bucket: &str) -> Result<PathBuf> {
#[cfg(windows)]
let bucket_path = self.root.join(bucket.replace('/', "\\"));
#[cfg(not(windows))]
let bucket_path = self.root.join(bucket);
self.check_valid_path(&bucket_path)?;
Ok(bucket_path)
local_disk_bucket_path(&self.root, bucket)
}
// Check if a path is valid
fn check_valid_path<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let path = normalize_path_components(path);
if !path.starts_with(&self.root) {
return Err(DiskError::InvalidPath);
}
self.reject_symlink_components(&path)
check_local_disk_valid_path(&self.root, path)
}
fn reject_symlink_components(&self, path: &Path) -> Result<()> {
let relative = path.strip_prefix(&self.root).map_err(|_| DiskError::InvalidPath)?;
let mut current = self.root.clone();
for component in relative.components() {
current.push(component.as_os_str());
match lstat_std(&current) {
Ok(metadata) => {
if metadata.file_type().is_symlink() {
return Err(DiskError::InvalidPath);
}
}
Err(err) if err.kind() == ErrorKind::NotFound => break,
Err(err) => return Err(to_file_error(err).into()),
}
}
Ok(())
reject_local_disk_symlink_components(&self.root, path)
}
// Batch path generation with single lock acquisition
@@ -2085,6 +2212,62 @@ fn skip_access_checks(p: impl AsRef<str>) -> bool {
false
}
fn local_disk_object_path(root: &Path, bucket: &str, key: &str) -> Result<PathBuf> {
let cache_key = if key.is_empty() {
bucket.to_string()
} else {
path_join_buf(&[bucket, key])
};
#[cfg(windows)]
let path = root.join(cache_key.replace('/', "\\"));
#[cfg(not(windows))]
let path = root.join(cache_key);
check_local_disk_valid_path(root, &path)?;
Ok(path)
}
fn local_disk_bucket_path(root: &Path, bucket: &str) -> Result<PathBuf> {
#[cfg(windows)]
let bucket_path = root.join(bucket.replace('/', "\\"));
#[cfg(not(windows))]
let bucket_path = root.join(bucket);
check_local_disk_valid_path(root, &bucket_path)?;
Ok(bucket_path)
}
fn check_local_disk_valid_path(root: &Path, path: impl AsRef<Path>) -> Result<()> {
let path = normalize_path_components(path);
if !path.starts_with(root) {
return Err(DiskError::InvalidPath);
}
reject_local_disk_symlink_components(root, &path)
}
fn reject_local_disk_symlink_components(root: &Path, path: &Path) -> Result<()> {
let relative = path.strip_prefix(root).map_err(|_| DiskError::InvalidPath)?;
let mut current = root.to_path_buf();
for component in relative.components() {
current.push(component.as_os_str());
match lstat_std(&current) {
Ok(metadata) => {
if metadata.file_type().is_symlink() {
return Err(DiskError::InvalidPath);
}
}
Err(err) if err.kind() == ErrorKind::NotFound => break,
Err(err) => return Err(to_file_error(err).into()),
}
}
Ok(())
}
// Lightweight path normalization without filesystem calls
fn normalize_path_components(path: impl AsRef<Path>) -> PathBuf {
let path = path.as_ref();
@@ -2784,105 +2967,303 @@ impl DiskAPI for LocalDisk {
#[allow(unsafe_code)]
#[tracing::instrument(level = "debug", skip(self))]
async fn read_file_mmap_copy(&self, volume: &str, path: &str, offset: usize, length: usize) -> Result<Bytes> {
let volume_dir = self.get_bucket_path(volume)?;
if !skip_access_checks(volume) {
access(&volume_dir)
.await
.map_err(|e| to_access_error(e, DiskError::VolumeAccessDenied))?;
}
let file_path = self.get_object_path(volume, path)?;
check_path_length(file_path.to_string_lossy().as_ref())?;
// Verify file exists and get metadata
let file_path_clone = file_path.clone();
let meta = tokio::task::spawn_blocking(move || std::fs::metadata(&file_path_clone).map_err(DiskError::from))
self.read_file_mmap_copy_with_metrics(volume, path, offset, length, None)
.await
.map_err(DiskError::from)??;
}
let end_offset = offset.checked_add(length).ok_or(DiskError::FileCorrupt)?;
if meta.len() < end_offset as u64 {
error!(
event = EVENT_DISK_LOCAL_READ_VERSION_FALLBACK,
component = LOG_COMPONENT_ECSTORE,
subsystem = LOG_SUBSYSTEM_DISK_LOCAL,
volume,
path,
offset,
length,
actual_size = meta.len(),
reason = "read_file_mmap_copy_out_of_bounds",
"Disk local read fallback failed"
);
/// File read using mmap-then-copy on Unix or efficient read on non-Unix.
// SAFETY: Unix unsafe calls in this function only query page size and mmap
// a read-only file region after bounds and alignment are validated.
#[allow(unsafe_code)]
#[tracing::instrument(level = "debug", skip(self))]
async fn read_file_mmap_copy_with_metrics(
&self,
volume: &str,
path: &str,
offset: usize,
length: usize,
metrics: Option<MmapCopyStageMetrics>,
) -> Result<Bytes> {
let metrics = metrics.filter(|_| rustfs_io_metrics::get_stage_metrics_enabled());
let metrics_enabled = metrics.is_some();
let metadata_validate_start = metrics_enabled.then(std::time::Instant::now);
let Some(end_offset) = offset.checked_add(length) else {
if let Some(metrics) = metrics {
record_mmap_copy_stage(metrics, metrics.metadata_validate_stage, metadata_validate_start);
}
return Err(DiskError::FileCorrupt);
}
};
// Unix: use mmap to read the data (copies into Bytes for safe ownership)
// Non-Unix: fall back to efficient read
#[cfg(unix)]
{
use memmap2::MmapOptions;
use std::time::Instant;
use std::time::{Duration as StdDuration, Instant as StdInstant};
let start = Instant::now();
let file_path_clone = file_path.clone();
struct MmapCopyReadResult {
bytes: Bytes,
access_check_duration: StdDuration,
path_resolve_duration: StdDuration,
metadata_lookup_duration: StdDuration,
metadata_validate_duration: StdDuration,
file_open_duration: StdDuration,
mmap_map_duration: StdDuration,
mmap_copy_duration: StdDuration,
direct_read_copy_duration: StdDuration,
mmap_map_fault_delta: MmapPageFaultDelta,
mmap_copy_fault_delta: MmapPageFaultDelta,
direct_read_copy_fault_delta: MmapPageFaultDelta,
blocking_task_duration: StdDuration,
}
enum MmapCopyReadError {
Disk(DiskError),
OutOfBounds { actual_size: u64 },
}
impl From<DiskError> for MmapCopyReadError {
fn from(err: DiskError) -> Self {
Self::Disk(err)
}
}
let start = StdInstant::now();
let root = self.root.clone();
let volume_owned = volume.to_owned();
let path_owned = path.to_owned();
let should_reclaim_after_read = should_reclaim_file_cache_after_read(length);
let bytes = tokio::task::spawn_blocking(move || {
let file = std::fs::File::open(&file_path_clone).map_err(DiskError::from)?;
let should_populate_mmap_read = should_populate_mmap_read(length);
let read_copy_method = local_read_copy_method();
let offset_u64 = u64::try_from(offset).map_err(|_| DiskError::FileCorrupt)?;
let end_offset_u64 = u64::try_from(end_offset).map_err(|_| DiskError::FileCorrupt)?;
let blocking_wait_start = metrics_enabled.then(std::time::Instant::now);
let read_result = tokio::task::spawn_blocking(move || {
let blocking_task_start = metrics_enabled.then(StdInstant::now);
let access_check_start = metrics_enabled.then(StdInstant::now);
let volume_dir = local_disk_bucket_path(&root, &volume_owned)?;
if !skip_access_checks(&volume_owned) {
access_std(&volume_dir).map_err(|e| DiskError::from(to_access_error(e, DiskError::VolumeAccessDenied)))?;
}
let access_check_duration = access_check_start.map_or(StdDuration::ZERO, |started_at| started_at.elapsed());
let path_resolve_start = metrics_enabled.then(StdInstant::now);
let file_path = local_disk_object_path(&root, &volume_owned, &path_owned)?;
check_path_length(file_path.to_string_lossy().as_ref())?;
let path_resolve_duration = path_resolve_start.map_or(StdDuration::ZERO, |started_at| started_at.elapsed());
let file_open_start = metrics_enabled.then(StdInstant::now);
let mut file = std::fs::File::open(&file_path).map_err(DiskError::from)?;
let file_open_duration = file_open_start.map_or(StdDuration::ZERO, |started_at| started_at.elapsed());
let metadata_lookup_start = metrics_enabled.then(StdInstant::now);
let meta = file.metadata().map_err(DiskError::from)?;
let metadata_lookup_duration = metadata_lookup_start.map_or(StdDuration::ZERO, |started_at| started_at.elapsed());
let metadata_validate_start = metrics_enabled.then(StdInstant::now);
if meta.len() < end_offset_u64 {
return Err(MmapCopyReadError::OutOfBounds { actual_size: meta.len() });
}
let metadata_validate_duration =
metadata_validate_start.map_or(StdDuration::ZERO, |started_at| started_at.elapsed());
#[cfg(target_os = "macos")]
if should_reclaim_after_read {
let _ = set_std_fd_nocache(&file);
}
// mmap offsets on Unix must be page-size aligned. Align the
// mapping down to the nearest page boundary, then slice out the
// originally requested logical range.
// SAFETY: `sysconf(_SC_PAGESIZE)` has no pointer arguments and
// only queries process-global OS configuration.
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
if page_size <= 0 {
return Err(DiskError::other("failed to determine system page size"));
}
let page_size = page_size as u64;
let offset_u64 = offset as u64;
let aligned_offset = offset_u64 - (offset_u64 % page_size);
let logical_offset = (offset_u64 - aligned_offset) as usize;
let map_len = logical_offset
.checked_add(length)
.ok_or_else(|| DiskError::other("mmap length overflow"))?;
let mut mmap_map_duration = StdDuration::ZERO;
let mut mmap_copy_duration = StdDuration::ZERO;
let mut direct_read_copy_duration = StdDuration::ZERO;
let mut mmap_map_fault_delta = MmapPageFaultDelta::default();
let mut mmap_copy_fault_delta = MmapPageFaultDelta::default();
let mut direct_read_copy_fault_delta = MmapPageFaultDelta::default();
let mut _reclaim_offset = offset_u64;
let mut _reclaim_len = length;
// SAFETY: The file is opened as read-only, and we're mapping a region
// that we've already verified exists and is within file bounds. The
// file offset passed to mmap is page-size aligned as required on Unix.
let mmap =
unsafe { MmapOptions::new().offset(aligned_offset).len(map_len).map(&file) }.map_err(DiskError::other)?;
let bytes = match read_copy_method {
LocalReadCopyMethod::MmapCopy => {
// mmap offsets on Unix must be page-size aligned. Align the
// mapping down to the nearest page boundary, then slice out the
// originally requested logical range.
let page_size = mmap_page_size()?;
let aligned_offset = offset_u64 - (offset_u64 % page_size);
let logical_offset =
usize::try_from(offset_u64 - aligned_offset).map_err(|_| DiskError::other("mmap offset overflow"))?;
let map_len = logical_offset
.checked_add(length)
.ok_or_else(|| DiskError::other("mmap length overflow"))?;
_reclaim_offset = aligned_offset;
_reclaim_len = map_len;
// Copy only the requested logical range into a Bytes buffer. This
// avoids undefined behavior from treating OS-managed mmap memory as
// allocator-managed Vec storage, at the cost of an extra copy.
let end = logical_offset
.checked_add(length)
.ok_or_else(|| DiskError::other("mmap slice length overflow"))?;
let bytes = Bytes::copy_from_slice(&mmap[logical_offset..end]);
// SAFETY: The file is opened as read-only, and we're mapping a region
// that we've already verified exists and is within file bounds. The
// file offset passed to mmap is page-size aligned as required on Unix.
let mmap_map_start = metrics_enabled.then(StdInstant::now);
let mmap_map_faults_before = read_mmap_page_fault_counts(metrics_enabled);
let mut mmap_options = MmapOptions::new();
mmap_options.offset(aligned_offset).len(map_len);
if should_populate_mmap_read {
mmap_options.populate();
}
let mmap = unsafe { mmap_options.map(&file) }.map_err(DiskError::other)?;
let mmap_map_faults_after = read_mmap_page_fault_counts(metrics_enabled);
mmap_map_duration = mmap_map_start.map_or(StdDuration::ZERO, |started_at| started_at.elapsed());
mmap_map_fault_delta = mmap_page_fault_delta(mmap_map_faults_before, mmap_map_faults_after);
// Copy only the requested logical range into a Bytes buffer. This
// avoids undefined behavior from treating OS-managed mmap memory as
// allocator-managed Vec storage, at the cost of an extra copy.
let end = logical_offset
.checked_add(length)
.ok_or_else(|| DiskError::other("mmap slice length overflow"))?;
let mmap_copy_start = metrics_enabled.then(StdInstant::now);
let mmap_copy_faults_before = read_mmap_page_fault_counts(metrics_enabled);
let bytes = Bytes::copy_from_slice(&mmap[logical_offset..end]);
let mmap_copy_faults_after = read_mmap_page_fault_counts(metrics_enabled);
mmap_copy_duration = mmap_copy_start.map_or(StdDuration::ZERO, |started_at| started_at.elapsed());
mmap_copy_fault_delta = mmap_page_fault_delta(mmap_copy_faults_before, mmap_copy_faults_after);
bytes
}
LocalReadCopyMethod::DirectReadCopy => {
use std::io::{Read as _, Seek as _};
let direct_read_copy_start = metrics_enabled.then(StdInstant::now);
let direct_read_copy_faults_before = read_mmap_page_fault_counts(metrics_enabled);
file.seek(SeekFrom::Start(offset_u64)).map_err(DiskError::from)?;
let mut buffer = vec![0; length];
file.read_exact(&mut buffer).map_err(DiskError::from)?;
let direct_read_copy_faults_after = read_mmap_page_fault_counts(metrics_enabled);
direct_read_copy_duration =
direct_read_copy_start.map_or(StdDuration::ZERO, |started_at| started_at.elapsed());
direct_read_copy_fault_delta =
mmap_page_fault_delta(direct_read_copy_faults_before, direct_read_copy_faults_after);
Bytes::from(buffer)
}
};
#[cfg(target_os = "linux")]
if should_reclaim_after_read {
if should_reclaim_after_read && _reclaim_len > 0 {
use core::num::NonZeroU64;
use rustix::fs::{Advice, fadvise};
let reclaim_len =
NonZeroU64::new(map_len as u64).ok_or_else(|| DiskError::other("mmap reclaim length overflow"))?;
fadvise(&file, aligned_offset, Some(reclaim_len), Advice::DontNeed)
let reclaim_len = NonZeroU64::new(
u64::try_from(_reclaim_len).map_err(|_| DiskError::other("read reclaim length overflow"))?,
)
.ok_or_else(|| DiskError::other("read reclaim length overflow"))?;
fadvise(&file, _reclaim_offset, Some(reclaim_len), Advice::DontNeed)
.map_err(std::io::Error::from)
.map_err(DiskError::from)?;
}
Ok::<Bytes, DiskError>(bytes)
let blocking_task_duration = blocking_task_start.map_or(StdDuration::ZERO, |started_at| started_at.elapsed());
Ok::<MmapCopyReadResult, MmapCopyReadError>(MmapCopyReadResult {
bytes,
access_check_duration,
path_resolve_duration,
metadata_lookup_duration,
metadata_validate_duration,
file_open_duration,
mmap_map_duration,
mmap_copy_duration,
direct_read_copy_duration,
mmap_map_fault_delta,
mmap_copy_fault_delta,
direct_read_copy_fault_delta,
blocking_task_duration,
})
})
.await
.map_err(DiskError::from)??;
.map_err(DiskError::from)
.map_err(MmapCopyReadError::Disk)
.and_then(|result| result);
if let Some(metrics) = metrics {
record_mmap_copy_stage(metrics, metrics.blocking_wait_stage, blocking_wait_start);
}
let read_result = match read_result {
Ok(read_result) => read_result,
Err(MmapCopyReadError::Disk(err)) => return Err(err),
Err(MmapCopyReadError::OutOfBounds { actual_size }) => {
error!(
event = EVENT_DISK_LOCAL_READ_VERSION_FALLBACK,
component = LOG_COMPONENT_ECSTORE,
subsystem = LOG_SUBSYSTEM_DISK_LOCAL,
volume,
path,
offset,
length,
actual_size,
reason = "read_file_mmap_copy_out_of_bounds",
"Disk local read fallback failed"
);
return Err(DiskError::FileCorrupt);
}
};
if metrics_enabled && let Some(metrics) = metrics {
rustfs_io_metrics::record_get_object_stage_duration(
metrics.path,
metrics.blocking_task_stage,
read_result.blocking_task_duration.as_secs_f64(),
);
rustfs_io_metrics::record_get_object_stage_duration(
metrics.path,
metrics.access_check_stage,
read_result.access_check_duration.as_secs_f64(),
);
rustfs_io_metrics::record_get_object_stage_duration(
metrics.path,
metrics.path_resolve_stage,
read_result.path_resolve_duration.as_secs_f64(),
);
rustfs_io_metrics::record_get_object_stage_duration(
metrics.path,
metrics.metadata_lookup_stage,
read_result.metadata_lookup_duration.as_secs_f64(),
);
rustfs_io_metrics::record_get_object_stage_duration(
metrics.path,
metrics.metadata_validate_stage,
read_result.metadata_validate_duration.as_secs_f64(),
);
rustfs_io_metrics::record_get_object_stage_duration(
metrics.path,
metrics.file_open_stage,
read_result.file_open_duration.as_secs_f64(),
);
match read_copy_method {
LocalReadCopyMethod::MmapCopy => {
rustfs_io_metrics::record_get_object_stage_duration(
metrics.path,
metrics.mmap_map_stage,
read_result.mmap_map_duration.as_secs_f64(),
);
rustfs_io_metrics::record_get_object_stage_duration(
metrics.path,
metrics.mmap_copy_stage,
read_result.mmap_copy_duration.as_secs_f64(),
);
record_mmap_page_fault_delta(metrics.path, metrics.mmap_map_stage, read_result.mmap_map_fault_delta);
record_mmap_page_fault_delta(metrics.path, metrics.mmap_copy_stage, read_result.mmap_copy_fault_delta);
}
LocalReadCopyMethod::DirectReadCopy => {
rustfs_io_metrics::record_get_object_stage_duration(
metrics.path,
metrics.direct_read_copy_stage,
read_result.direct_read_copy_duration.as_secs_f64(),
);
record_direct_read_page_fault_delta(
metrics.path,
metrics.direct_read_copy_stage,
read_result.direct_read_copy_fault_delta,
);
}
}
}
let bytes = read_result.bytes;
// Log successful mmap read metrics
let duration_ms = start.elapsed().as_secs_f64() * 1000.0;
@@ -2890,7 +3271,13 @@ impl DiskAPI for LocalDisk {
// Record mmap read metrics
rustfs_io_metrics::record_zero_copy_read(length, duration_ms);
debug!(size = length, duration_ms = duration_ms, "mmap_read_success");
debug!(
size = length,
duration_ms = duration_ms,
mmap_populate = should_populate_mmap_read,
read_copy_method = ?read_copy_method,
"mmap_read_success"
);
return Ok(bytes);
}
@@ -2903,6 +3290,58 @@ impl DiskAPI for LocalDisk {
debug!(reason = "non_unix_platform", "zero_copy_fallback");
let access_check_start = metrics_enabled.then(std::time::Instant::now);
let volume_dir = self.get_bucket_path(volume)?;
if !skip_access_checks(volume) {
access(&volume_dir)
.await
.map_err(|e| to_access_error(e, DiskError::VolumeAccessDenied))?;
}
if let Some(metrics) = metrics {
record_mmap_copy_stage(metrics, metrics.access_check_stage, access_check_start);
}
let path_resolve_start = metrics_enabled.then(std::time::Instant::now);
let file_path = self.get_object_path(volume, path)?;
check_path_length(file_path.to_string_lossy().as_ref())?;
if let Some(metrics) = metrics {
record_mmap_copy_stage(metrics, metrics.path_resolve_stage, path_resolve_start);
}
let file_path_clone = file_path.clone();
let metadata_lookup_start = metrics_enabled.then(std::time::Instant::now);
let meta_result = tokio::task::spawn_blocking(move || std::fs::metadata(&file_path_clone).map_err(DiskError::from))
.await
.map_err(DiskError::from)
.and_then(|result| result);
if let Some(metrics) = metrics {
record_mmap_copy_stage(metrics, metrics.metadata_lookup_stage, metadata_lookup_start);
}
let meta = meta_result?;
let metadata_validate_start = metrics_enabled.then(std::time::Instant::now);
if meta.len() < end_offset as u64 {
if let Some(metrics) = metrics {
record_mmap_copy_stage(metrics, metrics.metadata_validate_stage, metadata_validate_start);
}
error!(
event = EVENT_DISK_LOCAL_READ_VERSION_FALLBACK,
component = LOG_COMPONENT_ECSTORE,
subsystem = LOG_SUBSYSTEM_DISK_LOCAL,
volume,
path,
offset,
length,
actual_size = meta.len(),
reason = "read_file_mmap_copy_out_of_bounds",
"Disk local read fallback failed"
);
return Err(DiskError::FileCorrupt);
}
if let Some(metrics) = metrics {
record_mmap_copy_stage(metrics, metrics.metadata_validate_stage, metadata_validate_start);
}
let mut f = self.open_file(file_path, O_RDONLY, volume_dir).await?;
if offset > 0 {
@@ -5494,6 +5933,88 @@ mod test {
});
}
#[test]
fn should_populate_mmap_read_respects_env() {
temp_env::with_var_unset(ENV_RUSTFS_OBJECT_MMAP_POPULATE_ENABLE, || {
assert!(!should_populate_mmap_read(512 * 1024));
});
temp_env::with_var(ENV_RUSTFS_OBJECT_MMAP_POPULATE_ENABLE, Some("true"), || {
assert!(should_populate_mmap_read(512 * 1024));
assert!(!should_populate_mmap_read(0));
});
temp_env::with_var(ENV_RUSTFS_OBJECT_MMAP_POPULATE_ENABLE, Some("false"), || {
assert!(!should_populate_mmap_read(512 * 1024));
});
}
#[test]
fn local_read_copy_method_respects_env() {
temp_env::with_var_unset(ENV_RUSTFS_OBJECT_MMAP_READ_METHOD, || {
assert_eq!(local_read_copy_method(), LocalReadCopyMethod::MmapCopy);
});
temp_env::with_var(
ENV_RUSTFS_OBJECT_MMAP_READ_METHOD,
Some(RUSTFS_OBJECT_MMAP_READ_METHOD_DIRECT_READ_COPY),
|| {
assert_eq!(local_read_copy_method(), LocalReadCopyMethod::DirectReadCopy);
},
);
temp_env::with_var(ENV_RUSTFS_OBJECT_MMAP_READ_METHOD, Some("unknown"), || {
assert_eq!(local_read_copy_method(), LocalReadCopyMethod::MmapCopy);
});
}
#[cfg(unix)]
#[test]
fn mmap_page_size_is_cached_positive() {
let first = mmap_page_size().expect("page size should be available");
let second = mmap_page_size().expect("cached page size should be available");
assert!(first > 0);
assert_eq!(first, second);
}
#[cfg(unix)]
#[tokio::test]
async fn read_file_mmap_copy_supports_direct_read_copy_method() {
use tempfile::tempdir;
temp_env::async_with_vars(
[(ENV_RUSTFS_OBJECT_MMAP_READ_METHOD, Some(RUSTFS_OBJECT_MMAP_READ_METHOD_DIRECT_READ_COPY))],
async {
let root_dir = tempdir().expect("operation should succeed");
let endpoint = Endpoint::try_from(root_dir.path().to_string_lossy().as_ref()).expect("operation should succeed");
let disk = LocalDisk::new(&endpoint, false).await.expect("operation should succeed");
disk.make_volume("test-volume").await.expect("operation should succeed");
disk.write_all("test-volume", "test-file.txt", Bytes::from_static(b"0123456789abcdef"))
.await
.expect("operation should succeed");
let data = disk
.read_file_mmap_copy("test-volume", "test-file.txt", 4, 6)
.await
.expect("operation should succeed");
assert_eq!(data, Bytes::from_static(b"456789"));
},
)
.await;
}
#[cfg(unix)]
#[test]
fn mmap_page_fault_delta_clamps_non_monotonic_counts() {
let before = Some(MmapPageFaultCounts { minor: 10, major: 4 });
let after = Some(MmapPageFaultCounts { minor: 7, major: 6 });
assert_eq!(mmap_page_fault_delta(before, after), MmapPageFaultDelta { minor: 0, major: 2 });
assert_eq!(mmap_page_fault_delta(before, None), MmapPageFaultDelta::default());
}
#[test]
fn test_is_bitrot_size_mismatch_error_only_matches_target_message() {
assert!(is_bitrot_size_mismatch_error(&std::io::Error::other("bitrot shard file size mismatch")));
+44
View File
@@ -59,6 +59,21 @@ pub type DiskStore = Arc<Disk>;
pub type FileReader = Box<dyn AsyncRead + Send + Sync + Unpin>;
pub type FileWriter = Box<dyn AsyncWrite + Send + Sync + Unpin>;
#[derive(Clone, Copy, Debug)]
pub struct MmapCopyStageMetrics {
pub(crate) path: &'static str,
pub(crate) access_check_stage: &'static str,
pub(crate) path_resolve_stage: &'static str,
pub(crate) metadata_lookup_stage: &'static str,
pub(crate) metadata_validate_stage: &'static str,
pub(crate) blocking_wait_stage: &'static str,
pub(crate) blocking_task_stage: &'static str,
pub(crate) file_open_stage: &'static str,
pub(crate) mmap_map_stage: &'static str,
pub(crate) mmap_copy_stage: &'static str,
pub(crate) direct_read_copy_stage: &'static str,
}
#[derive(Debug)]
pub enum Disk {
Local(Box<LocalDiskWrapper>),
@@ -301,6 +316,24 @@ impl DiskAPI for Disk {
}
}
async fn read_file_mmap_copy_with_metrics(
&self,
volume: &str,
path: &str,
offset: usize,
length: usize,
metrics: Option<MmapCopyStageMetrics>,
) -> Result<Bytes> {
match self {
Disk::Local(local_disk) => {
local_disk
.read_file_mmap_copy_with_metrics(volume, path, offset, length, metrics)
.await
}
Disk::Remote(remote_disk) => remote_disk.read_file_mmap_copy(volume, path, offset, length).await,
}
}
#[tracing::instrument(skip(self))]
async fn append_file(&self, volume: &str, path: &str) -> Result<FileWriter> {
match self {
@@ -564,6 +597,17 @@ pub trait DiskAPI: Debug + Send + Sync + 'static {
/// File read using mmap-then-copy on Unix or an efficient read on non-Unix.
async fn read_file_mmap_copy(&self, volume: &str, path: &str, offset: usize, length: usize) -> Result<Bytes>;
async fn read_file_mmap_copy_with_metrics(
&self,
volume: &str,
path: &str,
offset: usize,
length: usize,
_metrics: Option<MmapCopyStageMetrics>,
) -> Result<Bytes> {
self.read_file_mmap_copy(volume, path, offset, length).await
}
/// Historical name for `read_file_mmap_copy`.
#[deprecated(
since = "1.0.0-beta.8",
+289 -16
View File
@@ -12,7 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::disk::{self, DiskAPI as _, DiskStore, FileReader, error::DiskError};
use crate::diagnostics::get::{
GET_STAGE_READER_MMAP_ACCESS_CHECK, GET_STAGE_READER_MMAP_BLOCKING_TASK, GET_STAGE_READER_MMAP_BLOCKING_WAIT,
GET_STAGE_READER_MMAP_COPY_BUFFER, GET_STAGE_READER_MMAP_DIRECT_READ_COPY, GET_STAGE_READER_MMAP_FILE_OPEN,
GET_STAGE_READER_MMAP_MAP, GET_STAGE_READER_MMAP_METADATA_LOOKUP, GET_STAGE_READER_MMAP_METADATA_VALIDATE,
GET_STAGE_READER_MMAP_PATH_RESOLVE, GET_STAGE_READER_OPEN_MMAP_COPY_FALLBACK, GET_STAGE_READER_OPEN_MMAP_COPY_SUCCESS,
GET_STAGE_READER_OPEN_STREAM, GET_STAGE_READER_STREAM_FIRST_READ, record_get_stage_duration_if_enabled,
};
use crate::disk::{self, DiskAPI as _, DiskStore, FileReader, MmapCopyStageMetrics, error::DiskError};
use crate::erasure::coding::{BitrotReader, BitrotWriterWrapper, CustomWriter};
use bytes::Bytes;
use rustfs_config::{DEFAULT_OBJECT_MMAP_READ_ENABLE, ENV_OBJECT_MMAP_READ_ENABLE, ENV_OBJECT_ZERO_COPY_ENABLE};
@@ -29,6 +36,14 @@ use tracing::debug;
type BoxedObjectReader = Box<dyn AsyncRead + Send + Sync + Unpin>;
type OpenObjectReaderFuture = Pin<Box<dyn Future<Output = disk::error::Result<Option<BoxedObjectReader>>> + Send>>;
#[derive(Clone, Copy)]
pub(crate) struct BitrotReaderStageMetrics {
pub(crate) path: &'static str,
pub(crate) reader_construction_stage: &'static str,
pub(crate) file_open_stage: &'static str,
pub(crate) bitrot_reader_init_stage: &'static str,
}
pub(crate) fn object_mmap_read_enabled() -> bool {
rustfs_utils::get_env_bool_with_aliases(
ENV_OBJECT_MMAP_READ_ENABLE,
@@ -46,6 +61,7 @@ struct BitrotReaderSource {
offset: usize,
length: usize,
use_mmap_read: bool,
stage_metrics: Option<BitrotReaderStageMetrics>,
}
impl BitrotReaderSource {
@@ -56,15 +72,67 @@ impl BitrotReaderSource {
rd.set_position(offset);
Ok(Some(Box::new(rd)))
} else if let Some(disk) = self.disk {
open_disk_reader(&disk, &self.bucket, &self.path, self.offset, self.length, self.use_mmap_read)
.await
.map(Some)
open_disk_reader(
&disk,
&self.bucket,
&self.path,
self.offset,
self.length,
self.use_mmap_read,
self.stage_metrics.map(|metrics| metrics.path),
)
.await
.map(Some)
} else {
Ok(None)
}
}
}
struct FirstReadMetricsReader {
inner: FileReader,
metrics_path: &'static str,
stage: &'static str,
started_at: Option<Instant>,
recorded: bool,
}
impl FirstReadMetricsReader {
fn new(inner: FileReader, metrics_path: &'static str, stage: &'static str) -> Self {
Self {
inner,
metrics_path,
stage,
started_at: None,
recorded: false,
}
}
}
impl AsyncRead for FirstReadMetricsReader {
fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<io::Result<()>> {
if self.recorded {
return Pin::new(&mut self.inner).poll_read(cx, buf);
}
let filled_before = buf.filled().len();
if self.started_at.is_none() {
self.started_at = Some(Instant::now());
}
match Pin::new(&mut self.inner).poll_read(cx, buf) {
Poll::Ready(Ok(())) => {
if buf.filled().len() > filled_before {
self.recorded = true;
record_get_stage_duration_if_enabled(self.metrics_path, self.stage, self.started_at.take());
}
Poll::Ready(Ok(()))
}
other => other,
}
}
}
struct DeferredObjectReader {
state: Mutex<DeferredObjectReaderState>,
}
@@ -146,14 +214,38 @@ async fn open_disk_reader(
offset: usize,
length: usize,
use_mmap_read: bool,
metrics_path: Option<&'static str>,
) -> disk::error::Result<FileReader> {
let metrics_path = metrics_path.filter(|_| rustfs_io_metrics::get_stage_metrics_enabled());
let stage_metrics_enabled = metrics_path.is_some();
if use_mmap_read && disk.is_local() {
let start = Instant::now();
match disk.read_file_mmap_copy(bucket, path, offset, length).await {
let start = stage_metrics_enabled.then(Instant::now);
let zero_copy_start = Instant::now();
let mmap_metrics = metrics_path.map(|metrics_path| MmapCopyStageMetrics {
path: metrics_path,
access_check_stage: GET_STAGE_READER_MMAP_ACCESS_CHECK,
path_resolve_stage: GET_STAGE_READER_MMAP_PATH_RESOLVE,
metadata_lookup_stage: GET_STAGE_READER_MMAP_METADATA_LOOKUP,
metadata_validate_stage: GET_STAGE_READER_MMAP_METADATA_VALIDATE,
blocking_wait_stage: GET_STAGE_READER_MMAP_BLOCKING_WAIT,
blocking_task_stage: GET_STAGE_READER_MMAP_BLOCKING_TASK,
file_open_stage: GET_STAGE_READER_MMAP_FILE_OPEN,
mmap_map_stage: GET_STAGE_READER_MMAP_MAP,
mmap_copy_stage: GET_STAGE_READER_MMAP_COPY_BUFFER,
direct_read_copy_stage: GET_STAGE_READER_MMAP_DIRECT_READ_COPY,
});
match disk
.read_file_mmap_copy_with_metrics(bucket, path, offset, length, mmap_metrics)
.await
{
Ok(bytes) => {
let duration_ms = start.elapsed().as_secs_f64() * 1000.0;
let duration_ms = zero_copy_start.elapsed().as_secs_f64() * 1000.0;
rustfs_io_metrics::record_zero_copy_read(bytes.len(), duration_ms);
if let Some(metrics_path) = metrics_path {
record_get_stage_duration_if_enabled(metrics_path, GET_STAGE_READER_OPEN_MMAP_COPY_SUCCESS, start);
}
debug!(
size = bytes.len(),
path = %path,
@@ -163,6 +255,9 @@ async fn open_disk_reader(
return Ok(Box::new(Cursor::new(bytes)));
}
Err(err) => {
if let Some(metrics_path) = metrics_path {
record_get_stage_duration_if_enabled(metrics_path, GET_STAGE_READER_OPEN_MMAP_COPY_FALLBACK, start);
}
let reason = format!("{err:?}");
rustfs_io_metrics::record_zero_copy_fallback(&reason);
debug!(
@@ -171,15 +266,36 @@ async fn open_disk_reader(
"zero_copy_fallback"
);
return match disk.read_file_stream(bucket, path, offset, length).await {
Ok(reader) => Ok(reader),
let stream_start = stage_metrics_enabled.then(Instant::now);
let stream_result = disk.read_file_stream(bucket, path, offset, length).await;
if let Some(metrics_path) = metrics_path {
record_get_stage_duration_if_enabled(metrics_path, GET_STAGE_READER_OPEN_STREAM, stream_start);
}
return match stream_result {
Ok(reader) => Ok(wrap_first_read_metrics(reader, metrics_path)),
Err(_) => Err(err),
};
}
}
}
disk.read_file_stream(bucket, path, offset, length).await
let stream_start = stage_metrics_enabled.then(Instant::now);
let reader = disk.read_file_stream(bucket, path, offset, length).await?;
if let Some(metrics_path) = metrics_path {
record_get_stage_duration_if_enabled(metrics_path, GET_STAGE_READER_OPEN_STREAM, stream_start);
}
Ok(wrap_first_read_metrics(reader, metrics_path))
}
fn wrap_first_read_metrics(reader: FileReader, metrics_path: Option<&'static str>) -> FileReader {
if let Some(metrics_path) = metrics_path
&& rustfs_io_metrics::get_stage_metrics_enabled()
{
return Box::new(FirstReadMetricsReader::new(reader, metrics_path, GET_STAGE_READER_STREAM_FIRST_READ));
}
reader
}
fn bitrot_encoded_range(offset: usize, length: usize, shard_size: usize, checksum_algo: HashAlgorithm) -> (usize, usize) {
@@ -215,21 +331,131 @@ pub async fn create_bitrot_reader(
skip_verify: bool,
use_mmap_read: bool,
) -> disk::error::Result<Option<BitrotReader<Box<dyn AsyncRead + Send + Sync + Unpin>>>> {
create_bitrot_reader_with_stage_metrics(
inline_data,
disk,
bucket,
path,
offset,
length,
shard_size,
checksum_algo,
skip_verify,
use_mmap_read,
None,
)
.await
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn create_bitrot_reader_with_stage_metrics(
inline_data: Option<&[u8]>,
disk: Option<&DiskStore>,
bucket: &str,
path: &str,
offset: usize,
length: usize,
shard_size: usize,
checksum_algo: HashAlgorithm,
skip_verify: bool,
use_mmap_read: bool,
stage_metrics: Option<BitrotReaderStageMetrics>,
) -> disk::error::Result<Option<BitrotReader<Box<dyn AsyncRead + Send + Sync + Unpin>>>> {
create_bitrot_reader_from_bytes_with_stage_metrics(
inline_data.map(Bytes::copy_from_slice),
disk,
bucket,
path,
offset,
length,
shard_size,
checksum_algo,
skip_verify,
use_mmap_read,
stage_metrics,
)
.await
}
/// Create a BitrotReader from owned inline Bytes or a disk file stream.
///
/// Passing `Bytes` preserves the shared inline data buffer and avoids copying
/// shard payloads that are already owned by metadata.
#[allow(clippy::too_many_arguments)]
pub async fn create_bitrot_reader_from_bytes(
inline_data: Option<Bytes>,
disk: Option<&DiskStore>,
bucket: &str,
path: &str,
offset: usize,
length: usize,
shard_size: usize,
checksum_algo: HashAlgorithm,
skip_verify: bool,
use_mmap_read: bool,
) -> disk::error::Result<Option<BitrotReader<Box<dyn AsyncRead + Send + Sync + Unpin>>>> {
create_bitrot_reader_from_bytes_with_stage_metrics(
inline_data,
disk,
bucket,
path,
offset,
length,
shard_size,
checksum_algo,
skip_verify,
use_mmap_read,
None,
)
.await
}
#[allow(clippy::too_many_arguments)]
async fn create_bitrot_reader_from_bytes_with_stage_metrics(
inline_data: Option<Bytes>,
disk: Option<&DiskStore>,
bucket: &str,
path: &str,
offset: usize,
length: usize,
shard_size: usize,
checksum_algo: HashAlgorithm,
skip_verify: bool,
use_mmap_read: bool,
stage_metrics: Option<BitrotReaderStageMetrics>,
) -> disk::error::Result<Option<BitrotReader<Box<dyn AsyncRead + Send + Sync + Unpin>>>> {
let stage_metrics = stage_metrics.filter(|_| rustfs_io_metrics::get_stage_metrics_enabled());
let stage_metrics_enabled = stage_metrics.is_some();
let reader_construction_start = stage_metrics_enabled.then(Instant::now);
let (offset, length) = bitrot_encoded_range(offset, length, shard_size, checksum_algo.clone());
let source = BitrotReaderSource {
inline_data: inline_data.map(Bytes::copy_from_slice),
inline_data,
disk: disk.cloned(),
bucket: bucket.to_string(),
path: path.to_string(),
offset,
length,
use_mmap_read,
stage_metrics,
};
if let Some(metrics) = stage_metrics {
record_get_stage_duration_if_enabled(metrics.path, metrics.reader_construction_stage, reader_construction_start);
}
source
.open()
.await
.map(|reader| reader.map(|reader| BitrotReader::new(reader, shard_size, checksum_algo, skip_verify)))
let file_open_start = stage_metrics_enabled.then(Instant::now);
let reader = source.open().await?;
if let Some(metrics) = stage_metrics {
record_get_stage_duration_if_enabled(metrics.path, metrics.file_open_stage, file_open_start);
}
let bitrot_reader_init_start = stage_metrics_enabled.then(Instant::now);
let reader = reader.map(|reader| BitrotReader::new(reader, shard_size, checksum_algo, skip_verify));
if let Some(metrics) = stage_metrics {
record_get_stage_duration_if_enabled(metrics.path, metrics.bitrot_reader_init_stage, bitrot_reader_init_start);
}
Ok(reader)
}
#[allow(clippy::too_many_arguments)]
@@ -254,6 +480,7 @@ pub fn create_deferred_bitrot_reader(
offset,
length,
use_mmap_read,
stage_metrics: None,
};
BitrotReader::new(Box::new(DeferredObjectReader::new(source)), shard_size, checksum_algo, skip_verify)
@@ -390,7 +617,7 @@ mod tests {
None,
"test-volume",
"test-path",
payload.len() as i64,
i64::try_from(payload.len()).expect("test payload length should fit i64"),
shard_size,
checksum_algo.clone(),
)
@@ -425,6 +652,52 @@ mod tests {
assert_eq!(&out[..n], b"efgh");
}
#[tokio::test]
async fn test_create_bitrot_reader_from_bytes_preserves_inline_body() {
let shard_size = 4;
let checksum_algo = HashAlgorithm::HighwayHash256S;
let payload = b"abcdefghijkl";
let mut writer = create_bitrot_writer(
true,
None,
"test-volume",
"test-path",
payload.len() as i64,
shard_size,
checksum_algo.clone(),
)
.await
.expect("inline bitrot writer");
for chunk in payload.chunks(shard_size) {
writer.write(chunk).await.expect("write chunk");
}
let inline_data = Bytes::from(writer.into_inline_data().expect("inline buffer"));
let mut reader = create_bitrot_reader_from_bytes(
Some(inline_data),
None,
"test-bucket",
"test-path",
shard_size,
shard_size,
shard_size,
checksum_algo,
false,
false,
)
.await
.expect("create reader from bytes")
.expect("reader");
let mut out = [0u8; 4];
let n = reader.read(&mut out).await.expect("read second shard from bytes");
assert_eq!(n, shard_size);
assert_eq!(&out[..n], b"efgh");
}
#[tokio::test]
async fn test_deferred_bitrot_reader_opens_inline_source_on_read() {
let shard_size = 4;
+4
View File
@@ -277,6 +277,7 @@ impl PutObjReader {
pub struct GetObjectReader {
pub stream: Box<dyn AsyncRead + Unpin + Send + Sync>,
pub object_info: ObjectInfo,
pub buffered_body: Option<Bytes>,
}
#[derive(Debug, Clone, Copy)]
@@ -529,6 +530,7 @@ impl ReadPlan {
GetObjectReader {
stream: reader,
object_info: oi.clone(),
buffered_body: None,
},
self.storage_offset,
self.storage_length,
@@ -582,6 +584,7 @@ impl ReadPlan {
GetObjectReader {
stream: final_reader,
object_info,
buffered_body: None,
},
self.storage_offset,
self.storage_length,
@@ -682,6 +685,7 @@ impl ReadPlan {
GetObjectReader {
stream: final_reader,
object_info,
buffered_body: None,
},
self.storage_offset,
self.storage_length,
+3
View File
@@ -31,6 +31,9 @@ pub struct ObjectOptions {
pub delete_prefix_object: bool,
pub version_id: Option<String>,
pub no_lock: bool,
/// True when an upper layer already holds the object read lock before
/// forwarding a no_lock read to the set layer.
pub metadata_cache_safe: bool,
pub versioned: bool,
pub version_suspended: bool,
@@ -148,6 +148,7 @@ impl MigrationBackendSpy {
GetObjectReader {
stream: Box::new(Cursor::new(vec![0_u8; 3])),
object_info: ObjectInfo::default(),
buffered_body: None,
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -332,8 +332,8 @@ impl ECStore {
pool_meta: RwLock::new(pool_meta),
rebalance_meta: RwLock::new(None),
decommission_cancelers,
start_gate: tokio::sync::Mutex::new(()),
pool_meta_save_gate: tokio::sync::Mutex::new(()),
start_gate: Mutex::new(()),
pool_meta_save_gate: Mutex::new(()),
});
// Only set it when the global deployment ID is not yet configured
@@ -545,6 +545,7 @@ mod tests {
Ok(GetObjectReader {
stream: Box::new(Cursor::new(self.read_payload.clone())),
object_info: self.object_info(bucket, object, self.read_payload.len()),
buffered_body: None,
})
}
+77
View File
@@ -428,6 +428,7 @@ impl ECStore {
diag_enabled,
);
opts.no_lock = true;
opts.metadata_cache_safe = true;
Ok(Some(ObjectLockDiagGuard::new(
guard,
@@ -1301,6 +1302,10 @@ impl ECStore {
mod tests {
use super::*;
use crate::bucket::lifecycle::core::TRANSITION_COMPLETE;
use crate::layout::{
endpoints::{Endpoints, PoolEndpoints},
format::FormatV3,
};
use bytes::Bytes;
use std::io::Cursor;
use std::sync::Arc;
@@ -1818,6 +1823,76 @@ mod tests {
assert!(lookup_opts.no_lock);
}
async fn new_read_lock_test_store() -> ECStore {
let format = FormatV3::new(1, 2);
let endpoints = vec![
Endpoint::try_from("http://127.0.0.1:9000/data0").expect("first endpoint should parse"),
Endpoint::try_from("http://127.0.0.1:9001/data1").expect("second endpoint should parse"),
];
let pool_endpoints = PoolEndpoints {
legacy: false,
set_count: 1,
drives_per_set: 2,
endpoints: Endpoints::from(endpoints),
cmd_line: "read-lock-metadata-cache-safe-test".to_string(),
platform: "test".to_string(),
};
let endpoint_pools = EndpointServerPools::from(vec![pool_endpoints.clone()]);
let sets = Sets::new(vec![None, None], &pool_endpoints, &format, 0, 1)
.await
.expect("test sets should be created with empty disks");
ECStore {
id: Uuid::new_v4(),
disk_map: HashMap::new(),
pools: vec![sets],
peer_sys: S3PeerSys::new(&endpoint_pools),
pool_meta: RwLock::new(PoolMeta::default()),
rebalance_meta: RwLock::new(None),
decommission_cancelers: RwLock::new(Vec::new()),
start_gate: Mutex::new(()),
pool_meta_save_gate: Mutex::new(()),
}
}
#[tokio::test]
async fn acquired_read_lock_marks_metadata_cache_safe_for_set_layer() {
let store = new_read_lock_test_store().await;
let mut opts = ObjectOptions::default();
let guard = store
.acquire_object_read_lock_if_needed("get_object", "bucket", "object", &mut opts)
.await
.expect("read lock should be acquired");
assert!(guard.is_some(), "read lock should be held by the outer store layer");
assert!(opts.no_lock, "set layer should not reacquire the object lock");
assert!(
opts.metadata_cache_safe,
"metadata cache is safe only because the outer store layer acquired the read lock"
);
}
#[tokio::test]
async fn prelocked_read_request_does_not_mark_metadata_cache_safe() {
let store = new_read_lock_test_store().await;
let mut opts = ObjectOptions {
no_lock: true,
..Default::default()
};
let guard = store
.acquire_object_read_lock_if_needed("get_object", "bucket", "object", &mut opts)
.await
.expect("prelocked read should not acquire another lock");
assert!(guard.is_none(), "prelocked caller should keep lock ownership outside ECStore");
assert!(
!opts.metadata_cache_safe,
"generic no_lock callers must stay ineligible for metadata cache unless explicitly marked safe"
);
}
#[tokio::test]
#[serial_test::serial]
async fn reader_lock_is_held_when_optimization_is_disabled() {
@@ -1841,6 +1916,7 @@ mod tests {
let reader = GetObjectReader {
stream: Box::new(Cursor::new(Vec::<u8>::new())),
object_info: ObjectInfo::default(),
buffered_body: None,
};
let reader = ECStore::attach_read_lock_guard(reader, Some(read_guard));
@@ -1879,6 +1955,7 @@ mod tests {
let reader = GetObjectReader {
stream: Box::new(Cursor::new(vec![1, 2, 3])),
object_info: ObjectInfo::default(),
buffered_body: None,
};
let mut reader = ECStore::attach_read_lock_guard(reader, Some(read_guard));
+224
View File
@@ -186,6 +186,25 @@ const SHARD_READ_COST_SAME_NODE: &str = "same_node";
const SHARD_READ_COST_UNKNOWN: &str = "unknown";
const LOW_COST_QUORUM_CANDIDATE_FALSE: &str = "false";
const LOW_COST_QUORUM_CANDIDATE_TRUE: &str = "true";
pub const GET_OBJECT_SIZE_BUCKET_LE_4_KIB: &str = "le_4kib";
pub const GET_OBJECT_SIZE_BUCKET_LE_16_KIB: &str = "le_16kib";
pub const GET_OBJECT_SIZE_BUCKET_LE_64_KIB: &str = "le_64kib";
pub const GET_OBJECT_SIZE_BUCKET_LE_128_KIB: &str = "le_128kib";
pub const GET_OBJECT_SIZE_BUCKET_LE_1_MIB: &str = "le_1mib";
pub const GET_OBJECT_SIZE_BUCKET_GT_1_MIB: &str = "gt_1mib";
/// Return the bounded size bucket used by small-object GET diagnostics.
#[inline(always)]
pub const fn get_object_size_bucket(size_bytes: i64) -> &'static str {
match size_bytes {
..=4_096 => GET_OBJECT_SIZE_BUCKET_LE_4_KIB,
4_097..=16_384 => GET_OBJECT_SIZE_BUCKET_LE_16_KIB,
16_385..=65_536 => GET_OBJECT_SIZE_BUCKET_LE_64_KIB,
65_537..=131_072 => GET_OBJECT_SIZE_BUCKET_LE_128_KIB,
131_073..=1_048_576 => GET_OBJECT_SIZE_BUCKET_LE_1_MIB,
_ => GET_OBJECT_SIZE_BUCKET_GT_1_MIB,
}
}
fn saturating_sub_atomic(counter: &AtomicU64, bytes: u64) -> u64 {
let mut current = counter.load(Ordering::Relaxed);
@@ -404,6 +423,39 @@ pub fn record_get_object_reader_stream_poll(
.record(duration_secs);
}
/// Record a poll of the single-chunk in-memory GetObject handoff stream.
#[inline(always)]
pub fn record_get_object_memory_body_stream_poll(source: &'static str, outcome: &'static str, bytes: usize, duration_secs: f64) {
if !get_stage_metrics_enabled() {
return;
}
let bytes_counter = u64::try_from(bytes).unwrap_or(u64::MAX);
counter!(
"rustfs_io_get_object_memory_body_stream_poll_total",
"source" => source,
"outcome" => outcome
)
.increment(1);
counter!(
"rustfs_io_get_object_memory_body_stream_poll_bytes_total",
"source" => source,
"outcome" => outcome
)
.increment(bytes_counter);
histogram!(
"rustfs_io_get_object_memory_body_stream_poll_bytes",
"source" => source,
"outcome" => outcome
)
.record(usize_to_f64(bytes));
histogram!(
"rustfs_io_get_object_memory_body_stream_poll_duration_seconds",
"source" => source,
"outcome" => outcome
)
.record(duration_secs);
}
/// Record I/O queue congestion observation.
#[inline(always)]
pub fn record_io_queue_congestion() {
@@ -446,6 +498,28 @@ pub fn record_get_object_stage_duration(path: &'static str, stage: &'static str,
histogram!("rustfs_io_get_object_stage_duration_seconds", "path" => path, "stage" => stage).record(duration_secs);
}
/// Record GetObject stage duration with bounded object class and size labels.
#[inline(always)]
pub fn record_get_object_stage_duration_by_size(
path: &'static str,
stage: &'static str,
object_class: &'static str,
size_bucket: &'static str,
duration_secs: f64,
) {
if !get_stage_metrics_enabled() {
return;
}
histogram!(
"rustfs_io_get_object_stage_duration_seconds_by_size",
"path" => path,
"stage" => stage,
"object_class" => object_class,
"size_bucket" => size_bucket
)
.record(duration_secs);
}
/// Record GetObject metadata fanout duration.
#[inline(always)]
pub fn record_get_object_metadata_fanout_duration(path: &'static str, duration_secs: f64) {
@@ -485,6 +559,16 @@ pub fn record_get_object_metadata_response(path: &'static str, outcome: &'static
counter!("rustfs_io_get_object_metadata_response_total", "path" => path, "outcome" => outcome).increment(1);
}
/// Record one bounded metadata cache decision.
#[inline(always)]
pub fn record_get_object_metadata_cache_decision(path: &'static str, decision: &'static str, reason: &'static str) {
if !get_stage_metrics_enabled() {
return;
}
counter!("rustfs_io_get_object_metadata_cache_total", "path" => path, "decision" => decision, "reason" => reason)
.increment(1);
}
/// Record aggregate metadata fanout shape for one GetObject metadata read.
#[inline(always)]
pub fn record_get_object_metadata_fanout_shape(path: &'static str, total: usize, valid: usize, ignored: usize, errors: usize) {
@@ -603,6 +687,37 @@ pub fn record_get_object_reader_path(path: &'static str) {
counter!("rustfs_io_get_object_reader_path_total", "path" => path).increment(1);
}
/// Record the selected GetObject reader path with bounded object class and size labels.
#[inline(always)]
pub fn record_get_object_reader_path_by_size(path: &'static str, object_class: &'static str, size_bucket: &'static str) {
if !get_stage_metrics_enabled() {
return;
}
counter!(
"rustfs_io_get_object_reader_path_by_size_total",
"path" => path,
"object_class" => object_class,
"size_bucket" => size_bucket
)
.increment(1);
}
/// Record the concrete subpath used by the direct-memory GetObject reader.
#[inline(always)]
pub fn record_get_object_direct_memory_subpath(subpath: &'static str, object_class: &'static str, size_bucket: &'static str) {
if !get_stage_metrics_enabled() {
return;
}
counter!("rustfs_io_get_object_direct_memory_subpath_total", "subpath" => subpath).increment(1);
counter!(
"rustfs_io_get_object_direct_memory_subpath_by_size_total",
"subpath" => subpath,
"object_class" => object_class,
"size_bucket" => size_bucket
)
.increment(1);
}
/// Record why the codec streaming reader was not selected.
#[inline(always)]
pub fn record_get_object_codec_streaming_fallback(reason: &'static str) {
@@ -627,6 +742,27 @@ pub fn record_get_object_codec_streaming_decision(outcome: &'static str, object_
.increment(1);
}
/// Record the final codec-streaming rollout decision with bounded size attribution.
#[inline(always)]
pub fn record_get_object_codec_streaming_decision_by_size(
outcome: &'static str,
object_class: &'static str,
reason: &'static str,
size_bucket: &'static str,
) {
if !get_stage_metrics_enabled() {
return;
}
counter!(
"rustfs_io_get_object_codec_streaming_decision_by_size_total",
"outcome" => outcome,
"object_class" => object_class,
"reason" => reason,
"size_bucket" => size_bucket
)
.increment(1);
}
/// Record one decoded reader stripe processed by a GetObject read path.
#[inline(always)]
pub fn record_get_object_reader_stripe(path: &'static str) {
@@ -974,6 +1110,66 @@ pub fn record_get_object_shard_read_fanout(
histogram!("rustfs_io_get_object_shard_read_failed", "path" => path).record(shard_read_fanout_to_f64(failed));
}
/// Record the bitrot reader setup scheduling strategy selected for a GET read.
#[inline(always)]
pub fn record_get_object_reader_setup_strategy(strategy: &'static str, mode: &'static str) {
if !get_stage_metrics_enabled() {
return;
}
counter!(
"rustfs_io_get_object_reader_setup_strategy_total",
"strategy" => strategy,
"mode" => mode
)
.increment(1);
}
/// Record the final bitrot reader setup fanout shape for a GET read.
#[inline(always)]
pub fn record_get_object_reader_setup_fanout(
strategy: &'static str,
mode: &'static str,
scheduled: usize,
attempted: usize,
ready: usize,
failed: usize,
deferred: usize,
) {
if !get_stage_metrics_enabled() {
return;
}
histogram!(
"rustfs_io_get_object_reader_setup_scheduled",
"strategy" => strategy,
"mode" => mode
)
.record(shard_read_fanout_to_f64(scheduled));
histogram!(
"rustfs_io_get_object_reader_setup_attempted",
"strategy" => strategy,
"mode" => mode
)
.record(shard_read_fanout_to_f64(attempted));
histogram!(
"rustfs_io_get_object_reader_setup_ready",
"strategy" => strategy,
"mode" => mode
)
.record(shard_read_fanout_to_f64(ready));
histogram!(
"rustfs_io_get_object_reader_setup_failed",
"strategy" => strategy,
"mode" => mode
)
.record(shard_read_fanout_to_f64(failed));
histogram!(
"rustfs_io_get_object_reader_setup_deferred",
"strategy" => strategy,
"mode" => mode
)
.record(shard_read_fanout_to_f64(deferred));
}
/// Record GetObject metadata resolution duration.
#[inline(always)]
pub fn record_get_object_metadata_phase_duration(duration_secs: f64) {
@@ -1694,10 +1890,13 @@ mod tests {
#[test]
fn test_record_get_object_stage_metrics() {
record_get_object_stage_duration("s3_handler", "request_context", 0.001);
record_get_object_stage_duration_by_size("legacy_duplex", "metadata", "plain_single_part", "le_4kib", 0.001);
record_get_object_reader_path("codec_streaming");
record_get_object_reader_path_by_size("codec_streaming", "plain_single_part", "le_1mib");
record_get_object_codec_streaming_fallback("range");
record_get_object_codec_streaming_decision("fallback", "range", "range");
record_get_object_codec_streaming_decision("use", "plain_single_part", "none");
record_get_object_codec_streaming_decision_by_size("fallback", "plain_single_part", "below_min_size", "le_128kib");
record_get_object_reader_stripe("codec_streaming");
record_get_object_reader_bytes("codec_streaming", 1024);
record_get_object_reader_buffer("codec_streaming", "output", 1024);
@@ -1735,6 +1934,8 @@ mod tests {
record_get_object_pipeline_failure_for_path("codec_streaming", "decode", "read_quorum");
record_get_object_shard_read_observation("codec_streaming", 0, "data", "local", "success", "none", 1024, 0.004, 0.001);
record_get_object_shard_read_cost_summary("codec_streaming", 3, 1, 2, 0, 4, 4, 4, true);
record_get_object_reader_setup_strategy("data_blocks_first", "read_quorum");
record_get_object_reader_setup_fanout("data_blocks_first", "read_quorum", 3, 2, 2, 0, 2);
assert!(0.005_f64.is_sign_positive());
}
@@ -1750,6 +1951,7 @@ mod tests {
record_get_object_reader_prefetch_bytes("codec_streaming", "single_inflight", 4096);
record_get_object_reader_stream_buffer_size("standard", "selected", 131072);
record_get_object_reader_stream_poll("standard", "selected", "ready_data", 8192, 4096, 0.0002);
record_get_object_memory_body_stream_poll("buffered_body", "ready_data", 4096, 0.0001);
assert!(0.0003_f64.is_sign_positive());
}
@@ -1794,8 +1996,11 @@ mod tests {
let _guard = METRICS_FLAG_LOCK.lock().unwrap_or_else(|e| e.into_inner());
set_get_stage_metrics_enabled(true);
record_get_object_stage_duration("s3_handler", "request_context", 0.001);
record_get_object_stage_duration_by_size("legacy_duplex", "metadata", "plain_single_part", "le_4kib", 0.001);
record_get_object_reader_path("codec_streaming");
record_get_object_reader_path_by_size("codec_streaming", "plain_single_part", "le_1mib");
record_get_object_codec_streaming_fallback("range");
record_get_object_codec_streaming_decision_by_size("fallback", "plain_single_part", "below_min_size", "le_128kib");
record_get_object_reader_stripe("codec_streaming");
record_get_object_reader_bytes("codec_streaming", 1024);
record_get_object_reader_buffer("codec_streaming", "output", 1024);
@@ -1838,8 +2043,11 @@ mod tests {
let _guard = METRICS_FLAG_LOCK.lock().unwrap_or_else(|e| e.into_inner());
set_get_stage_metrics_enabled(false);
record_get_object_stage_duration("s3_handler", "request_context", 0.001);
record_get_object_stage_duration_by_size("legacy_duplex", "metadata", "plain_single_part", "le_4kib", 0.001);
record_get_object_reader_path("codec_streaming");
record_get_object_reader_path_by_size("codec_streaming", "plain_single_part", "le_1mib");
record_get_object_codec_streaming_fallback("range");
record_get_object_codec_streaming_decision_by_size("fallback", "plain_single_part", "below_min_size", "le_128kib");
record_get_object_reader_stripe("codec_streaming");
record_get_object_reader_bytes("codec_streaming", 1024);
record_get_object_reader_buffer("codec_streaming", "output", 1024);
@@ -1879,6 +2087,22 @@ mod tests {
assert!(!get_stage_metrics_enabled());
}
#[test]
fn test_get_object_size_buckets_match_issue714_matrix() {
assert_eq!(get_object_size_bucket(0), GET_OBJECT_SIZE_BUCKET_LE_4_KIB);
assert_eq!(get_object_size_bucket(1024), GET_OBJECT_SIZE_BUCKET_LE_4_KIB);
assert_eq!(get_object_size_bucket(4096), GET_OBJECT_SIZE_BUCKET_LE_4_KIB);
assert_eq!(get_object_size_bucket(4097), GET_OBJECT_SIZE_BUCKET_LE_16_KIB);
assert_eq!(get_object_size_bucket(10 * 1024), GET_OBJECT_SIZE_BUCKET_LE_16_KIB);
assert_eq!(get_object_size_bucket(16 * 1024), GET_OBJECT_SIZE_BUCKET_LE_16_KIB);
assert_eq!(get_object_size_bucket((16 * 1024) + 1), GET_OBJECT_SIZE_BUCKET_LE_64_KIB);
assert_eq!(get_object_size_bucket(100 * 1024), GET_OBJECT_SIZE_BUCKET_LE_128_KIB);
assert_eq!(get_object_size_bucket(128 * 1024), GET_OBJECT_SIZE_BUCKET_LE_128_KIB);
assert_eq!(get_object_size_bucket((128 * 1024) + 1), GET_OBJECT_SIZE_BUCKET_LE_1_MIB);
assert_eq!(get_object_size_bucket(1024 * 1024), GET_OBJECT_SIZE_BUCKET_LE_1_MIB);
assert_eq!(get_object_size_bucket((1024 * 1024) + 1), GET_OBJECT_SIZE_BUCKET_GT_1_MIB);
}
#[test]
fn test_record_stage_duration_generic() {
// Generic stage duration should always record (no gating flag)
+55 -6
View File
@@ -54,7 +54,7 @@ use opentelemetry_otlp::{Compression, Protocol, WithExportConfig, WithHttpConfig
use opentelemetry_sdk::propagation::{BaggagePropagator, TraceContextPropagator};
use opentelemetry_sdk::{
logs::SdkLoggerProvider,
metrics::{PeriodicReader, SdkMeterProvider},
metrics::{Aggregation, Instrument, PeriodicReader, SdkMeterProvider, Stream},
trace::{RandomIdGenerator, Sampler, SdkTracerProvider},
};
use percent_encoding::percent_decode_str;
@@ -70,6 +70,19 @@ use tracing_error::ErrorLayer;
use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer};
use tracing_subscriber::{Layer, fmt::format::FmtSpan, layer::SubscriberExt, util::SubscriberInitExt};
const GET_OBJECT_DURATION_HISTOGRAM_METRICS: &[&str] = &[
"rustfs_io_get_object_request_duration_seconds",
"rustfs_io_get_object_total_duration_seconds",
"rustfs_io_get_object_total_duration_seconds_with_path",
"rustfs_io_get_object_stage_duration_seconds",
"rustfs_io_get_object_stage_duration_seconds_by_size",
];
const GET_OBJECT_DURATION_HISTOGRAM_BUCKETS: &[f64] = &[
0.0001, 0.00025, 0.0005, 0.00075, 0.001, 0.0015, 0.002, 0.003, 0.004, 0.005, 0.0075, 0.01, 0.015, 0.02, 0.03, 0.05, 0.075,
0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
];
/// Initialize the full OpenTelemetry HTTP pipeline (traces + metrics + logs).
///
/// This function is invoked when at least one OTLP endpoint has been
@@ -418,11 +431,14 @@ fn build_meter_provider(
let (provider, recorder) = Recorder::builder(service_name.to_string())
.with_meter_provider(|b: opentelemetry_sdk::metrics::MeterProviderBuilder| {
let b = b.with_resource(res).with_reader(
PeriodicReader::builder(exporter)
.with_interval(Duration::from_secs(meter_interval))
.build(),
);
let b = b
.with_resource(res)
.with_reader(
PeriodicReader::builder(exporter)
.with_interval(Duration::from_secs(meter_interval))
.build(),
)
.with_view(get_object_duration_histogram_view);
if use_stdout {
b.with_reader(create_periodic_reader(meter_interval))
} else {
@@ -437,6 +453,24 @@ fn build_meter_provider(
Ok(Some(provider))
}
fn get_object_duration_histogram_view(instrument: &Instrument) -> Option<Stream> {
if !is_get_object_duration_histogram_metric(instrument.name()) {
return None;
}
Stream::builder()
.with_aggregation(Aggregation::ExplicitBucketHistogram {
boundaries: GET_OBJECT_DURATION_HISTOGRAM_BUCKETS.to_vec(),
record_min_max: true,
})
.build()
.ok()
}
fn is_get_object_duration_histogram_metric(name: &str) -> bool {
GET_OBJECT_DURATION_HISTOGRAM_METRICS.contains(&name)
}
/// Build an optional [`SdkLoggerProvider`] for the given log endpoint.
///
/// Returns `None` when the endpoint is empty or log export is disabled.
@@ -687,4 +721,19 @@ mod tests {
assert_eq!(resolve_signal_timeout(Some(0), None), None);
assert_eq!(resolve_signal_timeout(None, Some(0)), None);
}
#[test]
fn test_get_object_duration_histogram_metric_match_is_scoped() {
assert!(is_get_object_duration_histogram_metric("rustfs_io_get_object_stage_duration_seconds"));
assert!(is_get_object_duration_histogram_metric("rustfs_io_get_object_request_duration_seconds"));
assert!(!is_get_object_duration_histogram_metric("rustfs_io_put_object_request_duration_seconds"));
assert!(!is_get_object_duration_histogram_metric("rustfs_io_get_object_response_size_bytes"));
}
#[test]
fn test_get_object_duration_histogram_buckets_are_sorted() {
assert!(GET_OBJECT_DURATION_HISTOGRAM_BUCKETS.windows(2).all(|pair| pair[0] < pair[1]));
assert_eq!(GET_OBJECT_DURATION_HISTOGRAM_BUCKETS.first(), Some(&0.0001));
assert_eq!(GET_OBJECT_DURATION_HISTOGRAM_BUCKETS.last(), Some(&10.0));
}
}
+1
View File
@@ -1215,6 +1215,7 @@ mod tests {
Ok(GetObjectReader {
stream: Box::new(Cursor::new(data)),
object_info: ObjectInfo::default(),
buffered_body: None,
})
}
+336 -46
View File
@@ -191,10 +191,32 @@ const LOG_SUBSYSTEM_OBJECT: &str = "object";
const EVENT_PUT_OBJECT_STORE_INFLIGHT_SLOW: &str = "put_object_store_inflight_slow";
const EVENT_PUT_OBJECT_STORE_RETURNED: &str = "put_object_store_returned";
const EVENT_GET_OBJECT_STREAM_BODY: &str = "get_object_stream_body";
const GET_OBJECT_STAGE_PATH_S3_HANDLER: &str = "s3_handler";
const GET_OBJECT_STAGE_REQUEST_INGRESS_TO_CONTEXT: &str = "request_ingress_to_context";
const GET_OBJECT_STAGE_OUTPUT_STRATEGY: &str = "output_strategy";
const GET_OBJECT_STAGE_BODY_BUILD: &str = "body_build";
const GET_OBJECT_STAGE_BODY_ENCRYPTED_BUFFER_READ: &str = "body_encrypted_buffer_read";
const GET_OBJECT_STAGE_BODY_MEMORY_BLOB: &str = "body_memory_blob";
const GET_OBJECT_STAGE_BODY_SEEK_BUFFER_READ: &str = "body_seek_buffer_read";
const GET_OBJECT_STAGE_BODY_STREAM_STRATEGY: &str = "body_stream_strategy";
const GET_OBJECT_STAGE_BODY_STREAMING_BLOB: &str = "body_streaming_blob";
const GET_OBJECT_STAGE_CHECKSUM_HEADERS: &str = "checksum_headers";
const GET_OBJECT_STAGE_LIFECYCLE_EXPIRATION: &str = "lifecycle_expiration";
const GET_OBJECT_STAGE_METADATA_FILTER: &str = "metadata_filter";
const PUT_OBJECT_STORE_WARN_THRESHOLD: Duration = Duration::from_secs(5);
const GET_OBJECT_STREAM_WARN_THRESHOLD: Duration = Duration::from_secs(5);
static GET_OBJECT_BUFFER_THRESHOLD_WARNED: AtomicBool = AtomicBool::new(false);
fn record_get_object_s3_handler_stage_duration(stage: &'static str, start: Option<std::time::Instant>) {
if let Some(start) = start {
rustfs_io_metrics::record_get_object_stage_duration(
GET_OBJECT_STAGE_PATH_S3_HANDLER,
stage,
start.elapsed().as_secs_f64(),
);
}
}
fn decoded_content_length_from_headers(headers: &HeaderMap) -> S3Result<Option<i64>> {
let Some(val) = headers.get(AMZ_DECODED_CONTENT_LENGTH) else {
return Ok(None);
@@ -235,6 +257,23 @@ impl DeadlockRequestGuard {
request_id,
}
}
fn register_if_enabled<F>(
deadlock_detector: Arc<deadlock_detector::DeadlockDetector>,
request_id: &str,
description: F,
) -> Option<Self>
where
F: FnOnce() -> String,
{
if !deadlock_detector.is_enabled() {
return None;
}
let request_id = request_id.to_string();
deadlock_detector.register_request(&request_id, description());
Some(Self::new(deadlock_detector, request_id))
}
}
impl Drop for DeadlockRequestGuard {
@@ -248,7 +287,7 @@ struct GetObjectBootstrap {
wrapper: RequestTimeoutWrapper,
request_start: std::time::Instant,
request_guard: GetObjectGuard,
_deadlock_request_guard: DeadlockRequestGuard,
_deadlock_request_guard: Option<DeadlockRequestGuard>,
concurrent_requests: usize,
}
@@ -273,6 +312,7 @@ struct GetObjectReadSetup {
info: ObjectInfo,
event_info: ObjectInfo,
final_stream: DynReader,
buffered_body: Option<Bytes>,
rs: Option<HTTPRangeSpec>,
content_type: Option<ContentType>,
last_modified: Option<Timestamp>,
@@ -332,6 +372,7 @@ const LARGE_SEQUENTIAL_GET_STREAM_BUFFER_CAP_BYTES: usize = 4 * MI_B;
const LARGE_SEQUENTIAL_GET_READAHEAD_MULTIPLIER: usize = 2;
const LARGE_BODY_READER_STREAM_BUFFER_FLOOR_BYTES: usize = MI_B;
const LARGE_BODY_READER_STREAM_BUFFER_THRESHOLD_BYTES: i64 = 4 * MI_B as i64;
const ENV_RUSTFS_GET_SEEK_BUFFER_ENABLE: &str = "RUSTFS_GET_SEEK_BUFFER_ENABLE";
const ENV_RUSTFS_GET_READER_STREAM_BUFFER_SIZE: &str = "RUSTFS_GET_READER_STREAM_BUFFER_SIZE";
const ENV_RUSTFS_GET_OUTPUT_HANDOFF_ATTRIBUTION_ENABLE: &str = "RUSTFS_GET_OUTPUT_HANDOFF_ATTRIBUTION_ENABLE";
const GET_READER_STREAM_BUFFER_SOURCE_SELECTED: &str = "selected";
@@ -340,6 +381,9 @@ const GET_READER_STREAM_POLL_PENDING: &str = "pending";
const GET_READER_STREAM_POLL_READY_DATA: &str = "ready_data";
const GET_READER_STREAM_POLL_READY_EMPTY: &str = "ready_empty";
const GET_READER_STREAM_POLL_READY_ERROR: &str = "ready_error";
const GET_MEMORY_BODY_SOURCE_BUFFERED_BODY: &str = "buffered_body";
const GET_MEMORY_BODY_SOURCE_SEEK_BUFFER: &str = "seek_buffer";
const GET_MEMORY_BODY_SOURCE_ENCRYPTED_BUFFER: &str = "encrypted_buffer";
fn get_reader_stream_buffer_size_override() -> Option<usize> {
static GET_READER_STREAM_BUFFER_SIZE_OVERRIDE: OnceLock<Option<usize>> = OnceLock::new();
@@ -356,6 +400,11 @@ fn is_get_output_handoff_attribution_enabled() -> bool {
*ENABLED.get_or_init(|| rustfs_utils::get_env_bool(ENV_RUSTFS_GET_OUTPUT_HANDOFF_ATTRIBUTION_ENABLE, false))
}
fn is_get_seek_buffer_enabled() -> bool {
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init(|| rustfs_utils::get_env_bool(ENV_RUSTFS_GET_SEEK_BUFFER_ENABLE, false))
}
fn resolve_reader_stream_buffer_size(selected_size: usize, override_size: Option<usize>) -> (usize, &'static str) {
if let Some(override_size) = override_size.filter(|value| *value > 0) {
return (override_size, GET_READER_STREAM_BUFFER_SOURCE_ENV_OVERRIDE);
@@ -437,6 +486,8 @@ pin_project! {
struct MemoryTrackedBytesStream {
bytes: Bytes,
emitted: bool,
started: std::time::Instant,
source: &'static str,
_guard: Option<rustfs_io_metrics::MemoryGaugeGuard>,
}
}
@@ -483,10 +534,12 @@ pin_project! {
}
impl MemoryTrackedBytesStream {
fn new(bytes: Bytes, guard: Option<rustfs_io_metrics::MemoryGaugeGuard>) -> Self {
fn new(bytes: Bytes, source: &'static str, guard: Option<rustfs_io_metrics::MemoryGaugeGuard>) -> Self {
Self {
bytes,
emitted: false,
started: std::time::Instant::now(),
source,
_guard: guard,
}
}
@@ -516,11 +569,32 @@ impl futures::Stream for MemoryTrackedBytesStream {
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.project();
let poll_start = is_get_output_handoff_attribution_enabled().then(std::time::Instant::now);
if *this.emitted {
if let Some(poll_start) = poll_start {
rustfs_io_metrics::record_get_object_memory_body_stream_poll(
this.source,
GET_READER_STREAM_POLL_READY_EMPTY,
0,
poll_start.elapsed().as_secs_f64(),
);
}
return Poll::Ready(None);
}
let first_byte_elapsed = (!this.bytes.is_empty()).then(|| this.started.elapsed());
*this.emitted = true;
if let Some(elapsed) = first_byte_elapsed {
rustfs_io_metrics::record_get_object_first_byte_latency(GET_OBJECT_STAGE_PATH_S3_HANDLER, elapsed.as_secs_f64());
}
if let Some(poll_start) = poll_start {
rustfs_io_metrics::record_get_object_memory_body_stream_poll(
this.source,
GET_READER_STREAM_POLL_READY_DATA,
this.bytes.len(),
poll_start.elapsed().as_secs_f64(),
);
}
Poll::Ready(Some(Ok(this.bytes.clone())))
}
}
@@ -662,6 +736,10 @@ impl<R: AsyncRead + Unpin> AsyncRead for GetObjectStreamingReader<R> {
if !self.first_byte_reported {
self.first_byte_reported = true;
let elapsed = self.elapsed();
rustfs_io_metrics::record_get_object_first_byte_latency(
GET_OBJECT_STAGE_PATH_S3_HANDLER,
elapsed.as_secs_f64(),
);
if elapsed >= GET_OBJECT_STREAM_WARN_THRESHOLD {
warn!(
event = EVENT_GET_OBJECT_STREAM_BODY,
@@ -1149,6 +1227,7 @@ fn should_buffer_get_object_in_memory(
has_range,
configured_threshold,
concurrent_requests,
is_get_seek_buffer_enabled(),
)
}
@@ -1159,8 +1238,12 @@ fn should_buffer_get_object_in_memory_with_threshold(
has_range: bool,
configured_threshold: i64,
concurrent_requests: usize,
seek_buffer_enabled: bool,
) -> bool {
if part_number.is_some() || has_range || response_content_length <= 0 || configured_threshold <= 0 {
if !seek_buffer_enabled || part_number.is_some() || has_range || response_content_length <= 0 || configured_threshold <= 0 {
return false;
}
if usize::try_from(response_content_length).is_err() {
return false;
}
@@ -1188,6 +1271,8 @@ fn should_buffer_get_object_in_memory_with_threshold(
mod deadlock_request_guard_tests {
use super::DeadlockRequestGuard;
use crate::app::storage_api::object_usecase::deadlock_detector::{DeadlockDetector, RequestHangDetectionPolicy};
use std::cell::Cell;
use std::rc::Rc;
use std::sync::Arc;
#[test]
@@ -1208,6 +1293,24 @@ mod deadlock_request_guard_tests {
assert_eq!(detector.tracked_count(), 0);
}
#[test]
fn deadlock_request_guard_skips_disabled_detector() {
let detector = Arc::new(DeadlockDetector::new(RequestHangDetectionPolicy {
enabled: false,
..RequestHangDetectionPolicy::default()
}));
let description_built = Rc::new(Cell::new(false));
let description_built_for_closure = Rc::clone(&description_built);
let guard = DeadlockRequestGuard::register_if_enabled(detector, "test-request-id", || {
description_built_for_closure.set(true);
"test request".to_string()
});
assert!(guard.is_none());
assert!(!description_built.get());
}
}
async fn maybe_enqueue_transition_immediate(obj_info: &ObjectInfo, src: LcEventSrc) {
@@ -1970,13 +2073,39 @@ impl DefaultObjectUsecase {
}
}
fn build_memory_blob(buf: Vec<u8>, response_content_length: i64, _optimal_buffer_size: usize) -> Option<StreamingBlob> {
let guard = rustfs_io_metrics::track_get_object_buffered_bytes(buf.len());
let bytes = Bytes::from(buf);
Some(StreamingBlob::wrap(bytes_stream(
MemoryTrackedBytesStream::new(bytes, guard),
response_content_length as usize,
)))
fn build_memory_bytes_blob(
bytes: Bytes,
response_content_length: i64,
_optimal_buffer_size: usize,
source: &'static str,
) -> Option<StreamingBlob> {
let get_stage_metrics_enabled = rustfs_io_metrics::get_stage_metrics_enabled();
let memory_blob_start = get_stage_metrics_enabled.then(std::time::Instant::now);
let handoff_start = get_stage_metrics_enabled.then(std::time::Instant::now);
let bytes_len = bytes.len();
let guard = rustfs_io_metrics::track_get_object_buffered_bytes(bytes_len);
let remaining = usize::try_from(response_content_length.max(0)).unwrap_or(usize::MAX);
let blob = StreamingBlob::wrap(bytes_stream(MemoryTrackedBytesStream::new(bytes, source, guard), remaining));
if let Some(handoff_start) = handoff_start {
rustfs_io_metrics::record_get_object_response_handoff(
"single_chunk",
source,
bytes_len,
response_content_length,
handoff_start.elapsed().as_secs_f64(),
);
}
record_get_object_s3_handler_stage_duration(GET_OBJECT_STAGE_BODY_MEMORY_BLOB, memory_blob_start);
Some(blob)
}
fn build_memory_blob(
buf: Vec<u8>,
response_content_length: i64,
optimal_buffer_size: usize,
source: &'static str,
) -> Option<StreamingBlob> {
Self::build_memory_bytes_blob(Bytes::from(buf), response_content_length, optimal_buffer_size, source)
}
fn select_stream_buffer_strategy(
@@ -2007,6 +2136,7 @@ impl DefaultObjectUsecase {
where
R: AsyncRead + Send + Sync + Unpin + 'static,
{
let streaming_blob_start = rustfs_io_metrics::get_stage_metrics_enabled().then(std::time::Instant::now);
let expected = usize::try_from(response_content_length.max(0)).unwrap_or(usize::MAX);
let tuned_stream_buffer_size =
tune_reader_stream_buffer_size(stream_buffer_size, response_content_length, stream_strategy);
@@ -2033,20 +2163,21 @@ impl DefaultObjectUsecase {
handoff_start.elapsed().as_secs_f64(),
);
}
record_get_object_s3_handler_stage_duration(GET_OBJECT_STAGE_BODY_STREAMING_BLOB, streaming_blob_start);
Some(blob)
}
fn init_get_object_bootstrap(bucket: &str, key: &str, request_id: &str) -> S3Result<GetObjectBootstrap> {
let timeout_config = GetObjectTimeoutPolicy::from_env();
let timeout_config = GetObjectTimeoutPolicy::cached_from_env();
let wrapper = RequestTimeoutWrapper::with_request_id(timeout_config.clone(), request_id.to_string());
let request_start = std::time::Instant::now();
let request_guard = ConcurrencyManager::track_request();
let concurrent_requests = GetObjectGuard::concurrent_requests();
let deadlock_detector = deadlock_detector::get_deadlock_detector();
let request_id = wrapper.request_id().to_string();
deadlock_detector.register_request(&request_id, format!("GetObject {bucket}/{key}"));
let deadlock_request_guard = DeadlockRequestGuard::new(deadlock_detector, request_id);
let deadlock_request_guard = DeadlockRequestGuard::register_if_enabled(deadlock_detector, wrapper.request_id(), || {
format!("GetObject {bucket}/{key}")
});
Self::ensure_get_object_not_timed_out(&wrapper, &timeout_config, bucket, key, GetObjectTimeoutStage::BeforeProcessing)?;
@@ -2255,6 +2386,8 @@ impl DefaultObjectUsecase {
}
let info = reader.object_info;
let stream = reader.stream;
let buffered_body = reader.buffered_body;
let read_duration = read_start.elapsed();
@@ -2342,6 +2475,7 @@ impl DefaultObjectUsecase {
ssekms_key_id,
encryption_applied,
final_stream,
buffered_body,
) = match sse_decryption(decryption_request).await? {
Some(material) => {
let server_side_encryption = Some(material.server_side_encryption.clone());
@@ -2353,10 +2487,11 @@ impl DefaultObjectUsecase {
sse_customer_key_md5,
material.kms_key_id,
true,
wrap_reader(reader.stream),
wrap_reader(stream),
None,
)
}
None => (None, None, None, None, false, wrap_reader(reader.stream)),
None => (None, None, None, None, false, wrap_reader(stream), buffered_body),
};
// Detect inline fast path: data is in memory, no disk I/O semaphore needed.
@@ -2367,6 +2502,7 @@ impl DefaultObjectUsecase {
info,
event_info,
final_stream,
buffered_body,
rs,
content_type,
last_modified,
@@ -2536,6 +2672,7 @@ impl DefaultObjectUsecase {
part_number: Option<usize>,
has_range: bool,
encryption_applied: bool,
buffered_body: Option<Bytes>,
bucket: &str,
key: &str,
) -> S3Result<Option<StreamingBlob>>
@@ -2548,7 +2685,10 @@ impl DefaultObjectUsecase {
if should_buffer_encrypted_object {
let mut buf = Vec::with_capacity(response_content_length as usize);
if let Err(e) = tokio::io::AsyncReadExt::read_to_end(&mut final_stream, &mut buf).await {
let buffer_read_start = rustfs_io_metrics::get_stage_metrics_enabled().then(std::time::Instant::now);
let read_result = tokio::io::AsyncReadExt::read_to_end(&mut final_stream, &mut buf).await;
record_get_object_s3_handler_stage_duration(GET_OBJECT_STAGE_BODY_ENCRYPTED_BUFFER_READ, buffer_read_start);
if let Err(e) = read_result {
error!(error = %e, "GetObject decrypted object buffering failed");
return Err(ApiError::from(StorageError::other(format!("Failed to read decrypted object: {e}"))).into());
}
@@ -2561,12 +2701,19 @@ impl DefaultObjectUsecase {
);
}
return Ok(Self::build_memory_blob(buf, response_content_length, optimal_buffer_size));
return Ok(Self::build_memory_blob(
buf,
response_content_length,
optimal_buffer_size,
GET_MEMORY_BODY_SOURCE_ENCRYPTED_BUFFER,
));
}
debug!(buffer_size = optimal_buffer_size, "Encrypted object uses streaming decrypt path");
let stream_strategy_start = rustfs_io_metrics::get_stage_metrics_enabled().then(std::time::Instant::now);
let (stream_buffer_size, stream_strategy) =
Self::select_stream_buffer_strategy(response_content_length, optimal_buffer_size, enable_readahead, has_range);
record_get_object_s3_handler_stage_duration(GET_OBJECT_STAGE_BODY_STREAM_STRATEGY, stream_strategy_start);
return Ok(Self::build_reader_blob(
final_stream,
response_content_length,
@@ -2577,12 +2724,32 @@ impl DefaultObjectUsecase {
));
}
if let Some(buffered_body) = buffered_body {
if buffered_body.len() != usize::try_from(response_content_length.max(0)).unwrap_or(usize::MAX) {
warn!(
expected = response_content_length,
actual = buffered_body.len(),
"Buffered GetObject body size mismatch"
);
}
return Ok(Self::build_memory_bytes_blob(
buffered_body,
response_content_length,
optimal_buffer_size,
GET_MEMORY_BODY_SOURCE_BUFFERED_BODY,
));
}
let should_provide_seek_support =
should_buffer_get_object_in_memory(info, response_content_length, part_number, has_range, concurrent_requests);
if should_provide_seek_support {
let mut buf = Vec::with_capacity(response_content_length as usize);
match tokio::io::AsyncReadExt::read_to_end(&mut final_stream, &mut buf).await {
let buffer_read_start = rustfs_io_metrics::get_stage_metrics_enabled().then(std::time::Instant::now);
let read_result = tokio::io::AsyncReadExt::read_to_end(&mut final_stream, &mut buf).await;
record_get_object_s3_handler_stage_duration(GET_OBJECT_STAGE_BODY_SEEK_BUFFER_READ, buffer_read_start);
match read_result {
Ok(_) => {
if buf.len() != response_content_length as usize {
warn!(
@@ -2592,7 +2759,12 @@ impl DefaultObjectUsecase {
);
}
return Ok(Self::build_memory_blob(buf, response_content_length, optimal_buffer_size));
return Ok(Self::build_memory_blob(
buf,
response_content_length,
optimal_buffer_size,
GET_MEMORY_BODY_SOURCE_SEEK_BUFFER,
));
}
Err(e) => {
error!(error = %e, "GetObject seek-support buffering failed");
@@ -2600,8 +2772,10 @@ impl DefaultObjectUsecase {
}
}
let stream_strategy_start = rustfs_io_metrics::get_stage_metrics_enabled().then(std::time::Instant::now);
let (stream_buffer_size, stream_strategy) =
Self::select_stream_buffer_strategy(response_content_length, optimal_buffer_size, enable_readahead, has_range);
record_get_object_s3_handler_stage_duration(GET_OBJECT_STAGE_BODY_STREAM_STRATEGY, stream_strategy_start);
Ok(Self::build_reader_blob(
final_stream,
response_content_length,
@@ -3297,6 +3471,7 @@ impl DefaultObjectUsecase {
info: ObjectInfo,
event_info: ObjectInfo,
final_stream: DynReader,
buffered_body: Option<Bytes>,
rs: Option<HTTPRangeSpec>,
content_type: Option<ContentType>,
last_modified: Option<Timestamp>,
@@ -3314,6 +3489,7 @@ impl DefaultObjectUsecase {
part_number: Option<usize>,
versioned: bool,
) -> S3Result<GetObjectOutputContext> {
let strategy_start = rustfs_io_metrics::get_stage_metrics_enabled().then(std::time::Instant::now);
let strategy = self.finalize_get_object_strategy(
manager,
bucket,
@@ -3326,12 +3502,14 @@ impl DefaultObjectUsecase {
queue_status,
concurrent_requests,
);
record_get_object_s3_handler_stage_duration(GET_OBJECT_STAGE_OUTPUT_STRATEGY, strategy_start);
let GetObjectStrategyContext {
io_strategy: _,
optimal_buffer_size,
enable_readahead,
} = strategy;
let body_build_start = rustfs_io_metrics::get_stage_metrics_enabled().then(std::time::Instant::now);
let body = Self::build_get_object_body(
final_stream,
&info,
@@ -3342,12 +3520,16 @@ impl DefaultObjectUsecase {
part_number,
rs.is_some(),
encryption_applied,
buffered_body,
bucket,
key,
)
.await?;
record_get_object_s3_handler_stage_duration(GET_OBJECT_STAGE_BODY_BUILD, body_build_start);
let checksum_headers_start = rustfs_io_metrics::get_stage_metrics_enabled().then(std::time::Instant::now);
let checksums = Self::build_get_object_checksums(&info, &req.headers, part_number, rs.as_ref())?;
record_get_object_s3_handler_stage_duration(GET_OBJECT_STAGE_CHECKSUM_HEADERS, checksum_headers_start);
let output_version_id = if versioned {
info.version_id.map(|vid| {
@@ -3368,10 +3550,16 @@ impl DefaultObjectUsecase {
});
// x-amz-expiration: predict from lifecycle configuration
let lifecycle_expiration_start = rustfs_io_metrics::get_stage_metrics_enabled().then(std::time::Instant::now);
let expiration = resolve_put_object_expiration(bucket, &info).await;
record_get_object_s3_handler_stage_duration(GET_OBJECT_STAGE_LIFECYCLE_EXPIRATION, lifecycle_expiration_start);
let storage_class = response_storage_class(&info, &info.user_defined);
let content_disposition = info.user_defined.get("content-disposition").cloned();
let metadata_filter_start = rustfs_io_metrics::get_stage_metrics_enabled().then(std::time::Instant::now);
let metadata = filter_object_metadata(&info.user_defined);
record_get_object_s3_handler_stage_duration(GET_OBJECT_STAGE_METADATA_FILTER, metadata_filter_start);
let output = GetObjectOutput {
body,
content_length: Some(response_content_length),
@@ -3382,7 +3570,7 @@ impl DefaultObjectUsecase {
accept_ranges: Some(ACCEPT_RANGES_BYTES.to_string()),
content_range,
e_tag: info.etag.map(|etag| to_s3s_etag(&etag)),
metadata: filter_object_metadata(&info.user_defined),
metadata,
server_side_encryption,
sse_customer_algorithm,
sse_customer_key_md5,
@@ -3418,11 +3606,19 @@ impl DefaultObjectUsecase {
let _ = context.object_store();
}
let request_id = req
.extensions
.get::<request_context::RequestContext>()
let inbound_request_context = req.extensions.get::<request_context::RequestContext>();
let request_id = inbound_request_context
.map(|ctx| ctx.request_id.clone())
.unwrap_or_else(|| request_context::RequestContext::fallback().request_id);
if rustfs_io_metrics::get_stage_metrics_enabled()
&& let Some(context) = inbound_request_context
{
rustfs_io_metrics::record_get_object_stage_duration(
GET_OBJECT_STAGE_PATH_S3_HANDLER,
GET_OBJECT_STAGE_REQUEST_INGRESS_TO_CONTEXT,
context.start_time.elapsed().as_secs_f64(),
);
}
let bootstrap = Self::init_get_object_bootstrap(&req.input.bucket, &req.input.key, &request_id)?;
let timeout_config = bootstrap.timeout_config;
let wrapper = bootstrap.wrapper;
@@ -3477,6 +3673,7 @@ impl DefaultObjectUsecase {
info,
event_info,
final_stream,
buffered_body,
rs,
content_type,
last_modified,
@@ -3495,15 +3692,6 @@ impl DefaultObjectUsecase {
final_stream
};
let versioning_start = rustfs_io_metrics::get_stage_metrics_enabled().then(std::time::Instant::now);
let versioned = BucketVersioningSys::prefix_enabled(&bucket, &key).await;
if let Some(versioning_start) = versioning_start {
rustfs_io_metrics::record_get_object_stage_duration(
"s3_handler",
"versioning_lookup",
versioning_start.elapsed().as_secs_f64(),
);
}
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(
@@ -3514,6 +3702,7 @@ impl DefaultObjectUsecase {
info,
event_info,
final_stream,
buffered_body,
rs,
content_type,
last_modified,
@@ -3529,7 +3718,7 @@ impl DefaultObjectUsecase {
&queue_status,
concurrent_requests,
part_number,
versioned,
opts.versioned,
)
.await?;
if let Some(output_build_start) = output_build_start {
@@ -6007,7 +6196,7 @@ mod tests {
let configured_threshold = 20_i64 * 1024 * 1024 * 1024;
let response_len = 80_i64 * 1024 * 1024;
let should_buffer =
should_buffer_get_object_in_memory_with_threshold(&info, response_len, None, false, configured_threshold, 1);
should_buffer_get_object_in_memory_with_threshold(&info, response_len, None, false, configured_threshold, 1, true);
assert!(
!should_buffer,
@@ -6026,7 +6215,8 @@ mod tests {
None,
false,
configured_threshold,
1
1,
true
));
assert!(!should_buffer_get_object_in_memory_with_threshold(
&info,
@@ -6034,7 +6224,8 @@ mod tests {
Some(1),
false,
configured_threshold,
1
1,
true
));
assert!(!should_buffer_get_object_in_memory_with_threshold(
&info,
@@ -6042,7 +6233,24 @@ mod tests {
None,
true,
configured_threshold,
1
1,
true
));
}
#[test]
fn should_buffer_get_object_in_memory_requires_seek_buffer_opt_in() {
let info = ObjectInfo::default();
let configured_threshold = 10_i64 * 1024 * 1024;
assert!(!should_buffer_get_object_in_memory_with_threshold(
&info,
1024,
None,
false,
configured_threshold,
1,
false
));
}
@@ -6057,7 +6265,8 @@ mod tests {
None,
false,
configured_threshold,
1
1,
true
));
assert!(!should_buffer_get_object_in_memory_with_threshold(
&info,
@@ -6065,7 +6274,8 @@ mod tests {
None,
false,
configured_threshold,
1
1,
true
));
}
@@ -6080,7 +6290,8 @@ mod tests {
None,
false,
configured_threshold,
1
1,
true
));
assert!(!should_buffer_get_object_in_memory_with_threshold(
&info,
@@ -6088,9 +6299,10 @@ mod tests {
None,
false,
configured_threshold,
1
1,
true
));
assert!(!should_buffer_get_object_in_memory_with_threshold(&info, 1024, None, false, 0, 1));
assert!(!should_buffer_get_object_in_memory_with_threshold(&info, 1024, None, false, 0, 1, true));
}
#[test]
@@ -6104,7 +6316,8 @@ mod tests {
None,
false,
configured_threshold,
1
1,
true
));
assert!(!should_buffer_get_object_in_memory_with_threshold(
&info,
@@ -6112,7 +6325,8 @@ mod tests {
None,
false,
configured_threshold,
32
32,
true
));
assert!(should_buffer_get_object_in_memory_with_threshold(
&info,
@@ -6120,7 +6334,8 @@ mod tests {
None,
false,
configured_threshold,
rustfs_config::DEFAULT_OBJECT_HIGH_CONCURRENCY_THRESHOLD
rustfs_config::DEFAULT_OBJECT_HIGH_CONCURRENCY_THRESHOLD,
true
));
}
@@ -6194,6 +6409,7 @@ mod tests {
None,
false,
false,
None,
"test-bucket",
"large-object",
)
@@ -6229,6 +6445,7 @@ mod tests {
None,
false,
true,
None,
"test-bucket",
"large-encrypted-object",
)
@@ -6243,6 +6460,78 @@ mod tests {
);
}
#[tokio::test]
async fn build_get_object_body_uses_buffered_body_without_reader_preread() {
let reads = Arc::new(AtomicUsize::new(0));
let reader = ReadProbeReader {
reads: Arc::clone(&reads),
};
let info = ObjectInfo {
size: 4,
..Default::default()
};
let body = DefaultObjectUsecase::build_get_object_body(
reader,
&info,
4,
128 * 1024,
false,
1,
None,
false,
false,
Some(Bytes::from_static(b"test")),
"test-bucket",
"direct-memory-object",
)
.await
.expect("build_get_object_body should consume buffered body");
assert!(body.is_some());
assert_eq!(
reads.load(AtomicOrdering::Relaxed),
0,
"buffered GetObject body must not be read from the fallback reader"
);
}
#[tokio::test]
async fn build_get_object_body_keeps_small_plain_objects_on_streaming_path_by_default() {
let reads = Arc::new(AtomicUsize::new(0));
let reader = ReadProbeReader {
reads: Arc::clone(&reads),
};
let info = ObjectInfo {
size: 4,
..Default::default()
};
let body = DefaultObjectUsecase::build_get_object_body(
reader,
&info,
4,
128 * 1024,
false,
1,
None,
false,
false,
None,
"test-bucket",
"small-plain-object",
)
.await
.expect("build_get_object_body should keep small plain object on streaming path");
assert!(body.is_some());
assert_eq!(
reads.load(AtomicOrdering::Relaxed),
0,
"default GetObject response construction should not pre-read small plain object data"
);
}
#[test]
fn select_stream_buffer_strategy_expands_large_sequential_gets() {
let (buffer_size, strategy) =
@@ -6887,6 +7176,7 @@ mod tests {
None,
None,
None,
None,
0,
None,
None,
+67 -37
View File
@@ -20,7 +20,8 @@ use rustfs_filemeta::ReplicationStatusType;
use rustfs_utils::http::{
AMZ_BUCKET_REPLICATION_STATUS, SUFFIX_FORCE_DELETE, SUFFIX_REPLICATION_ACTUAL_OBJECT_SIZE, SUFFIX_REPLICATION_SSEC_CRC,
SUFFIX_SOURCE_DELETEMARKER, SUFFIX_SOURCE_MTIME, SUFFIX_SOURCE_REPLICATION_REQUEST, SUFFIX_SOURCE_VERSION_ID, get_header,
insert_header_map, is_encryption_metadata_key, is_internal_key,
insert_header_map,
metadata_compat::{MINIO_INTERNAL_PREFIX, RUSTFS_INTERNAL_PREFIX},
};
use rustfs_utils::http::{
AMZ_META_UNENCRYPTED_CONTENT_LENGTH, AMZ_META_UNENCRYPTED_CONTENT_MD5, AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER,
@@ -493,6 +494,49 @@ pub fn extract_metadata_from_mime_with_object_name(
}
}
fn starts_with_ignore_ascii_case(value: &str, prefix: &str) -> bool {
value
.get(..prefix.len())
.is_some_and(|head| head.eq_ignore_ascii_case(prefix))
}
fn should_skip_object_metadata_key(key: &str, value: &str, excluded_headers: &[&str]) -> bool {
const MINIO_ENCRYPTION_PREFIX: &str = "x-minio-encryption-";
const RUSTFS_ENCRYPTION_PREFIX: &str = "x-rustfs-encryption-";
const X_AMZ_PREFIX: &str = "x-amz-";
// Skip internal/reserved metadata (x-rustfs-internal-* or x-minio-internal-*)
if starts_with_ignore_ascii_case(key, RUSTFS_INTERNAL_PREFIX) || starts_with_ignore_ascii_case(key, MINIO_INTERNAL_PREFIX) {
return true;
}
// Skip internal encryption metadata (x-rustfs-encryption-* or x-minio-encryption-*)
if starts_with_ignore_ascii_case(key, RUSTFS_ENCRYPTION_PREFIX) || starts_with_ignore_ascii_case(key, MINIO_ENCRYPTION_PREFIX)
{
return true;
}
// Skip empty object lock values
if value.is_empty()
&& (key.eq_ignore_ascii_case(X_AMZ_OBJECT_LOCK_MODE.as_str())
|| key.eq_ignore_ascii_case(X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.as_str()))
{
return true;
}
if key.eq_ignore_ascii_case(AMZ_META_UNENCRYPTED_CONTENT_MD5) || key.eq_ignore_ascii_case(AMZ_META_UNENCRYPTED_CONTENT_LENGTH)
{
return true;
}
if excluded_headers.iter().any(|excluded| key.eq_ignore_ascii_case(excluded)) {
return true;
}
// User metadata is stored without the x-amz-meta- prefix by extract_metadata_from_mime.
starts_with_ignore_ascii_case(key, X_AMZ_PREFIX)
}
pub(crate) fn filter_object_metadata(metadata: &HashMap<String, String>) -> Option<HashMap<String, String>> {
// HTTP headers that should NOT be returned in the Metadata field.
// These headers are returned as separate response headers, not user metadata.
@@ -513,48 +557,19 @@ pub(crate) fn filter_object_metadata(metadata: &HashMap<String, String>) -> Opti
"x-amz-server-side-encryption-aws-kms-key-id",
];
let mut filtered_metadata = HashMap::new();
let mut filtered_metadata = None;
for (k, v) in metadata {
let lower_key = k.to_ascii_lowercase();
// Skip internal/reserved metadata (x-rustfs-internal-* or x-minio-internal-*)
if is_internal_key(&lower_key) {
continue;
}
// Skip internal encryption metadata (x-rustfs-encryption-* or x-minio-encryption-*)
if is_encryption_metadata_key(&lower_key) {
continue;
}
// Skip empty object lock values
if v.is_empty() && (k == &X_AMZ_OBJECT_LOCK_MODE.to_string() || k == &X_AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE.to_string()) {
continue;
}
// Skip UNENCRYPTED metadata placeholders
if k == AMZ_META_UNENCRYPTED_CONTENT_MD5 || k == AMZ_META_UNENCRYPTED_CONTENT_LENGTH {
continue;
}
// Skip excluded HTTP headers (they are returned as separate headers, not metadata)
if EXCLUDED_HEADERS.contains(&lower_key.as_str()) {
continue;
}
// Skip any x-amz-* headers that are not user metadata
// User metadata was stored WITHOUT the x-amz-meta- prefix by extract_metadata_from_mime
if lower_key.starts_with("x-amz-") {
if should_skip_object_metadata_key(k, v, EXCLUDED_HEADERS) {
continue;
}
// Include user-defined metadata (keys like "meta1", "custom-key", etc.)
filtered_metadata.insert(k.clone(), v.clone());
}
if filtered_metadata.is_empty() {
None
} else {
Some(filtered_metadata)
filtered_metadata
.get_or_insert_with(HashMap::new)
.insert(k.clone(), v.clone());
}
filtered_metadata
}
/// Detects content type from object name based on file extension.
@@ -1504,6 +1519,21 @@ mod tests {
assert!(filtered.is_none(), "content-type must not be exposed as user metadata");
}
#[test]
fn test_filter_object_metadata_excludes_case_insensitive_system_headers() {
let mut metadata = HashMap::new();
metadata.insert("Content-Type".to_string(), "application/octet-stream".to_string());
metadata.insert("X-Amz-Storage-Class".to_string(), "STANDARD".to_string());
metadata.insert("X-RustFS-Internal-Healing".to_string(), "true".to_string());
metadata.insert("X-Minio-Encryption-Iv".to_string(), "secret".to_string());
metadata.insert("custom-key".to_string(), "custom-value".to_string());
let filtered = filter_object_metadata(&metadata).expect("user metadata should remain");
assert_eq!(filtered.len(), 1);
assert_eq!(filtered.get("custom-key"), Some(&"custom-value".to_string()));
}
#[test]
fn test_detect_content_type_from_object_name() {
// Test Parquet files (our custom handling)
+37
View File
@@ -53,6 +53,8 @@
//! }
//! ```
#[cfg(not(test))]
use std::sync::OnceLock;
use std::time::{Duration, Instant};
use tokio_util::sync::CancellationToken;
use tracing::{debug, warn};
@@ -118,6 +120,19 @@ impl GetObjectTimeoutPolicy {
}
}
/// Load the process-wide GetObject timeout policy once for hot request paths.
#[cfg(not(test))]
pub fn cached_from_env() -> Self {
static POLICY: OnceLock<GetObjectTimeoutPolicy> = OnceLock::new();
POLICY.get_or_init(Self::from_env).clone()
}
/// Load the GetObject timeout policy from the current test environment.
#[cfg(test)]
pub fn cached_from_env() -> Self {
Self::from_env()
}
/// Check if timeout is enabled (timeout > 0).
pub fn is_timeout_enabled(&self) -> bool {
self.get_object_timeout > Duration::ZERO
@@ -475,6 +490,28 @@ mod tests {
assert_eq!(config.get_object_timeout, Duration::from_secs(30));
}
#[test]
fn test_cached_timeout_config_is_stable() {
let first = GetObjectTimeoutPolicy::cached_from_env();
let second = GetObjectTimeoutPolicy::cached_from_env();
assert_eq!(first.get_object_timeout, second.get_object_timeout);
assert_eq!(first.enable_dynamic_timeout, second.enable_dynamic_timeout);
assert_eq!(first.bytes_per_second, second.bytes_per_second);
assert_eq!(first.min_timeout, second.min_timeout);
assert_eq!(first.max_timeout, second.max_timeout);
}
#[test]
fn test_cached_timeout_config_honors_test_env_overrides() {
temp_env::with_var(rustfs_config::ENV_OBJECT_GET_TIMEOUT, Some("7"), || {
assert_eq!(GetObjectTimeoutPolicy::cached_from_env().get_object_timeout, Duration::from_secs(7));
});
temp_env::with_var(rustfs_config::ENV_OBJECT_GET_TIMEOUT, Some("11"), || {
assert_eq!(GetObjectTimeoutPolicy::cached_from_env().get_object_timeout, Duration::from_secs(11));
});
}
#[test]
fn test_timeout_config_is_enabled() {
let config = GetObjectTimeoutPolicy::default();
File diff suppressed because it is too large Load Diff
+151 -2
View File
@@ -38,6 +38,13 @@ COOLDOWN_SECS=0
BASELINE_CSV=""
EXTRA_ARGS=()
FAILED_FINAL_ROUNDS=0
SERVICE_METRICS_URL=""
SERVICE_METRICS_DIR=""
SERVICE_METRICS_CAPTURE_ATTEMPTS=3
SERVICE_METRICS_CAPTURE_RETRY_SECS=1
SERVICE_METRICS_CONNECT_TIMEOUT_SECS=2
SERVICE_METRICS_MAX_TIME_SECS=15
SERVICE_METRICS_FILTER_REGEX="rustfs_io_get_object_"
usage() {
cat <<'USAGE'
@@ -77,11 +84,23 @@ Enhanced options:
--round-cooldown-secs Compatibility alias for --cooldown-secs
--baseline-csv Baseline median CSV to compare
--extra-args Extra args appended to tool command, quoted as one string
--service-metrics-url Optional Prometheus scrape URL captured before/after each round attempt
--service-metrics-dir Output directory for per-round service metric snapshots
--service-metrics-attempts Capture attempts for each snapshot (default: 3)
--service-metrics-retry-secs Sleep seconds between failed capture attempts (default: 1)
--service-metrics-connect-timeout-secs
Curl connect timeout for each metrics capture (default: 2)
--service-metrics-max-time-secs
Curl max time for each metrics capture (default: 15)
--service-metrics-filter-regex
Regex for retained metrics lines after scrape
(default: rustfs_io_get_object_)
Output files:
round_results.csv One row per round attempt (with retry trace)
median_summary.csv Median metrics per object size
baseline_compare.csv Delta vs baseline (if --baseline-csv is set)
<service-metrics-dir>/*.prom Optional per-round service metric snapshots
Example:
scripts/run_object_batch_bench_enhanced.sh \
@@ -134,6 +153,13 @@ parse_args() {
--cooldown-secs) COOLDOWN_SECS="$2"; shift 2 ;;
--round-cooldown-secs) COOLDOWN_SECS="$2"; shift 2 ;;
--baseline-csv) BASELINE_CSV="$2"; shift 2 ;;
--service-metrics-url) SERVICE_METRICS_URL="$2"; shift 2 ;;
--service-metrics-dir) SERVICE_METRICS_DIR="$2"; shift 2 ;;
--service-metrics-attempts) SERVICE_METRICS_CAPTURE_ATTEMPTS="$2"; shift 2 ;;
--service-metrics-retry-secs) SERVICE_METRICS_CAPTURE_RETRY_SECS="$2"; shift 2 ;;
--service-metrics-connect-timeout-secs) SERVICE_METRICS_CONNECT_TIMEOUT_SECS="$2"; shift 2 ;;
--service-metrics-max-time-secs) SERVICE_METRICS_MAX_TIME_SECS="$2"; shift 2 ;;
--service-metrics-filter-regex) SERVICE_METRICS_FILTER_REGEX="$2"; shift 2 ;;
--extra-args)
# shellcheck disable=SC2206
EXTRA_ARGS=($2)
@@ -193,6 +219,17 @@ validate_args() {
validate_positive_int "$RETRY_PER_ROUND" "--retry-per-round"
validate_positive_int "$RETRY_SLEEP_SECS" "--retry-sleep-secs"
validate_nonnegative_int "$COOLDOWN_SECS" "--cooldown-secs"
validate_positive_int "$SERVICE_METRICS_CAPTURE_ATTEMPTS" "--service-metrics-attempts"
validate_nonnegative_int "$SERVICE_METRICS_CAPTURE_RETRY_SECS" "--service-metrics-retry-secs"
validate_positive_int "$SERVICE_METRICS_CONNECT_TIMEOUT_SECS" "--service-metrics-connect-timeout-secs"
validate_positive_int "$SERVICE_METRICS_MAX_TIME_SECS" "--service-metrics-max-time-secs"
if [[ -n "$SERVICE_METRICS_URL" && -z "$SERVICE_METRICS_DIR" ]]; then
echo "ERROR: --service-metrics-dir is required when --service-metrics-url is set" >&2
exit 1
fi
if [[ -n "$SERVICE_METRICS_URL" ]]; then
require_cmd curl
fi
if [[ "$TOOL" == "s3bench" ]]; then
validate_positive_int "$SAMPLES" "--samples"
fi
@@ -215,6 +252,9 @@ setup_output() {
OUT_DIR="target/bench/object-batch-enhanced-$(date +%Y%m%d-%H%M%S)"
fi
mkdir -p "$OUT_DIR/logs"
if [[ -n "$SERVICE_METRICS_URL" ]]; then
mkdir -p "$SERVICE_METRICS_DIR"
fi
ROUND_CSV="$OUT_DIR/round_results.csv"
MEDIAN_CSV="$OUT_DIR/median_summary.csv"
@@ -334,6 +374,113 @@ extract_metrics() {
echo "$throughput,${reqps_num:-N/A},$latency,$req_p90,$req_p99"
}
metric_snapshot_token() {
local size="$1"
local round="$2"
local attempt="$3"
local safe_size
safe_size="$(printf '%s' "$size" | tr -c '[:alnum:]_.-' '_')"
printf '%s_%s_r%s_a%s' "$TOOL" "$safe_size" "$round" "$attempt"
}
filter_service_metrics_snapshot() {
local input_file="$1"
local output_file="$2"
if [[ -z "$SERVICE_METRICS_FILTER_REGEX" ]]; then
mv "$input_file" "$output_file"
return 0
fi
awk -v pattern="$SERVICE_METRICS_FILTER_REGEX" '$0 ~ pattern { print }' "$input_file" >"$output_file"
if [[ ! -s "$output_file" ]]; then
mv "$input_file" "$output_file"
else
rm -f "$input_file"
fi
}
capture_round_service_metrics() {
local size="$1"
local round="$2"
local attempt="$3"
local phase="$4"
if [[ -z "$SERVICE_METRICS_URL" ]]; then
return 0
fi
local token snapshot_file status_file tmp_file filtered_file capture_attempt raw_bytes snapshot_bytes
token="$(metric_snapshot_token "$size" "$round" "$attempt")"
snapshot_file="${SERVICE_METRICS_DIR}/${token}_${phase}.prom"
status_file="${SERVICE_METRICS_DIR}/${token}_${phase}.status"
tmp_file="${snapshot_file}.tmp"
filtered_file="${snapshot_file}.filtered.tmp"
if [[ "$DRY_RUN" == "true" ]]; then
: >"$snapshot_file"
cat >"$status_file" <<EOF
size=${size}
round=${round}
attempt=${attempt}
phase=${phase}
status=not_run_dry_run
url=${SERVICE_METRICS_URL}
filter_regex=${SERVICE_METRICS_FILTER_REGEX}
EOF
return 0
fi
for ((capture_attempt=1; capture_attempt<=SERVICE_METRICS_CAPTURE_ATTEMPTS; capture_attempt++)); do
if curl -fsS --noproxy '*' \
--connect-timeout "$SERVICE_METRICS_CONNECT_TIMEOUT_SECS" \
--max-time "$SERVICE_METRICS_MAX_TIME_SECS" \
"$SERVICE_METRICS_URL" >"$tmp_file"; then
if [[ -s "$tmp_file" ]]; then
raw_bytes="$(wc -c <"$tmp_file" | tr -d '[:space:]')"
filter_service_metrics_snapshot "$tmp_file" "$filtered_file"
mv "$filtered_file" "$snapshot_file"
snapshot_bytes="$(wc -c <"$snapshot_file" | tr -d '[:space:]')"
cat >"$status_file" <<EOF
size=${size}
round=${round}
attempt=${attempt}
phase=${phase}
status=ok
capture_attempt=${capture_attempt}
url=${SERVICE_METRICS_URL}
connect_timeout_secs=${SERVICE_METRICS_CONNECT_TIMEOUT_SECS}
max_time_secs=${SERVICE_METRICS_MAX_TIME_SECS}
filter_regex=${SERVICE_METRICS_FILTER_REGEX}
raw_bytes=${raw_bytes}
snapshot_bytes=${snapshot_bytes}
EOF
return 0
fi
fi
rm -f "$tmp_file" "$filtered_file"
if (( capture_attempt < SERVICE_METRICS_CAPTURE_ATTEMPTS && SERVICE_METRICS_CAPTURE_RETRY_SECS > 0 )); then
sleep "$SERVICE_METRICS_CAPTURE_RETRY_SECS"
fi
done
: >"$snapshot_file"
cat >"$status_file" <<EOF
size=${size}
round=${round}
attempt=${attempt}
phase=${phase}
status=capture_failed
capture_attempts=${SERVICE_METRICS_CAPTURE_ATTEMPTS}
url=${SERVICE_METRICS_URL}
connect_timeout_secs=${SERVICE_METRICS_CONNECT_TIMEOUT_SECS}
max_time_secs=${SERVICE_METRICS_MAX_TIME_SECS}
filter_regex=${SERVICE_METRICS_FILTER_REGEX}
EOF
echo "WARN: failed to capture service metrics size=${size} round=${round} attempt=${attempt} phase=${phase}" >&2
}
median_from_numbers() {
local values="$1"
local count
@@ -364,6 +511,7 @@ run_one_attempt() {
local status="ok"
local exit_code=0
local started_at_utc finished_at_utc
capture_round_service_metrics "$size" "$round" "$attempt" before
started_at_utc="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
if [[ "$TOOL" == "warp" ]]; then
@@ -383,7 +531,7 @@ run_one_attempt() {
if [[ "$INSECURE" == "true" ]]; then
cmd+=("--insecure")
fi
if [[ ${EXTRA_ARGS[@]+_} ]]; then
if ((${#EXTRA_ARGS[@]} > 0)); then
cmd+=("${EXTRA_ARGS[@]}")
fi
@@ -414,7 +562,7 @@ run_one_attempt() {
if [[ "$INSECURE" == "true" ]]; then
cmd+=("-insecure")
fi
if [[ ${EXTRA_ARGS[@]+_} ]]; then
if ((${#EXTRA_ARGS[@]} > 0)); then
cmd+=("${EXTRA_ARGS[@]}")
fi
@@ -432,6 +580,7 @@ run_one_attempt() {
fi
fi
finished_at_utc="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
capture_round_service_metrics "$size" "$round" "$attempt" after
local metrics throughput_human reqps latency_human throughput_bps latency_ms req_p90_human req_p90_ms req_p99_human req_p99_ms
if [[ "$DRY_RUN" == "true" ]]; then