Compare commits

..

1 Commits

Author SHA1 Message Date
overtrue fd79678559 test(object-lock): cover unretained version cleanup 2026-07-31 10:32:44 +08:00
75 changed files with 712 additions and 2479 deletions
Generated
+32 -89
View File
@@ -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.7", "generic-array 0.14.9",
] ]
[[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.7", "generic-array 0.14.9",
] ]
[[package]] [[package]]
@@ -1598,7 +1598,7 @@ version = "3.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dee98b0db6a962de883bf5d20362dee4d7ca0d12fe39a7c6c73c844e1cd7c1f" checksum = "6dee98b0db6a962de883bf5d20362dee4d7ca0d12fe39a7c6c73c844e1cd7c1f"
dependencies = [ dependencies = [
"darling 0.20.11", "darling 0.23.0",
"ident_case", "ident_case",
"prettyplease", "prettyplease",
"proc-macro2", "proc-macro2",
@@ -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.7", "crypto-common 0.1.6",
"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.7", "generic-array 0.14.9",
"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.7" version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [ dependencies = [
"generic-array 0.14.7", "generic-array 0.14.9",
"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.7", "crypto-common 0.1.6",
"subtle", "subtle",
] ]
@@ -3672,7 +3672,6 @@ dependencies = [
"flate2", "flate2",
"futures", "futures",
"hex", "hex",
"hotpath",
"http 1.5.0", "http 1.5.0",
"http-body-util", "http-body-util",
"hyper", "hyper",
@@ -3814,7 +3813,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.7", "generic-array 0.14.9",
"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",
@@ -4259,9 +4258,9 @@ dependencies = [
[[package]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.7" version = "0.14.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
dependencies = [ dependencies = [
"typenum", "typenum",
"version_check", "version_check",
@@ -4274,7 +4273,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.7", "generic-array 0.14.9",
"rustversion", "rustversion",
"typenum", "typenum",
] ]
@@ -4373,9 +4372,9 @@ dependencies = [
[[package]] [[package]]
name = "google-cloud-auth" name = "google-cloud-auth"
version = "1.15.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 = "f54aab44c16b8463ae11b165a87c3d484780231f157bb1ed65843d591beb5abd" checksum = "a3494870d06f3cbbb3561ada6f234982549e3a2fb31e719ef258e6eadb9ae09a"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"aws-lc-rs", "aws-lc-rs",
@@ -4402,9 +4401,9 @@ dependencies = [
[[package]] [[package]]
name = "google-cloud-gax" name = "google-cloud-gax"
version = "1.13.0" version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9a46dd0fd026bbc4a5d84e6ab0c941cee6e3b057976a0bb107fdb5238ce598f" checksum = "3103a4a9013f1aed573ca56e19a9680b0211643a99ea85caf524b397d6be8be3"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures", "futures",
@@ -4421,9 +4420,9 @@ dependencies = [
[[package]] [[package]]
name = "google-cloud-gax-internal" name = "google-cloud-gax-internal"
version = "0.7.16" version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb04c54317ace06d489213f761797240b3046142a9b7ce6b9a82a9d134e193d1" checksum = "c0df265fba091ed7e00ecd0755009423310163f8b52820f007b6b4d97f4c6617"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures", "futures",
@@ -4525,9 +4524,9 @@ dependencies = [
[[package]] [[package]]
name = "google-cloud-storage" name = "google-cloud-storage"
version = "1.17.0" version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9227f65175fa91a6e41f246797917697efdadfe09dd8ea84ad8b737a71efbd28" checksum = "dc4b1d78c88db5c2530b12461e373a7d0d3a6caa3f6c1fc14e5d824cf2aeb307"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"base64 0.22.1", "base64 0.22.1",
@@ -4578,9 +4577,9 @@ dependencies = [
[[package]] [[package]]
name = "google-cloud-wkt" name = "google-cloud-wkt"
version = "1.7.0" version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fccf98cfd5481a5f5a285181ab0c62123d7d47cd2bb7299448440649349e4e7" checksum = "46df1fcc3ab69164af3f4199ed21f45b5dbc56d9f03211eb4fa20116d442364b"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
@@ -4921,7 +4920,6 @@ dependencies = [
"async-trait", "async-trait",
"cfg-if", "cfg-if",
"crossbeam-channel", "crossbeam-channel",
"flate2",
"futures-channel", "futures-channel",
"futures-util", "futures-util",
"hdrhistogram", "hdrhistogram",
@@ -4929,7 +4927,6 @@ dependencies = [
"hotpath-meta", "hotpath-meta",
"http 1.5.0", "http 1.5.0",
"libc", "libc",
"object 0.36.7",
"parking_lot", "parking_lot",
"pin-project-lite", "pin-project-lite",
"prettytable-rs", "prettytable-rs",
@@ -4937,7 +4934,6 @@ dependencies = [
"regex", "regex",
"reqwest", "reqwest",
"reqwest-middleware", "reqwest-middleware",
"rustc-demangle",
"serde", "serde",
"serde_json", "serde_json",
"tiny_http", "tiny_http",
@@ -5051,9 +5047,9 @@ checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15"
[[package]] [[package]]
name = "hybrid-array" name = "hybrid-array"
version = "0.4.14" version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c"
dependencies = [ dependencies = [
"ctutils", "ctutils",
"subtle", "subtle",
@@ -5301,7 +5297,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.7", "generic-array 0.14.9",
] ]
[[package]] [[package]]
@@ -6811,15 +6807,6 @@ dependencies = [
"objc2-core-foundation", "objc2-core-foundation",
] ]
[[package]]
name = "object"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "object" name = "object"
version = "0.37.3" version = "0.37.3"
@@ -7857,7 +7844,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
dependencies = [ dependencies = [
"heck", "heck",
"itertools 0.13.0", "itertools 0.10.5",
"log", "log",
"multimap", "multimap",
"once_cell", "once_cell",
@@ -7877,7 +7864,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042"
dependencies = [ dependencies = [
"heck", "heck",
"itertools 0.13.0", "itertools 0.10.5",
"log", "log",
"multimap", "multimap",
"petgraph 0.8.3", "petgraph 0.8.3",
@@ -7898,7 +7885,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"itertools 0.13.0", "itertools 0.10.5",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.119", "syn 2.0.119",
@@ -7911,7 +7898,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"itertools 0.13.0", "itertools 0.10.5",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.119", "syn 2.0.119",
@@ -9040,7 +9027,6 @@ dependencies = [
"const-str", "const-str",
"futures", "futures",
"hashbrown 0.17.1", "hashbrown 0.17.1",
"hotpath",
"metrics", "metrics",
"rustfs-config", "rustfs-config",
"rustfs-s3-types", "rustfs-s3-types",
@@ -9061,7 +9047,6 @@ dependencies = [
"base64-simd", "base64-simd",
"bytes", "bytes",
"crc-fast", "crc-fast",
"hotpath",
"http 1.5.0", "http 1.5.0",
"md-5 0.11.0", "md-5 0.11.0",
"pretty_assertions", "pretty_assertions",
@@ -9075,7 +9060,6 @@ name = "rustfs-common"
version = "1.0.0-beta.12" version = "1.0.0-beta.12"
dependencies = [ dependencies = [
"chrono", "chrono",
"hotpath",
"metrics", "metrics",
"rmp-serde", "rmp-serde",
"s3s", "s3s",
@@ -9090,7 +9074,6 @@ dependencies = [
name = "rustfs-concurrency" name = "rustfs-concurrency"
version = "1.0.0-beta.12" version = "1.0.0-beta.12"
dependencies = [ dependencies = [
"hotpath",
"insta", "insta",
"rustfs-io-core", "rustfs-io-core",
"serde", "serde",
@@ -9104,7 +9087,6 @@ name = "rustfs-config"
version = "1.0.0-beta.12" version = "1.0.0-beta.12"
dependencies = [ dependencies = [
"const-str", "const-str",
"hotpath",
"serde", "serde",
"serde_json", "serde_json",
] ]
@@ -9115,7 +9097,6 @@ version = "1.0.0-beta.12"
dependencies = [ dependencies = [
"base64-simd", "base64-simd",
"hmac 0.13.0", "hmac 0.13.0",
"hotpath",
"rand 0.10.2", "rand 0.10.2",
"serde", "serde",
"serde_json", "serde_json",
@@ -9131,7 +9112,6 @@ dependencies = [
"argon2", "argon2",
"base64-simd", "base64-simd",
"chacha20poly1305", "chacha20poly1305",
"hotpath",
"jsonwebtoken 11.0.0", "jsonwebtoken 11.0.0",
"pbkdf2 0.13.0", "pbkdf2 0.13.0",
"rand 0.10.2", "rand 0.10.2",
@@ -9149,7 +9129,6 @@ name = "rustfs-data-usage"
version = "1.0.0-beta.12" version = "1.0.0-beta.12"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"hotpath",
"rmp-serde", "rmp-serde",
"rustfs-filemeta", "rustfs-filemeta",
"serde", "serde",
@@ -9295,7 +9274,6 @@ dependencies = [
name = "rustfs-extension-schema" name = "rustfs-extension-schema"
version = "1.0.0-beta.12" version = "1.0.0-beta.12"
dependencies = [ dependencies = [
"hotpath",
"serde", "serde",
"serde_json", "serde_json",
"thiserror 2.0.19", "thiserror 2.0.19",
@@ -9334,7 +9312,6 @@ dependencies = [
"async-trait", "async-trait",
"base64 0.23.0", "base64 0.23.0",
"futures", "futures",
"hotpath",
"http 1.5.0", "http 1.5.0",
"metrics", "metrics",
"rustfs-common", "rustfs-common",
@@ -9366,7 +9343,6 @@ dependencies = [
"async-trait", "async-trait",
"base64-simd", "base64-simd",
"futures", "futures",
"hotpath",
"http 1.5.0", "http 1.5.0",
"jsonwebtoken 11.0.0", "jsonwebtoken 11.0.0",
"moka", "moka",
@@ -9401,7 +9377,6 @@ name = "rustfs-io-core"
version = "1.0.0-beta.12" version = "1.0.0-beta.12"
dependencies = [ dependencies = [
"bytes", "bytes",
"hotpath",
"memmap2", "memmap2",
"rustfs-io-metrics", "rustfs-io-metrics",
"thiserror 2.0.19", "thiserror 2.0.19",
@@ -9414,7 +9389,6 @@ name = "rustfs-io-metrics"
version = "1.0.0-beta.12" version = "1.0.0-beta.12"
dependencies = [ dependencies = [
"criterion", "criterion",
"hotpath",
"metrics", "metrics",
"metrics-util", "metrics-util",
"num_cpus", "num_cpus",
@@ -9481,7 +9455,6 @@ version = "1.0.0-beta.12"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures", "futures",
"hotpath",
"http 1.5.0", "http 1.5.0",
"http-body 1.1.0", "http-body 1.1.0",
"http-body-util", "http-body-util",
@@ -9514,12 +9487,9 @@ dependencies = [
"base64 0.23.0", "base64 0.23.0",
"chacha20poly1305", "chacha20poly1305",
"hex", "hex",
"hotpath",
"insta", "insta",
"jiff", "jiff",
"md-5 0.11.0", "md-5 0.11.0",
"metrics",
"metrics-util",
"moka", "moka",
"rand 0.10.2", "rand 0.10.2",
"reqwest", "reqwest",
@@ -9547,7 +9517,6 @@ name = "rustfs-lifecycle"
version = "1.0.0-beta.12" version = "1.0.0-beta.12"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"hotpath",
"metrics", "metrics",
"metrics-util", "metrics-util",
"proptest", "proptest",
@@ -9572,7 +9541,6 @@ dependencies = [
"async-trait", "async-trait",
"crossbeam-queue", "crossbeam-queue",
"futures", "futures",
"hotpath",
"parking_lot", "parking_lot",
"rand 0.10.2", "rand 0.10.2",
"rustfs-io-metrics", "rustfs-io-metrics",
@@ -9594,7 +9562,6 @@ version = "1.0.0-beta.12"
dependencies = [ dependencies = [
"chrono", "chrono",
"flate2", "flate2",
"hotpath",
"regex", "regex",
"serde", "serde",
"serde_json", "serde_json",
@@ -9612,7 +9579,6 @@ name = "rustfs-madmin"
version = "1.0.0-beta.12" version = "1.0.0-beta.12"
dependencies = [ dependencies = [
"chrono", "chrono",
"hotpath",
"humantime", "humantime",
"hyper", "hyper",
"rmp-serde", "rmp-serde",
@@ -9633,7 +9599,6 @@ dependencies = [
"criterion", "criterion",
"form_urlencoded", "form_urlencoded",
"hashbrown 0.17.1", "hashbrown 0.17.1",
"hotpath",
"metrics", "metrics",
"percent-encoding", "percent-encoding",
"quick-xml", "quick-xml",
@@ -9663,7 +9628,6 @@ version = "1.0.0-beta.12"
dependencies = [ dependencies = [
"criterion", "criterion",
"futures", "futures",
"hotpath",
"rustfs-config", "rustfs-config",
"rustfs-io-metrics", "rustfs-io-metrics",
"rustfs-utils", "rustfs-utils",
@@ -9682,7 +9646,6 @@ version = "1.0.0-beta.12"
dependencies = [ dependencies = [
"bytes", "bytes",
"criterion", "criterion",
"hotpath",
"metrics", "metrics",
"metrics-util", "metrics-util",
"moka", "moka",
@@ -9705,7 +9668,6 @@ dependencies = [
"flate2", "flate2",
"futures-util", "futures-util",
"glob", "glob",
"hotpath",
"jiff", "jiff",
"libc", "libc",
"metrics", "metrics",
@@ -9754,7 +9716,6 @@ dependencies = [
"base64-simd", "base64-simd",
"chrono", "chrono",
"futures", "futures",
"hotpath",
"ipnetwork", "ipnetwork",
"jsonwebtoken 11.0.0", "jsonwebtoken 11.0.0",
"moka", "moka",
@@ -9791,7 +9752,6 @@ dependencies = [
"futures-util", "futures-util",
"hex", "hex",
"hmac 0.13.0", "hmac 0.13.0",
"hotpath",
"http 1.5.0", "http 1.5.0",
"http-body-util", "http-body-util",
"hyper", "hyper",
@@ -9844,7 +9804,6 @@ name = "rustfs-protos"
version = "1.0.0-beta.12" version = "1.0.0-beta.12"
dependencies = [ dependencies = [
"flatbuffers", "flatbuffers",
"hotpath",
"prost 0.14.4", "prost 0.14.4",
"rmp-serde", "rmp-serde",
"rustfs-common", "rustfs-common",
@@ -9869,7 +9828,6 @@ version = "1.0.0-beta.12"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"bytes", "bytes",
"hotpath",
"regex", "regex",
"rmp", "rmp",
"rmp-serde", "rmp-serde",
@@ -9928,7 +9886,6 @@ dependencies = [
"chacha20poly1305", "chacha20poly1305",
"hex", "hex",
"hmac 0.13.0", "hmac 0.13.0",
"hotpath",
"minlz", "minlz",
"pin-project-lite", "pin-project-lite",
"rand 0.10.2", "rand 0.10.2",
@@ -9946,7 +9903,6 @@ dependencies = [
name = "rustfs-s3-ops" name = "rustfs-s3-ops"
version = "1.0.0-beta.12" version = "1.0.0-beta.12"
dependencies = [ dependencies = [
"hotpath",
"rustfs-s3-types", "rustfs-s3-types",
] ]
@@ -9954,7 +9910,6 @@ dependencies = [
name = "rustfs-s3-types" name = "rustfs-s3-types"
version = "1.0.0-beta.12" version = "1.0.0-beta.12"
dependencies = [ dependencies = [
"hotpath",
"serde", "serde",
"serde_json", "serde_json",
] ]
@@ -9969,7 +9924,6 @@ dependencies = [
"datafusion", "datafusion",
"futures", "futures",
"futures-core", "futures-core",
"hotpath",
"http 1.5.0", "http 1.5.0",
"metrics", "metrics",
"parking_lot", "parking_lot",
@@ -9998,7 +9952,6 @@ dependencies = [
"datafusion", "datafusion",
"derive_builder", "derive_builder",
"futures", "futures",
"hotpath",
"parking_lot", "parking_lot",
"rustfs-s3select-api", "rustfs-s3select-api",
"s3s", "s3s",
@@ -10016,7 +9969,6 @@ dependencies = [
"futures", "futures",
"hex-simd", "hex-simd",
"hmac 0.13.0", "hmac 0.13.0",
"hotpath",
"http 1.5.0", "http 1.5.0",
"metrics", "metrics",
"rand 0.10.2", "rand 0.10.2",
@@ -10049,7 +10001,6 @@ dependencies = [
name = "rustfs-security-governance" name = "rustfs-security-governance"
version = "1.0.0-beta.12" version = "1.0.0-beta.12"
dependencies = [ dependencies = [
"hotpath",
"thiserror 2.0.19", "thiserror 2.0.19",
] ]
@@ -10059,7 +10010,6 @@ version = "1.0.0-beta.12"
dependencies = [ dependencies = [
"base64-simd", "base64-simd",
"bytes", "bytes",
"hotpath",
"http 1.5.0", "http 1.5.0",
"hyper", "hyper",
"rustfs-utils", "rustfs-utils",
@@ -10076,7 +10026,6 @@ name = "rustfs-storage-api"
version = "1.0.0-beta.12" version = "1.0.0-beta.12"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"hotpath",
"insta", "insta",
"rustfs-filemeta", "rustfs-filemeta",
"serde", "serde",
@@ -10098,7 +10047,6 @@ dependencies = [
"deadpool-postgres", "deadpool-postgres",
"futures-util", "futures-util",
"hashbrown 0.17.1", "hashbrown 0.17.1",
"hotpath",
"hyper", "hyper",
"hyper-rustls", "hyper-rustls",
"lapin", "lapin",
@@ -10144,7 +10092,6 @@ dependencies = [
name = "rustfs-test-utils" name = "rustfs-test-utils"
version = "1.0.0-beta.12" version = "1.0.0-beta.12"
dependencies = [ dependencies = [
"hotpath",
"rustfs-data-usage", "rustfs-data-usage",
"rustfs-ecstore", "rustfs-ecstore",
"rustfs-storage-api", "rustfs-storage-api",
@@ -10161,7 +10108,6 @@ name = "rustfs-tls-runtime"
version = "1.0.0-beta.12" version = "1.0.0-beta.12"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"hotpath",
"metrics", "metrics",
"rcgen", "rcgen",
"rustfs-common", "rustfs-common",
@@ -10183,7 +10129,6 @@ version = "1.0.0-beta.12"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
"hotpath",
"http 1.5.0", "http 1.5.0",
"ipnetwork", "ipnetwork",
"metrics", "metrics",
@@ -10230,7 +10175,6 @@ dependencies = [
"hex-simd", "hex-simd",
"highway", "highway",
"hmac 0.13.0", "hmac 0.13.0",
"hotpath",
"http 1.5.0", "http 1.5.0",
"hyper", "hyper",
"local-ip-address", "local-ip-address",
@@ -10263,7 +10207,6 @@ dependencies = [
"astral-tokio-tar", "astral-tokio-tar",
"async-compression", "async-compression",
"criterion", "criterion",
"hotpath",
"tempfile", "tempfile",
"thiserror 2.0.19", "thiserror 2.0.19",
"tokio", "tokio",
@@ -10607,7 +10550,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.7", "generic-array 0.14.9",
"pkcs8 0.10.2", "pkcs8 0.10.2",
"subtle", "subtle",
"zeroize", "zeroize",
@@ -11597,7 +11540,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",
+2 -2
View File
@@ -250,8 +250,8 @@ enumset = "1.1.14"
faster-hex = "0.10.0" faster-hex = "0.10.0"
flate2 = "1.1.9" flate2 = "1.1.9"
glob = "0.3.4" glob = "0.3.4"
google-cloud-storage = "1.17.0" google-cloud-storage = "1.16.0"
google-cloud-auth = "1.15.0" google-cloud-auth = "1.14.0"
hashbrown = { version = "0.17.1" } hashbrown = { version = "0.17.1" }
hex = "0.4.3" hex = "0.4.3"
hex-simd = "0.8.0" hex-simd = "0.8.0"
-26
View File
@@ -25,33 +25,7 @@ documentation = "https://docs.rs/rustfs-audit/latest/rustfs_audit/"
keywords = ["audit", "target", "management", "fan-out", "RustFS"] keywords = ["audit", "target", "management", "fan-out", "RustFS"]
categories = ["web-programming", "development-tools", "asynchronous", "api-bindings"] categories = ["web-programming", "development-tools", "asynchronous", "api-bindings"]
[features]
default = []
hotpath = [
"hotpath/hotpath",
"hotpath/tokio",
"hotpath/futures",
"rustfs-config/hotpath",
"rustfs-s3-types/hotpath",
"rustfs-targets/hotpath",
]
hotpath-alloc = [
"hotpath",
"hotpath/hotpath-alloc",
"rustfs-config/hotpath-alloc",
"rustfs-s3-types/hotpath-alloc",
"rustfs-targets/hotpath-alloc",
]
hotpath-cpu = [
"hotpath",
"hotpath/hotpath-cpu",
"rustfs-config/hotpath-cpu",
"rustfs-s3-types/hotpath-cpu",
"rustfs-targets/hotpath-cpu",
]
[dependencies] [dependencies]
hotpath.workspace = true
rustfs-targets = { workspace = true } rustfs-targets = { workspace = true }
rustfs-config = { workspace = true, features = ["audit", "server-config-model"] } rustfs-config = { workspace = true, features = ["audit", "server-config-model"] }
rustfs-s3-types = { workspace = true } rustfs-s3-types = { workspace = true }
-7
View File
@@ -28,14 +28,7 @@ documentation = "https://docs.rs/rustfs-checksums/latest/rustfs_checksum/"
[lints] [lints]
workspace = true workspace = true
[features]
default = []
hotpath = ["hotpath/hotpath"]
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
[dependencies] [dependencies]
hotpath.workspace = true
bytes = { workspace = true, features = ["serde"] } bytes = { workspace = true, features = ["serde"] }
crc-fast = { workspace = true } crc-fast = { workspace = true }
http = { workspace = true } http = { workspace = true }
-7
View File
@@ -27,14 +27,7 @@ categories = ["web-programming", "development-tools", "data-structures"]
[lints] [lints]
workspace = true workspace = true
[features]
default = []
hotpath = ["hotpath/hotpath", "hotpath/tokio"]
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
[dependencies] [dependencies]
hotpath.workspace = true
tokio = { workspace = true, features = ["fs", "rt-multi-thread"] } tokio = { workspace = true, features = ["fs", "rt-multi-thread"] }
tonic = { workspace = true, features = ["gzip", "deflate"] } tonic = { workspace = true, features = ["gzip", "deflate"] }
uuid = { workspace = true, features = ["v4", "fast-rng", "macro-diagnostics"] } uuid = { workspace = true, features = ["v4", "fast-rng", "macro-diagnostics"] }
-7
View File
@@ -13,14 +13,7 @@ categories = ["concurrency", "filesystem"]
[lints] [lints]
workspace = true workspace = true
[features]
default = []
hotpath = ["hotpath/hotpath", "hotpath/tokio", "rustfs-io-core/hotpath"]
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc", "rustfs-io-core/hotpath-alloc"]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu", "rustfs-io-core/hotpath-cpu"]
[dependencies] [dependencies]
hotpath.workspace = true
# Internal crates # Internal crates
rustfs-io-core = { workspace = true } rustfs-io-core = { workspace = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
-4
View File
@@ -25,7 +25,6 @@ keywords = ["configuration", "settings", "management", "rustfs", "Minio"]
categories = ["web-programming", "development-tools", "config"] categories = ["web-programming", "development-tools", "config"]
[dependencies] [dependencies]
hotpath.workspace = true
const-str = { workspace = true, optional = true, features = ["std", "proc"] } const-str = { workspace = true, optional = true, features = ["std", "proc"] }
serde = { workspace = true, optional = true, features = ["derive"] } serde = { workspace = true, optional = true, features = ["derive"] }
serde_json = { workspace = true, optional = true, features = ["raw_value"] } serde_json = { workspace = true, optional = true, features = ["raw_value"] }
@@ -35,9 +34,6 @@ workspace = true
[features] [features]
default = ["constants"] default = ["constants"]
hotpath = ["hotpath/hotpath"]
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
audit = ["dep:const-str", "constants"] audit = ["dep:const-str", "constants"]
constants = ["dep:const-str"] constants = ["dep:const-str"]
notify = ["dep:const-str", "constants"] notify = ["dep:const-str", "constants"]
-7
View File
@@ -24,14 +24,7 @@ description = "Credentials management utilities for RustFS, enabling secure hand
keywords = ["rustfs", "Minio", "credentials", "authentication", "authorization"] keywords = ["rustfs", "Minio", "credentials", "authentication", "authorization"]
categories = ["web-programming", "development-tools", "data-structures", "security"] categories = ["web-programming", "development-tools", "data-structures", "security"]
[features]
default = []
hotpath = ["hotpath/hotpath"]
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
[dependencies] [dependencies]
hotpath.workspace = true
base64-simd = { workspace = true } base64-simd = { workspace = true }
hmac = { workspace = true } hmac = { workspace = true }
rand = { workspace = true, features = ["serde"] } rand = { workspace = true, features = ["serde"] }
-4
View File
@@ -29,7 +29,6 @@ documentation = "https://docs.rs/rustfs-crypto/latest/rustfs_crypto/"
workspace = true workspace = true
[dependencies] [dependencies]
hotpath.workspace = true
aes-gcm = { workspace = true, optional = true, features = ["rand_core"] } aes-gcm = { workspace = true, optional = true, features = ["rand_core"] }
argon2 = { workspace = true, optional = true } argon2 = { workspace = true, optional = true }
chacha20poly1305 = { workspace = true, optional = true } chacha20poly1305 = { workspace = true, optional = true }
@@ -50,9 +49,6 @@ time = { workspace = true, features = ["parsing", "formatting", "macros", "serde
[features] [features]
default = ["crypto", "fips"] default = ["crypto", "fips"]
hotpath = ["hotpath/hotpath"]
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
fips = [] fips = []
crypto = [ crypto = [
"dep:aes-gcm", "dep:aes-gcm",
-7
View File
@@ -27,14 +27,7 @@ categories = ["data-structures", "filesystem"]
[lints] [lints]
workspace = true workspace = true
[features]
default = []
hotpath = ["hotpath/hotpath", "rustfs-filemeta/hotpath"]
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc", "rustfs-filemeta/hotpath-alloc"]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu", "rustfs-filemeta/hotpath-cpu"]
[dependencies] [dependencies]
hotpath.workspace = true
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
rmp-serde = { workspace = true } rmp-serde = { workspace = true }
async-trait = { workspace = true } async-trait = { workspace = true }
-48
View File
@@ -25,58 +25,10 @@ workspace = true
[features] [features]
default = [] default = []
hotpath = [
"hotpath/hotpath",
"hotpath/tokio",
"hotpath/futures",
"hotpath/reqwest-0-13",
"rustfs-config/hotpath",
"rustfs-credentials/hotpath",
"rustfs-data-usage/hotpath",
"rustfs-ecstore/hotpath",
"rustfs-filemeta/hotpath",
"rustfs-lock/hotpath",
"rustfs-madmin/hotpath",
"rustfs-protos/hotpath",
"rustfs-rio/hotpath",
"rustfs-signer/hotpath",
"rustfs-utils/hotpath",
]
hotpath-alloc = [
"hotpath",
"hotpath/hotpath-alloc",
"rustfs-config/hotpath-alloc",
"rustfs-credentials/hotpath-alloc",
"rustfs-data-usage/hotpath-alloc",
"rustfs-ecstore/hotpath-alloc",
"rustfs-filemeta/hotpath-alloc",
"rustfs-lock/hotpath-alloc",
"rustfs-madmin/hotpath-alloc",
"rustfs-protos/hotpath-alloc",
"rustfs-rio/hotpath-alloc",
"rustfs-signer/hotpath-alloc",
"rustfs-utils/hotpath-alloc",
]
hotpath-cpu = [
"hotpath",
"hotpath/hotpath-cpu",
"rustfs-config/hotpath-cpu",
"rustfs-credentials/hotpath-cpu",
"rustfs-data-usage/hotpath-cpu",
"rustfs-ecstore/hotpath-cpu",
"rustfs-filemeta/hotpath-cpu",
"rustfs-lock/hotpath-cpu",
"rustfs-madmin/hotpath-cpu",
"rustfs-protos/hotpath-cpu",
"rustfs-rio/hotpath-cpu",
"rustfs-signer/hotpath-cpu",
"rustfs-utils/hotpath-cpu",
]
ftps = [] ftps = []
sftp = [] sftp = []
[dependencies] [dependencies]
hotpath.workspace = true
rustfs-config = { workspace = true, features = ["constants"] } rustfs-config = { workspace = true, features = ["constants"] }
rustfs-credentials.workspace = true rustfs-credentials.workspace = true
rustfs-ecstore.workspace = true rustfs-ecstore.workspace = true
@@ -26,6 +26,7 @@
use super::common::*; use super::common::*;
use aws_sdk_s3::Client; use aws_sdk_s3::Client;
use aws_sdk_s3::error::ProvideErrorMetadata;
use aws_sdk_s3::primitives::{ByteStream, DateTimeFormat}; use aws_sdk_s3::primitives::{ByteStream, DateTimeFormat};
use aws_sdk_s3::types::{ use aws_sdk_s3::types::{
CompletedMultipartUpload, CompletedPart, Delete, MetadataDirective, ObjectIdentifier, ObjectLockLegalHoldStatus, CompletedMultipartUpload, CompletedPart, Delete, MetadataDirective, ObjectIdentifier, ObjectLockLegalHoldStatus,
@@ -2120,6 +2121,127 @@ async fn test_multipart_default_retention_fixed_at_create() {
// Versioning Auto-Enable Tests // Versioning Auto-Enable Tests
// ============================================================================ // ============================================================================
#[tokio::test]
#[serial]
async fn test_unretained_object_lock_object_delete_and_bucket_cleanup() {
init_logging();
info!("🧪 Test: Unretained Object Lock object delete and bucket cleanup (Issue #5339)");
let mut env = ObjectLockTestEnvironment::new()
.await
.expect("failed to create Object Lock test environment");
env.start_rustfs().await.expect("failed to start RustFS");
let bucket = "test-object-lock-delete-cleanup";
let key = "unretained-object";
env.create_object_lock_bucket(bucket)
.await
.expect("failed to create Object Lock bucket");
let client = env.s3_client();
let put_response = client
.put_object()
.bucket(bucket)
.key(key)
.body(ByteStream::from_static(b"unretained data"))
.send()
.await
.expect("failed to upload unretained object");
let object_version_id = put_response
.version_id()
.expect("Object Lock buckets must create versioned objects")
.to_string();
let delete_response = client
.delete_object()
.bucket(bucket)
.key(key)
.send()
.await
.expect("failed to create delete marker");
assert_eq!(delete_response.delete_marker(), Some(true));
let delete_marker_version_id = delete_response
.version_id()
.expect("Deleting without a version ID must create a delete marker")
.to_string();
let get_error = client
.get_object()
.bucket(bucket)
.key(key)
.send()
.await
.expect_err("GET must not return an object hidden by a delete marker");
assert_eq!(get_error.raw_response().map(|response| response.status().as_u16()), Some(404));
assert_eq!(get_error.as_service_error().and_then(|error| error.code()), Some("NoSuchKey"));
let listed_objects = client
.list_objects_v2()
.bucket(bucket)
.send()
.await
.expect("failed to list current objects");
assert!(
listed_objects.contents().iter().all(|object| object.key() != Some(key)),
"ListObjectsV2 must hide objects whose latest version is a delete marker"
);
let listed_versions = client
.list_object_versions()
.bucket(bucket)
.send()
.await
.expect("failed to list object versions");
assert!(
listed_versions
.versions()
.iter()
.any(|version| version.key() == Some(key) && version.version_id() == Some(object_version_id.as_str())),
"The data version must remain until it is explicitly deleted"
);
assert!(
listed_versions
.delete_markers()
.iter()
.any(|marker| marker.key() == Some(key) && marker.version_id() == Some(delete_marker_version_id.as_str())),
"ListObjectVersions must expose the delete marker"
);
client
.delete_object()
.bucket(bucket)
.key(key)
.version_id(object_version_id)
.send()
.await
.expect("failed to delete the data version");
client
.delete_object()
.bucket(bucket)
.key(key)
.version_id(delete_marker_version_id)
.send()
.await
.expect("failed to delete the delete marker");
let remaining_versions = client
.list_object_versions()
.bucket(bucket)
.send()
.await
.expect("failed to list versions after cleanup");
assert!(remaining_versions.versions().is_empty());
assert!(remaining_versions.delete_markers().is_empty());
client
.delete_bucket()
.bucket(bucket)
.send()
.await
.expect("Deleting every version must remove xl.meta so the bucket can be deleted normally");
}
#[tokio::test] #[tokio::test]
#[serial] #[serial]
async fn test_versioning_auto_enabled_with_object_lock() { async fn test_versioning_auto_enabled_with_object_lock() {
-70
View File
@@ -40,83 +40,13 @@ hotpath = [
"hotpath/async-channel", "hotpath/async-channel",
"hotpath/parking_lot", "hotpath/parking_lot",
"hotpath/reqwest-0-13", "hotpath/reqwest-0-13",
"rustfs-checksums/hotpath",
"rustfs-common/hotpath",
"rustfs-concurrency/hotpath",
"rustfs-config/hotpath",
"rustfs-credentials/hotpath",
"rustfs-data-usage/hotpath",
"rustfs-filemeta/hotpath", "rustfs-filemeta/hotpath",
"rustfs-io-metrics/hotpath",
"rustfs-lifecycle/hotpath",
"rustfs-lock/hotpath",
"rustfs-madmin/hotpath",
"rustfs-object-capacity/hotpath",
"rustfs-policy/hotpath",
"rustfs-protos/hotpath",
"rustfs-replication/hotpath",
"rustfs-rio/hotpath", "rustfs-rio/hotpath",
"rustfs-rio-v2?/hotpath",
"rustfs-s3-types/hotpath",
"rustfs-signer/hotpath",
"rustfs-storage-api/hotpath",
"rustfs-tls-runtime/hotpath",
"rustfs-utils/hotpath",
"rustfs-crypto/hotpath",
] ]
hotpath-alloc = [ hotpath-alloc = [
"hotpath",
"hotpath/hotpath-alloc", "hotpath/hotpath-alloc",
"rustfs-checksums/hotpath-alloc",
"rustfs-common/hotpath-alloc",
"rustfs-concurrency/hotpath-alloc",
"rustfs-config/hotpath-alloc",
"rustfs-credentials/hotpath-alloc",
"rustfs-data-usage/hotpath-alloc",
"rustfs-filemeta/hotpath-alloc", "rustfs-filemeta/hotpath-alloc",
"rustfs-io-metrics/hotpath-alloc",
"rustfs-lifecycle/hotpath-alloc",
"rustfs-lock/hotpath-alloc",
"rustfs-madmin/hotpath-alloc",
"rustfs-object-capacity/hotpath-alloc",
"rustfs-policy/hotpath-alloc",
"rustfs-protos/hotpath-alloc",
"rustfs-replication/hotpath-alloc",
"rustfs-rio/hotpath-alloc", "rustfs-rio/hotpath-alloc",
"rustfs-rio-v2?/hotpath-alloc",
"rustfs-s3-types/hotpath-alloc",
"rustfs-signer/hotpath-alloc",
"rustfs-storage-api/hotpath-alloc",
"rustfs-tls-runtime/hotpath-alloc",
"rustfs-utils/hotpath-alloc",
"rustfs-crypto/hotpath-alloc",
]
hotpath-cpu = [
"hotpath",
"hotpath/hotpath-cpu",
"rustfs-checksums/hotpath-cpu",
"rustfs-common/hotpath-cpu",
"rustfs-concurrency/hotpath-cpu",
"rustfs-config/hotpath-cpu",
"rustfs-credentials/hotpath-cpu",
"rustfs-data-usage/hotpath-cpu",
"rustfs-filemeta/hotpath-cpu",
"rustfs-io-metrics/hotpath-cpu",
"rustfs-lifecycle/hotpath-cpu",
"rustfs-lock/hotpath-cpu",
"rustfs-madmin/hotpath-cpu",
"rustfs-object-capacity/hotpath-cpu",
"rustfs-policy/hotpath-cpu",
"rustfs-protos/hotpath-cpu",
"rustfs-replication/hotpath-cpu",
"rustfs-rio/hotpath-cpu",
"rustfs-rio-v2?/hotpath-cpu",
"rustfs-s3-types/hotpath-cpu",
"rustfs-signer/hotpath-cpu",
"rustfs-storage-api/hotpath-cpu",
"rustfs-tls-runtime/hotpath-cpu",
"rustfs-utils/hotpath-cpu",
"rustfs-crypto/hotpath-cpu",
] ]
# Exposes shared lifecycle/tier test utilities (MockWarmBackend, fault # Exposes shared lifecycle/tier test utilities (MockWarmBackend, fault
# injection, xl.meta transition assertions) via `api::tier::test_util`. # injection, xl.meta transition assertions) via `api::tier::test_util`.
+6 -93
View File
@@ -579,26 +579,8 @@ pub struct BucketMetadataSys {
/// Serializes metadata-map commits and their derived cache updates for one /// Serializes metadata-map commits and their derived cache updates for one
/// bucket. Namespace locks, when present, are acquired before this lock. /// bucket. Namespace locks, when present, are acquired before this lock.
metadata_publish_locks: Arc<MetadataPublishLockRegistry>, metadata_publish_locks: Arc<MetadataPublishLockRegistry>,
/// Deduplicates concurrent lazy loads of one bucket's metadata, so N
/// simultaneous cache misses issue a single disk read instead of N.
///
/// This is the `singleflight` that upstream applies to its own lazy
/// `GetConfig`. Without it the namespace *read* lock the load holds is no
/// help: read locks are shared, so it excludes concurrent config writers
/// but not concurrent readers, and every caller still pays a full
/// erasure-set metadata fanout. A separate registry from
/// `metadata_publish_locks`, reusing the same per-bucket lock machinery.
///
/// Lock order: this lock, then the namespace lock, then the publish lock,
/// then the metadata map. It is only ever taken as the first of those, so
/// it cannot invert against a path that already holds one of the others.
lazy_load_locks: Arc<MetadataPublishLockRegistry>,
#[cfg(test)] #[cfg(test)]
lazy_load_lock_probe: std::sync::atomic::AtomicBool, lazy_load_lock_probe: std::sync::atomic::AtomicBool,
/// Counts disk loads taken by the lazy `get_config` path, so a test can
/// prove concurrent misses collapse into one.
#[cfg(test)]
lazy_disk_loads: std::sync::atomic::AtomicUsize,
/// Buckets recently observed to have no persisted metadata. Serving the /// Buckets recently observed to have no persisted metadata. Serving the
/// fabricated default from here (instead of re-reading disk) keeps the /// fabricated default from here (instead of re-reading disk) keeps the
/// per-request cost of repeated lookups for such names bounded — without /// per-request cost of repeated lookups for such names bounded — without
@@ -617,13 +599,8 @@ impl BucketMetadataSys {
metadata_publish_locks: Arc::new(MetadataPublishLockRegistry { metadata_publish_locks: Arc::new(MetadataPublishLockRegistry {
locks: StdMutex::new(HashMap::new()), locks: StdMutex::new(HashMap::new()),
}), }),
lazy_load_locks: Arc::new(MetadataPublishLockRegistry {
locks: StdMutex::new(HashMap::new()),
}),
#[cfg(test)] #[cfg(test)]
lazy_load_lock_probe: std::sync::atomic::AtomicBool::new(false), lazy_load_lock_probe: std::sync::atomic::AtomicBool::new(false),
#[cfg(test)]
lazy_disk_loads: std::sync::atomic::AtomicUsize::new(0),
absent_metadata: moka::future::Cache::builder() absent_metadata: moka::future::Cache::builder()
.max_capacity(ABSENT_BUCKET_METADATA_MAX_ENTRIES) .max_capacity(ABSENT_BUCKET_METADATA_MAX_ENTRIES)
.time_to_live(ABSENT_BUCKET_METADATA_TTL) .time_to_live(ABSENT_BUCKET_METADATA_TTL)
@@ -638,22 +615,16 @@ impl BucketMetadataSys {
} }
fn metadata_publish_lock(&self, bucket: &str) -> Arc<Mutex<MetadataPublishLockState>> { fn metadata_publish_lock(&self, bucket: &str) -> Arc<Mutex<MetadataPublishLockState>> {
Self::bucket_lock_in(&self.metadata_publish_locks, bucket) let mut locks = self
} .metadata_publish_locks
.locks
/// Per-bucket gate for the lazy `get_config` disk load. See .lock()
/// [`Self::lazy_load_locks`]. .unwrap_or_else(|poisoned| poisoned.into_inner());
fn lazy_load_lock(&self, bucket: &str) -> Arc<Mutex<MetadataPublishLockState>> {
Self::bucket_lock_in(&self.lazy_load_locks, bucket)
}
fn bucket_lock_in(registry: &Arc<MetadataPublishLockRegistry>, bucket: &str) -> Arc<Mutex<MetadataPublishLockState>> {
let mut locks = registry.locks.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
locks.get(bucket).and_then(Weak::upgrade).unwrap_or_else(|| { locks.get(bucket).and_then(Weak::upgrade).unwrap_or_else(|| {
let lock = Arc::new_cyclic(|lock| { let lock = Arc::new_cyclic(|lock| {
Mutex::new(MetadataPublishLockState { Mutex::new(MetadataPublishLockState {
bucket: bucket.to_string(), bucket: bucket.to_string(),
registry: Arc::downgrade(registry), registry: Arc::downgrade(&self.metadata_publish_locks),
lock: lock.clone(), lock: lock.clone(),
}) })
}); });
@@ -1108,27 +1079,6 @@ impl BucketMetadataSys {
return Ok((Arc::new(bm), true)); return Ok((Arc::new(bm), true));
} }
// Collapse concurrent misses for this bucket into one disk load.
// Taken before the namespace lock — see `lazy_load_locks` for the
// ordering rule.
let load_lock = self.lazy_load_lock(bucket);
let _load_guard = load_lock.lock_owned().await;
// Re-check both caches: whoever held the gate before us may have
// already answered this exact question, and repeating the fanout
// is the whole cost this gate exists to avoid.
if let Some(bm) = self.metadata_map.read().await.get(bucket).cloned() {
return Ok((bm, true));
}
if self.absent_metadata.get(bucket).await.is_some() {
let mut bm = BucketMetadata::new(bucket);
bm.default_timestamps();
return Ok((Arc::new(bm), true));
}
#[cfg(test)]
self.lazy_disk_loads.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let lock = self.api.new_ns_lock(bucket, bucket).await?; let lock = self.api.new_ns_lock(bucket, bucket).await?;
let guard = lock.get_read_lock(crate::set_disk::get_lock_acquire_timeout()).await?; let guard = lock.get_read_lock(crate::set_disk::get_lock_acquire_timeout()).await?;
#[cfg(test)] #[cfg(test)]
@@ -1481,43 +1431,6 @@ mod tests {
use serial_test::serial; use serial_test::serial;
use tokio::time::timeout; use tokio::time::timeout;
/// Concurrent cache misses for one bucket must collapse into a single disk
/// load.
///
/// The namespace read lock the lazy path already holds does not provide
/// this: read locks are shared, so it excludes concurrent config writers
/// but not concurrent readers. Without the dedup gate every caller pays its
/// own namespace-lock acquisition plus a full erasure-set metadata fanout —
/// and the paths that reach `get_config` are per-request, so the multiplier
/// is request concurrency.
#[tokio::test]
async fn concurrent_lazy_loads_of_one_bucket_issue_a_single_disk_read() {
use std::sync::atomic::Ordering;
let (_dirs, ecstore) = isolated_store_over_temp_disks().await;
let sys = Arc::new(BucketMetadataSys::new(ecstore));
// A name with no persisted metadata: every caller misses the map, and
// the absent-cache entry does not exist until the first load records it.
let bucket = "singleflight-bucket";
let waiters = 8;
let results = futures::future::join_all((0..waiters).map(|_| {
let sys = Arc::clone(&sys);
async move { sys.get_config(bucket).await.map(|(bm, _)| bm.name.clone()) }
}))
.await;
for result in results {
assert_eq!(result.expect("every caller must get an answer"), bucket);
}
assert_eq!(
sys.lazy_disk_loads.load(Ordering::Relaxed),
1,
"concurrent misses for one bucket must share a single disk load"
);
}
/// Pins the fail-closed caching contract of the lazy `get_config` path /// Pins the fail-closed caching contract of the lazy `get_config` path
/// and the refresh no-replace rule: fabricated defaults are returned but /// and the refresh no-replace rule: fabricated defaults are returned but
/// never served by the map-only `get()`, persisted metadata is cached on /// never served by the map-only `get()`, persisted metadata is cached on
-2
View File
@@ -12,8 +12,6 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
#![recursion_limit = "256"]
/// Scope-based hotpath measurement for `#[async_trait]` methods, where /// Scope-based hotpath measurement for `#[async_trait]` methods, where
/// `#[hotpath::measure]` would only time the boxed-future construction. /// `#[hotpath::measure]` would only time the boxed-future construction.
/// The guard records wall time from this statement until the enclosing /// The guard records wall time from this statement until the enclosing
-7
View File
@@ -27,14 +27,7 @@ categories = ["web-programming", "development-tools"]
[lib] [lib]
doctest = false doctest = false
[features]
default = []
hotpath = ["hotpath/hotpath"]
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
[dependencies] [dependencies]
hotpath.workspace = true
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
thiserror.workspace = true thiserror.workspace = true
+2 -3
View File
@@ -27,9 +27,8 @@ documentation = "https://docs.rs/rustfs-filemeta/latest/rustfs_filemeta/"
[features] [features]
default = [] default = []
hotpath = ["hotpath/hotpath", "hotpath/tokio", "rustfs-utils/hotpath"] hotpath = ["hotpath/hotpath", "hotpath/tokio"]
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc", "rustfs-utils/hotpath-alloc"] hotpath-alloc = ["hotpath/hotpath-alloc"]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu", "rustfs-utils/hotpath-cpu"]
[dependencies] [dependencies]
hotpath.workspace = true hotpath.workspace = true
-41
View File
@@ -29,48 +29,7 @@ categories = ["web-programming", "development-tools", "filesystem"]
[lints] [lints]
workspace = true workspace = true
[features]
default = []
hotpath = [
"hotpath/hotpath",
"hotpath/tokio",
"hotpath/futures",
"rustfs-common/hotpath",
"rustfs-concurrency/hotpath",
"rustfs-config/hotpath",
"rustfs-ecstore/hotpath",
"rustfs-madmin/hotpath",
"rustfs-storage-api/hotpath",
"rustfs-utils/hotpath",
"rustfs-test-utils/hotpath",
]
hotpath-alloc = [
"hotpath",
"hotpath/hotpath-alloc",
"rustfs-common/hotpath-alloc",
"rustfs-concurrency/hotpath-alloc",
"rustfs-config/hotpath-alloc",
"rustfs-ecstore/hotpath-alloc",
"rustfs-madmin/hotpath-alloc",
"rustfs-storage-api/hotpath-alloc",
"rustfs-utils/hotpath-alloc",
"rustfs-test-utils/hotpath-alloc",
]
hotpath-cpu = [
"hotpath",
"hotpath/hotpath-cpu",
"rustfs-common/hotpath-cpu",
"rustfs-concurrency/hotpath-cpu",
"rustfs-config/hotpath-cpu",
"rustfs-ecstore/hotpath-cpu",
"rustfs-madmin/hotpath-cpu",
"rustfs-storage-api/hotpath-cpu",
"rustfs-utils/hotpath-cpu",
"rustfs-test-utils/hotpath-cpu",
]
[dependencies] [dependencies]
hotpath.workspace = true
rustfs-config = { workspace = true } rustfs-config = { workspace = true }
rustfs-concurrency = { workspace = true } rustfs-concurrency = { workspace = true }
rustfs-ecstore = { workspace = true } rustfs-ecstore = { workspace = true }
-1
View File
@@ -159,7 +159,6 @@ impl ErasureSetHealer {
/// execute erasure set heal with resume /// execute erasure set heal with resume
#[tracing::instrument(skip(self, buckets), fields(set_disk_id = %set_disk_id, bucket_count = buckets.len()))] #[tracing::instrument(skip(self, buckets), fields(set_disk_id = %set_disk_id, bucket_count = buckets.len()))]
#[hotpath::measure]
pub async fn heal_erasure_set(&self, buckets: &[String], set_disk_id: &str) -> Result<()> { pub async fn heal_erasure_set(&self, buckets: &[String], set_disk_id: &str) -> Result<()> {
debug!( debug!(
target: "rustfs::heal::erasure_healer", target: "rustfs::heal::erasure_healer",
-3
View File
@@ -584,7 +584,6 @@ impl HealTask {
} }
#[tracing::instrument(skip(self), fields(task_id = %self.id, heal_type = ?self.heal_type))] #[tracing::instrument(skip(self), fields(task_id = %self.id, heal_type = ?self.heal_type))]
#[hotpath::measure]
pub async fn execute(&self) -> Result<()> { pub async fn execute(&self) -> Result<()> {
// update status and timestamps atomically to avoid race conditions // update status and timestamps atomically to avoid race conditions
let now = SystemTime::now(); let now = SystemTime::now();
@@ -760,7 +759,6 @@ impl HealTask {
// specific heal implementation method // specific heal implementation method
#[tracing::instrument(skip(self), fields(bucket = %bucket, object = %object, version_id = ?version_id))] #[tracing::instrument(skip(self), fields(bucket = %bucket, object = %object, version_id = ?version_id))]
#[hotpath::measure]
async fn heal_object(&self, bucket: &str, object: &str, version_id: Option<&str>) -> Result<()> { async fn heal_object(&self, bucket: &str, object: &str, version_id: Option<&str>) -> Result<()> {
debug!( debug!(
target: "rustfs::heal::task", target: "rustfs::heal::task",
@@ -1406,7 +1404,6 @@ impl HealTask {
self.heal_bucket_objects(bucket, prefix).await self.heal_bucket_objects(bucket, prefix).await
} }
#[hotpath::measure]
async fn heal_bucket_objects(&self, bucket: &str, prefix: &str) -> Result<()> { async fn heal_bucket_objects(&self, bucket: &str, prefix: &str) -> Result<()> {
let mut continuation_token: Option<String> = None; let mut continuation_token: Option<String> = None;
let mut scanned = 0u64; let mut scanned = 0u64;
-48
View File
@@ -28,55 +28,7 @@ documentation = "https://docs.rs/rustfs-iam/latest/rustfs_iam/"
[lints] [lints]
workspace = true workspace = true
[features]
default = []
hotpath = [
"hotpath/hotpath",
"hotpath/tokio",
"hotpath/futures",
"hotpath/reqwest-0-13",
"rustfs-config/hotpath",
"rustfs-credentials/hotpath",
"rustfs-crypto/hotpath",
"rustfs-ecstore/hotpath",
"rustfs-io-metrics/hotpath",
"rustfs-madmin/hotpath",
"rustfs-policy/hotpath",
"rustfs-storage-api/hotpath",
"rustfs-utils/hotpath",
"rustfs-test-utils/hotpath",
]
hotpath-alloc = [
"hotpath",
"hotpath/hotpath-alloc",
"rustfs-config/hotpath-alloc",
"rustfs-credentials/hotpath-alloc",
"rustfs-crypto/hotpath-alloc",
"rustfs-ecstore/hotpath-alloc",
"rustfs-io-metrics/hotpath-alloc",
"rustfs-madmin/hotpath-alloc",
"rustfs-policy/hotpath-alloc",
"rustfs-storage-api/hotpath-alloc",
"rustfs-utils/hotpath-alloc",
"rustfs-test-utils/hotpath-alloc",
]
hotpath-cpu = [
"hotpath",
"hotpath/hotpath-cpu",
"rustfs-config/hotpath-cpu",
"rustfs-credentials/hotpath-cpu",
"rustfs-crypto/hotpath-cpu",
"rustfs-ecstore/hotpath-cpu",
"rustfs-io-metrics/hotpath-cpu",
"rustfs-madmin/hotpath-cpu",
"rustfs-policy/hotpath-cpu",
"rustfs-storage-api/hotpath-cpu",
"rustfs-utils/hotpath-cpu",
"rustfs-test-utils/hotpath-cpu",
]
[dependencies] [dependencies]
hotpath.workspace = true
rustfs-credentials = { workspace = true } rustfs-credentials = { workspace = true }
rustfs-config = { workspace = true, features = ["server-config-model"] } rustfs-config = { workspace = true, features = ["server-config-model"] }
tokio = { workspace = true, features = ["fs", "rt-multi-thread"] } tokio = { workspace = true, features = ["fs", "rt-multi-thread"] }
-2
View File
@@ -469,7 +469,6 @@ impl ObjectStore {
}); });
} }
#[hotpath::measure]
async fn list_all_iamconfig_items(&self) -> Result<HashMap<String, Vec<String>>> { async fn list_all_iamconfig_items(&self) -> Result<HashMap<String, Vec<String>>> {
let (tx, mut rx) = mpsc::channel::<StringOrErr>(100); let (tx, mut rx) = mpsc::channel::<StringOrErr>(100);
@@ -509,7 +508,6 @@ impl ObjectStore {
Ok(res) Ok(res)
} }
#[hotpath::measure]
async fn load_policy_doc_concurrent(&self, names: &[String], mode: LoadMode) -> Result<Vec<PolicyDoc>> { async fn load_policy_doc_concurrent(&self, names: &[String], mode: LoadMode) -> Result<Vec<PolicyDoc>> {
let mut futures = Vec::with_capacity(names.len()); let mut futures = Vec::with_capacity(names.len());
-7
View File
@@ -27,14 +27,7 @@ categories = ["development-tools", "filesystem"]
[lints] [lints]
workspace = true workspace = true
[features]
default = []
hotpath = ["hotpath/hotpath", "hotpath/tokio", "rustfs-io-metrics/hotpath"]
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc", "rustfs-io-metrics/hotpath-alloc"]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu", "rustfs-io-metrics/hotpath-cpu"]
[dependencies] [dependencies]
hotpath.workspace = true
bytes = { workspace = true, features = ["serde"] } bytes = { workspace = true, features = ["serde"] }
thiserror = { workspace = true } thiserror = { workspace = true }
tokio = { workspace = true, features = ["io-util", "fs", "sync", "rt-multi-thread"] } tokio = { workspace = true, features = ["io-util", "fs", "sync", "rt-multi-thread"] }
-25
View File
@@ -28,32 +28,7 @@ categories = ["development-tools", "filesystem"]
name = "metrics_pipeline" name = "metrics_pipeline"
harness = false harness = false
[features]
default = []
hotpath = [
"hotpath/hotpath",
"hotpath/tokio",
"rustfs-common/hotpath",
"rustfs-s3-ops/hotpath",
"rustfs-utils/hotpath",
]
hotpath-alloc = [
"hotpath",
"hotpath/hotpath-alloc",
"rustfs-common/hotpath-alloc",
"rustfs-s3-ops/hotpath-alloc",
"rustfs-utils/hotpath-alloc",
]
hotpath-cpu = [
"hotpath",
"hotpath/hotpath-cpu",
"rustfs-common/hotpath-cpu",
"rustfs-s3-ops/hotpath-cpu",
"rustfs-utils/hotpath-cpu",
]
[dependencies] [dependencies]
hotpath.workspace = true
metrics = { workspace = true } metrics = { workspace = true }
rustfs-common = { workspace = true } rustfs-common = { workspace = true }
rustfs-s3-ops = { workspace = true } rustfs-s3-ops = { workspace = true }
-27
View File
@@ -28,34 +28,7 @@ authors.workspace = true
[lints] [lints]
workspace = true workspace = true
[features]
default = []
hotpath = [
"hotpath/hotpath",
"hotpath/tokio",
"hotpath/futures",
"hotpath/reqwest-0-13",
"rustfs-credentials/hotpath",
"rustfs-policy/hotpath",
"rustfs-utils/hotpath",
]
hotpath-alloc = [
"hotpath",
"hotpath/hotpath-alloc",
"rustfs-credentials/hotpath-alloc",
"rustfs-policy/hotpath-alloc",
"rustfs-utils/hotpath-alloc",
]
hotpath-cpu = [
"hotpath",
"hotpath/hotpath-cpu",
"rustfs-credentials/hotpath-cpu",
"rustfs-policy/hotpath-cpu",
"rustfs-utils/hotpath-cpu",
]
[dependencies] [dependencies]
hotpath.workspace = true
tokio = { workspace = true, features = ["rt", "sync"] } tokio = { workspace = true, features = ["rt", "sync"] }
reqwest = { workspace = true, features = ["json"] } reqwest = { workspace = true, features = ["json"] }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
-2
View File
@@ -95,7 +95,6 @@ impl KeystoneClient {
} }
/// Validate a Keystone token /// Validate a Keystone token
#[hotpath::measure]
pub async fn validate_token(&self, token: &str) -> Result<KeystoneToken> { pub async fn validate_token(&self, token: &str) -> Result<KeystoneToken> {
match self.version { match self.version {
KeystoneVersion::V3 => self.validate_token_v3(token).await, KeystoneVersion::V3 => self.validate_token_v3(token).await,
@@ -239,7 +238,6 @@ impl KeystoneClient {
} }
/// Get EC2 credentials for a user /// Get EC2 credentials for a user
#[hotpath::measure]
pub async fn get_ec2_credentials(&self, user_id: &str, project_id: Option<&str>) -> Result<Vec<EC2Credential>> { pub async fn get_ec2_credentials(&self, user_id: &str, project_id: Option<&str>) -> Result<Vec<EC2Credential>> {
let admin_token = self.get_admin_token().await?; let admin_token = self.get_admin_token().await?;
-19
View File
@@ -28,7 +28,6 @@ categories = ["cryptography", "web-programming", "authentication"]
workspace = true workspace = true
[dependencies] [dependencies]
hotpath.workspace = true
# Core dependencies # Core dependencies
async-trait = { workspace = true } async-trait = { workspace = true }
tokio = { workspace = true, features = ["fs", "io-util", "macros", "rt-multi-thread", "sync", "time"] } tokio = { workspace = true, features = ["fs", "io-util", "macros", "rt-multi-thread", "sync", "time"] }
@@ -38,8 +37,6 @@ serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["raw_value"] } serde_json = { workspace = true, features = ["raw_value"] }
tracing = { workspace = true } tracing = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
# Operation metrics emitted by the retry policy engine (crate::policy).
metrics = { workspace = true }
# Cryptography # Cryptography
aes-gcm = { workspace = true, features = ["rand_core"] } aes-gcm = { workspace = true, features = ["rand_core"] }
@@ -75,8 +72,6 @@ tokio-util = { workspace = true }
[dev-dependencies] [dev-dependencies]
anyhow = { workspace = true } anyhow = { workspace = true }
# Debugging recorder for asserting emitted metrics in tests.
metrics-util = { version = "0.20", features = ["debugging"] }
insta = { workspace = true, features = ["yaml", "json"] } insta = { workspace = true, features = ["yaml", "json"] }
tempfile = { workspace = true } tempfile = { workspace = true }
temp-env = { workspace = true } temp-env = { workspace = true }
@@ -85,17 +80,3 @@ tokio = { workspace = true, features = ["net", "test-util"] }
[features] [features]
default = [] default = []
hotpath = [
"hotpath/hotpath",
"hotpath/tokio",
"hotpath/reqwest-0-13",
"rustfs-security-governance/hotpath",
"rustfs-utils/hotpath",
]
hotpath-alloc = [
"hotpath",
"hotpath/hotpath-alloc",
"rustfs-security-governance/hotpath-alloc",
"rustfs-utils/hotpath-alloc",
]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu", "rustfs-security-governance/hotpath-cpu", "rustfs-utils/hotpath-cpu"]
+33 -48
View File
@@ -27,11 +27,11 @@
//! server, so they are `#[ignore]`d in CI. Static is covered by its own //! server, so they are `#[ignore]`d in CI. Static is covered by its own
//! stateless contract below. //! stateless contract below.
use super::KmsBackend;
use super::local::LocalKmsBackend; use super::local::LocalKmsBackend;
use super::static_kms::StaticKmsBackend; use super::static_kms::StaticKmsBackend;
use super::vault::VaultKmsBackend; use super::vault::VaultKmsBackend;
use super::vault_transit::VaultTransitKmsBackend; use super::vault_transit::VaultTransitKmsBackend;
use super::{KmsBackend, KmsClient};
use crate::config::KmsConfig; use crate::config::KmsConfig;
use crate::error::{KmsError, Result}; use crate::error::{KmsError, Result};
use crate::manager::KmsManager; use crate::manager::KmsManager;
@@ -46,24 +46,6 @@ use rand::RngExt as _;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
fn expect_unsupported<T: std::fmt::Debug>(result: Result<T>) {
match result {
Err(KmsError::UnsupportedCapability { .. }) => {}
other => panic!("expected UnsupportedCapability, got {other:?}"),
}
}
/// Rotation while not Enabled: backends with rotation support must reject it
/// through the state machine; backends without it report the capability gap.
async fn expect_rotate_rejected(backend: &dyn KmsBackend, key_id: &str) {
let result = backend.rotate_key(key_id).await;
if backend.capabilities().rotate {
expect_invalid_key_state(result, "");
} else {
expect_unsupported(result);
}
}
fn expect_invalid_key_state<T: std::fmt::Debug>(result: Result<T>, expected_fragment: &str) { fn expect_invalid_key_state<T: std::fmt::Debug>(result: Result<T>, expected_fragment: &str) {
match result { match result {
Err(KmsError::InvalidOperation { message }) => assert!( Err(KmsError::InvalidOperation { message }) => assert!(
@@ -135,9 +117,11 @@ async fn assert_key_state(backend: &dyn KmsBackend, key_id: &str, expected: KeyS
assert_eq!(described.key_metadata.key_state, expected, "unexpected state for key {key_id}"); assert_eq!(described.key_metadata.key_state, expected, "unexpected state for key {key_id}");
} }
/// Drives one freshly created (Enabled) key through the full state matrix, /// Drives one freshly created (Enabled) key through the full state matrix.
/// entirely through the `KmsBackend` product surface. ///
async fn assert_state_machine_contract(backend: &dyn KmsBackend, key_id: &str) { /// `backend` is the product surface; `client` drives the lifecycle
/// transitions not yet exposed through `KmsBackend`.
async fn assert_state_machine_contract(backend: &dyn KmsBackend, client: &dyn KmsClient, key_id: &str) {
// Enabled: cryptographic use is allowed. Keep an envelope around to prove // Enabled: cryptographic use is allowed. Keep an envelope around to prove
// decryption keeps working in later states. // decryption keeps working in later states.
let data_key = backend let data_key = backend
@@ -150,13 +134,16 @@ async fn assert_state_machine_contract(backend: &dyn KmsBackend, key_id: &str) {
.expect("Enabled key must encrypt"); .expect("Enabled key must encrypt");
// Enabled -> Disabled. // Enabled -> Disabled.
backend.disable_key(key_id).await.expect("disable from Enabled must succeed"); client
.disable_key(key_id, None)
.await
.expect("disable from Enabled must succeed");
assert_key_state(backend, key_id, KeyState::Disabled).await; assert_key_state(backend, key_id, KeyState::Disabled).await;
// Disabled: new cryptographic use and rotation are rejected... // Disabled: new cryptographic use and rotation are rejected...
expect_invalid_key_state(backend.encrypt(encrypt_request(key_id)).await, "disabled"); expect_invalid_key_state(backend.encrypt(encrypt_request(key_id)).await, "disabled");
expect_invalid_key_state(backend.generate_data_key(generate_request(key_id)).await, "disabled"); expect_invalid_key_state(backend.generate_data_key(generate_request(key_id)).await, "disabled");
expect_rotate_rejected(backend, key_id).await; expect_invalid_key_state(client.rotate_key(key_id, None).await, "");
// ...but decryption of existing data keeps working (explicit AWS deviation)... // ...but decryption of existing data keeps working (explicit AWS deviation)...
let decrypted = backend let decrypted = backend
.decrypt(decrypt_request(data_key.ciphertext_blob.clone())) .decrypt(decrypt_request(data_key.ciphertext_blob.clone()))
@@ -164,14 +151,17 @@ async fn assert_state_machine_contract(backend: &dyn KmsBackend, key_id: &str) {
.expect("decrypt with a disabled key must keep working"); .expect("decrypt with a disabled key must keep working");
assert_eq!(decrypted.plaintext, data_key.plaintext_key, "decrypt must recover the original data key"); assert_eq!(decrypted.plaintext, data_key.plaintext_key, "decrypt must recover the original data key");
// ...disable stays idempotent, cancel has nothing to cancel, and enable recovers. // ...disable stays idempotent, cancel has nothing to cancel, and enable recovers.
backend.disable_key(key_id).await.expect("disable must be idempotent"); client.disable_key(key_id, None).await.expect("disable must be idempotent");
expect_invalid_key_state(backend.cancel_key_deletion(cancel_request(key_id)).await, "not pending deletion"); expect_invalid_key_state(backend.cancel_key_deletion(cancel_request(key_id)).await, "not pending deletion");
backend.enable_key(key_id).await.expect("enable from Disabled must succeed"); client
.enable_key(key_id, None)
.await
.expect("enable from Disabled must succeed");
assert_key_state(backend, key_id, KeyState::Enabled).await; assert_key_state(backend, key_id, KeyState::Enabled).await;
// Disabled keys may still be scheduled for deletion. // Disabled keys may still be scheduled for deletion.
backend client
.disable_key(key_id) .disable_key(key_id, None)
.await .await
.expect("disable before scheduling must succeed"); .expect("disable before scheduling must succeed");
backend backend
@@ -183,9 +173,10 @@ async fn assert_state_machine_contract(backend: &dyn KmsBackend, key_id: &str) {
// PendingDeletion: everything except decryption and cancellation is rejected. // PendingDeletion: everything except decryption and cancellation is rejected.
expect_invalid_key_state(backend.encrypt(encrypt_request(key_id)).await, "pending deletion"); expect_invalid_key_state(backend.encrypt(encrypt_request(key_id)).await, "pending deletion");
expect_invalid_key_state(backend.generate_data_key(generate_request(key_id)).await, "pending deletion"); expect_invalid_key_state(backend.generate_data_key(generate_request(key_id)).await, "pending deletion");
expect_invalid_key_state(backend.enable_key(key_id).await, "pending deletion"); expect_invalid_key_state(client.enable_key(key_id, None).await, "pending deletion");
expect_invalid_key_state(backend.disable_key(key_id).await, "pending deletion"); expect_invalid_key_state(client.disable_key(key_id, None).await, "pending deletion");
expect_rotate_rejected(backend, key_id).await; expect_invalid_key_state(client.rotate_key(key_id, None).await, "");
expect_invalid_key_state(client.schedule_key_deletion(key_id, 7, None).await, "pending deletion");
expect_invalid_key_state(backend.delete_key(schedule_request(key_id)).await, "pending deletion"); expect_invalid_key_state(backend.delete_key(schedule_request(key_id)).await, "pending deletion");
let decrypted = backend let decrypted = backend
.decrypt(decrypt_request(data_key.ciphertext_blob.clone())) .decrypt(decrypt_request(data_key.ciphertext_blob.clone()))
@@ -224,7 +215,7 @@ async fn local_fixture() -> (tempfile::TempDir, KmsConfig, LocalKmsBackend, Stri
#[tokio::test] #[tokio::test]
async fn local_backend_state_machine_contract() { async fn local_backend_state_machine_contract() {
let (_temp_dir, _config, backend, key_id) = local_fixture().await; let (_temp_dir, _config, backend, key_id) = local_fixture().await;
assert_state_machine_contract(&backend, &key_id).await; assert_state_machine_contract(&backend, backend.lifecycle_client(), &key_id).await;
} }
/// SSE-shaped regression: disabling a key must not break decryption of data /// SSE-shaped regression: disabling a key must not break decryption of data
@@ -265,7 +256,10 @@ async fn static_backend_stateless_contract() {
rand::rng().fill(&mut raw_key[..]); rand::rng().fill(&mut raw_key[..]);
let config = KmsConfig::static_kms(key_id.to_string(), BASE64.encode(raw_key)); let config = KmsConfig::static_kms(key_id.to_string(), BASE64.encode(raw_key));
let static_backend = StaticKmsBackend::new(config).await.expect("static backend should build"); let static_backend = StaticKmsBackend::new(config).await.expect("static backend should build");
// StaticKmsBackend implements both traits with overlapping method names,
// so pin each surface once instead of qualifying every call.
let backend: &dyn KmsBackend = &static_backend; let backend: &dyn KmsBackend = &static_backend;
let client: &dyn KmsClient = &static_backend;
let data_key = backend let data_key = backend
.generate_data_key(generate_request(key_id)) .generate_data_key(generate_request(key_id))
@@ -281,11 +275,9 @@ async fn static_backend_stateless_contract() {
expect_invalid_key_state(backend.create_key(create_request("another-key".to_string())).await, "read-only"); expect_invalid_key_state(backend.create_key(create_request("another-key".to_string())).await, "read-only");
expect_invalid_key_state(backend.delete_key(schedule_request(key_id)).await, "read-only"); expect_invalid_key_state(backend.delete_key(schedule_request(key_id)).await, "read-only");
expect_invalid_key_state(backend.cancel_key_deletion(cancel_request(key_id)).await, "read-only"); expect_invalid_key_state(backend.cancel_key_deletion(cancel_request(key_id)).await, "read-only");
// Enable/disable and rotation are capability gaps at the product expect_invalid_key_state(client.disable_key(key_id, None).await, "read-only");
// surface, not state-machine rejections. expect_invalid_key_state(client.schedule_key_deletion(key_id, 7, None).await, "read-only");
expect_unsupported(backend.enable_key(key_id).await); expect_invalid_key_state(client.rotate_key(key_id, None).await, "read-only");
expect_unsupported(backend.disable_key(key_id).await);
expect_unsupported(backend.rotate_key(key_id).await);
} }
fn vault_dev_config(constructor: fn(url::Url, String) -> KmsConfig) -> KmsConfig { fn vault_dev_config(constructor: fn(url::Url, String) -> KmsConfig) -> KmsConfig {
@@ -306,15 +298,7 @@ async fn vault_kv2_backend_state_machine_contract() {
.await .await
.expect("key should be created"); .expect("key should be created");
assert_state_machine_contract(&backend, &created.key_id).await; assert_state_machine_contract(&backend, backend.lifecycle_client(), &created.key_id).await;
// KV2 additionally supports version-retaining rotation, which must only
// work while the key is Enabled (the shared matrix covered the
// rejections).
backend
.rotate_key(&created.key_id)
.await
.expect("rotation of an Enabled KV2 key must succeed");
// Cleanup: leave the key pending deletion so repeated runs stay tidy. // Cleanup: leave the key pending deletion so repeated runs stay tidy.
let _ = backend.delete_key(schedule_request(&created.key_id)).await; let _ = backend.delete_key(schedule_request(&created.key_id)).await;
@@ -332,12 +316,13 @@ async fn vault_transit_backend_state_machine_contract() {
.await .await
.expect("key should be created"); .expect("key should be created");
assert_state_machine_contract(&backend, &created.key_id).await; assert_state_machine_contract(&backend, backend.lifecycle_client(), &created.key_id).await;
// Transit additionally supports rotation, which must only work while the // Transit additionally supports rotation, which must only work while the
// key is Enabled (the shared matrix already covered the rejections). // key is Enabled (the shared matrix already covered the rejections).
backend backend
.rotate_key(&created.key_id) .lifecycle_client()
.rotate_key(&created.key_id, None)
.await .await
.expect("rotation of an Enabled transit key must succeed"); .expect("rotation of an Enabled transit key must succeed");
+28 -35
View File
@@ -14,7 +14,9 @@
//! Local file-based KMS backend implementation //! Local file-based KMS backend implementation
use crate::backends::{BackendCapabilities, ExpiredKeyRemoval, KmsBackend, StateGatedOperation, ensure_key_status_permits}; use crate::backends::{
BackendCapabilities, BackendInfo, ExpiredKeyRemoval, KmsBackend, KmsClient, StateGatedOperation, ensure_key_status_permits,
};
use crate::config::KmsConfig; use crate::config::KmsConfig;
use crate::config::LocalConfig; use crate::config::LocalConfig;
use crate::encryption::{AesDekCrypto, DataKeyEnvelope, DekCrypto, generate_key_material}; use crate::encryption::{AesDekCrypto, DataKeyEnvelope, DekCrypto, generate_key_material};
@@ -935,12 +937,9 @@ impl LocalKmsClient {
} }
} }
impl LocalKmsClient { #[async_trait]
pub(crate) async fn generate_data_key( impl KmsClient for LocalKmsClient {
&self, async fn generate_data_key(&self, request: &GenerateKeyRequest, context: Option<&OperationContext>) -> Result<DataKeyInfo> {
request: &GenerateKeyRequest,
context: Option<&OperationContext>,
) -> Result<DataKeyInfo> {
debug!("Generating data key for master key: {}", request.master_key_id); debug!("Generating data key for master key: {}", request.master_key_id);
let key_info = self.describe_key(&request.master_key_id, context).await?; let key_info = self.describe_key(&request.master_key_id, context).await?;
@@ -981,7 +980,7 @@ impl LocalKmsClient {
Ok(data_key) Ok(data_key)
} }
pub(crate) async fn encrypt(&self, request: &EncryptRequest, context: Option<&OperationContext>) -> Result<EncryptResponse> { async fn encrypt(&self, request: &EncryptRequest, context: Option<&OperationContext>) -> Result<EncryptResponse> {
debug!("Encrypting data with key: {}", request.key_id); debug!("Encrypting data with key: {}", request.key_id);
// Verify key exists and its state allows encryption // Verify key exists and its state allows encryption
@@ -998,7 +997,7 @@ impl LocalKmsClient {
}) })
} }
pub(crate) async fn decrypt(&self, request: &DecryptRequest, _context: Option<&OperationContext>) -> Result<Vec<u8>> { async fn decrypt(&self, request: &DecryptRequest, _context: Option<&OperationContext>) -> Result<Vec<u8>> {
debug!("Decrypting data"); debug!("Decrypting data");
// Parse the data key envelope from ciphertext // Parse the data key envelope from ciphertext
@@ -1032,14 +1031,7 @@ impl LocalKmsClient {
Ok(plaintext) Ok(plaintext)
} }
/// Test-only lifecycle driver: the product path goes through [`KmsBackend`]. async fn create_key(&self, key_id: &str, algorithm: &str, context: Option<&OperationContext>) -> Result<MasterKeyInfo> {
#[cfg(test)]
pub(crate) async fn create_key(
&self,
key_id: &str,
algorithm: &str,
context: Option<&OperationContext>,
) -> Result<MasterKeyInfo> {
debug!("Creating master key: {}", key_id); debug!("Creating master key: {}", key_id);
// Check if key already exists // Check if key already exists
@@ -1068,18 +1060,14 @@ impl LocalKmsClient {
Ok(master_key) Ok(master_key)
} }
pub(crate) async fn describe_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<KeyInfo> { async fn describe_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<KeyInfo> {
debug!("Describing key: {}", key_id); debug!("Describing key: {}", key_id);
let master_key = self.load_master_key(key_id).await?; let master_key = self.load_master_key(key_id).await?;
Ok(master_key.into()) Ok(master_key.into())
} }
pub(crate) async fn list_keys( async fn list_keys(&self, request: &ListKeysRequest, _context: Option<&OperationContext>) -> Result<ListKeysResponse> {
&self,
request: &ListKeysRequest,
_context: Option<&OperationContext>,
) -> Result<ListKeysResponse> {
debug!("Listing keys"); debug!("Listing keys");
let mut keys = Vec::new(); let mut keys = Vec::new();
@@ -1123,7 +1111,7 @@ impl LocalKmsClient {
}) })
} }
pub(crate) async fn enable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> { async fn enable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
debug!("Enabling key: {}", key_id); debug!("Enabling key: {}", key_id);
let _write_guard = self.lock_key_for_write(key_id).await; let _write_guard = self.lock_key_for_write(key_id).await;
@@ -1141,7 +1129,7 @@ impl LocalKmsClient {
Ok(()) Ok(())
} }
pub(crate) async fn disable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> { async fn disable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
debug!("Disabling key: {}", key_id); debug!("Disabling key: {}", key_id);
let _write_guard = self.lock_key_for_write(key_id).await; let _write_guard = self.lock_key_for_write(key_id).await;
@@ -1158,9 +1146,7 @@ impl LocalKmsClient {
Ok(()) Ok(())
} }
/// Test-only lifecycle driver: the product path goes through [`KmsBackend`]. async fn schedule_key_deletion(
#[cfg(test)]
pub(crate) async fn schedule_key_deletion(
&self, &self,
key_id: &str, key_id: &str,
pending_window_days: u32, pending_window_days: u32,
@@ -1184,9 +1170,7 @@ impl LocalKmsClient {
Ok(()) Ok(())
} }
/// Test-only lifecycle driver: the product path goes through [`KmsBackend`]. async fn cancel_key_deletion(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
#[cfg(test)]
pub(crate) async fn cancel_key_deletion(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
debug!("Canceling deletion for key: {}", key_id); debug!("Canceling deletion for key: {}", key_id);
let _write_guard = self.lock_key_for_write(key_id).await; let _write_guard = self.lock_key_for_write(key_id).await;
@@ -1206,9 +1190,7 @@ impl LocalKmsClient {
Ok(()) Ok(())
} }
/// Test-only lifecycle driver: the product path goes through [`KmsBackend`]. async fn rotate_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<MasterKeyInfo> {
#[cfg(test)]
pub(crate) async fn rotate_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<MasterKeyInfo> {
if !fs::try_exists(self.master_key_path(key_id)?).await? { if !fs::try_exists(self.master_key_path(key_id)?).await? {
return Err(KmsError::key_not_found(key_id)); return Err(KmsError::key_not_found(key_id));
} }
@@ -1217,7 +1199,7 @@ impl LocalKmsClient {
)) ))
} }
pub(crate) async fn health_check(&self) -> Result<()> { async fn health_check(&self) -> Result<()> {
// Check if key directory is accessible // Check if key directory is accessible
if !self.config.key_dir.exists() { if !self.config.key_dir.exists() {
return Err(KmsError::backend_error("Key directory does not exist")); return Err(KmsError::backend_error("Key directory does not exist"));
@@ -1228,6 +1210,17 @@ impl LocalKmsClient {
Ok(()) Ok(())
} }
fn backend_info(&self) -> BackendInfo {
BackendInfo::new(
"local".to_string(),
env!("CARGO_PKG_VERSION").to_string(),
self.config.key_dir.to_string_lossy().to_string(),
true, // We'll assume healthy for now
)
.with_metadata("key_dir".to_string(), self.config.key_dir.to_string_lossy().to_string())
.with_metadata("encrypted_at_rest".to_string(), self.master_cipher.is_some().to_string())
}
} }
/// LocalKmsBackend wraps LocalKmsClient and implements the KmsBackend trait /// LocalKmsBackend wraps LocalKmsClient and implements the KmsBackend trait
+183
View File
@@ -19,6 +19,7 @@ use crate::types::*;
use async_trait::async_trait; use async_trait::async_trait;
use jiff::Zoned; use jiff::Zoned;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[cfg(test)] #[cfg(test)]
mod contract_tests; mod contract_tests;
@@ -98,6 +99,136 @@ pub(crate) fn ensure_key_status_permits(key_id: &str, status: &KeyStatus, operat
ensure_key_state_permits(key_id, &state, operation) ensure_key_state_permits(key_id, &state, operation)
} }
/// Abstract KMS client interface that all backends must implement
#[async_trait]
pub trait KmsClient: Send + Sync {
/// Generate a new data encryption key (DEK)
///
/// Creates a new data key using the specified master key. The returned DataKey
/// contains both the plaintext and encrypted versions of the key.
///
/// # Arguments
/// * `request` - The key generation request
/// * `context` - Optional operation context for auditing
///
/// # Returns
/// Returns a DataKey containing both plaintext and encrypted key material
async fn generate_data_key(&self, request: &GenerateKeyRequest, context: Option<&OperationContext>) -> Result<DataKeyInfo>;
/// Encrypt data directly using a master key
///
/// Encrypts the provided plaintext using the specified master key.
/// This is different from generate_data_key as it encrypts user data directly.
///
/// # Arguments
/// * `request` - The encryption request containing plaintext and key ID
/// * `context` - Optional operation context for auditing
async fn encrypt(&self, request: &EncryptRequest, context: Option<&OperationContext>) -> Result<EncryptResponse>;
/// Decrypt data using a master key
///
/// Decrypts the provided ciphertext. The KMS automatically determines
/// which key was used for encryption based on the ciphertext metadata.
///
/// # Arguments
/// * `request` - The decryption request containing ciphertext
/// * `context` - Optional operation context for auditing
async fn decrypt(&self, request: &DecryptRequest, context: Option<&OperationContext>) -> Result<Vec<u8>>;
/// Create a new master key
///
/// Creates a new master key in the KMS with the specified ID.
/// Returns an error if a key with the same ID already exists.
///
/// # Arguments
/// * `key_id` - Unique identifier for the new key
/// * `algorithm` - Key algorithm (e.g., "AES_256")
/// * `context` - Optional operation context for auditing
async fn create_key(&self, key_id: &str, algorithm: &str, context: Option<&OperationContext>) -> Result<MasterKeyInfo>;
/// Get information about a specific key
///
/// Returns metadata and information about the specified key.
///
/// # Arguments
/// * `key_id` - The key identifier
/// * `context` - Optional operation context for auditing
async fn describe_key(&self, key_id: &str, context: Option<&OperationContext>) -> Result<KeyInfo>;
/// List available keys
///
/// Returns a paginated list of keys available in the KMS.
///
/// # Arguments
/// * `request` - List request parameters (pagination, filters)
/// * `context` - Optional operation context for auditing
async fn list_keys(&self, request: &ListKeysRequest, context: Option<&OperationContext>) -> Result<ListKeysResponse>;
/// Enable a key
///
/// Enables a previously disabled key, allowing it to be used for cryptographic operations.
///
/// # Arguments
/// * `key_id` - The key identifier
/// * `context` - Optional operation context for auditing
async fn enable_key(&self, key_id: &str, context: Option<&OperationContext>) -> Result<()>;
/// Disable a key
///
/// Disables a key, preventing it from being used for new cryptographic operations.
/// Existing encrypted data can still be decrypted.
///
/// # Arguments
/// * `key_id` - The key identifier
/// * `context` - Optional operation context for auditing
async fn disable_key(&self, key_id: &str, context: Option<&OperationContext>) -> Result<()>;
/// Schedule key deletion
///
/// Schedules a key for deletion after a specified number of days.
/// This allows for a grace period to recover the key if needed.
///
/// # Arguments
/// * `key_id` - The key identifier
/// * `pending_window_days` - Number of days before actual deletion
/// * `context` - Optional operation context for auditing
async fn schedule_key_deletion(
&self,
key_id: &str,
pending_window_days: u32,
context: Option<&OperationContext>,
) -> Result<()>;
/// Cancel key deletion
///
/// Cancels a previously scheduled key deletion.
///
/// # Arguments
/// * `key_id` - The key identifier
/// * `context` - Optional operation context for auditing
async fn cancel_key_deletion(&self, key_id: &str, context: Option<&OperationContext>) -> Result<()>;
/// Rotate a key
///
/// Creates a new version of the specified key. Previous versions remain
/// available for decryption but new operations will use the new version.
///
/// # Arguments
/// * `key_id` - The key identifier
/// * `context` - Optional operation context for auditing
async fn rotate_key(&self, key_id: &str, context: Option<&OperationContext>) -> Result<MasterKeyInfo>;
/// Health check
///
/// Performs a health check on the KMS backend to ensure it's operational.
async fn health_check(&self) -> Result<()>;
/// Get backend information
///
/// Returns information about the KMS backend (type, version, etc.).
fn backend_info(&self) -> BackendInfo;
}
/// Simplified KMS backend interface for manager /// Simplified KMS backend interface for manager
#[async_trait] #[async_trait]
pub trait KmsBackend: Send + Sync { pub trait KmsBackend: Send + Sync {
@@ -196,6 +327,58 @@ pub enum ExpiredKeyRemoval {
NotExpired, NotExpired,
} }
/// Information about a KMS backend
#[derive(Debug, Clone)]
pub struct BackendInfo {
/// Backend type name (e.g., "local", "vault")
pub backend_type: String,
/// Backend version
pub version: String,
/// Backend endpoint or location
pub endpoint: String,
/// Whether the backend is currently healthy
pub healthy: bool,
/// Additional metadata about the backend
pub metadata: HashMap<String, String>,
}
impl BackendInfo {
/// Create a new backend info
///
/// # Arguments
/// * `backend_type` - The type of the backend
/// * `version` - The version of the backend
/// * `endpoint` - The endpoint or location of the backend
/// * `healthy` - Whether the backend is healthy
///
/// # Returns
/// A new BackendInfo instance
///
pub fn new(backend_type: String, version: String, endpoint: String, healthy: bool) -> Self {
Self {
backend_type,
version,
endpoint,
healthy,
metadata: HashMap::new(),
}
}
/// Add metadata to the backend info
///
/// # Arguments
/// * `key` - Metadata key
/// * `value` - Metadata value
///
/// # Returns
/// Updated BackendInfo instance
///
pub fn with_metadata(mut self, key: String, value: String) -> Self {
self.metadata.insert(key, value);
self
}
}
/// Set of operations a KMS backend supports. /// Set of operations a KMS backend supports.
/// ///
/// Reported by [`KmsBackend::capabilities`] so callers (manager, admin API) /// Reported by [`KmsBackend::capabilities`] so callers (manager, admin API)
@@ -8,7 +8,7 @@ expression: capabilities_snapshot(backend.capabilities())
"encrypt": true, "encrypt": true,
"generate_data_key": true, "generate_data_key": true,
"physical_delete": true, "physical_delete": true,
"rotate": true, "rotate": false,
"schedule_deletion": true, "schedule_deletion": true,
"versioning": true "versioning": false
} }
+142 -77
View File
@@ -21,7 +21,7 @@
//! //!
//! encrypted_data(plaintext_len+16) || nonce (12 bytes) //! encrypted_data(plaintext_len+16) || nonce (12 bytes)
use crate::backends::{BackendCapabilities, KmsBackend}; use crate::backends::{BackendCapabilities, BackendInfo, KmsBackend, KmsClient};
use crate::config::{BackendConfig, KmsConfig}; use crate::config::{BackendConfig, KmsConfig};
use crate::encryption::DataKeyEnvelope; use crate::encryption::DataKeyEnvelope;
use crate::error::{KmsError, Result}; use crate::error::{KmsError, Result};
@@ -98,10 +98,9 @@ impl StaticKmsBackend {
} }
} }
impl StaticKmsBackend { #[async_trait]
/// Generate a fresh data key and wrap it in the standard KMS envelope, impl KmsClient for StaticKmsBackend {
/// authenticated against the canonical encryption context. async fn generate_data_key(&self, request: &GenerateKeyRequest, _context: Option<&OperationContext>) -> Result<DataKeyInfo> {
pub(crate) fn generate_data_key_envelope(&self, request: &GenerateKeyRequest) -> Result<DataKeyInfo> {
if request.master_key_id != self.key_id { if request.master_key_id != self.key_id {
return Err(KmsError::key_not_found(&request.master_key_id)); return Err(KmsError::key_not_found(&request.master_key_id));
} }
@@ -152,8 +151,7 @@ impl StaticKmsBackend {
)) ))
} }
/// Encrypt caller-provided plaintext into the standard KMS envelope. async fn encrypt(&self, request: &EncryptRequest, _context: Option<&OperationContext>) -> Result<EncryptResponse> {
pub(crate) fn encrypt_to_envelope(&self, request: &EncryptRequest) -> Result<EncryptResponse> {
if request.key_id != self.key_id { if request.key_id != self.key_id {
return Err(KmsError::key_not_found(&request.key_id)); return Err(KmsError::key_not_found(&request.key_id));
} }
@@ -198,8 +196,7 @@ impl StaticKmsBackend {
}) })
} }
/// Open a KMS envelope produced by this backend. async fn decrypt(&self, request: &DecryptRequest, _context: Option<&OperationContext>) -> Result<Vec<u8>> {
pub(crate) fn decrypt_envelope(&self, request: &DecryptRequest) -> Result<Vec<u8>> {
let envelope: DataKeyEnvelope = serde_json::from_slice(&request.ciphertext) let envelope: DataKeyEnvelope = serde_json::from_slice(&request.ciphertext)
.map_err(|error| KmsError::cryptographic_error("parse", format!("Failed to parse data key envelope: {error}")))?; .map_err(|error| KmsError::cryptographic_error("parse", format!("Failed to parse data key envelope: {error}")))?;
if envelope.master_key_id != self.key_id { if envelope.master_key_id != self.key_id {
@@ -238,8 +235,14 @@ impl StaticKmsBackend {
Ok(plaintext) Ok(plaintext)
} }
/// Describe the single configured key. async fn create_key(&self, key_id: &str, _algorithm: &str, _context: Option<&OperationContext>) -> Result<MasterKeyInfo> {
pub(crate) fn configured_key_info(&self, key_id: &str) -> Result<KeyInfo> { if key_id == self.key_id {
return Err(KmsError::key_already_exists(key_id));
}
Err(KmsError::invalid_operation("Static KMS is read-only: cannot create new keys"))
}
async fn describe_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<KeyInfo> {
if key_id != self.key_id { if key_id != self.key_id {
return Err(KmsError::key_not_found(key_id)); return Err(KmsError::key_not_found(key_id));
} }
@@ -260,8 +263,7 @@ impl StaticKmsBackend {
}) })
} }
/// List the single configured key, honouring the pagination marker. async fn list_keys(&self, request: &ListKeysRequest, _context: Option<&OperationContext>) -> Result<ListKeysResponse> {
pub(crate) fn list_configured_key(&self, request: &ListKeysRequest) -> Result<ListKeysResponse> {
let key_info = KeyInfo { let key_info = KeyInfo {
key_id: self.key_id.clone(), key_id: self.key_id.clone(),
description: Some("Static single-key KMS backend".to_string()), description: Some("Static single-key KMS backend".to_string()),
@@ -293,6 +295,57 @@ impl StaticKmsBackend {
truncated: false, truncated: false,
}) })
} }
async fn enable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
if key_id != self.key_id {
return Err(KmsError::key_not_found(key_id));
}
// Static KMS key is always enabled
Ok(())
}
async fn disable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
if key_id != self.key_id {
return Err(KmsError::key_not_found(key_id));
}
Err(KmsError::invalid_operation("Static KMS is read-only: cannot disable keys"))
}
async fn schedule_key_deletion(
&self,
key_id: &str,
_pending_window_days: u32,
_context: Option<&OperationContext>,
) -> Result<()> {
if key_id != self.key_id {
return Err(KmsError::key_not_found(key_id));
}
Err(KmsError::invalid_operation("Static KMS is read-only: cannot schedule key deletion"))
}
async fn cancel_key_deletion(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
if key_id != self.key_id {
return Err(KmsError::key_not_found(key_id));
}
Err(KmsError::invalid_operation("Static KMS is read-only: cannot cancel key deletion"))
}
async fn rotate_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<MasterKeyInfo> {
if key_id != self.key_id {
return Err(KmsError::key_not_found(key_id));
}
Err(KmsError::invalid_operation("Static KMS is read-only: cannot rotate keys"))
}
async fn health_check(&self) -> Result<()> {
// Static KMS is always healthy if it was successfully initialized
Ok(())
}
fn backend_info(&self) -> BackendInfo {
BackendInfo::new("static".to_string(), env!("CARGO_PKG_VERSION").to_string(), "local".to_string(), true)
.with_metadata("key_id".to_string(), self.key_id.clone())
}
} }
#[async_trait] #[async_trait]
@@ -306,12 +359,12 @@ impl KmsBackend for StaticKmsBackend {
} }
async fn encrypt(&self, request: EncryptRequest) -> Result<EncryptResponse> { async fn encrypt(&self, request: EncryptRequest) -> Result<EncryptResponse> {
self.encrypt_to_envelope(&request) <Self as KmsClient>::encrypt(self, &request, None).await
} }
async fn decrypt(&self, request: DecryptRequest) -> Result<DecryptResponse> { async fn decrypt(&self, request: DecryptRequest) -> Result<DecryptResponse> {
let key_id = self.key_id.clone(); let key_id = self.key_id.clone();
let plaintext = self.decrypt_envelope(&request)?; let plaintext = <Self as KmsClient>::decrypt(self, &request, None).await?;
Ok(DecryptResponse { Ok(DecryptResponse {
plaintext, plaintext,
key_id, key_id,
@@ -327,7 +380,7 @@ impl KmsBackend for StaticKmsBackend {
encryption_context: request.encryption_context, encryption_context: request.encryption_context,
grant_tokens: Vec::new(), grant_tokens: Vec::new(),
}; };
let data_key = self.generate_data_key_envelope(&gen_req)?; let data_key = <Self as KmsClient>::generate_data_key(self, &gen_req, None).await?;
let plaintext_key = data_key let plaintext_key = data_key
.plaintext .plaintext
@@ -342,7 +395,7 @@ impl KmsBackend for StaticKmsBackend {
} }
async fn describe_key(&self, request: DescribeKeyRequest) -> Result<DescribeKeyResponse> { async fn describe_key(&self, request: DescribeKeyRequest) -> Result<DescribeKeyResponse> {
let key_info = self.configured_key_info(&request.key_id)?; let key_info = <Self as KmsClient>::describe_key(self, &request.key_id, None).await?;
let key_metadata = KeyMetadata { let key_metadata = KeyMetadata {
key_id: key_info.key_id.clone(), key_id: key_info.key_id.clone(),
key_state: if key_info.status == KeyStatus::Active { key_state: if key_info.status == KeyStatus::Active {
@@ -362,7 +415,7 @@ impl KmsBackend for StaticKmsBackend {
} }
async fn list_keys(&self, request: ListKeysRequest) -> Result<ListKeysResponse> { async fn list_keys(&self, request: ListKeysRequest) -> Result<ListKeysResponse> {
self.list_configured_key(&request) <Self as KmsClient>::list_keys(self, &request, None).await
} }
async fn delete_key(&self, request: DeleteKeyRequest) -> Result<DeleteKeyResponse> { async fn delete_key(&self, request: DeleteKeyRequest) -> Result<DeleteKeyResponse> {
@@ -393,7 +446,7 @@ impl KmsBackend for StaticKmsBackend {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::backends::KmsBackend as KmsBackendTrait; use crate::backends::{KmsBackend as KmsBackendTrait, KmsClient};
use crate::config::{BackendConfig, KmsBackend, StaticConfig}; use crate::config::{BackendConfig, KmsBackend, StaticConfig};
use crate::encryption::is_data_key_envelope; use crate::encryption::is_data_key_envelope;
use base64::Engine as _; use base64::Engine as _;
@@ -438,8 +491,8 @@ mod tests {
// Generate data key // Generate data key
let request = GenerateKeyRequest::new(key_id.clone(), "AES_256".to_string()) let request = GenerateKeyRequest::new(key_id.clone(), "AES_256".to_string())
.with_context("bucket".to_string(), "test-bucket".to_string()); .with_context("bucket".to_string(), "test-bucket".to_string());
let data_key = backend let data_key = KmsClient::generate_data_key(&backend, &request, None)
.generate_data_key_envelope(&request) .await
.expect("Failed to generate data key"); .expect("Failed to generate data key");
assert_eq!(data_key.key_id, key_id); assert_eq!(data_key.key_id, key_id);
@@ -456,7 +509,9 @@ mod tests {
// Decrypt the data key // Decrypt the data key
let decrypt_request = let decrypt_request =
DecryptRequest::new(data_key.ciphertext.clone()).with_context("bucket".to_string(), "test-bucket".to_string()); DecryptRequest::new(data_key.ciphertext.clone()).with_context("bucket".to_string(), "test-bucket".to_string());
let decrypted = backend.decrypt_envelope(&decrypt_request).expect("Failed to decrypt"); let decrypted = KmsClient::decrypt(&backend, &decrypt_request, None)
.await
.expect("Failed to decrypt");
assert_eq!(decrypted.as_slice(), data_key.plaintext.as_deref().expect("plaintext should exist")); assert_eq!(decrypted.as_slice(), data_key.plaintext.as_deref().expect("plaintext should exist"));
} }
@@ -468,8 +523,8 @@ mod tests {
.with_context("bucket".to_string(), "source-bucket".to_string()) .with_context("bucket".to_string(), "source-bucket".to_string())
.with_context("object".to_string(), "source-object".to_string()); .with_context("object".to_string(), "source-object".to_string());
let data_key = backend let data_key = KmsClient::generate_data_key(&backend, &request, None)
.generate_data_key_envelope(&request) .await
.expect("generate static KMS data key"); .expect("generate static KMS data key");
assert!( assert!(
@@ -513,8 +568,8 @@ mod tests {
let (backend, key_id, _key) = create_test_backend().await; let (backend, key_id, _key) = create_test_backend().await;
let request = GenerateKeyRequest::new(key_id, "AES_256".to_string()) let request = GenerateKeyRequest::new(key_id, "AES_256".to_string())
.with_context("bucket".to_string(), "source-bucket".to_string()); .with_context("bucket".to_string(), "source-bucket".to_string());
let generated = backend let generated = KmsClient::generate_data_key(&backend, &request, None)
.generate_data_key_envelope(&request) .await
.expect("generate context-bound data key"); .expect("generate context-bound data key");
let mut envelope: DataKeyEnvelope = serde_json::from_slice(&generated.ciphertext).expect("parse static KMS envelope"); let mut envelope: DataKeyEnvelope = serde_json::from_slice(&generated.ciphertext).expect("parse static KMS envelope");
envelope envelope
@@ -523,8 +578,8 @@ mod tests {
let decrypt_request = DecryptRequest::new(serde_json::to_vec(&envelope).expect("serialize tampered envelope")) let decrypt_request = DecryptRequest::new(serde_json::to_vec(&envelope).expect("serialize tampered envelope"))
.with_context("bucket".to_string(), "different-bucket".to_string()); .with_context("bucket".to_string(), "different-bucket".to_string());
let error = backend let error = KmsClient::decrypt(&backend, &decrypt_request, None)
.decrypt_envelope(&decrypt_request) .await
.expect_err("tampering with authenticated envelope context must fail"); .expect_err("tampering with authenticated envelope context must fail");
assert!(matches!(error, KmsError::CryptographicError { .. })); assert!(matches!(error, KmsError::CryptographicError { .. }));
@@ -535,7 +590,7 @@ mod tests {
let (backend, _key_id, _key) = create_test_backend().await; let (backend, _key_id, _key) = create_test_backend().await;
let request = GenerateKeyRequest::new("wrong-key-id".to_string(), "AES_256".to_string()); let request = GenerateKeyRequest::new("wrong-key-id".to_string(), "AES_256".to_string());
let result = backend.generate_data_key_envelope(&request); let result = KmsClient::generate_data_key(&backend, &request, None).await;
assert!(result.is_err()); assert!(result.is_err());
assert!(result.expect_err("should be Err").to_string().contains("wrong-key-id")); assert!(result.expect_err("should be Err").to_string().contains("wrong-key-id"));
} }
@@ -547,7 +602,7 @@ mod tests {
// Ciphertext too short // Ciphertext too short
let short = vec![0u8; 10]; let short = vec![0u8; 10];
let request = DecryptRequest::new(short); let request = DecryptRequest::new(short);
let result = backend.decrypt_envelope(&request); let result = KmsClient::decrypt(&backend, &request, None).await;
assert!(result.is_err()); assert!(result.is_err());
} }
@@ -557,7 +612,9 @@ mod tests {
// Generate a valid ciphertext first // Generate a valid ciphertext first
let gen_request = GenerateKeyRequest::new(key_id, "AES_256".to_string()); let gen_request = GenerateKeyRequest::new(key_id, "AES_256".to_string());
let data_key = backend.generate_data_key_envelope(&gen_request).expect("generate"); let data_key = KmsClient::generate_data_key(&backend, &gen_request, None)
.await
.expect("generate");
// Tamper with the ciphertext (flip a bit in the encrypted portion) // Tamper with the ciphertext (flip a bit in the encrypted portion)
let mut tampered = data_key.ciphertext.clone(); let mut tampered = data_key.ciphertext.clone();
@@ -566,7 +623,7 @@ mod tests {
} }
let request = DecryptRequest::new(tampered); let request = DecryptRequest::new(tampered);
let result = backend.decrypt_envelope(&request); let result = KmsClient::decrypt(&backend, &request, None).await;
assert!(result.is_err()); assert!(result.is_err());
} }
@@ -575,14 +632,7 @@ mod tests {
let (backend, key_id, _key) = create_test_backend().await; let (backend, key_id, _key) = create_test_backend().await;
// Creating the pre-configured key should return KeyAlreadyExists // Creating the pre-configured key should return KeyAlreadyExists
let result = KmsBackendTrait::create_key( let result = KmsClient::create_key(&backend, &key_id, "AES_256", None).await;
&backend,
CreateKeyRequest {
key_name: Some(key_id.clone()),
..Default::default()
},
)
.await;
assert!(result.is_err()); assert!(result.is_err());
assert!(result.expect_err("should be Err").to_string().contains("already exists")); assert!(result.expect_err("should be Err").to_string().contains("already exists"));
} }
@@ -592,14 +642,7 @@ mod tests {
let (backend, _key_id, _key) = create_test_backend().await; let (backend, _key_id, _key) = create_test_backend().await;
// Creating any other key should return invalid operation (read-only) // Creating any other key should return invalid operation (read-only)
let result = KmsBackendTrait::create_key( let result = KmsClient::create_key(&backend, "other-key", "AES_256", None).await;
&backend,
CreateKeyRequest {
key_name: Some("other-key".to_string()),
..Default::default()
},
)
.await;
assert!(result.is_err()); assert!(result.is_err());
let err_msg = result.expect_err("should be Err").to_string(); let err_msg = result.expect_err("should be Err").to_string();
assert!(err_msg.contains("read-only") || err_msg.contains("cannot create")); assert!(err_msg.contains("read-only") || err_msg.contains("cannot create"));
@@ -609,13 +652,15 @@ mod tests {
async fn test_describe_key() { async fn test_describe_key() {
let (backend, key_id, _key) = create_test_backend().await; let (backend, key_id, _key) = create_test_backend().await;
let key_info = backend.configured_key_info(&key_id).expect("describe_key should succeed"); let key_info = KmsClient::describe_key(&backend, &key_id, None)
.await
.expect("describe_key should succeed");
assert_eq!(key_info.key_id, key_id); assert_eq!(key_info.key_id, key_id);
assert_eq!(key_info.status, KeyStatus::Active); assert_eq!(key_info.status, KeyStatus::Active);
assert_eq!(key_info.algorithm, "AES_256"); assert_eq!(key_info.algorithm, "AES_256");
// Wrong key ID // Wrong key ID
let result = backend.configured_key_info("nonexistent"); let result = KmsClient::describe_key(&backend, "nonexistent", None).await;
assert!(result.is_err()); assert!(result.is_err());
} }
@@ -623,8 +668,8 @@ mod tests {
async fn test_list_keys() { async fn test_list_keys() {
let (backend, key_id, _key) = create_test_backend().await; let (backend, key_id, _key) = create_test_backend().await;
let response = backend let response = KmsClient::list_keys(&backend, &ListKeysRequest::default(), None)
.list_configured_key(&ListKeysRequest::default()) .await
.expect("list_keys should succeed"); .expect("list_keys should succeed");
assert_eq!(response.keys.len(), 1); assert_eq!(response.keys.len(), 1);
assert_eq!(response.keys[0].key_id, key_id); assert_eq!(response.keys[0].key_id, key_id);
@@ -632,45 +677,61 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
async fn lifecycle_mutations_are_unsupported_at_the_product_surface() { async fn test_disable_key_returns_error() {
let (backend, key_id, _key) = create_test_backend().await; let (backend, key_id, _key) = create_test_backend().await;
// The static backend advertises no enable/disable or rotation let result = KmsClient::disable_key(&backend, &key_id, None).await;
// capability, so the shared KmsBackend defaults reject all three. assert!(result.is_err());
for result in [ assert!(result.expect_err("should be Err").to_string().contains("read-only"));
KmsBackendTrait::enable_key(&backend, &key_id).await, }
KmsBackendTrait::disable_key(&backend, &key_id).await,
KmsBackendTrait::rotate_key(&backend, &key_id).await, #[tokio::test]
] { async fn test_enable_key_is_noop() {
let error = result.expect_err("static lifecycle mutations must be rejected"); let (backend, key_id, _key) = create_test_backend().await;
assert!(matches!(error, KmsError::UnsupportedCapability { .. }), "got {error:?}");
} // Enable should succeed (no-op for static KMS)
KmsClient::enable_key(&backend, &key_id, None)
.await
.expect("enable_key should be no-op");
// Wrong key should still fail
let result = KmsClient::enable_key(&backend, "wrong", None).await;
assert!(result.is_err());
} }
#[tokio::test] #[tokio::test]
async fn test_delete_key_returns_error() { async fn test_delete_key_returns_error() {
let (backend, key_id, _key) = create_test_backend().await; let (backend, key_id, _key) = create_test_backend().await;
let result = KmsBackendTrait::delete_key( let result = KmsClient::schedule_key_deletion(&backend, &key_id, 7, None).await;
&backend,
DeleteKeyRequest {
key_id: key_id.clone(),
pending_window_in_days: Some(7),
force_immediate: None,
},
)
.await;
assert!(result.is_err()); assert!(result.is_err());
assert!(result.expect_err("should be Err").to_string().contains("read-only")); assert!(result.expect_err("should be Err").to_string().contains("read-only"));
} }
#[tokio::test]
async fn test_rotate_key_returns_error() {
let (backend, key_id, _key) = create_test_backend().await;
let result = KmsClient::rotate_key(&backend, &key_id, None).await;
assert!(result.is_err());
}
#[tokio::test] #[tokio::test]
async fn test_health_check() { async fn test_health_check() {
let (backend, _key_id, _key) = create_test_backend().await; let (backend, _key_id, _key) = create_test_backend().await;
KmsBackendTrait::health_check(&backend) KmsClient::health_check(&backend).await.expect("health_check should succeed");
.await }
.expect("health_check should succeed");
#[tokio::test]
async fn test_backend_info() {
let (backend, key_id, _key) = create_test_backend().await;
let info = KmsClient::backend_info(&backend);
assert_eq!(info.backend_type, "static");
assert_eq!(info.endpoint, "local");
assert!(info.healthy);
assert_eq!(info.metadata.get("key_id"), Some(&key_id));
} }
#[tokio::test] #[tokio::test]
@@ -679,13 +740,17 @@ mod tests {
let plaintext = b"Hello, static KMS world!"; let plaintext = b"Hello, static KMS world!";
let enc_request = EncryptRequest::new(key_id.clone(), plaintext.to_vec()); let enc_request = EncryptRequest::new(key_id.clone(), plaintext.to_vec());
let enc_response = backend.encrypt_to_envelope(&enc_request).expect("encrypt should succeed"); let enc_response = KmsClient::encrypt(&backend, &enc_request, None)
.await
.expect("encrypt should succeed");
assert_eq!(enc_response.key_id, key_id); assert_eq!(enc_response.key_id, key_id);
assert!(!enc_response.ciphertext.is_empty()); assert!(!enc_response.ciphertext.is_empty());
let dec_request = DecryptRequest::new(enc_response.ciphertext); let dec_request = DecryptRequest::new(enc_response.ciphertext);
let decrypted = backend.decrypt_envelope(&dec_request).expect("decrypt should succeed"); let decrypted = KmsClient::decrypt(&backend, &dec_request, None)
.await
.expect("decrypt should succeed");
assert_eq!(decrypted, plaintext); assert_eq!(decrypted, plaintext);
} }
+95 -210
View File
@@ -19,7 +19,8 @@ use crate::backends::vault_credentials::{
token_source_for, token_source_for,
}; };
use crate::backends::{ use crate::backends::{
BackendCapabilities, ExpiredKeyRemoval, KmsBackend, StateGatedOperation, ensure_key_state_permits, ensure_key_status_permits, BackendCapabilities, BackendInfo, ExpiredKeyRemoval, KmsBackend, KmsClient, StateGatedOperation, ensure_key_state_permits,
ensure_key_status_permits,
}; };
use crate::config::{KmsConfig, VaultConfig}; use crate::config::{KmsConfig, VaultConfig};
use crate::encryption::{AesDekCrypto, DataKeyEnvelope, DekCrypto, generate_key_material}; use crate::encryption::{AesDekCrypto, DataKeyEnvelope, DekCrypto, generate_key_material};
@@ -41,6 +42,7 @@ use vaultrs::{api::kv2::requests::SetSecretRequestOptions, error::ClientError, k
/// Vault KMS client implementation /// Vault KMS client implementation
pub struct VaultKmsClient { pub struct VaultKmsClient {
credentials: Arc<VaultCredentialProvider>, credentials: Arc<VaultCredentialProvider>,
config: VaultConfig,
/// Mount path for the KV engine (typically "kv" or "secret") /// Mount path for the KV engine (typically "kv" or "secret")
kv_mount: String, kv_mount: String,
/// Path prefix for storing keys /// Path prefix for storing keys
@@ -198,6 +200,7 @@ impl VaultKmsClient {
credentials, credentials,
kv_mount: config.kv_mount.clone(), kv_mount: config.kv_mount.clone(),
key_path_prefix: config.key_path_prefix.clone(), key_path_prefix: config.key_path_prefix.clone(),
config,
dek_crypto: AesDekCrypto::new(), dek_crypto: AesDekCrypto::new(),
retry: RetryPolicy::from_config(kms_config), retry: RetryPolicy::from_config(kms_config),
cancel: CancellationToken::new(), cancel: CancellationToken::new(),
@@ -251,6 +254,13 @@ impl VaultKmsClient {
Ok(general_purpose::STANDARD.encode(key_material)) Ok(general_purpose::STANDARD.encode(key_material))
} }
/// Decode key material from KV2 storage (plain Base64, see `encrypt_key_material`).
async fn decrypt_key_material(&self, encrypted_material: &str) -> Result<Vec<u8>> {
general_purpose::STANDARD
.decode(encrypted_material)
.map_err(|e| KmsError::cryptographic_error("decrypt", e.to_string()))
}
/// Read the immutable material record of one key version. /// Read the immutable material record of one key version.
/// ///
/// A missing record fails closed with [`KmsError::KeyVersionNotFound`]; falling /// A missing record fails closed with [`KmsError::KeyVersionNotFound`]; falling
@@ -579,12 +589,9 @@ impl VaultKmsClient {
} }
} }
impl VaultKmsClient { #[async_trait]
pub(crate) async fn generate_data_key( impl KmsClient for VaultKmsClient {
&self, async fn generate_data_key(&self, request: &GenerateKeyRequest, _context: Option<&OperationContext>) -> Result<DataKeyInfo> {
request: &GenerateKeyRequest,
_context: Option<&OperationContext>,
) -> Result<DataKeyInfo> {
debug!("Generating data key for master key: {}", request.master_key_id); debug!("Generating data key for master key: {}", request.master_key_id);
let key_data = self.get_key_data(&request.master_key_id).await?; let key_data = self.get_key_data(&request.master_key_id).await?;
@@ -625,32 +632,20 @@ impl VaultKmsClient {
Ok(data_key) Ok(data_key)
} }
pub(crate) async fn encrypt(&self, request: &EncryptRequest, _context: Option<&OperationContext>) -> Result<EncryptResponse> { async fn encrypt(&self, request: &EncryptRequest, _context: Option<&OperationContext>) -> Result<EncryptResponse> {
debug!("Encrypting data with key: {}", request.key_id); debug!("Encrypting data with key: {}", request.key_id);
// Single read of the key record: the material we wrap with and the // Get the master key and verify its state allows encryption
// version stamped into the envelope must come from the same snapshot
// (see generate_data_key).
let key_data = self.get_key_data(&request.key_id).await?; let key_data = self.get_key_data(&request.key_id).await?;
ensure_key_status_permits(&request.key_id, &key_data.status, StateGatedOperation::Encrypt)?; ensure_key_status_permits(&request.key_id, &key_data.status, StateGatedOperation::Encrypt)?;
let key_material = decode_stored_key_material(&request.key_id, &key_data.encrypted_key_material) let key_material = self.decrypt_key_material(&key_data.encrypted_key_material).await?;
.inspect_err(|error| warn!(key_id = %request.key_id, %error, "Vault KMS key material failed validation"))?;
let (encrypted_key, nonce) = self.dek_crypto.encrypt(&key_material, &request.plaintext).await?;
// Wrap the ciphertext in the same authenticated envelope that // For simplicity, we'll use a basic encryption approach
// generate_data_key emits, so decrypt() round-trips it and resolves // In practice, you'd use proper AEAD encryption
// the wrapping master key version after rotations. let mut ciphertext = request.plaintext.clone();
let envelope = DataKeyEnvelope { for (i, byte) in ciphertext.iter_mut().enumerate() {
key_id: uuid::Uuid::new_v4().to_string(), *byte ^= key_material[i % key_material.len()];
master_key_id: request.key_id.clone(), }
key_spec: "AES_256".to_string(),
encrypted_key,
nonce,
encryption_context: request.encryption_context.clone(),
created_at: Zoned::now(),
master_key_version: Some(key_data.version),
};
let ciphertext = serde_json::to_vec(&envelope)?;
Ok(EncryptResponse { Ok(EncryptResponse {
ciphertext, ciphertext,
@@ -660,7 +655,7 @@ impl VaultKmsClient {
}) })
} }
pub(crate) async fn decrypt(&self, request: &DecryptRequest, _context: Option<&OperationContext>) -> Result<Vec<u8>> { async fn decrypt(&self, request: &DecryptRequest, _context: Option<&OperationContext>) -> Result<Vec<u8>> {
debug!("Decrypting data"); debug!("Decrypting data");
// Parse the data key envelope from ciphertext // Parse the data key envelope from ciphertext
@@ -702,12 +697,7 @@ impl VaultKmsClient {
Ok(plaintext) Ok(plaintext)
} }
pub(crate) async fn create_key( async fn create_key(&self, key_id: &str, algorithm: &str, _context: Option<&OperationContext>) -> Result<MasterKeyInfo> {
&self,
key_id: &str,
algorithm: &str,
_context: Option<&OperationContext>,
) -> Result<MasterKeyInfo> {
debug!("Creating master key: {} with algorithm: {}", key_id, algorithm); debug!("Creating master key: {} with algorithm: {}", key_id, algorithm);
// Existence pre-check with read-confirm recovery: a create whose // Existence pre-check with read-confirm recovery: a create whose
@@ -789,7 +779,7 @@ impl VaultKmsClient {
Ok(master_key) Ok(master_key)
} }
pub(crate) async fn describe_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<KeyInfo> { async fn describe_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<KeyInfo> {
debug!("Describing key: {}", key_id); debug!("Describing key: {}", key_id);
let key_data = self.get_key_data(key_id).await?; let key_data = self.get_key_data(key_id).await?;
@@ -809,11 +799,7 @@ impl VaultKmsClient {
}) })
} }
pub(crate) async fn list_keys( async fn list_keys(&self, request: &ListKeysRequest, _context: Option<&OperationContext>) -> Result<ListKeysResponse> {
&self,
request: &ListKeysRequest,
_context: Option<&OperationContext>,
) -> Result<ListKeysResponse> {
debug!("Listing keys with limit: {:?}", request.limit); debug!("Listing keys with limit: {:?}", request.limit);
let all_keys = self.list_vault_keys().await?; let all_keys = self.list_vault_keys().await?;
@@ -850,7 +836,7 @@ impl VaultKmsClient {
}) })
} }
pub(crate) async fn enable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> { async fn enable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
debug!("Enabling key: {}", key_id); debug!("Enabling key: {}", key_id);
let mut key_data = self.get_key_data(key_id).await?; let mut key_data = self.get_key_data(key_id).await?;
@@ -862,7 +848,7 @@ impl VaultKmsClient {
Ok(()) Ok(())
} }
pub(crate) async fn disable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> { async fn disable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
debug!("Disabling key: {}", key_id); debug!("Disabling key: {}", key_id);
let mut key_data = self.get_key_data(key_id).await?; let mut key_data = self.get_key_data(key_id).await?;
@@ -874,6 +860,39 @@ impl VaultKmsClient {
Ok(()) Ok(())
} }
async fn schedule_key_deletion(
&self,
key_id: &str,
pending_window_days: u32,
_context: Option<&OperationContext>,
) -> Result<()> {
debug!("Scheduling key deletion: {}", key_id);
let mut key_data = self.get_key_data(key_id).await?;
ensure_key_status_permits(key_id, &key_data.status, StateGatedOperation::ScheduleDeletion)?;
key_data.status = KeyStatus::PendingDeletion;
key_data.deletion_date = Some(Zoned::now() + Duration::from_secs(pending_window_days as u64 * 86400));
self.store_key_data(key_id, &key_data).await?;
debug!(key_id, "Vault KMS key deletion scheduled");
Ok(())
}
async fn cancel_key_deletion(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
debug!("Canceling key deletion: {}", key_id);
let mut key_data = self.get_key_data(key_id).await?;
if key_data.status != KeyStatus::PendingDeletion {
return Err(KmsError::invalid_key_state(format!("Key {key_id} is not pending deletion")));
}
key_data.status = KeyStatus::Active;
key_data.deletion_date = None;
self.store_key_data(key_id, &key_data).await?;
debug!(key_id, "Vault KMS key deletion canceled");
Ok(())
}
/// Rotate the master key while keeping every historical version decryptable. /// Rotate the master key while keeping every historical version decryptable.
/// ///
/// Commit protocol (all writes check-and-set, in this order): /// Commit protocol (all writes check-and-set, in this order):
@@ -889,11 +908,10 @@ impl VaultKmsClient {
/// or interrupted rotation never exposes half-committed material. Concurrent /// or interrupted rotation never exposes half-committed material. Concurrent
/// rotations are serialized by the check-and-set writes: at most one caller /// rotations are serialized by the check-and-set writes: at most one caller
/// commits each version and the losers fail without side effects on current. /// commits each version and the losers fail without side effects on current.
pub(crate) async fn rotate_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<MasterKeyInfo> { async fn rotate_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<MasterKeyInfo> {
debug!("Rotating master key: {}", key_id); debug!("Rotating master key: {}", key_id);
let (mut cas, mut key_data) = self.get_key_data_versioned(key_id).await?; let (mut cas, mut key_data) = self.get_key_data_versioned(key_id).await?;
ensure_key_status_permits(key_id, &key_data.status, StateGatedOperation::Rotate)?;
// The material about to be frozen must be decodable: freezing poisoned // The material about to be frozen must be decodable: freezing poisoned
// material would give legacy envelopes a permanently broken baseline. This // material would give legacy envelopes a permanently broken baseline. This
@@ -974,7 +992,7 @@ impl VaultKmsClient {
}) })
} }
pub(crate) async fn health_check(&self) -> Result<()> { async fn health_check(&self) -> Result<()> {
debug!("Performing Vault health check"); debug!("Performing Vault health check");
// Use list_vault_keys but handle the case where no keys exist (which is normal) // Use list_vault_keys but handle the case where no keys exist (which is normal)
@@ -996,6 +1014,15 @@ impl VaultKmsClient {
} }
} }
} }
fn backend_info(&self) -> BackendInfo {
BackendInfo::new("vault-kv2".to_string(), "0.1.0".to_string(), self.config.address.clone(), true)
.with_metadata("kv_mount".to_string(), self.kv_mount.clone())
.with_metadata("key_prefix".to_string(), self.key_path_prefix.clone())
// Master key material is protected only by Vault ACLs and KV2 at-rest
// encryption; there is no additional cryptographic wrapping.
.with_metadata("at_rest_protection".to_string(), "vault-kv2-acl".to_string())
}
} }
/// VaultKmsBackend wraps VaultKmsClient and implements the KmsBackend trait /// VaultKmsBackend wraps VaultKmsClient and implements the KmsBackend trait
@@ -1004,6 +1031,12 @@ pub struct VaultKmsBackend {
} }
impl VaultKmsBackend { impl VaultKmsBackend {
/// Lifecycle driver for the shared state-machine contract tests.
#[cfg(test)]
pub(crate) fn lifecycle_client(&self) -> &VaultKmsClient {
&self.client
}
/// Create a new VaultKmsBackend /// Create a new VaultKmsBackend
pub async fn new(config: KmsConfig) -> Result<Self> { pub async fn new(config: KmsConfig) -> Result<Self> {
config.validate()?; config.validate()?;
@@ -1263,32 +1296,17 @@ impl KmsBackend for VaultKmsBackend {
}) })
} }
async fn enable_key(&self, key_id: &str) -> Result<()> {
self.client.enable_key(key_id, None).await
}
async fn disable_key(&self, key_id: &str) -> Result<()> {
self.client.disable_key(key_id, None).await
}
async fn rotate_key(&self, key_id: &str) -> Result<()> {
self.client.rotate_key(key_id, None).await.map(|_| ())
}
async fn health_check(&self) -> Result<bool> { async fn health_check(&self) -> Result<bool> {
self.client.health_check().await.map(|_| true) self.client.health_check().await.map(|_| true)
} }
fn capabilities(&self) -> BackendCapabilities { fn capabilities(&self) -> BackendCapabilities {
// Rotation freezes the outgoing material as an immutable version // Rotation is unadvertised: the KV2 backend cannot rotate without
// record before switching the current pointer, and envelopes resolve // replacing key material in place, and no historical versions are
// their wrapping version on decrypt, so every historical version // retained, so versioning is unsupported as well.
// stays decryptable after a rotation.
BackendCapabilities::minimal() BackendCapabilities::minimal()
.with_rotate(true)
.with_enable_disable(true) .with_enable_disable(true)
.with_schedule_deletion(true) .with_schedule_deletion(true)
.with_versioning(true)
.with_physical_delete(true) .with_physical_delete(true)
} }
@@ -1702,6 +1720,19 @@ mod tests {
assert!(!is_cas_conflict(&not_found)); assert!(!is_cas_conflict(&not_found));
} }
#[tokio::test]
async fn test_vault_kv2_backend_info_reports_at_rest_protection() {
let client = VaultKmsClient::new(integration_vault_config(), &KmsConfig::default())
.await
.expect("client");
let info = client.backend_info();
assert_eq!(info.backend_type, "vault-kv2");
assert_eq!(info.metadata.get("at_rest_protection").map(String::as_str), Some("vault-kv2-acl"));
// The KV2 backend must not present itself as Transit-backed.
assert!(!format!("{info:?}").contains("Transit"));
}
fn integration_generate_request(key_id: &str) -> GenerateKeyRequest { fn integration_generate_request(key_id: &str) -> GenerateKeyRequest {
GenerateKeyRequest { GenerateKeyRequest {
master_key_id: key_id.to_string(), master_key_id: key_id.to_string(),
@@ -2050,150 +2081,4 @@ mod tests {
let legacy: VaultKeyData = serde_json::from_value(value).expect("legacy record must deserialize"); let legacy: VaultKeyData = serde_json::from_value(value).expect("legacy record must deserialize");
assert!(legacy.deletion_date.is_none()); assert!(legacy.deletion_date.is_none());
} }
/// KV2 write acknowledgement (`SecretVersionMetadata`) for `kv2::set`.
fn kv2_write_ack() -> serde_json::Value {
serde_json::json!({
"created_time": "2026-01-01T00:00:00Z",
"custom_metadata": null,
"deletion_time": "",
"destroyed": false,
"version": 2,
})
}
#[tokio::test]
async fn wired_kv2_encrypt_round_trips_through_decrypt() {
// One key-record read for the encrypt, one for the decrypt.
let (_vault, client) = scripted_client(vec![
ScriptedResponse::ok(kv2_read_data(&healthy_key_data())),
ScriptedResponse::ok(kv2_read_data(&healthy_key_data())),
])
.await;
let context = HashMap::from([("bucket".to_string(), "kv2".to_string())]);
let encrypted = client
.encrypt(
&EncryptRequest {
key_id: "wired-key".to_string(),
plaintext: b"kv2-direct-encrypt".to_vec(),
encryption_context: context.clone(),
grant_tokens: Vec::new(),
},
None,
)
.await
.expect("encrypt must produce an envelope");
// The ciphertext is a real KMS envelope wrapping AEAD output that
// decrypt() can open, not an XOR of the plaintext with the master key
// material.
let envelope: DataKeyEnvelope = serde_json::from_slice(&encrypted.ciphertext).expect("envelope must parse");
assert_eq!(envelope.master_key_id, "wired-key");
assert_eq!(envelope.master_key_version, Some(1));
let decrypted = client
.decrypt(
&DecryptRequest {
ciphertext: encrypted.ciphertext.clone(),
encryption_context: context,
grant_tokens: Vec::new(),
},
None,
)
.await
.expect("decrypt must round-trip the envelope");
assert_eq!(decrypted, b"kv2-direct-encrypt".to_vec());
// A different object context must not decrypt (checked before any
// Vault read, so no scripted response is consumed).
let error = client
.decrypt(
&DecryptRequest {
ciphertext: encrypted.ciphertext,
encryption_context: HashMap::from([("bucket".to_string(), "other".to_string())]),
grant_tokens: Vec::new(),
},
None,
)
.await
.expect_err("a different context must not decrypt");
assert!(matches!(error, KmsError::ContextMismatch { .. }), "got {error:?}");
}
/// KV2 secret-metadata read payload (`kv2::read_metadata`) pinning the
/// current secret version used as the rotation check-and-set base.
fn kv2_metadata_read_data(current_version: u64) -> serde_json::Value {
serde_json::json!({
"cas_required": false,
"created_time": "2026-01-01T00:00:00Z",
"current_version": current_version,
"delete_version_after": "0s",
"max_versions": 0,
"oldest_version": 0,
"updated_time": "2026-01-01T00:00:00Z",
"custom_metadata": null,
"versions": {},
})
}
#[tokio::test]
async fn wired_kv2_rotate_rejected_while_disabled() {
let mut key_data = healthy_key_data();
key_data.status = KeyStatus::Disabled;
let (vault, client) = scripted_client(vec![
ScriptedResponse::ok(kv2_metadata_read_data(1)),
ScriptedResponse::ok(kv2_read_data(&key_data)),
])
.await;
let error = client
.rotate_key("wired-key", None)
.await
.expect_err("rotation of a disabled key must be rejected");
assert!(matches!(error, KmsError::InvalidOperation { .. }), "got {error:?}");
let requests = vault.requests();
assert_eq!(
requests.len(),
2,
"the state gate must reject after the versioned read, before any write: {requests:?}"
);
assert!(requests.iter().all(|line| line.starts_with("GET ")), "{requests:?}");
}
#[tokio::test]
async fn wired_backend_lifecycle_overrides_reach_the_client() {
let mut disabled = healthy_key_data();
disabled.status = KeyStatus::Disabled;
let vault = ScriptedVault::serve(vec![
// disable: read the Active record, persist it Disabled.
ScriptedResponse::ok(kv2_read_data(&healthy_key_data())),
ScriptedResponse::ok(kv2_write_ack()),
// enable: read the Disabled record, persist it Active.
ScriptedResponse::ok(kv2_read_data(&disabled)),
ScriptedResponse::ok(kv2_write_ack()),
])
.await;
let config = KmsConfig::vault(
url::Url::parse(&vault.address).expect("scripted vault address should parse"),
"scripted-token".to_string(),
)
.with_insecure_development_defaults();
let backend = VaultKmsBackend::new(config).await.expect("vault kv2 backend should build");
backend
.disable_key("wired-key")
.await
.expect("KmsBackend::disable_key must persist through the client");
backend
.enable_key("wired-key")
.await
.expect("KmsBackend::enable_key must persist through the client");
let requests = vault.requests();
assert_eq!(requests.len(), 4, "each transition is one read plus one write: {requests:?}");
assert!(requests[0].starts_with("GET ") && requests[2].starts_with("GET "), "{requests:?}");
assert!(requests[1].starts_with("POST ") && requests[3].starts_with("POST "), "{requests:?}");
}
} }
+39 -98
View File
@@ -18,7 +18,9 @@ use crate::backends::vault_credentials::{
CredentialTaskHandle, VaultClientHandle, VaultConnectionSettings, VaultCredentialPolicy, VaultCredentialProvider, CredentialTaskHandle, VaultClientHandle, VaultConnectionSettings, VaultCredentialPolicy, VaultCredentialProvider,
token_source_for, token_source_for,
}; };
use crate::backends::{BackendCapabilities, ExpiredKeyRemoval, KmsBackend, StateGatedOperation, ensure_key_state_permits}; use crate::backends::{
BackendCapabilities, BackendInfo, ExpiredKeyRemoval, KmsBackend, KmsClient, StateGatedOperation, ensure_key_state_permits,
};
use crate::config::{KmsConfig, VaultTransitConfig}; use crate::config::{KmsConfig, VaultTransitConfig};
use crate::encryption::{DataKeyEnvelope, generate_key_material}; use crate::encryption::{DataKeyEnvelope, generate_key_material};
use crate::error::{KmsError, Result}; use crate::error::{KmsError, Result};
@@ -504,12 +506,9 @@ impl VaultTransitKmsClient {
} }
} }
impl VaultTransitKmsClient { #[async_trait]
pub(crate) async fn generate_data_key( impl KmsClient for VaultTransitKmsClient {
&self, async fn generate_data_key(&self, request: &GenerateKeyRequest, _context: Option<&OperationContext>) -> Result<DataKeyInfo> {
request: &GenerateKeyRequest,
_context: Option<&OperationContext>,
) -> Result<DataKeyInfo> {
self.ensure_key_state_allows(&request.master_key_id, StateGatedOperation::GenerateDataKey) self.ensure_key_state_allows(&request.master_key_id, StateGatedOperation::GenerateDataKey)
.await?; .await?;
@@ -541,7 +540,7 @@ impl VaultTransitKmsClient {
)) ))
} }
pub(crate) async fn encrypt(&self, request: &EncryptRequest, _context: Option<&OperationContext>) -> Result<EncryptResponse> { async fn encrypt(&self, request: &EncryptRequest, _context: Option<&OperationContext>) -> Result<EncryptResponse> {
let metadata = self let metadata = self
.ensure_key_state_allows(&request.key_id, StateGatedOperation::Encrypt) .ensure_key_state_allows(&request.key_id, StateGatedOperation::Encrypt)
.await?; .await?;
@@ -557,7 +556,7 @@ impl VaultTransitKmsClient {
}) })
} }
pub(crate) async fn decrypt(&self, request: &DecryptRequest, _context: Option<&OperationContext>) -> Result<Vec<u8>> { async fn decrypt(&self, request: &DecryptRequest, _context: Option<&OperationContext>) -> Result<Vec<u8>> {
let envelope: DataKeyEnvelope = serde_json::from_slice(&request.ciphertext) let envelope: DataKeyEnvelope = serde_json::from_slice(&request.ciphertext)
.map_err(|e| KmsError::cryptographic_error("parse", format!("Failed to parse data key envelope: {e}")))?; .map_err(|e| KmsError::cryptographic_error("parse", format!("Failed to parse data key envelope: {e}")))?;
@@ -579,14 +578,7 @@ impl VaultTransitKmsClient {
.await .await
} }
/// Test-only lifecycle driver: the product path goes through [`KmsBackend`]. async fn create_key(&self, key_id: &str, algorithm: &str, _context: Option<&OperationContext>) -> Result<MasterKeyInfo> {
#[cfg(test)]
pub(crate) async fn create_key(
&self,
key_id: &str,
algorithm: &str,
_context: Option<&OperationContext>,
) -> Result<MasterKeyInfo> {
if algorithm != "AES_256" { if algorithm != "AES_256" {
return Err(KmsError::unsupported_algorithm(algorithm)); return Err(KmsError::unsupported_algorithm(algorithm));
} }
@@ -653,17 +645,11 @@ impl VaultTransitKmsClient {
}) })
} }
/// Test-only lifecycle driver: the product path goes through [`KmsBackend`]. async fn describe_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<KeyInfo> {
#[cfg(test)]
pub(crate) async fn describe_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<KeyInfo> {
self.key_info(key_id).await self.key_info(key_id).await
} }
pub(crate) async fn list_keys( async fn list_keys(&self, request: &ListKeysRequest, _context: Option<&OperationContext>) -> Result<ListKeysResponse> {
&self,
request: &ListKeysRequest,
_context: Option<&OperationContext>,
) -> Result<ListKeysResponse> {
let all_keys = self let all_keys = self
.run("vault_transit_list_keys", OpClass::ReadIdempotent, move || async move { .run("vault_transit_list_keys", OpClass::ReadIdempotent, move || async move {
let vault = self.vault().map_err(AttemptError::fatal)?; let vault = self.vault().map_err(AttemptError::fatal)?;
@@ -706,7 +692,7 @@ impl VaultTransitKmsClient {
}) })
} }
pub(crate) async fn enable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> { async fn enable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
// A pending deletion must be reverted through cancel_key_deletion, not // A pending deletion must be reverted through cancel_key_deletion, not
// silently by enabling, so the gate rejects PendingDeletion here. // silently by enabling, so the gate rejects PendingDeletion here.
let mut metadata = self.ensure_key_state_allows(key_id, StateGatedOperation::Enable).await?; let mut metadata = self.ensure_key_state_allows(key_id, StateGatedOperation::Enable).await?;
@@ -715,15 +701,13 @@ impl VaultTransitKmsClient {
self.store_key_metadata(key_id, &metadata).await self.store_key_metadata(key_id, &metadata).await
} }
pub(crate) async fn disable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> { async fn disable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
let mut metadata = self.ensure_key_state_allows(key_id, StateGatedOperation::Disable).await?; let mut metadata = self.ensure_key_state_allows(key_id, StateGatedOperation::Disable).await?;
metadata.key_state = KeyState::Disabled; metadata.key_state = KeyState::Disabled;
self.store_key_metadata(key_id, &metadata).await self.store_key_metadata(key_id, &metadata).await
} }
/// Test-only lifecycle driver: the product path goes through [`KmsBackend`]. async fn schedule_key_deletion(
#[cfg(test)]
pub(crate) async fn schedule_key_deletion(
&self, &self,
key_id: &str, key_id: &str,
pending_window_days: u32, pending_window_days: u32,
@@ -737,7 +721,17 @@ impl VaultTransitKmsClient {
self.store_key_metadata(key_id, &metadata).await self.store_key_metadata(key_id, &metadata).await
} }
pub(crate) async fn rotate_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<MasterKeyInfo> { async fn cancel_key_deletion(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
let mut metadata = self.get_key_metadata(key_id).await?;
if metadata.key_state != KeyState::PendingDeletion {
return Err(KmsError::invalid_key_state(format!("Key {key_id} is not pending deletion")));
}
metadata.key_state = KeyState::Enabled;
metadata.deletion_date = None;
self.store_key_metadata(key_id, &metadata).await
}
async fn rotate_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<MasterKeyInfo> {
self.ensure_key_state_allows(key_id, StateGatedOperation::Rotate).await?; self.ensure_key_state_allows(key_id, StateGatedOperation::Rotate).await?;
// Single attempt, never retried: replaying a rotate whose response was // Single attempt, never retried: replaying a rotate whose response was
@@ -774,7 +768,7 @@ impl VaultTransitKmsClient {
}) })
} }
pub(crate) async fn health_check(&self) -> Result<()> { async fn health_check(&self) -> Result<()> {
self.run("vault_transit_health_check", OpClass::ReadIdempotent, move || async move { self.run("vault_transit_health_check", OpClass::ReadIdempotent, move || async move {
let vault = self.vault().map_err(AttemptError::fatal)?; let vault = self.vault().map_err(AttemptError::fatal)?;
key::list(&vault.client, &self.config.mount_path) key::list(&vault.client, &self.config.mount_path)
@@ -786,6 +780,11 @@ impl VaultTransitKmsClient {
}) })
.await .await
} }
fn backend_info(&self) -> BackendInfo {
BackendInfo::new("vault-transit".to_string(), "0.1.0".to_string(), self.config.address.clone(), true)
.with_metadata("mount_path".to_string(), self.config.mount_path.clone())
}
} }
pub struct VaultTransitKmsBackend { pub struct VaultTransitKmsBackend {
@@ -793,6 +792,14 @@ pub struct VaultTransitKmsBackend {
} }
impl VaultTransitKmsBackend { impl VaultTransitKmsBackend {
/// Lifecycle driver for the shared state-machine contract tests. Using the
/// backend's own client keeps its in-process metadata cache coherent with
/// the transitions the tests perform.
#[cfg(test)]
pub(crate) fn lifecycle_client(&self) -> &VaultTransitKmsClient {
&self.client
}
pub async fn new(config: KmsConfig) -> Result<Self> { pub async fn new(config: KmsConfig) -> Result<Self> {
config.validate()?; config.validate()?;
@@ -993,18 +1000,6 @@ impl KmsBackend for VaultTransitKmsBackend {
}) })
} }
async fn enable_key(&self, key_id: &str) -> Result<()> {
self.client.enable_key(key_id, None).await
}
async fn disable_key(&self, key_id: &str) -> Result<()> {
self.client.disable_key(key_id, None).await
}
async fn rotate_key(&self, key_id: &str) -> Result<()> {
self.client.rotate_key(key_id, None).await.map(|_| ())
}
async fn health_check(&self) -> Result<bool> { async fn health_check(&self) -> Result<bool> {
self.client.health_check().await.map(|_| true) self.client.health_check().await.map(|_| true)
} }
@@ -1442,58 +1437,4 @@ mod tests {
assert_eq!(metadata.key_state, KeyState::Enabled); assert_eq!(metadata.key_state, KeyState::Enabled);
assert!(metadata.deletion_date.is_none()); assert!(metadata.deletion_date.is_none());
} }
/// KV2 write acknowledgement (`SecretVersionMetadata`) for `kv2::set`.
fn kv2_write_ack() -> serde_json::Value {
serde_json::json!({
"created_time": "2026-01-01T00:00:00Z",
"custom_metadata": null,
"deletion_time": "",
"destroyed": false,
"version": 2,
})
}
#[tokio::test]
async fn wired_backend_lifecycle_overrides_reach_the_client() {
let metadata = TransitKeyMetadata::from_create_request(&CreateKeyRequest::default());
let vault = ScriptedVault::serve(vec![
// disable: metadata cache miss reads KV, then persists Disabled.
ScriptedResponse::ok(metadata_read_data(&metadata)),
ScriptedResponse::ok(kv2_write_ack()),
// enable: the state gate hits the metadata cache, so only the
// persisting write goes out.
ScriptedResponse::ok(kv2_write_ack()),
// rotate: the gate hits the cache again; the single rotate
// attempt fails and must not be retried.
ScriptedResponse::error(503, "standby"),
])
.await;
let config = KmsConfig::vault_transit(
url::Url::parse(&vault.address).expect("scripted vault address should parse"),
"scripted-token".to_string(),
)
.with_insecure_development_defaults();
let backend = VaultTransitKmsBackend::new(config)
.await
.expect("vault transit backend should build");
backend
.disable_key("wired-key")
.await
.expect("KmsBackend::disable_key must persist through the client");
backend
.enable_key("wired-key")
.await
.expect("KmsBackend::enable_key must persist through the client");
let error = backend
.rotate_key("wired-key")
.await
.expect_err("the scripted 503 must fail the rotation");
assert!(matches!(error, KmsError::BackendError { .. }), "got {error:?}");
let requests = vault.requests();
assert_eq!(requests.len(), 4, "gated reads, two writes and one rotate attempt: {requests:?}");
assert_eq!(requests[3], "POST /v1/transit/keys/wired-key/rotate", "{requests:?}");
}
} }
+1
View File
@@ -182,6 +182,7 @@ impl DeletionWorker {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::backends::KmsClient as _;
use crate::backends::local::LocalKmsBackend; use crate::backends::local::LocalKmsBackend;
use crate::config::KmsConfig; use crate::config::KmsConfig;
use crate::error::KmsError; use crate::error::KmsError;
-3
View File
@@ -66,19 +66,16 @@ impl KmsManager {
} }
/// Encrypt data with a master key /// Encrypt data with a master key
#[hotpath::measure]
pub async fn encrypt(&self, request: EncryptRequest) -> Result<EncryptResponse> { pub async fn encrypt(&self, request: EncryptRequest) -> Result<EncryptResponse> {
self.backend.encrypt(request).await self.backend.encrypt(request).await
} }
/// Decrypt data with a master key /// Decrypt data with a master key
#[hotpath::measure]
pub async fn decrypt(&self, request: DecryptRequest) -> Result<DecryptResponse> { pub async fn decrypt(&self, request: DecryptRequest) -> Result<DecryptResponse> {
self.backend.decrypt(request).await self.backend.decrypt(request).await
} }
/// Generate a data encryption key /// Generate a data encryption key
#[hotpath::measure]
pub async fn generate_data_key(&self, request: GenerateDataKeyRequest) -> Result<GenerateDataKeyResponse> { pub async fn generate_data_key(&self, request: GenerateDataKeyRequest) -> Result<GenerateDataKeyResponse> {
self.backend.generate_data_key(request).await self.backend.generate_data_key(request).await
} }
+22 -508
View File
@@ -27,12 +27,6 @@
//! retried automatically: a response lost after the server applied the write //! retried automatically: a response lost after the server applied the write
//! would otherwise be replayed into duplicate side effects (extra key versions, //! would otherwise be replayed into duplicate side effects (extra key versions,
//! repeated deletes). //! repeated deletes).
//!
//! Every execution also records operation metrics (attempt failures by retry
//! class, terminal outcome, attempts used, wall-clock duration) through the
//! process-global `metrics` recorder. Metric labels carry only static enum
//! values — operation names, classes, outcomes — never key identifiers, key
//! material, ciphertext, or tokens.
use std::future::Future; use std::future::Future;
use std::time::Duration; use std::time::Duration;
@@ -206,130 +200,6 @@ fn equal_jitter(rng: &mut impl RngExt, cap: Duration) -> Duration {
half + Duration::from_nanos(rng.random_range(0..=spread)) half + Duration::from_nanos(rng.random_range(0..=spread))
} }
// ---------------------------------------------------------------------------
// Metrics
//
// Every execution is recorded here, at the single choke point all backend
// calls flow through, so instrumenting a new call site costs nothing beyond
// naming its operation. Label values are exclusively static enum strings
// (operation names, classes, outcomes) — key identifiers, key material,
// ciphertext, and tokens must never reach a metric label.
// ---------------------------------------------------------------------------
/// Counter: operations executed, by `operation`, `op_class`, and `outcome`.
const METRIC_OPERATIONS_TOTAL: &str = "rustfs_kms_backend_operations_total";
/// Counter: failed attempts, by `operation` and `error_class` (including
/// `attempt_timeout` for attempts cut off by the per-attempt timeout).
const METRIC_ATTEMPT_FAILURES_TOTAL: &str = "rustfs_kms_backend_attempt_failures_total";
/// Histogram: wall-clock duration of a whole operation (attempts plus
/// backoff), in seconds, by `operation` and `outcome`.
const METRIC_OPERATION_DURATION_SECONDS: &str = "rustfs_kms_backend_operation_duration_seconds";
/// Histogram: attempts one operation used before completing, by `operation`
/// and `outcome`.
const METRIC_OPERATION_ATTEMPTS: &str = "rustfs_kms_backend_operation_attempts";
impl OpClass {
fn as_label(self) -> &'static str {
match self {
OpClass::ReadIdempotent => "read_idempotent",
OpClass::MutatingNonIdempotent => "mutating_non_idempotent",
OpClass::Auth => "auth",
}
}
}
impl ErrorClass {
fn as_label(self) -> &'static str {
match self {
ErrorClass::RetryableConn => "retryable_conn",
ErrorClass::RetryableStatus => "retryable_status",
ErrorClass::Fatal => "fatal",
}
}
}
/// How one policy execution terminated, for the `outcome` metric label.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Outcome {
Success,
/// A fatal-classified failure ended the operation on its first observation.
Fatal,
/// The attempt budget ran out; the last failure was retryable (including a
/// timed-out final attempt).
BudgetExhausted,
/// The operation deadline ran out before another attempt could complete.
DeadlineExceeded,
Cancelled,
}
impl Outcome {
fn as_label(self) -> &'static str {
match self {
Outcome::Success => "success",
Outcome::Fatal => "fatal",
Outcome::BudgetExhausted => "budget_exhausted",
Outcome::DeadlineExceeded => "deadline_exceeded",
Outcome::Cancelled => "cancelled",
}
}
}
/// Register metric descriptions once per process.
fn describe_metrics() {
static DESCRIBE: std::sync::Once = std::sync::Once::new();
DESCRIBE.call_once(|| {
metrics::describe_counter!(
METRIC_OPERATIONS_TOTAL,
"Total KMS backend operations executed under the operation policy, by operation, operation class, and outcome"
);
metrics::describe_counter!(
METRIC_ATTEMPT_FAILURES_TOTAL,
"Total failed KMS backend attempts, by operation and retry classification"
);
metrics::describe_histogram!(
METRIC_OPERATION_DURATION_SECONDS,
"Wall-clock duration of KMS backend operations including retries and backoff, in seconds"
);
metrics::describe_histogram!(
METRIC_OPERATION_ATTEMPTS,
"Number of attempts a KMS backend operation used before completing"
);
});
}
/// Record one failed attempt with its retry classification.
fn record_attempt_failure(operation: &'static str, error_class: &'static str) {
metrics::counter!(
METRIC_ATTEMPT_FAILURES_TOTAL,
"operation" => operation,
"error_class" => error_class
)
.increment(1);
}
/// Record the terminal outcome of one policy execution.
fn record_operation(operation: &'static str, class: OpClass, outcome: Outcome, attempts: u32, elapsed: Duration) {
metrics::counter!(
METRIC_OPERATIONS_TOTAL,
"operation" => operation,
"op_class" => class.as_label(),
"outcome" => outcome.as_label()
)
.increment(1);
metrics::histogram!(
METRIC_OPERATION_DURATION_SECONDS,
"operation" => operation,
"outcome" => outcome.as_label()
)
.record(elapsed.as_secs_f64());
metrics::histogram!(
METRIC_OPERATION_ATTEMPTS,
"operation" => operation,
"outcome" => outcome.as_label()
)
.record(f64::from(attempts));
}
/// Run `attempt` under the policy. /// Run `attempt` under the policy.
/// ///
/// Each attempt is bounded by `attempt_timeout` (further capped by whatever is /// Each attempt is bounded by `attempt_timeout` (further capped by whatever is
@@ -360,37 +230,13 @@ where
/// [`execute`] with an injectable jitter source so tests can pin deterministic /// [`execute`] with an injectable jitter source so tests can pin deterministic
/// backoff durations instead of asserting around random sleeps. /// backoff durations instead of asserting around random sleeps.
pub(crate) async fn execute_with_jitter<T, F, Fut, J>( pub(crate) async fn execute_with_jitter<T, F, Fut, J>(
operation: &'static str,
class: OpClass,
policy: &RetryPolicy,
cancel: &CancellationToken,
jitter: J,
attempt: F,
) -> Result<T>
where
F: FnMut() -> Fut,
Fut: Future<Output = std::result::Result<T, AttemptError>>,
J: FnMut(Duration) -> Duration,
{
describe_metrics();
let started = Instant::now();
let mut attempts_made = 0u32;
let (outcome, result) = drive_attempts(operation, class, policy, cancel, jitter, attempt, &mut attempts_made).await;
record_operation(operation, class, outcome, attempts_made, started.elapsed());
result
}
/// The attempt loop behind [`execute_with_jitter`], returning the terminal
/// outcome alongside the result so the caller can record it exactly once.
async fn drive_attempts<T, F, Fut, J>(
operation: &'static str, operation: &'static str,
class: OpClass, class: OpClass,
policy: &RetryPolicy, policy: &RetryPolicy,
cancel: &CancellationToken, cancel: &CancellationToken,
mut jitter: J, mut jitter: J,
mut attempt: F, mut attempt: F,
attempts_made: &mut u32, ) -> Result<T>
) -> (Outcome, Result<T>)
where where
F: FnMut() -> Fut, F: FnMut() -> Fut,
Fut: Future<Output = std::result::Result<T, AttemptError>>, Fut: Future<Output = std::result::Result<T, AttemptError>>,
@@ -401,68 +247,48 @@ where
let mut attempt_no = 0u32; let mut attempt_no = 0u32;
loop { loop {
attempt_no += 1;
if cancel.is_cancelled() { if cancel.is_cancelled() {
return ( return Err(KmsError::operation_cancelled(format!(
Outcome::Cancelled, "{operation} cancelled before attempt {attempt_no}"
Err(KmsError::operation_cancelled(format!( )));
"{operation} cancelled before attempt {}",
attempt_no + 1
))),
);
} }
let remaining = deadline.saturating_duration_since(Instant::now()); let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() { if remaining.is_zero() {
return ( return Err(KmsError::operation_timed_out(format!(
Outcome::DeadlineExceeded, "{operation} exceeded operation deadline of {:?}",
Err(KmsError::operation_timed_out(format!( policy.op_deadline
"{operation} exceeded operation deadline of {:?}", )));
policy.op_deadline
))),
);
} }
attempt_no += 1;
*attempts_made = attempt_no;
let attempt_budget = policy.attempt_timeout.min(remaining); let attempt_budget = policy.attempt_timeout.min(remaining);
let outcome = tokio::select! { let outcome = tokio::select! {
biased; biased;
_ = cancel.cancelled() => { _ = cancel.cancelled() => {
return ( return Err(KmsError::operation_cancelled(format!("{operation} cancelled during attempt {attempt_no}")));
Outcome::Cancelled,
Err(KmsError::operation_cancelled(format!("{operation} cancelled during attempt {attempt_no}"))),
);
} }
outcome = tokio::time::timeout(attempt_budget, attempt()) => outcome, outcome = tokio::time::timeout(attempt_budget, attempt()) => outcome,
}; };
let failure = match outcome { let failure = match outcome {
Ok(Ok(value)) => return (Outcome::Success, Ok(value)), Ok(Ok(value)) => return Ok(value),
Ok(Err(failure)) => { Ok(Err(failure)) => failure,
record_attempt_failure(operation, failure.class.as_label()); Err(_) => AttemptError {
failure class: ErrorClass::RetryableConn,
} error: KmsError::operation_timed_out(format!(
Err(_) => { "{operation} attempt {attempt_no} timed out after {attempt_budget:?}"
record_attempt_failure(operation, "attempt_timeout"); )),
AttemptError { },
class: ErrorClass::RetryableConn,
error: KmsError::operation_timed_out(format!(
"{operation} attempt {attempt_no} timed out after {attempt_budget:?}"
)),
}
}
}; };
if failure.class == ErrorClass::Fatal { if failure.class == ErrorClass::Fatal || attempt_no >= max_attempts {
return (Outcome::Fatal, Err(failure.error)); return Err(failure.error);
}
if attempt_no >= max_attempts {
return (Outcome::BudgetExhausted, Err(failure.error));
} }
let backoff = jitter(backoff_cap(policy, attempt_no)); let backoff = jitter(backoff_cap(policy, attempt_no));
if backoff >= deadline.saturating_duration_since(Instant::now()) { if backoff >= deadline.saturating_duration_since(Instant::now()) {
// Not enough deadline budget left for another attempt. // Not enough deadline budget left for another attempt.
return (Outcome::DeadlineExceeded, Err(failure.error)); return Err(failure.error);
} }
tracing::warn!( tracing::warn!(
operation, operation,
@@ -474,10 +300,7 @@ where
tokio::select! { tokio::select! {
biased; biased;
_ = cancel.cancelled() => { _ = cancel.cancelled() => {
return ( return Err(KmsError::operation_cancelled(format!("{operation} cancelled during retry backoff")));
Outcome::Cancelled,
Err(KmsError::operation_cancelled(format!("{operation} cancelled during retry backoff"))),
);
} }
_ = tokio::time::sleep(backoff) => {} _ = tokio::time::sleep(backoff) => {}
} }
@@ -805,313 +628,4 @@ mod tests {
assert_eq!(classify_vaultrs(&ClientError::ResponseEmptyError), ErrorClass::Fatal); assert_eq!(classify_vaultrs(&ClientError::ResponseEmptyError), ErrorClass::Fatal);
assert_eq!(classify_vaultrs(&ClientError::InvalidLoginMethodError), ErrorClass::Fatal); assert_eq!(classify_vaultrs(&ClientError::InvalidLoginMethodError), ErrorClass::Fatal);
} }
// -- Metric emission ----------------------------------------------------
//
// Each test installs a thread-local debugging recorder and drives a
// paused-clock current-thread runtime inside it, so the emitted metrics
// (including virtual-clock durations) are fully deterministic.
use metrics_util::MetricKind;
use metrics_util::debugging::{DebugValue, DebuggingRecorder};
type MetricEntry = (
metrics_util::CompositeKey,
Option<metrics::Unit>,
Option<metrics::SharedString>,
DebugValue,
);
/// Run `test` on a paused current-thread runtime under a debugging
/// recorder and return one snapshot of everything it emitted.
///
/// A single snapshot per test on purpose: `Snapshotter::snapshot` drains
/// the recorded state, so taking it per assertion would only show the
/// first assertion any data.
fn record_metrics<Out>(test: impl FnOnce() -> std::pin::Pin<Box<dyn Future<Output = Out>>>) -> (Vec<MetricEntry>, Out) {
let recorder = DebuggingRecorder::new();
let snapshotter = recorder.snapshotter();
let out = metrics::with_local_recorder(&recorder, || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_time()
.start_paused(true)
.build()
.expect("current-thread runtime must build");
runtime.block_on(test())
});
(snapshotter.snapshot().into_vec(), out)
}
fn labels_match(key: &metrics::Key, labels: &[(&str, &str)]) -> bool {
labels
.iter()
.all(|(label, expected)| key.labels().any(|l| l.key() == *label && l.value() == *expected))
}
fn counter_value(snapshot: &[MetricEntry], name: &str, labels: &[(&str, &str)]) -> u64 {
snapshot
.iter()
.filter_map(|(composite, _unit, _description, value)| {
let matches = composite.kind() == MetricKind::Counter
&& composite.key().name() == name
&& labels_match(composite.key(), labels);
match (matches, value) {
(true, DebugValue::Counter(count)) => Some(*count),
_ => None,
}
})
.sum()
}
fn histogram_values(snapshot: &[MetricEntry], name: &str, labels: &[(&str, &str)]) -> Vec<f64> {
snapshot
.iter()
.filter_map(|(composite, _unit, _description, value)| {
let matches = composite.kind() == MetricKind::Histogram
&& composite.key().name() == name
&& labels_match(composite.key(), labels);
match (matches, value) {
(true, DebugValue::Histogram(values)) => Some(values),
_ => None,
}
})
.flatten()
.map(|value| value.into_inner())
.collect()
}
#[test]
fn metrics_record_retried_success_with_attempts_and_duration() {
let calls_in_test = Arc::new(AtomicU32::new(0));
let (snapshot, ()) = record_metrics(move || {
Box::pin(async move {
let policy = policy_of(1_000, 60_000, 3, 100, 2_000);
let cancel = CancellationToken::new();
let calls_in_attempt = calls_in_test.clone();
execute_with_jitter("metrics_read", OpClass::ReadIdempotent, &policy, &cancel, full_jitter, move || {
let calls = calls_in_attempt.clone();
async move {
if calls.fetch_add(1, Ordering::SeqCst) < 2 {
Err(AttemptError {
class: ErrorClass::RetryableStatus,
error: KmsError::backend_error("throttled (429)"),
})
} else {
Ok(())
}
}
})
.await
.expect("retries within budget must succeed");
})
});
assert_eq!(
counter_value(
&snapshot,
METRIC_OPERATIONS_TOTAL,
&[
("operation", "metrics_read"),
("op_class", "read_idempotent"),
("outcome", "success")
]
),
1
);
assert_eq!(
counter_value(
&snapshot,
METRIC_ATTEMPT_FAILURES_TOTAL,
&[("operation", "metrics_read"), ("error_class", "retryable_status")]
),
2
);
assert_eq!(
histogram_values(
&snapshot,
METRIC_OPERATION_ATTEMPTS,
&[("operation", "metrics_read"), ("outcome", "success")]
),
vec![3.0]
);
// Full-cap backoffs of 100ms and 200ms on the paused clock.
let durations = histogram_values(
&snapshot,
METRIC_OPERATION_DURATION_SECONDS,
&[("operation", "metrics_read"), ("outcome", "success")],
);
assert_eq!(durations.len(), 1);
assert!((durations[0] - 0.3).abs() < 1e-9, "expected 0.3s of virtual backoff, got {durations:?}");
}
#[test]
fn metrics_record_fatal_outcome_with_single_attempt() {
let (snapshot, ()) = record_metrics(|| {
Box::pin(async {
let policy = policy_of(1_000, 60_000, 5, 100, 2_000);
let cancel = CancellationToken::new();
let result: Result<()> =
execute_with_jitter("metrics_fatal", OpClass::ReadIdempotent, &policy, &cancel, full_jitter, || async {
Err(AttemptError {
class: ErrorClass::Fatal,
error: KmsError::access_denied("permission denied (403)"),
})
})
.await;
result.expect_err("a fatal failure must end the operation");
})
});
assert_eq!(
counter_value(
&snapshot,
METRIC_OPERATIONS_TOTAL,
&[("operation", "metrics_fatal"), ("outcome", "fatal")]
),
1
);
assert_eq!(
counter_value(
&snapshot,
METRIC_ATTEMPT_FAILURES_TOTAL,
&[("operation", "metrics_fatal"), ("error_class", "fatal")]
),
1
);
assert_eq!(
histogram_values(
&snapshot,
METRIC_OPERATION_ATTEMPTS,
&[("operation", "metrics_fatal"), ("outcome", "fatal")]
),
vec![1.0]
);
}
#[test]
fn metrics_record_mutating_budget_exhausted_after_one_attempt() {
let (snapshot, ()) = record_metrics(|| {
Box::pin(async {
let policy = policy_of(1_000, 60_000, 5, 100, 2_000);
let cancel = CancellationToken::new();
let result: Result<()> = execute_with_jitter(
"metrics_rotate",
OpClass::MutatingNonIdempotent,
&policy,
&cancel,
full_jitter,
|| async { Err(retryable_conn_error()) },
)
.await;
result.expect_err("a mutating operation must not retry a retryable failure");
})
});
assert_eq!(
counter_value(
&snapshot,
METRIC_OPERATIONS_TOTAL,
&[
("operation", "metrics_rotate"),
("op_class", "mutating_non_idempotent"),
("outcome", "budget_exhausted")
]
),
1
);
assert_eq!(
histogram_values(
&snapshot,
METRIC_OPERATION_ATTEMPTS,
&[("operation", "metrics_rotate"), ("outcome", "budget_exhausted")]
),
vec![1.0]
);
}
#[test]
fn metrics_record_timeouts_and_deadline_outcome() {
let (snapshot, ()) = record_metrics(|| {
Box::pin(async {
// Hung attempts: 10s each against a 25s deadline (see
// total_duration_never_exceeds_deadline for the timeline).
let policy = policy_of(10_000, 25_000, 5, 100, 2_000);
let cancel = CancellationToken::new();
let result: Result<()> =
execute_with_jitter("metrics_hung", OpClass::ReadIdempotent, &policy, &cancel, full_jitter, || {
std::future::pending::<AttemptResult<()>>()
})
.await;
result.expect_err("hung attempts must exhaust the deadline");
})
});
assert_eq!(
counter_value(
&snapshot,
METRIC_OPERATIONS_TOTAL,
&[("operation", "metrics_hung"), ("outcome", "deadline_exceeded")]
),
1
);
assert_eq!(
counter_value(
&snapshot,
METRIC_ATTEMPT_FAILURES_TOTAL,
&[("operation", "metrics_hung"), ("error_class", "attempt_timeout")]
),
3
);
let durations = histogram_values(
&snapshot,
METRIC_OPERATION_DURATION_SECONDS,
&[("operation", "metrics_hung"), ("outcome", "deadline_exceeded")],
);
assert_eq!(durations.len(), 1);
assert!((durations[0] - 25.0).abs() < 1e-9, "expected the full 25s deadline, got {durations:?}");
}
#[test]
fn metrics_record_cancelled_outcome() {
let calls_in_test = Arc::new(AtomicU32::new(0));
let (snapshot, ()) = record_metrics(move || {
Box::pin(async move {
let policy = policy_of(1_000, 600_000, 5, 10_000, 10_000);
let cancel = CancellationToken::new();
let canceller = cancel.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(500)).await;
canceller.cancel();
});
let calls_in_attempt = calls_in_test.clone();
let result: Result<()> =
execute_with_jitter("metrics_cancel", OpClass::ReadIdempotent, &policy, &cancel, full_jitter, move || {
let calls = calls_in_attempt.clone();
async move {
calls.fetch_add(1, Ordering::SeqCst);
Err(retryable_conn_error())
}
})
.await;
result.expect_err("cancellation must abort the backoff");
})
});
assert_eq!(
counter_value(
&snapshot,
METRIC_OPERATIONS_TOTAL,
&[("operation", "metrics_cancel"), ("outcome", "cancelled")]
),
1
);
assert_eq!(
histogram_values(
&snapshot,
METRIC_OPERATION_ATTEMPTS,
&[("operation", "metrics_cancel"), ("outcome", "cancelled")]
),
vec![1.0]
);
}
} }
-255
View File
@@ -1,255 +0,0 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Fault-injection matrix for the Vault backend operation policy.
//!
//! Offline cases run against locally injected transport faults (a closed
//! port, a listener that never responds) — deterministic, no external
//! dependencies. Real-Vault cases are `#[ignore]`d and need a dev Vault
//! (default `http://127.0.0.1:8200`, override with `RUSTFS_KMS_VAULT_ADDR`).
//!
//! Throttling (429) and recoverable 5xx responses cannot be forced on a stock
//! dev Vault, so their retry and metric behavior is pinned deterministically
//! by the scripted-Vault wiring tests in `backends::vault` and the engine
//! tests in `policy.rs`. Pointing `RUSTFS_KMS_VAULT_ADDR` at a
//! fault-injecting proxy reuses the ignored cases here unchanged.
//!
//! Every case installs a thread-local debugging metrics recorder and drives a
//! current-thread runtime inside it, so the policy metrics double as the
//! request-count assertion even against a real server.
use std::time::Duration;
use metrics_util::MetricKind;
use metrics_util::debugging::{DebugValue, DebuggingRecorder};
use rustfs_kms::backends::KmsClient;
use rustfs_kms::backends::vault::VaultKmsClient;
use rustfs_kms::{KmsConfig, KmsError, VaultAuthMethod, VaultConfig};
const OPERATIONS_TOTAL: &str = "rustfs_kms_backend_operations_total";
const ATTEMPT_FAILURES_TOTAL: &str = "rustfs_kms_backend_attempt_failures_total";
fn vault_config(address: &str, token: &str) -> VaultConfig {
VaultConfig {
address: address.to_string(),
auth_method: VaultAuthMethod::Token {
token: token.to_string(),
},
namespace: None,
mount_path: "transit".to_string(),
kv_mount: "secret".to_string(),
key_path_prefix: "rustfs/kms/fault-injection".to_string(),
tls: None,
}
}
fn kms_config(attempt_timeout: Duration, retry_attempts: u32) -> KmsConfig {
KmsConfig {
timeout: attempt_timeout,
retry_attempts,
..KmsConfig::default()
}
}
type MetricEntry = (
metrics_util::CompositeKey,
Option<metrics::Unit>,
Option<metrics::SharedString>,
DebugValue,
);
/// Run `test` on a current-thread runtime under a debugging metrics recorder
/// and return one snapshot of everything it emitted.
///
/// A single snapshot per test on purpose: `Snapshotter::snapshot` drains the
/// recorded state, so taking it per assertion would only show the first
/// assertion any data.
fn record_metrics(test: impl FnOnce() -> std::pin::Pin<Box<dyn std::future::Future<Output = ()>>>) -> Vec<MetricEntry> {
let recorder = DebuggingRecorder::new();
let snapshotter = recorder.snapshotter();
metrics::with_local_recorder(&recorder, || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("current-thread runtime must build");
runtime.block_on(test());
});
snapshotter.snapshot().into_vec()
}
/// Sum of counters with `name` whose labels include every `(key, value)` pair.
fn counter_value(snapshot: &[MetricEntry], name: &str, labels: &[(&str, &str)]) -> u64 {
snapshot
.iter()
.filter_map(|(composite, _unit, _description, value)| {
let key = composite.key();
let matches = composite.kind() == MetricKind::Counter
&& key.name() == name
&& labels
.iter()
.all(|(label, expected)| key.labels().any(|l| l.key() == *label && l.value() == *expected));
match (matches, value) {
(true, DebugValue::Counter(count)) => Some(*count),
_ => None,
}
})
.sum()
}
/// Connection refused: connection-class failures are retried up to the
/// configured budget, then surface as a backend error.
#[test]
fn connection_refused_is_retried_within_budget() {
// Reserve a loopback port and release it so nothing is listening there.
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("reserve a loopback port");
let address = format!("http://{}", listener.local_addr().expect("reserved port addr"));
drop(listener);
let snapshot = record_metrics(|| {
Box::pin(async move {
let client = VaultKmsClient::new(vault_config(&address, "unused"), &kms_config(Duration::from_secs(2), 2))
.await
.expect("client construction performs no network calls");
let error = client
.describe_key("fault-injection-refused", None)
.await
.expect_err("a refused connection must fail the operation");
assert!(matches!(error, KmsError::BackendError { .. }), "got {error:?}");
})
});
assert_eq!(
counter_value(&snapshot, ATTEMPT_FAILURES_TOTAL, &[("error_class", "retryable_conn")]),
2,
"both budgeted attempts must observe the refused connection"
);
assert_eq!(counter_value(&snapshot, OPERATIONS_TOTAL, &[("outcome", "budget_exhausted")]), 1);
// The static-token login records its own success; the Vault read must not.
assert_eq!(
counter_value(
&snapshot,
OPERATIONS_TOTAL,
&[("operation", "vault_kv2_read_key"), ("outcome", "success")]
),
0
);
}
/// Stalled connection: a server that accepts but never responds is cut off by
/// the per-attempt timeout (either the policy timer or the equally sized HTTP
/// client timeout, whichever fires first) instead of hanging forever.
#[test]
fn stalled_connection_is_cut_off_by_the_attempt_timeout() {
let snapshot = record_metrics(|| {
Box::pin(async move {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind stall listener");
let address = format!("http://{}", listener.local_addr().expect("stall listener addr"));
// Accept and park every connection without ever responding.
tokio::spawn(async move {
let mut parked = Vec::new();
loop {
let Ok((socket, _)) = listener.accept().await else { return };
parked.push(socket);
}
});
let client = VaultKmsClient::new(vault_config(&address, "unused"), &kms_config(Duration::from_millis(250), 1))
.await
.expect("client construction performs no network calls");
let error = client
.describe_key("fault-injection-stalled", None)
.await
.expect_err("a stalled request must be cut off by the attempt timeout");
assert!(
matches!(error, KmsError::OperationTimedOut { .. } | KmsError::BackendError { .. }),
"got {error:?}"
);
})
});
// The policy timer reports attempt_timeout; the client-level HTTP timeout
// surfaces as a connection-class failure. Either way it is exactly one
// attempt that was cut off.
let cut_off = counter_value(&snapshot, ATTEMPT_FAILURES_TOTAL, &[("error_class", "attempt_timeout")])
+ counter_value(&snapshot, ATTEMPT_FAILURES_TOTAL, &[("error_class", "retryable_conn")]);
assert_eq!(cut_off, 1, "the single budgeted attempt must be cut off by a timeout");
assert_eq!(counter_value(&snapshot, OPERATIONS_TOTAL, &[("outcome", "budget_exhausted")]), 1);
}
fn real_vault_address() -> String {
std::env::var("RUSTFS_KMS_VAULT_ADDR").unwrap_or_else(|_| "http://127.0.0.1:8200".to_string())
}
/// Invalid token against a real Vault: the 403 is fatal — exactly one
/// attempt, no retry, and the operation fails closed.
#[test]
#[ignore] // Requires a running Vault dev server
fn real_vault_invalid_token_is_fatal_and_never_retried() {
let snapshot = record_metrics(|| {
Box::pin(async {
let config = vault_config(&real_vault_address(), "fault-injection-invalid-token");
let client = VaultKmsClient::new(config, &kms_config(Duration::from_secs(5), 3))
.await
.expect("client construction performs no network calls");
let error = client
.describe_key("fault-injection-forbidden", None)
.await
.expect_err("an invalid token must be rejected");
assert!(matches!(error, KmsError::BackendError { .. }), "got {error:?}");
})
});
assert_eq!(
counter_value(&snapshot, ATTEMPT_FAILURES_TOTAL, &[("error_class", "fatal")]),
1,
"a 403 must be observed by exactly one attempt"
);
assert_eq!(counter_value(&snapshot, OPERATIONS_TOTAL, &[("outcome", "fatal")]), 1);
assert_eq!(
counter_value(&snapshot, ATTEMPT_FAILURES_TOTAL, &[("error_class", "retryable_status")]),
0,
"an auth failure must never be classified as retryable"
);
}
/// Healthy read against a real Vault: a missing key resolves in one attempt
/// (404 is fatal for retry purposes) and records a fatal outcome rather than
/// burning the retry budget.
#[test]
#[ignore] // Requires a running Vault dev server
fn real_vault_missing_key_is_resolved_in_one_attempt() {
let token = std::env::var("RUSTFS_KMS_VAULT_TOKEN").unwrap_or_else(|_| "dev-only-token".to_string());
let snapshot = record_metrics(|| {
Box::pin(async move {
let config = vault_config(&real_vault_address(), &token);
let client = VaultKmsClient::new(config, &kms_config(Duration::from_secs(5), 3))
.await
.expect("client construction performs no network calls");
let error = client
.describe_key("fault-injection-definitely-missing", None)
.await
.expect_err("a missing key must resolve to key-not-found");
assert!(matches!(error, KmsError::KeyNotFound { .. }), "got {error:?}");
})
});
assert_eq!(
counter_value(&snapshot, ATTEMPT_FAILURES_TOTAL, &[("error_class", "fatal")]),
1,
"a 404 must be observed by exactly one attempt"
);
assert_eq!(counter_value(&snapshot, OPERATIONS_TOTAL, &[("outcome", "fatal")]), 1);
}
-28
View File
@@ -25,35 +25,7 @@ keywords = ["lifecycle", "storage", "rustfs", "Minio"]
categories = ["web-programming", "development-tools", "filesystem"] categories = ["web-programming", "development-tools", "filesystem"]
documentation = "https://docs.rs/rustfs-lifecycle/latest/rustfs_lifecycle/" documentation = "https://docs.rs/rustfs-lifecycle/latest/rustfs_lifecycle/"
[features]
default = []
hotpath = [
"hotpath/hotpath",
"hotpath/tokio",
"rustfs-common/hotpath",
"rustfs-config/hotpath",
"rustfs-replication/hotpath",
"rustfs-storage-api/hotpath",
]
hotpath-alloc = [
"hotpath",
"hotpath/hotpath-alloc",
"rustfs-common/hotpath-alloc",
"rustfs-config/hotpath-alloc",
"rustfs-replication/hotpath-alloc",
"rustfs-storage-api/hotpath-alloc",
]
hotpath-cpu = [
"hotpath",
"hotpath/hotpath-cpu",
"rustfs-common/hotpath-cpu",
"rustfs-config/hotpath-cpu",
"rustfs-replication/hotpath-cpu",
"rustfs-storage-api/hotpath-cpu",
]
[dependencies] [dependencies]
hotpath.workspace = true
async-trait.workspace = true async-trait.workspace = true
metrics.workspace = true metrics.workspace = true
rustfs-common.workspace = true rustfs-common.workspace = true
-14
View File
@@ -28,21 +28,7 @@ documentation = "https://docs.rs/rustfs-lock/latest/rustfs_lock/"
[lints] [lints]
workspace = true workspace = true
[features]
default = []
hotpath = [
"hotpath/hotpath",
"hotpath/tokio",
"hotpath/futures",
"hotpath/parking_lot",
"rustfs-io-metrics/hotpath",
"rustfs-utils/hotpath",
]
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc", "rustfs-io-metrics/hotpath-alloc", "rustfs-utils/hotpath-alloc"]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu", "rustfs-io-metrics/hotpath-cpu", "rustfs-utils/hotpath-cpu"]
[dependencies] [dependencies]
hotpath.workspace = true
rustfs-io-metrics = { workspace = true } rustfs-io-metrics = { workspace = true }
rustfs-utils = { workspace = true } rustfs-utils = { workspace = true }
async-trait.workspace = true async-trait.workspace = true
-4
View File
@@ -540,7 +540,6 @@ impl DistributedLock {
} }
/// Acquire a lock and return a RAII guard /// Acquire a lock and return a RAII guard
#[hotpath::measure]
pub(crate) async fn acquire_guard(&self, request: &LockRequest) -> Result<Option<DistributedLockGuard>> { pub(crate) async fn acquire_guard(&self, request: &LockRequest) -> Result<Option<DistributedLockGuard>> {
if self.clients.is_empty() { if self.clients.is_empty() {
return Err(LockError::internal("No lock clients available")); return Err(LockError::internal("No lock clients available"));
@@ -627,7 +626,6 @@ impl DistributedLock {
} }
/// Convenience: acquire exclusive lock as a guard /// Convenience: acquire exclusive lock as a guard
#[hotpath::measure]
pub async fn lock_guard( pub async fn lock_guard(
&self, &self,
resource: ObjectKey, resource: ObjectKey,
@@ -642,7 +640,6 @@ impl DistributedLock {
} }
/// Convenience: acquire exclusive lock with expected contention logs suppressed /// Convenience: acquire exclusive lock with expected contention logs suppressed
#[hotpath::measure]
pub async fn lock_guard_quiet( pub async fn lock_guard_quiet(
&self, &self,
resource: ObjectKey, resource: ObjectKey,
@@ -658,7 +655,6 @@ impl DistributedLock {
} }
/// Convenience: acquire shared lock as a guard /// Convenience: acquire shared lock as a guard
#[hotpath::measure]
pub async fn rlock_guard( pub async fn rlock_guard(
&self, &self,
resource: ObjectKey, resource: ObjectKey,
-7
View File
@@ -32,14 +32,7 @@ doctest = false
name = "la-dump-anchors" name = "la-dump-anchors"
path = "src/bin/la_dump_anchors.rs" path = "src/bin/la_dump_anchors.rs"
[features]
default = []
hotpath = ["hotpath/hotpath"]
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
[dependencies] [dependencies]
hotpath.workspace = true
chrono = { workspace = true, features = ["serde"] } chrono = { workspace = true, features = ["serde"] }
flate2 = { workspace = true } flate2 = { workspace = true }
regex = { workspace = true } regex = { workspace = true }
-7
View File
@@ -28,14 +28,7 @@ documentation = "https://docs.rs/rustfs-madmin/latest/rustfs_madmin/"
[lints] [lints]
workspace = true workspace = true
[features]
default = []
hotpath = ["hotpath/hotpath"]
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
[dependencies] [dependencies]
hotpath.workspace = true
chrono = { workspace = true, features = ["serde"] } chrono = { workspace = true, features = ["serde"] }
humantime.workspace = true humantime.workspace = true
hyper = { workspace = true, features = ["http2", "http1", "server"] } hyper = { workspace = true, features = ["http2", "http1", "server"] }
-31
View File
@@ -26,40 +26,9 @@ categories = ["web-programming", "development-tools", "filesystem"]
documentation = "https://docs.rs/rustfs-notify/latest/rustfs_notify/" documentation = "https://docs.rs/rustfs-notify/latest/rustfs_notify/"
[features] [features]
hotpath = [
"hotpath/hotpath",
"hotpath/tokio",
"rustfs-config/hotpath",
"rustfs-ecstore/hotpath",
"rustfs-s3-ops/hotpath",
"rustfs-s3-types/hotpath",
"rustfs-targets/hotpath",
"rustfs-utils/hotpath",
]
hotpath-alloc = [
"hotpath",
"hotpath/hotpath-alloc",
"rustfs-config/hotpath-alloc",
"rustfs-ecstore/hotpath-alloc",
"rustfs-s3-ops/hotpath-alloc",
"rustfs-s3-types/hotpath-alloc",
"rustfs-targets/hotpath-alloc",
"rustfs-utils/hotpath-alloc",
]
hotpath-cpu = [
"hotpath",
"hotpath/hotpath-cpu",
"rustfs-config/hotpath-cpu",
"rustfs-ecstore/hotpath-cpu",
"rustfs-s3-ops/hotpath-cpu",
"rustfs-s3-types/hotpath-cpu",
"rustfs-targets/hotpath-cpu",
"rustfs-utils/hotpath-cpu",
]
demo-examples = [] demo-examples = []
[dependencies] [dependencies]
hotpath.workspace = true
rustfs-config = { workspace = true, features = ["notify", "server-config-model"] } rustfs-config = { workspace = true, features = ["notify", "server-config-model"] }
rustfs-ecstore = { workspace = true } rustfs-ecstore = { workspace = true }
rustfs-s3-types = { workspace = true } rustfs-s3-types = { workspace = true }
-26
View File
@@ -34,33 +34,7 @@ harness = false
[lints] [lints]
workspace = true workspace = true
[features]
default = []
hotpath = [
"hotpath/hotpath",
"hotpath/tokio",
"hotpath/futures",
"rustfs-config/hotpath",
"rustfs-io-metrics/hotpath",
"rustfs-utils/hotpath",
]
hotpath-alloc = [
"hotpath",
"hotpath/hotpath-alloc",
"rustfs-config/hotpath-alloc",
"rustfs-io-metrics/hotpath-alloc",
"rustfs-utils/hotpath-alloc",
]
hotpath-cpu = [
"hotpath",
"hotpath/hotpath-cpu",
"rustfs-config/hotpath-cpu",
"rustfs-io-metrics/hotpath-cpu",
"rustfs-utils/hotpath-cpu",
]
[dependencies] [dependencies]
hotpath.workspace = true
rustfs-config = { workspace = true, features = ["constants"] } rustfs-config = { workspace = true, features = ["constants"] }
rustfs-io-metrics = { workspace = true } rustfs-io-metrics = { workspace = true }
rustfs-utils = { workspace = true, features = ["os"] } rustfs-utils = { workspace = true, features = ["os"] }
@@ -1035,7 +1035,6 @@ impl HybridCapacityManager {
/// ///
/// Joiners subscribe to the watch channel *before* releasing the mutex, which guarantees /// Joiners subscribe to the watch channel *before* releasing the mutex, which guarantees
/// they cannot miss the completion notification even if the leader finishes very quickly. /// they cannot miss the completion notification even if the leader finishes very quickly.
#[hotpath::measure]
pub async fn refresh_or_join<F, Fut>(&self, source: DataSource, refresh_fn: F) -> Result<CapacityUpdate, String> pub async fn refresh_or_join<F, Fut>(&self, source: DataSource, refresh_fn: F) -> Result<CapacityUpdate, String>
where where
F: FnOnce() -> Fut, F: FnOnce() -> Fut,
@@ -1143,7 +1142,6 @@ impl HybridCapacityManager {
} }
/// Start a background refresh if one is not already in flight. /// Start a background refresh if one is not already in flight.
#[hotpath::measure]
pub async fn spawn_refresh_if_needed<F, Fut>(self: Arc<Self>, source: DataSource, refresh_fn: F) -> bool pub async fn spawn_refresh_if_needed<F, Fut>(self: Arc<Self>, source: DataSource, refresh_fn: F) -> bool
where where
F: FnOnce() -> Fut + Send + 'static, F: FnOnce() -> Fut + Send + 'static,
-1
View File
@@ -338,7 +338,6 @@ pub async fn select_capacity_refresh_disks(
} }
} }
#[hotpath::measure]
pub async fn refresh_capacity_with_scope(disks: Vec<CapacityDiskRef>, dirty_subset: bool) -> Result<CapacityUpdate, String> { pub async fn refresh_capacity_with_scope(disks: Vec<CapacityDiskRef>, dirty_subset: bool) -> Result<CapacityUpdate, String> {
let scan_started_at = Instant::now(); let scan_started_at = Instant::now();
let report = calculate_data_dir_used_capacity_report(&disks) let report = calculate_data_dir_used_capacity_report(&disks)
-7
View File
@@ -27,14 +27,7 @@ categories = ["web-programming", "development-tools"]
[lib] [lib]
doctest = false doctest = false
[features]
default = []
hotpath = ["hotpath/hotpath", "hotpath/tokio"]
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
[dependencies] [dependencies]
hotpath.workspace = true
bytes = { workspace = true, features = ["serde"] } bytes = { workspace = true, features = ["serde"] }
metrics = { workspace = true } metrics = { workspace = true }
moka = { workspace = true, features = ["future"] } moka = { workspace = true, features = ["future"] }
-45
View File
@@ -27,50 +27,6 @@ documentation = "https://docs.rs/rustfs-obs/latest/rustfs_obs/"
[features] [features]
default = [] default = []
hotpath = [
"hotpath/hotpath",
"hotpath/tokio",
"hotpath/futures",
"hotpath/crossbeam",
"rustfs-audit/hotpath",
"rustfs-common/hotpath",
"rustfs-config/hotpath",
"rustfs-ecstore/hotpath",
"rustfs-iam/hotpath",
"rustfs-io-metrics/hotpath",
"rustfs-notify/hotpath",
"rustfs-security-governance/hotpath",
"rustfs-storage-api/hotpath",
"rustfs-utils/hotpath",
]
hotpath-alloc = [
"hotpath",
"hotpath/hotpath-alloc",
"rustfs-audit/hotpath-alloc",
"rustfs-common/hotpath-alloc",
"rustfs-config/hotpath-alloc",
"rustfs-ecstore/hotpath-alloc",
"rustfs-iam/hotpath-alloc",
"rustfs-io-metrics/hotpath-alloc",
"rustfs-notify/hotpath-alloc",
"rustfs-security-governance/hotpath-alloc",
"rustfs-storage-api/hotpath-alloc",
"rustfs-utils/hotpath-alloc",
]
hotpath-cpu = [
"hotpath",
"hotpath/hotpath-cpu",
"rustfs-audit/hotpath-cpu",
"rustfs-common/hotpath-cpu",
"rustfs-config/hotpath-cpu",
"rustfs-ecstore/hotpath-cpu",
"rustfs-iam/hotpath-cpu",
"rustfs-io-metrics/hotpath-cpu",
"rustfs-notify/hotpath-cpu",
"rustfs-security-governance/hotpath-cpu",
"rustfs-storage-api/hotpath-cpu",
"rustfs-utils/hotpath-cpu",
]
# Tokio runtime-level telemetry. Requires a `--cfg tokio_unstable` build; the # Tokio runtime-level telemetry. Requires a `--cfg tokio_unstable` build; the
# build script fails the compile when that flag is missing. Off by default so # build script fails the compile when that flag is missing. Off by default so
# ordinary builds neither pay for nor depend on Tokio's unstable API. # ordinary builds neither pay for nor depend on Tokio's unstable API.
@@ -102,7 +58,6 @@ required-features = ["dial9"]
workspace = true workspace = true
[dependencies] [dependencies]
hotpath.workspace = true
rustfs-audit = { workspace = true } rustfs-audit = { workspace = true }
rustfs-common = { workspace = true } rustfs-common = { workspace = true }
rustfs-config = { workspace = true, features = ["observability"] } rustfs-config = { workspace = true, features = ["observability"] }
-25
View File
@@ -28,30 +28,6 @@ documentation = "https://docs.rs/rustfs-policy/latest/rustfs_policy/"
[lints] [lints]
workspace = true workspace = true
[features]
default = []
hotpath = [
"hotpath/hotpath",
"hotpath/reqwest-0-13",
"rustfs-config/hotpath",
"rustfs-credentials/hotpath",
"rustfs-crypto/hotpath",
]
hotpath-alloc = [
"hotpath",
"hotpath/hotpath-alloc",
"rustfs-config/hotpath-alloc",
"rustfs-credentials/hotpath-alloc",
"rustfs-crypto/hotpath-alloc",
]
hotpath-cpu = [
"hotpath",
"hotpath/hotpath-cpu",
"rustfs-config/hotpath-cpu",
"rustfs-credentials/hotpath-cpu",
"rustfs-crypto/hotpath-cpu",
]
[dependencies] [dependencies]
rustfs-credentials = { workspace = true } rustfs-credentials = { workspace = true }
rustfs-config = { workspace = true, features = ["opa"] } rustfs-config = { workspace = true, features = ["opa"] }
@@ -73,7 +49,6 @@ moka = { workspace = true, features = ["future"] }
async-trait.workspace = true async-trait.workspace = true
futures.workspace = true futures.workspace = true
pollster.workspace = true pollster.workspace = true
hotpath.workspace = true
[dev-dependencies] [dev-dependencies]
pollster.workspace = true pollster.workspace = true
+1 -9
View File
@@ -32,15 +32,10 @@ impl Args {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct AuthZPlugin { pub struct AuthZPlugin {
client: OpaHttpClient, client: reqwest::Client,
args: Args, args: Args,
} }
#[cfg(feature = "hotpath")]
type OpaHttpClient = hotpath::wrap::reqwest::Client;
#[cfg(not(feature = "hotpath"))]
type OpaHttpClient = reqwest::Client;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum OpaConfigError { pub enum OpaConfigError {
#[error("Missing required env var: {0}")] #[error("Missing required env var: {0}")]
@@ -146,9 +141,6 @@ impl AuthZPlugin {
reqwest::Client::new() reqwest::Client::new()
}); });
#[cfg(feature = "hotpath")]
let client = hotpath::http!(client, label = "Policy::OPA");
Self { client, args: config } Self { client, args: config }
} }
-50
View File
@@ -30,55 +30,6 @@ workspace = true
[features] [features]
default = [] default = []
hotpath = [
"hotpath/hotpath",
"hotpath/tokio",
"hotpath/futures",
"rustfs-config/hotpath",
"rustfs-credentials/hotpath",
"rustfs-ecstore?/hotpath",
"rustfs-iam/hotpath",
"rustfs-keystone?/hotpath",
"rustfs-policy/hotpath",
"rustfs-rio?/hotpath",
"rustfs-storage-api/hotpath",
"rustfs-tls-runtime?/hotpath",
"rustfs-trusted-proxies?/hotpath",
"rustfs-utils/hotpath",
"rustfs-test-utils/hotpath",
]
hotpath-alloc = [
"hotpath",
"hotpath/hotpath-alloc",
"rustfs-config/hotpath-alloc",
"rustfs-credentials/hotpath-alloc",
"rustfs-ecstore?/hotpath-alloc",
"rustfs-iam/hotpath-alloc",
"rustfs-keystone?/hotpath-alloc",
"rustfs-policy/hotpath-alloc",
"rustfs-rio?/hotpath-alloc",
"rustfs-storage-api/hotpath-alloc",
"rustfs-tls-runtime?/hotpath-alloc",
"rustfs-trusted-proxies?/hotpath-alloc",
"rustfs-utils/hotpath-alloc",
"rustfs-test-utils/hotpath-alloc",
]
hotpath-cpu = [
"hotpath",
"hotpath/hotpath-cpu",
"rustfs-config/hotpath-cpu",
"rustfs-credentials/hotpath-cpu",
"rustfs-ecstore?/hotpath-cpu",
"rustfs-iam/hotpath-cpu",
"rustfs-keystone?/hotpath-cpu",
"rustfs-policy/hotpath-cpu",
"rustfs-rio?/hotpath-cpu",
"rustfs-storage-api/hotpath-cpu",
"rustfs-tls-runtime?/hotpath-cpu",
"rustfs-trusted-proxies?/hotpath-cpu",
"rustfs-utils/hotpath-cpu",
"rustfs-test-utils/hotpath-cpu",
]
ftps = ["dep:libunftp", "dep:unftp-core", "dep:rustls", "dep:rustfs-tls-runtime", "dep:subtle"] ftps = ["dep:libunftp", "dep:unftp-core", "dep:rustls", "dep:rustfs-tls-runtime", "dep:subtle"]
swift = [ swift = [
"dep:rustfs-keystone", "dep:rustfs-keystone",
@@ -111,7 +62,6 @@ webdav = ["dep:dav-server", "dep:hyper", "dep:hyper-util", "dep:http-body-util",
sftp = ["dep:russh", "dep:russh-sftp", "dep:uuid", "dep:subtle", "dep:tokio-util", "dep:socket2"] sftp = ["dep:russh", "dep:russh-sftp", "dep:uuid", "dep:subtle", "dep:tokio-util", "dep:socket2"]
[dependencies] [dependencies]
hotpath.workspace = true
# Core RustFS dependencies # Core RustFS dependencies
rustfs-iam = { workspace = true } rustfs-iam = { workspace = true }
rustfs-credentials = { workspace = true } rustfs-credentials = { workspace = true }
-31
View File
@@ -32,38 +32,7 @@ workspace = true
name = "gproto" name = "gproto"
path = "src/main.rs" path = "src/main.rs"
[features]
default = []
hotpath = [
"hotpath/hotpath",
"hotpath/tokio",
"rustfs-common/hotpath",
"rustfs-config/hotpath",
"rustfs-io-metrics/hotpath",
"rustfs-tls-runtime/hotpath",
"rustfs-utils/hotpath",
]
hotpath-alloc = [
"hotpath",
"hotpath/hotpath-alloc",
"rustfs-common/hotpath-alloc",
"rustfs-config/hotpath-alloc",
"rustfs-io-metrics/hotpath-alloc",
"rustfs-tls-runtime/hotpath-alloc",
"rustfs-utils/hotpath-alloc",
]
hotpath-cpu = [
"hotpath",
"hotpath/hotpath-cpu",
"rustfs-common/hotpath-cpu",
"rustfs-config/hotpath-cpu",
"rustfs-io-metrics/hotpath-cpu",
"rustfs-tls-runtime/hotpath-cpu",
"rustfs-utils/hotpath-cpu",
]
[dependencies] [dependencies]
hotpath.workspace = true
rustfs-common.workspace = true rustfs-common.workspace = true
rustfs-io-metrics.workspace = true rustfs-io-metrics.workspace = true
rustfs-config.workspace = true rustfs-config.workspace = true
-7
View File
@@ -25,14 +25,7 @@ keywords = ["replication", "storage", "rustfs", "Minio"]
categories = ["web-programming", "development-tools", "filesystem"] categories = ["web-programming", "development-tools", "filesystem"]
documentation = "https://docs.rs/rustfs-replication/latest/rustfs_replication/" documentation = "https://docs.rs/rustfs-replication/latest/rustfs_replication/"
[features]
default = []
hotpath = ["hotpath/hotpath", "hotpath/tokio"]
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
[dependencies] [dependencies]
hotpath.workspace = true
bytes = { workspace = true, features = ["serde"] } bytes = { workspace = true, features = ["serde"] }
byteorder.workspace = true byteorder.workspace = true
regex.workspace = true regex.workspace = true
-25
View File
@@ -28,32 +28,7 @@ documentation = "https://docs.rs/rustfs-rio-v2/latest/rustfs_rio_v2/"
[lints] [lints]
workspace = true workspace = true
[features]
default = []
hotpath = [
"hotpath/hotpath",
"hotpath/tokio",
"rustfs-rio/hotpath",
"rustfs-utils/hotpath",
"rustfs-filemeta/hotpath",
]
hotpath-alloc = [
"hotpath",
"hotpath/hotpath-alloc",
"rustfs-rio/hotpath-alloc",
"rustfs-utils/hotpath-alloc",
"rustfs-filemeta/hotpath-alloc",
]
hotpath-cpu = [
"hotpath",
"hotpath/hotpath-cpu",
"rustfs-rio/hotpath-cpu",
"rustfs-utils/hotpath-cpu",
"rustfs-filemeta/hotpath-cpu",
]
[dependencies] [dependencies]
hotpath.workspace = true
aes-gcm = { workspace = true, features = ["rand_core"] } aes-gcm = { workspace = true, features = ["rand_core"] }
bytes = { workspace = true, features = ["serde"] } bytes = { workspace = true, features = ["serde"] }
chacha20poly1305.workspace = true chacha20poly1305.workspace = true
+2 -26
View File
@@ -30,32 +30,8 @@ workspace = true
[features] [features]
default = [] default = []
hotpath = [ hotpath = ["hotpath/hotpath", "hotpath/tokio", "hotpath/futures", "hotpath/reqwest-0-13"]
"hotpath/hotpath", hotpath-alloc = ["hotpath/hotpath-alloc"]
"hotpath/tokio",
"hotpath/futures",
"hotpath/reqwest-0-13",
"rustfs-config/hotpath",
"rustfs-io-metrics/hotpath",
"rustfs-tls-runtime/hotpath",
"rustfs-utils/hotpath",
]
hotpath-alloc = [
"hotpath",
"hotpath/hotpath-alloc",
"rustfs-config/hotpath-alloc",
"rustfs-io-metrics/hotpath-alloc",
"rustfs-tls-runtime/hotpath-alloc",
"rustfs-utils/hotpath-alloc",
]
hotpath-cpu = [
"hotpath",
"hotpath/hotpath-cpu",
"rustfs-config/hotpath-cpu",
"rustfs-io-metrics/hotpath-cpu",
"rustfs-tls-runtime/hotpath-cpu",
"rustfs-utils/hotpath-cpu",
]
[dependencies] [dependencies]
hotpath.workspace = true hotpath.workspace = true
-7
View File
@@ -27,14 +27,7 @@ categories = ["data-structures"]
[lints] [lints]
workspace = true workspace = true
[features]
default = []
hotpath = ["hotpath/hotpath", "rustfs-s3-types/hotpath"]
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc", "rustfs-s3-types/hotpath-alloc"]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu", "rustfs-s3-types/hotpath-cpu"]
[dependencies] [dependencies]
hotpath.workspace = true
rustfs-s3-types = { workspace = true } rustfs-s3-types = { workspace = true }
[lib] [lib]
-7
View File
@@ -27,14 +27,7 @@ categories = ["data-structures"]
[lints] [lints]
workspace = true workspace = true
[features]
default = []
hotpath = ["hotpath/hotpath"]
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
[dependencies] [dependencies]
hotpath.workspace = true
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["raw_value"] } serde_json = { workspace = true, features = ["raw_value"] }
-30
View File
@@ -28,37 +28,7 @@ documentation = "https://docs.rs/rustfs-s3select-api/latest/rustfs_s3select_api/
[lints] [lints]
workspace = true workspace = true
[features]
default = []
hotpath = [
"hotpath/hotpath",
"hotpath/tokio",
"hotpath/futures",
"hotpath/parking_lot",
"rustfs-common/hotpath",
"rustfs-ecstore/hotpath",
"rustfs-storage-api/hotpath",
"rustfs-test-utils/hotpath",
]
hotpath-alloc = [
"hotpath",
"hotpath/hotpath-alloc",
"rustfs-common/hotpath-alloc",
"rustfs-ecstore/hotpath-alloc",
"rustfs-storage-api/hotpath-alloc",
"rustfs-test-utils/hotpath-alloc",
]
hotpath-cpu = [
"hotpath",
"hotpath/hotpath-cpu",
"rustfs-common/hotpath-cpu",
"rustfs-ecstore/hotpath-cpu",
"rustfs-storage-api/hotpath-cpu",
"rustfs-test-utils/hotpath-cpu",
]
[dependencies] [dependencies]
hotpath.workspace = true
metrics = { workspace = true } metrics = { workspace = true }
async-trait.workspace = true async-trait.workspace = true
bytes = { workspace = true, features = ["serde"] } bytes = { workspace = true, features = ["serde"] }
-13
View File
@@ -28,20 +28,7 @@ documentation = "https://docs.rs/rustfs-s3select-query/latest/rustfs_s3select_qu
[lints] [lints]
workspace = true workspace = true
[features]
default = []
hotpath = [
"hotpath/hotpath",
"hotpath/tokio",
"hotpath/futures",
"hotpath/parking_lot",
"rustfs-s3select-api/hotpath",
]
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc", "rustfs-s3select-api/hotpath-alloc"]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu", "rustfs-s3select-api/hotpath-cpu"]
[dependencies] [dependencies]
hotpath.workspace = true
rustfs-s3select-api = { workspace = true } rustfs-s3select-api = { workspace = true }
async-recursion = { workspace = true } async-recursion = { workspace = true }
async-trait.workspace = true async-trait.workspace = true
-41
View File
@@ -29,48 +29,7 @@ documentation = "https://docs.rs/rustfs-scanner/latest/rustfs_scanner/"
[lints] [lints]
workspace = true workspace = true
[features]
default = []
hotpath = [
"hotpath/hotpath",
"hotpath/tokio",
"hotpath/futures",
"rustfs-common/hotpath",
"rustfs-config/hotpath",
"rustfs-credentials/hotpath",
"rustfs-data-usage/hotpath",
"rustfs-ecstore/hotpath",
"rustfs-filemeta/hotpath",
"rustfs-storage-api/hotpath",
"rustfs-utils/hotpath",
]
hotpath-alloc = [
"hotpath",
"hotpath/hotpath-alloc",
"rustfs-common/hotpath-alloc",
"rustfs-config/hotpath-alloc",
"rustfs-credentials/hotpath-alloc",
"rustfs-data-usage/hotpath-alloc",
"rustfs-ecstore/hotpath-alloc",
"rustfs-filemeta/hotpath-alloc",
"rustfs-storage-api/hotpath-alloc",
"rustfs-utils/hotpath-alloc",
]
hotpath-cpu = [
"hotpath",
"hotpath/hotpath-cpu",
"rustfs-common/hotpath-cpu",
"rustfs-config/hotpath-cpu",
"rustfs-credentials/hotpath-cpu",
"rustfs-data-usage/hotpath-cpu",
"rustfs-ecstore/hotpath-cpu",
"rustfs-filemeta/hotpath-cpu",
"rustfs-storage-api/hotpath-cpu",
"rustfs-utils/hotpath-cpu",
]
[dependencies] [dependencies]
hotpath.workspace = true
rustfs-config = { workspace = true, features = ["server-config-model"] } rustfs-config = { workspace = true, features = ["server-config-model"] }
rustfs-common = { workspace = true } rustfs-common = { workspace = true }
rustfs-credentials = { workspace = true } rustfs-credentials = { workspace = true }
-1
View File
@@ -2531,7 +2531,6 @@ where
} }
#[instrument(skip_all)] #[instrument(skip_all)]
#[hotpath::measure]
async fn run_data_scanner_cycle( async fn run_data_scanner_cycle(
ctx: &CancellationToken, ctx: &CancellationToken,
storeapi: &Arc<ECStore>, storeapi: &Arc<ECStore>,
-7
View File
@@ -30,12 +30,5 @@ doctest = false
[lints] [lints]
workspace = true workspace = true
[features]
default = []
hotpath = ["hotpath/hotpath"]
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
[dependencies] [dependencies]
hotpath.workspace = true
thiserror = { workspace = true } thiserror = { workspace = true }
-7
View File
@@ -25,14 +25,7 @@ keywords = ["digital-signature", "verification", "integrity", "rustfs", "Minio"]
categories = ["web-programming", "development-tools", "cryptography"] categories = ["web-programming", "development-tools", "cryptography"]
documentation = "https://docs.rs/rustfs-signer/latest/rustfs_signer/" documentation = "https://docs.rs/rustfs-signer/latest/rustfs_signer/"
[features]
default = []
hotpath = ["hotpath/hotpath", "rustfs-utils/hotpath"]
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc", "rustfs-utils/hotpath-alloc"]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu", "rustfs-utils/hotpath-cpu"]
[dependencies] [dependencies]
hotpath.workspace = true
tracing.workspace = true tracing.workspace = true
bytes = { workspace = true, features = ["serde"] } bytes = { workspace = true, features = ["serde"] }
http.workspace = true http.workspace = true
-7
View File
@@ -27,14 +27,7 @@ categories = ["web-programming", "development-tools", "filesystem"]
[lib] [lib]
doctest = false doctest = false
[features]
default = []
hotpath = ["hotpath/hotpath", "hotpath/tokio", "rustfs-filemeta/hotpath"]
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc", "rustfs-filemeta/hotpath-alloc"]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu", "rustfs-filemeta/hotpath-cpu"]
[dependencies] [dependencies]
hotpath.workspace = true
async-trait.workspace = true async-trait.workspace = true
# Storage-facing replication contracts are isolated in src/replication.rs until # Storage-facing replication contracts are isolated in src/replication.rs until
# the underlying wire types can move without creating a replication/storage-api cycle. # the underlying wire types can move without creating a replication/storage-api cycle.
-34
View File
@@ -11,41 +11,7 @@ keywords = ["file-system", "notification", "target", "rustfs", "Minio"]
categories = ["web-programming", "development-tools", "filesystem"] categories = ["web-programming", "development-tools", "filesystem"]
documentation = "https://docs.rs/rustfs-targets/latest/rustfs_targets/" documentation = "https://docs.rs/rustfs-targets/latest/rustfs_targets/"
[features]
default = []
hotpath = [
"hotpath/hotpath",
"hotpath/tokio",
"hotpath/futures",
"hotpath/parking_lot",
"hotpath/reqwest-0-13",
"rustfs-config/hotpath",
"rustfs-extension-schema/hotpath",
"rustfs-s3-types/hotpath",
"rustfs-tls-runtime/hotpath",
"rustfs-utils/hotpath",
]
hotpath-alloc = [
"hotpath",
"hotpath/hotpath-alloc",
"rustfs-config/hotpath-alloc",
"rustfs-extension-schema/hotpath-alloc",
"rustfs-s3-types/hotpath-alloc",
"rustfs-tls-runtime/hotpath-alloc",
"rustfs-utils/hotpath-alloc",
]
hotpath-cpu = [
"hotpath",
"hotpath/hotpath-cpu",
"rustfs-config/hotpath-cpu",
"rustfs-extension-schema/hotpath-cpu",
"rustfs-s3-types/hotpath-cpu",
"rustfs-tls-runtime/hotpath-cpu",
"rustfs-utils/hotpath-cpu",
]
[dependencies] [dependencies]
hotpath.workspace = true
rustfs-config = { workspace = true, features = ["notify", "audit", "server-config-model"] } rustfs-config = { workspace = true, features = ["notify", "audit", "server-config-model"] }
rustfs-extension-schema = { workspace = true } rustfs-extension-schema = { workspace = true }
rustfs-tls-runtime = { workspace = true } rustfs-tls-runtime = { workspace = true }
-5
View File
@@ -522,7 +522,6 @@ fn snapshot_from_delivery(target_id: TargetID, delivery: TargetDeliverySnapshot)
} }
} }
#[hotpath::measure]
pub async fn init_target_and_optionally_start_replay<E, F, G>( pub async fn init_target_and_optionally_start_replay<E, F, G>(
target: Box<dyn Target<E> + Send + Sync>, target: Box<dyn Target<E> + Send + Sync>,
on_replay_start: F, on_replay_start: F,
@@ -558,7 +557,6 @@ where
Some((shared, cancel)) Some((shared, cancel))
} }
#[hotpath::measure]
pub(crate) async fn prepare_target<E>( pub(crate) async fn prepare_target<E>(
target: Box<dyn Target<E> + Send + Sync>, target: Box<dyn Target<E> + Send + Sync>,
cancellation: Option<&CancellationToken>, cancellation: Option<&CancellationToken>,
@@ -600,7 +598,6 @@ where
type ActivatedTarget<E> = (SharedTarget<E>, Option<(mpsc::Sender<()>, JoinHandle<()>)>); type ActivatedTarget<E> = (SharedTarget<E>, Option<(mpsc::Sender<()>, JoinHandle<()>)>);
#[hotpath::measure]
pub async fn activate_targets_with_replay<E, F, Fut>( pub async fn activate_targets_with_replay<E, F, Fut>(
targets: Vec<Box<dyn Target<E> + Send + Sync>>, targets: Vec<Box<dyn Target<E> + Send + Sync>>,
mut activate_one: F, mut activate_one: F,
@@ -673,7 +670,6 @@ fn seed_interval_start(now: tokio::time::Instant, interval: Duration) -> tokio::
now.checked_sub(interval).unwrap_or(now) now.checked_sub(interval).unwrap_or(now)
} }
#[hotpath::measure]
async fn stream_replay_worker<E>( async fn stream_replay_worker<E>(
store: &mut (dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send), store: &mut (dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send),
target: SharedTarget<E>, target: SharedTarget<E>,
@@ -809,7 +805,6 @@ async fn stream_replay_worker<E>(
/// Returns `true` if a cancel signal was observed while processing (e.g. during /// Returns `true` if a cancel signal was observed while processing (e.g. during
/// retry backoff), so the caller can stop promptly instead of continuing to /// retry backoff), so the caller can stop promptly instead of continuing to
/// drain a store that a replacement worker may already own. /// drain a store that a replacement worker may already own.
#[hotpath::measure]
async fn process_replay_batch<E>( async fn process_replay_batch<E>(
store: &(dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send), store: &(dyn Store<QueuedPayload, Error = StoreError, Key = Key> + Send),
batch_keys: &mut Vec<Key>, batch_keys: &mut Vec<Key>,
-2
View File
@@ -85,7 +85,6 @@ fn classify_probe_error(err: &reqwest::Error) -> TargetHealthReason {
TargetHealthReason::Unreachable TargetHealthReason::Unreachable
} }
#[hotpath::measure]
async fn probe_health_url(client: &Client, health_check_url: &Url) -> TargetHealth { async fn probe_health_url(client: &Client, health_check_url: &Url) -> TargetHealth {
match tokio::time::timeout(WEBHOOK_HEALTH_TIMEOUT, client.head(health_check_url.as_str()).send()).await { match tokio::time::timeout(WEBHOOK_HEALTH_TIMEOUT, client.head(health_check_url.as_str()).send()).await {
Ok(Ok(_)) => TargetHealth::online(TargetHealthReason::Reachable), Ok(Ok(_)) => TargetHealth::online(TargetHealthReason::Reachable),
@@ -481,7 +480,6 @@ where
build_queued_payload(event) build_queued_payload(event)
} }
#[hotpath::measure]
async fn send_body(&self, body: Vec<u8>, meta: &QueuedPayloadMeta) -> Result<(), TargetError> { async fn send_body(&self, body: Vec<u8>, meta: &QueuedPayloadMeta) -> Result<(), TargetError> {
debug!( debug!(
event = EVENT_WEBHOOK_DELIVERY_STATE, event = EVENT_WEBHOOK_DELIVERY_STATE,
-25
View File
@@ -25,32 +25,7 @@ keywords = ["testing", "storage", "rustfs", "Minio"]
categories = ["development-tools", "filesystem"] categories = ["development-tools", "filesystem"]
documentation = "https://docs.rs/rustfs-test-utils/latest/rustfs_test_utils/" documentation = "https://docs.rs/rustfs-test-utils/latest/rustfs_test_utils/"
[features]
default = []
hotpath = [
"hotpath/hotpath",
"hotpath/tokio",
"rustfs-ecstore/hotpath",
"rustfs-storage-api/hotpath",
"rustfs-data-usage/hotpath",
]
hotpath-alloc = [
"hotpath",
"hotpath/hotpath-alloc",
"rustfs-ecstore/hotpath-alloc",
"rustfs-storage-api/hotpath-alloc",
"rustfs-data-usage/hotpath-alloc",
]
hotpath-cpu = [
"hotpath",
"hotpath/hotpath-cpu",
"rustfs-ecstore/hotpath-cpu",
"rustfs-storage-api/hotpath-cpu",
"rustfs-data-usage/hotpath-cpu",
]
[dependencies] [dependencies]
hotpath.workspace = true
rustfs-ecstore = { workspace = true } rustfs-ecstore = { workspace = true }
rustfs-storage-api = { workspace = true } rustfs-storage-api = { workspace = true }
tokio = { workspace = true, features = ["fs", "rt-multi-thread"] } tokio = { workspace = true, features = ["fs", "rt-multi-thread"] }
-7
View File
@@ -27,14 +27,7 @@ categories = ["network-programming", "web-programming", "development-tools"]
[lints] [lints]
workspace = true workspace = true
[features]
default = []
hotpath = ["hotpath/hotpath", "hotpath/tokio", "rustfs-common/hotpath", "rustfs-config/hotpath"]
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc", "rustfs-common/hotpath-alloc", "rustfs-config/hotpath-alloc"]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu", "rustfs-common/hotpath-cpu", "rustfs-config/hotpath-cpu"]
[dependencies] [dependencies]
hotpath.workspace = true
rustfs-common.workspace = true rustfs-common.workspace = true
rustfs-config.workspace = true rustfs-config.workspace = true
arc-swap.workspace = true arc-swap.workspace = true
-13
View File
@@ -24,20 +24,7 @@ description = " RustFS Trusted Proxies module provides secure and efficient mana
keywords = ["trusted-proxies", "network-security", "rustfs", "proxy-management"] keywords = ["trusted-proxies", "network-security", "rustfs", "proxy-management"]
categories = ["network-programming", "security", "web-programming"] categories = ["network-programming", "security", "web-programming"]
[features]
default = []
hotpath = [
"hotpath/hotpath",
"hotpath/tokio",
"hotpath/reqwest-0-13",
"rustfs-config/hotpath",
"rustfs-utils/hotpath",
]
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc", "rustfs-config/hotpath-alloc", "rustfs-utils/hotpath-alloc"]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu", "rustfs-config/hotpath-cpu", "rustfs-utils/hotpath-cpu"]
[dependencies] [dependencies]
hotpath.workspace = true
async-trait = { workspace = true } async-trait = { workspace = true }
axum = { workspace = true } axum = { workspace = true }
http = { workspace = true } http = { workspace = true }
@@ -227,7 +227,6 @@ pub struct GoogleCloudIpRanges;
impl GoogleCloudIpRanges { impl GoogleCloudIpRanges {
/// Fetches the latest Google Cloud IP ranges from their official source. /// Fetches the latest Google Cloud IP ranges from their official source.
#[hotpath::measure]
pub async fn fetch() -> Result<Vec<IpNetwork>, AppError> { pub async fn fetch() -> Result<Vec<IpNetwork>, AppError> {
let client = Client::builder() let client = Client::builder()
.timeout(Duration::from_secs(10)) .timeout(Duration::from_secs(10))
-4
View File
@@ -25,7 +25,6 @@ keywords = ["utilities", "hashing", "compression", "network", "rustfs"]
categories = ["web-programming", "development-tools", "cryptography"] categories = ["web-programming", "development-tools", "cryptography"]
[dependencies] [dependencies]
hotpath.workspace = true
base64-simd = { workspace = true, optional = true } base64-simd = { workspace = true, optional = true }
blake2 = { workspace = true, optional = true } blake2 = { workspace = true, optional = true }
brotli = { workspace = true, optional = true } brotli = { workspace = true, optional = true }
@@ -77,9 +76,6 @@ workspace = true
[features] [features]
default = ["ip"] # features that are enabled by default default = ["ip"] # features that are enabled by default
hotpath = ["hotpath/hotpath", "hotpath/tokio", "hotpath/futures", "hotpath/reqwest-0-13"]
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
ip = ["dep:local-ip-address"] # ip characteristics and their dependencies ip = ["dep:local-ip-address"] # ip characteristics and their dependencies
net = ["ip", "dep:url", "dep:netif", "dep:futures", "dep:transform-stream", "dep:bytes", "dep:hyper", "dep:tokio"] # network features with DNS resolver net = ["ip", "dep:url", "dep:netif", "dep:futures", "dep:transform-stream", "dep:bytes", "dep:hyper", "dep:tokio"] # network features with DNS resolver
egress = ["ip", "dep:reqwest", "dep:tokio", "dep:url"] egress = ["ip", "dep:reqwest", "dep:tokio", "dep:url"]
-7
View File
@@ -32,14 +32,7 @@ doctest = false
name = "zip_benchmark" name = "zip_benchmark"
harness = false harness = false
[features]
default = []
hotpath = ["hotpath/hotpath", "hotpath/tokio"]
hotpath-alloc = ["hotpath", "hotpath/hotpath-alloc"]
hotpath-cpu = ["hotpath", "hotpath/hotpath-cpu"]
[dependencies] [dependencies]
hotpath.workspace = true
async-compression = { workspace = true, features = [ async-compression = { workspace = true, features = [
"tokio", "tokio",
"bzip2", "bzip2",
-122
View File
@@ -60,137 +60,15 @@ hotpath = [
"hotpath/crossbeam", "hotpath/crossbeam",
"hotpath/parking_lot", "hotpath/parking_lot",
"hotpath/reqwest-0-13", "hotpath/reqwest-0-13",
"rustfs-audit/hotpath",
"rustfs-common/hotpath",
"rustfs-concurrency/hotpath",
"rustfs-config/hotpath",
"rustfs-credentials/hotpath",
"rustfs-crypto/hotpath",
"rustfs-data-usage/hotpath",
"rustfs-ecstore/hotpath", "rustfs-ecstore/hotpath",
"rustfs-extension-schema/hotpath",
"rustfs-filemeta/hotpath", "rustfs-filemeta/hotpath",
"rustfs-heal/hotpath",
"rustfs-iam/hotpath",
"rustfs-io-core/hotpath",
"rustfs-io-metrics/hotpath",
"rustfs-keystone/hotpath",
"rustfs-kms/hotpath",
"rustfs-lock/hotpath",
"rustfs-log-analyzer/hotpath",
"rustfs-madmin/hotpath",
"rustfs-notify/hotpath",
"rustfs-object-capacity/hotpath",
"rustfs-object-data-cache/hotpath",
"rustfs-obs/hotpath",
"rustfs-policy/hotpath",
"rustfs-protocols/hotpath",
"rustfs-protos/hotpath",
"rustfs-rio/hotpath", "rustfs-rio/hotpath",
"rustfs-s3-ops/hotpath",
"rustfs-s3-types/hotpath",
"rustfs-s3select-api/hotpath",
"rustfs-s3select-query/hotpath",
"rustfs-scanner/hotpath",
"rustfs-security-governance/hotpath",
"rustfs-signer/hotpath",
"rustfs-storage-api/hotpath",
"rustfs-targets/hotpath",
"rustfs-tls-runtime/hotpath",
"rustfs-trusted-proxies/hotpath",
"rustfs-utils/hotpath",
"rustfs-zip/hotpath",
"rustfs-test-utils/hotpath",
] ]
hotpath-alloc = [ hotpath-alloc = [
"hotpath",
"hotpath/hotpath-alloc", "hotpath/hotpath-alloc",
"rustfs-audit/hotpath-alloc",
"rustfs-common/hotpath-alloc",
"rustfs-concurrency/hotpath-alloc",
"rustfs-config/hotpath-alloc",
"rustfs-credentials/hotpath-alloc",
"rustfs-crypto/hotpath-alloc",
"rustfs-data-usage/hotpath-alloc",
"rustfs-ecstore/hotpath-alloc", "rustfs-ecstore/hotpath-alloc",
"rustfs-extension-schema/hotpath-alloc",
"rustfs-filemeta/hotpath-alloc", "rustfs-filemeta/hotpath-alloc",
"rustfs-heal/hotpath-alloc",
"rustfs-iam/hotpath-alloc",
"rustfs-io-core/hotpath-alloc",
"rustfs-io-metrics/hotpath-alloc",
"rustfs-keystone/hotpath-alloc",
"rustfs-kms/hotpath-alloc",
"rustfs-lock/hotpath-alloc",
"rustfs-log-analyzer/hotpath-alloc",
"rustfs-madmin/hotpath-alloc",
"rustfs-notify/hotpath-alloc",
"rustfs-object-capacity/hotpath-alloc",
"rustfs-object-data-cache/hotpath-alloc",
"rustfs-obs/hotpath-alloc",
"rustfs-policy/hotpath-alloc",
"rustfs-protocols/hotpath-alloc",
"rustfs-protos/hotpath-alloc",
"rustfs-rio/hotpath-alloc", "rustfs-rio/hotpath-alloc",
"rustfs-s3-ops/hotpath-alloc",
"rustfs-s3-types/hotpath-alloc",
"rustfs-s3select-api/hotpath-alloc",
"rustfs-s3select-query/hotpath-alloc",
"rustfs-scanner/hotpath-alloc",
"rustfs-security-governance/hotpath-alloc",
"rustfs-signer/hotpath-alloc",
"rustfs-storage-api/hotpath-alloc",
"rustfs-targets/hotpath-alloc",
"rustfs-tls-runtime/hotpath-alloc",
"rustfs-trusted-proxies/hotpath-alloc",
"rustfs-utils/hotpath-alloc",
"rustfs-zip/hotpath-alloc",
"rustfs-test-utils/hotpath-alloc",
]
hotpath-cpu = [
"hotpath",
"hotpath/hotpath-cpu",
"rustfs-audit/hotpath-cpu",
"rustfs-common/hotpath-cpu",
"rustfs-concurrency/hotpath-cpu",
"rustfs-config/hotpath-cpu",
"rustfs-credentials/hotpath-cpu",
"rustfs-crypto/hotpath-cpu",
"rustfs-data-usage/hotpath-cpu",
"rustfs-ecstore/hotpath-cpu",
"rustfs-extension-schema/hotpath-cpu",
"rustfs-filemeta/hotpath-cpu",
"rustfs-heal/hotpath-cpu",
"rustfs-iam/hotpath-cpu",
"rustfs-io-core/hotpath-cpu",
"rustfs-io-metrics/hotpath-cpu",
"rustfs-keystone/hotpath-cpu",
"rustfs-kms/hotpath-cpu",
"rustfs-lock/hotpath-cpu",
"rustfs-log-analyzer/hotpath-cpu",
"rustfs-madmin/hotpath-cpu",
"rustfs-notify/hotpath-cpu",
"rustfs-object-capacity/hotpath-cpu",
"rustfs-object-data-cache/hotpath-cpu",
"rustfs-obs/hotpath-cpu",
"rustfs-policy/hotpath-cpu",
"rustfs-protocols/hotpath-cpu",
"rustfs-protos/hotpath-cpu",
"rustfs-rio/hotpath-cpu",
"rustfs-s3-ops/hotpath-cpu",
"rustfs-s3-types/hotpath-cpu",
"rustfs-s3select-api/hotpath-cpu",
"rustfs-s3select-query/hotpath-cpu",
"rustfs-scanner/hotpath-cpu",
"rustfs-security-governance/hotpath-cpu",
"rustfs-signer/hotpath-cpu",
"rustfs-storage-api/hotpath-cpu",
"rustfs-targets/hotpath-cpu",
"rustfs-tls-runtime/hotpath-cpu",
"rustfs-trusted-proxies/hotpath-cpu",
"rustfs-utils/hotpath-cpu",
"rustfs-zip/hotpath-cpu",
"rustfs-test-utils/hotpath-cpu",
] ]
[lints] [lints]