mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 08:18:18 +00:00
fix(tier): handle mutation intent CAS races (#5227)
* test(scripts): expand internode grpc ab env coverage * fix(tier): handle mutation intent CAS races Make tier mutation intent advancement retry idempotently when an If-Match CAS loses to a matching committed peer update. Expand four-node inline fallback E2E coverage for mixed msgpack controls, transition readiness evidence, and per-node environment overrides. Co-Authored-By: heihutu <heihutu@gmail.com> --------- Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
Generated
+61
-45
@@ -850,9 +850,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-config"
|
name = "aws-config"
|
||||||
version = "1.10.0"
|
version = "1.10.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "701418aa459dac33e50a0f8e818e5662a16bc018a6ac7423659b70f3799d67a8"
|
checksum = "1b180a3c8b55960db3426d8964b8745e652466a1a49fe1a2eda828046d30b5e4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-credential-types",
|
"aws-credential-types",
|
||||||
"aws-runtime",
|
"aws-runtime",
|
||||||
@@ -917,9 +917,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-runtime"
|
name = "aws-runtime"
|
||||||
version = "1.9.0"
|
version = "1.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a6b50a43f3ccdf331521c6d6c68b7cc9668b6e09d439ebda9569df5722324d76"
|
checksum = "c9007227e10b5fed2f3e0a2beff489211e2b5604c400b7a9d5d81ca9d64c24bb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-credential-types",
|
"aws-credential-types",
|
||||||
"aws-sigv4",
|
"aws-sigv4",
|
||||||
@@ -945,9 +945,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-sdk-s3"
|
name = "aws-sdk-s3"
|
||||||
version = "1.139.0"
|
version = "1.140.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a159b9721a6a41468f967d1029bece78f410b0beb0594498435deb6ff72bfe48"
|
checksum = "e9660cf991e512fbe6094f1041ff3d3282bc5aeeeb6184e8de437ec2de024a10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"aws-credential-types",
|
"aws-credential-types",
|
||||||
@@ -982,9 +982,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-sdk-sso"
|
name = "aws-sdk-sso"
|
||||||
version = "1.104.0"
|
version = "1.105.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b53416d16c278234845392e38d93bd4481d2f09daa0f005a2277f0aa91f59c22"
|
checksum = "6ffd0fbe7873cb548a7aa60f9573c268fff94155397fd4f14dc9f1ecaaab8516"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"aws-credential-types",
|
"aws-credential-types",
|
||||||
@@ -1008,9 +1008,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-sdk-ssooidc"
|
name = "aws-sdk-ssooidc"
|
||||||
version = "1.106.0"
|
version = "1.107.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cc9b706c3305ed0285d5b1b696c747aa34950f830fb03e3e6c76890f99b9f188"
|
checksum = "175763eb222a46377df7aa257a3bca980ab3e96703fefc8f4d0b8da6ad2e254c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"aws-credential-types",
|
"aws-credential-types",
|
||||||
@@ -1034,9 +1034,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-sdk-sts"
|
name = "aws-sdk-sts"
|
||||||
version = "1.109.0"
|
version = "1.110.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32d214cdfa5bbe17f117e76a7643fadf32a5234fb597322ef8b1fb4b2f17dbbd"
|
checksum = "dd8b14781dfbff48984017d57167b6ea0b6471c6920ec52b44a2677c7feb3c13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"aws-credential-types",
|
"aws-credential-types",
|
||||||
@@ -1210,9 +1210,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-smithy-runtime"
|
name = "aws-smithy-runtime"
|
||||||
version = "1.12.0"
|
version = "1.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bea94a9ff8464016338c851e24b472d7131c388c88898a502e781815b2ee6045"
|
checksum = "07505b34e8f4b3591a4fa69e9792b52289b95488dbbc68c3c0075b7bedb245e1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-smithy-async",
|
"aws-smithy-async",
|
||||||
"aws-smithy-http",
|
"aws-smithy-http",
|
||||||
@@ -1236,9 +1236,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-smithy-runtime-api"
|
name = "aws-smithy-runtime-api"
|
||||||
version = "1.13.0"
|
version = "1.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "22ed1ebe6e0a95ea84570225f5a8208dec4b8f77e61a9b0d6f51773fcb4612f0"
|
checksum = "3b98f2e1fd67ec06618f9c291e5e495a468e60519e44c9c1979cd0521f3affdb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-smithy-async",
|
"aws-smithy-async",
|
||||||
"aws-smithy-runtime-api-macros",
|
"aws-smithy-runtime-api-macros",
|
||||||
@@ -1528,7 +1528,7 @@ version = "0.10.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array 0.14.9",
|
"generic-array 0.14.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1547,7 +1547,7 @@ version = "0.3.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
|
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array 0.14.9",
|
"generic-array 0.14.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1760,9 +1760,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.3.0"
|
version = "1.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8"
|
checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"jobserver",
|
"jobserver",
|
||||||
@@ -1870,7 +1870,7 @@ version = "0.4.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crypto-common 0.1.6",
|
"crypto-common 0.1.7",
|
||||||
"inout 0.1.4",
|
"inout 0.1.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2328,7 +2328,7 @@ version = "0.5.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
|
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array 0.14.9",
|
"generic-array 0.14.7",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
"subtle",
|
"subtle",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
@@ -2353,11 +2353,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.6"
|
version = "0.1.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array 0.14.9",
|
"generic-array 0.14.7",
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3554,7 +3554,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer 0.10.4",
|
"block-buffer 0.10.4",
|
||||||
"const-oid 0.9.6",
|
"const-oid 0.9.6",
|
||||||
"crypto-common 0.1.6",
|
"crypto-common 0.1.7",
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3796,9 +3796,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.16.0"
|
version = "1.17.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
|
checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "elliptic-curve"
|
name = "elliptic-curve"
|
||||||
@@ -3810,7 +3810,7 @@ dependencies = [
|
|||||||
"crypto-bigint 0.5.5",
|
"crypto-bigint 0.5.5",
|
||||||
"digest 0.10.7",
|
"digest 0.10.7",
|
||||||
"ff 0.13.1",
|
"ff 0.13.1",
|
||||||
"generic-array 0.14.9",
|
"generic-array 0.14.7",
|
||||||
"group 0.13.0",
|
"group 0.13.0",
|
||||||
"hkdf 0.12.4",
|
"hkdf 0.12.4",
|
||||||
"pem-rfc7468 0.7.0",
|
"pem-rfc7468 0.7.0",
|
||||||
@@ -4256,9 +4256,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.9"
|
version = "0.14.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
|
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
"version_check",
|
"version_check",
|
||||||
@@ -4271,7 +4271,7 @@ version = "1.4.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ab4e5aa225bc56696909483320f0ff9b600f1a971b52e07a17d70f3d9b43254b"
|
checksum = "ab4e5aa225bc56696909483320f0ff9b600f1a971b52e07a17d70f3d9b43254b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array 0.14.9",
|
"generic-array 0.14.7",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
@@ -4383,7 +4383,7 @@ dependencies = [
|
|||||||
"hex",
|
"hex",
|
||||||
"hmac 0.13.0",
|
"hmac 0.13.0",
|
||||||
"http 1.4.2",
|
"http 1.4.2",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken 10.4.0",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"rustls",
|
"rustls",
|
||||||
@@ -5271,7 +5271,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-padding 0.3.3",
|
"block-padding 0.3.3",
|
||||||
"generic-array 0.14.9",
|
"generic-array 0.14.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5535,6 +5535,22 @@ name = "jsonwebtoken"
|
|||||||
version = "10.4.0"
|
version = "10.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc"
|
checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc"
|
||||||
|
dependencies = [
|
||||||
|
"aws-lc-rs",
|
||||||
|
"base64 0.22.1",
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"js-sys",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"signature 2.2.0",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonwebtoken"
|
||||||
|
version = "11.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "881733cbc631fc9e472e24447ce32a64bedf2da498d6d8570b08edc87de71f65"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
@@ -6686,7 +6702,7 @@ version = "5.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d"
|
checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.21.7",
|
||||||
"chrono",
|
"chrono",
|
||||||
"getrandom 0.2.17",
|
"getrandom 0.2.17",
|
||||||
"http 1.4.2",
|
"http 1.4.2",
|
||||||
@@ -7808,7 +7824,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
|
checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"itertools 0.14.0",
|
"itertools 0.10.5",
|
||||||
"log",
|
"log",
|
||||||
"multimap",
|
"multimap",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -7828,7 +7844,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042"
|
checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"itertools 0.14.0",
|
"itertools 0.10.5",
|
||||||
"log",
|
"log",
|
||||||
"multimap",
|
"multimap",
|
||||||
"petgraph 0.8.3",
|
"petgraph 0.8.3",
|
||||||
@@ -7849,7 +7865,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
|
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"itertools 0.14.0",
|
"itertools 0.10.5",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
@@ -7862,7 +7878,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf"
|
checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"itertools 0.14.0",
|
"itertools 0.10.5",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
@@ -9061,7 +9077,7 @@ dependencies = [
|
|||||||
"argon2",
|
"argon2",
|
||||||
"base64-simd",
|
"base64-simd",
|
||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken 11.0.0",
|
||||||
"pbkdf2 0.13.0",
|
"pbkdf2 0.13.0",
|
||||||
"rand 0.10.2",
|
"rand 0.10.2",
|
||||||
"rsa 0.10.0-rc.18",
|
"rsa 0.10.0-rc.18",
|
||||||
@@ -9293,7 +9309,7 @@ dependencies = [
|
|||||||
"base64-simd",
|
"base64-simd",
|
||||||
"futures",
|
"futures",
|
||||||
"http 1.4.2",
|
"http 1.4.2",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken 11.0.0",
|
||||||
"moka",
|
"moka",
|
||||||
"openidconnect",
|
"openidconnect",
|
||||||
"pollster",
|
"pollster",
|
||||||
@@ -9655,7 +9671,7 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"futures",
|
"futures",
|
||||||
"ipnetwork",
|
"ipnetwork",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken 11.0.0",
|
||||||
"moka",
|
"moka",
|
||||||
"pollster",
|
"pollster",
|
||||||
"proptest",
|
"proptest",
|
||||||
@@ -10273,7 +10289,7 @@ dependencies = [
|
|||||||
"security-framework",
|
"security-framework",
|
||||||
"security-framework-sys",
|
"security-framework-sys",
|
||||||
"webpki-root-certs",
|
"webpki-root-certs",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -10483,7 +10499,7 @@ checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"base16ct 0.2.0",
|
"base16ct 0.2.0",
|
||||||
"der 0.7.10",
|
"der 0.7.10",
|
||||||
"generic-array 0.14.9",
|
"generic-array 0.14.7",
|
||||||
"pkcs8 0.10.2",
|
"pkcs8 0.10.2",
|
||||||
"subtle",
|
"subtle",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
@@ -11473,7 +11489,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.3.4",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
|
|||||||
+4
-4
@@ -196,7 +196,7 @@ blake2 = "=0.11.0-rc.6"
|
|||||||
chacha20poly1305 = { version = "=0.11.0" }
|
chacha20poly1305 = { version = "=0.11.0" }
|
||||||
crc-fast = "1.10.0"
|
crc-fast = "1.10.0"
|
||||||
hmac = { version = "0.13.0" }
|
hmac = { version = "0.13.0" }
|
||||||
jsonwebtoken = { version = "10.4.0" }
|
jsonwebtoken = { version = "11.0.0" }
|
||||||
openidconnect = { default-features = false, version = "4.0" }
|
openidconnect = { default-features = false, version = "4.0" }
|
||||||
pbkdf2 = "0.13.0"
|
pbkdf2 = "0.13.0"
|
||||||
rsa = { version = "=0.10.0-rc.18" }
|
rsa = { version = "=0.10.0-rc.18" }
|
||||||
@@ -225,11 +225,11 @@ arc-swap = "1.9.2"
|
|||||||
astral-tokio-tar = "0.6.4"
|
astral-tokio-tar = "0.6.4"
|
||||||
atoi = "3.1.0"
|
atoi = "3.1.0"
|
||||||
atomic_enum = "0.3.0"
|
atomic_enum = "0.3.0"
|
||||||
aws-config = { version = "1.10.0" }
|
aws-config = { version = "1.10.1" }
|
||||||
aws-credential-types = { version = "1.3.0" }
|
aws-credential-types = { version = "1.3.0" }
|
||||||
aws-sdk-s3 = { default-features = false, version = "1.139.0" }
|
aws-sdk-s3 = { default-features = false, version = "1.140.0" }
|
||||||
aws-smithy-http-client = { default-features = false, version = "1.2.0" }
|
aws-smithy-http-client = { default-features = false, version = "1.2.0" }
|
||||||
aws-smithy-runtime-api = { version = "1.13.0" }
|
aws-smithy-runtime-api = { version = "1.14.0" }
|
||||||
aws-smithy-types = { version = "1.6.1" }
|
aws-smithy-types = { version = "1.6.1" }
|
||||||
base64 = "0.23.0"
|
base64 = "0.23.0"
|
||||||
base64-simd = "0.8.0"
|
base64-simd = "0.8.0"
|
||||||
|
|||||||
@@ -892,6 +892,7 @@ pub struct RustFSTestClusterEnvironment {
|
|||||||
pub access_key: String,
|
pub access_key: String,
|
||||||
pub secret_key: String,
|
pub secret_key: String,
|
||||||
pub extra_env: Vec<(String, String)>,
|
pub extra_env: Vec<(String, String)>,
|
||||||
|
pub node_extra_env: Vec<Vec<(String, String)>>,
|
||||||
pub topology: ClusterTopology,
|
pub topology: ClusterTopology,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -990,6 +991,7 @@ impl RustFSTestClusterEnvironment {
|
|||||||
access_key: "rustfs-cluster-test-access".to_string(),
|
access_key: "rustfs-cluster-test-access".to_string(),
|
||||||
secret_key: "rustfs-cluster-test-secret".to_string(),
|
secret_key: "rustfs-cluster-test-secret".to_string(),
|
||||||
extra_env,
|
extra_env,
|
||||||
|
node_extra_env: vec![Vec::new(); topology.node_count],
|
||||||
topology,
|
topology,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1003,6 +1005,22 @@ impl RustFSTestClusterEnvironment {
|
|||||||
self.extra_env.push((key.into(), value.into()));
|
self.extra_env.push((key.into(), value.into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add an extra environment variable applied to a single cluster node.
|
||||||
|
pub fn set_node_env<K, V>(
|
||||||
|
&mut self,
|
||||||
|
node_idx: usize,
|
||||||
|
key: K,
|
||||||
|
value: V,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
|
||||||
|
where
|
||||||
|
K: Into<String>,
|
||||||
|
V: Into<String>,
|
||||||
|
{
|
||||||
|
self.ensure_node_index(node_idx)?;
|
||||||
|
self.node_extra_env[node_idx].push((key.into(), value.into()));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn ensure_node_index(&self, node_idx: usize) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
fn ensure_node_index(&self, node_idx: usize) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
if node_idx >= self.nodes.len() {
|
if node_idx >= self.nodes.len() {
|
||||||
return Err(format!("node_idx {node_idx} is invalid").into());
|
return Err(format!("node_idx {node_idx} is invalid").into());
|
||||||
@@ -1089,6 +1107,9 @@ impl RustFSTestClusterEnvironment {
|
|||||||
for (key, value) in &self.extra_env {
|
for (key, value) in &self.extra_env {
|
||||||
command.env(key, value);
|
command.env(key, value);
|
||||||
}
|
}
|
||||||
|
for (key, value) in &self.node_extra_env[i] {
|
||||||
|
command.env(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
let process = command.current_dir(&node.data_dir).spawn()?;
|
let process = command.current_dir(&node.data_dir).spawn()?;
|
||||||
|
|
||||||
@@ -1130,6 +1151,9 @@ impl RustFSTestClusterEnvironment {
|
|||||||
for (key, value) in &self.extra_env {
|
for (key, value) in &self.extra_env {
|
||||||
command.env(key, value);
|
command.env(key, value);
|
||||||
}
|
}
|
||||||
|
for (key, value) in &self.node_extra_env[node_idx] {
|
||||||
|
command.env(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
let process = command.current_dir(&node.data_dir).spawn()?;
|
let process = command.current_dir(&node.data_dir).spawn()?;
|
||||||
node.process = Some(process);
|
node.process = Some(process);
|
||||||
@@ -1371,6 +1395,7 @@ mod tests {
|
|||||||
access_key: DEFAULT_ACCESS_KEY.to_string(),
|
access_key: DEFAULT_ACCESS_KEY.to_string(),
|
||||||
secret_key: DEFAULT_SECRET_KEY.to_string(),
|
secret_key: DEFAULT_SECRET_KEY.to_string(),
|
||||||
extra_env: Vec::new(),
|
extra_env: Vec::new(),
|
||||||
|
node_extra_env: vec![Vec::new(); topology.node_count],
|
||||||
topology,
|
topology,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1455,4 +1480,24 @@ mod tests {
|
|||||||
assert!(ClusterTopology::single_pool_multidrive(4, 4).validate().is_ok());
|
assert!(ClusterTopology::single_pool_multidrive(4, 4).validate().is_ok());
|
||||||
assert!(ClusterTopology::single_pool_multidrive(1, 1).validate().is_ok());
|
assert!(ClusterTopology::single_pool_multidrive(1, 1).validate().is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cluster_node_env_supports_per_node_overrides() {
|
||||||
|
let mut env = fake_cluster(ClusterTopology::single_pool(4));
|
||||||
|
env.set_node_env(2, "RUSTFS_INTERNODE_RPC_MSGPACK_ONLY", "true").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
env.node_extra_env[2].as_slice(),
|
||||||
|
[("RUSTFS_INTERNODE_RPC_MSGPACK_ONLY".to_string(), "true".to_string())]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cluster_node_env_rejects_invalid_index() {
|
||||||
|
let mut env = fake_cluster(ClusterTopology::single_pool(4));
|
||||||
|
let err = env
|
||||||
|
.set_node_env(4, "RUSTFS_INTERNODE_RPC_MSGPACK_ONLY", "true")
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string();
|
||||||
|
assert!(err.contains("invalid"), "unexpected error: {err}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ type MetricValues = Arc<Mutex<BTreeMap<String, MetricPointVersions>>>;
|
|||||||
|
|
||||||
const KIB: usize = 1024;
|
const KIB: usize = 1024;
|
||||||
const READER_PATH_COUNTER: &str = "rustfs_io_get_object_reader_path_by_size_total";
|
const READER_PATH_COUNTER: &str = "rustfs_io_get_object_reader_path_by_size_total";
|
||||||
|
const MSGPACK_JSON_FALLBACK_COUNTER: &str = "rustfs_system_network_internode_msgpack_json_fallback_total";
|
||||||
|
const DIRECTION_LABEL: &str = "direction";
|
||||||
|
const MESSAGE_LABEL: &str = "message";
|
||||||
const INLINE_DIRECT: &str = "inline_direct";
|
const INLINE_DIRECT: &str = "inline_direct";
|
||||||
const LEGACY_DUPLEX: &str = "legacy_duplex";
|
const LEGACY_DUPLEX: &str = "legacy_duplex";
|
||||||
const EMPTY: &str = "empty";
|
const EMPTY: &str = "empty";
|
||||||
@@ -74,11 +77,30 @@ const ENCRYPTED: &str = "encrypted";
|
|||||||
const COMPRESSED: &str = "compressed";
|
const COMPRESSED: &str = "compressed";
|
||||||
const RANGE: &str = "range";
|
const RANGE: &str = "range";
|
||||||
const REMOTE: &str = "remote";
|
const REMOTE: &str = "remote";
|
||||||
|
const FALLBACK_REQUEST_DIRECTION: &str = "request";
|
||||||
|
const FALLBACK_RESPONSE_DIRECTION: &str = "response";
|
||||||
const MPU_PART_1_SIZE: usize = 5 * 1024 * 1024;
|
const MPU_PART_1_SIZE: usize = 5 * 1024 * 1024;
|
||||||
const MPU_PART_2_SIZE: usize = 16 * KIB;
|
const MPU_PART_2_SIZE: usize = 16 * KIB;
|
||||||
const TIER_NAME: &str = "COLDTIER";
|
const TIER_NAME: &str = "COLDTIER";
|
||||||
const TIER_BUCKET: &str = "inline-fallback-cold-tier";
|
const TIER_BUCKET: &str = "inline-fallback-cold-tier";
|
||||||
const TIER_PREFIX: &str = "tiered";
|
const TIER_PREFIX: &str = "tiered";
|
||||||
|
const MSGPACK_FALLBACK_CONTROL_SERIES: [(&str, &str); 4] = [
|
||||||
|
(FALLBACK_REQUEST_DIRECTION, "ReadMultipleReq"),
|
||||||
|
(FALLBACK_RESPONSE_DIRECTION, "ReadMultipleResp"),
|
||||||
|
(FALLBACK_REQUEST_DIRECTION, "BatchReadVersionReq"),
|
||||||
|
(FALLBACK_RESPONSE_DIRECTION, "BatchReadVersionResp"),
|
||||||
|
];
|
||||||
|
const READER_SIZE_BUCKETS: [&str; 9] = [
|
||||||
|
"le_4kib",
|
||||||
|
"le_16kib",
|
||||||
|
"le_64kib",
|
||||||
|
"le_128kib",
|
||||||
|
"le_192kib",
|
||||||
|
"le_256kib",
|
||||||
|
"le_512kib",
|
||||||
|
"le_1mib",
|
||||||
|
"gt_1mib",
|
||||||
|
];
|
||||||
|
|
||||||
struct BoundaryCase {
|
struct BoundaryCase {
|
||||||
label: String,
|
label: String,
|
||||||
@@ -113,6 +135,7 @@ impl VersionState {
|
|||||||
struct OtlpMetricCollector {
|
struct OtlpMetricCollector {
|
||||||
endpoint: String,
|
endpoint: String,
|
||||||
values: MetricValues,
|
values: MetricValues,
|
||||||
|
fallback_values: MetricValues,
|
||||||
task: JoinHandle<()>,
|
task: JoinHandle<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,24 +144,32 @@ impl OtlpMetricCollector {
|
|||||||
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||||
let endpoint = format!("http://{}/v1/metrics", listener.local_addr()?);
|
let endpoint = format!("http://{}/v1/metrics", listener.local_addr()?);
|
||||||
let values = Arc::new(Mutex::new(BTreeMap::new()));
|
let values = Arc::new(Mutex::new(BTreeMap::new()));
|
||||||
|
let fallback_values = Arc::new(Mutex::new(BTreeMap::new()));
|
||||||
let task_values = values.clone();
|
let task_values = values.clone();
|
||||||
|
let task_fallback_values = fallback_values.clone();
|
||||||
let task = tokio::spawn(async move {
|
let task = tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
let Ok((stream, _)) = listener.accept().await else {
|
let Ok((stream, _)) = listener.accept().await else {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
let values = task_values.clone();
|
let values = task_values.clone();
|
||||||
|
let fallback_values = task_fallback_values.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let _ = hyper::server::conn::http1::Builder::new()
|
let _ = hyper::server::conn::http1::Builder::new()
|
||||||
.serve_connection(
|
.serve_connection(
|
||||||
TokioIo::new(stream),
|
TokioIo::new(stream),
|
||||||
service_fn(move |request| handle_metric_export(request, values.clone())),
|
service_fn(move |request| handle_metric_export(request, values.clone(), fallback_values.clone())),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Ok(Self { endpoint, values, task })
|
Ok(Self {
|
||||||
|
endpoint,
|
||||||
|
values,
|
||||||
|
fallback_values,
|
||||||
|
task,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn reader_path_total(&self, path: &str, object_class: &str, size_bucket: &str) -> u64 {
|
async fn reader_path_total(&self, path: &str, object_class: &str, size_bucket: &str) -> u64 {
|
||||||
@@ -166,6 +197,15 @@ impl OtlpMetricCollector {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn msgpack_json_fallback_totals(&self) -> BTreeMap<String, u64> {
|
||||||
|
self.fallback_values
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.iter()
|
||||||
|
.map(|(key, points)| (key.clone(), points.values().map(|(_, value)| value).sum()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
async fn reader_path_totals_for(&self, object_class: &str, size_bucket: &str) -> BTreeMap<String, u64> {
|
async fn reader_path_totals_for(&self, object_class: &str, size_bucket: &str) -> BTreeMap<String, u64> {
|
||||||
let values = self.values.lock().await;
|
let values = self.values.lock().await;
|
||||||
[INLINE_DIRECT, LEGACY_DUPLEX, EMPTY, REMOTE_TRANSITION]
|
[INLINE_DIRECT, LEGACY_DUPLEX, EMPTY, REMOTE_TRANSITION]
|
||||||
@@ -231,7 +271,11 @@ impl Drop for OtlpMetricCollector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_metric_export(request: Request<Incoming>, values: MetricValues) -> Result<Response<Full<Bytes>>, Infallible> {
|
async fn handle_metric_export(
|
||||||
|
request: Request<Incoming>,
|
||||||
|
values: MetricValues,
|
||||||
|
fallback_values: MetricValues,
|
||||||
|
) -> Result<Response<Full<Bytes>>, Infallible> {
|
||||||
if request.uri().path() != "/v1/metrics" {
|
if request.uri().path() != "/v1/metrics" {
|
||||||
return Ok(response(StatusCode::NOT_FOUND));
|
return Ok(response(StatusCode::NOT_FOUND));
|
||||||
}
|
}
|
||||||
@@ -261,7 +305,9 @@ async fn handle_metric_export(request: Request<Incoming>, values: MetricValues)
|
|||||||
match ExportMetricsServiceRequest::decode(payload.as_slice()) {
|
match ExportMetricsServiceRequest::decode(payload.as_slice()) {
|
||||||
Ok(export) => {
|
Ok(export) => {
|
||||||
let mut values = values.lock().await;
|
let mut values = values.lock().await;
|
||||||
|
let mut fallback_values = fallback_values.lock().await;
|
||||||
record_reader_path_metrics(&export, &mut values);
|
record_reader_path_metrics(&export, &mut values);
|
||||||
|
record_msgpack_fallback_metrics(&export, &mut fallback_values);
|
||||||
Ok(response(StatusCode::OK))
|
Ok(response(StatusCode::OK))
|
||||||
}
|
}
|
||||||
Err(_) => Ok(response(StatusCode::BAD_REQUEST)),
|
Err(_) => Ok(response(StatusCode::BAD_REQUEST)),
|
||||||
@@ -326,6 +372,66 @@ fn record_reader_path_metric(metric: &Metric, values: &mut BTreeMap<String, Metr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn record_msgpack_fallback_metrics(export: &ExportMetricsServiceRequest, values: &mut BTreeMap<String, MetricPointVersions>) {
|
||||||
|
for resource_metrics in &export.resource_metrics {
|
||||||
|
for scope_metrics in &resource_metrics.scope_metrics {
|
||||||
|
for metric in &scope_metrics.metrics {
|
||||||
|
if metric.name != MSGPACK_JSON_FALLBACK_COUNTER {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(metric::Data::Sum(sum)) = &metric.data else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
for point in &sum.data_points {
|
||||||
|
let Some(direction) = attribute_string(&point.attributes, DIRECTION_LABEL) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(message) = attribute_string(&point.attributes, MESSAGE_LABEL) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(number_data_point::Value::AsInt(value)) = point.value.as_ref() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let value = u64::try_from(*value).unwrap_or_default();
|
||||||
|
values
|
||||||
|
.entry(msgpack_fallback_metric_key(direction, message))
|
||||||
|
.or_default()
|
||||||
|
.entry(point.start_time_unix_nano)
|
||||||
|
.and_modify(|current| {
|
||||||
|
if point.time_unix_nano >= current.0 {
|
||||||
|
*current = (point.time_unix_nano, value);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.or_insert((point.time_unix_nano, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn msgpack_fallback_metric_key(direction: &str, message: &str) -> String {
|
||||||
|
format!("{direction}\u{1f}{message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn assert_msgpack_fallback_unchanged(
|
||||||
|
collector: &OtlpMetricCollector,
|
||||||
|
before: &BTreeMap<String, u64>,
|
||||||
|
series: &[(&str, &str)],
|
||||||
|
) -> TestResult {
|
||||||
|
let after = collector.msgpack_json_fallback_totals().await;
|
||||||
|
for &(direction, message) in series {
|
||||||
|
let key = msgpack_fallback_metric_key(direction, message);
|
||||||
|
assert_eq!(
|
||||||
|
before.get(&key).copied().unwrap_or_default(),
|
||||||
|
after.get(&key).copied().unwrap_or_default(),
|
||||||
|
"fallback counter changed for {direction}/{message}: before={:?}, after={:?}",
|
||||||
|
before.get(&key),
|
||||||
|
after.get(&key),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn attribute_string<'a>(attributes: &'a [KeyValue], wanted_key: &str) -> Option<&'a str> {
|
fn attribute_string<'a>(attributes: &'a [KeyValue], wanted_key: &str) -> Option<&'a str> {
|
||||||
attributes.iter().find_map(|attribute| {
|
attributes.iter().find_map(|attribute| {
|
||||||
if attribute.key != wanted_key {
|
if attribute.key != wanted_key {
|
||||||
@@ -439,6 +545,17 @@ fn configure_reader_metric_cluster(cluster: &mut RustFSTestClusterEnvironment, c
|
|||||||
cluster.set_env("RUSTFS_GET_SMALL_OBJECT_DIRECT_MEMORY", "false");
|
cluster.set_env("RUSTFS_GET_SMALL_OBJECT_DIRECT_MEMORY", "false");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn configure_mixed_msgpack_cluster(cluster: &mut RustFSTestClusterEnvironment, collector: &OtlpMetricCollector) -> TestResult {
|
||||||
|
configure_reader_metric_cluster(cluster, collector);
|
||||||
|
cluster.set_node_env(0, "RUSTFS_INTERNODE_RPC_MSGPACK_ONLY", "true")?;
|
||||||
|
cluster.set_node_env(0, "RUSTFS_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED", "true")?;
|
||||||
|
cluster.set_node_env(1, "RUSTFS_INTERNODE_RPC_MSGPACK_ONLY", "true")?;
|
||||||
|
cluster.set_node_env(1, "RUSTFS_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED", "true")?;
|
||||||
|
cluster.set_node_env(2, "RUSTFS_INTERNODE_RPC_MSGPACK_ONLY", "false")?;
|
||||||
|
cluster.set_node_env(3, "RUSTFS_INTERNODE_RPC_MSGPACK_ONLY", "false")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn assert_case(
|
async fn assert_case(
|
||||||
cluster: &RustFSTestClusterEnvironment,
|
cluster: &RustFSTestClusterEnvironment,
|
||||||
client: &Client,
|
client: &Client,
|
||||||
@@ -513,7 +630,7 @@ struct ReaderPathExpectation<'a> {
|
|||||||
object: ReaderObject<'a>,
|
object: ReaderObject<'a>,
|
||||||
expected_path: &'a str,
|
expected_path: &'a str,
|
||||||
object_class: &'a str,
|
object_class: &'a str,
|
||||||
expected_size_bucket: &'a str,
|
expected_size_bucket: Option<&'a str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ReaderPathExpectation<'a> {
|
impl<'a> ReaderPathExpectation<'a> {
|
||||||
@@ -527,7 +644,7 @@ impl<'a> ReaderPathExpectation<'a> {
|
|||||||
object,
|
object,
|
||||||
expected_path,
|
expected_path,
|
||||||
object_class,
|
object_class,
|
||||||
expected_size_bucket,
|
expected_size_bucket: Some(expected_size_bucket),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,7 +658,16 @@ impl<'a> ReaderPathExpectation<'a> {
|
|||||||
object,
|
object,
|
||||||
expected_path,
|
expected_path,
|
||||||
object_class,
|
object_class,
|
||||||
expected_size_bucket,
|
expected_size_bucket: Some(expected_size_bucket),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_any_size_bucket(object: ReaderObject<'a>, expected_path: &'a str, object_class: &'a str) -> Self {
|
||||||
|
Self {
|
||||||
|
object,
|
||||||
|
expected_path,
|
||||||
|
object_class,
|
||||||
|
expected_size_bucket: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -558,22 +684,102 @@ async fn assert_reader_path(
|
|||||||
expected_size_bucket,
|
expected_size_bucket,
|
||||||
} = expectation;
|
} = expectation;
|
||||||
let paths = [INLINE_DIRECT, LEGACY_DUPLEX, EMPTY, REMOTE_TRANSITION];
|
let paths = [INLINE_DIRECT, LEGACY_DUPLEX, EMPTY, REMOTE_TRANSITION];
|
||||||
let mut before = BTreeMap::<&str, u64>::new();
|
let size_buckets: Vec<&str> = match expected_size_bucket {
|
||||||
for path in paths {
|
Some(expected_size_bucket) => vec![expected_size_bucket],
|
||||||
before.insert(path, collector.reader_path_total(path, object_class, expected_size_bucket).await);
|
None => READER_SIZE_BUCKETS.to_vec(),
|
||||||
|
};
|
||||||
|
let size_buckets = size_buckets.as_slice();
|
||||||
|
|
||||||
|
let mut before = BTreeMap::<&str, BTreeMap<&str, u64>>::new();
|
||||||
|
for &size_bucket in size_buckets {
|
||||||
|
let mut bucket_before = BTreeMap::new();
|
||||||
|
for path in paths {
|
||||||
|
bucket_before.insert(path, collector.reader_path_total(path, object_class, size_bucket).await);
|
||||||
|
}
|
||||||
|
before.insert(size_bucket, bucket_before);
|
||||||
}
|
}
|
||||||
|
|
||||||
get_and_assert(client, object.bucket, object.key, object.body, object.etag, object.version_id).await?;
|
get_and_assert(client, object.bucket, object.key, object.body, object.etag, object.version_id).await?;
|
||||||
let expected_after = collector
|
|
||||||
.wait_for_reader_path_total(expected_path, object_class, expected_size_bucket, before[expected_path] + 1)
|
let selected_size_bucket = match expected_size_bucket {
|
||||||
.await?;
|
Some(expected_size_bucket) => {
|
||||||
assert!(
|
let path_before = before
|
||||||
expected_after > before[expected_path],
|
.get(expected_size_bucket)
|
||||||
"{READER_PATH_COUNTER}{{path={expected_path}}} must advance for {}",
|
.and_then(|values| values.get(expected_path))
|
||||||
object.key
|
.copied()
|
||||||
);
|
.ok_or_else(|| format!("reader-path baseline missing for size bucket {expected_size_bucket}"))?;
|
||||||
|
let expected_after = collector
|
||||||
|
.wait_for_reader_path_total(expected_path, object_class, expected_size_bucket, path_before + 1)
|
||||||
|
.await?;
|
||||||
|
assert!(
|
||||||
|
expected_after > path_before,
|
||||||
|
"{READER_PATH_COUNTER}{{path={expected_path}}} must advance for {}",
|
||||||
|
object.key
|
||||||
|
);
|
||||||
|
expected_size_bucket
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(20);
|
||||||
|
loop {
|
||||||
|
let mut matched_size_bucket = None;
|
||||||
|
for &size_bucket in size_buckets {
|
||||||
|
let path_before = before
|
||||||
|
.get(size_bucket)
|
||||||
|
.and_then(|values| values.get(expected_path))
|
||||||
|
.copied()
|
||||||
|
.ok_or_else(|| format!("reader-path baseline missing for size bucket {size_bucket}"))?;
|
||||||
|
let path_after = collector.reader_path_total(expected_path, object_class, size_bucket).await;
|
||||||
|
if path_after < path_before + 1 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut conflicting = false;
|
||||||
|
for path in paths {
|
||||||
|
if path == expected_path || expected_path == EMPTY {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let path_before = before
|
||||||
|
.get(size_bucket)
|
||||||
|
.and_then(|values| values.get(path))
|
||||||
|
.copied()
|
||||||
|
.ok_or_else(|| format!("reader-path baseline missing for size bucket {size_bucket}"))?;
|
||||||
|
let path_after = collector.reader_path_total(path, object_class, size_bucket).await;
|
||||||
|
if path_after != path_before {
|
||||||
|
conflicting = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !conflicting {
|
||||||
|
matched_size_bucket = Some(size_bucket);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(size_bucket) = matched_size_bucket {
|
||||||
|
break size_bucket;
|
||||||
|
}
|
||||||
|
if Instant::now() >= deadline {
|
||||||
|
let mut observed = BTreeMap::<&str, BTreeMap<&str, u64>>::new();
|
||||||
|
for &size_bucket in size_buckets {
|
||||||
|
let mut bucket_after = BTreeMap::new();
|
||||||
|
for path in paths {
|
||||||
|
bucket_after.insert(path, collector.reader_path_total(path, object_class, size_bucket).await);
|
||||||
|
}
|
||||||
|
observed.insert(size_bucket, bucket_after);
|
||||||
|
}
|
||||||
|
return Err(format!(
|
||||||
|
"timed out waiting for {READER_PATH_COUNTER}{{object_class={object_class}, path={expected_path}}} to advance in any candidate size bucket; before={before:?}; after={observed:?}"
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
collector
|
collector
|
||||||
.wait_for_reader_paths_to_settle(object_class, expected_size_bucket)
|
.wait_for_reader_paths_to_settle(object_class, selected_size_bucket)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
for path in paths {
|
for path in paths {
|
||||||
if path == expected_path {
|
if path == expected_path {
|
||||||
continue;
|
continue;
|
||||||
@@ -582,9 +788,13 @@ async fn assert_reader_path(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
collector.reader_path_total(path, object_class, expected_size_bucket).await,
|
collector.reader_path_total(path, object_class, selected_size_bucket).await,
|
||||||
before[path],
|
before
|
||||||
"{READER_PATH_COUNTER}{{path={path}, object_class={object_class}, size_bucket={expected_size_bucket}}} must not advance for {}; expected {expected_path} only",
|
.get(selected_size_bucket)
|
||||||
|
.and_then(|values| values.get(path))
|
||||||
|
.copied()
|
||||||
|
.ok_or_else(|| format!("reader-path baseline missing for size bucket {selected_size_bucket}"))?,
|
||||||
|
"{READER_PATH_COUNTER}{{path={path}, object_class={object_class}, size_bucket={selected_size_bucket}}} must not advance for {}; expected {expected_path} only",
|
||||||
object.key
|
object.key
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -734,6 +944,7 @@ async fn add_rustfs_tier(hot: &RustFSTestClusterEnvironment, cold: &RustFSTestEn
|
|||||||
})
|
})
|
||||||
.to_string();
|
.to_string();
|
||||||
let deadline = Instant::now() + Duration::from_secs(30);
|
let deadline = Instant::now() + Duration::from_secs(30);
|
||||||
|
let mut attempts = Vec::new();
|
||||||
let final_error = loop {
|
let final_error = loop {
|
||||||
let (status, response) = signed_admin_request(
|
let (status, response) = signed_admin_request(
|
||||||
&hot.nodes[0].url,
|
&hot.nodes[0].url,
|
||||||
@@ -744,23 +955,60 @@ async fn add_rustfs_tier(hot: &RustFSTestClusterEnvironment, cold: &RustFSTestEn
|
|||||||
&hot.secret_key,
|
&hot.secret_key,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
if status.is_success() || response.contains("<Code>TierNameAlreadyExist</Code>") {
|
let attempt = format!("status={status}, body={}", compact_body(&response));
|
||||||
wait_for_tier_verifiable(hot).await?;
|
if status.is_success() {
|
||||||
|
wait_for_tier_verifiable(hot, &format!("status={status}, body={response}")).await?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
attempts.push(attempt);
|
||||||
if Instant::now() >= deadline {
|
if Instant::now() >= deadline {
|
||||||
break format!("status={status}, body={response}");
|
break attempts.join("; ");
|
||||||
|
}
|
||||||
|
if !is_retryable_add_tier_error(&response) {
|
||||||
|
break attempts.join("; ");
|
||||||
}
|
}
|
||||||
sleep(Duration::from_millis(500)).await;
|
sleep(Duration::from_millis(500)).await;
|
||||||
};
|
};
|
||||||
Err(format!("AddTier(RustFS) failed after readiness polling: {final_error}").into())
|
Err(format!("AddTier(RustFS) failed after readiness polling: {final_error}").into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn wait_for_tier_verifiable(hot: &RustFSTestClusterEnvironment) -> TestResult {
|
async fn wait_for_tier_verifiable(hot: &RustFSTestClusterEnvironment, add_tier_response: &str) -> TestResult {
|
||||||
let deadline = Instant::now() + Duration::from_secs(30);
|
let deadline = Instant::now() + Duration::from_secs(60);
|
||||||
let node = &hot.nodes[0];
|
|
||||||
let final_error = loop {
|
let final_error = loop {
|
||||||
let (status, response) = signed_admin_request(
|
let snapshot = tier_readiness_snapshot(hot).await?;
|
||||||
|
if snapshot.iter().any(|node| node.verify_status.is_success()) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if snapshot.iter().any(|node| !is_retryable_tier_error(&node.verify_body)) {
|
||||||
|
break format!("non-retryable tier verification failure: {}", format_tier_readiness_snapshot(&snapshot));
|
||||||
|
}
|
||||||
|
if Instant::now() >= deadline {
|
||||||
|
break format_tier_readiness_snapshot(&snapshot);
|
||||||
|
}
|
||||||
|
sleep(Duration::from_millis(500)).await;
|
||||||
|
};
|
||||||
|
Err(format!(
|
||||||
|
"tier {TIER_NAME} was not verifiable on any hot node within 60s after AddTier({add_tier_response}): {final_error}"
|
||||||
|
)
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TierNodeReadiness {
|
||||||
|
node_index: usize,
|
||||||
|
node_url: String,
|
||||||
|
list_status: StatusCode,
|
||||||
|
list_has_tier: bool,
|
||||||
|
list_body: String,
|
||||||
|
verify_status: StatusCode,
|
||||||
|
verify_body: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn tier_readiness_snapshot(hot: &RustFSTestClusterEnvironment) -> TestResult<Vec<TierNodeReadiness>> {
|
||||||
|
let mut snapshot = Vec::with_capacity(hot.nodes.len());
|
||||||
|
for (node_index, node) in hot.nodes.iter().enumerate() {
|
||||||
|
let (list_status, list_body) =
|
||||||
|
signed_admin_request(&node.url, Method::GET, "/rustfs/admin/v3/tier", None, &hot.access_key, &hot.secret_key).await?;
|
||||||
|
let (verify_status, verify_body) = signed_admin_request(
|
||||||
&node.url,
|
&node.url,
|
||||||
Method::GET,
|
Method::GET,
|
||||||
&format!("/rustfs/admin/v3/tier/{TIER_NAME}"),
|
&format!("/rustfs/admin/v3/tier/{TIER_NAME}"),
|
||||||
@@ -769,15 +1017,75 @@ async fn wait_for_tier_verifiable(hot: &RustFSTestClusterEnvironment) -> TestRes
|
|||||||
&hot.secret_key,
|
&hot.secret_key,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
if status.is_success() {
|
snapshot.push(TierNodeReadiness {
|
||||||
return Ok(());
|
node_index,
|
||||||
}
|
node_url: node.url.clone(),
|
||||||
if Instant::now() >= deadline {
|
list_status,
|
||||||
break format!("node {} verify tier failed: status={status}, body={response}", node.url);
|
list_has_tier: tier_list_contains(&list_body),
|
||||||
}
|
list_body,
|
||||||
sleep(Duration::from_millis(500)).await;
|
verify_status,
|
||||||
};
|
verify_body,
|
||||||
Err(format!("tier {TIER_NAME} was not verifiable on the primary hot node within 30s: {final_error}").into())
|
});
|
||||||
|
}
|
||||||
|
Ok(snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tier_list_contains(response: &str) -> bool {
|
||||||
|
serde_json::from_str::<serde_json::Value>(response)
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| value.as_array().cloned())
|
||||||
|
.is_some_and(|tiers| {
|
||||||
|
tiers.iter().any(|tier| {
|
||||||
|
tier.get("name").and_then(serde_json::Value::as_str) == Some(TIER_NAME)
|
||||||
|
|| tier
|
||||||
|
.get("rustfs")
|
||||||
|
.and_then(|rustfs| rustfs.get("name"))
|
||||||
|
.and_then(serde_json::Value::as_str)
|
||||||
|
== Some(TIER_NAME)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_tier_readiness_snapshot(snapshot: &[TierNodeReadiness]) -> String {
|
||||||
|
snapshot
|
||||||
|
.iter()
|
||||||
|
.map(|node| {
|
||||||
|
format!(
|
||||||
|
"node {} {} list_status={} list_has_tier={} list_body={} verify_status={} verify_body={}",
|
||||||
|
node.node_index,
|
||||||
|
node.node_url,
|
||||||
|
node.list_status,
|
||||||
|
node.list_has_tier,
|
||||||
|
compact_body(&node.list_body),
|
||||||
|
node.verify_status,
|
||||||
|
compact_body(&node.verify_body)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("; ")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compact_body(body: &str) -> String {
|
||||||
|
const MAX_BODY_CHARS: usize = 1024;
|
||||||
|
let mut value = body.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||||
|
if value.chars().count() > MAX_BODY_CHARS {
|
||||||
|
value = value.chars().take(MAX_BODY_CHARS).collect::<String>();
|
||||||
|
value.push_str("...");
|
||||||
|
}
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_retryable_tier_error(response: &str) -> bool {
|
||||||
|
response.contains("<Code>TierNotFound</Code>")
|
||||||
|
|| response.contains("<Code>NoSuchTier</Code>")
|
||||||
|
|| (response.contains("<Code>TierVerificationFailed</Code>")
|
||||||
|
&& response.contains("Remote tier configuration is being replaced"))
|
||||||
|
|| response.contains("TierNotFound")
|
||||||
|
|| response.contains("NoSuchTier")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_retryable_add_tier_error(response: &str) -> bool {
|
||||||
|
response.contains("Remote tier configuration is already being replaced")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn transition_rule() -> TestResult<LifecycleRule> {
|
fn transition_rule() -> TestResult<LifecycleRule> {
|
||||||
@@ -1005,11 +1313,10 @@ async fn four_node_inline_fallback_controls() -> TestResult {
|
|||||||
assert_reader_path(
|
assert_reader_path(
|
||||||
&collector,
|
&collector,
|
||||||
&client,
|
&client,
|
||||||
ReaderPathExpectation::with_size_bucket(
|
ReaderPathExpectation::with_any_size_bucket(
|
||||||
ReaderObject::new(bucket, encrypted_key, &encrypted_body, encrypted_put.e_tag(), None),
|
ReaderObject::new(bucket, encrypted_key, &encrypted_body, encrypted_put.e_tag(), None),
|
||||||
LEGACY_DUPLEX,
|
LEGACY_DUPLEX,
|
||||||
ENCRYPTED,
|
ENCRYPTED,
|
||||||
size_bucket(64 * KIB),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -1083,6 +1390,198 @@ async fn four_node_multipart_ignores_disk_compression_fallback() -> TestResult {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn four_node_mixed_msgpack_compat_mode_preserves_fallback_controls() -> TestResult {
|
||||||
|
init_logging();
|
||||||
|
|
||||||
|
let collector = OtlpMetricCollector::start().await?;
|
||||||
|
let mut cluster = RustFSTestClusterEnvironment::new(4).await?;
|
||||||
|
let sse_master_key = base64::engine::general_purpose::STANDARD.encode([0x42u8; 32]);
|
||||||
|
cluster.set_env("RUSTFS_SSE_S3_MASTER_KEY", sse_master_key);
|
||||||
|
cluster.set_env("RUSTFS_COMPRESSION_ENABLED", "true");
|
||||||
|
configure_mixed_msgpack_cluster(&mut cluster, &collector)?;
|
||||||
|
cluster.start().await?;
|
||||||
|
|
||||||
|
let fallback_before = collector.msgpack_json_fallback_totals().await;
|
||||||
|
|
||||||
|
let bucket = "inline-mixed-msgpack-controls";
|
||||||
|
cluster.create_test_bucket(bucket).await?;
|
||||||
|
let client = cluster.create_s3_client(0)?;
|
||||||
|
|
||||||
|
let multipart_key = "mixed/multipart.bin";
|
||||||
|
let (multipart_body, second_part, multipart_etag) = put_two_part_multipart(&client, bucket, multipart_key).await?;
|
||||||
|
assert_reader_path(
|
||||||
|
&collector,
|
||||||
|
&client,
|
||||||
|
ReaderPathExpectation::for_class(
|
||||||
|
ReaderObject::new(bucket, multipart_key, &multipart_body, multipart_etag.as_deref(), None),
|
||||||
|
LEGACY_DUPLEX,
|
||||||
|
MULTIPART,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_part_number_reader_path(
|
||||||
|
&collector,
|
||||||
|
&client,
|
||||||
|
bucket,
|
||||||
|
multipart_key,
|
||||||
|
&second_part,
|
||||||
|
multipart_body.len(),
|
||||||
|
LEGACY_DUPLEX,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_msgpack_fallback_unchanged(&collector, &fallback_before, &MSGPACK_FALLBACK_CONTROL_SERIES).await?;
|
||||||
|
|
||||||
|
let encrypted_key = "encrypted/sse-s3.bin";
|
||||||
|
let encrypted_body = payload(16 * KIB, 0xE3);
|
||||||
|
let encrypted_put = client
|
||||||
|
.put_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(encrypted_key)
|
||||||
|
.server_side_encryption(ServerSideEncryption::Aes256)
|
||||||
|
.body(ByteStream::from(encrypted_body.clone()))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
let encrypted_head = client.head_object().bucket(bucket).key(encrypted_key).send().await?;
|
||||||
|
assert_eq!(
|
||||||
|
encrypted_head.server_side_encryption(),
|
||||||
|
Some(&ServerSideEncryption::Aes256),
|
||||||
|
"HEAD must preserve SSE-S3 metadata for {encrypted_key}"
|
||||||
|
);
|
||||||
|
assert_reader_path(
|
||||||
|
&collector,
|
||||||
|
&client,
|
||||||
|
ReaderPathExpectation::with_any_size_bucket(
|
||||||
|
ReaderObject::new(bucket, encrypted_key, &encrypted_body, encrypted_put.e_tag(), None),
|
||||||
|
LEGACY_DUPLEX,
|
||||||
|
ENCRYPTED,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let compressed_key = "compressed/repeated.txt";
|
||||||
|
let compressed_body = compressible_payload(64 * KIB);
|
||||||
|
let compressed_put = client
|
||||||
|
.put_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(compressed_key)
|
||||||
|
.body(ByteStream::from(compressed_body.clone()))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
assert_reader_path(
|
||||||
|
&collector,
|
||||||
|
&client,
|
||||||
|
ReaderPathExpectation::with_any_size_bucket(
|
||||||
|
ReaderObject::new(bucket, compressed_key, &compressed_body, compressed_put.e_tag(), None),
|
||||||
|
LEGACY_DUPLEX,
|
||||||
|
COMPRESSED,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert_msgpack_fallback_unchanged(&collector, &fallback_before, &MSGPACK_FALLBACK_CONTROL_SERIES).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn four_node_mixed_msgpack_compat_mode_preserves_fallback_controls_during_transition() -> TestResult {
|
||||||
|
init_logging();
|
||||||
|
|
||||||
|
let mut cold = RustFSTestEnvironment::new().await?;
|
||||||
|
cold.access_key = "inlinecoldadmin".to_string();
|
||||||
|
cold.secret_key = "inlinecoldsecret".to_string();
|
||||||
|
cold.start_rustfs_server_without_cleanup(vec![]).await?;
|
||||||
|
let cold_client = cold.create_s3_client();
|
||||||
|
cold_client.create_bucket().bucket(TIER_BUCKET).send().await?;
|
||||||
|
|
||||||
|
let collector = OtlpMetricCollector::start().await?;
|
||||||
|
let mut hot = RustFSTestClusterEnvironment::new(4).await?;
|
||||||
|
configure_mixed_msgpack_cluster(&mut hot, &collector)?;
|
||||||
|
hot.set_env("RUSTFS_SCANNER_CYCLE", "1");
|
||||||
|
hot.set_env("RUSTFS_ILM_PROCESS_TIME", "1");
|
||||||
|
|
||||||
|
let sse_master_key = base64::engine::general_purpose::STANDARD.encode([0x42u8; 32]);
|
||||||
|
hot.set_env("RUSTFS_SSE_S3_MASTER_KEY", sse_master_key);
|
||||||
|
hot.set_env("RUSTFS_COMPRESSION_ENABLED", "true");
|
||||||
|
hot.start().await?;
|
||||||
|
let hot_client = hot.create_s3_client(0)?;
|
||||||
|
|
||||||
|
let fallback_before = collector.msgpack_json_fallback_totals().await;
|
||||||
|
|
||||||
|
add_rustfs_tier(&hot, &cold).await?;
|
||||||
|
let bucket = "inline-transitioned-mixed-msgpack-controls";
|
||||||
|
hot_client.create_bucket().bucket(bucket).send().await?;
|
||||||
|
put_lifecycle_with_transition_retry(&hot_client, bucket).await?;
|
||||||
|
|
||||||
|
let key = "transition/mixed-multipart.bin";
|
||||||
|
let (body, _, etag) = put_two_part_multipart(&hot_client, bucket, key).await?;
|
||||||
|
wait_for_transition(&hot_client, bucket, key).await?;
|
||||||
|
assert!(
|
||||||
|
cold_tier_object_count(&cold_client).await? >= 1,
|
||||||
|
"cold-tier bucket must hold transitioned objects"
|
||||||
|
);
|
||||||
|
assert_reader_path(
|
||||||
|
&collector,
|
||||||
|
&hot_client,
|
||||||
|
ReaderPathExpectation::for_class(ReaderObject::new(bucket, key, &body, etag.as_deref(), None), REMOTE_TRANSITION, REMOTE),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let encrypted_key = "transition/encrypted-sse.bin";
|
||||||
|
let encrypted_body = payload(16 * KIB, 0xAB);
|
||||||
|
let encrypted_put = hot_client
|
||||||
|
.put_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(encrypted_key)
|
||||||
|
.server_side_encryption(ServerSideEncryption::Aes256)
|
||||||
|
.body(ByteStream::from(encrypted_body.clone()))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
let encrypted_head = hot_client.head_object().bucket(bucket).key(encrypted_key).send().await?;
|
||||||
|
assert_eq!(
|
||||||
|
encrypted_head.server_side_encryption(),
|
||||||
|
Some(&ServerSideEncryption::Aes256),
|
||||||
|
"HEAD must preserve SSE-S3 metadata for {encrypted_key}"
|
||||||
|
);
|
||||||
|
assert_reader_path(
|
||||||
|
&collector,
|
||||||
|
&hot_client,
|
||||||
|
ReaderPathExpectation::with_any_size_bucket(
|
||||||
|
ReaderObject::new(bucket, encrypted_key, &encrypted_body, encrypted_put.e_tag(), None),
|
||||||
|
LEGACY_DUPLEX,
|
||||||
|
ENCRYPTED,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let compressed_key = "transition/compressed-repeated.txt";
|
||||||
|
let compressed_body = compressible_payload(64 * KIB);
|
||||||
|
let compressed_put = hot_client
|
||||||
|
.put_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(compressed_key)
|
||||||
|
.body(ByteStream::from(compressed_body.clone()))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
assert_reader_path(
|
||||||
|
&collector,
|
||||||
|
&hot_client,
|
||||||
|
ReaderPathExpectation::with_any_size_bucket(
|
||||||
|
ReaderObject::new(bucket, compressed_key, &compressed_body, compressed_put.e_tag(), None),
|
||||||
|
LEGACY_DUPLEX,
|
||||||
|
COMPRESSED,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert_msgpack_fallback_unchanged(&collector, &fallback_before, &MSGPACK_FALLBACK_CONTROL_SERIES).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[serial]
|
#[serial]
|
||||||
async fn four_node_transitioned_inline_fallback() -> TestResult {
|
async fn four_node_transitioned_inline_fallback() -> TestResult {
|
||||||
|
|||||||
@@ -6133,6 +6133,36 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn intent_advance_treats_matching_committed_cas_race_as_idempotent() {
|
||||||
|
let store = Arc::new(CasConfigStore::default());
|
||||||
|
let mutation_id = uuid::Uuid::from_u128(34);
|
||||||
|
let prepared = prepared_remove_intent("COLD-A", mutation_id);
|
||||||
|
crate::services::tier::tier_mutation_intent::save_tier_mutation_intent_record(store.clone(), &prepared)
|
||||||
|
.await
|
||||||
|
.expect("prepared intent fixture should persist");
|
||||||
|
|
||||||
|
let committed = committed_remove_intent("COLD-A", mutation_id, "etag-new");
|
||||||
|
let object = crate::services::tier::tier_mutation_intent::tier_mutation_intent_record_object_name(mutation_id)
|
||||||
|
.expect("record object should build");
|
||||||
|
store
|
||||||
|
.rewrite_on_next_if_match(object, committed.encode().expect("committed intent fixture should encode"))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (observed, applied) = crate::services::tier::tier_mutation_intent::advance_tier_mutation_intent_record_idempotent(
|
||||||
|
store,
|
||||||
|
mutation_id,
|
||||||
|
TierMutationIntentState::Committed,
|
||||||
|
Some("etag-new".to_string()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("same-state CAS race should be idempotent");
|
||||||
|
|
||||||
|
assert!(!applied);
|
||||||
|
assert_eq!(observed.state, TierMutationIntentState::Committed);
|
||||||
|
assert_eq!(observed.committed_config_etag.as_deref(), Some("etag-new"));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn committed_mutation_recovery_requires_every_peer_to_commit() {
|
async fn committed_mutation_recovery_requires_every_peer_to_commit() {
|
||||||
let mutation_id = uuid::Uuid::from_u128(17);
|
let mutation_id = uuid::Uuid::from_u128(17);
|
||||||
@@ -8225,6 +8255,7 @@ mod tests {
|
|||||||
struct CasConfigStore {
|
struct CasConfigStore {
|
||||||
objects: tokio::sync::Mutex<HashMap<String, (Vec<u8>, String)>>,
|
objects: tokio::sync::Mutex<HashMap<String, (Vec<u8>, String)>>,
|
||||||
legacy_state: tokio::sync::Mutex<Option<Vec<u8>>>,
|
legacy_state: tokio::sync::Mutex<Option<Vec<u8>>>,
|
||||||
|
if_match_race_rewrite: tokio::sync::Mutex<Option<(String, Vec<u8>)>>,
|
||||||
next_etag: AtomicUsize,
|
next_etag: AtomicUsize,
|
||||||
fail_put: AtomicBool,
|
fail_put: AtomicBool,
|
||||||
truncate_reference_page_without_marker: AtomicBool,
|
truncate_reference_page_without_marker: AtomicBool,
|
||||||
@@ -8238,6 +8269,7 @@ mod tests {
|
|||||||
Self {
|
Self {
|
||||||
objects: tokio::sync::Mutex::new(HashMap::new()),
|
objects: tokio::sync::Mutex::new(HashMap::new()),
|
||||||
legacy_state: tokio::sync::Mutex::new(None),
|
legacy_state: tokio::sync::Mutex::new(None),
|
||||||
|
if_match_race_rewrite: tokio::sync::Mutex::new(None),
|
||||||
next_etag: AtomicUsize::new(0),
|
next_etag: AtomicUsize::new(0),
|
||||||
fail_put: AtomicBool::new(false),
|
fail_put: AtomicBool::new(false),
|
||||||
truncate_reference_page_without_marker: AtomicBool::new(false),
|
truncate_reference_page_without_marker: AtomicBool::new(false),
|
||||||
@@ -8263,6 +8295,10 @@ mod tests {
|
|||||||
.insert(object, (data, "reference-proof-etag".to_string()));
|
.insert(object, (data, "reference-proof-etag".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn rewrite_on_next_if_match(&self, object: String, data: Vec<u8>) {
|
||||||
|
*self.if_match_race_rewrite.lock().await = Some((object, data));
|
||||||
|
}
|
||||||
|
|
||||||
fn omit_truncated_reference_marker(&self) {
|
fn omit_truncated_reference_marker(&self) {
|
||||||
self.truncate_reference_page_without_marker.store(true, Ordering::SeqCst);
|
self.truncate_reference_page_without_marker.store(true, Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
@@ -8323,7 +8359,23 @@ mod tests {
|
|||||||
}
|
}
|
||||||
let mut payload = Vec::new();
|
let mut payload = Vec::new();
|
||||||
tokio::io::AsyncReadExt::read_to_end(&mut data.stream, &mut payload).await?;
|
tokio::io::AsyncReadExt::read_to_end(&mut data.stream, &mut payload).await?;
|
||||||
|
let race_rewrite = if opts
|
||||||
|
.http_preconditions
|
||||||
|
.as_ref()
|
||||||
|
.and_then(HTTPPreconditions::if_match_value)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
self.if_match_race_rewrite.lock().await.take()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let mut objects = self.objects.lock().await;
|
let mut objects = self.objects.lock().await;
|
||||||
|
if let Some((target, data)) = race_rewrite
|
||||||
|
&& target == object
|
||||||
|
{
|
||||||
|
let etag = format!("etag-{}", self.next_etag.fetch_add(1, Ordering::SeqCst) + 1);
|
||||||
|
objects.insert(object.to_string(), (data, etag));
|
||||||
|
}
|
||||||
match objects.get(object) {
|
match objects.get(object) {
|
||||||
Some((_, etag)) => opts.precondition_check(&ObjectInfo {
|
Some((_, etag)) => opts.precondition_check(&ObjectInfo {
|
||||||
etag: Some(etag.clone()),
|
etag: Some(etag.clone()),
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ pub(crate) const TIER_MUTATION_INTENT_SCHEMA: &str = "rustfs-tier-mutation-inten
|
|||||||
pub(crate) const MAX_TIER_MUTATION_INTENT_SIZE: usize = rustfs_protos::TIER_MUTATION_RPC_MAX_PREPARE_PAYLOAD_SIZE;
|
pub(crate) const MAX_TIER_MUTATION_INTENT_SIZE: usize = rustfs_protos::TIER_MUTATION_RPC_MAX_PREPARE_PAYLOAD_SIZE;
|
||||||
pub(crate) const TIER_MUTATION_INTENT_RECORD_PREFIX: &str = "tier/mutation-intents/records";
|
pub(crate) const TIER_MUTATION_INTENT_RECORD_PREFIX: &str = "tier/mutation-intents/records";
|
||||||
pub(crate) const TIER_COORDINATOR_MUTATION_INTENT_RECORD_PREFIX: &str = "tier/mutation-intents/coordinators";
|
pub(crate) const TIER_COORDINATOR_MUTATION_INTENT_RECORD_PREFIX: &str = "tier/mutation-intents/coordinators";
|
||||||
|
const TIER_MUTATION_INTENT_ADVANCE_CAS_ATTEMPTS: usize = 3;
|
||||||
pub(crate) type TierMutationDigest = [u8; 32];
|
pub(crate) type TierMutationDigest = [u8; 32];
|
||||||
|
|
||||||
pub(crate) type Result<T> = std::result::Result<T, TierMutationIntentError>;
|
pub(crate) type Result<T> = std::result::Result<T, TierMutationIntentError>;
|
||||||
@@ -555,15 +556,22 @@ async fn advance_tier_mutation_intent_record_idempotent_at_prefix<S>(
|
|||||||
where
|
where
|
||||||
S: EcstoreObjectIO,
|
S: EcstoreObjectIO,
|
||||||
{
|
{
|
||||||
let (mut intent, current_etag) =
|
for attempt in 0..TIER_MUTATION_INTENT_ADVANCE_CAS_ATTEMPTS {
|
||||||
load_tier_mutation_intent_record_with_etag_at_prefix(api.clone(), prefix, mutation_id).await?;
|
let (mut intent, current_etag) =
|
||||||
let advanced = intent
|
load_tier_mutation_intent_record_with_etag_at_prefix(api.clone(), prefix, mutation_id).await?;
|
||||||
.advance_idempotent(next, committed_config_etag)
|
let advanced = intent
|
||||||
.map_err(tier_mutation_intent_store_error)?;
|
.advance_idempotent(next, committed_config_etag.clone())
|
||||||
if advanced {
|
.map_err(tier_mutation_intent_store_error)?;
|
||||||
save_tier_mutation_intent_record_if_current_with_prefix(api, prefix, &intent, ¤t_etag).await?;
|
if !advanced {
|
||||||
|
return Ok((intent, false));
|
||||||
|
}
|
||||||
|
match save_tier_mutation_intent_record_if_current_with_prefix(api.clone(), prefix, &intent, ¤t_etag).await {
|
||||||
|
Ok(()) => return Ok((intent, true)),
|
||||||
|
Err(Error::PreconditionFailed) if attempt + 1 < TIER_MUTATION_INTENT_ADVANCE_CAS_ATTEMPTS => continue,
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok((intent, advanced))
|
Err(Error::PreconditionFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn list_tier_mutation_intent_records<S>(
|
pub(crate) async fn list_tier_mutation_intent_records<S>(
|
||||||
|
|||||||
@@ -15,11 +15,23 @@ trap cleanup EXIT
|
|||||||
|
|
||||||
BEFORE_ENV="${TMP_DIR}/p2-before/server-env.sh"
|
BEFORE_ENV="${TMP_DIR}/p2-before/server-env.sh"
|
||||||
AFTER_ENV="${TMP_DIR}/p2-after/server-env.sh"
|
AFTER_ENV="${TMP_DIR}/p2-after/server-env.sh"
|
||||||
|
P0_BEFORE_ENV="${TMP_DIR}/p0-before/server-env.sh"
|
||||||
|
P0_AFTER_ENV="${TMP_DIR}/p0-after/server-env.sh"
|
||||||
|
P1_BEFORE_ENV="${TMP_DIR}/p1-before/server-env.sh"
|
||||||
|
P1_AFTER_ENV="${TMP_DIR}/p1-after/server-env.sh"
|
||||||
|
P3_BEFORE_ENV="${TMP_DIR}/p3-before/server-env.sh"
|
||||||
|
P3_AFTER_ENV="${TMP_DIR}/p3-after/server-env.sh"
|
||||||
|
|
||||||
rg -qx 'export RUSTFS_INTERNODE_RPC_MSGPACK_ONLY=false' "$BEFORE_ENV"
|
rg -qx 'export RUSTFS_INTERNODE_RPC_MSGPACK_ONLY=false' "$BEFORE_ENV"
|
||||||
rg -qx 'export RUSTFS_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED=false' "$BEFORE_ENV"
|
rg -qx 'export RUSTFS_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED=false' "$BEFORE_ENV"
|
||||||
rg -qx 'export RUSTFS_INTERNODE_RPC_MSGPACK_ONLY=true' "$AFTER_ENV"
|
rg -qx 'export RUSTFS_INTERNODE_RPC_MSGPACK_ONLY=true' "$AFTER_ENV"
|
||||||
rg -qx 'export RUSTFS_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED=true' "$AFTER_ENV"
|
rg -qx 'export RUSTFS_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED=true' "$AFTER_ENV"
|
||||||
|
"$RUNNER" --stage p0 --phase before --out-root "$TMP_DIR" --dry-run >/dev/null
|
||||||
|
"$RUNNER" --stage p0 --phase after --out-root "$TMP_DIR" --dry-run >/dev/null
|
||||||
|
"$RUNNER" --stage p1 --phase before --out-root "$TMP_DIR" --dry-run >/dev/null
|
||||||
|
"$RUNNER" --stage p1 --phase after --out-root "$TMP_DIR" --dry-run >/dev/null
|
||||||
|
"$RUNNER" --stage p3 --phase before --out-root "$TMP_DIR" --dry-run >/dev/null
|
||||||
|
"$RUNNER" --stage p3 --phase after --out-root "$TMP_DIR" --dry-run >/dev/null
|
||||||
|
|
||||||
if grep -q 'RUSTFS_INTERNODE_RPC_MSGPACK_ONLY=true' "$BEFORE_ENV"; then
|
if grep -q 'RUSTFS_INTERNODE_RPC_MSGPACK_ONLY=true' "$BEFORE_ENV"; then
|
||||||
echo "p2 before must not request msgpack-only" >&2
|
echo "p2 before must not request msgpack-only" >&2
|
||||||
@@ -31,4 +43,23 @@ if grep -q 'RUSTFS_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED=true' "$BEFORE_ENV
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
rg -qx 'export RUSTFS_INTERNODE_RPC_TCP_NODELAY=false' "$P0_BEFORE_ENV"
|
||||||
|
rg -qx 'export RUSTFS_INTERNODE_RPC_HTTP2_STREAM_WINDOW_SIZE=0' "$P0_BEFORE_ENV"
|
||||||
|
rg -qx 'export RUSTFS_INTERNODE_RPC_HTTP2_CONN_WINDOW_SIZE=0' "$P0_BEFORE_ENV"
|
||||||
|
rg -qx 'export RUSTFS_INTERNODE_RPC_MAX_MESSAGE_SIZE=4194304' "$P0_BEFORE_ENV"
|
||||||
|
rg -qx '# \(cluster defaults — no overrides needed for this stage/phase\)' "$P0_AFTER_ENV"
|
||||||
|
|
||||||
|
rg -qx 'export RUSTFS_INTERNODE_CHANNEL_ISOLATION=false' "$P1_BEFORE_ENV"
|
||||||
|
rg -qx 'export RUSTFS_INTERNODE_CHANNEL_ISOLATION=true' "$P1_AFTER_ENV"
|
||||||
|
rg -qx 'export RUSTFS_INTERNODE_BULK_CHANNELS=2' "$P1_AFTER_ENV"
|
||||||
|
if rg -q 'export RUSTFS_INTERNODE_BULK_CHANNELS=' "$P1_BEFORE_ENV"; then
|
||||||
|
echo "p1 before should not pin an explicit bulk channel count" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rg -qx 'export RUSTFS_INTERNODE_PREWARM=true' "$P3_AFTER_ENV"
|
||||||
|
rg -qx 'export RUSTFS_INTERNODE_OFFLINE_BYPASS=true' "$P3_AFTER_ENV"
|
||||||
|
rg -qx 'export RUSTFS_INTERNODE_OFFLINE_REPROBE_SECS=5' "$P3_AFTER_ENV"
|
||||||
|
rg -qx 'export RUSTFS_INTERNODE_OFFLINE_FAILURE_THRESHOLD=3' "$P3_AFTER_ENV"
|
||||||
|
|
||||||
echo "internode grpc A/B bench env tests passed"
|
echo "internode grpc A/B bench env tests passed"
|
||||||
|
|||||||
Reference in New Issue
Block a user