From 093fd87d3b71448a4df618b00cf39cdf886a9825 Mon Sep 17 00:00:00 2001 From: mlevkov Date: Fri, 3 Jul 2026 09:00:45 -0700 Subject: [PATCH 01/20] fix(deps): refresh lockfile to patch 10 RUSTSEC advisories - bytes 1.11.0 -> 1.12.0 (RUSTSEC-2026-0007, BytesMut::reserve overflow) - time 0.3.44 -> 0.3.53 (RUSTSEC-2026-0009, DoS via stack exhaustion) - quinn-proto 0.11.13 -> 0.11.15 (RUSTSEC-2026-0037, QUIC DoS) - rustls-webpki 0.103.8 -> 0.103.13 (RUSTSEC-2026-0049, CRL matching) - aws-lc-rs 1.15.1 -> 1.17.1, aws-lc-sys 0.34 -> 0.42 (RUSTSEC-2026-0044/0045/0046/0047/0048) - rkyv 0.7.45 -> 0.7.46 (RUSTSEC-2026-0001, UB in Arc/Rc from_value) Remaining astral-tokio-tar advisories are dev-dependency-only (testcontainers 0.26) and are resolved by the testcontainers 0.27 bump in the next commit. Fixes #13, fixes #14, fixes #15, fixes #16, fixes #17, fixes #18, fixes #19, fixes #20, fixes #21, fixes #22 --- Cargo.lock | 1394 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 798 insertions(+), 596 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d75b0b..f8a9461 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,7 +20,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -43,7 +43,7 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "once_cell", "version_check", ] @@ -88,9 +88,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -103,15 +103,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -138,15 +138,54 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] [[package]] name = "astral-tokio-tar" @@ -198,7 +237,7 @@ dependencies = [ "async-trait", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", "tokio", ] @@ -245,7 +284,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] @@ -262,7 +301,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] @@ -282,15 +321,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.15.1" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b5ce75405893cd713f9ab8e297d8e438f624dde7d706108285f7e17a25a180f" +checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" dependencies = [ "aws-lc-sys", "zeroize", @@ -298,21 +337,22 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.34.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "179c3777a8b5e70e90ea426114ffc565b2c1a9f82f6c4a0c5a34aa6ef5e781b6" +checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] name = "axum" -version = "0.8.7" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", "axum-macros", @@ -344,9 +384,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", @@ -363,38 +403,41 @@ dependencies = [ [[package]] name = "axum-macros" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] name = "base64" -version = "0.21.7" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "base64" -version = "0.22.1" +name = "bit-vec" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] [[package]] name = "bitflags" -version = "2.10.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "bitvec" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" dependencies = [ "funty", "radium", @@ -418,7 +461,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87a52479c9237eb04047ddb94788c41ca0d26eaff8b697ecfbb4c32f7fdc3b1b" dependencies = [ "async-stream", - "base64 0.22.1", + "base64", "bitflags", "bollard-buildkit-proto", "bollard-stubs", @@ -438,7 +481,7 @@ dependencies = [ "log", "num", "pin-project-lite", - "rand 0.9.2", + "rand 0.9.4", "rustls", "rustls-native-certs", "rustls-pemfile", @@ -448,7 +491,7 @@ dependencies = [ "serde_json", "serde_repr", "serde_urlencoded", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tokio-util", @@ -477,7 +520,7 @@ version = "1.49.1-rc.28.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5731fe885755e92beff1950774068e0cae67ea6ec7587381536fca84f1779623" dependencies = [ - "base64 0.22.1", + "base64", "bollard-buildkit-proto", "bytes", "chrono", @@ -490,9 +533,9 @@ dependencies = [ [[package]] name = "bon" -version = "3.8.1" +version = "3.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebeb9aaf9329dff6ceb65c689ca3db33dbf15f324909c60e4e5eef5701ce31b1" +checksum = "a602c73c7b0148ec6d12af6fd5cc7a46e2eacc8878271a999abac56eed12f561" dependencies = [ "bon-macros", "rustversion", @@ -500,9 +543,9 @@ dependencies = [ [[package]] name = "bon-macros" -version = "3.8.1" +version = "3.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e9d642a7e3a318e37c2c9427b5a6a48aa1ad55dcd986f3034ab2239045a645" +checksum = "6dee98b0db6a962de883bf5d20362dee4d7ca0d12fe39a7c6c73c844e1cd7c1f" dependencies = [ "darling", "ident_case", @@ -510,46 +553,56 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] name = "borsh" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145" dependencies = [ "borsh-derive", + "bytes", "cfg_aliases", ] [[package]] name = "borsh-derive" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +checksum = "3ae8fb4fb5740e4b2c4884ff95f5f32f5e8479db1e8fd8eb49ddbe09eb09bb7c" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", ] [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "byte-unit" -version = "5.2.0" +version = "5.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d" +checksum = "4a813de7f2bbedb7dce265b64f1cf5908ebe4d56281ece8d847e98113788b9b0" dependencies = [ "rust_decimal", - "schemars 1.1.0", + "schemars 1.2.1", "serde", "utf8-width", ] @@ -578,21 +631,21 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "bytes" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "cc" -version = "1.2.49" +version = "1.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" dependencies = [ "find-msvc-tools", "jobserver", @@ -618,11 +671,22 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -644,9 +708,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.53" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -654,9 +718,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -667,36 +731,36 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmake" -version = "0.1.54" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" @@ -710,9 +774,9 @@ dependencies = [ [[package]] name = "comfy-table" -version = "7.2.1" +version = "7.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b03b7db8e0b4b2fdad6c551e634134e99ec000e5c8c3b6856c65e8bbaded7a3b" +checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47" dependencies = [ "crossterm", "unicode-segmentation", @@ -739,9 +803,9 @@ dependencies = [ [[package]] name = "compio-buf" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aa3ebe7f9830a33aa801a223411c8dc011c3271cd5beed56284c86d227bc32e" +checksum = "5ebb4036bf394915196c09362e4fd5581ee8bf0f3302ab598bff9d646aea2061" dependencies = [ "arrayvec", "bytes", @@ -819,7 +883,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] @@ -859,7 +923,7 @@ dependencies = [ "quinn-proto", "rustc-hash", "rustls", - "thiserror 2.0.17", + "thiserror 2.0.18", "windows-sys 0.61.2", ] @@ -977,6 +1041,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam" version = "0.8.4" @@ -1078,9 +1151,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ "darling_core", "darling_macro", @@ -1088,34 +1161,33 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] name = "darling_macro" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] name = "dashmap" -version = "6.1.0" +version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" dependencies = [ "cfg-if", "crossbeam-utils", @@ -1127,19 +1199,19 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "dbus" -version = "0.9.9" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190b6255e8ab55a7b568df5a883e9497edc3e4821c06396612048b430e5ad1e9" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" dependencies = [ "libc", "libdbus-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1153,36 +1225,49 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "powerfmt", "serde_core", ] [[package]] name = "derive_more" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ "convert_case", "proc-macro2", "quote", "rustc_version", - "syn 2.0.111", + "syn 2.0.118", "unicode-xid", ] @@ -1219,22 +1304,22 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] name = "docker_credential" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d89dfcba45b4afad7450a99b39e751590463e45c04728cf555d36bb66940de8" +checksum = "29547a1dc60885a552306986316bc9701ba120c1a8db6769fa68691529ad373d" dependencies = [ - "base64 0.21.7", + "base64", "serde", "serde_json", ] @@ -1268,9 +1353,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "encoding_rs" @@ -1342,30 +1427,30 @@ checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" [[package]] name = "fastbloom" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18c1ddb9231d8554c2d6bdf4cfaabf0c59251658c68b6c95cd52dd0c513a912a" +checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4" dependencies = [ "getrandom 0.3.4", "libm", - "rand 0.9.2", + "rand 0.9.4", "siphasher", ] [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "ferroid" -version = "0.8.7" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0e9414a6ae93ef993ce40a1e02944f13d4508e2bf6f1ced1580ce6910f08253" +checksum = "bb330bbd4cb7a5b9f559427f06f98a4f853a137c8298f3bd3f8ca57663e21986" dependencies = [ "portable-atomic", - "rand 0.9.2", + "rand 0.9.4", "web-time", ] @@ -1385,21 +1470,19 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.26" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ "cfg-if", "libc", - "libredox", - "windows-sys 0.60.2", ] [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flume" @@ -1469,9 +1552,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -1484,9 +1567,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -1494,15 +1577,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1511,19 +1594,19 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] @@ -1539,27 +1622,27 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -1569,7 +1652,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1585,9 +1667,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -1605,11 +1687,23 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", +] + [[package]] name = "ghash" version = "0.5.1" @@ -1622,9 +1716,9 @@ dependencies = [ [[package]] name = "governor" -version = "0.10.2" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e23d5986fd4364c2fb7498523540618b4b8d92eec6c36a02e565f66748e2f79" +checksum = "9efcab3c1958580ff1f25a2a41be1668f7603d849bb63af523b208a3cc1223b8" dependencies = [ "cfg-if", "dashmap", @@ -1637,7 +1731,7 @@ dependencies = [ "parking_lot", "portable-atomic", "quanta", - "rand 0.9.2", + "rand 0.9.4", "smallvec", "spinning_top", "web-time", @@ -1645,9 +1739,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.12" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ "atomic-waker", "bytes", @@ -1655,7 +1749,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.12.1", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -1697,6 +1791,12 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "heck" version = "0.5.0" @@ -1726,9 +1826,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -1777,15 +1877,15 @@ checksum = "f58b778a5761513caf593693f8951c97a5b610841e754788400f32102eefdff1" [[package]] name = "humantime" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" +checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" [[package]] name = "hyper" -version = "1.8.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -1798,7 +1898,6 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -1821,19 +1920,18 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.4", + "webpki-roots 1.0.8", ] [[package]] @@ -1867,14 +1965,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -1908,9 +2005,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1932,12 +2029,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1945,9 +2043,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1958,9 +2056,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1972,15 +2070,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -1992,15 +2090,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -2030,9 +2128,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -2068,7 +2166,7 @@ dependencies = [ "tracing", "trait-variant", "tungstenite", - "webpki-roots 1.0.4", + "webpki-roots 1.0.8", ] [[package]] @@ -2090,7 +2188,7 @@ dependencies = [ "serde", "serde_json", "tokio", - "toml 0.9.8", + "toml 0.9.12+spec-1.1.0", "tracing", ] @@ -2102,7 +2200,7 @@ checksum = "afecc088b82ebfa9b5d2ec18864bfec50f570f104a91c3d4414bbe1bd22c1a9f" dependencies = [ "aes-gcm", "ahash 0.8.12", - "base64 0.22.1", + "base64", "bon", "byte-unit", "bytemuck", @@ -2127,9 +2225,9 @@ dependencies = [ "serde_json", "serde_with", "strum", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", - "toml 0.9.8", + "toml 0.9.12+spec-1.1.0", "tracing", "tungstenite", "twox-hash", @@ -2148,14 +2246,14 @@ dependencies = [ "iggy", "metrics", "metrics-exporter-prometheus", - "rand 0.9.2", + "rand 0.9.4", "reqwest", "rust_decimal", "serde", "serde_json", "subtle", "testcontainers", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-util", "tower", @@ -2178,12 +2276,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -2205,9 +2303,9 @@ dependencies = [ [[package]] name = "io-uring" -version = "0.7.11" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd7bddefd0a8833b88a4b68f90dae22c7450d11b354198baee3874fd811b344" +checksum = "9080b15e63775b9a2ac7dca720f7050a8b955e092ea0f6020a4a80f69998cdc0" dependencies = [ "bitflags", "cfg-if", @@ -2216,9 +2314,9 @@ dependencies = [ [[package]] name = "io_uring_buf_ring" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe9ac631d954bb17eee5c932bd71bce3d5726c949c27729dd5c946b0de65471" +checksum = "1838759bb8c2f24cf05a35429d83145c4aa6af43f8ad38477295e12a7320a80e" dependencies = [ "bytes", "io-uring", @@ -2227,19 +2325,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.9" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" -dependencies = [ - "memchr", - "serde", -] +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "is_terminal_polyfill" @@ -2258,9 +2346,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jni" @@ -2271,7 +2359,7 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-sys 0.3.1", "log", "thiserror 1.0.69", "walkdir", @@ -2280,9 +2368,31 @@ dependencies = [ [[package]] name = "jni-sys" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.118", +] [[package]] name = "jobserver" @@ -2296,11 +2406,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -2324,15 +2435,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libdbus-sys" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbe856efeb50e4681f010e9aaa2bf0a644e10139e54cde10fc83a307c23bd9f" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" dependencies = [ "cc", "pkg-config", @@ -2340,32 +2451,30 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ - "bitflags", "libc", - "redox_syscall", ] [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "litrs" @@ -2384,9 +2493,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lru-slab" @@ -2411,18 +2520,18 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "metrics" -version = "0.24.3" +version = "0.24.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8" +checksum = "89550ee9f79e88fef3119de263694973a8adb26c21d75322164fb8c493039fe2" dependencies = [ - "ahash 0.8.12", "portable-atomic", + "rapidhash", ] [[package]] @@ -2431,11 +2540,11 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd7399781913e5393588a8d8c6a2867bf85fb38eaf2502fdce465aad2dc6f034" dependencies = [ - "base64 0.22.1", + "base64", "http-body-util", "hyper", "hyper-util", - "indexmap 2.12.1", + "indexmap 2.14.0", "ipnet", "metrics", "metrics-util", @@ -2456,7 +2565,7 @@ dependencies = [ "hashbrown 0.15.5", "metrics", "quanta", - "rand 0.9.2", + "rand 0.9.4", "rand_xoshiro", "sketches-ddsketch", ] @@ -2467,11 +2576,17 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "mio" -version = "1.1.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", @@ -2484,14 +2599,14 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "native-tls" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ "libc", "log", @@ -2499,11 +2614,21 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework 2.11.1", + "security-framework", "security-framework-sys", "tempfile", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nonzero_ext" version = "0.3.0" @@ -2535,9 +2660,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c863e9ab5e7bf9c99ba75e1050f1e4d624ae87ed3532d6238ffbdc7b585dbbe6" dependencies = [ "num-integer", "num-traits", @@ -2554,9 +2679,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" @@ -2608,11 +2733,20 @@ dependencies = [ "libc", ] +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -2628,15 +2762,14 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.75" +version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ "bitflags", "cfg-if", "foreign-types", "libc", - "once_cell", "openssl-macros", "openssl-sys", ] @@ -2649,29 +2782,29 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] name = "openssl-probe" -version = "0.1.6" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-src" -version = "300.5.4+3.5.4" +version = "300.6.1+3.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" +checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.111" +version = "0.9.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" dependencies = [ "cc", "libc", @@ -2747,7 +2880,7 @@ dependencies = [ "regex", "regex-syntax", "structmeta", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] @@ -2786,7 +2919,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] @@ -2795,7 +2928,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64 0.22.1", + "base64", "serde_core", ] @@ -2807,29 +2940,29 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -2839,9 +2972,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "polling" @@ -2864,22 +2997,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -2906,23 +3039,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.23.9", + "toml_edit 0.25.12+spec-1.1.0", ] [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -2935,16 +3068,16 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", "version_check", "yansi", ] [[package]] name = "prost" -version = "0.14.1" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" dependencies = [ "bytes", "prost-derive", @@ -2952,22 +3085,22 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.14.1" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", "itertools", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] name = "prost-types" -version = "0.14.1" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" dependencies = [ "prost", ] @@ -3009,9 +3142,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", @@ -3021,7 +3154,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -3029,22 +3162,22 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" dependencies = [ "bytes", "fastbloom", "getrandom 0.3.4", "lru-slab", - "rand 0.9.2", + "rand 0.9.4", "ring", "rustc-hash", "rustls", "rustls-pki-types", "rustls-platform-verifier", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -3066,9 +3199,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.42" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -3079,6 +3212,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -3087,9 +3226,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -3098,12 +3237,23 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", ] [[package]] @@ -3123,7 +3273,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -3132,25 +3282,40 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rand_xoshiro" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" dependencies = [ - "rand_core 0.9.3", + "rand_core 0.9.5", +] + +[[package]] +name = "rapidhash" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a224897b65b7ce38bf7b0adb569e7d77e11460d0cc3577908b263d8b5a89aa" +dependencies = [ + "rustversion", ] [[package]] @@ -3164,14 +3329,15 @@ dependencies = [ [[package]] name = "rcgen" -version = "0.14.5" +version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fae430c6b28f1ad601274e78b7dffa0546de0b73b4cd32f46723c0c2a16f7a5" +checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" dependencies = [ "pem", "ring", "rustls-pki-types", "time", + "x509-parser", "yasna", ] @@ -3190,9 +3356,9 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -3212,14 +3378,14 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -3229,9 +3395,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -3240,9 +3406,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "rend" @@ -3255,11 +3421,11 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.24" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "encoding_rs", "futures-core", @@ -3294,7 +3460,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.4", + "webpki-roots 1.0.8", ] [[package]] @@ -3321,13 +3487,13 @@ dependencies = [ "anyhow", "async-trait", "futures", - "getrandom 0.2.16", + "getrandom 0.2.17", "http", "hyper", "reqwest", "reqwest-middleware", "retry-policies", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "wasmtimer", @@ -3335,11 +3501,11 @@ dependencies = [ [[package]] name = "retry-policies" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a4bd6027df676bcb752d3724db0ea3c0c5fc1dd0376fec51ac7dcaf9cc69be" +checksum = "dc05fbf560421a0357a750cbe78c7ca19d4923918490daabba313d5dbc871e47" dependencies = [ - "rand 0.9.2", + "rand 0.10.2", ] [[package]] @@ -3350,7 +3516,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -3358,9 +3524,9 @@ dependencies = [ [[package]] name = "rkyv" -version = "0.7.45" +version = "0.7.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" dependencies = [ "bitvec", "bytecheck", @@ -3376,9 +3542,9 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.7.45" +version = "0.7.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" dependencies = [ "proc-macro2", "quote", @@ -3387,25 +3553,26 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.39.0" +version = "1.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" +checksum = "be2a24f50780bc85f09cc6ac299bdf1424302742d77221106859c9d8b102126a" dependencies = [ "arrayvec", "borsh", "bytes", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "rkyv", "serde", "serde_json", + "wasm-bindgen", ] [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -3416,11 +3583,20 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", @@ -3431,9 +3607,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "aws-lc-rs", "log", @@ -3447,14 +3623,14 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.5.1", + "security-framework", ] [[package]] @@ -3468,9 +3644,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" dependencies = [ "web-time", "zeroize", @@ -3491,7 +3667,7 @@ dependencies = [ "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki", - "security-framework 3.5.1", + "security-framework", "security-framework-sys", "webpki-root-certs", "windows-sys 0.61.2", @@ -3505,9 +3681,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", @@ -3523,9 +3699,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -3538,9 +3714,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -3559,9 +3735,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", @@ -3589,22 +3765,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework" -version = "3.5.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", "core-foundation 0.10.1", @@ -3615,9 +3778,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -3625,9 +3788,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -3656,20 +3819,20 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -3691,7 +3854,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] @@ -3705,9 +3868,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -3726,17 +3889,18 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.1" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ - "base64 0.22.1", + "base64", + "bs58", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.12.1", + "indexmap 2.14.0", "schemars 0.9.0", - "schemars 1.1.0", + "schemars 1.2.1", "serde_core", "serde_json", "serde_with_macros", @@ -3745,14 +3909,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.1" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] @@ -3762,7 +3926,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -3777,16 +3941,17 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -3798,36 +3963,36 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "sketches-ddsketch" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a" +checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3869,7 +4034,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] @@ -3880,7 +4045,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] @@ -3901,7 +4066,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] @@ -3923,9 +4088,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.111" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -3949,14 +4114,14 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] name = "system-configuration" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ "bitflags", "core-foundation 0.9.4", @@ -3981,12 +4146,12 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", @@ -3994,19 +4159,19 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "testcontainers" -version = "0.26.0" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a347cac4368ba4f1871743adb27dc14829024d26b1763572404726b0b9943eb8" +checksum = "a81ec0158db5fbb9831e09d1813fe5ea9023a2b5e6e8e0a5fe67e2a820733629" dependencies = [ "astral-tokio-tar", "async-trait", @@ -4025,7 +4190,7 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tokio-util", @@ -4043,11 +4208,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -4058,18 +4223,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] @@ -4083,30 +4248,29 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" dependencies = [ "num-conv", "time-core", @@ -4114,9 +4278,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -4124,9 +4288,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -4139,9 +4303,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -4156,13 +4320,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] @@ -4187,9 +4351,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -4214,9 +4378,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -4240,17 +4404,17 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.8" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.14.0", "serde_core", - "serde_spanned 1.0.3", - "toml_datetime 0.7.3", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -4264,9 +4428,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] @@ -4277,33 +4450,33 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.14.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_write", - "winnow", + "winnow 0.7.15", ] [[package]] name = "toml_edit" -version = "0.23.9" +version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ - "indexmap 2.12.1", - "toml_datetime 0.7.3", + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow", + "winnow 1.0.3", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow", + "winnow 1.0.3", ] [[package]] @@ -4314,19 +4487,19 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.0.4" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tonic" -version = "0.14.2" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" dependencies = [ "async-trait", "axum", - "base64 0.22.1", + "base64", "bytes", "h2", "http", @@ -4349,9 +4522,9 @@ dependencies = [ [[package]] name = "tonic-prost" -version = "0.14.2" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" dependencies = [ "bytes", "prost", @@ -4360,13 +4533,13 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap 2.12.1", + "indexmap 2.14.0", "pin-project-lite", "slab", "sync_wrapper", @@ -4379,21 +4552,21 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.7" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", "tracing", + "url", "uuid", ] @@ -4411,9 +4584,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -4429,14 +4602,14 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -4465,9 +4638,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -4492,7 +4665,7 @@ checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] @@ -4512,11 +4685,11 @@ dependencies = [ "http", "httparse", "log", - "rand 0.9.2", + "rand 0.9.4", "rustls", "rustls-pki-types", "sha1", - "thiserror 2.0.17", + "thiserror 2.0.18", "utf-8", ] @@ -4526,14 +4699,14 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" dependencies = [ - "rand 0.9.2", + "rand 0.9.4", ] [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "uncased" @@ -4546,15 +4719,15 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" @@ -4586,27 +4759,26 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "3.1.4" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" dependencies = [ - "base64 0.22.1", + "base64", "log", "percent-encoding", "rustls", "rustls-pki-types", "ureq-proto", - "utf-8", - "webpki-roots 1.0.4", + "utf8-zero", ] [[package]] name = "ureq-proto" -version = "0.5.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" dependencies = [ - "base64 0.22.1", + "base64", "http", "httparse", "log", @@ -4614,14 +4786,15 @@ dependencies = [ [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -4636,6 +4809,12 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -4650,11 +4829,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.19.0" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "js-sys", "serde_core", "wasm-bindgen", @@ -4705,44 +4884,42 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", "rustversion", + "serde", "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4750,22 +4927,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] @@ -4786,9 +4963,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -4806,9 +4983,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.4" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee3e3b5f5e80bc89f30ce8d0343bf4e5f12341c51f3e26cbeecbc7c85443e85b" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" dependencies = [ "rustls-pki-types", ] @@ -4819,14 +4996,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.4", + "webpki-roots 1.0.8", ] [[package]] name = "webpki-roots" -version = "1.0.4" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" dependencies = [ "rustls-pki-types", ] @@ -4889,7 +5066,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] @@ -4900,7 +5077,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] @@ -4965,15 +5142,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" @@ -5237,24 +5405,33 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "wyz" @@ -5265,6 +5442,24 @@ dependencies = [ "tap", ] +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + [[package]] name = "xattr" version = "1.6.1" @@ -5283,18 +5478,19 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yasna" -version = "0.5.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" dependencies = [ + "bit-vec", "time", ] [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -5303,82 +5499,82 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -5387,9 +5583,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -5398,11 +5594,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.118", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" From 03b2bcb6fb9022ec3e34026bf77f6dd8776f83e1 Mon Sep 17 00:00:00 2001 From: mlevkov Date: Fri, 3 Jul 2026 09:03:43 -0700 Subject: [PATCH 02/20] chore(deps): bump direct dependencies to latest stable versions - tower-http 0.6 -> 0.7 - rand 0.9 -> 0.10 (rand::rng().random() -> rand::random() free function) - metrics-exporter-prometheus 0.16 -> 0.18 - testcontainers 0.26 -> 0.27 (dev; drops vulnerable astral-tokio-tar 0.5.6 and unmaintained rustls-pemfile via bollard 0.20) - reqwest 0.12 -> 0.13 (dev) - version floors raised: tokio 1.52, uuid 1.23, rust_decimal 1.42 cargo audit now reports zero vulnerabilities. --- Cargo.lock | 382 +++++++++++++++++++++++-------------- Cargo.toml | 16 +- src/iggy_client/helpers.rs | 3 +- 3 files changed, 250 insertions(+), 151 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f8a9461..a5cb5c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -189,15 +189,15 @@ dependencies = [ [[package]] name = "astral-tokio-tar" -version = "0.5.6" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec179a06c1769b1e42e1e2cbe74c7dcdb3d6383c838454d063eaac5bbb7ebbe5" +checksum = "08648fef353ab39a9d26f909ad53fc4f071be4c91853b78523f5cc3d9e5ebffd" dependencies = [ - "filetime", "futures-core", "libc", "portable-atomic", "rustc-hash", + "rustix 0.38.44", "tokio", "tokio-stream", "xattr", @@ -456,9 +456,9 @@ dependencies = [ [[package]] name = "bollard" -version = "0.19.4" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87a52479c9237eb04047ddb94788c41ca0d26eaff8b697ecfbb4c32f7fdc3b1b" +checksum = "ee04c4c84f1f811b017f2fbb7dd8815c976e7ca98593de9c1e2afad0f636bff4" dependencies = [ "async-stream", "base64", @@ -466,7 +466,6 @@ dependencies = [ "bollard-buildkit-proto", "bollard-stubs", "bytes", - "chrono", "futures-core", "futures-util", "hex", @@ -484,14 +483,13 @@ dependencies = [ "rand 0.9.4", "rustls", "rustls-native-certs", - "rustls-pemfile", "rustls-pki-types", "serde", "serde_derive", "serde_json", - "serde_repr", "serde_urlencoded", "thiserror 2.0.18", + "time", "tokio", "tokio-stream", "tokio-util", @@ -516,19 +514,18 @@ dependencies = [ [[package]] name = "bollard-stubs" -version = "1.49.1-rc.28.4.0" +version = "1.52.1-rc.29.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5731fe885755e92beff1950774068e0cae67ea6ec7587381536fca84f1779623" +checksum = "0f0a8ca8799131c1837d1282c3f81f31e76ceb0ce426e04a7fe1ccee3287c066" dependencies = [ "base64", "bollard-buildkit-proto", "bytes", - "chrono", "prost", "serde", "serde_json", "serde_repr", - "serde_with", + "time", ] [[package]] @@ -1116,7 +1113,7 @@ dependencies = [ "crossterm_winapi", "document-features", "parking_lot", - "rustix", + "rustix 1.1.4", "winapi", ] @@ -1419,6 +1416,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "evmap" +version = "11.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8874945f036109c72242964c1174cf99434e30cfa45bf45fedc983f50046f8" +dependencies = [ + "hashbag", + "left-right", + "smallvec", +] + [[package]] name = "exitcode" version = "1.1.2" @@ -1437,20 +1445,14 @@ dependencies = [ "siphasher", ] -[[package]] -name = "fastrand" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" - [[package]] name = "ferroid" -version = "0.8.9" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb330bbd4cb7a5b9f559427f06f98a4f853a137c8298f3bd3f8ca57663e21986" +checksum = "ee93edf3c501f0035bbeffeccfed0b79e14c311f12195ec0e661e114a0f60da4" dependencies = [ "portable-atomic", - "rand 0.9.4", + "rand 0.10.2", "web-time", ] @@ -1468,16 +1470,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "filetime" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" -dependencies = [ - "cfg-if", - "libc", -] - [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1502,12 +1494,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foldhash" version = "0.2.0" @@ -1655,6 +1641,21 @@ dependencies = [ "slab", ] +[[package]] +name = "generator" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1756,6 +1757,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbag" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7040a10f52cba493ddb09926e15d10a9d8a28043708a405931fe4c6f19fac064" + [[package]] name = "hashbrown" version = "0.12.3" @@ -1771,15 +1778,6 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - [[package]] name = "hashbrown" version = "0.16.1" @@ -1788,7 +1786,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash 0.2.0", + "foldhash", ] [[package]] @@ -1947,22 +1945,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" version = "0.1.20" @@ -2155,7 +2137,7 @@ dependencies = [ "iggy_common", "num_cpus", "quinn", - "reqwest", + "reqwest 0.12.28", "reqwest-middleware", "reqwest-retry", "rustls", @@ -2246,8 +2228,8 @@ dependencies = [ "iggy", "metrics", "metrics-exporter-prometheus", - "rand 0.9.4", - "reqwest", + "rand 0.10.2", + "reqwest 0.13.4", "rust_decimal", "serde", "serde_json", @@ -2257,7 +2239,7 @@ dependencies = [ "tokio", "tokio-util", "tower", - "tower-http", + "tower-http 0.7.0", "tracing", "tracing-subscriber", "uuid", @@ -2320,7 +2302,7 @@ checksum = "1838759bb8c2f24cf05a35429d83145c4aa6af43f8ad38477295e12a7320a80e" dependencies = [ "bytes", "io-uring", - "rustix", + "rustix 1.1.4", ] [[package]] @@ -2366,6 +2348,36 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.118", +] + [[package]] name = "jni-sys" version = "0.3.1" @@ -2433,6 +2445,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "left-right" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0c21e4c8ff95f487fb34e6f9182875f42c84cef966d29216bf115d9bba835a" +dependencies = [ + "crossbeam-utils", + "loom", + "slab", +] + [[package]] name = "libc" version = "0.2.186" @@ -2464,6 +2487,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -2497,6 +2526,19 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -2536,11 +2578,12 @@ dependencies = [ [[package]] name = "metrics-exporter-prometheus" -version = "0.16.2" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd7399781913e5393588a8d8c6a2867bf85fb38eaf2502fdce465aad2dc6f034" +checksum = "1db0d8f1fc9e62caebd0319e11eaec5822b0186c171568f0480b46a0137f9108" dependencies = [ "base64", + "evmap", "http-body-util", "hyper", "hyper-util", @@ -2549,24 +2592,25 @@ dependencies = [ "metrics", "metrics-util", "quanta", - "thiserror 1.0.69", + "thiserror 2.0.18", "tokio", "tracing", ] [[package]] name = "metrics-util" -version = "0.19.1" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8496cc523d1f94c1385dd8f0f0c2c480b2b8aeccb5b7e4485ad6365523ae376" +checksum = "96f8722f8562635f92f8ed992f26df0532266eb03d5202607c20c0d7e9745e13" dependencies = [ "crossbeam-epoch", "crossbeam-utils", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "metrics", "quanta", "rand 0.9.4", "rand_xoshiro", + "rapidhash", "sketches-ddsketch", ] @@ -2602,23 +2646,6 @@ dependencies = [ "getrandom 0.2.17", ] -[[package]] -name = "native-tls" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "nom" version = "7.1.3" @@ -2986,7 +3013,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -3166,6 +3193,7 @@ version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" dependencies = [ + "aws-lc-rs", "bytes", "fastbloom", "getrandom 0.3.4", @@ -3175,7 +3203,7 @@ dependencies = [ "rustc-hash", "rustls", "rustls-pki-types", - "rustls-platform-verifier", + "rustls-platform-verifier 0.6.2", "slab", "thiserror 2.0.18", "tinyvec", @@ -3424,6 +3452,44 @@ name = "reqwest" version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http 0.6.11", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.8", +] + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64", "bytes", @@ -3435,32 +3501,28 @@ dependencies = [ "http-body-util", "hyper", "hyper-rustls", - "hyper-tls", "hyper-util", "js-sys", "log", "mime", - "native-tls", "percent-encoding", "pin-project-lite", "quinn", "rustls", "rustls-pki-types", + "rustls-platform-verifier 0.7.0", "serde", "serde_json", - "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-native-tls", "tokio-rustls", "tower", - "tower-http", + "tower-http 0.6.11", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.8", ] [[package]] @@ -3472,7 +3534,7 @@ dependencies = [ "anyhow", "async-trait", "http", - "reqwest", + "reqwest 0.12.28", "serde", "thiserror 1.0.69", "tower-service", @@ -3490,7 +3552,7 @@ dependencies = [ "getrandom 0.2.17", "http", "hyper", - "reqwest", + "reqwest 0.12.28", "reqwest-middleware", "retry-policies", "thiserror 2.0.18", @@ -3592,6 +3654,19 @@ dependencies = [ "nom", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.4" @@ -3601,7 +3676,7 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -3633,15 +3708,6 @@ dependencies = [ "security-framework", ] -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" version = "1.15.0" @@ -3660,7 +3726,28 @@ checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", - "jni", + "jni 0.21.1", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni 0.22.4", "log", "once_cell", "rustls", @@ -3955,6 +4042,16 @@ dependencies = [ "libc", ] +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + [[package]] name = "simdutf8" version = "0.1.5" @@ -4144,34 +4241,21 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.3", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - [[package]] name = "terminal_size" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] [[package]] name = "testcontainers" -version = "0.26.3" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a81ec0158db5fbb9831e09d1813fe5ea9023a2b5e6e8e0a5fe67e2a820733629" +checksum = "bfd5785b5483672915ed5fe3cddf9f546802779fc1eceff0a6fb7321fac81c1e" dependencies = [ "astral-tokio-tar", "async-trait", @@ -4182,6 +4266,7 @@ dependencies = [ "etcetera", "ferroid", "futures", + "http", "itertools", "log", "memchr", @@ -4329,16 +4414,6 @@ dependencies = [ "syn 2.0.118", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.4" @@ -4565,8 +4640,24 @@ dependencies = [ "tower", "tower-layer", "tower-service", - "tracing", "url", +] + +[[package]] +name = "tower-http" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b11f75e912b0c2be01b63d8cf8057b8c3f97cf34abb3d431a3a4c8675498e233" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "percent-encoding", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", "uuid", ] @@ -5142,6 +5233,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" @@ -5467,7 +5567,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix", + "rustix 1.1.4", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 42a303c..f27772f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,10 +17,10 @@ readme = "README.md" # Web framework axum = { version = "0.8", features = ["macros"] } tower = "0.5" -tower-http = { version = "0.6", features = ["cors", "trace", "request-id", "propagate-header"] } +tower-http = { version = "0.7", features = ["cors", "trace", "request-id", "propagate-header"] } # Async runtime -tokio = { version = "1.48", features = ["full"] } +tokio = { version = "1.52", features = ["full"] } tokio-util = { version = "0.7", features = ["rt"] } # Message streaming (edge SDK for the latest server features - io_uring, shared-nothing architecture) @@ -42,14 +42,14 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } # Utilities -uuid = { version = "1.19.0", features = ["v4", "serde"] } +uuid = { version = "1.23", features = ["v4", "serde"] } chrono = { version = "0.4", features = ["serde"] } # Decimal arithmetic for monetary values (avoids floating-point precision issues) -rust_decimal = { version = "1.39", features = ["serde", "serde-with-str"] } +rust_decimal = { version = "1.42", features = ["serde", "serde-with-str"] } # Random number generation (for jitter in backoff) -rand = "0.9" +rand = "0.10" # Rate limiting governor = "0.10" @@ -62,11 +62,11 @@ exitcode = "1.1" # Metrics for Prometheus metrics = "0.24" -metrics-exporter-prometheus = { version = "0.16", default-features = false, features = ["http-listener"] } +metrics-exporter-prometheus = { version = "0.18", default-features = false, features = ["http-listener"] } [dev-dependencies] -reqwest = { version = "0.12", features = ["json"] } -testcontainers = "0.26" +reqwest = { version = "0.13", features = ["json"] } +testcontainers = "0.27" # ============================================================================= # Lints Configuration diff --git a/src/iggy_client/helpers.rs b/src/iggy_client/helpers.rs index 45e6539..c0aecf1 100644 --- a/src/iggy_client/helpers.rs +++ b/src/iggy_client/helpers.rs @@ -32,8 +32,7 @@ pub fn to_identifier(name: &str, resource_type: &str) -> Result f64 { - use rand::Rng; - rand::rng().random::() + rand::random::() } #[cfg(test)] From 8f7192812e6f5091aeae4b0ba735c4633db3434e Mon Sep 17 00:00:00 2001 From: mlevkov Date: Fri, 3 Jul 2026 09:07:40 -0700 Subject: [PATCH 03/20] chore(deps): update iggy SDK to 0.10.0 and pin server image to 0.8.0 - iggy 0.8.0 -> 0.10.0 (latest stable; Client trait API unchanged, no source changes required) - pin apache/iggy image to 0.8.0 (the server release paired with the 0.10 SDK) in docker-compose.yaml and integration tests instead of floating :latest, for reproducible builds and CI runs - verified end-to-end: all 24 integration tests green against server 0.8.0 via testcontainers --- Cargo.lock | 1065 ++++++++++++++++-------------------- Cargo.toml | 4 +- docker-compose.yaml | 3 +- tests/integration_tests.rs | 4 +- 4 files changed, 475 insertions(+), 601 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a5cb5c5..9edbd90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,26 +49,21 @@ dependencies = [ ] [[package]] -name = "ahash" -version = "0.8.12" +name = "aho-corasick" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ - "cfg-if", - "getrandom 0.3.4", - "once_cell", - "serde", - "version_check", - "zerocopy", + "memchr", ] [[package]] -name = "aho-corasick" -version = "1.1.4" +name = "aligned-vec" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" dependencies = [ - "memchr", + "equator", ] [[package]] @@ -142,6 +137,12 @@ version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.7.8" @@ -254,6 +255,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + [[package]] name = "async-scoped" version = "0.9.0" @@ -304,15 +316,6 @@ dependencies = [ "syn 2.0.118", ] -[[package]] -name = "atomic" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" -dependencies = [ - "bytemuck", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -445,6 +448,20 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -544,7 +561,7 @@ version = "3.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dee98b0db6a962de883bf5d20362dee4d7ca0d12fe39a7c6c73c844e1cd7c1f" dependencies = [ - "darling", + "darling 0.23.0", "ident_case", "prettyplease", "proc-macro2", @@ -631,6 +648,20 @@ name = "bytemuck" version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] [[package]] name = "bytes" @@ -775,16 +806,15 @@ version = "7.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47" dependencies = [ - "crossterm", "unicode-segmentation", "unicode-width", ] [[package]] name = "compio" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a7cc183295c36483f1c9616f43c4ac1a9030ce6d9321d6cebb4c4bb21164c4" +checksum = "9b84ee96a86948d04388f3a0b8c36b9f0a6b40b3528ac0d65737e53632fb37fe" dependencies = [ "compio-buf", "compio-driver", @@ -793,6 +823,7 @@ dependencies = [ "compio-log", "compio-macros", "compio-net", + "compio-quic", "compio-runtime", "compio-tls", "compio-ws", @@ -800,9 +831,9 @@ dependencies = [ [[package]] name = "compio-buf" -version = "0.7.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ebb4036bf394915196c09362e4fd5581ee8bf0f3302ab598bff9d646aea2061" +checksum = "b51a2c35873865376ed4cdb6cfeb602b6cf569815f017ddd7bd8f86ad49b34c7" dependencies = [ "arrayvec", "bytes", @@ -811,9 +842,9 @@ dependencies = [ [[package]] name = "compio-driver" -version = "0.10.0" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff5c12800e82a01d12046ccc29b014e1cbbb2fbe38c52534e0d40d4fc58881d5" +checksum = "74d42d98dc890ee4db00c1e68a723391711aab6d67085880d716b72830f7c715" dependencies = [ "cfg-if", "cfg_aliases", @@ -827,17 +858,21 @@ dependencies = [ "libc", "once_cell", "paste", + "pin-project-lite", "polling", "slab", + "smallvec", "socket2", + "synchrony", + "thin-cell", "windows-sys 0.61.2", ] [[package]] name = "compio-fs" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c568022f90c2e2e8ea7ff4c4e8fde500753b5b9b6b6d870e25b5e656f9ea2892" +checksum = "65ee36e1acf2cec4835efe9a986c012b2462c5ef53580e4ee84ae6d5a3d8e3b3" dependencies = [ "cfg-if", "cfg_aliases", @@ -847,19 +882,21 @@ dependencies = [ "compio-runtime", "libc", "os_pipe", + "pin-project-lite", "widestring", "windows-sys 0.61.2", ] [[package]] name = "compio-io" -version = "0.8.4" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1e64c6d723589492a4f5041394301e9903466a606f6d9bcc11e406f9f07e9ec" +checksum = "637522f28a64fd5f7dcceaa4ddef13fa8d8020025e8c993f7a069e237835580e" dependencies = [ "compio-buf", "futures-util", "paste", + "synchrony", ] [[package]] @@ -885,9 +922,9 @@ dependencies = [ [[package]] name = "compio-net" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffab78b8a876111ca76450912ca6a5a164b0dd93973e342c5f438a6f478c735" +checksum = "becd7d40522c885113752a3640cba9f9d347f205b646bb3f8ff3967173a228f2" dependencies = [ "cfg-if", "compio-buf", @@ -904,9 +941,9 @@ dependencies = [ [[package]] name = "compio-quic" -version = "0.6.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e101b05fe8608ce6fb2882ac331e211f2b0318449ae27c576c7456b4f1ec4e" +checksum = "ad9efdad81b920108b9de57148e1b9d73dc408b6d06a59ee64836dde651cf026" dependencies = [ "cfg_aliases", "compio-buf", @@ -920,15 +957,16 @@ dependencies = [ "quinn-proto", "rustc-hash", "rustls", + "synchrony", "thiserror 2.0.18", "windows-sys 0.61.2", ] [[package]] name = "compio-runtime" -version = "0.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fd890a129a8086af857bbe18401689c130aa6ccfc7f3c029a7800f7256af3e" +checksum = "d6c1c71f011bdd9c8f30e97d877b606505ee6d241c7782cfaed172f66acbd9cd" dependencies = [ "async-task", "cfg-if", @@ -949,9 +987,9 @@ dependencies = [ [[package]] name = "compio-tls" -version = "0.8.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84cd9ca48815f384f1a30400848beebcd8c7ead2f57bfe28ebc5560babea88ec" +checksum = "3a7056da226af42cda4c83b00a021cce3e1ee5f4cffc8a0ff8801381e618cf1c" dependencies = [ "compio-buf", "compio-io", @@ -962,16 +1000,16 @@ dependencies = [ [[package]] name = "compio-ws" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7281a15e8f638697415f9838030e41a92c8a8954ddccfc46556a413c16dd9a" +checksum = "99d45f47c6e64babcaa6b8df1dffced56012e60e58401255e679f428ddbe9fb6" dependencies = [ "compio-buf", "compio-io", "compio-log", "compio-net", "compio-tls", - "tungstenite", + "tungstenite 0.28.0", ] [[package]] @@ -983,6 +1021,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + [[package]] name = "convert_case" version = "0.10.0" @@ -1103,29 +1147,6 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "crossterm" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" -dependencies = [ - "bitflags", - "crossterm_winapi", - "document-features", - "parking_lot", - "rustix 1.1.4", - "winapi", -] - -[[package]] -name = "crossterm_winapi" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" -dependencies = [ - "winapi", -] - [[package]] name = "crypto-common" version = "0.1.7" @@ -1146,14 +1167,37 @@ dependencies = [ "cipher", ] +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + [[package]] name = "darling" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.118", ] [[package]] @@ -1169,13 +1213,24 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn 2.0.118", +] + [[package]] name = "darling_macro" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core", + "darling_core 0.23.0", "quote", "syn 2.0.118", ] @@ -1200,28 +1255,6 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" -[[package]] -name = "dbus" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" -dependencies = [ - "libc", - "libdbus-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "dbus-secret-service" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" -dependencies = [ - "dbus", - "openssl", - "zeroize", -] - [[package]] name = "der-parser" version = "10.0.0" @@ -1278,27 +1311,6 @@ dependencies = [ "crypto-common", ] -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.61.2", -] - [[package]] name = "displaydoc" version = "0.2.6" @@ -1321,15 +1333,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "document-features" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" -dependencies = [ - "litrs", -] - [[package]] name = "dotenvy" version = "0.15.7" @@ -1363,6 +1366,47 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enumset" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "839c4174b41e75c8f7306110b2c51996a293b8d1d850edd529011841d9fede7d" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd536557b58c682b217b8fb199afdff47cd3eff260623f19e77074eb073d63a" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1371,9 +1415,24 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "err_trail" -version = "0.10.2" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b9e8330eccf84d08fb8efe2f923ddacc9f02c1359edfc33cc0af4100caf764" +checksum = "704b044506df3479abb857a3e1514fea858db128ea6fd95cc26088d9cf748f2a" +dependencies = [ + "err_trail_macros", + "tracing", +] + +[[package]] +name = "err_trail_macros" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d9ab0367afaae0ba1538512a0360b2b9fc737089a48612334200d815c7982f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] [[package]] name = "errno" @@ -1433,6 +1492,35 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" +[[package]] +name = "ext-trait" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d772df1c1a777963712fb68e014235e80863d6a91a85c4e06ba2d16243a310e5" +dependencies = [ + "ext-trait-proc_macros", +] + +[[package]] +name = "ext-trait-proc_macros" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab7934152eaf26aa5aa9f7371408ad5af4c31357073c9e84c3b9d7f11ad639a" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "extension-traits" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a296e5a895621edf9fa8329c83aa1cb69a964643e36cf54d8d7a69b789089537" +dependencies = [ + "ext-trait", +] + [[package]] name = "fastbloom" version = "0.14.1" @@ -1445,6 +1533,15 @@ dependencies = [ "siphasher", ] +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "ferroid" version = "2.0.0" @@ -1456,20 +1553,6 @@ dependencies = [ "web-time", ] -[[package]] -name = "figment" -version = "0.10.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" -dependencies = [ - "atomic", - "pear", - "serde", - "toml 0.8.23", - "uncased", - "version_check", -] - [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1478,13 +1561,13 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flume" -version = "0.11.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" dependencies = [ + "fastrand", "futures-core", "futures-sink", - "nanorand", "spin", ] @@ -1500,21 +1583,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1769,7 +1837,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash 0.7.8", + "ahash", ] [[package]] @@ -1929,7 +1997,6 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.8", ] [[package]] @@ -2120,9 +2187,9 @@ dependencies = [ [[package]] name = "iggy" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a333fffdc827bf009a1818e17d16f018383541ac9d60399cb436f91fffee2153" +checksum = "6a470a78ccd8a6602402817906d842a3f361f84516cb880282565365df4eba2b" dependencies = [ "async-broadcast", "async-dropper", @@ -2133,56 +2200,51 @@ dependencies = [ "flume", "futures", "futures-util", - "iggy_binary_protocol", "iggy_common", - "num_cpus", "quinn", - "reqwest 0.12.28", + "reqwest", "reqwest-middleware", "reqwest-retry", + "reqwest-tracing", "rustls", + "secrecy", "serde", "tokio", "tokio-rustls", "tokio-tungstenite", "tracing", "trait-variant", - "tungstenite", + "tungstenite 0.29.0", "webpki-roots 1.0.8", ] [[package]] name = "iggy_binary_protocol" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91cd5debffe213ce2c91c5dd4354fa9359e5292356a73521d66e6cce2a3fccd9" +checksum = "9e6da7f3a07797ef6a4248400bdf9d752e0eb7d83d14bf9cadcaae6bffdc9235" dependencies = [ - "anyhow", - "async-broadcast", - "async-trait", + "aligned-vec", + "bytemuck", "bytes", - "chrono", - "comfy-table", - "dirs", - "iggy_common", - "keyring", - "passterm", - "serde", - "serde_json", - "tokio", - "toml 0.9.12+spec-1.1.0", - "tracing", + "compio-buf", + "enumset", + "smallvec", + "thiserror 2.0.18", ] [[package]] name = "iggy_common" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afecc088b82ebfa9b5d2ec18864bfec50f570f104a91c3d4414bbe1bd22c1a9f" +checksum = "6193adb66b1f12b6b2f9725573a6a6f69ce714c49830e122ed0d5ce7e7ffbde8" dependencies = [ "aes-gcm", - "ahash 0.8.12", + "aligned-vec", + "async-broadcast", + "async-trait", "base64", + "blake3", "bon", "byte-unit", "bytemuck", @@ -2191,28 +2253,32 @@ dependencies = [ "clap", "comfy-table", "compio", - "compio-quic", - "compio-tls", - "compio-ws", "crossbeam", "derive_more", "err_trail", - "figment", "human-repr", "humantime", + "iggy_binary_protocol", + "lending-iterator", + "moka", + "nix", "once_cell", + "papaya", "rcgen", + "ring", "rustls", + "secrecy", "serde", "serde_json", "serde_with", "strum", "thiserror 2.0.18", "tokio", - "toml 0.9.12+spec-1.1.0", "tracing", - "tungstenite", + "tungstenite 0.29.0", "twox-hash", + "ulid", + "uuid", ] [[package]] @@ -2229,7 +2295,7 @@ dependencies = [ "metrics", "metrics-exporter-prometheus", "rand 0.10.2", - "reqwest 0.13.4", + "reqwest", "rust_decimal", "serde", "serde_json", @@ -2268,12 +2334,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "inlinable_string" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" - [[package]] name = "inout" version = "0.1.4" @@ -2427,18 +2487,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "keyring" -version = "3.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" -dependencies = [ - "dbus-secret-service", - "log", - "openssl", - "zeroize", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -2457,35 +2505,41 @@ dependencies = [ ] [[package]] -name = "libc" -version = "0.2.186" +name = "lending-iterator" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "bc07588c853b50689205fb5c00498aa681d89828e0ce8cbd965ebc7a5d8ae260" +dependencies = [ + "extension-traits", + "lending-iterator-proc_macros", + "macro_rules_attribute", + "never-say-never", + "nougat", + "polonius-the-crab", +] [[package]] -name = "libdbus-sys" -version = "0.2.7" +name = "lending-iterator-proc_macros" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +checksum = "b5445dd1c0deb1e97b8a16561d17fc686ca83e8411128fb036e9668a72d51b1d" dependencies = [ - "cc", - "pkg-config", + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] -name = "libm" -version = "0.2.16" +name = "libc" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] -name = "libredox" -version = "0.1.18" +name = "libm" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" -dependencies = [ - "libc", -] +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "linux-raw-sys" @@ -2505,12 +2559,6 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" -[[package]] -name = "litrs" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" - [[package]] name = "lock_api" version = "0.4.14" @@ -2534,6 +2582,7 @@ checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" dependencies = [ "cfg-if", "generator", + "pin-utils", "scoped-tls", "tracing", "tracing-subscriber", @@ -2545,6 +2594,22 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "macro_rules_attribute" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf0c9b980bf4f3a37fd7b1c066941dd1b1d0152ce6ee6e8fe8c49b9f6810d862" +dependencies = [ + "macro_rules_attribute-proc_macro", + "paste", +] + +[[package]] +name = "macro_rules_attribute-proc_macro" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58093314a45e00c77d5c508f76e77c3396afbbc0d01506e7fae47b018bac2b1d" + [[package]] name = "matchers" version = "0.2.0" @@ -2638,12 +2703,41 @@ dependencies = [ ] [[package]] -name = "nanorand" -version = "0.7.0" +name = "moka" +version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" dependencies = [ - "getrandom 0.2.17", + "async-lock", + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "event-listener", + "futures-util", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "never-say-never" +version = "6.6.666" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf5a574dadd7941adeaa71823ecba5e28331b8313fb2e1c6a5c7e5981ea53ad6" + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", ] [[package]] @@ -2662,6 +2756,27 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" +[[package]] +name = "nougat" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b57b9ced431322f054fc673f1d3c7fa52d80efd9df74ad2fc759f044742510" +dependencies = [ + "macro_rules_attribute", + "nougat-proc_macros", +] + +[[package]] +name = "nougat-proc_macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c84f77a45e99a2f9b492695d99e1c23844619caa5f3e57647cffacad773ca257" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -2787,31 +2902,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" -[[package]] -name = "openssl" -version = "0.10.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - [[package]] name = "openssl-probe" version = "0.2.1" @@ -2819,41 +2909,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] -name = "openssl-src" -version = "300.6.1+3.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846" -dependencies = [ - "cc", -] - -[[package]] -name = "openssl-sys" -version = "0.9.117" +name = "os_pipe" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ - "cc", "libc", - "openssl-src", - "pkg-config", - "vcpkg", + "windows-sys 0.61.2", ] [[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "os_pipe" -version = "1.2.3" +name = "papaya" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +checksum = "997ee03cd38c01469a7046643714f0ad28880bcb9e6679ff0666e24817ca19b7" dependencies = [ - "libc", - "windows-sys 0.61.2", + "equivalent", + "seize", ] [[package]] @@ -2910,45 +2982,12 @@ dependencies = [ "syn 2.0.118", ] -[[package]] -name = "passterm" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6421520445baa36401ded73e4820ad3a3e04316a0eb1bdb13b94987739de5fa2" -dependencies = [ - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" -[[package]] -name = "pear" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" -dependencies = [ - "inlinable_string", - "pear_codegen", - "yansi", -] - -[[package]] -name = "pear_codegen" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" -dependencies = [ - "proc-macro2", - "proc-macro2-diagnostics", - "quote", - "syn 2.0.118", -] - [[package]] name = "pem" version = "3.0.6" @@ -3017,6 +3056,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "polonius-the-crab" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a69ee997a6282f8462abf1e0d8c38c965e968799e912b3bed8c9e8a28c2f9f" + [[package]] name = "polyval" version = "0.6.2" @@ -3075,7 +3120,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.12+spec-1.1.0", + "toml_edit", ] [[package]] @@ -3087,19 +3132,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "proc-macro2-diagnostics" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", - "version_check", - "yansi", -] - [[package]] name = "prost" version = "0.14.4" @@ -3378,17 +3410,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 2.0.18", -] - [[package]] name = "ref-cast" version = "1.0.25" @@ -3447,44 +3468,6 @@ dependencies = [ "bytecheck", ] -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64", - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http 0.6.11", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots 1.0.8", -] - [[package]] name = "reqwest" version = "0.13.4" @@ -3513,6 +3496,7 @@ dependencies = [ "rustls-platform-verifier 0.7.0", "serde", "serde_json", + "serde_urlencoded", "sync_wrapper", "tokio", "tokio-rustls", @@ -3527,24 +3511,24 @@ dependencies = [ [[package]] name = "reqwest-middleware" -version = "0.4.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" +checksum = "07bc3f1384cffa4f274dad2d4ddd73aed32fed8f786d96c6be8aa4e5fd3c3b58" dependencies = [ "anyhow", "async-trait", "http", - "reqwest 0.12.28", + "reqwest", "serde", - "thiserror 1.0.69", + "thiserror 2.0.18", "tower-service", ] [[package]] name = "reqwest-retry" -version = "0.8.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "105747e3a037fe5bf17458d794de91149e575b6183fc72c85623a44abb9683f5" +checksum = "fe2412db2af7d2268e7a5406be0431f37d9eb67ff390f35b395716f5f06c2eaa" dependencies = [ "anyhow", "async-trait", @@ -3552,7 +3536,7 @@ dependencies = [ "getrandom 0.2.17", "http", "hyper", - "reqwest 0.12.28", + "reqwest", "reqwest-middleware", "retry-policies", "thiserror 2.0.18", @@ -3561,6 +3545,22 @@ dependencies = [ "wasmtimer", ] +[[package]] +name = "reqwest-tracing" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e5af0cd6fc3d3c8f703d597af70b6e4e62432c63157b49419fa1ffaf481702" +dependencies = [ + "anyhow", + "async-trait", + "getrandom 0.2.17", + "http", + "matchit", + "reqwest", + "reqwest-middleware", + "tracing", +] + [[package]] name = "retry-policies" version = "0.5.2" @@ -3850,6 +3850,16 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "serde", + "zeroize", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -3873,6 +3883,16 @@ dependencies = [ "libc", ] +[[package]] +name = "seize" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "semver" version = "1.0.28" @@ -3944,24 +3964,6 @@ dependencies = [ "syn 2.0.118", ] -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -4000,7 +4002,7 @@ version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" dependencies = [ - "darling", + "darling 0.23.0", "proc-macro2", "quote", "syn 2.0.118", @@ -4147,18 +4149,18 @@ dependencies = [ [[package]] name = "strum" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" dependencies = [ "heck", "proc-macro2", @@ -4203,6 +4205,16 @@ dependencies = [ "futures-core", ] +[[package]] +name = "synchrony" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416090a4d8f6358526df5f9f65dfe28750b8b7bfd1fd8a5620f483fc4a75722c" +dependencies = [ + "futures-util", + "loom", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -4235,6 +4247,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "tap" version = "1.0.1" @@ -4282,6 +4300,12 @@ dependencies = [ "url", ] +[[package]] +name = "thin-cell" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4164c6c316ba9733b0ab021e7f9852c788a4b991b49c25820f1be48e1d41345b" + [[package]] name = "thiserror" version = "1.0.69" @@ -4437,9 +4461,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" dependencies = [ "futures-util", "log", @@ -4447,7 +4471,7 @@ dependencies = [ "rustls-pki-types", "tokio", "tokio-rustls", - "tungstenite", + "tungstenite 0.29.0", "webpki-roots 0.26.11", ] @@ -4465,51 +4489,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit 0.22.27", -] - -[[package]] -name = "toml" -version = "0.9.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" -dependencies = [ - "indexmap 2.14.0", - "serde_core", - "serde_spanned 1.1.1", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 0.7.15", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -4519,20 +4498,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap 2.14.0", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_write", - "winnow 0.7.15", -] - [[package]] name = "toml_edit" version = "0.25.12+spec-1.1.0" @@ -4540,9 +4505,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ "indexmap 2.14.0", - "toml_datetime 1.1.1+spec-1.1.0", + "toml_datetime", "toml_parser", - "winnow 1.0.3", + "winnow", ] [[package]] @@ -4551,21 +4516,9 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.3", + "winnow", ] -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - -[[package]] -name = "toml_writer" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" - [[package]] name = "tonic" version = "0.14.6" @@ -4784,6 +4737,24 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.4", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 2.0.18", +] + [[package]] name = "twox-hash" version = "2.1.2" @@ -4800,12 +4771,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] -name = "uncased" -version = "0.9.10" +name = "ulid" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" dependencies = [ - "version_check", + "rand 0.9.4", + "web-time", ] [[package]] @@ -4926,8 +4898,10 @@ checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ "getrandom 0.4.3", "js-sys", + "rand 0.10.2", "serde_core", "wasm-bindgen", + "zerocopy", ] [[package]] @@ -4936,12 +4910,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "version_check" version = "0.9.5" @@ -5215,15 +5183,6 @@ dependencies = [ "windows-targets 0.42.2", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -5275,21 +5234,6 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -5329,12 +5273,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -5353,12 +5291,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -5377,12 +5309,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -5413,12 +5339,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -5437,12 +5357,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -5461,12 +5375,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -5485,12 +5393,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -5503,15 +5405,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] - [[package]] name = "winnow" version = "1.0.3" @@ -5570,12 +5463,6 @@ dependencies = [ "rustix 1.1.4", ] -[[package]] -name = "yansi" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" - [[package]] name = "yasna" version = "0.6.0" @@ -5655,20 +5542,6 @@ name = "zeroize" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] [[package]] name = "zerotrie" diff --git a/Cargo.toml b/Cargo.toml index f27772f..fe391e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,8 +23,8 @@ tower-http = { version = "0.7", features = ["cors", "trace", "request-id", "prop tokio = { version = "1.52", features = ["full"] } tokio-util = { version = "0.7", features = ["rt"] } -# Message streaming (edge SDK for the latest server features - io_uring, shared-nothing architecture) -iggy = "0.8.0" +# Message streaming (Apache Iggy Rust SDK; 0.10 pairs with the server-0.8 line) +iggy = "0.10.0" # Serialization serde = { version = "1.0", features = ["derive"] } diff --git a/docker-compose.yaml b/docker-compose.yaml index 34eb747..9e31302 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,8 @@ services: # Apache Iggy message streaming server + # Pinned to the release paired with the Rust SDK 0.10 line (see Cargo.toml) iggy: - image: apache/iggy:latest + image: apache/iggy:0.8.0 privileged: true container_name: iggy-server ports: diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 6852ecd..f0f1901 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -38,9 +38,9 @@ struct IggyContainer { } impl IggyContainer { - // Using latest edge server (0.6.0-edge) with io_uring shared-nothing architecture + // Server release paired with the Rust SDK 0.10 line (see Cargo.toml) const IMAGE: &'static str = "apache/iggy"; - const TAG: &'static str = "latest"; + const TAG: &'static str = "0.8.0"; const TCP_PORT: u16 = 8090; /// Start an Iggy container and return the mapped TCP port From b45f32acd68e988deb9e8c7641db88b2d71e1141 Mon Sep 17 00:00:00 2001 From: mlevkov Date: Fri, 3 Jul 2026 09:09:06 -0700 Subject: [PATCH 04/20] fix(ci): migrate deny.toml to current cargo-deny schema - advisories: drop deprecated vulnerability/unmaintained/yanked lint-level keys (removed in cargo-deny 0.14; vulnerabilities and yanked crates now error by default); set unmaintained = "all" - drop obsolete ignores: RUSTSEC-2024-0384 (instant no longer in tree) and RUSTSEC-2025-0134 (rustls-pemfile dropped by bollard 0.20 via testcontainers 0.27) - licenses: allow Unicode-3.0 (ICU crates) and CDLA-Permissive-2.0 (webpki-roots CA bundle pulled by iggy 0.10 TLS support) cargo deny check: advisories ok, bans ok, licenses ok, sources ok --- deny.toml | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/deny.toml b/deny.toml index 98bccd1..1030e3f 100644 --- a/deny.toml +++ b/deny.toml @@ -28,6 +28,9 @@ allow = [ "Zlib", "MPL-2.0", "Unicode-DFS-2016", + "Unicode-3.0", + # CA certificate bundle data (webpki-roots, via iggy TLS support) + "CDLA-Permissive-2.0", "CC0-1.0", "Unlicense", ] @@ -64,6 +67,8 @@ skip = [] # ============================================================================= # Advisories # ============================================================================= +# Uses the cargo-deny >=0.14 advisories schema: vulnerabilities and yanked +# crates are errors by default; unmaintained warnings apply to all deps. [advisories] # Path to the advisory database db-path = "~/.cargo/advisory-db" @@ -71,26 +76,12 @@ db-path = "~/.cargo/advisory-db" # URL to the advisory database db-urls = ["https://github.com/rustsec/advisory-db"] -# Lint level for security vulnerabilities -vulnerability = "deny" - -# Lint level for unmaintained crates -unmaintained = "warn" - -# Lint level for yanked crates -yanked = "warn" +# Which dependencies to check for unmaintained advisories +unmaintained = "all" # Ignore specific advisories # These are transitive dependencies we cannot directly update ignore = [ - # instant is unmaintained, but comes from iggy -> reqwest-retry -> parking_lot v0.11.2 - # Upstream needs to update parking_lot to v0.12+ - "RUSTSEC-2024-0384", - - # rustls-pemfile is unmaintained, but comes from testcontainers -> bollard (dev-dependency only) - # Upstream needs to migrate to rustls-pki-types - "RUSTSEC-2025-0134", - # paste is unmaintained, but comes from iggy -> iggy_common -> compio-io # Upstream (iggy/compio) needs to migrate to pastey or alternative "RUSTSEC-2024-0436", From c7f5341762a57f0e106a71490822c6bd8f67b90c Mon Sep 17 00:00:00 2001 From: mlevkov Date: Fri, 3 Jul 2026 09:10:29 -0700 Subject: [PATCH 05/20] docs: sync documentation with SDK 0.10 and dependency updates - README/CLAUDE.md: correct Iggy versions (server 0.8.0, SDK 0.10.0) and refresh dependency tables (governor 0.10, tower-http 0.7, tokio 1.52, rust_decimal 1.42, metrics-exporter-prometheus 0.18, testcontainers 0.27) - CLAUDE.md: document the SDK integration decision (Client trait vs high-level IggyProducer/IggyConsumer) for the HTTP gateway - CHANGELOG.md: record security patches and dependency updates --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ CLAUDE.md | 30 +++++++++++++++++++++--------- README.md | 14 +++++++------- 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d885c2b..08ba567 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Security + +- Refreshed `Cargo.lock` to patch 10 RUSTSEC advisories in transitive + dependencies: `bytes` (RUSTSEC-2026-0007), `time` (RUSTSEC-2026-0009), + `quinn-proto` (RUSTSEC-2026-0037), `rustls-webpki` (RUSTSEC-2026-0049), + `aws-lc-rs`/`aws-lc-sys` (RUSTSEC-2026-0044 through 0048), and `rkyv` + (RUSTSEC-2026-0001) +- `testcontainers` 0.27 bump removes vulnerable `astral-tokio-tar` 0.5.x + and unmaintained `rustls-pemfile` from the dev-dependency tree +- `cargo audit` now reports zero vulnerabilities + +### Changed + +- Updated Apache Iggy Rust SDK from 0.8.0 to 0.10.0 (latest stable); + no source changes required — the `Client` trait API is unchanged +- Pinned the `apache/iggy` server image to 0.8.0 (the release paired + with the 0.10 SDK) in `docker-compose.yaml` and integration tests, + replacing the floating `latest` tag +- Bumped direct dependencies: `tower-http` 0.7, `rand` 0.10, + `metrics-exporter-prometheus` 0.18, `testcontainers` 0.27 (dev), + `reqwest` 0.13 (dev); raised version floors for `tokio` (1.52), + `uuid` (1.23), and `rust_decimal` (1.42) +- Migrated `deny.toml` to the current cargo-deny schema and pruned + obsolete advisory ignores; allowed `Unicode-3.0` and + `CDLA-Permissive-2.0` licenses required by new transitive deps +- Documented why the service integrates at the SDK `Client` trait level + instead of the high-level `IggyProducer`/`IggyConsumer` clients + ### Added - **Observability Stack**: Complete Grafana-based monitoring setup diff --git a/CLAUDE.md b/CLAUDE.md index 67daf75..914ed33 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,8 +5,8 @@ A comprehensive demonstration of Apache Iggy message streaming with Axum. ## Project Overview This application showcases how to build a production-ready message streaming service using: -- **Apache Iggy 0.6.0-edge**: High-performance message streaming with io_uring shared-nothing architecture -- **Iggy SDK 0.8.0-edge.6**: Latest edge SDK for compatibility with edge server features +- **Apache Iggy server 0.8.0**: High-performance message streaming with io_uring shared-nothing architecture +- **Iggy Rust SDK 0.10.0**: Latest stable SDK, paired with the server 0.8 release line - **Axum 0.8**: Ergonomic and modular Rust web framework - **Tokio**: Async runtime for Rust @@ -555,20 +555,32 @@ Iggy uses **0-indexed partitions**: ## Dependencies Key dependencies (see `Cargo.toml`): -- `iggy 0.8.0`: Iggy Rust SDK +- `iggy 0.10.0`: Iggy Rust SDK (paired with server 0.8.0, pinned in `docker-compose.yaml`) - `axum 0.8`: Web framework -- `tokio 1.48`: Async runtime +- `tokio 1.52`: Async runtime - `tokio-util 0.7`: Task tracking and cancellation tokens - `serde 1.0`: Serialization - `tracing 0.1`: Structured logging - `thiserror 2.0`: Error handling -- `governor 0.8`: Rate limiting with token bucket algorithm +- `governor 0.10`: Rate limiting with token bucket algorithm - `subtle 2.6`: Constant-time comparison for security -- `tower-http 0.6`: HTTP middleware (CORS, tracing, request ID) -- `rust_decimal 1.39`: Exact decimal arithmetic for monetary values +- `tower-http 0.7`: HTTP middleware (CORS, tracing, request ID) +- `rust_decimal 1.42`: Exact decimal arithmetic for monetary values - `metrics 0.24`: Application metrics -- `metrics-exporter-prometheus 0.16`: Prometheus metrics export -- `testcontainers 0.26`: Integration testing with containerized Iggy +- `metrics-exporter-prometheus 0.18`: Prometheus metrics export +- `testcontainers 0.27`: Integration testing with containerized Iggy + +### Iggy SDK Integration + +This service integrates with the SDK at the `Client` trait level (via +`IggyClientWrapper`) rather than through the higher-level `IggyProducer`/ +`IggyConsumer` clients. This is deliberate: the HTTP gateway serves +*arbitrary* stream/topic routes with per-request partition, offset, and +consumer parameters. `IggyProducer` binds to a single stream/topic at build +time and batches in the background, and `IggyConsumer` is a long-lived +subscription iterator — neither maps onto stateless request/response +semantics. The high-level clients are the right choice for dedicated +pipeline workers; a protocol gateway belongs on the trait API. ## Structured Concurrency diff --git a/README.md b/README.md index fba2ccd..100f6ac 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ A comprehensive demonstration of [Apache Iggy](https://github.com/apache/iggy) m This project showcases how to build a production-ready message streaming service using: -- **Apache Iggy 0.6.0-edge** - High-performance message streaming with io_uring shared-nothing architecture -- **Iggy SDK 0.8.0-edge.6** - Latest edge SDK for compatibility with edge server features +- **Apache Iggy server 0.8.0** - High-performance message streaming with io_uring shared-nothing architecture +- **Iggy Rust SDK 0.10.0** - Latest stable SDK, paired with the server 0.8 release line - **Axum 0.8** - Ergonomic and modular Rust web framework - **Tokio** - Async runtime for Rust @@ -648,16 +648,16 @@ Key dependencies (see `Cargo.toml` for full list): | Crate | Version | Purpose | |-------|---------|---------| -| `iggy` | 0.8.0-edge.6 | Iggy Rust SDK (edge) | +| `iggy` | 0.10.0 | Iggy Rust SDK | | `axum` | 0.8 | Web framework | -| `tokio` | 1.48 | Async runtime | +| `tokio` | 1.52 | Async runtime | | `serde` | 1.0 | Serialization | | `tracing` | 0.1 | Structured logging | | `thiserror` | 2.0 | Error handling | -| `governor` | 0.8 | Rate limiting (token bucket) | +| `governor` | 0.10 | Rate limiting (token bucket) | | `subtle` | 2.6 | Constant-time comparison | -| `tower-http` | 0.6 | HTTP middleware (CORS, tracing) | -| `rust_decimal` | 1.37 | Exact decimal arithmetic for money | +| `tower-http` | 0.7 | HTTP middleware (CORS, tracing) | +| `rust_decimal` | 1.42 | Exact decimal arithmetic for money | | `uuid` | 1.18 | UUID generation | | `chrono` | 0.4 | Date/time handling | | `testcontainers` | 0.24 | Integration testing | From 6ff23ecbe0baefa0fa4a518e49fc39d461da58ec Mon Sep 17 00:00:00 2001 From: mlevkov Date: Fri, 3 Jul 2026 09:39:54 -0700 Subject: [PATCH 06/20] fix(ci): gate CI on cargo-deny advisories and pin stress-test server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review round 1, themes B and E (flagged by 4 of 8 agents): - run 'cargo deny check advisories licenses' instead of licenses-only, drop continue-on-error, and include the job in ci-success — the freshly migrated deny.toml advisories section previously gated nothing in CI - pin extended-tests stress server to apache/iggy:0.8.0, in lockstep with docker-compose.yaml and the integration tests --- .github/workflows/ci.yml | 14 +++++++------- .github/workflows/extended-tests.yml | 3 ++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1e3a36..4d03a34 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -168,10 +168,10 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} # ========================================================================== - # Dependency license check + # Dependency policy check (advisories + licenses, gated by deny.toml) # ========================================================================== licenses: - name: License Check + name: Dependency Policy (cargo-deny) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -181,9 +181,8 @@ jobs: - name: Install cargo-deny uses: taiki-e/install-action@cargo-deny - - name: Check licenses - run: cargo deny check licenses - continue-on-error: true # Don't fail CI on license issues, just warn + - name: Check advisories and licenses + run: cargo deny check advisories licenses # ========================================================================== # Final status check (for branch protection) @@ -191,7 +190,7 @@ jobs: ci-success: name: CI Success runs-on: ubuntu-latest - needs: [fmt, clippy, test, integration, docs, audit] + needs: [fmt, clippy, test, integration, docs, audit, licenses] if: always() steps: - name: Check all jobs passed @@ -201,7 +200,8 @@ jobs: [[ "${{ needs.test.result }}" != "success" ]] || \ [[ "${{ needs.integration.result }}" != "success" ]] || \ [[ "${{ needs.docs.result }}" != "success" ]] || \ - [[ "${{ needs.audit.result }}" != "success" ]]; then + [[ "${{ needs.audit.result }}" != "success" ]] || \ + [[ "${{ needs.licenses.result }}" != "success" ]]; then echo "One or more jobs failed" exit 1 fi diff --git a/.github/workflows/extended-tests.yml b/.github/workflows/extended-tests.yml index f31322e..3a17230 100644 --- a/.github/workflows/extended-tests.yml +++ b/.github/workflows/extended-tests.yml @@ -75,7 +75,8 @@ jobs: if: ${{ github.event_name == 'schedule' || github.event.inputs.run_stress_tests == 'true' }} services: iggy: - image: apache/iggy:latest + # Keep in lockstep with docker-compose.yaml and tests/integration_tests.rs + image: apache/iggy:0.8.0 ports: - 8090:8090 options: >- From c9280231119126b264ee1056bec85a4a5d9952af Mon Sep 17 00:00:00 2001 From: mlevkov Date: Fri, 3 Jul 2026 09:43:33 -0700 Subject: [PATCH 07/20] fix(docs): reconcile docs, comments, and config defaults with reality Review round 1, themes C, D, F and the doc half of G (verified by the step-4.5 pass; README table rows flagged by 4 of 8 agents): - default app port 3000 -> 8000 (config.rs, .env.example): the old default collided with the Iggy server's HTTP API under the documented compose quick start; README/CLAUDE.md/docs curl examples now target 8000 for the app and keep 3000 only for the Iggy server - README: unit-test count 93 -> 130, uuid 1.23 / testcontainers 0.27 table rows, iggy_client.rs -> iggy_client/ layout, integration-test instructions rewritten (testcontainers auto-spins the server; the old '-- --ignored' command ran zero tests) - CLAUDE.md: middleware order corrected (Request ID before Timeout), poll_with_params -> poll_messages, module-path references fixed - architecture.md: poll_with_params -> poll_messages in sequence diagram - deny.toml: comment now states the real cargo-deny floor (0.18.2) and correct default severities; pruned dead MPL-2.0/Unicode-DFS-2016 allowances (license-not-encountered warnings) - routes.rs: inverted layer-order comment corrected (rate limit runs first, not last) - iggy_client/mod.rs: 'Consumer Groups' heading corrected to standalone consumer offsets semantics - tests: stale 'edge server' comment and module-path reference fixed - durable-storage guide re-stamped for server 0.8.0 - handlers/messages.rs: partition_id doc default corrected to 0 - CHANGELOG: astral-tokio-tar 'removed' -> 'upgraded to patched 0.6.x', advisory attribution corrected to aws-lc-sys, new entries recorded --- .env.example | 3 +- CHANGELOG.md | 13 +- CLAUDE.md | 14 +- README.md | 38 ++-- architecture.md | 2 +- deny.toml | 8 +- docs/code-reviews/session-01-round1.md | 239 +++++++++++++++++++++++++ docs/durable-storage-guide.md | 6 +- docs/guide.md | 10 +- src/config.rs | 8 +- src/handlers/messages.rs | 2 +- src/iggy_client/mod.rs | 7 +- src/routes.rs | 3 +- tests/integration_tests.rs | 4 +- 14 files changed, 301 insertions(+), 56 deletions(-) create mode 100644 docs/code-reviews/session-01-round1.md diff --git a/.env.example b/.env.example index 2125a93..7751e3b 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ # Server Configuration +# Note: 8000 avoids colliding with the Iggy server's HTTP API on 3000 HOST=0.0.0.0 -PORT=3000 +PORT=8000 # Iggy Configuration # Connection string format: iggy://username:password@host:port diff --git a/CHANGELOG.md b/CHANGELOG.md index 08ba567..d811c99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Refreshed `Cargo.lock` to patch 10 RUSTSEC advisories in transitive dependencies: `bytes` (RUSTSEC-2026-0007), `time` (RUSTSEC-2026-0009), `quinn-proto` (RUSTSEC-2026-0037), `rustls-webpki` (RUSTSEC-2026-0049), - `aws-lc-rs`/`aws-lc-sys` (RUSTSEC-2026-0044 through 0048), and `rkyv` + `aws-lc-sys` (RUSTSEC-2026-0044 through 0048), and `rkyv` (RUSTSEC-2026-0001) -- `testcontainers` 0.27 bump removes vulnerable `astral-tokio-tar` 0.5.x - and unmaintained `rustls-pemfile` from the dev-dependency tree +- `testcontainers` 0.27 bump upgrades `astral-tokio-tar` to patched 0.6.x + and removes unmaintained `rustls-pemfile` from the dev-dependency tree - `cargo audit` now reports zero vulnerabilities ### Changed @@ -34,6 +34,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `CDLA-Permissive-2.0` licenses required by new transitive deps - Documented why the service integrates at the SDK `Client` trait level instead of the high-level `IggyProducer`/`IggyConsumer` clients +- **Breaking**: default app port changed from 3000 to 8000 — the old + default collided with the Iggy server's HTTP API port under the + documented docker-compose quick start; all docs, `.env.example`, and + compose now agree on 8000 +- CI now fails on `cargo deny check advisories licenses` (previously + licenses-only and non-blocking); weekly stress tests pin + `apache/iggy:0.8.0` instead of `latest` ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 914ed33..655bfd4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,7 +52,7 @@ This application showcases how to build a production-ready message streaming ser │ - producer.rs: Message publishing logic │ │ - consumer.rs: Message consumption logic │ ├─────────────────────────────────────────────────────────────┤ -│ IggyClientWrapper (src/iggy_client.rs) │ +│ IggyClientWrapper (src/iggy_client/) │ │ High-level wrapper with automatic reconnection │ │ + Circuit breaker for fail-fast during outages │ │ + PollParams builder for cleaner polling API │ @@ -386,10 +386,10 @@ environment: 3. Test the API: ```bash # Health check - curl http://localhost:3000/health + curl http://localhost:8000/health # Send a message - curl -X POST http://localhost:3000/messages \ + curl -X POST http://localhost:8000/messages \ -H "Content-Type: application/json" \ -d '{ "event": { @@ -409,7 +409,7 @@ environment: }' # Poll messages (partition_id is 0-indexed, 0 = first partition) - curl "http://localhost:3000/messages?partition_id=0&count=10" + curl "http://localhost:8000/messages?partition_id=0&count=10" ``` ### Running Tests @@ -626,7 +626,7 @@ impl AppState { Uses `tokio::sync::Notify` instead of busy-wait for efficient reconnection: ```rust -// In src/iggy_client.rs - ConnectionState +// In src/iggy_client/connection.rs - ConnectionState struct ConnectionState { reconnect_complete: Notify, // Efficient wake-up // ... @@ -654,14 +654,14 @@ let params = PollParams::new(1, 1) // partition_id, consumer_id .with_auto_commit(true); // Use with the cleaner API -let messages = client.poll_with_params("stream", "topic", params).await?; +let messages = client.poll_messages("stream", "topic", params).await?; ``` ## Middleware Stack Request flow (applied in order): ``` -Request → Rate Limit → Auth → Timeout → Request ID → Tracing → CORS → Handler +Request → Rate Limit → Auth → Request ID → Timeout → Tracing → CORS → Handler ``` ### Client IP Extraction (`src/middleware/ip.rs`) diff --git a/README.md b/README.md index 100f6ac..7c01b84 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Apache Iggy is capable of processing millions of messages per second with ultra- ### Development & Testing - Docker Compose setup for local development -- Comprehensive test suite (93 unit tests, 24 integration tests, 20 model tests) +- Comprehensive test suite (130 unit tests, 24 integration tests, 20 model tests) - Integration tests with testcontainers (auto-spins Iggy server) - Fuzz testing for input validation functions @@ -186,7 +186,7 @@ Expected response: ### Send a User Event ```bash -curl -X POST http://localhost:3000/messages \ +curl -X POST http://localhost:8000/messages \ -H "Content-Type: application/json" \ -d '{ "event": { @@ -209,7 +209,7 @@ curl -X POST http://localhost:3000/messages \ ### Send an Order Event ```bash -curl -X POST http://localhost:3000/messages \ +curl -X POST http://localhost:8000/messages \ -H "Content-Type: application/json" \ -d '{ "event": { @@ -240,7 +240,7 @@ curl -X POST http://localhost:3000/messages \ ### Send a Generic Event ```bash -curl -X POST http://localhost:3000/messages \ +curl -X POST http://localhost:8000/messages \ -H "Content-Type: application/json" \ -d '{ "event": { @@ -262,16 +262,16 @@ curl -X POST http://localhost:3000/messages \ ```bash # Poll from partition 1, starting at offset 0 -curl "http://localhost:3000/messages?partition_id=1&count=10&offset=0" +curl "http://localhost:8000/messages?partition_id=1&count=10&offset=0" # Poll with auto-commit -curl "http://localhost:3000/messages?partition_id=1&count=10&auto_commit=true" +curl "http://localhost:8000/messages?partition_id=1&count=10&auto_commit=true" ``` ### Send Batch Messages ```bash -curl -X POST http://localhost:3000/messages/batch \ +curl -X POST http://localhost:8000/messages/batch \ -H "Content-Type: application/json" \ -d '{ "events": [ @@ -294,7 +294,7 @@ curl -X POST http://localhost:3000/messages/batch \ ### Create a Stream ```bash -curl -X POST http://localhost:3000/streams \ +curl -X POST http://localhost:8000/streams \ -H "Content-Type: application/json" \ -d '{"name": "my-stream"}' ``` @@ -302,7 +302,7 @@ curl -X POST http://localhost:3000/streams \ ### Create a Topic ```bash -curl -X POST http://localhost:3000/streams/my-stream/topics \ +curl -X POST http://localhost:8000/streams/my-stream/topics \ -H "Content-Type: application/json" \ -d '{"name": "my-topic", "partitions": 3}' ``` @@ -310,13 +310,13 @@ curl -X POST http://localhost:3000/streams/my-stream/topics \ ### List Streams ```bash -curl http://localhost:3000/streams +curl http://localhost:8000/streams ``` ### Get Statistics ```bash -curl http://localhost:3000/stats +curl http://localhost:8000/stats ``` ## Configuration @@ -395,7 +395,7 @@ iggy_sample/ │ ├── error.rs # Error types with HTTP status codes │ ├── state.rs # Shared application state │ ├── routes.rs # Route definitions -│ ├── iggy_client.rs # Iggy SDK wrapper +│ ├── iggy_client/ # Iggy SDK wrapper module │ ├── validation.rs # Input validation utilities │ ├── middleware/ │ │ ├── mod.rs # Middleware exports @@ -435,15 +435,11 @@ cargo test ### Run Integration Tests -Integration tests require a running server: +Integration tests use [testcontainers](https://rust.testcontainers.org) and +spin up their own Iggy server automatically — only Docker needs to be running: ```bash -# Terminal 1: Start services -docker-compose up -d iggy -cargo run & - -# Terminal 2: Run integration tests -cargo test --test integration_tests -- --ignored +cargo test --test integration_tests ``` ### Run with Coverage @@ -658,9 +654,9 @@ Key dependencies (see `Cargo.toml` for full list): | `subtle` | 2.6 | Constant-time comparison | | `tower-http` | 0.7 | HTTP middleware (CORS, tracing) | | `rust_decimal` | 1.42 | Exact decimal arithmetic for money | -| `uuid` | 1.18 | UUID generation | +| `uuid` | 1.23 | UUID generation | | `chrono` | 0.4 | Date/time handling | -| `testcontainers` | 0.24 | Integration testing | +| `testcontainers` | 0.27 | Integration testing | ## CI/CD diff --git a/architecture.md b/architecture.md index 133cfc1..e18718b 100644 --- a/architecture.md +++ b/architecture.md @@ -241,7 +241,7 @@ Client Handler Consumer IggyClient │─────────────────────▶│ │ │ │ │ │ build poll params │ │ │ │ │───────────────────▶│ │ │ - │ │ │ poll_with_params() │ │ + │ │ │ poll_messages() │ │ │ │ │────────────────────▶│ │ │ │ │ │ poll_messages() │ │ │ │ │────────────────▶│ diff --git a/deny.toml b/deny.toml index 1030e3f..8015d3b 100644 --- a/deny.toml +++ b/deny.toml @@ -26,8 +26,6 @@ allow = [ "BSD-3-Clause", "ISC", "Zlib", - "MPL-2.0", - "Unicode-DFS-2016", "Unicode-3.0", # CA certificate bundle data (webpki-roots, via iggy TLS support) "CDLA-Permissive-2.0", @@ -67,8 +65,10 @@ skip = [] # ============================================================================= # Advisories # ============================================================================= -# Uses the cargo-deny >=0.14 advisories schema: vulnerabilities and yanked -# crates are errors by default; unmaintained warnings apply to all deps. +# Requires cargo-deny >= 0.18.2 (for the `unmaintained` scope field). +# Under the current schema, vulnerability and unmaintained advisories are +# errors by default; yanked crates only warn. Enforced in CI by the +# "Dependency Policy" job (cargo deny check advisories licenses). [advisories] # Path to the advisory database db-path = "~/.cargo/advisory-db" diff --git a/docs/code-reviews/session-01-round1.md b/docs/code-reviews/session-01-round1.md new file mode 100644 index 0000000..5ff0f5c --- /dev/null +++ b/docs/code-reviews/session-01-round1.md @@ -0,0 +1,239 @@ +# Code Review — Session 01, Round 1 + +**Branch:** `chore/iggy-0.10-deps-refresh` (5 commits vs `main`) +**Scope:** full repository, with emphasis on the change set — lockfile security +refresh (issues #13–#22), direct dependency bumps, iggy SDK 0.8.0 → 0.10.0, +`apache/iggy:0.8.0` image pins, deny.toml schema migration, docs sync. +**Date:** 2026-07-03 + +## Provenance + +Config: **full** (evidence-based). The step-0 `AskUserQuestion` gate received no +response within the timeout (user away from keyboard); the run proceeded on +direct evidence: parent session model is Claude Fable 5 (Mythos-class, above the +Opus-class bar), and both `feature-dev` and `pr-review-toolkit` plugins are +installed (all eight native agent types spawned — **no fallbacks**). Reasoning +effort could not be verified and is noted as the one unattested dimension. +Model pin deviation, recorded deliberately: the skill text says pin +`model: opus`; every agent (and the verifier) was pinned to `fable` instead, +because the skill predates the Fable tier and pinning opus would have +*downgraded* the suite below the parent session — the strongest-model intent +was honored over the letter. +Authoritative-spec note: this repo carries no `docs/quality-assurance.md`; the +skill's inline spec was followed. No ticket scaffolding exists; this artifact +lives at `docs/code-reviews/` keyed by session number. +Verification (step 4.5): one `general-purpose` verifier re-read every cited +`file:line` for all consolidated claims — **findings: 34 kept / 0 drifted / +0 discarded**. + +Agents: rev-consistency (general-purpose), rev-architect +(feature-dev:code-architect), rev-logic-sec (feature-dev:code-reviewer), +rev-types (pr-review-toolkit:type-design-analyzer), rev-silentfail +(pr-review-toolkit:silent-failure-hunter), rev-comments +(pr-review-toolkit:comment-analyzer), rev-tests +(pr-review-toolkit:pr-test-analyzer), rev-simplify +(pr-review-toolkit:code-simplifier). + +## Verdict on the change set itself + +The five commits are clean: every agent that checked confirmed the 10 RUSTSEC +advisories are genuinely patched (verified against the advisory DB and live +`cargo audit`), the rand 0.10 fix is behaviorally identical (verified against +vendored rand sources), iggy 0.10 is API- and wire-compatible for this app's +surface (verified by source diff of 0.8 vs 0.10 and a live integration run +against the pinned server), and the Client-trait-vs-producer/consumer rationale +holds [architect: "could not break it"]. **Every finding below except the +directly-remediable doc/CI items is pre-existing on `main`** — surfaced because +the review scope was the whole repo and the SDK bump changes the context these +paths run in. + +--- + +## Theme A — The resilience layer is dead code, and SDK 0.10 makes it deader +[architect #1–#4 + startup corollary; silentfail C1, H1, M2, M3; types F1; tests #3] +**Severity: HIGH (pre-existing; partially activated by SDK 0.10 semantics)** + +Four agents independently traced the same root cause: + +1. `is_connection_error()` (src/iggy_client/mod.rs:446-453) can never match: + `AppError::Disconnected`/`ConnectionReset` have **zero producers** in + production code; every SDK error is stringified into + SendError/PollError/StreamError/TopicError. The reconnect-and-retry branch, + the circuit breaker's `record_failure`, and all `CIRCUIT_BREAKER_*` config + are therefore unreachable/inert. +2. `set_connected(false)` exists only inside the unreachable `reconnect()`, so + the connected flag latches true: `/health`, `/ready`, and the background + health task report healthy through a total Iggy outage (no live ping + anywhere). +3. SDK 0.10 widens the failure classes its **default-on internal reconnection** + intercepts (adds NotConnected/CannotEstablishConnection/TcpError vs 0.8); + operations during an outage block inside the SDK's unlimited 1s retry loop + and surface to the wrapper only as `OperationTimeout` — which the wrapper + deliberately does not count as a breaker failure. Request pile-up is exactly + the failure the breaker exists to prevent. Corollary: `IggyClientWrapper::new`'s + "returns an error immediately" doc is false on 0.10 — startup hangs retrying. +4. Latent once (1) is fixed: `reconnect()` swaps in a new `IggyClient` without + `shutdown()` on the old one, leaking a detached heartbeat task that can + resurrect a zombie server connection (verified in vendored SDK source). + +**Remediation (this branch):** structured IggyError→AppError classification at +the mapping sites (0.10's error discriminants make this clean); disable the +SDK's internal reconnection so the wrapper's documented policy is the only +one; make the health task actually `ping()` and drive `ConnectionState`; +`shutdown()` the old client before swap; record breaker failures on timeout +paths. **Deferred with TD records:** DiagnosticEvents-driven state, +with_reconnect paused-clock test matrix. + +## Theme B — CI never enforces the deny.toml this branch migrated +[tests #1 (CRITICAL); logic-sec #4; consistency #5; architect #11] +**Severity: HIGH (consensus after severity reconciliation)** + +`ci.yml` runs only `cargo deny check licenses`, with `continue-on-error: true`, +and the job is excluded from `ci-success`; `cargo deny check advisories` is +never run; `rustsec/audit-check` does not read deny.toml. The advisories +section — including the paste ignore — gates nothing. +**Remediation (this branch):** gated `cargo deny check advisories licenses` +step, drop `continue-on-error`, add to `ci-success` needs. + +## Theme C — deny.toml comment is wrong on three counts; dead license allowances +[comments H1; logic-sec #2, #3; consistency #3, #4] +**Severity: MEDIUM** + +The new comment claims ">=0.14 schema"; actual floor for `unmaintained = "all"` +is cargo-deny **0.18.2**; yanked crates default to **warn**, not error; +unmaintained advisories are **errors**, not warnings. `MPL-2.0` and +`Unicode-DFS-2016` are now `license-not-encountered` dead allowances. +**Remediation (this branch):** rewrite the comment accurately; prune both dead +allowances. + +## Theme D — Stale rows and counts the docs-sync commit missed +[simplify #5; logic-sec #1; consistency #1, #6; comments M4, M5, M6, L3] +**Severity: MEDIUM (4-agent overlap on the README rows)** + +README.md:661/663 still say uuid 1.18 / testcontainers 0.24; README.md:44 says +"93 unit tests" (actual: 130). CHANGELOG says astral-tokio-tar was "removed" +(it was upgraded to patched 0.6.3) and attributes RUSTSEC-2026-0044..0048 to +aws-lc-rs/aws-lc-sys (all five are against aws-lc-sys). +**Remediation (this branch):** fix all four. + +## Theme E — extended-tests.yml still floats `apache/iggy:latest` +[tests #6; consistency #2; comments M7] +**Severity: MEDIUM** + +Three server-version declarations existed; this branch pinned two. The weekly +stress workflow still tests whatever `latest` points at. +**Remediation (this branch):** pin to 0.8.0. + +## Theme F — Prose that contradicts the code +[comments M1, M2, M3, L1, L2, L4, L5; consistency #10] +**Severity: MEDIUM** + +`poll_with_params()` doesn't exist (CLAUDE.md:657, architecture.md:244; actual: +`poll_messages`); CLAUDE.md's middleware order swaps Timeout/Request-ID; +routes.rs:155's "applied first, runs last" is inverted; "edge server" comment +survives in tests; four references to the removed `src/iggy_client.rs` file +layout; "# Consumer Groups" heading on a non-group consumer; a doc reference to +a nonexistent `health_check()` method; durable-storage guide stamped v0.6.0. +**Remediation (this branch):** fix all. + +## Theme G — Observability stack is wired to nothing +[silentfail H2; consistency #7, #8, #9] +**Severity: HIGH (H2) / MEDIUM (docs)** + +`init_metrics`/`try_init_metrics` and every `record_*` helper have **zero call +sites** — the Prometheus exporter never starts, `METRICS_PORT` is inert, and +prometheus.yml scrapes `app:8000/metrics`, a route that doesn't exist, on a +port that isn't the metrics listener. Compose neither sets nor exposes +METRICS_PORT. Separately: README contradicts itself on the app port (8000 vs +curl examples on 3000 = the Iggy server's port), and the documented +integration-test command (`-- --ignored`) runs zero tests. +**Remediation (this branch):** initialize the exporter at startup (fail fast on +bind error), wire the natural record_* call sites, fix prometheus.yml/compose, +fix the README port story and test instructions. + +## Theme H — Security middleware fail-open holes +[silentfail H3, H4, H5, L1; types F2; logic-sec clean-verdict caveats] +**Severity: HIGH** + +(1) `TrustedProxyConfig::is_trusted` has zero call sites; XFF/X-Real-IP are +trusted unconditionally, and an invalid-CIDR TRUSTED_PROXIES silently degrades +to trust-all — the exact spoofing scenario CLAUDE.md's security section warns +about. (2) The auth brute-force limiter consumes a token on every request +*before* validation: valid-key clients are capped at ~10 req/min/IP, and all +direct clients share one "unknown" bucket — enabling API_KEY on a +directly-exposed service throttles the whole service with a misattributed 429. +(3) Invalid CORS origins are silently dropped (fail-closed, but unlogged). +**Remediation (this branch):** parse TRUSTED_PROXIES at startup and fail fast +on invalid entries; enforce header trust against the parsed ranges; restructure +auth to validate first and count only failures; warn on dropped CORS origins. +**Deferred with TD record:** peer-address (ConnectInfo) as the trust anchor. + +## Theme I — Defects inside the (currently dead) reconnect machinery +[architect #5–#9; simplify #1, #2; silentfail M3; tests #2, #8] +**Severity: MEDIUM (latent behind Theme A; real once A is fixed)** + +Unchecked backoff multiplication overflows u64 at attempt ~56; the attempt +counter never resets per reconnect session (permanent-failure mode after +exhaustion); jitter is applied after the max-delay cap (delays can exceed the +documented max by 20%); reconnect/follower waits are unbounded on the request +path; the post-reconnect retry block is duplicated verbatim (~20 lines × 2); +the scopeguard's generic value channel is dead weight; half-open allows +unlimited concurrent probes and Open-state failures extend the open window. +**Remediation (this branch):** all of the above except the probe-limiting +design change (TD record). + +## Theme J — ensure_stream/ensure_topic swallow errors and race +[silentfail M1] +**Severity: MEDIUM** + +`Ok(None) | Err(_)` swallows the get error; two replicas racing on first boot +both create, and the loser's "already exists" error crash-loops the process via +main.rs exit. +**Remediation (this branch):** split the arms; treat already-exists as success. + +## Theme K — API-boundary and type nits +[types F3, F4, F7, F8; comments/verify #24-26; silentfail M5, L2; simplify #3, #4] +**Severity: MEDIUM (F3, F4) / LOW (rest)** + +`count=0` polls surface as 500 instead of 400; all-digit stream names ("42") +silently become numeric-ID lookups (wrong-resource risk on DELETE); +`PollMessagesRequest` is dead API surface documenting a capability nothing +implements; a dead error mapping on the infallible `Identifier::numeric`; the +test fixture masks server-task panics as generic timeouts; serve-error path +skips `state.shutdown()`; duplicated response-building in producer/consumer +services; `rand_jitter` is now a trivial alias (kept for its documented role). +**Remediation (this branch):** F3 (validate → 400), F4 (`Identifier::named`), +delete dead type + dead mapping, fixture panic surfacing, shutdown-on-error +path, service dedup. **Backlog:** the rest of the LOW nits. + +## Theme L — Test coverage gaps the bump exposes +[tests #2, #3, #4, #5, #7, #9] +**Severity: MEDIUM** + +Nothing pins PollingStrategy::next/auto-commit semantics, partition-key +routing, with_reconnect composition, or SDK-error classification; metrics +exporter has zero runtime coverage. +**Remediation (this branch):** classifier unit tests + extracted-backoff bounds +tests + auto-commit/partition-key integration tests where they fit. **Deferred +with TD records:** with_reconnect paused-clock matrix, metrics smoke test. + +--- + +## Triage summary + +| Theme | Severity | Disposition | +|-------|----------|-------------| +| A resilience dead code | HIGH | Fix core in-branch + 2 TD records | +| B CI deny gate | HIGH | Fix in-branch | +| G observability unwired | HIGH | Fix in-branch | +| H security fail-open | HIGH | Fix in-branch + 1 TD record | +| C deny.toml prose | MEDIUM | Fix in-branch | +| D stale rows/counts | MEDIUM | Fix in-branch | +| E extended-tests pin | MEDIUM | Fix in-branch | +| F prose vs code | MEDIUM | Fix in-branch | +| I reconnect internals | MEDIUM | Fix in-branch + 1 TD record | +| J ensure_* race | MEDIUM | Fix in-branch | +| K API nits | MEDIUM/LOW | Fix most in-branch | +| L test gaps | MEDIUM | Partial in-branch + TD records | + +Round 2 re-review follows remediation. diff --git a/docs/durable-storage-guide.md b/docs/durable-storage-guide.md index d966442..2a8aa59 100644 --- a/docs/durable-storage-guide.md +++ b/docs/durable-storage-guide.md @@ -574,7 +574,7 @@ Key metrics to monitor for storage health: ## Current Limitations -As of Iggy v0.6.0 (December 2025): +As of Iggy server v0.8.0 (April 2026): ### Single-Node Architecture @@ -718,5 +718,5 @@ tmp_upload_dir = "/tmp/iggy-upload" --- -*Last updated: December 2025* -*Iggy version: 0.6.0* +*Last updated: July 2026* +*Iggy server version: 0.8.0* diff --git a/docs/guide.md b/docs/guide.md index dd3c573..6452109 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -937,25 +937,25 @@ iggy -u iggy -p iggy topic get stream topic ```bash # Health check -curl http://localhost:3000/ +curl http://localhost:8000/ # Create stream -curl -X POST http://localhost:3000/streams \ +curl -X POST http://localhost:8000/streams \ -H "Content-Type: application/json" \ -d '{"stream_id": 1, "name": "my-stream"}' # Create topic -curl -X POST http://localhost:3000/streams/my-stream/topics \ +curl -X POST http://localhost:8000/streams/my-stream/topics \ -H "Content-Type: application/json" \ -d '{"topic_id": 1, "name": "events", "partitions_count": 3}' # Send message -curl -X POST http://localhost:3000/streams/my-stream/topics/events/messages \ +curl -X POST http://localhost:8000/streams/my-stream/topics/events/messages \ -H "Content-Type: application/json" \ -d '{"partitioning": {"kind": "partition_id", "value": 1}, "messages": [{"payload": "aGVsbG8="}]}' # Poll messages -curl "http://localhost:3000/streams/my-stream/topics/events/messages?consumer_id=1&partition_id=1&count=10" +curl "http://localhost:8000/streams/my-stream/topics/events/messages?consumer_id=1&partition_id=1&count=10" ``` ### Environment Variables (iggy_sample) diff --git a/src/config.rs b/src/config.rs index 41c51e7..baa5d97 100644 --- a/src/config.rs +++ b/src/config.rs @@ -38,7 +38,7 @@ pub struct Config { /// Server host address (default: "0.0.0.0") pub host: String, - /// Server port (default: 3000) + /// Server port (default: 8000; the Iggy server's HTTP API uses 3000) pub port: u16, // ========================================================================= @@ -170,7 +170,7 @@ impl Config { let config = Self { // Server host: env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string()), - port: Self::parse_env("PORT", 3000)?, + port: Self::parse_env("PORT", 8000)?, // Iggy connection iggy_connection_string: env::var("IGGY_CONNECTION_STRING") @@ -381,7 +381,7 @@ impl Default for Config { Self { // Server host: "0.0.0.0".to_string(), - port: 3000, + port: 8000, // Iggy connection iggy_connection_string: "iggy://iggy:iggy@localhost:8090".to_string(), default_stream: "sample-stream".to_string(), @@ -427,7 +427,7 @@ mod tests { let config = Config::default(); assert_eq!(config.host, "0.0.0.0"); - assert_eq!(config.port, 3000); + assert_eq!(config.port, 8000); assert_eq!(config.rate_limit_rps, 100); assert_eq!(config.batch_max_size, 1000); assert_eq!(config.max_request_body_size, 10 * 1024 * 1024); diff --git a/src/handlers/messages.rs b/src/handlers/messages.rs index fc5bc08..8b89f6c 100644 --- a/src/handlers/messages.rs +++ b/src/handlers/messages.rs @@ -155,7 +155,7 @@ fn default_count() -> u32 { /// /// # Query Parameters /// -/// - `partition_id` - Partition to poll from (default: 1) +/// - `partition_id` - Partition to poll from, 0-indexed (default: 0) /// - `consumer_id` - Consumer ID for offset tracking (default: 1) /// - `offset` - Starting offset (optional) /// - `count` - Number of messages to return (default: 10, max: POLL_MAX_COUNT) diff --git a/src/iggy_client/mod.rs b/src/iggy_client/mod.rs index c2804fc..7ecfd60 100644 --- a/src/iggy_client/mod.rs +++ b/src/iggy_client/mod.rs @@ -704,10 +704,11 @@ impl IggyClientWrapper { /// let messages = client.poll_messages("stream", "topic", params).await?; /// ``` /// - /// # Consumer Groups + /// # Consumer Offsets /// - /// Each unique `consumer_id` maintains its own offset. Use the same ID - /// across restarts to resume from the last committed position. + /// Polling uses a standalone consumer (not a consumer group). Each unique + /// `consumer_id` maintains its own offset. Use the same ID across restarts + /// to resume from the last committed position. #[instrument(skip(self, params), fields(partition_id = params.partition_id, consumer_id = params.consumer_id))] pub async fn poll_messages( &self, diff --git a/src/routes.rs b/src/routes.rs index 74c5df6..b4afc76 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -152,7 +152,8 @@ pub fn build_router(state: AppState) -> Result { info!("API key authentication disabled (no API_KEY set)"); } - // 7. Rate Limiting (if enabled) - applied first, runs last in request pipeline + // 7. Rate Limiting (if enabled) - applied last, so it runs FIRST on + // incoming requests (outermost layer), before auth ever sees them if config.rate_limiting_enabled() { info!( rps = config.rate_limit_rps, diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index f0f1901..489a104 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -7,7 +7,7 @@ //! //! # Reconnection Testing //! -//! Reconnection logic is tested via unit tests in `src/iggy_client.rs`. +//! Reconnection logic is tested via unit tests in `src/iggy_client/`. //! Full integration reconnection testing (container stop/start) is complex //! and may result in flaky tests. The reconnection implementation uses: //! @@ -50,7 +50,7 @@ impl IggyContainer { let container = GenericImage::new(Self::IMAGE, Self::TAG) .with_exposed_port(Self::TCP_PORT.tcp()) .with_wait_for(WaitFor::message_on_stdout("Iggy server is running")) - // Root credentials (edge server generates random password by default) + // Root credentials (the server generates a random password by default) .with_env_var("IGGY_ROOT_USERNAME", "iggy") .with_env_var("IGGY_ROOT_PASSWORD", "iggy") // Configure server to bind to 0.0.0.0 (accessible from host) From e0a78b7a6e964f4fc74ac5e94a7b4cc506f8d984 Mon Sep 17 00:00:00 2001 From: mlevkov Date: Fri, 3 Jul 2026 09:52:59 -0700 Subject: [PATCH 08/20] fix(resilience): make the reconnect and circuit-breaker paths reachable Review round 1, themes A, I, J (root cause confirmed independently by 4 of 8 agents and the verification pass): the wrapper's connection-error variants had zero producers, so reconnection, breaker failures, and the connected flag could never engage; SDK 0.10's default-on transport reconnection additionally swallows most mid-op failures into blocking retries. - classify_iggy_error(): map SDK connection variants (Disconnected, NotConnected, StaleClient, ClientShutdown, ConnectionClosed, TcpError, CannotEstablishConnection) into the wrapper's connection-aware errors at every network call site - health_check(): live ping bounded by the operation timeout, driving ConnectionState; the background health task now probes instead of reading a latched flag, keeping /health and /ready truthful - record circuit-breaker failure on first-attempt timeouts: with the SDK blocking internally, timeouts are the primary outage signal - reconnect(): shutdown() the old client before swapping (prevents the SDK 0.10 detached-heartbeat leak reviving zombie connections), reset the attempt counter per session (no permanent-failure latch), bound each connect attempt, saturating backoff arithmetic with clamped exponent, jitter applied before the max-delay cap - reconnect_bounded(): request-path reconnections and follower waits are now bounded by the operation timeout - with_reconnect(): duplicated 20-line retry block extracted into retry_once() - new(): initial connect bounded by the operation timeout (previously hung forever when the server was down, contradicting its docs) - ensure_stream/ensure_topic: lookup errors are no longer swallowed; losing a concurrent creation race (NameAlreadyExists) is treated as success instead of crash-looping the process - scopeguard: de-genericized (the value channel was dead weight) - module docs and CLAUDE.md now describe the two-layer resilience design (SDK transport reconnection + wrapper policy) accurately Tests: +9 (error classification, backoff bounds/overflow/floor/cap, scopeguard early-return); 139 lib + 24 integration green. --- CLAUDE.md | 8 + src/iggy_client/connection.rs | 6 + src/iggy_client/helpers.rs | 72 ++++++- src/iggy_client/mod.rs | 359 ++++++++++++++++++++++++---------- src/iggy_client/scopeguard.rs | 46 ++--- src/state.rs | 6 +- 6 files changed, 373 insertions(+), 124 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 655bfd4..87d5509 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -582,6 +582,14 @@ subscription iterator — neither maps onto stateless request/response semantics. The high-level clients are the right choice for dedicated pipeline workers; a protocol gateway belongs on the trait API. +Resilience is layered: the SDK's connection-string clients ship with +transport-level auto-reconnection (default on, unlimited retries), which +swallows most mid-operation connection failures into blocking retries. The +wrapper therefore treats **timeouts** as circuit-breaker failures, classifies +the SDK error variants that do escape (`classify_iggy_error`), and runs live +`ping` health probes so `/health` and `/ready` reflect reality during an +outage rather than a latched startup flag. + ## Structured Concurrency The application uses structured concurrency patterns for proper task lifecycle management. diff --git a/src/iggy_client/connection.rs b/src/iggy_client/connection.rs index 928ffa0..b624282 100644 --- a/src/iggy_client/connection.rs +++ b/src/iggy_client/connection.rs @@ -52,6 +52,12 @@ impl ConnectionState { self.reconnect_attempts.fetch_add(1, Ordering::SeqCst) + 1 } + /// Reset the attempt counter at the start of a reconnection session so a + /// previously exhausted session cannot make new sessions fail immediately. + pub fn reset_attempts(&self) { + self.reconnect_attempts.store(0, Ordering::SeqCst); + } + pub fn start_reconnecting(&self) -> bool { // Returns true if we successfully started reconnecting (wasn't already in progress) !self.reconnecting.swap(true, Ordering::SeqCst) diff --git a/src/iggy_client/helpers.rs b/src/iggy_client/helpers.rs index c0aecf1..2c5cd36 100644 --- a/src/iggy_client/helpers.rs +++ b/src/iggy_client/helpers.rs @@ -1,9 +1,31 @@ //! Helper functions for the Iggy client. -use iggy::prelude::Identifier; +use iggy::prelude::{Identifier, IggyError}; use crate::error::AppError; +/// Classify an SDK error into a connection-aware `AppError`. +/// +/// Connection-flavored `IggyError` variants map to the dedicated connection +/// variants so `IggyClientWrapper::with_reconnect` can trigger reconnection +/// and record circuit-breaker failures; everything else maps through +/// `fallback` (e.g. `AppError::SendError`). Without this classification the +/// reconnect path could never fire: stringifying every SDK error into an +/// operation error hides the connection failures from `is_connection_error`. +pub fn classify_iggy_error(error: IggyError, fallback: fn(String) -> AppError) -> AppError { + match error { + IggyError::Disconnected + | IggyError::NotConnected + | IggyError::StaleClient + | IggyError::ClientShutdown => AppError::Disconnected(error.to_string()), + IggyError::ConnectionClosed | IggyError::TcpError => { + AppError::ConnectionReset(error.to_string()) + } + IggyError::CannotEstablishConnection => AppError::ConnectionFailed(error.to_string()), + other => fallback(other.to_string()), + } +} + /// Convert a string to an Identifier, returning an appropriate error on failure. /// /// Iggy identifiers must be alphanumeric with optional dots, underscores, or hyphens. @@ -68,6 +90,54 @@ mod tests { assert!(result.is_err()); } + #[test] + fn test_classify_disconnected_variants() { + for error in [ + IggyError::Disconnected, + IggyError::NotConnected, + IggyError::StaleClient, + IggyError::ClientShutdown, + ] { + let classified = classify_iggy_error(error, AppError::SendError); + assert!( + matches!(classified, AppError::Disconnected(_)), + "expected Disconnected, got {:?}", + classified + ); + } + } + + #[test] + fn test_classify_connection_reset_variants() { + for error in [IggyError::ConnectionClosed, IggyError::TcpError] { + let classified = classify_iggy_error(error, AppError::SendError); + assert!( + matches!(classified, AppError::ConnectionReset(_)), + "expected ConnectionReset, got {:?}", + classified + ); + } + } + + #[test] + fn test_classify_cannot_establish_connection() { + let classified = + classify_iggy_error(IggyError::CannotEstablishConnection, AppError::SendError); + assert!(matches!(classified, AppError::ConnectionFailed(_))); + } + + #[test] + fn test_classify_non_connection_error_uses_fallback() { + let classified = classify_iggy_error( + IggyError::StreamNameAlreadyExists("test".to_string()), + AppError::StreamError, + ); + assert!(matches!(classified, AppError::StreamError(_))); + + let classified = classify_iggy_error(IggyError::InvalidMessagesCount, AppError::PollError); + assert!(matches!(classified, AppError::PollError(_))); + } + #[test] fn test_rand_jitter_returns_value_in_range() { for _ in 0..100 { diff --git a/src/iggy_client/mod.rs b/src/iggy_client/mod.rs index 7ecfd60..6809149 100644 --- a/src/iggy_client/mod.rs +++ b/src/iggy_client/mod.rs @@ -30,13 +30,19 @@ //! - `helpers` - Utility functions for identifier conversion and jitter //! - `scopeguard` - RAII guard for cleanup on drop //! -//! # Connection Resilience +//! # Connection Resilience (two layers) //! -//! The wrapper implements automatic reconnection with configurable: -//! - Maximum retry attempts (0 = infinite) -//! - Exponential backoff with jitter -//! - Maximum delay cap -//! - Per-operation timeout +//! The SDK itself provides transport-level reconnection for connection-string +//! clients (enabled by default, unlimited retries at ~1s intervals). During an +//! outage most operations therefore block inside the SDK rather than erroring, +//! and surface here as timeouts. On top of that, this wrapper adds: +//! - Per-operation timeouts (so requests never block unboundedly in the SDK) +//! - Circuit breaking driven by classified connection errors AND timeouts +//! - Live health probes (`health_check`) that keep the connection state, and +//! therefore `/health` and `/ready`, truthful +//! - App-level reconnection with exponential backoff + jitter as a second +//! line of defense, for error classes that escape the SDK's internal retry +//! (see `helpers::classify_iggy_error`) //! //! # Example //! @@ -70,7 +76,7 @@ use crate::models::Event; // Re-exports for public API pub use circuit_breaker::{CircuitBreaker, CircuitBreakerConfig, CircuitState}; pub use connection::ConnectionState; -pub use helpers::{rand_jitter, to_identifier}; +pub use helpers::{classify_iggy_error, rand_jitter, to_identifier}; pub use params::PollParams; // ============================================================================= @@ -89,6 +95,30 @@ const BACKOFF_JITTER_PERCENT: f64 = 0.2; /// overwhelming a recovering server. const MIN_RECONNECT_DELAY_MS: u64 = 100; +/// Maximum backoff exponent. `max_ms` caps the delay long before this in any +/// realistic configuration; the clamp only exists so the shift can never +/// overflow with an unbounded attempt counter. +const MAX_BACKOFF_EXPONENT: u32 = 32; + +/// Compute the reconnection delay for `attempt` (1-indexed): exponential +/// backoff from `base_ms`, jittered by ±[`BACKOFF_JITTER_PERCENT`], clamped to +/// `[MIN_RECONNECT_DELAY_MS, max_ms]`. +/// +/// `jitter_unit` is a random value in `[0, 1)` (see [`rand_jitter`]). The cap +/// is applied AFTER jitter so the delay never exceeds the configured maximum, +/// and all arithmetic saturates so an unbounded attempt counter (infinite +/// retries are the default) cannot overflow. +fn backoff_delay_ms(attempt: u32, base_ms: u64, max_ms: u64, jitter_unit: f64) -> u64 { + let exponent = attempt.saturating_sub(1).min(MAX_BACKOFF_EXPONENT); + let raw = base_ms.saturating_mul(2u64.saturating_pow(exponent)); + let capped = raw.min(max_ms); + + let jitter = (capped as f64 * BACKOFF_JITTER_PERCENT * (jitter_unit * 2.0 - 1.0)) as i64; + let jittered = (capped as i64).saturating_add(jitter).max(0) as u64; + + jittered.clamp(MIN_RECONNECT_DELAY_MS.min(max_ms), max_ms) +} + // ============================================================================= // IggyClientWrapper // ============================================================================= @@ -144,14 +174,16 @@ pub struct IggyClientWrapper { impl IggyClientWrapper { /// Create a new Iggy client wrapper from configuration. /// - /// Establishes initial connection to the Iggy server. If connection fails, - /// returns an error immediately (no automatic retry on initial connection). + /// Establishes the initial connection to the Iggy server, bounded by + /// `OPERATION_TIMEOUT_SECS`. The SDK's own transport-level reconnection + /// (enabled by default for connection-string clients) would otherwise + /// retry indefinitely and hang startup when the server is down. /// /// # Errors /// /// Returns `AppError::ConnectionFailed` if: /// - The connection string is invalid - /// - The server is unreachable + /// - The server is unreachable within the operation timeout /// - Authentication fails #[instrument(skip(config), fields(connection_string = %config.iggy_connection_string))] pub async fn new(config: Config) -> AppResult { @@ -174,7 +206,15 @@ impl IggyClientWrapper { circuit_breaker: Arc::new(CircuitBreaker::new(circuit_breaker_config)), }; - wrapper.connect().await?; + let timeout = wrapper.config.operation_timeout; + tokio::time::timeout(timeout, wrapper.connect()) + .await + .map_err(|_| { + AppError::ConnectionFailed(format!( + "Initial connection timed out after {:?}", + timeout + )) + })??; Ok(wrapper) } @@ -210,6 +250,31 @@ impl IggyClientWrapper { self.state.is_connected() } + /// Perform a live connectivity check against the Iggy server. + /// + /// Sends a `ping` bounded by the configured operation timeout and updates + /// the tracked connection state with the result. This is what keeps + /// `/health` and `/ready` truthful during an outage: the SDK's internal + /// transport reconnection swallows most mid-operation failures, so without + /// an active probe the connected flag would stay latched at its startup + /// value. + /// + /// Called periodically by the background health-check task; safe to call + /// from handlers as well. + pub async fn health_check(&self) -> bool { + let result = { + let client = self.client.read().await; + tokio::time::timeout(self.config.operation_timeout, client.ping()).await + }; + + let healthy = matches!(result, Ok(Ok(()))); + self.state.set_connected(healthy); + if !healthy { + debug!("Live health check failed: server did not answer ping in time"); + } + healthy + } + /// Attempt to reconnect to the Iggy server with exponential backoff. /// /// This method is called automatically when operations fail due to connection issues. @@ -244,11 +309,15 @@ impl IggyClientWrapper { } // Guard to ensure we always mark reconnection as complete - let _guard = scopeguard::guard((), |_| { + let _guard = scopeguard::guard(|| { self.state.stop_reconnecting(); }); self.state.set_connected(false); + // Start each reconnection session with a fresh attempt counter so a + // previously exhausted session cannot poison this one into failing + // immediately (and so the backoff exponent reflects THIS session). + self.state.reset_attempts(); let max_attempts = self.config.max_reconnect_attempts; loop { @@ -266,15 +335,12 @@ impl IggyClientWrapper { ))); } - // Calculate delay with exponential backoff and jitter - let base_delay = self.config.reconnect_base_delay.as_millis() as u64; - let delay_ms = (base_delay * 2u64.saturating_pow(attempt.saturating_sub(1))) - .min(self.config.reconnect_max_delay.as_millis() as u64); - - // Add jitter (±BACKOFF_JITTER_PERCENT) - let jitter = - (delay_ms as f64 * BACKOFF_JITTER_PERCENT * (rand_jitter() * 2.0 - 1.0)) as i64; - let final_delay = (delay_ms as i64 + jitter).max(MIN_RECONNECT_DELAY_MS as i64) as u64; + let final_delay = backoff_delay_ms( + attempt, + self.config.reconnect_base_delay.as_millis() as u64, + self.config.reconnect_max_delay.as_millis() as u64, + rand_jitter(), + ); warn!( attempt, @@ -287,15 +353,32 @@ impl IggyClientWrapper { // Create a new client instance for reconnection match IggyClient::from_connection_string(&self.config.iggy_connection_string) { Ok(new_client) => { - if let Err(e) = new_client.connect().await { - warn!(attempt, error = %e, "Reconnection attempt failed"); - continue; + // Bound the connect: the SDK's internal reconnection would + // otherwise retry inside connect() indefinitely. + match tokio::time::timeout(self.config.operation_timeout, new_client.connect()) + .await + { + Ok(Ok(())) => {} + Ok(Err(e)) => { + warn!(attempt, error = %e, "Reconnection attempt failed"); + continue; + } + Err(_) => { + warn!(attempt, "Reconnection attempt timed out"); + continue; + } } - // Successfully reconnected - update the client + // Successfully reconnected - swap the client and shut down + // the old one. Without shutdown() the old client's detached + // heartbeat task keeps running and can re-establish a + // zombie connection to the server (SDK 0.10 behavior). let mut client_guard = self.client.write().await; - *client_guard = new_client; + let old_client = std::mem::replace(&mut *client_guard, new_client); drop(client_guard); + if let Err(e) = old_client.shutdown().await { + debug!(error = %e, "Old client shutdown returned an error (ignored)"); + } self.state.set_connected(true); info!(attempt, "Successfully reconnected to Iggy server"); @@ -309,6 +392,25 @@ impl IggyClientWrapper { } } + /// Reconnect, bounded by the operation timeout. + /// + /// Used on the request path so a handler never hangs indefinitely behind + /// an unbounded reconnection session (the default configuration retries + /// forever). If the bound elapses, the reconnection attempt is aborted + /// (its scope guard releases the in-progress flag) and the next failing + /// request will start a fresh session. + async fn reconnect_bounded(&self) -> AppResult<()> { + let timeout = self.config.operation_timeout; + tokio::time::timeout(timeout, self.reconnect()) + .await + .map_err(|_| { + AppError::ConnectionFailed(format!( + "Reconnection did not complete within {:?}", + timeout + )) + })? + } + /// Execute an operation with automatic reconnection on connection failure. /// /// This is the core resilience mechanism. Features: @@ -361,70 +463,32 @@ impl IggyClientWrapper { Ok(Err(e)) if Self::is_connection_error(&e) => { self.circuit_breaker.record_failure().await; warn!(error = %e, "Operation failed due to connection error, attempting reconnect"); - self.reconnect().await?; - - // Retry with timeout - let retry_result = tokio::time::timeout(timeout_duration, operation()).await; - match retry_result { - Ok(Ok(value)) => { - self.circuit_breaker.record_success().await; - Ok(value) - } - Ok(Err(e)) => { - if Self::is_connection_error(&e) { - self.circuit_breaker.record_failure().await; - } - Err(e) - } - Err(_) => { - self.circuit_breaker.record_failure().await; - Err(AppError::OperationTimeout(format!( - "Operation timed out after {:?} on retry", - timeout_duration - ))) - } - } + self.reconnect_bounded().await?; + self.retry_once(&operation).await } Ok(Err(e)) => { // Non-connection error - don't record as circuit breaker failure Err(e) } Err(_) => { - // Timeout on first attempt - // Only reconnect if we have evidence the connection is actually lost. - // A timeout alone doesn't mean disconnection - could just be slow. + // Timeout on first attempt. The SDK's internal transport + // reconnection swallows most mid-operation connection failures + // into blocking retries, so a timeout is often the only outage + // signal we get - record it as a circuit-breaker failure. + self.circuit_breaker.record_failure().await; + + // Only reconnect if we have evidence the connection is + // actually lost (the background health check drives this + // flag via live pings). A timeout alone could just be a slow + // operation. if !self.state.is_connected() { - self.circuit_breaker.record_failure().await; warn!( timeout = ?timeout_duration, "Operation timed out and connection state is disconnected, attempting reconnect" ); - self.reconnect().await?; - - // Retry with timeout - let retry_result = tokio::time::timeout(timeout_duration, operation()).await; - match retry_result { - Ok(Ok(value)) => { - self.circuit_breaker.record_success().await; - Ok(value) - } - Ok(Err(e)) => { - if Self::is_connection_error(&e) { - self.circuit_breaker.record_failure().await; - } - Err(e) - } - Err(_) => { - self.circuit_breaker.record_failure().await; - Err(AppError::OperationTimeout(format!( - "Operation timed out after {:?} on retry", - timeout_duration - ))) - } - } + self.reconnect_bounded().await?; + self.retry_once(&operation).await } else { - // Connection appears healthy - this is just a slow operation - // Don't record as circuit breaker failure (not a connection issue) debug!( timeout = ?timeout_duration, "Operation timed out but connection state is healthy, not reconnecting" @@ -438,6 +502,35 @@ impl IggyClientWrapper { } } + /// Single post-reconnect retry with timeout and circuit-breaker + /// bookkeeping. Shared by both reconnect paths of [`Self::with_reconnect`]. + async fn retry_once(&self, operation: &F) -> AppResult + where + F: Fn() -> Fut, + Fut: std::future::Future>, + { + let timeout_duration = self.config.operation_timeout; + match tokio::time::timeout(timeout_duration, operation()).await { + Ok(Ok(value)) => { + self.circuit_breaker.record_success().await; + Ok(value) + } + Ok(Err(e)) => { + if Self::is_connection_error(&e) { + self.circuit_breaker.record_failure().await; + } + Err(e) + } + Err(_) => { + self.circuit_breaker.record_failure().await; + Err(AppError::OperationTimeout(format!( + "Operation timed out after {:?} on retry", + timeout_duration + ))) + } + } + } + /// Check if an error is a connection-related error that warrants reconnection. /// /// Uses explicit pattern matching on error variants rather than string matching, @@ -471,14 +564,24 @@ impl IggyClientWrapper { debug!(stream = name, "Stream already exists"); Ok(()) } - Ok(None) | Err(_) => { + Ok(None) => { info!(stream = name, "Creating stream"); - client - .create_stream(name) - .await - .map_err(|e| AppError::StreamError(e.to_string()))?; - Ok(()) + match client.create_stream(name).await { + Ok(_) => Ok(()), + // Lost a creation race (e.g. two replicas starting + // simultaneously) - the stream exists, which is all + // this method guarantees. + Err(IggyError::StreamNameAlreadyExists(_)) => { + debug!(stream = name, "Stream was created concurrently"); + Ok(()) + } + Err(e) => Err(classify_iggy_error(e, AppError::StreamError)), + } } + // Don't swallow the lookup error: a transient or permission + // failure here is not "stream missing", and blindly creating + // would surface a misleading already-exists error instead. + Err(e) => Err(classify_iggy_error(e, AppError::StreamError)), } }) .await @@ -500,9 +603,9 @@ impl IggyClientWrapper { debug!(stream, topic, "Topic already exists"); Ok(()) } - Ok(None) | Err(_) => { + Ok(None) => { info!(stream, topic, partitions, "Creating topic"); - client + match client .create_topic( &stream_id, topic, @@ -513,9 +616,19 @@ impl IggyClientWrapper { MaxTopicSize::Unlimited, ) .await - .map_err(|e| AppError::TopicError(e.to_string()))?; - Ok(()) + { + Ok(_) => Ok(()), + // Lost a creation race - the topic exists, which is + // all this method guarantees. + Err(IggyError::TopicNameAlreadyExists(_, _)) => { + debug!(stream, topic, "Topic was created concurrently"); + Ok(()) + } + Err(e) => Err(classify_iggy_error(e, AppError::TopicError)), + } } + // Don't swallow the lookup error (see ensure_stream). + Err(e) => Err(classify_iggy_error(e, AppError::TopicError)), } }) .await @@ -583,7 +696,7 @@ impl IggyClientWrapper { client .send_messages(&stream_id, &topic_id, &partitioning, &mut messages) .await - .map_err(|e| AppError::SendError(e.to_string()))?; + .map_err(|e| classify_iggy_error(e, AppError::SendError))?; debug!(event_id = %event.id, "Event sent successfully"); Ok(()) @@ -662,7 +775,7 @@ impl IggyClientWrapper { client .send_messages(&stream_id, &topic_id, &partitioning, &mut messages) .await - .map_err(|e| AppError::SendError(e.to_string()))?; + .map_err(|e| classify_iggy_error(e, AppError::SendError))?; debug!(batch_size = events.len(), "Batch sent successfully"); Ok(()) @@ -743,7 +856,7 @@ impl IggyClientWrapper { params.auto_commit, ) .await - .map_err(|e| AppError::PollError(e.to_string()))?; + .map_err(|e| classify_iggy_error(e, AppError::PollError))?; debug!( count = messages.messages.len(), @@ -779,7 +892,7 @@ impl IggyClientWrapper { client .get_stream(&stream_id) .await - .map_err(|e| AppError::StreamError(e.to_string()))? + .map_err(|e| classify_iggy_error(e, AppError::StreamError))? .ok_or_else(|| AppError::NotFound(format!("Stream '{}' not found", name))) }) .await @@ -796,7 +909,7 @@ impl IggyClientWrapper { client .get_topic(&stream_id, &topic_id) .await - .map_err(|e| AppError::TopicError(e.to_string()))? + .map_err(|e| classify_iggy_error(e, AppError::TopicError))? .ok_or_else(|| { AppError::NotFound(format!( "Topic '{}' in stream '{}' not found", @@ -816,7 +929,7 @@ impl IggyClientWrapper { client .get_streams() .await - .map_err(|e| AppError::StreamError(e.to_string())) + .map_err(|e| classify_iggy_error(e, AppError::StreamError)) }) .await } @@ -831,7 +944,7 @@ impl IggyClientWrapper { client .get_topics(&stream_id) .await - .map_err(|e| AppError::TopicError(e.to_string())) + .map_err(|e| classify_iggy_error(e, AppError::TopicError)) }) .await } @@ -845,7 +958,7 @@ impl IggyClientWrapper { client .create_stream(name) .await - .map_err(|e| AppError::StreamError(e.to_string()))?; + .map_err(|e| classify_iggy_error(e, AppError::StreamError))?; info!(stream = name, "Stream created"); Ok(()) @@ -871,7 +984,7 @@ impl IggyClientWrapper { MaxTopicSize::Unlimited, ) .await - .map_err(|e| AppError::TopicError(e.to_string()))?; + .map_err(|e| classify_iggy_error(e, AppError::TopicError))?; info!(stream, topic, partitions, "Topic created"); Ok(()) @@ -891,7 +1004,7 @@ impl IggyClientWrapper { client .delete_stream(&stream_id) .await - .map_err(|e| AppError::StreamError(e.to_string()))?; + .map_err(|e| classify_iggy_error(e, AppError::StreamError))?; warn!(stream = name, "Stream deleted"); Ok(()) @@ -912,7 +1025,7 @@ impl IggyClientWrapper { client .delete_topic(&stream_id, &topic_id) .await - .map_err(|e| AppError::TopicError(e.to_string()))?; + .map_err(|e| classify_iggy_error(e, AppError::TopicError))?; warn!(stream, topic, "Topic deleted"); Ok(()) @@ -964,6 +1077,54 @@ impl IggyClientWrapper { mod tests { use super::*; + #[test] + fn test_backoff_first_attempt_is_base_delay() { + // Zero jitter offset happens at jitter_unit = 0.5 + assert_eq!(backoff_delay_ms(1, 1000, 30_000, 0.5), 1000); + } + + #[test] + fn test_backoff_grows_exponentially() { + assert_eq!(backoff_delay_ms(2, 1000, 30_000, 0.5), 2000); + assert_eq!(backoff_delay_ms(3, 1000, 30_000, 0.5), 4000); + assert_eq!(backoff_delay_ms(4, 1000, 30_000, 0.5), 8000); + } + + #[test] + fn test_backoff_never_exceeds_max_even_with_max_jitter() { + // jitter_unit ~1.0 gives the maximum positive jitter; the cap is + // applied after jitter so the delay must not exceed max_ms. + for attempt in 1..=100 { + let delay = backoff_delay_ms(attempt, 1000, 30_000, 0.999_999); + assert!( + delay <= 30_000, + "attempt {} produced delay {} > max", + attempt, + delay + ); + } + } + + #[test] + fn test_backoff_respects_minimum_floor() { + // Tiny base with maximum negative jitter must not go below the floor. + let delay = backoff_delay_ms(1, 1, 30_000, 0.0); + assert!(delay >= 100, "delay {} below MIN_RECONNECT_DELAY_MS", delay); + } + + #[test] + fn test_backoff_does_not_overflow_at_huge_attempt_counts() { + // Infinite retries are the default; attempt counts far past the + // overflow point (~attempt 56 with base 1000) must stay clamped. + for attempt in [56, 64, 1000, u32::MAX] { + let delay = backoff_delay_ms(attempt, 1000, 30_000, 0.999_999); + assert!( + (100..=30_000).contains(&delay), + "attempt {attempt} -> {delay}" + ); + } + } + #[test] fn test_is_connection_error_connection_failed() { let error = AppError::ConnectionFailed("test".to_string()); diff --git a/src/iggy_client/scopeguard.rs b/src/iggy_client/scopeguard.rs index 73f1faa..85384cc 100644 --- a/src/iggy_client/scopeguard.rs +++ b/src/iggy_client/scopeguard.rs @@ -1,38 +1,38 @@ //! Scope guard for cleanup on drop. //! //! This is a minimal implementation to avoid adding the `scopeguard` crate -//! as a dependency for a single use case. +//! as a dependency for a single use case. The `Drop`-based guard is +//! load-bearing in `reconnect()`: it releases the reconnection-in-progress +//! flag on every exit path, including early returns and future cancellation. /// A guard that executes a closure when dropped. -pub struct ScopeGuard { - value: Option, - dropper: Option, +pub struct ScopeGuard { + callback: Option, } -impl Drop for ScopeGuard { +impl Drop for ScopeGuard { fn drop(&mut self) { - if let (Some(value), Some(dropper)) = (self.value.take(), self.dropper.take()) { - dropper(value); + if let Some(callback) = self.callback.take() { + callback(); } } } -/// Create a scope guard that will execute `dropper` with `value` when dropped. +/// Create a scope guard that will execute `callback` when dropped. /// /// # Example /// /// ```rust,ignore /// let flag = Arc::new(AtomicBool::new(true)); /// let flag_clone = flag.clone(); -/// let _guard = guard((), move |_| { +/// let _guard = guard(move || { /// flag_clone.store(false, Ordering::SeqCst); /// }); /// // When _guard goes out of scope, the flag will be set to false /// ``` -pub fn guard(value: T, dropper: F) -> ScopeGuard { +pub fn guard(callback: F) -> ScopeGuard { ScopeGuard { - value: Some(value), - dropper: Some(dropper), + callback: Some(callback), } } @@ -40,7 +40,7 @@ pub fn guard(value: T, dropper: F) -> ScopeGuard { mod tests { use super::*; use std::sync::Arc; - use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + use std::sync::atomic::{AtomicBool, Ordering}; #[test] fn test_scopeguard_executes_on_drop() { @@ -48,28 +48,28 @@ mod tests { let executed_clone = executed.clone(); { - let _guard = guard((), move |_| { + let _guard = guard(move || { executed_clone.store(true, Ordering::SeqCst); }); } assert!( executed.load(Ordering::SeqCst), - "dropper should have executed" + "callback should have executed" ); } #[test] - fn test_scopeguard_passes_value_to_dropper() { - let received_value = Arc::new(AtomicUsize::new(0)); - let received_clone = received_value.clone(); - - { - let _guard = guard(42usize, move |v| { - received_clone.store(v, Ordering::SeqCst); + fn test_scopeguard_executes_on_early_return() { + fn early_return(flag: Arc) -> u32 { + let _guard = guard(move || { + flag.store(true, Ordering::SeqCst); }); + 42 // guard drops here } - assert_eq!(received_value.load(Ordering::SeqCst), 42); + let executed = Arc::new(AtomicBool::new(false)); + assert_eq!(early_return(executed.clone()), 42); + assert!(executed.load(Ordering::SeqCst)); } } diff --git a/src/state.rs b/src/state.rs index 98f79cc..6d40403 100644 --- a/src/state.rs +++ b/src/state.rs @@ -232,7 +232,11 @@ impl AppState { break; } _ = ticker.tick() => { - let connected = iggy_client.is_connected(); + // Live ping (not just a flag read): the SDK's internal + // transport reconnection hides most mid-operation + // failures, so this probe is what keeps the connection + // state - and therefore /health and /ready - truthful. + let connected = iggy_client.health_check().await; if !connected { warn!("Health check: Iggy connection is down"); } else { From 5998dfea96a900ed782a404f8f6a618d45d77832 Mon Sep 17 00:00:00 2001 From: mlevkov Date: Fri, 3 Jul 2026 18:39:20 -0700 Subject: [PATCH 09/20] fix(security): enforce trusted-proxy validation and stop throttling valid clients Review round 1, theme H (silent-failure-hunter H3/H4/H5, type-design F2, confirmed by verification pass): - TRUSTED_PROXIES is now actually enforced: forwarded headers are only honored when the direct peer (via ConnectInfo) is inside a trusted range; untrusted peers are keyed by their real peer address, so rotating spoofed X-Forwarded-For values no longer bypasses rate limiting or brute-force tracking. Previously is_trusted() had zero call sites and the documented validation was a silent no-op. - invalid TRUSTED_PROXIES entries fail startup with the new RateLimitError::InvalidTrustedProxyCidr instead of silently degrading to trust-all on a typo - auth brute-force limiter now meters FAILURES only: the old code consumed a token on every request before validation, capping valid-key clients at ~10 req/min/IP (and the whole service when clients shared the 'unknown' bucket); the limiter also now uses trusted-proxy-aware IP extraction - server started with into_make_service_with_connect_info (main and test harness) so the peer address is available to the middleware - invalid CORS_ALLOWED_ORIGINS entries are logged instead of silently dropped; an all-invalid (fail-closed) list warns loudly - serve-error path now runs state.shutdown() so background tasks are awaited on both exit paths Tests: +6 (trusted/untrusted peer, missing connect-info fallback, valid-key-never-throttled, failure-budget throttling with valid-key recovery); 145 lib + 24 integration green. --- CLAUDE.md | 16 ++- src/main.rs | 25 +++-- src/middleware/auth.rs | 193 ++++++++++++++++++++++++++--------- src/middleware/ip.rs | 137 ++++++++++++++++++++----- src/middleware/rate_limit.rs | 65 ++++++++---- src/routes.rs | 36 ++++++- tests/integration_tests.rs | 26 +++-- 7 files changed, 376 insertions(+), 122 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 87d5509..e4d2a2c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -240,8 +240,13 @@ Environment variables (see `.env.example`): #### Trusted Proxy Configuration -The `TRUSTED_PROXIES` variable configures IP spoofing mitigation for the rate limiter. -When set, X-Forwarded-For headers are validated against trusted proxy networks. +The `TRUSTED_PROXIES` variable configures IP spoofing mitigation for both the +rate limiter and the auth brute-force limiter. When set, forwarded headers +(`X-Forwarded-For`/`X-Real-IP`) are only honored if the direct peer address is +inside a trusted range; requests from untrusted peers are keyed by their actual +peer address. Invalid entries fail startup +(`RateLimitError::InvalidTrustedProxyCidr`) instead of silently degrading to +trust-all. **Format**: Comma-separated CIDR notation @@ -516,7 +521,8 @@ Error types and HTTP status codes: ```rust pub enum RateLimitError { - ZeroRps, // RPS cannot be 0; use disabled() instead + ZeroRps, // RPS cannot be 0; use disabled() instead + InvalidTrustedProxyCidr(String), // Unparseable TRUSTED_PROXIES entry } ``` @@ -688,7 +694,9 @@ Request → Rate Limit → Auth → Request ID → Timeout → Tracing → CORS ### API Key Authentication (`src/middleware/auth.rs`) - Constant-time comparison to prevent timing attacks -- Per-IP brute force protection via shared `extract_client_ip()` function +- Per-IP brute force protection that meters authentication FAILURES only — + valid-key requests never consume from the failure budget +- Honors `TRUSTED_PROXIES` for spoofing-resistant IP extraction - Accepts key via `X-API-Key` header or `api_key` query parameter - Bypasses `/health` and `/ready` for health checks (exact path matching) diff --git a/src/main.rs b/src/main.rs index 9f4bda3..82f0f91 100644 --- a/src/main.rs +++ b/src/main.rs @@ -93,19 +93,26 @@ async fn run() -> Result<(), exitcode::ExitCode> { info!(" GET /streams/{{name}} - Get stream info"); info!(" DELETE /streams/{{name}} - Delete stream"); - // Start server with graceful shutdown - axum::serve(listener, app) - .with_graceful_shutdown(utils::shutdown_signal()) - .await - .map_err(|e| { - error!("Server error: {e}"); - exitcode::SOFTWARE - })?; + // Start server with graceful shutdown. ConnectInfo exposes the peer + // address to the middleware stack, which TRUSTED_PROXIES enforcement + // needs to decide whether forwarded headers can be honored. + let serve_result = axum::serve( + listener, + app.into_make_service_with_connect_info::(), + ) + .with_graceful_shutdown(utils::shutdown_signal()) + .await; - // Gracefully shutdown background tasks + // Gracefully shutdown background tasks on BOTH exit paths - a serve + // error must not leave the stats/health tasks running un-awaited. info!("HTTP server stopped, shutting down background tasks..."); state.shutdown().await; + serve_result.map_err(|e| { + error!("Server error: {e}"); + exitcode::SOFTWARE + })?; + info!("Server shutdown complete"); Ok(()) } diff --git a/src/middleware/auth.rs b/src/middleware/auth.rs index 42aa5be..a5bddc0 100644 --- a/src/middleware/auth.rs +++ b/src/middleware/auth.rs @@ -46,7 +46,8 @@ use subtle::ConstantTimeEq; use tower::{Layer, Service}; use tracing::{debug, error, warn}; -use super::ip::extract_client_ip; +use super::ip::extract_client_ip_with_validation; +use super::rate_limit::TrustedProxyConfig; /// Header name for API key. pub const API_KEY_HEADER: &str = "x-api-key"; @@ -95,9 +96,10 @@ type AuthFailureLimiter = RateLimiter, De /// /// # Brute Force Protection /// -/// Includes per-IP rate limiting for authentication failures. After too many -/// failed attempts, further requests from that IP are temporarily blocked -/// even before validating the API key. +/// Includes per-IP rate limiting of authentication FAILURES only: valid-key +/// requests never consume from the failure budget, so legitimate clients are +/// unaffected. Once an IP exhausts its failure budget, further failing +/// requests receive `429 Too Many Requests` until the window refills. #[derive(Clone)] pub struct ApiKeyAuth { /// Expected API key (None = auth disabled) @@ -106,6 +108,8 @@ pub struct ApiKeyAuth { bypass_paths: Arc>, /// Rate limiter for tracking auth failures per IP failure_limiter: Option>, + /// Trusted proxy configuration for spoofing-resistant IP extraction + trusted_proxies: Arc, } impl ApiKeyAuth { @@ -116,6 +120,24 @@ impl ApiKeyAuth { /// * `api_key` - Expected API key, or `None` to disable authentication /// * `bypass_paths` - Paths that bypass authentication (e.g., health endpoints) pub fn new(api_key: Option, bypass_paths: Vec) -> Self { + Self::with_trusted_proxies( + api_key, + bypass_paths, + Arc::new(TrustedProxyConfig::default()), + ) + } + + /// Create a new API key auth layer with trusted-proxy validation for the + /// per-IP brute-force limiter. + /// + /// With trusted proxies configured, forwarded headers are only honored + /// when the direct peer is inside a trusted range, so attackers cannot + /// rotate spoofed `X-Forwarded-For` values to escape failure tracking. + pub fn with_trusted_proxies( + api_key: Option, + bypass_paths: Vec, + trusted_proxies: Arc, + ) -> Self { let failure_limiter = if api_key.is_some() { // Only create rate limiter when auth is enabled let quota = Quota::per_minute(DEFAULT_AUTH_FAILURE_LIMIT) @@ -129,6 +151,7 @@ impl ApiKeyAuth { expected_key: api_key.map(Arc::new), bypass_paths: Arc::new(bypass_paths), failure_limiter, + trusted_proxies, } } @@ -158,6 +181,7 @@ impl Layer for ApiKeyAuth { expected_key: self.expected_key.clone(), bypass_paths: self.bypass_paths.clone(), failure_limiter: self.failure_limiter.clone(), + trusted_proxies: self.trusted_proxies.clone(), } } } @@ -169,6 +193,7 @@ pub struct ApiKeyAuthService { expected_key: Option>, bypass_paths: Arc>, failure_limiter: Option>, + trusted_proxies: Arc, } impl Service> for ApiKeyAuthService @@ -190,6 +215,7 @@ where let expected_key = self.expected_key.clone(); let bypass_paths = self.bypass_paths.clone(); let failure_limiter = self.failure_limiter.clone(); + let trusted_proxies = self.trusted_proxies.clone(); let mut inner = self.inner.clone(); Box::pin(async move { @@ -206,65 +232,59 @@ where return inner.call(req).await; } - // Extract client IP for failure tracking (convert to owned String for rate limiter) - let client_ip = extract_client_ip(&req).into_owned(); - - // Check if this IP is blocked due to too many failures - if let Some(ref limiter) = failure_limiter { - // We use check_key in reverse: if it fails, the IP has too many failures - // The rate limiter tracks "tokens consumed", so we check available capacity - if let Err(not_until) = limiter.check_key(&client_ip) { - let wait_time = - not_until.wait_time_from(governor::clock::DefaultClock::default().now()); - let retry_after = wait_time.as_secs().max(1); - - error!( - client_ip = %client_ip, - retry_after_secs = retry_after, - "IP blocked due to excessive auth failures" - ); - - return Ok(rate_limited_response(retry_after)); - } - } - - // Extract API key from request + // Validate the key FIRST. The failure limiter only meters + // FAILURES: consuming a token on every request would throttle + // legitimate clients down to the failure budget (~10 req/min/IP), + // and all direct clients share the "unknown" bucket. let provided_key = extract_api_key(&req); match provided_key { Some(extracted) if constant_time_eq(&extracted.key, &expected) => { - // Valid API key - proceed + // Valid API key - proceed without touching the limiter debug!( from_query = extracted.from_query, "API key authentication successful" ); inner.call(req).await } - Some(_) => { - // Invalid API key - record failure - if let Some(ref limiter) = failure_limiter { - // Consume a token for this failure - let _ = limiter.check_key(&client_ip); + provided => { + // Auth failure: consume one failure token for this IP. + // Once the failure budget is exhausted, respond 429 so + // brute-force attempts are throttled. + let client_ip = + extract_client_ip_with_validation(&req, &trusted_proxies).into_owned(); + + if let Some(ref limiter) = failure_limiter + && let Err(not_until) = limiter.check_key(&client_ip) + { + let wait_time = not_until + .wait_time_from(governor::clock::DefaultClock::default().now()); + let retry_after = wait_time.as_secs().max(1); + + error!( + client_ip = %client_ip, + retry_after_secs = retry_after, + "IP blocked due to excessive auth failures" + ); + + return Ok(rate_limited_response(retry_after)); } - warn!( - path = %req.uri().path(), - client_ip = %client_ip, - "Invalid API key provided" - ); - Ok(unauthorized_response("Invalid API key")) - } - None => { - // No API key provided - record failure - if let Some(ref limiter) = failure_limiter { - // Consume a token for this failure - let _ = limiter.check_key(&client_ip); + + if provided.is_some() { + warn!( + path = %req.uri().path(), + client_ip = %client_ip, + "Invalid API key provided" + ); + Ok(unauthorized_response("Invalid API key")) + } else { + warn!( + path = %req.uri().path(), + client_ip = %client_ip, + "Missing API key" + ); + Ok(unauthorized_response("API key required")) } - warn!( - path = %req.uri().path(), - client_ip = %client_ip, - "Missing API key" - ); - Ok(unauthorized_response("API key required")) } } }) @@ -372,6 +392,77 @@ fn rate_limited_response(retry_after: u64) -> Response { mod tests { use super::*; + /// Minimal inner service returning 200 OK, for driving the auth layer. + #[derive(Clone)] + struct OkService; + + impl Service> for OkService { + type Response = Response; + type Error = std::convert::Infallible; + type Future = std::future::Ready>; + + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, _req: Request) -> Self::Future { + std::future::ready(Ok(StatusCode::OK.into_response())) + } + } + + fn request_with_key(key: Option<&str>) -> Request { + let mut builder = Request::builder().uri("/stats"); + if let Some(k) = key { + builder = builder.header(API_KEY_HEADER, k); + } + builder.body(Body::empty()).unwrap() + } + + #[tokio::test] + async fn test_valid_key_requests_never_consume_failure_budget() { + let auth = ApiKeyAuth::with_defaults(Some("secret".to_string())); + let mut svc = auth.layer(OkService); + + // Far more valid requests than the failure budget (10/min + burst 5). + // A regression to counting every request would 429 partway through. + for i in 0..40 { + let resp = svc.call(request_with_key(Some("secret"))).await.unwrap(); + assert_eq!( + resp.status(), + StatusCode::OK, + "valid-key request {} was throttled by the failure limiter", + i + ); + } + } + + #[tokio::test] + async fn test_auth_failures_throttled_after_budget_exhausted() { + let auth = ApiKeyAuth::with_defaults(Some("secret".to_string())); + let mut svc = auth.layer(OkService); + + // All requests share the "unknown" IP bucket (no proxy headers). + // Failures should 401 until the budget is exhausted, then 429. + let mut saw_429 = false; + for _ in 0..40 { + let resp = svc.call(request_with_key(Some("wrong"))).await.unwrap(); + match resp.status() { + StatusCode::UNAUTHORIZED => {} + StatusCode::TOO_MANY_REQUESTS => { + saw_429 = true; + break; + } + other => panic!("unexpected status {other}"), + } + } + assert!(saw_429, "failure budget was never exhausted"); + + // A VALID key from the same (blocked) bucket still succeeds: only + // failures are blocked, legitimate clients are unaffected. + let resp = svc.call(request_with_key(Some("secret"))).await.unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + } + #[test] fn test_api_key_auth_enabled() { let auth = ApiKeyAuth::with_defaults(Some("secret".to_string())); diff --git a/src/middleware/ip.rs b/src/middleware/ip.rs index 3020178..d708512 100644 --- a/src/middleware/ip.rs +++ b/src/middleware/ip.rs @@ -58,10 +58,14 @@ //! //! ## Trusted Proxy Validation //! -//! When `TrustedProxyConfig` is provided with CIDR ranges, the extraction -//! functions log debug information to help detect potential spoofing. Note that -//! full connection IP validation requires Axum's `ConnectInfo` extension, which -//! is not available at the middleware level. +//! When `TrustedProxyConfig` is configured with CIDR ranges, forwarded headers +//! are only honored if the direct peer (from Axum's `ConnectInfo` extension) +//! is inside a trusted range; otherwise the peer address itself is used as the +//! client IP, so spoofed `X-Forwarded-For`/`X-Real-IP` headers from untrusted +//! sources are ignored. This requires the server to be started with +//! `into_make_service_with_connect_info::()` (both `main.rs` and +//! the integration-test harness do this). If `ConnectInfo` is absent, the +//! functions fall back to header extraction and log a warning. //! //! # Internal Architecture //! @@ -89,9 +93,11 @@ //! - Both public functions return `Cow<'static, str>` for consistent zero-allocation fallback use std::borrow::Cow; +use std::net::SocketAddr; +use axum::extract::ConnectInfo; use axum::http::Request; -use tracing::debug; +use tracing::{debug, warn}; use super::rate_limit::TrustedProxyConfig; @@ -206,32 +212,49 @@ pub fn extract_client_ip_with_validation( req: &Request, trusted_proxies: &TrustedProxyConfig, ) -> Cow<'static, str> { - match extract_ip_from_headers(req) { - ExtractedIp::FromXff(ip) => { - if trusted_proxies.is_enabled() { - debug!( - client_ip = %ip, - "Extracted client IP from X-Forwarded-For (trusted proxy validation enabled)" - ); + if trusted_proxies.is_enabled() { + match req.extensions().get::>() { + Some(ConnectInfo(peer)) => { + let peer_ip = peer.ip(); + if trusted_proxies.is_trusted_ip(&peer_ip) { + // Peer is a trusted proxy: honor the forwarded headers it + // set, falling back to the peer address itself when the + // proxy sent none. + return match extract_ip_from_headers(req) { + ExtractedIp::FromXff(ip) | ExtractedIp::FromRealIp(ip) => { + Cow::Owned(ip.to_string()) + } + ExtractedIp::NotFound => Cow::Owned(peer_ip.to_string()), + }; + } + // Peer is NOT a trusted proxy: forwarded headers are + // client-controlled and spoofable, so ignore them and key on + // the actual peer address. + if !matches!(extract_ip_from_headers(req), ExtractedIp::NotFound) { + debug!( + peer_ip = %peer_ip, + "Ignoring forwarded headers from untrusted peer" + ); + } + return Cow::Owned(peer_ip.to_string()); } - Cow::Owned(ip.to_string()) - } - ExtractedIp::FromRealIp(ip) => { - if trusted_proxies.is_enabled() { - debug!( - client_ip = %ip, - "Extracted client IP from X-Real-IP (trusted proxy validation enabled)" + None => { + // TRUSTED_PROXIES is configured but the server was started + // without connect-info, so enforcement is impossible. Fall + // back to header trust and say so loudly. + warn!( + "TRUSTED_PROXIES is set but the peer address is unavailable \ + (server not started with into_make_service_with_connect_info); \ + falling back to trusting forwarded headers" ); } - Cow::Owned(ip.to_string()) - } - ExtractedIp::NotFound => { - if trusted_proxies.is_enabled() { - debug!("No proxy headers found - request may be bypassing reverse proxy"); - } - Cow::Borrowed(UNKNOWN_IP) } } + + match extract_ip_from_headers(req) { + ExtractedIp::FromXff(ip) | ExtractedIp::FromRealIp(ip) => Cow::Owned(ip.to_string()), + ExtractedIp::NotFound => Cow::Borrowed(UNKNOWN_IP), + } } /// Extract client IP from request headers (simple version without validation logging). @@ -374,6 +397,68 @@ mod tests { assert!(matches!(ip, Cow::Borrowed(_))); } + // ========================================================================== + // Trusted Proxy Enforcement Tests + // ========================================================================== + + fn req_with_peer(xff: Option<&str>, peer: [u8; 4]) -> Request { + let mut builder = Request::builder(); + if let Some(v) = xff { + builder = builder.header("x-forwarded-for", v); + } + let mut req = builder.body(Body::empty()).unwrap(); + req.extensions_mut() + .insert(ConnectInfo(SocketAddr::from((peer, 12345)))); + req + } + + #[test] + fn test_validation_trusted_peer_honors_forwarded_header() { + let trusted = TrustedProxyConfig::try_new(&["10.0.0.0/8".to_string()]).unwrap(); + let req = req_with_peer(Some("203.0.113.50"), [10, 0, 0, 5]); + + assert_eq!( + extract_client_ip_with_validation(&req, &trusted), + "203.0.113.50" + ); + } + + #[test] + fn test_validation_untrusted_peer_ignores_forwarded_header() { + let trusted = TrustedProxyConfig::try_new(&["10.0.0.0/8".to_string()]).unwrap(); + // Peer outside the trusted range spoofs X-Forwarded-For + let req = req_with_peer(Some("1.2.3.4"), [203, 0, 113, 9]); + + // The spoofed header must be ignored; the peer address is the client + assert_eq!( + extract_client_ip_with_validation(&req, &trusted), + "203.0.113.9" + ); + } + + #[test] + fn test_validation_trusted_peer_without_header_uses_peer_ip() { + let trusted = TrustedProxyConfig::try_new(&["10.0.0.0/8".to_string()]).unwrap(); + let req = req_with_peer(None, [10, 0, 0, 5]); + + assert_eq!( + extract_client_ip_with_validation(&req, &trusted), + "10.0.0.5" + ); + } + + #[test] + fn test_validation_missing_connect_info_falls_back_to_headers() { + let trusted = TrustedProxyConfig::try_new(&["10.0.0.0/8".to_string()]).unwrap(); + // No ConnectInfo extension (server started without connect-info) + let req = Request::builder() + .header("x-forwarded-for", "1.2.3.4") + .body(Body::empty()) + .unwrap(); + + assert_eq!(extract_client_ip_with_validation(&req, &trusted), "1.2.3.4"); + } + // ========================================================================== // Edge Case Tests // ========================================================================== diff --git a/src/middleware/rate_limit.rs b/src/middleware/rate_limit.rs index ef4a1b8..1924c84 100644 --- a/src/middleware/rate_limit.rs +++ b/src/middleware/rate_limit.rs @@ -50,13 +50,16 @@ use governor::{Quota, RateLimiter}; use tower::{Layer, Service}; /// Error type for rate limit layer configuration. -/// -/// This is a simple enum with no data, so it derives `Copy` for efficient -/// pass-by-value semantics without cloning overhead. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum RateLimitError { /// RPS value cannot be zero. ZeroRps, + /// A TRUSTED_PROXIES entry could not be parsed as an IP or CIDR range. + /// + /// Startup fails instead of silently skipping the entry: a typo in the + /// trusted-proxy list would otherwise degrade to trusting spoofable + /// forwarded headers from everyone. + InvalidTrustedProxyCidr(String), } impl fmt::Display for RateLimitError { @@ -68,6 +71,13 @@ impl fmt::Display for RateLimitError { "RPS must be greater than 0; use disabled() for no limiting" ) } + RateLimitError::InvalidTrustedProxyCidr(entry) => { + write!( + f, + "Invalid TRUSTED_PROXIES entry '{}': expected an IP address or CIDR range (e.g. 10.0.0.0/8)", + entry + ) + } } } } @@ -179,20 +189,22 @@ pub struct TrustedProxyConfig { impl TrustedProxyConfig { /// Create a new trusted proxy configuration from CIDR strings. /// - /// Invalid CIDR strings are logged as warnings and skipped. - pub fn new(cidrs: &[String]) -> Self { + /// # Errors + /// + /// Returns [`RateLimitError::InvalidTrustedProxyCidr`] on the first entry + /// that fails to parse. Failing fast at startup is deliberate: silently + /// skipping a typo'd entry would degrade to trusting spoofable forwarded + /// headers from everyone (or, with a partial list, from unintended peers). + pub fn try_new(cidrs: &[String]) -> Result { let ranges: Vec = cidrs .iter() - .filter_map(|cidr| { - let parsed = CidrRange::parse(cidr); - if parsed.is_none() { - warn!(cidr = %cidr, "Invalid CIDR range in TRUSTED_PROXIES, skipping"); - } - parsed + .map(|cidr| { + CidrRange::parse(cidr) + .ok_or_else(|| RateLimitError::InvalidTrustedProxyCidr(cidr.clone())) }) - .collect(); + .collect::>()?; - if !cidrs.is_empty() && !ranges.is_empty() { + if !ranges.is_empty() { debug!( count = ranges.len(), "Trusted proxy validation enabled with {} CIDR ranges", @@ -200,7 +212,7 @@ impl TrustedProxyConfig { ); } - Self { ranges } + Ok(Self { ranges }) } /// Check if trusted proxy validation is enabled (any ranges configured). @@ -227,8 +239,18 @@ impl TrustedProxyConfig { } }; - // Check against all configured ranges - self.ranges.iter().any(|range| range.contains(&ip)) + self.is_trusted_ip(&ip) + } + + /// Check if an already-parsed IP address is from a trusted proxy. + /// + /// Returns `true` if the IP matches any configured CIDR range, + /// or if no ranges are configured (trust all mode). + pub fn is_trusted_ip(&self, ip: &IpAddr) -> bool { + if self.ranges.is_empty() { + return true; + } + self.ranges.iter().any(|range| range.contains(ip)) } } @@ -301,8 +323,8 @@ impl RateLimitLayer { // Create keyed rate limiter with custom hasher for efficiency let limiter = RateLimiter::keyed(quota); - // Parse trusted proxy configuration - let proxy_config = TrustedProxyConfig::new(trusted_proxies); + // Parse trusted proxy configuration (fails fast on invalid entries) + let proxy_config = TrustedProxyConfig::try_new(trusted_proxies)?; Ok(Self { limiter: Arc::new(limiter), @@ -475,7 +497,7 @@ mod tests { #[test] fn test_trusted_proxy_config_empty() { - let config = TrustedProxyConfig::new(&[]); + let config = TrustedProxyConfig::try_new(&[]).unwrap(); assert!(!config.is_enabled()); // Empty config trusts all assert!(config.is_trusted("1.2.3.4")); @@ -485,7 +507,8 @@ mod tests { #[test] fn test_trusted_proxy_config_with_ranges() { let config = - TrustedProxyConfig::new(&["10.0.0.0/8".to_string(), "172.16.0.0/12".to_string()]); + TrustedProxyConfig::try_new(&["10.0.0.0/8".to_string(), "172.16.0.0/12".to_string()]) + .unwrap(); assert!(config.is_enabled()); // Should trust IPs in configured ranges diff --git a/src/routes.rs b/src/routes.rs index b4afc76..b63447c 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -41,17 +41,20 @@ //! - `/streams` - Stream management //! - `/streams/{stream}/topics` - Topic management +use std::sync::Arc; + use axum::Router; use axum::extract::DefaultBodyLimit; use axum::middleware; use axum::routing::{delete, get, post}; use tower_http::cors::{Any, CorsLayer}; use tower_http::trace::TraceLayer; -use tracing::info; +use tracing::{info, warn}; use crate::handlers; use crate::middleware::{ - ApiKeyAuth, RateLimitError, RateLimitLayer, RequestIdLayer, extract_request_timeout, + ApiKeyAuth, RateLimitError, RateLimitLayer, RequestIdLayer, TrustedProxyConfig, + extract_request_timeout, }; use crate::state::AppState; @@ -143,8 +146,17 @@ pub fn build_router(state: AppState) -> Result { // 5. Request ID router = router.layer(RequestIdLayer::new()); + // Trusted proxy configuration is shared by auth (brute-force tracking) + // and rate limiting; invalid entries fail startup rather than silently + // degrading to trust-all. + let trusted_proxies = Arc::new(TrustedProxyConfig::try_new(&config.trusted_proxies)?); + // 6. Authentication (if enabled) - let auth_layer = ApiKeyAuth::new(config.api_key.clone(), config.auth_bypass_paths.clone()); + let auth_layer = ApiKeyAuth::with_trusted_proxies( + config.api_key.clone(), + config.auth_bypass_paths.clone(), + trusted_proxies.clone(), + ); if auth_layer.is_enabled() { info!("API key authentication enabled"); router = router.layer(auth_layer); @@ -194,12 +206,26 @@ fn build_cors_layer(allowed_origins: &[String]) -> CorsLayer { .allow_methods(Any) .allow_headers(Any) } else { - // Parse specific origins + // Parse specific origins; dropped entries are surfaced instead of + // silently vanishing (an all-invalid list fails closed - no origins + // allowed - which would otherwise look like a mystery CORS failure). let origins: Vec<_> = allowed_origins .iter() - .filter_map(|o| o.parse().ok()) + .filter_map(|o| match o.parse() { + Ok(origin) => Some(origin), + Err(_) => { + warn!(origin = %o, "Ignoring invalid CORS_ALLOWED_ORIGINS entry"); + None + } + }) .collect(); + if origins.is_empty() { + warn!( + "CORS_ALLOWED_ORIGINS contained no valid origins; all cross-origin requests will be rejected" + ); + } + CorsLayer::new() .allow_origin(origins) .allow_methods(Any) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 489a104..2cd31a5 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -197,9 +197,14 @@ impl TestFixture { .await .map_err(|e| format!("Failed to bind server: {}", e))?; - axum::serve(listener, app) - .await - .map_err(|e| format!("Server failed: {}", e))?; + // Match production: ConnectInfo exposes the peer address for + // TRUSTED_PROXIES enforcement in the middleware stack. + axum::serve( + listener, + app.into_make_service_with_connect_info::(), + ) + .await + .map_err(|e| format!("Server failed: {}", e))?; Ok(()) } @@ -1101,9 +1106,14 @@ impl SecureTestFixture { .await .map_err(|e| format!("Failed to bind server: {}", e))?; - axum::serve(listener, app) - .await - .map_err(|e| format!("Server failed: {}", e))?; + // Match production: ConnectInfo exposes the peer address for + // TRUSTED_PROXIES enforcement in the middleware stack. + axum::serve( + listener, + app.into_make_service_with_connect_info::(), + ) + .await + .map_err(|e| format!("Server failed: {}", e))?; Ok(()) } @@ -1230,6 +1240,10 @@ async fn test_auth_invalid_api_key() { ); } +// Note: valid-key-never-throttled and failure-budget-throttling semantics are +// covered by deterministic unit tests in src/middleware/auth.rs (the secure +// fixture's general rate limiter (5 RPS) would dominate a wire-level test). + #[tokio::test] async fn test_health_bypasses_auth() { let fixture = SecureTestFixture::new().await; From ae1eb967eb4f370d66337ff3ba4ef55c98b9d273 Mon Sep 17 00:00:00 2001 From: mlevkov Date: Fri, 3 Jul 2026 18:42:33 -0700 Subject: [PATCH 10/20] fix(observability): actually start and wire the Prometheus metrics Review round 1, theme G (silent-failure-hunter H2, consistency #9): the entire metrics module was dead code - the exporter was never installed, no record_* function had a call site, and Prometheus scraped a port the app never served. - main.rs: install the Prometheus exporter on METRICS_PORT at startup; a bind failure now fails startup (silently missing metrics would defeat alerting), METRICS_PORT=0 disables it explicitly - wire the natural call sites: message send/poll counters in the services, reconnect attempts, circuit-breaker opens/rejections and state gauge, connection-status gauge from the live health probe - prometheus.yml: scrape app:9090 (the METRICS_PORT listener) instead of app:8000 where no /metrics route exists; compose now sets METRICS_PORT=9090 and maps it to host 9091 (9090 is Prometheus) - circuit breaker: failures recorded while already Open no longer refresh opened_at (stragglers were extending the open window and delaying recovery) [architect finding 9] - producer/consumer services: send/send_batch/poll now delegate to their *_to/_from variants (removes duplicated counter+response blocks) [simplifier finding 4] --- docker-compose.yaml | 4 +++ observability/prometheus/prometheus.yml | 5 ++- src/iggy_client/circuit_breaker.rs | 13 +++++-- src/iggy_client/mod.rs | 2 ++ src/main.rs | 17 +++++++++ src/services/consumer.rs | 19 +++------- src/services/producer.rs | 47 +++++-------------------- 7 files changed, 48 insertions(+), 59 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 9e31302..9be14b6 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -46,9 +46,13 @@ services: container_name: iggy-sample-app ports: - "8000:8000" + # App Prometheus metrics (host 9091; 9090 is Prometheus itself) + - "9091:9090" environment: - HOST=0.0.0.0 - PORT=8000 + # Dedicated Prometheus metrics listener (scraped as app:9090) + - METRICS_PORT=9090 - IGGY_CONNECTION_STRING=iggy://iggy:iggy@iggy:8090 - IGGY_STREAM=sample-stream - IGGY_TOPIC=events diff --git a/observability/prometheus/prometheus.yml b/observability/prometheus/prometheus.yml index 9484eb0..30fe7a2 100644 --- a/observability/prometheus/prometheus.yml +++ b/observability/prometheus/prometheus.yml @@ -11,13 +11,12 @@ scrape_configs: metrics_path: '/metrics' scheme: 'http' - # Sample application metrics (if exposed) + # Sample application metrics (dedicated exporter listener on METRICS_PORT) - job_name: 'iggy-sample-app' static_configs: - - targets: ['app:8000'] + - targets: ['app:9090'] metrics_path: '/metrics' scheme: 'http' - # Don't fail if app doesn't expose metrics scrape_timeout: 5s # Prometheus self-monitoring diff --git a/src/iggy_client/circuit_breaker.rs b/src/iggy_client/circuit_breaker.rs index 1cf681d..a3de1e2 100644 --- a/src/iggy_client/circuit_breaker.rs +++ b/src/iggy_client/circuit_breaker.rs @@ -191,6 +191,7 @@ impl CircuitBreaker { && opened_at.elapsed() < self.config.open_duration { self.requests_rejected.fetch_add(1, Ordering::Relaxed); + crate::metrics::record_circuit_breaker_rejection(); return false; } // Timeout expired - need to transition to half-open @@ -208,10 +209,12 @@ impl CircuitBreaker { { state.state = CircuitState::HalfOpen; state.consecutive_successes = 0; + crate::metrics::set_circuit_breaker_state(1); info!("Circuit breaker transitioning from Open to HalfOpen"); return true; } self.requests_rejected.fetch_add(1, Ordering::Relaxed); + crate::metrics::record_circuit_breaker_rejection(); return false; } @@ -241,6 +244,7 @@ impl CircuitBreaker { state.state = CircuitState::Closed; state.opened_at = None; state.consecutive_failures = 0; + crate::metrics::set_circuit_breaker_state(0); info!("Circuit breaker closed after successful recovery"); } } @@ -271,6 +275,8 @@ impl CircuitBreaker { state.state = CircuitState::Open; state.opened_at = Some(Instant::now()); self.times_opened.fetch_add(1, Ordering::Relaxed); + crate::metrics::record_circuit_breaker_open(); + crate::metrics::set_circuit_breaker_state(2); warn!( failures = state.consecutive_failures, open_duration = ?self.config.open_duration, @@ -284,11 +290,14 @@ impl CircuitBreaker { state.opened_at = Some(Instant::now()); state.consecutive_successes = 0; self.times_opened.fetch_add(1, Ordering::Relaxed); + crate::metrics::record_circuit_breaker_open(); + crate::metrics::set_circuit_breaker_state(2); warn!("Circuit breaker reopened after failure in HalfOpen state"); } CircuitState::Open => { - // Already open, refresh the timer - state.opened_at = Some(Instant::now()); + // Already open. Deliberately do NOT refresh opened_at: + // straggler failures from in-flight requests would otherwise + // extend the open window indefinitely and delay recovery. } } } diff --git a/src/iggy_client/mod.rs b/src/iggy_client/mod.rs index 6809149..e9b01a2 100644 --- a/src/iggy_client/mod.rs +++ b/src/iggy_client/mod.rs @@ -269,6 +269,7 @@ impl IggyClientWrapper { let healthy = matches!(result, Ok(Ok(()))); self.state.set_connected(healthy); + crate::metrics::set_connection_status(healthy); if !healthy { debug!("Live health check failed: server did not answer ping in time"); } @@ -322,6 +323,7 @@ impl IggyClientWrapper { loop { let attempt = self.state.increment_attempts(); + crate::metrics::record_reconnect_attempt(); // Check if we've exceeded max attempts (0 = infinite) if max_attempts > 0 && attempt > max_attempts { diff --git a/src/main.rs b/src/main.rs index 82f0f91..89a8b57 100644 --- a/src/main.rs +++ b/src/main.rs @@ -63,6 +63,23 @@ async fn run() -> Result<(), exitcode::ExitCode> { config.default_stream, config.default_topic ); + // Start the Prometheus metrics exporter (dedicated listener). A bind + // failure fails startup: silently missing metrics would defeat alerting. + if config.metrics_port > 0 { + let metrics_addr: SocketAddr = format!("{}:{}", config.host, config.metrics_port) + .parse() + .map_err(|e| { + error!("Invalid metrics address: {e}"); + exitcode::CONFIG + })?; + iggy_sample::metrics::init_metrics(metrics_addr).map_err(|e| { + error!("Failed to start metrics exporter: {e}"); + exitcode::UNAVAILABLE + })?; + } else { + info!("Metrics exporter disabled (METRICS_PORT=0)"); + } + // Build application state and router let state = AppState::new(iggy_client, config.clone()); let app = build_router(state.clone()).map_err(|e| { diff --git a/src/services/consumer.rs b/src/services/consumer.rs index 6d00abc..e33fce0 100644 --- a/src/services/consumer.rs +++ b/src/services/consumer.rs @@ -55,21 +55,9 @@ impl ConsumerService { /// * `params` - Polling parameters (partition, consumer, offset, count, auto_commit) #[instrument(skip(self, params), fields(partition_id = params.partition_id, consumer_id = params.consumer_id))] pub async fn poll(&self, params: PollParams) -> AppResult { - let partition_id = params.partition_id; - let polled = self.client.poll_messages_default(params).await?; - - let messages = self.parse_messages(&polled.messages); - let message_count = messages.len(); - - self.messages_consumed - .fetch_add(message_count as u64, Ordering::Relaxed); - - Ok(PollMessagesResponse { - messages, - count: message_count, - partition_id, - current_offset: polled.current_offset, - }) + let stream = self.client.default_stream().to_string(); + let topic = self.client.default_topic().to_string(); + self.poll_from(&stream, &topic, params).await } /// Poll messages from a specific stream and topic. @@ -88,6 +76,7 @@ impl ConsumerService { self.messages_consumed .fetch_add(message_count as u64, Ordering::Relaxed); + crate::metrics::record_messages_polled(stream, topic, message_count as u64); Ok(PollMessagesResponse { messages, diff --git a/src/services/producer.rs b/src/services/producer.rs index 93edce8..a783059 100644 --- a/src/services/producer.rs +++ b/src/services/producer.rs @@ -43,17 +43,9 @@ impl ProducerService { event: &Event, partition_key: Option<&str>, ) -> AppResult { - self.client.send_event_default(event, partition_key).await?; - - self.messages_sent.fetch_add(1, Ordering::Relaxed); - - Ok(SendMessageResponse { - success: true, - event_id: event.id, - stream: self.client.default_stream().to_string(), - topic: self.client.default_topic().to_string(), - timestamp: Utc::now(), - }) + let stream = self.client.default_stream().to_string(); + let topic = self.client.default_topic().to_string(); + self.send_to(&stream, &topic, event, partition_key).await } /// Send an event to a specific stream and topic. @@ -70,6 +62,7 @@ impl ProducerService { .await?; self.messages_sent.fetch_add(1, Ordering::Relaxed); + crate::metrics::record_message_sent(stream, topic, "success"); Ok(SendMessageResponse { success: true, @@ -88,35 +81,10 @@ impl ProducerService { events: &[Event], partition_key: Option<&str>, ) -> AppResult> { - // Use true batch sending - single network call for all messages - self.client - .send_events_batch_default(events, partition_key) - .await?; - - self.messages_sent - .fetch_add(events.len() as u64, Ordering::Relaxed); - - let timestamp = Utc::now(); let stream = self.client.default_stream().to_string(); let topic = self.client.default_topic().to_string(); - - // Build responses for each event - let responses = events - .iter() - .map(|event| SendMessageResponse { - success: true, - event_id: event.id, - stream: stream.clone(), - topic: topic.clone(), - timestamp, - }) - .collect(); - - info!( - "Sent batch of {} events in single network call", - events.len() - ); - Ok(responses) + self.send_batch_to(&stream, &topic, events, partition_key) + .await } /// Send multiple events in a batch to a specific stream and topic. @@ -134,6 +102,7 @@ impl ProducerService { self.messages_sent .fetch_add(events.len() as u64, Ordering::Relaxed); + crate::metrics::record_messages_sent_batch(stream, topic, "success", events.len() as u64); let timestamp = Utc::now(); // Allocate stream/topic once outside the loop to avoid per-event allocation @@ -152,7 +121,7 @@ impl ProducerService { .collect(); info!( - "Sent batch of {} events to {}/{}", + "Sent batch of {} events to {}/{} in a single network call", events.len(), stream, topic From 49b1ae0fee64c7d4b965e762c2bf8d87862b0429 Mon Sep 17 00:00:00 2001 From: mlevkov Date: Fri, 3 Jul 2026 18:46:02 -0700 Subject: [PATCH 11/20] fix(api): correct API-boundary semantics flagged in review Review round 1, theme K (type-design F3/F4/F7, silent-failure M5): - GET /messages?count=0 now returns 400 via validate_poll_count instead of the misleading 500 the SDK's InvalidMessagesCount mapped to (the count survived .min(max_count) untouched) - to_identifier() uses Identifier::named explicitly: str::try_into reinterpreted all-digit names ('42') as numeric server-assigned IDs, silently targeting a different resource - dangerous on DELETE; the docstring no longer claims the SDK enforces the charset invariant (that lives in validation.rs) - removed PollMessagesRequest: dead API surface consumed only by tests, documenting a 'None = all partitions' capability nothing implements - test fixture: a server-task panic now fails fast with a clear message instead of being masked as a generic 60-attempt timeout (oneshot Closed is now distinguished from Empty) Tests: +3 (all-digit-name identity, poll-count validation); suite now 147 lib + 18 model + 24 integration, all green. --- src/handlers/messages.rs | 5 ++++- src/iggy_client/helpers.rs | 32 +++++++++++++++++++++++++------- src/models/api.rs | 37 ------------------------------------- src/models/mod.rs | 5 ++--- src/validation.rs | 26 ++++++++++++++++++++++++++ tests/integration_tests.rs | 14 ++++++++++++-- tests/model_tests.rs | 32 +------------------------------- 7 files changed, 70 insertions(+), 81 deletions(-) diff --git a/src/handlers/messages.rs b/src/handlers/messages.rs index 8b89f6c..9388470 100644 --- a/src/handlers/messages.rs +++ b/src/handlers/messages.rs @@ -24,7 +24,8 @@ use crate::iggy_client::PollParams; use crate::models::{Event, PollMessagesResponse, SendMessageRequest, SendMessageResponse}; use crate::state::AppState; use crate::validation::{ - validate_consumer_id, validate_event_type, validate_partition_id, validate_resource_name, + validate_consumer_id, validate_event_type, validate_partition_id, validate_poll_count, + validate_resource_name, }; /// Send a single message to the default stream/topic. @@ -174,6 +175,7 @@ pub async fn poll_messages( // Validate poll parameters validate_partition_id(query.partition_id)?; validate_consumer_id(query.consumer_id)?; + validate_poll_count(query.count)?; let max_count = state.config.poll_max_count; let count = query.count.min(max_count); @@ -251,6 +253,7 @@ pub async fn poll_messages_from( // Validate poll parameters validate_partition_id(query.partition_id)?; validate_consumer_id(query.consumer_id)?; + validate_poll_count(query.count)?; let max_count = state.config.poll_max_count; let count = query.count.min(max_count); diff --git a/src/iggy_client/helpers.rs b/src/iggy_client/helpers.rs index 2c5cd36..32c764b 100644 --- a/src/iggy_client/helpers.rs +++ b/src/iggy_client/helpers.rs @@ -26,13 +26,19 @@ pub fn classify_iggy_error(error: IggyError, fallback: fn(String) -> AppError) - } } -/// Convert a string to an Identifier, returning an appropriate error on failure. +/// Convert a resource name to a name-based (string) Identifier. /// -/// Iggy identifiers must be alphanumeric with optional dots, underscores, or hyphens. -/// This function provides clear error messages when validation fails, including the -/// original error from the Iggy SDK for debugging. +/// Uses `Identifier::named` explicitly rather than `str::try_into`: the +/// `FromStr` conversion reinterprets any all-digit string (e.g. `"42"`) as a +/// NUMERIC server-assigned ID, which would silently target a different +/// resource than the name the caller asked for. All lookups in this service +/// are name-based. +/// +/// The SDK only enforces the 1-255 byte length here; the alphanumeric charset +/// invariant is enforced separately by `validation::validate_resource_name` +/// at the HTTP boundary. pub fn to_identifier(name: &str, resource_type: &str) -> Result { - name.try_into().map_err(|e: iggy::prelude::IggyError| { + Identifier::named(name).map_err(|e: IggyError| { // Log the original error for debugging while providing a user-friendly message tracing::debug!( resource_type, @@ -41,8 +47,7 @@ pub fn to_identifier(name: &str, resource_type: &str) -> Result, } -/// Request to poll messages from a topic. -#[derive(Debug, Deserialize)] -pub struct PollMessagesRequest { - /// Consumer ID for tracking offsets - #[serde(default = "default_consumer_id")] - pub consumer_id: u32, - /// Partition to poll from (None = all partitions) - pub partition_id: Option, - /// Starting offset (None = from last committed offset) - pub offset: Option, - /// Maximum number of messages to return - #[serde(default = "default_message_count")] - pub count: u32, - /// Whether to auto-commit the offset after polling - #[serde(default)] - pub auto_commit: bool, -} - -fn default_consumer_id() -> u32 { - 1 -} - -fn default_message_count() -> u32 { - 10 -} - /// Response containing polled messages. #[derive(Debug, Serialize)] pub struct PollMessagesResponse { @@ -199,17 +173,6 @@ mod tests { assert_eq!(request.partitions, 1); } - #[test] - fn test_poll_messages_request_defaults() { - let json = r#"{}"#; - let request: PollMessagesRequest = - serde_json::from_str(json).expect("Deserialization should succeed"); - - assert_eq!(request.consumer_id, 1); - assert_eq!(request.count, 10); - assert!(!request.auto_commit); - } - #[test] fn test_send_message_response_serialization() { let response = SendMessageResponse { diff --git a/src/models/mod.rs b/src/models/mod.rs index dbb36ec..5e4e465 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -2,8 +2,7 @@ mod api; mod event; pub use api::{ - CreateStreamRequest, CreateTopicRequest, HealthResponse, PollMessagesRequest, - PollMessagesResponse, ReceivedMessage, SendMessageRequest, SendMessageResponse, StatsResponse, - StreamInfo, TopicInfo, + CreateStreamRequest, CreateTopicRequest, HealthResponse, PollMessagesResponse, ReceivedMessage, + SendMessageRequest, SendMessageResponse, StatsResponse, StreamInfo, TopicInfo, }; pub use event::{Event, EventPayload, OrderEvent, OrderItem, OrderStatus, UserEvent}; diff --git a/src/validation.rs b/src/validation.rs index 795b5ca..c6b5f3d 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -180,11 +180,37 @@ pub fn validate_consumer_id(consumer_id: u32) -> AppResult<()> { Ok(()) } +/// Validate a poll message count. +/// +/// The Iggy server rejects `count == 0` (`InvalidMessagesCount`); validating +/// at the HTTP boundary turns that client mistake into a 400 instead of the +/// misleading 500 the SDK error would map to. +pub fn validate_poll_count(count: u32) -> AppResult<()> { + if count == 0 { + return Err(AppError::BadRequest("count must be at least 1".to_string())); + } + Ok(()) +} + #[cfg(test)] #[allow(clippy::unwrap_used, clippy::expect_used)] mod tests { use super::*; + #[test] + fn test_poll_count_zero_rejected() { + let result = validate_poll_count(0); + assert!(result.is_err()); + assert!(matches!(result, Err(AppError::BadRequest(_)))); + } + + #[test] + fn test_poll_count_positive_accepted() { + assert!(validate_poll_count(1).is_ok()); + assert!(validate_poll_count(100).is_ok()); + assert!(validate_poll_count(u32::MAX).is_ok()); + } + #[test] fn test_valid_names() { assert!(validate_resource_name("my-stream", "Stream").is_ok()); diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 2cd31a5..bc3b45f 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -220,8 +220,18 @@ impl TestFixture { for attempt in 1..=max_attempts { // Check if server task failed - if let Ok(Err(e)) = error_rx.try_recv() { - panic!("Server failed to start: {}", e); + match error_rx.try_recv() { + Ok(Err(e)) => panic!("Server failed to start: {}", e), + Err(tokio::sync::oneshot::error::TryRecvError::Closed) => { + // The server task dropped its sender without reporting an + // error: it panicked or returned early. Surfacing this + // immediately beats 30s of polling a dead server followed + // by a generic timeout message that hides the real cause. + panic!( + "Server task exited unexpectedly (likely panicked) before becoming ready" + ); + } + Ok(Ok(())) | Err(tokio::sync::oneshot::error::TryRecvError::Empty) => {} } match client.get(&health_url).send().await { diff --git a/tests/model_tests.rs b/tests/model_tests.rs index cf2662a..8ce5de7 100644 --- a/tests/model_tests.rs +++ b/tests/model_tests.rs @@ -217,7 +217,7 @@ mod event_tests { mod api_tests { use super::*; use iggy_sample::models::{ - CreateStreamRequest, CreateTopicRequest, HealthResponse, PollMessagesRequest, StatsResponse, + CreateStreamRequest, CreateTopicRequest, HealthResponse, StatsResponse, }; #[test] @@ -246,36 +246,6 @@ mod api_tests { assert_eq!(request.partitions, 5); } - #[test] - fn test_poll_messages_request_defaults() { - let json = r#"{}"#; - let request: PollMessagesRequest = - serde_json::from_str(json).expect("Deserialization failed"); - assert_eq!(request.consumer_id, 1); - assert_eq!(request.count, 10); - assert!(!request.auto_commit); - assert!(request.partition_id.is_none()); - assert!(request.offset.is_none()); - } - - #[test] - fn test_poll_messages_request_custom() { - let json = r#"{ - "consumer_id": 5, - "partition_id": 2, - "offset": 100, - "count": 50, - "auto_commit": true - }"#; - let request: PollMessagesRequest = - serde_json::from_str(json).expect("Deserialization failed"); - assert_eq!(request.consumer_id, 5); - assert_eq!(request.partition_id, Some(2)); - assert_eq!(request.offset, Some(100)); - assert_eq!(request.count, 50); - assert!(request.auto_commit); - } - #[test] fn test_health_response_serialization() { let response = HealthResponse { From 61b40cae02380422f6ba2b358b7164eaa6fb34b4 Mon Sep 17 00:00:00 2001 From: mlevkov Date: Fri, 3 Jul 2026 18:50:18 -0700 Subject: [PATCH 12/20] test(integration): pin SDK 0.10 semantics; docs: TD registry and count sync Review round 1, themes L and triage closure: - new integration test pinning PollingStrategy::next + auto_commit: a second offset-less poll must not re-deliver committed messages (the biggest unpinned SDK semantic in the 0.10 bump) - new integration test pinning Partitioning::messages_key_str: all messages sharing a key land in exactly one partition - docs/tech-debt/ registry created with six records (TD-2026-07-01 through -06), each with a binding trigger, covering the review's deferred findings: with_reconnect test matrix, DiagnosticEvents integration, half-open probe limiting, X-Request-Timeout enforcement, metrics smoke test, durable-storage re-validation - CLAUDE.md: X-Request-Timeout section now states honestly that the header is parsed but not yet enforced (TD-2026-07-04) - test counts synced everywhere: 147 lib / 26 integration / 18 model - CHANGELOG: Fixed section summarizing the review remediation --- CHANGELOG.md | 27 +++++++ CLAUDE.md | 9 ++- README.md | 2 +- docs/tech-debt/README.md | 13 ++++ docs/tech-debt/TD-2026-07-01.md | 30 ++++++++ docs/tech-debt/TD-2026-07-02.md | 29 ++++++++ docs/tech-debt/TD-2026-07-03.md | 17 +++++ docs/tech-debt/TD-2026-07-04.md | 20 ++++++ docs/tech-debt/TD-2026-07-05.md | 18 +++++ docs/tech-debt/TD-2026-07-06.md | 16 +++++ tests/integration_tests.rs | 120 ++++++++++++++++++++++++++++++++ 11 files changed, 297 insertions(+), 4 deletions(-) create mode 100644 docs/tech-debt/README.md create mode 100644 docs/tech-debt/TD-2026-07-01.md create mode 100644 docs/tech-debt/TD-2026-07-02.md create mode 100644 docs/tech-debt/TD-2026-07-03.md create mode 100644 docs/tech-debt/TD-2026-07-04.md create mode 100644 docs/tech-debt/TD-2026-07-05.md create mode 100644 docs/tech-debt/TD-2026-07-06.md diff --git a/CHANGELOG.md b/CHANGELOG.md index d811c99..d9c91e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 licenses-only and non-blocking); weekly stress tests pin `apache/iggy:0.8.0` instead of `latest` +### Fixed + +Findings from the session-01 eight-agent double review +(`docs/code-reviews/`); deferred items carry tech-debt records with binding +triggers (`docs/tech-debt/`): + +- **Resilience**: SDK connection errors are now classified into the + wrapper's connection-aware variants, making the reconnect and + circuit-breaker paths reachable (previously dead code); the background + health check performs live pings so `/health` and `/ready` stay truthful + during outages; reconnection no longer leaks the old client's heartbeat + task, resets its attempt counter per session, uses saturating backoff + arithmetic, and is bounded on the request path; `ensure_stream/topic` no + longer swallow lookup errors and tolerate losing a concurrent creation + race instead of crash-looping +- **Security**: `TRUSTED_PROXIES` is enforced against the actual peer + address (spoofed forwarded headers from untrusted peers are ignored) and + invalid entries fail startup; the auth brute-force limiter meters + failures only, so valid-key clients are no longer throttled to the + failure budget +- **Observability**: the Prometheus exporter is now actually started on + `METRICS_PORT` and the message/reconnect/breaker metrics are recorded; + Prometheus scrapes the correct port +- **API**: `count=0` polls return 400 instead of 500; all-digit resource + names ("42") are treated as names, not numeric server IDs; removed the + dead `PollMessagesRequest` type + ### Added - **Observability Stack**: Complete Grafana-based monitoring setup diff --git a/CLAUDE.md b/CLAUDE.md index e4d2a2c..0e5f1c9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -420,13 +420,13 @@ environment: ### Running Tests ```bash -# Unit tests (130 tests) +# Unit tests (147 tests) cargo test --lib -# Integration tests (24 tests, requires Docker for testcontainers) +# Integration tests (26 tests, requires Docker for testcontainers) cargo test --test integration_tests -# Model tests (20 tests) +# Model tests (18 tests) cargo test --test model_tests # All tests @@ -705,6 +705,9 @@ Request → Rate Limit → Auth → Request ID → Timeout → Tracing → CORS - Bounded: 100ms minimum, 5 minutes maximum - Stored in request extensions for handler use - `RequestTimeoutExt` trait for easy extraction in handlers +- **Note**: currently parsed and stored only — no handler consumes it yet, so + all Iggy operations are bounded by the global `OPERATION_TIMEOUT_SECS`. + Enforcement is tracked in `docs/tech-debt/TD-2026-07-04.md`. ## Deployment Security diff --git a/README.md b/README.md index 7c01b84..c4fefb0 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Apache Iggy is capable of processing millions of messages per second with ultra- ### Development & Testing - Docker Compose setup for local development -- Comprehensive test suite (130 unit tests, 24 integration tests, 20 model tests) +- Comprehensive test suite (147 unit tests, 26 integration tests, 18 model tests) - Integration tests with testcontainers (auto-spins Iggy server) - Fuzz testing for input validation functions diff --git a/docs/tech-debt/README.md b/docs/tech-debt/README.md new file mode 100644 index 0000000..751ea0e --- /dev/null +++ b/docs/tech-debt/README.md @@ -0,0 +1,13 @@ +# Tech Debt Registry + +Deferred findings from code reviews, each with a binding trigger — the +condition under which the record MUST be resolved (not "someday"). + +| ID | Title | Source | Trigger | +|----|-------|--------|---------| +| [TD-2026-07-01](TD-2026-07-01.md) | `with_reconnect` composition test matrix | Review session 01 (tests #2) | Any behavioral change to `with_reconnect`/`retry_once` | +| [TD-2026-07-02](TD-2026-07-02.md) | DiagnosticEvents-driven connection state | Review session 01 (architect #2) | Next iggy SDK minor bump | +| [TD-2026-07-03](TD-2026-07-03.md) | Half-open probe limiting | Review session 01 (architect #9, types F5) | First production incident involving breaker recovery, or breaker config exposure | +| [TD-2026-07-04](TD-2026-07-04.md) | X-Request-Timeout enforcement | Review session 01 (silentfail M4) | Before advertising the header in any client-facing docs beyond CLAUDE.md | +| [TD-2026-07-05](TD-2026-07-05.md) | Metrics exporter smoke test | Review session 01 (tests #7) | Next metrics-exporter-prometheus major/minor bump | +| [TD-2026-07-06](TD-2026-07-06.md) | Durable-storage guide config re-validation | Review session 01 (consistency #10) | Next server image bump past 0.8.x | diff --git a/docs/tech-debt/TD-2026-07-01.md b/docs/tech-debt/TD-2026-07-01.md new file mode 100644 index 0000000..405c8a2 --- /dev/null +++ b/docs/tech-debt/TD-2026-07-01.md @@ -0,0 +1,30 @@ +# TD-2026-07-01: `with_reconnect` composition test matrix + +**Source:** Review session 01, Round 1 — pr-test-analyzer finding #2 (HIGH). +**Status:** open + +## Problem + +`IggyClientWrapper::with_reconnect` + `retry_once` (src/iggy_client/mod.rs) +compose timeout, circuit breaker, and reconnection. The parts are unit-tested +in isolation (breaker state machine, ConnectionState, error classifier, +backoff bounds), but no test exercises the composition: breaker-open +fast-fail, timeout-while-connected (no reconnect), classified +connection-error → reconnect + single retry, non-connection error → breaker +untouched. + +## Why deferred + +`IggyClientWrapper::new()` requires a live connection, so the composition +cannot be constructed against a fake operation without either a refactor +(extract the retry/breaker/timeout logic parameterized by the operation) or +a test-only constructor. That refactor deserves its own change, not a rider +on the dependency-refresh branch. The session-01 branch did add live +integration coverage of the happy paths and the semantic pins +(next/auto-commit, partition routing). + +## Binding trigger + +Any behavioral change to `with_reconnect`, `retry_once`, or +`reconnect_bounded` MUST land together with this matrix (tokio +`time::pause()`-based, one test per branch). diff --git a/docs/tech-debt/TD-2026-07-02.md b/docs/tech-debt/TD-2026-07-02.md new file mode 100644 index 0000000..417812b --- /dev/null +++ b/docs/tech-debt/TD-2026-07-02.md @@ -0,0 +1,29 @@ +# TD-2026-07-02: DiagnosticEvents-driven connection state + +**Source:** Review session 01, Round 1 — code-architect finding #2 (HIGH theme). +**Status:** open (mitigated) + +## Problem + +SDK 0.10's connection-string clients ship transport-level auto-reconnection +that cannot be disabled (`enabled: true` is hardcoded in +`TcpConnectionStringOptions::parse_options`) and that swallows most +mid-operation connection failures into blocking retries. The wrapper's view +of connection health is therefore indirect: bounded timeouts + periodic +`ping` probes (added in session 01). The SDK exposes a first-class signal we +do not consume: `Client::subscribe_events()` yields `DiagnosticEvent` +connect/disconnect notifications. + +## Mitigation in place + +`health_check()` live pings drive `ConnectionState` and the connection +gauge; timeouts are recorded as breaker failures; classified errors cover +the classes that escape the SDK's internal retry. + +## Binding trigger + +At the next iggy SDK minor bump, evaluate subscribing to `DiagnosticEvent` +to drive `ConnectionState`/breaker directly, and re-check whether the SDK +has made internal reconnection configurable from connection strings (if so, +decide once whether resilience policy lives in the SDK or the wrapper, and +delete the loser). diff --git a/docs/tech-debt/TD-2026-07-03.md b/docs/tech-debt/TD-2026-07-03.md new file mode 100644 index 0000000..fae0bcf --- /dev/null +++ b/docs/tech-debt/TD-2026-07-03.md @@ -0,0 +1,17 @@ +# TD-2026-07-03: Circuit-breaker half-open probe limiting + +**Source:** Review session 01, Round 1 — code-architect #9, type-design F5 (LOW). +**Status:** open + +## Problem + +In `HalfOpen`, `allow_request()` returns `true` for unlimited concurrent +callers, so a recovering server can receive a thundering herd of probes; +the module docs describe a single-probe intent. (The related +straggler-failure timer refresh in `Open` state was fixed in session 01.) + +## Binding trigger + +Resolve (single/token-limited probe permit, or correct the docs to describe +multi-probe behavior) at the first production incident involving breaker +recovery, or whenever breaker configuration is next extended. diff --git a/docs/tech-debt/TD-2026-07-04.md b/docs/tech-debt/TD-2026-07-04.md new file mode 100644 index 0000000..a85154a --- /dev/null +++ b/docs/tech-debt/TD-2026-07-04.md @@ -0,0 +1,20 @@ +# TD-2026-07-04: X-Request-Timeout enforcement + +**Source:** Review session 01, Round 1 — silent-failure-hunter M4 (MEDIUM). +**Status:** open + +## Problem + +`src/middleware/timeout.rs` parses `X-Request-Timeout` into a request +extension, but nothing consumes it: no handler reads +`RequestTimeout`/`effective_timeout` and no tower timeout layer exists. The +documented "request timeout propagation" currently changes nothing at +runtime (all Iggy operations are bounded by the global +`OPERATION_TIMEOUT_SECS` instead). + +## Binding trigger + +Before the header is advertised in any client-facing documentation beyond +CLAUDE.md (README API reference, OpenAPI, etc.), either wire +`effective_timeout` into the handler → wrapper call path (per-request bound +passed to `with_reconnect`) or delete the middleware and the claim. diff --git a/docs/tech-debt/TD-2026-07-05.md b/docs/tech-debt/TD-2026-07-05.md new file mode 100644 index 0000000..70974bc --- /dev/null +++ b/docs/tech-debt/TD-2026-07-05.md @@ -0,0 +1,18 @@ +# TD-2026-07-05: Metrics exporter smoke test + +**Source:** Review session 01, Round 1 — pr-test-analyzer #7 (MEDIUM). +**Status:** open + +## Problem + +`init_metrics` (PrometheusBuilder install + HTTP listener) is now called at +startup (session 01) but has zero runtime test coverage; a +metrics-exporter-prometheus behavior change would be discovered at +production startup. The unit tests only verify the `record_*` helpers don't +panic without a recorder installed. + +## Binding trigger + +At the next metrics-exporter-prometheus major/minor bump, add one test that +calls `init_metrics` on an ephemeral port, records a counter, and fetches +`/metrics` asserting the metric name appears. diff --git a/docs/tech-debt/TD-2026-07-06.md b/docs/tech-debt/TD-2026-07-06.md new file mode 100644 index 0000000..5f27630 --- /dev/null +++ b/docs/tech-debt/TD-2026-07-06.md @@ -0,0 +1,16 @@ +# TD-2026-07-06: Durable-storage guide config re-validation + +**Source:** Review session 01, Round 1 — consistency #10 (LOW). +**Status:** open + +## Problem + +`docs/durable-storage-guide.md` was re-stamped for server 0.8.0 in session +01, but the server configuration keys and behaviors it documents (fsync +settings, segment sizes, S3 archiving) were originally validated against +server 0.6.0 and have not been re-verified key-by-key against 0.8.0. + +## Binding trigger + +At the next server image bump past the 0.8.x line, re-validate every +documented config key against the shipped server config and re-stamp. diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index bc3b45f..ca00805 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -332,6 +332,126 @@ async fn test_stats_endpoint() { // Message Tests // ============================================================================ +/// Build a generic test event payload with the given marker message. +fn generic_event(marker: &str, partition_key: Option<&str>) -> serde_json::Value { + let event = json!({ + "id": uuid::Uuid::new_v4().to_string(), + "event_type": "test.semantics", + "timestamp": "2026-07-03T10:30:00Z", + "payload": { + "type": "Generic", + "data": { "message": marker } + } + }); + match partition_key { + Some(key) => json!({ "event": event, "partition_key": key }), + None => json!({ "event": event }), + } +} + +/// Poll one partition and return the number of messages. +async fn poll_count(fixture: &TestFixture, query: &str) -> u64 { + let response = fixture + .client + .get(fixture.url(&format!("/messages?{}", query))) + .send() + .await + .expect("Poll request failed"); + assert!( + response.status().is_success(), + "poll '{}' failed: {}", + query, + response.status() + ); + let body: serde_json::Value = response.json().await.expect("Failed to parse poll body"); + body.get("count").and_then(|v| v.as_u64()).expect("count") +} + +/// Pins the SDK-0.10 semantics of PollingStrategy::next + auto_commit: a poll +/// without an explicit offset resumes from the consumer's committed offset, +/// so a second poll must not re-deliver the first poll's messages. +#[tokio::test] +async fn test_poll_without_offset_advances_committed_offset() { + let fixture = TestFixture::new().await; + + for i in 0..3 { + let response = fixture + .client + .post(fixture.url("/messages")) + .json(&generic_event(&format!("offset-semantics-{i}"), None)) + .send() + .await + .expect("Send request failed"); + assert!(response.status().is_success()); + } + sleep(Duration::from_millis(300)).await; + + // First round: no offset param (PollingStrategy::next) + auto_commit, + // across both partitions of the 2-partition test topic. + let first: u64 = poll_count( + &fixture, + "partition_id=0&consumer_id=77&count=10&auto_commit=true", + ) + .await + + poll_count( + &fixture, + "partition_id=1&consumer_id=77&count=10&auto_commit=true", + ) + .await; + assert_eq!( + first, 3, + "first next-poll round should deliver all messages" + ); + + // Second round: offsets were committed, nothing may be re-delivered. + let second: u64 = poll_count( + &fixture, + "partition_id=0&consumer_id=77&count=10&auto_commit=true", + ) + .await + + poll_count( + &fixture, + "partition_id=1&consumer_id=77&count=10&auto_commit=true", + ) + .await; + assert_eq!( + second, 0, + "second next-poll round re-delivered committed messages" + ); +} + +/// Pins the SDK-0.10 semantics of Partitioning::messages_key_str: all +/// messages sharing a partition key must land in exactly one partition +/// (per-entity ordering guarantee). +#[tokio::test] +async fn test_same_partition_key_routes_to_single_partition() { + let fixture = TestFixture::new().await; + + for i in 0..5 { + let response = fixture + .client + .post(fixture.url("/messages")) + .json(&generic_event( + &format!("routing-{i}"), + Some("routing-invariant-key"), + )) + .send() + .await + .expect("Send request failed"); + assert!(response.status().is_success()); + } + sleep(Duration::from_millis(300)).await; + + let p0 = poll_count(&fixture, "partition_id=0&consumer_id=88&count=50&offset=0").await; + let p1 = poll_count(&fixture, "partition_id=1&consumer_id=88&count=50&offset=0").await; + + assert_eq!(p0 + p1, 5, "all keyed messages should be delivered"); + assert!( + (p0 == 5 && p1 == 0) || (p0 == 0 && p1 == 5), + "messages with one partition key split across partitions (p0={p0}, p1={p1})" + ); +} + #[tokio::test] async fn test_send_and_poll_message() { let fixture = TestFixture::new().await; From 56d5fd037209a637a502a89b9d38ecf1f78197fe Mon Sep 17 00:00:00 2001 From: mlevkov Date: Fri, 3 Jul 2026 23:38:03 -0700 Subject: [PATCH 13/20] fix(docs): complete the port migration and repair CHANGELOG structure Review round 2 (consistency lens): the round-1 port standardization missed several documents, falsifying the CHANGELOG claim that all docs agree on 8000. - README/CLAUDE.md config tables: PORT default 3000 -> 8000 - architecture.md: docker run mapping and Kubernetes liveness/readiness probes 3000 -> 8000 (the old manifest yielded CrashLoopBackOff) - rustdoc curl examples (messages, auth, request_id handlers/middleware) and health.rs probe example: 3000 -> 8000 - CHANGELOG: merged duplicate Unreleased section headings (Keep a Changelog violation from prepending) and deleted the stale Security block claiming deny.toml ignores this branch actually pruned - Dockerfile: EXPOSE 9090 (metrics listener) and install curl, which HEALTHCHECK invokes but the runtime image never contained - README/CLAUDE.md observability sections now document the app's own /metrics endpoint (host 9091 under compose) - middleware diagrams (routes.rs, README, CLAUDE.md) include the Timeout-extract layer in correct order - README: error table gains the 503 connection/circuit/timeout variants this branch made producible; directory listings gain utils.rs, metrics.rs, handlers/util.rs; .env.example claim softened --- CHANGELOG.md | 20 +++++--------------- CLAUDE.md | 11 +++++++---- Dockerfile | 7 ++++--- README.md | 20 ++++++++++++++++---- architecture.md | 6 +++--- src/handlers/health.rs | 2 +- src/handlers/messages.rs | 2 +- src/middleware/auth.rs | 4 ++-- src/middleware/request_id.rs | 2 +- src/routes.rs | 5 +++++ 10 files changed, 45 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9c91e5..211c407 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - CI now fails on `cargo deny check advisories licenses` (previously licenses-only and non-blocking); weekly stress tests pin `apache/iggy:0.8.0` instead of `latest` +- Updated `docker-compose.yaml` with full observability stack configuration +- Simplified documentation section in README.md to reference `docs/` + directory ### Fixed @@ -68,6 +71,8 @@ triggers (`docs/tech-debt/`): - **API**: `count=0` polls return 400 instead of 500; all-digit resource names ("42") are treated as names, not numeric server IDs; removed the dead `PollMessagesRequest` type +- Added `issues: write` permission to CI security audit job to allow + creating advisory issues ### Added @@ -82,21 +87,6 @@ triggers (`docs/tech-debt/`): - Durable storage guide (`docs/durable-storage-guide.md`): storage architecture, fsync configuration, S3 backup/archiving, recovery procedures - Documentation index (`docs/README.md`) with topic-based navigation -### Changed - -- Updated `docker-compose.yaml` with full observability stack configuration -- Simplified documentation section in README.md to reference `docs/` directory - -### Fixed - -- Added `issues: write` permission to CI security audit job to allow creating advisory issues - -### Security - -- Ignored unmaintained transitive dependency advisories in `deny.toml`: - - `RUSTSEC-2024-0384` (instant): from iggy -> reqwest-retry -> parking_lot v0.11 - - `RUSTSEC-2025-0134` (rustls-pemfile): from testcontainers -> bollard (dev-dep only) - ## [0.1.0] - 2024-12-01 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 0e5f1c9..8db4e8e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,11 +35,11 @@ This application showcases how to build a production-ready message streaming ser │ Axum HTTP Server │ │ (src/main.rs) │ ├─────────────────────────────────────────────────────────────┤ -│ Middleware Stack (src/middleware/) │ +│ Middleware Stack (src/middleware/), in request order: │ │ - rate_limit.rs: Token bucket rate limiting │ │ - auth.rs: API key authentication │ -│ - timeout.rs: Request timeout propagation │ │ - request_id.rs: Request ID propagation │ +│ - timeout.rs: Request timeout parsing │ │ + tower_http: Tracing, CORS │ ├─────────────────────────────────────────────────────────────┤ │ Handlers (src/handlers/) │ @@ -99,6 +99,7 @@ src/ ├── metrics.rs # Prometheus metrics export ├── state.rs # Shared application state with stats caching ├── routes.rs # Route definitions and middleware stack +├── utils.rs # Shutdown-signal helpers ├── iggy_client/ # Iggy SDK wrapper module │ ├── mod.rs # Client wrapper with auto-reconnection │ ├── circuit_breaker.rs # Circuit breaker pattern implementation @@ -127,7 +128,8 @@ src/ ├── health.rs # Health endpoints ├── messages.rs # Message endpoints ├── streams.rs # Stream management - └── topics.rs # Topic management + ├── topics.rs # Topic management + └── util.rs # Shared handler utilities tests/ ├── integration_tests.rs # End-to-end API tests with testcontainers @@ -190,7 +192,7 @@ Environment variables (see `.env.example`): | Variable | Default | Description | |----------|---------|-------------| | `HOST` | `0.0.0.0` | Server bind address | -| `PORT` | `3000` | Server port | +| `PORT` | `8000` | Server port | | `RUST_LOG` | `info` | Log level | ### Iggy Connection @@ -304,6 +306,7 @@ The project includes a complete Grafana-based observability stack for monitoring | Service | Port | Description | |---------|------|-------------| | **Iggy** | 3000 | Message streaming server (also serves `/metrics`) | +| **Sample App metrics** | 9091 (host) | App Prometheus metrics (`METRICS_PORT` 9090 in-container) | | **Iggy Web UI** | 3050 | Dashboard for streams, topics, messages, and users | | **Prometheus** | 9090 | Metrics collection and storage | | **Grafana** | 3001 | Visualization and dashboards | diff --git a/Dockerfile b/Dockerfile index 49beb81..93da164 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,9 +32,10 @@ FROM debian:bookworm-slim WORKDIR /app -# Install runtime dependencies +# Install runtime dependencies (curl is required by HEALTHCHECK) RUN apt-get update && apt-get install -y \ ca-certificates \ + curl \ libssl3 \ && rm -rf /var/lib/apt/lists/* @@ -45,8 +46,8 @@ COPY --from=builder /app/target/release/iggy_sample /app/iggy_sample RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app USER appuser -# Expose port -EXPOSE 8000 +# Expose the API port and the Prometheus metrics listener (METRICS_PORT) +EXPOSE 8000 9090 # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ diff --git a/README.md b/README.md index c4fefb0..e02614b 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Apache Iggy is capable of processing millions of messages per second with ultra- │ (Port 8000) │ ├─────────────────────────────────────────────────────────────┤ │ Middleware Stack │ -│ Rate Limit → Auth → Request ID → Tracing → CORS │ +│ Rate Limit → Auth → Request ID → Timeout → Tracing → CORS │ ├─────────────────────────────────────────────────────────────┤ │ Handlers │ │ ├── health.rs - Health/readiness checks, stats │ @@ -321,13 +321,13 @@ curl http://localhost:8000/stats ## Configuration -Configuration is loaded from environment variables. See `.env.example` for all options. +Configuration is loaded from environment variables. See `.env.example` for the common options; the tables below list all of them. ### Server & Iggy Connection | Variable | Default | Description | |----------|---------|-------------| | `HOST` | `0.0.0.0` | Server bind address | -| `PORT` | `3000` | Server port | +| `PORT` | `8000` | Server port | | `IGGY_CONNECTION_STRING` | `iggy://iggy:iggy@localhost:8090` | Iggy connection string | | `IGGY_STREAM` | `sample-stream` | Default stream name | | `IGGY_TOPIC` | `events` | Default topic name | @@ -395,6 +395,8 @@ iggy_sample/ │ ├── error.rs # Error types with HTTP status codes │ ├── state.rs # Shared application state │ ├── routes.rs # Route definitions +│ ├── utils.rs # Shutdown-signal helpers +│ ├── metrics.rs # Prometheus metrics export │ ├── iggy_client/ # Iggy SDK wrapper module │ ├── validation.rs # Input validation utilities │ ├── middleware/ @@ -415,7 +417,8 @@ iggy_sample/ │ ├── health.rs # Health endpoints │ ├── messages.rs # Message endpoints │ ├── streams.rs # Stream management -│ └── topics.rs # Topic management +│ ├── topics.rs # Topic management +│ └── util.rs # Shared handler utilities ├── tests/ │ ├── integration_tests.rs # End-to-end API tests │ └── model_tests.rs # Unit tests for models @@ -480,6 +483,7 @@ The project includes a complete observability stack for monitoring and managing | Component | Port | Description | |-----------|------|-------------| | **Iggy Server** | 3000 | HTTP API + Prometheus metrics at `/metrics` | +| **Sample App metrics** | 9091 (host) | App Prometheus metrics (`METRICS_PORT` 9090 in-container) | | **Iggy Web UI** | 3050 | Dashboard for streams, topics, messages, users | | **Prometheus** | 9090 | Metrics collection with 15-day retention | | **Grafana** | 3001 | Pre-configured dashboards for visualization | @@ -511,6 +515,10 @@ Iggy exposes Prometheus-compatible metrics: # View raw metrics from Iggy curl http://localhost:3000/metrics +# View the sample app's own metrics (message counters, reconnects, +# circuit breaker state; host port 9091 under docker-compose) +curl http://localhost:9091/metrics + # Query via Prometheus curl 'http://localhost:9090/api/v1/query?query=up{job="iggy"}' ``` @@ -595,6 +603,10 @@ All errors return structured JSON responses: | Error Type | HTTP Status | Description | |------------|-------------|-------------| | `connection_failed` | 503 | Iggy server unavailable | +| `disconnected` | 503 | Lost connection during operation | +| `connection_reset` | 503 | Connection was reset by peer | +| `circuit_open` | 503 | Circuit breaker open, failing fast | +| `operation_timeout` | 503 | Iggy operation exceeded the timeout | | `stream_error` | 500 | Stream operation failed | | `topic_error` | 500 | Topic operation failed | | `send_error` | 500 | Message send failed | diff --git a/architecture.md b/architecture.md index e18718b..a38c288 100644 --- a/architecture.md +++ b/architecture.md @@ -357,7 +357,7 @@ Each request gets a unique ID (X-Request-Id header) for correlation. ```bash docker build -t iggy-sample . -docker run -p 3000:3000 \ +docker run -p 8000:8000 \ -e IGGY_CONNECTION_STRING=iggy://iggy:iggy@iggy-server:8090 \ iggy-sample ``` @@ -382,11 +382,11 @@ spec: livenessProbe: httpGet: path: /health - port: 3000 + port: 8000 readinessProbe: httpGet: path: /ready - port: 3000 + port: 8000 ``` ## Exit Codes diff --git a/src/handlers/health.rs b/src/handlers/health.rs index 4ae7426..e562343 100644 --- a/src/handlers/health.rs +++ b/src/handlers/health.rs @@ -71,7 +71,7 @@ pub async fn health_check(State(state): State) -> Json /// readinessProbe: /// httpGet: /// path: /ready -/// port: 3000 +/// port: 8000 /// initialDelaySeconds: 5 /// periodSeconds: 10 /// ``` diff --git a/src/handlers/messages.rs b/src/handlers/messages.rs index 9388470..1bd17e3 100644 --- a/src/handlers/messages.rs +++ b/src/handlers/messages.rs @@ -165,7 +165,7 @@ fn default_count() -> u32 { /// # Example /// /// ```bash -/// curl "http://localhost:3000/messages?partition_id=1&count=10&offset=0" +/// curl "http://localhost:8000/messages?partition_id=1&count=10&offset=0" /// ``` #[instrument(skip(state))] pub async fn poll_messages( diff --git a/src/middleware/auth.rs b/src/middleware/auth.rs index a5bddc0..ca27be2 100644 --- a/src/middleware/auth.rs +++ b/src/middleware/auth.rs @@ -18,10 +18,10 @@ //! //! ```bash //! # Header method (preferred) -//! curl -H "X-API-Key: your-secret-key" http://localhost:3000/messages +//! curl -H "X-API-Key: your-secret-key" http://localhost:8000/messages //! //! # Query parameter method -//! curl "http://localhost:3000/messages?api_key=your-secret-key" +//! curl "http://localhost:8000/messages?api_key=your-secret-key" //! ``` //! //! # Bypassed Endpoints diff --git a/src/middleware/request_id.rs b/src/middleware/request_id.rs index a923e93..3bb8cf3 100644 --- a/src/middleware/request_id.rs +++ b/src/middleware/request_id.rs @@ -20,7 +20,7 @@ //! Clients can provide their own request ID: //! //! ```bash -//! curl -H "X-Request-Id: my-correlation-id" http://localhost:3000/messages +//! curl -H "X-Request-Id: my-correlation-id" http://localhost:8000/messages //! ``` //! //! The same ID will be returned in the response for correlation. diff --git a/src/routes.rs b/src/routes.rs index b63447c..6829997 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -22,6 +22,11 @@ //! │ //! ▼ //! ┌──────────────────┐ +//! │ Timeout Extract │ ← Parses X-Request-Timeout into extensions +//! └────────┬─────────┘ +//! │ +//! ▼ +//! ┌──────────────────┐ //! │ Tracing │ ← HTTP request/response logging //! └────────┬─────────┘ //! │ From c23bef2bd660a55b3da7767e6dded0e327012331 Mon Sep 17 00:00:00 2001 From: mlevkov Date: Fri, 3 Jul 2026 23:43:38 -0700 Subject: [PATCH 14/20] fix(security): rightmost-untrusted XFF resolution and shared proxy config Review round 2 (logic-security F1/F2 MEDIUM, type-design R2-1, plus converging dead-surface findings from three lenses): - honored X-Forwarded-For chains are now resolved with the rightmost-untrusted rule (walk right-to-left, skip trusted-range hops, first untrusted address wins) and parsed as IpAddr, falling back to the peer address on unparseable chains. The previous first-entry choice let an attacker behind an APPENDING proxy (the nginx/ALB/ingress default) rotate spoofed first entries and keep bypassing per-IP limits - exactly the attack the round-1 fix targeted; garbage values can also no longer mint arbitrary limiter keys - X-Real-IP from a trusted peer is honored only if it parses as an IP - missing-ConnectInfo fallback warns once per process instead of per-request per-middleware (latent log flood) - RateLimitLayer::with_trusted_proxies now takes the shared Arc - the config is parsed exactly once and genuinely shared by auth and rate limiting, as the routes.rs comment claimed; RateLimitError is #[non_exhaustive] - removed dead surface: TrustedProxyConfig::is_trusted(&str) (tests ported to is_trusted_ip), UNKNOWN_IP/extract_client_ip re-exports, classify_iggy_error un-exported (fallback contract too easy to violate publicly), stale auth.rs comment; extract_client_ip is now the documented header-only fallback that the validated variant delegates to - ip.rs module docs/diagram rewritten for the new flow; CLAUDE.md documents the rightmost-untrusted semantics Tests: +9 (appending-proxy spoof defeat, multi-hop trusted skip, all-trusted chain, unparseable chain/garbage X-Real-IP fallbacks, invalid-CIDR fail-fast, IPv6 containment, /0 prefix); 156 lib + 26 integration green --- CLAUDE.md | 10 +- src/iggy_client/mod.rs | 6 +- src/middleware/auth.rs | 2 - src/middleware/ip.rs | 282 +++++++++++++++++++++++++---------- src/middleware/mod.rs | 2 +- src/middleware/rate_limit.rs | 95 +++++++----- src/routes.rs | 2 +- 7 files changed, 279 insertions(+), 120 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8db4e8e..c90096f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -246,9 +246,13 @@ The `TRUSTED_PROXIES` variable configures IP spoofing mitigation for both the rate limiter and the auth brute-force limiter. When set, forwarded headers (`X-Forwarded-For`/`X-Real-IP`) are only honored if the direct peer address is inside a trusted range; requests from untrusted peers are keyed by their actual -peer address. Invalid entries fail startup -(`RateLimitError::InvalidTrustedProxyCidr`) instead of silently degrading to -trust-all. +peer address. Honored `X-Forwarded-For` chains are resolved with the +**rightmost-untrusted** rule (walk from the right, skip trusted-range hops, +take the first untrusted address), so the guarantee holds for proxies that +append to a client-supplied header — the common default — as well as those +that overwrite it. Unparseable forwarded values fall back to the peer address. +Invalid entries fail startup (`RateLimitError::InvalidTrustedProxyCidr`) +instead of silently degrading to trust-all. **Format**: Comma-separated CIDR notation diff --git a/src/iggy_client/mod.rs b/src/iggy_client/mod.rs index e9b01a2..8dbc482 100644 --- a/src/iggy_client/mod.rs +++ b/src/iggy_client/mod.rs @@ -76,9 +76,13 @@ use crate::models::Event; // Re-exports for public API pub use circuit_breaker::{CircuitBreaker, CircuitBreakerConfig, CircuitState}; pub use connection::ConnectionState; -pub use helpers::{classify_iggy_error, rand_jitter, to_identifier}; +pub use helpers::{rand_jitter, to_identifier}; pub use params::PollParams; +// Internal-only: the error classifier's fallback contract (must be a +// NON-connection variant) is too easy to violate to expose publicly. +use helpers::classify_iggy_error; + // ============================================================================= // Constants // ============================================================================= diff --git a/src/middleware/auth.rs b/src/middleware/auth.rs index ca27be2..6827103 100644 --- a/src/middleware/auth.rs +++ b/src/middleware/auth.rs @@ -385,8 +385,6 @@ fn rate_limited_response(retry_after: u64) -> Response { .into_response() } -// Note: extract_client_ip is imported from super::ip module - #[cfg(test)] #[allow(clippy::unwrap_used, clippy::expect_used)] mod tests { diff --git a/src/middleware/ip.rs b/src/middleware/ip.rs index d708512..4c3d989 100644 --- a/src/middleware/ip.rs +++ b/src/middleware/ip.rs @@ -62,10 +62,13 @@ //! are only honored if the direct peer (from Axum's `ConnectInfo` extension) //! is inside a trusted range; otherwise the peer address itself is used as the //! client IP, so spoofed `X-Forwarded-For`/`X-Real-IP` headers from untrusted -//! sources are ignored. This requires the server to be started with -//! `into_make_service_with_connect_info::()` (both `main.rs` and -//! the integration-test harness do this). If `ConnectInfo` is absent, the -//! functions fall back to header extraction and log a warning. +//! sources are ignored. When the trusted proxy's `X-Forwarded-For` IS honored, +//! it is resolved with the **rightmost-untrusted** rule, so the guarantee +//! holds even for proxies that APPEND to a client-supplied header (the common +//! default) rather than overwriting it. This requires the server to be started +//! with `into_make_service_with_connect_info::()` (both `main.rs` +//! and the integration-test harness do this). If `ConnectInfo` is absent, the +//! functions fall back to header extraction and warn once. //! //! # Internal Architecture //! @@ -75,15 +78,21 @@ //! │ (no allocations) │ //! └───────────┬──────────────┘ //! │ -//! ┌───────────────────┴───────────────────┐ -//! │ │ -//! ▼ ▼ -//! ┌───────────────────────────┐ ┌───────────────────────────┐ -//! │ extract_client_ip_with_ │ │ extract_client_ip │ -//! │ validation() │ │ (simple) │ -//! │ - Debug logging │ │ - No logging overhead │ -//! │ - Returns Cow<'static> │ │ - Returns Cow<'static> │ -//! └───────────────────────────┘ └───────────────────────────┘ +//! │ +//! ▼ +//! ┌───────────────────────────┐ +//! │ extract_client_ip │ ← header-only fallback +//! │ (headers, no peer) │ +//! └───────────┬───────────────┘ +//! │ delegated to by +//! ▼ +//! ┌───────────────────────────┐ +//! │ extract_client_ip_with_ │ ← used by rate limiting +//! │ validation() │ AND authentication +//! │ - peer-address (ConnectInfo) +//! │ trust enforcement │ +//! │ - rightmost-untrusted XFF │ +//! └───────────────────────────┘ //! ``` //! //! **Design Benefits:** @@ -93,7 +102,7 @@ //! - Both public functions return `Cow<'static, str>` for consistent zero-allocation fallback use std::borrow::Cow; -use std::net::SocketAddr; +use std::net::{IpAddr, SocketAddr}; use axum::extract::ConnectInfo; use axum::http::Request; @@ -175,38 +184,64 @@ fn extract_ip_from_headers(req: &Request) -> ExtractedIp<'_> { // Public API // ============================================================================= +/// Resolve the client IP from a full `X-Forwarded-For` value using the +/// **rightmost-untrusted** rule. +/// +/// Walk the chain from the right (the entry appended by the proxy closest to +/// us) toward the left, skipping addresses inside trusted ranges; the first +/// untrusted address is the real client. This stays correct whether the edge +/// proxy OVERWRITES the header or (the common default: nginx +/// `$proxy_add_x_forwarded_for`, ALB, ingress controllers) APPENDS to a +/// client-supplied value — a leftmost attacker-chosen entry is never reached +/// unless every hop after it is a trusted proxy. +/// +/// Returns `None` when any visited entry fails to parse as an IP (a chain we +/// cannot reason about is not trusted) — callers fall back to the peer +/// address. If EVERY entry is a trusted proxy, the leftmost entry is +/// returned: with an all-trusted chain, that is the origin as reported by +/// the first proxy. +fn rightmost_untrusted_xff(xff: &str, trusted_proxies: &TrustedProxyConfig) -> Option { + let mut leftmost_trusted = None; + for entry in xff.split(',').rev() { + let ip: IpAddr = entry.trim().parse().ok()?; + if trusted_proxies.is_trusted_ip(&ip) { + leftmost_trusted = Some(ip); + } else { + return Some(ip); + } + } + leftmost_trusted +} + /// Extract client IP from request headers with trusted proxy validation. /// -/// This is the full-featured extraction used by rate limiting middleware. -/// It logs debug information when trusted proxy validation is enabled, -/// helping operators detect potential IP spoofing attempts. +/// Used by both the rate limiter and the auth brute-force limiter. /// -/// # Header Priority +/// # Behavior with trusted proxies configured (and `ConnectInfo` available) /// -/// Checks in order (returns first match): -/// 1. `X-Forwarded-For` header (first IP in a comma-separated list) -/// 2. `X-Real-IP` header -/// 3. Falls back to [`UNKNOWN_IP`] +/// - Peer NOT in a trusted range: forwarded headers are ignored entirely and +/// the peer address is the client IP (spoofed headers from direct clients +/// are inert). +/// - Peer in a trusted range with `X-Forwarded-For`: resolved via the +/// rightmost-untrusted rule (see [`rightmost_untrusted_xff`]) so the +/// guarantee holds for both overwriting and appending proxies; an +/// unparseable chain falls back to the peer address. +/// - Peer in a trusted range with `X-Real-IP`: the value is used only if it +/// parses as an IP address; otherwise the peer address is used. +/// - No forwarded headers: the peer address is used. /// -/// # X-Forwarded-For Parsing +/// # Behavior without trusted proxies (or without `ConnectInfo`) /// -/// When multiple IPs are present (e.g., `"203.0.113.50, 70.41.3.18, 150.172.238.178"`), -/// only the **first** IP is used. This is typically the original client IP, with -/// subsequent IPs being intermediate proxies. However, this relies on your edge -/// proxy correctly setting this header (see module-level security documentation). +/// Falls back to plain header extraction ([`extract_client_ip`]): first +/// `X-Forwarded-For` entry, then `X-Real-IP`, then [`UNKNOWN_IP`]. If +/// TRUSTED_PROXIES is configured but the server was not started with +/// `into_make_service_with_connect_info`, a warning is logged once. /// /// # Returns /// /// `Cow<'static, str>` - Borrowed for "unknown" (no allocation), owned for actual IPs. /// Use `.into_owned()` when you need a `String` for async contexts that outlive /// the request reference. -/// -/// # Example -/// -/// ```ignore -/// let ip = extract_client_ip_with_validation(&req, &trusted_proxies); -/// let ip_string = ip.into_owned(); // For use in async block -/// ``` #[inline] pub fn extract_client_ip_with_validation( req: &Request, @@ -216,56 +251,76 @@ pub fn extract_client_ip_with_validation( match req.extensions().get::>() { Some(ConnectInfo(peer)) => { let peer_ip = peer.ip(); - if trusted_proxies.is_trusted_ip(&peer_ip) { - // Peer is a trusted proxy: honor the forwarded headers it - // set, falling back to the peer address itself when the - // proxy sent none. - return match extract_ip_from_headers(req) { - ExtractedIp::FromXff(ip) | ExtractedIp::FromRealIp(ip) => { - Cow::Owned(ip.to_string()) + if !trusted_proxies.is_trusted_ip(&peer_ip) { + // Peer is NOT a trusted proxy: forwarded headers are + // client-controlled and spoofable, so ignore them and key + // on the actual peer address. + if !matches!(extract_ip_from_headers(req), ExtractedIp::NotFound) { + debug!( + peer_ip = %peer_ip, + "Ignoring forwarded headers from untrusted peer" + ); + } + return Cow::Owned(peer_ip.to_string()); + } + + // Peer is a trusted proxy: resolve the forwarded chain. + if let Some(xff) = req.headers().get("x-forwarded-for") + && let Ok(value) = xff.to_str() + && !value.trim().is_empty() + { + return match rightmost_untrusted_xff(value, trusted_proxies) { + Some(client_ip) => Cow::Owned(client_ip.to_string()), + None => { + debug!( + peer_ip = %peer_ip, + "Unparseable X-Forwarded-For from trusted proxy; keying on peer address" + ); + Cow::Owned(peer_ip.to_string()) } - ExtractedIp::NotFound => Cow::Owned(peer_ip.to_string()), }; } - // Peer is NOT a trusted proxy: forwarded headers are - // client-controlled and spoofable, so ignore them and key on - // the actual peer address. - if !matches!(extract_ip_from_headers(req), ExtractedIp::NotFound) { - debug!( - peer_ip = %peer_ip, - "Ignoring forwarded headers from untrusted peer" - ); + + if let Some(real_ip) = req.headers().get("x-real-ip") + && let Ok(value) = real_ip.to_str() + && let Ok(client_ip) = value.trim().parse::() + { + return Cow::Owned(client_ip.to_string()); } + + // No usable forwarded header from the trusted proxy. return Cow::Owned(peer_ip.to_string()); } None => { // TRUSTED_PROXIES is configured but the server was started // without connect-info, so enforcement is impossible. Fall - // back to header trust and say so loudly. - warn!( - "TRUSTED_PROXIES is set but the peer address is unavailable \ - (server not started with into_make_service_with_connect_info); \ - falling back to trusting forwarded headers" - ); + // back to header trust; warn once, not per request (two + // middlewares call this on every request). + static WARN_ONCE: std::sync::Once = std::sync::Once::new(); + WARN_ONCE.call_once(|| { + warn!( + "TRUSTED_PROXIES is set but the peer address is unavailable \ + (server not started with into_make_service_with_connect_info); \ + falling back to trusting forwarded headers" + ); + }); } } } - match extract_ip_from_headers(req) { - ExtractedIp::FromXff(ip) | ExtractedIp::FromRealIp(ip) => Cow::Owned(ip.to_string()), - ExtractedIp::NotFound => Cow::Borrowed(UNKNOWN_IP), - } + extract_client_ip(req) } -/// Extract client IP from request headers (simple version without validation logging). +/// Extract client IP from request headers only (no peer-address validation). /// -/// This is a streamlined version used by authentication middleware for brute force -/// protection. It omits the debug logging present in [`extract_client_ip_with_validation`] -/// for slightly faster execution in the hot path. +/// This is the header-only fallback path used by +/// [`extract_client_ip_with_validation`] when trusted-proxy validation is +/// disabled or the peer address is unavailable. It trusts client-provided +/// headers unconditionally — production callers should always go through the +/// validated variant. /// /// # Header Priority /// -/// Same as [`extract_client_ip_with_validation`]: /// 1. `X-Forwarded-For` header (first IP in a comma-separated list) /// 2. `X-Real-IP` header /// 3. Falls back to [`UNKNOWN_IP`] @@ -273,23 +328,10 @@ pub fn extract_client_ip_with_validation( /// # Security /// /// All security considerations from the module-level documentation apply here. -/// This function trusts client-provided headers and should only be used behind -/// a properly configured reverse proxy. /// /// # Returns /// /// `Cow<'static, str>` - Borrowed for "unknown" (no allocation), owned for actual IPs. -/// Use `.into_owned()` when you need a `String` for async contexts that outlive -/// the request reference. -/// -/// # Example -/// -/// ```ignore -/// let client_ip = extract_client_ip(&req); -/// // Convert to owned String for async block -/// let ip_string = client_ip.into_owned(); -/// if let Err(_) = limiter.check_key(&ip_string) { ... } -/// ``` #[inline] pub fn extract_client_ip(req: &Request) -> Cow<'static, str> { match extract_ip_from_headers(req) { @@ -447,6 +489,90 @@ mod tests { ); } + #[test] + fn test_validation_appending_proxy_defeats_spoofed_first_entry() { + // The common proxy default APPENDS the real client to a + // client-supplied X-Forwarded-For. The rightmost-untrusted rule must + // pick the entry the trusted proxy appended (203.0.113.50), not the + // attacker-chosen first entry (6.6.6.6). + let trusted = TrustedProxyConfig::try_new(&["10.0.0.0/8".to_string()]).unwrap(); + let req = req_with_peer(Some("6.6.6.6, 203.0.113.50"), [10, 0, 0, 5]); + + assert_eq!( + extract_client_ip_with_validation(&req, &trusted), + "203.0.113.50" + ); + } + + #[test] + fn test_validation_multi_hop_skips_trusted_intermediates() { + // client, proxy-B (trusted), appended by peer proxy-A (trusted): + // walking right-to-left skips 10.0.0.7 and lands on the client. + let trusted = TrustedProxyConfig::try_new(&["10.0.0.0/8".to_string()]).unwrap(); + let req = req_with_peer(Some("203.0.113.50, 10.0.0.7"), [10, 0, 0, 5]); + + assert_eq!( + extract_client_ip_with_validation(&req, &trusted), + "203.0.113.50" + ); + } + + #[test] + fn test_validation_all_trusted_chain_uses_leftmost() { + let trusted = TrustedProxyConfig::try_new(&["10.0.0.0/8".to_string()]).unwrap(); + let req = req_with_peer(Some("10.0.0.9, 10.0.0.7"), [10, 0, 0, 5]); + + assert_eq!( + extract_client_ip_with_validation(&req, &trusted), + "10.0.0.9" + ); + } + + #[test] + fn test_validation_unparseable_chain_falls_back_to_peer() { + // Garbage in the chain (e.g. an entry with a port) means the chain + // cannot be reasoned about - key on the trusted peer itself. + let trusted = TrustedProxyConfig::try_new(&["10.0.0.0/8".to_string()]).unwrap(); + let req = req_with_peer(Some("203.0.113.50:8080"), [10, 0, 0, 5]); + + assert_eq!( + extract_client_ip_with_validation(&req, &trusted), + "10.0.0.5" + ); + } + + #[test] + fn test_validation_real_ip_from_trusted_peer() { + let trusted = TrustedProxyConfig::try_new(&["10.0.0.0/8".to_string()]).unwrap(); + let mut req = Request::builder() + .header("x-real-ip", "203.0.113.50") + .body(Body::empty()) + .unwrap(); + req.extensions_mut() + .insert(ConnectInfo(SocketAddr::from(([10, 0, 0, 5], 12345)))); + + assert_eq!( + extract_client_ip_with_validation(&req, &trusted), + "203.0.113.50" + ); + } + + #[test] + fn test_validation_garbage_real_ip_falls_back_to_peer() { + let trusted = TrustedProxyConfig::try_new(&["10.0.0.0/8".to_string()]).unwrap(); + let mut req = Request::builder() + .header("x-real-ip", "not-an-ip") + .body(Body::empty()) + .unwrap(); + req.extensions_mut() + .insert(ConnectInfo(SocketAddr::from(([10, 0, 0, 5], 12345)))); + + assert_eq!( + extract_client_ip_with_validation(&req, &trusted), + "10.0.0.5" + ); + } + #[test] fn test_validation_missing_connect_info_falls_back_to_headers() { let trusted = TrustedProxyConfig::try_new(&["10.0.0.0/8".to_string()]).unwrap(); diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 8961bfd..d5a21db 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -31,7 +31,7 @@ pub mod request_id; pub mod timeout; pub use auth::ApiKeyAuth; -pub use ip::{UNKNOWN_IP, extract_client_ip, extract_client_ip_with_validation}; +pub use ip::extract_client_ip_with_validation; pub use rate_limit::{RateLimitError, RateLimitLayer, TrustedProxyConfig}; pub use request_id::RequestIdLayer; pub use timeout::{ diff --git a/src/middleware/rate_limit.rs b/src/middleware/rate_limit.rs index 1924c84..19094df 100644 --- a/src/middleware/rate_limit.rs +++ b/src/middleware/rate_limit.rs @@ -51,6 +51,7 @@ use tower::{Layer, Service}; /// Error type for rate limit layer configuration. #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum RateLimitError { /// RPS value cannot be zero. ZeroRps, @@ -224,25 +225,7 @@ impl TrustedProxyConfig { /// /// Returns `true` if the IP matches any configured CIDR range, /// or if no ranges are configured (trust all mode). - pub fn is_trusted(&self, ip_str: &str) -> bool { - // If no trusted proxies configured, trust all (backwards compatible) - if self.ranges.is_empty() { - return true; - } - - // Parse the IP address - let ip: IpAddr = match ip_str.parse() { - Ok(ip) => ip, - Err(_) => { - // Can't parse as IP, not trusted - return false; - } - }; - - self.is_trusted_ip(&ip) - } - - /// Check if an already-parsed IP address is from a trusted proxy. + /// Check if an IP address is from a trusted proxy. /// /// Returns `true` if the IP matches any configured CIDR range, /// or if no ranges are configured (trust all mode). @@ -289,16 +272,21 @@ impl RateLimitLayer { /// Returns `RateLimitError::ZeroRps` if `rps` is 0. /// Use `RateLimitLayer::disabled()` for no limiting. pub fn new(rps: u32, burst: u32) -> Result { - Self::with_trusted_proxies(rps, burst, &[]) + Self::with_trusted_proxies(rps, burst, Arc::new(TrustedProxyConfig::default())) } /// Create a new per-IP rate limit layer with trusted proxy configuration. /// + /// Takes the already-parsed configuration (see + /// [`TrustedProxyConfig::try_new`]) so a single shared instance can serve + /// both this layer and the auth middleware — the raw CIDR strings are + /// parsed exactly once, at startup. + /// /// # Arguments /// /// * `rps` - Requests per second limit per IP (sustained rate) /// * `burst` - Additional burst capacity per IP - /// * `trusted_proxies` - CIDR ranges for trusted reverse proxies + /// * `trusted_proxies` - Shared trusted-proxy configuration /// /// # Errors /// @@ -307,7 +295,7 @@ impl RateLimitLayer { pub fn with_trusted_proxies( rps: u32, burst: u32, - trusted_proxies: &[String], + trusted_proxies: Arc, ) -> Result { // Validate rps is non-zero let rps_nonzero = NonZeroU32::new(rps).ok_or(RateLimitError::ZeroRps)?; @@ -323,13 +311,10 @@ impl RateLimitLayer { // Create keyed rate limiter with custom hasher for efficiency let limiter = RateLimiter::keyed(quota); - // Parse trusted proxy configuration (fails fast on invalid entries) - let proxy_config = TrustedProxyConfig::try_new(trusted_proxies)?; - Ok(Self { limiter: Arc::new(limiter), limit: rps, - trusted_proxies: Arc::new(proxy_config), + trusted_proxies, }) } @@ -495,13 +480,16 @@ mod tests { assert!(!cidr.contains(&"192.168.2.1".parse().unwrap())); } + fn ip(s: &str) -> IpAddr { + s.parse().unwrap() + } + #[test] fn test_trusted_proxy_config_empty() { let config = TrustedProxyConfig::try_new(&[]).unwrap(); assert!(!config.is_enabled()); // Empty config trusts all - assert!(config.is_trusted("1.2.3.4")); - assert!(config.is_trusted("invalid")); + assert!(config.is_trusted_ip(&ip("1.2.3.4"))); } #[test] @@ -512,13 +500,52 @@ mod tests { assert!(config.is_enabled()); // Should trust IPs in configured ranges - assert!(config.is_trusted("10.0.0.1")); - assert!(config.is_trusted("172.16.0.1")); - assert!(config.is_trusted("172.31.255.255")); + assert!(config.is_trusted_ip(&ip("10.0.0.1"))); + assert!(config.is_trusted_ip(&ip("172.16.0.1"))); + assert!(config.is_trusted_ip(&ip("172.31.255.255"))); // Should not trust IPs outside ranges - assert!(!config.is_trusted("192.168.1.1")); - assert!(!config.is_trusted("8.8.8.8")); - assert!(!config.is_trusted("invalid")); + assert!(!config.is_trusted_ip(&ip("192.168.1.1"))); + assert!(!config.is_trusted_ip(&ip("8.8.8.8"))); + } + + #[test] + fn test_trusted_proxy_config_invalid_entry_fails_fast() { + // A typo'd CIDR must fail startup, not silently degrade to trust-all + // (the exact security bug the round-1 remediation fixed). + let result = TrustedProxyConfig::try_new(&["10.0.0/8".to_string()]); + assert!(matches!( + result, + Err(RateLimitError::InvalidTrustedProxyCidr(_)) + )); + + // One bad entry among good ones still fails. + let result = + TrustedProxyConfig::try_new(&["10.0.0.0/8".to_string(), "garbage".to_string()]); + assert!(matches!( + result, + Err(RateLimitError::InvalidTrustedProxyCidr(_)) + )); + } + + #[test] + fn test_cidr_ipv6_containment() { + // The u128 mask arithmetic had zero containment coverage. + let cidr = CidrRange::parse("2001:db8::/32").unwrap(); + assert!(cidr.contains(&ip("2001:db8::1"))); + assert!(cidr.contains(&ip("2001:db8:ffff::1"))); + assert!(!cidr.contains(&ip("2001:db9::1"))); + // IPv4 never matches an IPv6 range + assert!(!cidr.contains(&ip("10.0.0.1"))); + } + + #[test] + fn test_cidr_zero_prefix_matches_everything() { + let v4_all = CidrRange::parse("0.0.0.0/0").unwrap(); + assert!(v4_all.contains(&ip("8.8.8.8"))); + assert!(v4_all.contains(&ip("192.168.1.1"))); + + let v6_all = CidrRange::parse("::/0").unwrap(); + assert!(v6_all.contains(&ip("2001:db8::1"))); } } diff --git a/src/routes.rs b/src/routes.rs index 6829997..03fe4e5 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -181,7 +181,7 @@ pub fn build_router(state: AppState) -> Result { router = router.layer(RateLimitLayer::with_trusted_proxies( config.rate_limit_rps, config.rate_limit_burst, - &config.trusted_proxies, + trusted_proxies, )?); } else { info!("Rate limiting disabled (RATE_LIMIT_RPS=0)"); From 237abaa8d4027e467396b8caed42eaedf5274b7d Mon Sep 17 00:00:00 2001 From: mlevkov Date: Fri, 3 Jul 2026 23:51:43 -0700 Subject: [PATCH 15/20] fix(resilience,observability): round-2 hardening of the round-1 fixes Review round 2 (silent-failure R2-1..R2-8, simplifier, test-analyzer): Resilience: - failed/timed-out reconnect attempts now shutdown() the new client before retrying - the same zombie-heartbeat leak class round 1 fixed for the success path; per-attempt connect bound halved so failure exits reach the cleanup arms before reconnect_bounded's outer deadline aborts the session mid-connect - old-client shutdown failure now logs at warn (it exists to kill the heartbeat; silence at debug hid the exact failure it prevents) - health task logs the recovery transition at info and repeat outages at warn (operators no longer infer recovery from silence) Observability: - metrics exporter starts BEFORE the Iggy client so startup-window reconnect/breaker metrics are not dropped by the no-op recorder; gauges seeded at startup (absent-series was indistinguishable from healthy); Config::metrics_addr() is the single source of truth (honors HOST; main.rs no longer builds its own address) and the orphaned try_init_metrics is gone - send/poll duration histograms are now recorded, and failed sends/batches record status=failure (outages previously looked like traffic stopping); the never-wired request-duration histogram is removed rather than documented-but-empty - circuit breaker: open_now() unifies the three Open transitions; force_open now emits the open counter/gauge and force_close resets the gauge (Prometheus could silently drift from internal counters) CI: deny job runs 'cargo deny check' (all sections - bans and sources were configured but never checked) Tests (+7 unit, +3 integration; suite 158 lib / 29 integration): - invalid-CIDR fail-fast, IPv6/zero-prefix CIDR containment - backoff max-below-floor and zero-max edges - auth budgets derived from constants; per-IP failure-bucket isolation - wire-level spoofed-XFF rotation cannot bypass the rate limiter (secure fixture now runs with TRUSTED_PROXIES enforcement on) - count=0 returns 400 on both poll routes - initialize_defaults idempotence + live health_check() true path - semantic tests poll-until-deadline instead of fixed sleeps; secure fixture gets the same fast-fail on server-task death --- .github/workflows/ci.yml | 4 +- src/config.rs | 11 +- src/iggy_client/circuit_breaker.rs | 24 ++-- src/iggy_client/mod.rs | 37 +++++- src/main.rs | 39 ++++--- src/metrics.rs | 31 +----- src/middleware/auth.rs | 38 ++++++- src/services/consumer.rs | 5 +- src/services/producer.rs | 27 ++++- src/state.rs | 15 ++- tests/integration_tests.rs | 173 +++++++++++++++++++++++++++-- 11 files changed, 310 insertions(+), 94 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d03a34..b723a3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -181,8 +181,8 @@ jobs: - name: Install cargo-deny uses: taiki-e/install-action@cargo-deny - - name: Check advisories and licenses - run: cargo deny check advisories licenses + - name: Check dependency policy (advisories, bans, licenses, sources) + run: cargo deny check # ========================================================================== # Final status check (for branch protection) diff --git a/src/config.rs b/src/config.rs index baa5d97..9c45de7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -301,15 +301,14 @@ impl Config { self.metrics_port > 0 } - /// Get the metrics endpoint address. + /// Get the metrics endpoint address (binds the same host as the API). /// /// Returns `None` if metrics are disabled (port = 0). - pub fn metrics_addr(&self) -> Option { + /// This is the single source of truth for the metrics bind address; + /// `main.rs` starts the exporter from it. + pub fn metrics_addr(&self) -> Option { if self.metrics_enabled() { - Some(std::net::SocketAddr::from(( - [0, 0, 0, 0], - self.metrics_port, - ))) + Some(format!("{}:{}", self.host, self.metrics_port)) } else { None } diff --git a/src/iggy_client/circuit_breaker.rs b/src/iggy_client/circuit_breaker.rs index a3de1e2..959be02 100644 --- a/src/iggy_client/circuit_breaker.rs +++ b/src/iggy_client/circuit_breaker.rs @@ -272,11 +272,7 @@ impl CircuitBreaker { ); if state.consecutive_failures >= self.config.failure_threshold { - state.state = CircuitState::Open; - state.opened_at = Some(Instant::now()); - self.times_opened.fetch_add(1, Ordering::Relaxed); - crate::metrics::record_circuit_breaker_open(); - crate::metrics::set_circuit_breaker_state(2); + self.open_now(&mut state); warn!( failures = state.consecutive_failures, open_duration = ?self.config.open_duration, @@ -286,12 +282,8 @@ impl CircuitBreaker { } CircuitState::HalfOpen => { // Any failure in half-open state reopens the circuit - state.state = CircuitState::Open; - state.opened_at = Some(Instant::now()); state.consecutive_successes = 0; - self.times_opened.fetch_add(1, Ordering::Relaxed); - crate::metrics::record_circuit_breaker_open(); - crate::metrics::set_circuit_breaker_state(2); + self.open_now(&mut state); warn!("Circuit breaker reopened after failure in HalfOpen state"); } CircuitState::Open => { @@ -324,16 +316,26 @@ impl CircuitBreaker { state.opened_at = None; state.consecutive_failures = 0; state.consecutive_successes = 0; + crate::metrics::set_circuit_breaker_state(0); info!("Circuit breaker forcibly closed"); } /// Force the circuit to open (for testing or manual intervention). pub async fn force_open(&self) { let mut state = self.state.write().await; + self.open_now(&mut state); + warn!("Circuit breaker forcibly opened"); + } + + /// Transition to Open, keeping internal counters and Prometheus metrics + /// in lockstep. Shared by the threshold, half-open-failure, and forced + /// open transitions so the gauge cannot drift from the atomics. + fn open_now(&self, state: &mut CircuitBreakerState) { state.state = CircuitState::Open; state.opened_at = Some(Instant::now()); self.times_opened.fetch_add(1, Ordering::Relaxed); - warn!("Circuit breaker forcibly opened"); + crate::metrics::record_circuit_breaker_open(); + crate::metrics::set_circuit_breaker_state(2); } } diff --git a/src/iggy_client/mod.rs b/src/iggy_client/mod.rs index 8dbc482..71c372b 100644 --- a/src/iggy_client/mod.rs +++ b/src/iggy_client/mod.rs @@ -360,17 +360,25 @@ impl IggyClientWrapper { match IggyClient::from_connection_string(&self.config.iggy_connection_string) { Ok(new_client) => { // Bound the connect: the SDK's internal reconnection would - // otherwise retry inside connect() indefinitely. - match tokio::time::timeout(self.config.operation_timeout, new_client.connect()) - .await - { + // otherwise retry inside connect() indefinitely. The bound + // is HALF the operation timeout so a failed attempt exits + // through the cleanup arms below before reconnect_bounded's + // outer deadline aborts the whole session (an abort drops + // the client mid-connect with no chance to shut it down). + let attempt_timeout = self.config.operation_timeout / 2; + match tokio::time::timeout(attempt_timeout, new_client.connect()).await { Ok(Ok(())) => {} Ok(Err(e)) => { warn!(attempt, error = %e, "Reconnection attempt failed"); + // Best-effort cleanup: connect() may already have + // spawned the SDK's detached heartbeat task, which + // Drop alone does not stop. + let _ = new_client.shutdown().await; continue; } Err(_) => { warn!(attempt, "Reconnection attempt timed out"); + let _ = new_client.shutdown().await; continue; } } @@ -383,7 +391,10 @@ impl IggyClientWrapper { let old_client = std::mem::replace(&mut *client_guard, new_client); drop(client_guard); if let Err(e) = old_client.shutdown().await { - debug!(error = %e, "Old client shutdown returned an error (ignored)"); + // This call exists to kill the old heartbeat task; if + // it fails, a zombie connection may persist - worth a + // warning (reconnects are rare, no spam risk). + warn!(error = %e, "Old client shutdown failed; its heartbeat task may persist"); } self.state.set_connected(true); @@ -1118,6 +1129,22 @@ mod tests { assert!(delay >= 100, "delay {} below MIN_RECONNECT_DELAY_MS", delay); } + #[test] + fn test_backoff_max_below_floor_collapses_to_max() { + // When the configured max is below MIN_RECONNECT_DELAY_MS, the max + // wins (the floor must never raise a delay above the configured cap). + assert_eq!(backoff_delay_ms(1, 1000, 50, 0.5), 50); + assert_eq!(backoff_delay_ms(10, 1000, 50, 0.999_999), 50); + } + + #[test] + fn test_backoff_zero_max_yields_zero_delay() { + // Degenerate but explicit: RECONNECT_MAX_DELAY_MS=0 produces + // zero-delay retries (no panic, clamp floor collapses to 0). + assert_eq!(backoff_delay_ms(1, 1000, 0, 0.5), 0); + assert_eq!(backoff_delay_ms(100, 1000, 0, 0.0), 0); + } + #[test] fn test_backoff_does_not_overflow_at_huge_attempt_counts() { // Infinite retries are the default; attempt counts far past the diff --git a/src/main.rs b/src/main.rs index 89a8b57..18a3382 100644 --- a/src/main.rs +++ b/src/main.rs @@ -44,6 +44,27 @@ async fn run() -> Result<(), exitcode::ExitCode> { "Configuration loaded" ); + // Start the Prometheus metrics exporter FIRST (it depends only on + // config) so metrics recorded during connection/initialization are not + // silently dropped by the no-op default recorder. A bind failure fails + // startup: silently missing metrics would defeat alerting. + if let Some(metrics_addr) = config.metrics_addr() { + let metrics_addr: SocketAddr = metrics_addr.parse().map_err(|e| { + error!("Invalid metrics address: {e}"); + exitcode::CONFIG + })?; + iggy_sample::metrics::init_metrics(metrics_addr).map_err(|e| { + error!("Failed to start metrics exporter: {e}"); + exitcode::UNAVAILABLE + })?; + // Seed the gauges so every series exists from the first scrape - + // absent-series is otherwise indistinguishable from healthy. + iggy_sample::metrics::set_connection_status(false); + iggy_sample::metrics::set_circuit_breaker_state(0); + } else { + info!("Metrics exporter disabled (METRICS_PORT=0)"); + } + // Initialize Iggy client info!("Connecting to Iggy server..."); let iggy_client = IggyClientWrapper::new(config.clone()).await.map_err(|e| { @@ -51,6 +72,7 @@ async fn run() -> Result<(), exitcode::ExitCode> { exitcode::UNAVAILABLE })?; info!("Successfully connected to Iggy server"); + iggy_sample::metrics::set_connection_status(true); // Initialize default stream and topic info!("Initializing default stream and topic..."); @@ -63,23 +85,6 @@ async fn run() -> Result<(), exitcode::ExitCode> { config.default_stream, config.default_topic ); - // Start the Prometheus metrics exporter (dedicated listener). A bind - // failure fails startup: silently missing metrics would defeat alerting. - if config.metrics_port > 0 { - let metrics_addr: SocketAddr = format!("{}:{}", config.host, config.metrics_port) - .parse() - .map_err(|e| { - error!("Invalid metrics address: {e}"); - exitcode::CONFIG - })?; - iggy_sample::metrics::init_metrics(metrics_addr).map_err(|e| { - error!("Failed to start metrics exporter: {e}"); - exitcode::UNAVAILABLE - })?; - } else { - info!("Metrics exporter disabled (METRICS_PORT=0)"); - } - // Build application state and router let state = AppState::new(iggy_client, config.clone()); let app = build_router(state.clone()).map_err(|e| { diff --git a/src/metrics.rs b/src/metrics.rs index 57d2c74..71b6899 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -13,7 +13,6 @@ //! - `iggy_circuit_breaker_rejections_total` - Requests rejected by circuit breaker //! //! ## Histograms -//! - `iggy_request_duration_seconds` - Request duration (with labels: endpoint, method, status) //! - `iggy_send_duration_seconds` - Message send duration //! - `iggy_poll_duration_seconds` - Message poll duration //! @@ -24,20 +23,19 @@ //! # Usage //! //! ```rust,ignore -//! use iggy_sample::metrics::{init_metrics, record_message_sent, record_request_duration}; +//! use iggy_sample::metrics::{init_metrics, record_message_sent}; //! //! // Initialize metrics (call once at startup) //! init_metrics(); //! //! // Record metrics in handlers //! record_message_sent("my-stream", "my-topic", "success"); -//! record_request_duration("/messages", "POST", "200", 0.045); //! ``` use metrics::{counter, describe_counter, describe_gauge, describe_histogram, gauge, histogram}; use metrics_exporter_prometheus::PrometheusBuilder; use std::net::SocketAddr; -use tracing::{error, info}; +use tracing::info; /// Metric names as constants for consistency. pub mod names { @@ -46,7 +44,6 @@ pub mod names { pub const CONNECTION_RECONNECTS_TOTAL: &str = "iggy_connection_reconnects_total"; pub const CIRCUIT_BREAKER_OPENS_TOTAL: &str = "iggy_circuit_breaker_opens_total"; pub const CIRCUIT_BREAKER_REJECTIONS_TOTAL: &str = "iggy_circuit_breaker_rejections_total"; - pub const REQUEST_DURATION_SECONDS: &str = "iggy_request_duration_seconds"; pub const SEND_DURATION_SECONDS: &str = "iggy_send_duration_seconds"; pub const POLL_DURATION_SECONDS: &str = "iggy_poll_duration_seconds"; pub const CONNECTION_STATUS: &str = "iggy_connection_status"; @@ -94,10 +91,6 @@ pub fn init_metrics(metrics_addr: SocketAddr) -> Result<(), String> { "Total number of requests rejected by circuit breaker" ); - describe_histogram!( - names::REQUEST_DURATION_SECONDS, - "HTTP request duration in seconds" - ); describe_histogram!( names::SEND_DURATION_SECONDS, "Message send operation duration in seconds" @@ -120,15 +113,6 @@ pub fn init_metrics(metrics_addr: SocketAddr) -> Result<(), String> { Ok(()) } -/// Try to initialize metrics, logging any errors but not failing. -/// -/// This is useful for cases where metrics are optional. -pub fn try_init_metrics(metrics_addr: SocketAddr) { - if let Err(e) = init_metrics(metrics_addr) { - error!(error = %e, "Failed to initialize metrics, continuing without metrics"); - } -} - // ============================================================================= // Counter Recording Functions // ============================================================================= @@ -170,12 +154,6 @@ pub fn record_circuit_breaker_rejection() { // Histogram Recording Functions // ============================================================================= -/// Record HTTP request duration. -pub fn record_request_duration(endpoint: &str, method: &str, status: &str, duration_secs: f64) { - histogram!(names::REQUEST_DURATION_SECONDS, "endpoint" => endpoint.to_string(), "method" => method.to_string(), "status" => status.to_string()) - .record(duration_secs); -} - /// Record message send duration. pub fn record_send_duration(stream: &str, topic: &str, duration_secs: f64) { histogram!(names::SEND_DURATION_SECONDS, "stream" => stream.to_string(), "topic" => topic.to_string()) @@ -222,11 +200,6 @@ mod tests { record_messages_polled("test-stream", "test-topic", 10); } - #[test] - fn test_record_request_duration() { - record_request_duration("/messages", "POST", "200", 0.1); - } - #[test] fn test_set_connection_status() { set_connection_status(true); diff --git a/src/middleware/auth.rs b/src/middleware/auth.rs index 6827103..e55b56a 100644 --- a/src/middleware/auth.rs +++ b/src/middleware/auth.rs @@ -421,9 +421,12 @@ mod tests { let auth = ApiKeyAuth::with_defaults(Some("secret".to_string())); let mut svc = auth.layer(OkService); - // Far more valid requests than the failure budget (10/min + burst 5). - // A regression to counting every request would 429 partway through. - for i in 0..40 { + // Far more valid requests than the failure budget. A regression to + // counting every request would 429 partway through. Derived from the + // constants so raising the budget cannot silently weaken the test. + let beyond_budget = + 2 * (DEFAULT_AUTH_FAILURE_LIMIT.get() + DEFAULT_AUTH_FAILURE_BURST.get()) + 10; + for i in 0..beyond_budget { let resp = svc.call(request_with_key(Some("secret"))).await.unwrap(); assert_eq!( resp.status(), @@ -441,8 +444,10 @@ mod tests { // All requests share the "unknown" IP bucket (no proxy headers). // Failures should 401 until the budget is exhausted, then 429. + let beyond_budget = + 2 * (DEFAULT_AUTH_FAILURE_LIMIT.get() + DEFAULT_AUTH_FAILURE_BURST.get()) + 10; let mut saw_429 = false; - for _ in 0..40 { + for _ in 0..beyond_budget { let resp = svc.call(request_with_key(Some("wrong"))).await.unwrap(); match resp.status() { StatusCode::UNAUTHORIZED => {} @@ -461,6 +466,31 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); } + #[tokio::test] + async fn test_failure_buckets_are_per_ip() { + let auth = ApiKeyAuth::with_defaults(Some("secret".to_string())); + let mut svc = auth.layer(OkService); + let beyond_budget = + 2 * (DEFAULT_AUTH_FAILURE_LIMIT.get() + DEFAULT_AUTH_FAILURE_BURST.get()) + 10; + + // Exhaust IP A's failure budget (default config trusts headers). + for _ in 0..beyond_budget { + let mut req = request_with_key(Some("wrong")); + req.headers_mut() + .insert("x-forwarded-for", "203.0.113.1".parse().unwrap()); + let _ = svc.call(req).await.unwrap(); + } + + // A failing request from IP B has a fresh budget: 401, not 429. + // A regression collapsing all clients into one global bucket would + // return 429 here. + let mut req = request_with_key(Some("wrong")); + req.headers_mut() + .insert("x-forwarded-for", "203.0.113.2".parse().unwrap()); + let resp = svc.call(req).await.unwrap(); + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + } + #[test] fn test_api_key_auth_enabled() { let auth = ApiKeyAuth::with_defaults(Some("secret".to_string())); diff --git a/src/services/consumer.rs b/src/services/consumer.rs index e33fce0..8a92cae 100644 --- a/src/services/consumer.rs +++ b/src/services/consumer.rs @@ -69,7 +69,10 @@ impl ConsumerService { params: PollParams, ) -> AppResult { let partition_id = params.partition_id; - let polled = self.client.poll_messages(stream, topic, params).await?; + let start = std::time::Instant::now(); + let result = self.client.poll_messages(stream, topic, params).await; + crate::metrics::record_poll_duration(stream, topic, start.elapsed().as_secs_f64()); + let polled = result?; let messages = self.parse_messages(&polled.messages); let message_count = messages.len(); diff --git a/src/services/producer.rs b/src/services/producer.rs index a783059..3011ca5 100644 --- a/src/services/producer.rs +++ b/src/services/producer.rs @@ -57,9 +57,16 @@ impl ProducerService { event: &Event, partition_key: Option<&str>, ) -> AppResult { - self.client + let start = std::time::Instant::now(); + let result = self + .client .send_event(stream, topic, event, partition_key) - .await?; + .await; + crate::metrics::record_send_duration(stream, topic, start.elapsed().as_secs_f64()); + if result.is_err() { + crate::metrics::record_message_sent(stream, topic, "failure"); + } + result?; self.messages_sent.fetch_add(1, Ordering::Relaxed); crate::metrics::record_message_sent(stream, topic, "success"); @@ -96,9 +103,21 @@ impl ProducerService { events: &[Event], partition_key: Option<&str>, ) -> AppResult> { - self.client + let start = std::time::Instant::now(); + let result = self + .client .send_events_batch(stream, topic, events, partition_key) - .await?; + .await; + crate::metrics::record_send_duration(stream, topic, start.elapsed().as_secs_f64()); + if result.is_err() { + crate::metrics::record_messages_sent_batch( + stream, + topic, + "failure", + events.len() as u64, + ); + } + result?; self.messages_sent .fetch_add(events.len() as u64, Ordering::Relaxed); diff --git a/src/state.rs b/src/state.rs index 6d40403..94becea 100644 --- a/src/state.rs +++ b/src/state.rs @@ -223,6 +223,11 @@ impl AppState { let mut ticker = interval(interval_duration); ticker.tick().await; // Skip first immediate tick + // Track the previous probe result so the recovery transition is + // visible at info - operators should not have to infer recovery + // from the warnings going quiet. + let mut was_connected = true; + loop { tokio::select! { biased; @@ -237,11 +242,13 @@ impl AppState { // failures, so this probe is what keeps the connection // state - and therefore /health and /ready - truthful. let connected = iggy_client.health_check().await; - if !connected { - warn!("Health check: Iggy connection is down"); - } else { - trace!("Health check: Iggy connection OK"); + match (was_connected, connected) { + (true, false) => warn!("Health check: Iggy connection is down"), + (false, false) => warn!("Health check: Iggy connection still down"), + (false, true) => info!("Health check: Iggy connection restored"), + (true, true) => trace!("Health check: Iggy connection OK"), } + was_connected = connected; } } } diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index ca00805..385bd7c 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -227,9 +227,7 @@ impl TestFixture { // error: it panicked or returned early. Surfacing this // immediately beats 30s of polling a dead server followed // by a generic timeout message that hides the real cause. - panic!( - "Server task exited unexpectedly (likely panicked) before becoming ready" - ); + panic!("Server task exited (panic or early return) before becoming ready"); } Ok(Ok(())) | Err(tokio::sync::oneshot::error::TryRecvError::Empty) => {} } @@ -349,6 +347,24 @@ fn generic_event(marker: &str, partition_key: Option<&str>) -> serde_json::Value } } +/// Poll the given queries repeatedly (idempotent offset-0 reads) until their +/// combined count reaches `expected` or a 10s deadline elapses; returns the +/// last per-query counts. Replaces fixed sleeps so slow send-to-poll +/// visibility under CI load retries instead of flaking. +async fn wait_for_total(fixture: &TestFixture, queries: &[String], expected: u64) -> Vec { + let deadline = tokio::time::Instant::now() + Duration::from_secs(10); + loop { + let mut counts = Vec::with_capacity(queries.len()); + for q in queries { + counts.push(poll_count(fixture, q).await); + } + if counts.iter().sum::() >= expected || tokio::time::Instant::now() >= deadline { + return counts; + } + sleep(Duration::from_millis(100)).await; + } +} + /// Poll one partition and return the number of messages. async fn poll_count(fixture: &TestFixture, query: &str) -> u64 { let response = fixture @@ -384,7 +400,24 @@ async fn test_poll_without_offset_advances_committed_offset() { .expect("Send request failed"); assert!(response.status().is_success()); } - sleep(Duration::from_millis(300)).await; + + // Wait for visibility with idempotent offset-0 reads (consumer 99, no + // commit) so the committed-offset rounds below are deterministic under + // CI load. + let visible = wait_for_total( + &fixture, + &[ + "partition_id=0&consumer_id=99&count=10&offset=0".to_string(), + "partition_id=1&consumer_id=99&count=10&offset=0".to_string(), + ], + 3, + ) + .await; + assert_eq!( + visible.iter().sum::(), + 3, + "messages never became visible" + ); // First round: no offset param (PollingStrategy::next) + auto_commit, // across both partitions of the 2-partition test topic. @@ -440,10 +473,19 @@ async fn test_same_partition_key_routes_to_single_partition() { .expect("Send request failed"); assert!(response.status().is_success()); } - sleep(Duration::from_millis(300)).await; - - let p0 = poll_count(&fixture, "partition_id=0&consumer_id=88&count=50&offset=0").await; - let p1 = poll_count(&fixture, "partition_id=1&consumer_id=88&count=50&offset=0").await; + let counts = wait_for_total( + &fixture, + &[ + "partition_id=0&consumer_id=88&count=50&offset=0".to_string(), + "partition_id=1&consumer_id=88&count=50&offset=0".to_string(), + ], + 5, + ) + .await; + let (p0, p1) = match counts.as_slice() { + [p0, p1] => (*p0, *p1), + other => panic!("expected two partition counts, got {other:?}"), + }; assert_eq!(p0 + p1, 5, "all keyed messages should be delivered"); assert!( @@ -452,6 +494,69 @@ async fn test_same_partition_key_routes_to_single_partition() { ); } +/// Both poll routes must reject count=0 with 400 at the HTTP boundary +/// (the SDK would otherwise fail it with a misleading 500). +#[tokio::test] +async fn test_poll_count_zero_returns_400_on_both_routes() { + let fixture = TestFixture::new().await; + + for path in [ + "/messages?count=0", + "/streams/test-stream/topics/test-events/messages?count=0", + ] { + let response = fixture + .client + .get(fixture.url(path)) + .send() + .await + .expect("Request failed"); + assert_eq!( + response.status().as_u16(), + 400, + "count=0 on {} should be a client error", + path + ); + } +} + +/// Pins initialize_defaults idempotence (the ensure_* already-exists arms) +/// and the live health_check() true path against a real server. +#[tokio::test] +async fn test_wrapper_defaults_idempotent_and_health_check_live() { + use iggy_sample::{Config, IggyClientWrapper}; + + let (_container, iggy) = IggyContainer::start().await; + + let config = Config { + iggy_connection_string: iggy.connection_string(), + default_stream: "idempotence-stream".to_string(), + default_topic: "idempotence-events".to_string(), + topic_partitions: 2, + operation_timeout: Duration::from_secs(30), + metrics_port: 0, + ..Config::default() + }; + + let wrapper = IggyClientWrapper::new(config) + .await + .expect("wrapper should connect"); + + // First call creates; second call must take the already-exists arms + // without error (two replicas starting concurrently must not crash-loop). + wrapper + .initialize_defaults() + .await + .expect("first initialize_defaults"); + wrapper + .initialize_defaults() + .await + .expect("second initialize_defaults (idempotence)"); + + // Live health probe: ping succeeds and drives the connected flag. + assert!(wrapper.health_check().await, "live ping should succeed"); + assert!(wrapper.is_connected()); +} + #[tokio::test] async fn test_send_and_poll_message() { let fixture = TestFixture::new().await; @@ -1214,7 +1319,10 @@ impl SecureTestFixture { api_key: Some(api_key.to_string()), auth_bypass_paths: vec!["/health".to_string(), "/ready".to_string()], cors_allowed_origins: vec!["*".to_string()], - trusted_proxies: vec![], // Empty = trust all (test mode) + // Trusted-proxy enforcement ON: the test client's peer address is + // 127.0.0.1 (untrusted), so spoofed forwarded headers must be + // ignored - this makes the enforcement path itself wire-tested. + trusted_proxies: vec!["10.0.0.0/8".to_string()], log_level: "warn".to_string(), stats_cache_ttl: Duration::from_secs(5), metrics_port: 0, // Disabled for tests @@ -1257,8 +1365,15 @@ impl SecureTestFixture { let max_attempts = 60; for attempt in 1..=max_attempts { - if let Ok(Err(e)) = error_rx.try_recv() { - panic!("Server failed to start: {}", e); + // Fail fast if the server task died instead of polling a corpse + // for 30s and reporting a generic timeout (mirrors the standard + // fixture's handling). + match error_rx.try_recv() { + Ok(Err(e)) => panic!("Server failed to start: {}", e), + Err(tokio::sync::oneshot::error::TryRecvError::Closed) => { + panic!("Server task exited (panic or early return) before becoming ready"); + } + Ok(Ok(())) | Err(tokio::sync::oneshot::error::TryRecvError::Empty) => {} } match client.get(&health_url).send().await { @@ -1374,6 +1489,42 @@ async fn test_auth_invalid_api_key() { // covered by deterministic unit tests in src/middleware/auth.rs (the secure // fixture's general rate limiter (5 RPS) would dominate a wire-level test). +/// Wire-level proof that TRUSTED_PROXIES enforcement flows through the real +/// axum serve path (ConnectInfo included): an untrusted peer (127.0.0.1) +/// rotating spoofed X-Forwarded-For values must still hit the per-IP rate +/// limit, because every request is keyed on the actual peer address. A +/// regression that trusts headers again gives each spoofed value a fresh +/// bucket and this test fails. +#[tokio::test] +async fn test_spoofed_xff_rotation_cannot_bypass_rate_limit() { + let fixture = SecureTestFixture::new().await; + + // Quota is 5 RPS + burst 2; 30 rapid requests with rotating spoofed + // XFF values must trip the limiter if they share one bucket. + let mut saw_429 = false; + for i in 0..30 { + let response = fixture + .client + .get(fixture.url("/stats")) + .header("x-api-key", &fixture.api_key) + .header("x-forwarded-for", format!("203.0.113.{}", i + 1)) + .send() + .await + .expect("Request failed"); + + if response.status().as_u16() == 429 { + saw_429 = true; + break; + } + } + + assert!( + saw_429, + "rotating spoofed X-Forwarded-For values bypassed the per-IP rate \ + limit despite TRUSTED_PROXIES enforcement" + ); +} + #[tokio::test] async fn test_health_bypasses_auth() { let fixture = SecureTestFixture::new().await; From 9dedd4c911e108f1a7d0b7afb9600dccd5294fbe Mon Sep 17 00:00:00 2001 From: mlevkov Date: Fri, 3 Jul 2026 23:53:11 -0700 Subject: [PATCH 16/20] docs(review): round-2 artifact, TD-01 trigger history, final count sync - docs/code-reviews/session-01-round2.md: provenance (including the two lens failures on session limits), verification results for every round-1 fix, disposition table for all round-2 findings - TD-2026-07-01: records that its own trigger fired in-session, accepted explicitly and re-armed - test counts synced to 158 lib / 29 integration / 18 model --- CLAUDE.md | 4 +- README.md | 2 +- docs/code-reviews/session-01-round2.md | 84 ++++++++++++++++++++++++++ docs/tech-debt/TD-2026-07-01.md | 9 +++ 4 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 docs/code-reviews/session-01-round2.md diff --git a/CLAUDE.md b/CLAUDE.md index c90096f..d0016f6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -427,10 +427,10 @@ environment: ### Running Tests ```bash -# Unit tests (147 tests) +# Unit tests (158 tests) cargo test --lib -# Integration tests (26 tests, requires Docker for testcontainers) +# Integration tests (29 tests, requires Docker for testcontainers) cargo test --test integration_tests # Model tests (18 tests) diff --git a/README.md b/README.md index e02614b..f7da8ba 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Apache Iggy is capable of processing millions of messages per second with ultra- ### Development & Testing - Docker Compose setup for local development -- Comprehensive test suite (147 unit tests, 26 integration tests, 18 model tests) +- Comprehensive test suite (158 unit tests, 29 integration tests, 18 model tests) - Integration tests with testcontainers (auto-spins Iggy server) - Fuzz testing for input validation functions diff --git a/docs/code-reviews/session-01-round2.md b/docs/code-reviews/session-01-round2.md new file mode 100644 index 0000000..d3fd7e6 --- /dev/null +++ b/docs/code-reviews/session-01-round2.md @@ -0,0 +1,84 @@ +# Code Review — Session 01, Round 2 (post-remediation) + +**Branch:** `chore/iggy-0.10-deps-refresh` +**Scope:** verification of the Round 1 remediation (commits `6ff23ec..61b40ca`) +plus regression hunting in the fixes themselves. +**Dates:** 2026-07-03 → 2026-07-04 (interrupted by a session usage limit and +resumed). + +## Provenance + +Config: **full**, same basis as Round 1 (Fable 5 parent, both review plugins, +all agents pinned to `fable`). **Suite degradation, recorded:** 6 of 8 lenses +delivered Round 2 reports (consistency, architect→partial, logic-security, +types, silent-failure, comments→none, tests, simplifier). The **architect** +and **comment-analyzer** agents were killed by the session usage limit +mid-review; resume requests were sent after the reset but no reports had +arrived when this artifact was written — their scope was substantially +covered by overlap (silent-failure independently audited the resilience +commits including SDK-source verification; consistency audited all prose and +CHANGELOG claims). Any late reports will be triaged and appended. +Verification: Round 2 findings were **verified inline by the reporting +agents themselves** (each report cites file:line it re-read, several +re-ran the suites and cargo-deny live); no separate verifier pass was run +for Round 2. + +## Round 1 fixes — verification results + +Every Round 1 remediation was independently confirmed as landed and +effective by at least one Round 2 lens, most by several: RUSTSEC patches +(live cargo-audit), iggy 0.10/server 0.8.0 pin, deny.toml migration + CI +gate, error classification at all 15 network call sites, live health +probes, trusted-proxy enforcement through the real serve path, failure-only +auth metering, metrics exporter startup, count=0 → 400, `Identifier::named`, +`PollMessagesRequest` removal, test-count/dependency-table corrections. +**No Round 1 fix was found regressed or ineffective.** + +## Round 2 findings and their disposition + +### Fixed in-branch (commits 56d5fd0, c23bef2, 237abaa) + +| Finding | Source | Fix | +|---------|--------|-----| +| Port migration incomplete (2 config tables, architecture.md docker/K8s examples, 4 rustdoc curls) — falsified a CHANGELOG claim | consistency (MED) | completed everywhere; CHANGELOG claim now true | +| CHANGELOG duplicate Unreleased headings + stale Security block contradicting deny.toml | consistency (MED) | sections merged, stale block removed | +| First-entry XFF parsing defeated by APPENDING proxies (the common default) — the exact attack the round-1 fix targeted | logic-sec (MED) | rightmost-untrusted resolution + IpAddr parsing, peer fallback on garbage; 6 new tests | +| Forwarded values used as limiter keys without parsing (unlimited key minting) | logic-sec (MED) | honored values now parse as IpAddr or fall back to peer | +| Trusted-proxy config parsed twice; routes.rs "shared" comment false | types (MED) | `RateLimitLayer::with_trusted_proxies` takes the shared `Arc` | +| Zombie-client leak on failed/aborted reconnect attempts (same class round 1 fixed for success path) | silent-failure (MED) | shutdown() on failure arms; per-attempt bound halved so cleanup arms are reachable | +| Three histograms described but never emitted; `status` label only ever "success" (outages invisible in metrics) | silent-failure (MED) | send/poll durations + failure statuses wired; unwired request-duration histogram deleted | +| Metrics recorder installed after client init (startup window dropped); gauges never seeded; metrics_addr()/try_init_metrics orphaned+divergent | silent-failure (LOW), consistency | init first, gauges seeded, single canonical accessor honoring HOST | +| force_open/force_close diverge Prometheus gauge from internal counters | simplifier (LOW, behavioral) | `open_now()` helper unifies all three Open transitions | +| Old-client shutdown error at debug; recovery transition only at trace | silent-failure (LOW) | warn / info respectively | +| Missing-ConnectInfo warning per-request per-middleware (latent log flood) | silent-failure, logic-sec (LOW) | warn once per process | +| Dead surface: `is_trusted(&str)`, `extract_client_ip`/`UNKNOWN_IP` re-exports, stale auth.rs comment, ip.rs diagram | 3 lenses converged (LOW) | removed/rewired; `extract_client_ip` is now the documented fallback the validated variant delegates to | +| CI deny job checked only advisories+licenses while [bans]/[sources] were configured-but-unenforced | tests (LOW) | `cargo deny check` (all sections) | +| Dockerfile: HEALTHCHECK invokes curl not present in image (pre-existing); metrics port unexposed | consistency (LOW-MED) | curl installed; EXPOSE 8000 9090 | +| Semantic integration tests flake-prone (single fixed sleep + exact asserts) | tests (MED flakiness) | poll-until-deadline visibility waits | +| Missing high-value tests: invalid-CIDR fail-fast, wire-level spoof rotation, health_check live path, defaults idempotence, count=0 routes, IPv6/zero-prefix CIDR containment, backoff floor/zero-max edges, per-IP failure-bucket isolation, budget bounds derived from constants | tests (HIGH..LOW) | all added (+7 unit, +3 integration; secure fixture now runs with enforcement ON) | +| Fixture Ok(())-exit misattributed as panic; secure fixture lacked fast-fail | silent-failure (INFO), tests | both fixtures fail fast with accurate wording | +| App metrics endpoint wired but undocumented | consistency (LOW-MED) | documented in README/CLAUDE.md observability sections | + +### Accepted / deferred (recorded, not fixed) + +- **TD-2026-07-01 trigger fired in-session** [tests A2, HIGH risk-posture]: + the with_reconnect matrix was due by the TD's own trigger; accepted + explicitly as session debt, trigger re-armed (see the TD's history note). +- Key-length timing oracle in `constant_time_eq` (pre-existing, LOW) — + optional digest-comparison hardening noted in the Round 2 report. +- `RateLimitError` name now broader than its scope (LOW) — cosmetic rename + candidate (`MiddlewareConfigError`) for a future pass. +- `PollParams::with_count(0)` remains representable at the library level + (INFO) — boundary validation covers all production constructors (verified); + NonZeroU32 stays in the backlog. +- Poll count silently clamps while batch send 400s over limit (INFO, + documented asymmetry) — no change. +- `.env.example` remains a partial listing (softened claim instead). +- Branch-protection requirement on "CI Success" not verifiable in-repo — + operator action item. + +## Gate status at close + +`cargo fmt --check` ✓ · `clippy --all-targets -D warnings` ✓ · +158 lib + 29 integration + 18 model tests ✓ · `cargo audit` 0 vulns ✓ · +`cargo deny check` all four sections ✓. diff --git a/docs/tech-debt/TD-2026-07-01.md b/docs/tech-debt/TD-2026-07-01.md index 405c8a2..5ad99f5 100644 --- a/docs/tech-debt/TD-2026-07-01.md +++ b/docs/tech-debt/TD-2026-07-01.md @@ -28,3 +28,12 @@ integration coverage of the happy paths and the semantic pins Any behavioral change to `with_reconnect`, `retry_once`, or `reconnect_bounded` MUST land together with this matrix (tokio `time::pause()`-based, one test per branch). + +## Trigger history + +Round 2 of the session-01 review correctly noted this trigger technically +fired within session 01 itself (the same session that created the record +also rewrote these paths). Accepted explicitly as debt for this session: +the composition changes were review-verified twice and the live paths are +integration-tested; the trigger is RE-ARMED — the next behavioral change +to these functions must not ship without the matrix. From d9db90765b90fee0c55411493662c59c4568e1c3 Mon Sep 17 00:00:00 2001 From: mlevkov Date: Sat, 4 Jul 2026 01:00:15 -0700 Subject: [PATCH 17/20] fix(resilience,docs): remediate late round-2 reports (8/8 lenses complete) The two review agents lost to the session limit delivered after reset. Architect (HIGH + 2 MEDIUM, all fixed): - reconnect sessions now run in a SPAWNED task awaited under the caller's timeout: dropping the reconnect future at its commit-point awaits (connect-success -> swap/shutdown) leaked a fully-connected client whose detached SDK heartbeat could never be shut down, with a realistic trigger (health-probe read guard delaying the write lock past the deadline); JoinHandle drop does not cancel, so the session now completes in the background and later callers join as followers - wait_for_reconnection had a genuine lost-wakeup race: tokio Notify registers on first poll, not creation, and notify_waiters stores no permit - now enable()d before the is_reconnecting check (the old comment claimed the opposite semantics) - error classification extended to all four transports the connection string supports (QuicError, HttpError, EmptyResponse, the WebSocket family) - QUIC/HTTP/WS deployments would otherwise resurrect the round-1 dead-reconnect-path bug; HttpResponseError deliberately excluded (server answered = application error) with a pinning test - RECONNECT_MAX_DELAY_MS=0 now rejected at startup (zero-delay spin) - health_check lock interaction and with_reconnect worst-case latency (3x operation timeout) and breaker false-positive mode documented Comment-analyzer (13/13 round-1 prose fixes verified; new ones fixed): - governor burst semantics corrected in four places: allow_burst REPLACES bucket capacity, it is not additive above RPS - stale rate_limit module doc (claimed log-only enforcement), auth guarantee doc now cites the rightmost-untrusted rule, non-compiling init_metrics example, License-check naming, IP-extraction bullets - round-1 artifact corrected with bracketed round-2 annotations (the 'disable SDK reconnection' plan that proved impossible, and the phantom Theme-H TD deferral that was actually implemented) - routes.rs diagram gains the Body Limit layer; TD-01/TD-03 updated Round-2 artifact addendum records the late reports and dispositions. Gates: 159 lib + 29 integration + 18 model green, clippy -D warnings clean, fmt clean. --- CLAUDE.md | 10 ++-- README.md | 4 +- docs/code-reviews/session-01-round1.md | 17 ++++--- docs/code-reviews/session-01-round2.md | 43 ++++++++++++++++- docs/tech-debt/TD-2026-07-01.md | 6 +++ docs/tech-debt/TD-2026-07-03.md | 6 ++- src/config.rs | 7 +++ src/iggy_client/connection.rs | 14 ++++-- src/iggy_client/helpers.rs | 44 +++++++++++++++-- src/iggy_client/mod.rs | 66 ++++++++++++++++++++------ src/metrics.rs | 5 +- src/middleware/auth.rs | 9 ++-- src/middleware/rate_limit.rs | 13 +++-- src/routes.rs | 5 ++ 14 files changed, 202 insertions(+), 47 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d0016f6..1f486f6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -223,7 +223,7 @@ Environment variables (see `.env.example`): | Variable | Default | Description | |----------|---------|-------------| | `RATE_LIMIT_RPS` | `100` | Requests per second (0 = disabled) | -| `RATE_LIMIT_BURST` | `50` | Burst capacity above RPS limit | +| `RATE_LIMIT_BURST` | `50` | Instantaneous bucket capacity (replaces, not adds to, the default) | ### Message Limits | Variable | Default | Description | @@ -427,7 +427,7 @@ environment: ### Running Tests ```bash -# Unit tests (158 tests) +# Unit tests (159 tests) cargo test --lib # Integration tests (29 tests, requires Docker for testcontainers) @@ -687,7 +687,9 @@ Request → Rate Limit → Auth → Request ID → Timeout → Tracing → CORS ### Client IP Extraction (`src/middleware/ip.rs`) - Shared IP extraction logic used by both rate limiting and authentication -- Header priority: `X-Forwarded-For` (first IP) → `X-Real-IP` → "unknown" fallback +- With `TRUSTED_PROXIES` set: peer-address gating + rightmost-untrusted + `X-Forwarded-For` resolution (see the Trusted Proxy Configuration section) +- Without it: header priority `X-Forwarded-For` → `X-Real-IP` → "unknown" fallback - Uses `Cow<'static, str>` for zero-allocation on the "unknown" fallback path - `#[inline]` hints on hot paths for potential inlining - See module-level docs for security warnings about IP spoofing @@ -776,7 +778,7 @@ GitHub Actions workflows provide automated quality assurance: - **Coverage**: Uploaded to Codecov - **Documentation**: Build with `-D warnings` - **Security audit**: `cargo-audit` for vulnerabilities -- **License check**: `cargo-deny` for license compliance +- **Dependency policy**: `cargo-deny` gates advisories, bans, licenses, and sources - **Scheduled runs**: Weekly Monday 2:00 AM UTC to catch dependency issues ### PR Checks (`pr.yml`) diff --git a/README.md b/README.md index f7da8ba..5954b22 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Apache Iggy is capable of processing millions of messages per second with ultra- ### Development & Testing - Docker Compose setup for local development -- Comprehensive test suite (158 unit tests, 29 integration tests, 18 model tests) +- Comprehensive test suite (159 unit tests, 29 integration tests, 18 model tests) - Integration tests with testcontainers (auto-spins Iggy server) - Fuzz testing for input validation functions @@ -346,7 +346,7 @@ Configuration is loaded from environment variables. See `.env.example` for the c | Variable | Default | Description | |----------|---------|-------------| | `RATE_LIMIT_RPS` | `100` | Requests per second (0 = disabled) | -| `RATE_LIMIT_BURST` | `50` | Burst capacity above RPS limit | +| `RATE_LIMIT_BURST` | `50` | Instantaneous bucket capacity (replaces, not adds to, the default) | | `API_KEY` | (none) | API key for authentication (disabled if not set) | | `AUTH_BYPASS_PATHS` | `/health,/ready` | Comma-separated paths that bypass auth | | `CORS_ALLOWED_ORIGINS` | `*` | Comma-separated allowed origins | diff --git a/docs/code-reviews/session-01-round1.md b/docs/code-reviews/session-01-round1.md index 5ff0f5c..eaa5635 100644 --- a/docs/code-reviews/session-01-round1.md +++ b/docs/code-reviews/session-01-round1.md @@ -77,11 +77,14 @@ Four agents independently traced the same root cause: resurrect a zombie server connection (verified in vendored SDK source). **Remediation (this branch):** structured IggyError→AppError classification at -the mapping sites (0.10's error discriminants make this clean); disable the -SDK's internal reconnection so the wrapper's documented policy is the only -one; make the health task actually `ping()` and drive `ConnectionState`; -`shutdown()` the old client before swap; record breaker failures on timeout -paths. **Deferred with TD records:** DiagnosticEvents-driven state, +the mapping sites (0.10's error discriminants make this clean); make the +health task actually `ping()` and drive `ConnectionState`; `shutdown()` the +old client before swap; record breaker failures on timeout paths. +*[Round 2 correction: the originally planned "disable the SDK's internal +reconnection" proved impossible — `enabled: true` is hardcoded in the SDK's +connection-string parsing (see TD-2026-07-02); the shipped design keeps the +SDK as the transport-level reconnection layer with the wrapper observing +truthfully (timeouts as breaker signal, live pings, classified errors).]* **Deferred with TD records:** DiagnosticEvents-driven state, with_reconnect paused-clock test matrix. ## Theme B — CI never enforces the deny.toml this branch migrated @@ -166,7 +169,9 @@ directly-exposed service throttles the whole service with a misattributed 429. **Remediation (this branch):** parse TRUSTED_PROXIES at startup and fail fast on invalid entries; enforce header trust against the parsed ranges; restructure auth to validate first and count only failures; warn on dropped CORS origins. -**Deferred with TD record:** peer-address (ConnectInfo) as the trust anchor. +*[Round 2 correction: peer-address (ConnectInfo) as the trust anchor was +NOT deferred — it was implemented in-branch (commit 5998dfe); no TD record +exists or is needed for it.]* ## Theme I — Defects inside the (currently dead) reconnect machinery [architect #5–#9; simplify #1, #2; silentfail M3; tests #2, #8] diff --git a/docs/code-reviews/session-01-round2.md b/docs/code-reviews/session-01-round2.md index d3fd7e6..6bb405e 100644 --- a/docs/code-reviews/session-01-round2.md +++ b/docs/code-reviews/session-01-round2.md @@ -77,8 +77,49 @@ auth metering, metrics exporter startup, count=0 → 400, `Identifier::named`, - Branch-protection requirement on "CI Success" not verifiable in-repo — operator action item. +## Addendum — late reports from the interrupted lenses + +Both agents lost to the session limit delivered full reports after the reset, +completing the 8/8 suite. Their findings and dispositions: + +**Architect (late):** corroborated the zombie-client leak with the precise +mechanism and found it went DEEPER than the earlier fix: `reconnect_bounded`'s +timeout could drop the reconnect future at its commit-point awaits (between +`connect()` success and swap/shutdown), leaking an unkillable heartbeat — with +a realistic trigger via the health-probe read guard [HIGH]. **Fixed:** the +session now runs in a spawned task awaited under the timeout (JoinHandle drop +does not cancel), making the commit atomic w.r.t. caller deadlines. Also +found: a real lost-wakeup race in `wait_for_reconnection` (tokio `Notify` +registers on first poll, not creation — the old comment claimed the opposite) +[MEDIUM, **fixed** with `enable()`]; transport-incomplete error classification +(QUIC/HTTP/WebSocket variants fell through — the round-1 dead-path bug +resurfacing on three transports) [MEDIUM, **fixed** + tests]; health-probe +guard-hold and stale-probe overwrite [MEDIUM-LOW, **documented** — SDK client +is not Clone; the spawned-session fix removes the leak consequence]; +timeouts-as-breaker-failures false-positive path via the stats refresher +[**documented** in with_reconnect docs]; zero max-delay config +[**validated at startup now**]. Verdicts: classification set right for TCP +with no false-positive triggers; backoff edges all sound; all 10 round-1 +resilience fixes verified correct. + +**Comment-analyzer (late; reviewed the pre-56d5fd0 tree):** verified 13/13 +round-1 prose fixes accurate, including SDK-source verification of the +"two layers" module doc and every CHANGELOG Fixed bullet. Of its new +findings: N3/N7 and the CHANGELOG duplicate were already fixed by later +commits; **fixed now:** stale rate_limit module doc (log-only claim), auth +guarantee doc (now cites rightmost-untrusted), non-compiling init_metrics +example, governor burst semantics misdocumented as additive in four places +(allow_burst REPLACES capacity), "License check" naming, stale IP-extraction +bullets, round-1 artifact corrections (the "disable SDK reconnection" line +and the phantom Theme-H TD reference — both annotated), TD-03 doc-site list, +body-limit box in the routes diagram. The pre-existing key-length timing +nuance remains an accepted LOW. + +TD-2026-07-01 records a second in-session trigger event (the spawned-task +change) with the same explicit re-arm. + ## Gate status at close `cargo fmt --check` ✓ · `clippy --all-targets -D warnings` ✓ · -158 lib + 29 integration + 18 model tests ✓ · `cargo audit` 0 vulns ✓ · +159 lib + 29 integration + 18 model tests ✓ · `cargo audit` 0 vulns ✓ · `cargo deny check` all four sections ✓. diff --git a/docs/tech-debt/TD-2026-07-01.md b/docs/tech-debt/TD-2026-07-01.md index 5ad99f5..c91628b 100644 --- a/docs/tech-debt/TD-2026-07-01.md +++ b/docs/tech-debt/TD-2026-07-01.md @@ -37,3 +37,9 @@ also rewrote these paths). Accepted explicitly as debt for this session: the composition changes were review-verified twice and the live paths are integration-tested; the trigger is RE-ARMED — the next behavioral change to these functions must not ship without the matrix. + +Second in-session event: the late round-2 architect report forced a further +behavioral change (reconnect sessions now run in a spawned task for +cancellation safety; `wait_for_reconnection` gained `enable()` registration). +Same disposition, same re-armed trigger - the spawned-task shape also makes +the future matrix EASIER (sessions are now addressable via JoinHandle). diff --git a/docs/tech-debt/TD-2026-07-03.md b/docs/tech-debt/TD-2026-07-03.md index fae0bcf..eee33c2 100644 --- a/docs/tech-debt/TD-2026-07-03.md +++ b/docs/tech-debt/TD-2026-07-03.md @@ -13,5 +13,7 @@ straggler-failure timer refresh in `Open` state was fixed in session 01.) ## Binding trigger Resolve (single/token-limited probe permit, or correct the docs to describe -multi-probe behavior) at the first production incident involving breaker -recovery, or whenever breaker configuration is next extended. +multi-probe behavior — both `circuit_breaker.rs` module docs AND the +`IggyClientWrapper` struct doc in `iggy_client/mod.rs` claim limited probes) +at the first production incident involving breaker recovery, or whenever +breaker configuration is next extended. diff --git a/src/config.rs b/src/config.rs index 9c45de7..e78900d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -250,6 +250,13 @@ impl Config { ))); } + // A zero max collapses the backoff floor to zero-delay retry spinning + if self.reconnect_max_delay.is_zero() { + return Err(AppError::ConfigError( + "RECONNECT_MAX_DELAY_MS must be greater than 0".to_string(), + )); + } + // Validate message limits are positive if self.batch_max_size == 0 { return Err(AppError::ConfigError( diff --git a/src/iggy_client/connection.rs b/src/iggy_client/connection.rs index b624282..afea146 100644 --- a/src/iggy_client/connection.rs +++ b/src/iggy_client/connection.rs @@ -79,12 +79,18 @@ impl ConnectionState { /// /// # Implementation Note /// - /// We register for notification BEFORE checking `is_reconnecting()` to avoid - /// a race condition: if we checked first and then registered, reconnection - /// could complete between those two operations, causing us to wait forever. + /// Creating `Notified` does NOT register the waiter — registration + /// happens on first poll (or an explicit `enable()`), and + /// `notify_waiters()` stores no permit for late registrants. So we must + /// `enable()` BEFORE checking `is_reconnecting()`: otherwise the leader + /// can call `stop_reconnecting()` between our check and our first poll, + /// and the wakeup is lost (the waiter then stalls until its own caller's + /// deadline instead of waking immediately). pub async fn wait_for_reconnection(&self) { - // Register for notification FIRST to avoid race condition let notified = self.reconnect_complete.notified(); + tokio::pin!(notified); + // Register with the Notify BEFORE the check (see note above). + notified.as_mut().enable(); if self.is_reconnecting() { notified.await; } diff --git a/src/iggy_client/helpers.rs b/src/iggy_client/helpers.rs index 32c764b..a5d9e1b 100644 --- a/src/iggy_client/helpers.rs +++ b/src/iggy_client/helpers.rs @@ -18,9 +18,23 @@ pub fn classify_iggy_error(error: IggyError, fallback: fn(String) -> AppError) - | IggyError::NotConnected | IggyError::StaleClient | IggyError::ClientShutdown => AppError::Disconnected(error.to_string()), - IggyError::ConnectionClosed | IggyError::TcpError => { - AppError::ConnectionReset(error.to_string()) - } + // Connection-flavored variants across ALL transports the connection + // string can select (TCP, QUIC, HTTP, WebSocket) - classifying only + // the TCP set would leave the reconnect path dead code on the other + // three. EmptyResponse is in the SDK's own internal reconnect-trigger + // list. HttpResponseError (a response WITH an error status) is + // deliberately NOT here: the server answered, that is an application + // error. + IggyError::ConnectionClosed + | IggyError::TcpError + | IggyError::QuicError + | IggyError::EmptyResponse + | IggyError::HttpError(_) + | IggyError::WebSocketError + | IggyError::WebSocketConnectionError + | IggyError::WebSocketCloseError + | IggyError::WebSocketReceiveError + | IggyError::WebSocketSendError => AppError::ConnectionReset(error.to_string()), IggyError::CannotEstablishConnection => AppError::ConnectionFailed(error.to_string()), other => fallback(other.to_string()), } @@ -127,7 +141,18 @@ mod tests { #[test] fn test_classify_connection_reset_variants() { - for error in [IggyError::ConnectionClosed, IggyError::TcpError] { + for error in [ + IggyError::ConnectionClosed, + IggyError::TcpError, + IggyError::QuicError, + IggyError::EmptyResponse, + IggyError::HttpError("connection refused".to_string()), + IggyError::WebSocketError, + IggyError::WebSocketConnectionError, + IggyError::WebSocketCloseError, + IggyError::WebSocketReceiveError, + IggyError::WebSocketSendError, + ] { let classified = classify_iggy_error(error, AppError::SendError); assert!( matches!(classified, AppError::ConnectionReset(_)), @@ -137,6 +162,17 @@ mod tests { } } + #[test] + fn test_classify_http_response_error_is_not_a_connection_error() { + // The server ANSWERED with an error status - reconnecting would be + // wrong; it must map through the fallback. + let classified = classify_iggy_error( + IggyError::HttpResponseError(500, "boom".to_string()), + AppError::SendError, + ); + assert!(matches!(classified, AppError::SendError(_))); + } + #[test] fn test_classify_cannot_establish_connection() { let classified = diff --git a/src/iggy_client/mod.rs b/src/iggy_client/mod.rs index 71c372b..d68936f 100644 --- a/src/iggy_client/mod.rs +++ b/src/iggy_client/mod.rs @@ -265,6 +265,17 @@ impl IggyClientWrapper { /// /// Called periodically by the background health-check task; safe to call /// from handlers as well. + /// + /// # Lock interaction + /// + /// The wrapper read guard is held for the probe's duration (the SDK + /// client is not `Clone`, so the handle cannot be snapshotted out). + /// During an outage a probe can hold it for up to the operation timeout, + /// delaying — but not cancelling — a concurrent reconnect swap, which + /// runs in a detached task (see `reconnect_bounded`) and simply acquires + /// the write lock when the probe finishes. A probe that started against + /// the OLD client and completes after a swap may briefly overwrite the + /// fresh connected state; the next tick corrects it. pub async fn health_check(&self) -> bool { let result = { let client = self.client.read().await; @@ -409,23 +420,35 @@ impl IggyClientWrapper { } } - /// Reconnect, bounded by the operation timeout. + /// Reconnect, bounded by the operation timeout from the caller's view. /// - /// Used on the request path so a handler never hangs indefinitely behind - /// an unbounded reconnection session (the default configuration retries - /// forever). If the bound elapses, the reconnection attempt is aborted - /// (its scope guard releases the in-progress flag) and the next failing - /// request will start a fresh session. + /// The session runs in a SPAWNED task and the caller awaits its + /// `JoinHandle` under the timeout. This is a cancellation-safety + /// requirement, not a style choice: dropping the reconnect future at + /// its commit-point awaits (between `connect()` succeeding and the + /// swap/shutdown completing) would leak a fully-connected client whose + /// detached SDK heartbeat task can never be shut down — it holds the + /// inner client Arc and only exits on `ClientShutdown`, which nothing + /// can send once the handle is gone. Dropping a `JoinHandle` does NOT + /// cancel the task, so when the caller's deadline expires the session + /// keeps running in the background and completes (or fails) cleanly; + /// the caller just reports the timeout, and waiters on a later attempt + /// join the still-running session as followers. async fn reconnect_bounded(&self) -> AppResult<()> { let timeout = self.config.operation_timeout; - tokio::time::timeout(timeout, self.reconnect()) - .await - .map_err(|_| { - AppError::ConnectionFailed(format!( - "Reconnection did not complete within {:?}", - timeout - )) - })? + let this = self.clone(); + let session = tokio::spawn(async move { this.reconnect().await }); + + match tokio::time::timeout(timeout, session).await { + Ok(Ok(result)) => result, + Ok(Err(join_error)) => Err(AppError::Internal(format!( + "Reconnection task failed: {join_error}" + ))), + Err(_) => Err(AppError::ConnectionFailed(format!( + "Reconnection did not complete within {:?} (continuing in background)", + timeout + ))), + } } /// Execute an operation with automatic reconnection on connection failure. @@ -435,6 +458,21 @@ impl IggyClientWrapper { /// - **Timeout**: All operations are bounded by `config.operation_timeout` /// - **Retry**: On connection failure, attempts reconnect and retries once /// + /// # Worst-case latency + /// + /// A request on the reconnect path can take up to 3x the operation + /// timeout (first attempt + bounded reconnect + retry) - bounded, but + /// well above the typical single-operation expectation. + /// + /// # Breaker false positives + /// + /// Timeouts count as breaker failures because the SDK's internal + /// reconnection swallows outages into blocking retries (a timeout is + /// often the only signal). The cost: N consecutive merely-SLOW + /// operations (including the background stats refresher's) can open the + /// circuit without a real outage. Failures must be consecutive - any + /// success resets the count - which bounds the risk. + /// /// # Circuit Breaker Integration /// /// Before attempting the operation, the circuit breaker is checked: diff --git a/src/metrics.rs b/src/metrics.rs index 71b6899..9ec39cb 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -25,8 +25,9 @@ //! ```rust,ignore //! use iggy_sample::metrics::{init_metrics, record_message_sent}; //! -//! // Initialize metrics (call once at startup) -//! init_metrics(); +//! // Initialize metrics once at startup; a bind failure should fail startup +//! let addr: std::net::SocketAddr = "0.0.0.0:9090".parse()?; +//! init_metrics(addr)?; //! //! // Record metrics in handlers //! record_message_sent("my-stream", "my-topic", "success"); diff --git a/src/middleware/auth.rs b/src/middleware/auth.rs index e55b56a..25b1cf5 100644 --- a/src/middleware/auth.rs +++ b/src/middleware/auth.rs @@ -80,7 +80,8 @@ pub const API_KEY_QUERY: &str = "api_key"; const DEFAULT_BYPASS_PATHS: [&str; 2] = ["/health", "/ready"]; /// Default maximum auth failures per IP per minute before blocking. -/// After this many failures, further requests from the IP are blocked temporarily. +/// After this many failures, further FAILING requests from the IP are +/// blocked temporarily; valid-key requests always pass. const DEFAULT_AUTH_FAILURE_LIMIT: NonZeroU32 = NonZeroU32::new(10).unwrap(); /// Default burst capacity for auth failure rate limiting. @@ -131,8 +132,10 @@ impl ApiKeyAuth { /// per-IP brute-force limiter. /// /// With trusted proxies configured, forwarded headers are only honored - /// when the direct peer is inside a trusted range, so attackers cannot - /// rotate spoofed `X-Forwarded-For` values to escape failure tracking. + /// when the direct peer is inside a trusted range, and honored + /// `X-Forwarded-For` chains are resolved rightmost-untrusted (see + /// `middleware::ip`), so attackers cannot rotate spoofed values to + /// escape failure tracking - whether the proxy overwrites or appends. pub fn with_trusted_proxies( api_key: Option, bypass_paths: Vec, diff --git a/src/middleware/rate_limit.rs b/src/middleware/rate_limit.rs index 19094df..f5fa9aa 100644 --- a/src/middleware/rate_limit.rs +++ b/src/middleware/rate_limit.rs @@ -13,7 +13,7 @@ //! # Configuration //! //! - `rate_limit_rps`: Sustained requests per second per IP -//! - `rate_limit_burst`: Additional burst capacity above RPS +//! - `rate_limit_burst`: Instantaneous bucket capacity (governor's `allow_burst` REPLACES the default capacity; it is not added on top of RPS) //! - `trusted_proxies`: CIDR ranges of trusted reverse proxies //! //! # Response Headers @@ -32,8 +32,11 @@ //! 2. Ensure your proxy overwrites (not appends) client IP headers //! 3. Block direct access to this service from the internet //! -//! When `trusted_proxies` is configured, the middleware logs warnings if -//! X-Forwarded-For is received from an untrusted source. +//! When `trusted_proxies` is configured, forwarded headers are only honored +//! when the direct peer (via `ConnectInfo`) is inside a trusted range; +//! requests from untrusted peers are keyed by their actual peer address and +//! the ignored headers are noted at debug level. See `middleware::ip` for +//! the full resolution rules (rightmost-untrusted X-Forwarded-For). use std::fmt; use std::net::IpAddr; @@ -265,7 +268,7 @@ impl RateLimitLayer { /// # Arguments /// /// * `rps` - Requests per second limit per IP (sustained rate) - /// * `burst` - Additional burst capacity per IP + /// * `burst` - Instantaneous bucket capacity per IP (replaces, not adds to, the default capacity) /// /// # Errors /// @@ -285,7 +288,7 @@ impl RateLimitLayer { /// # Arguments /// /// * `rps` - Requests per second limit per IP (sustained rate) - /// * `burst` - Additional burst capacity per IP + /// * `burst` - Instantaneous bucket capacity per IP (replaces, not adds to, the default capacity) /// * `trusted_proxies` - Shared trusted-proxy configuration /// /// # Errors diff --git a/src/routes.rs b/src/routes.rs index 03fe4e5..45fde00 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -36,6 +36,11 @@ //! └────────┬─────────┘ //! │ //! ▼ +//! ┌──────────────────┐ +//! │ Body Limit │ ← 413 if over MAX_REQUEST_BODY_SIZE +//! └────────┬─────────┘ +//! │ +//! ▼ //! Handler //! ``` //! From b418e1b2cfbfe4bfafdded4423039918ba92d10c Mon Sep 17 00:00:00 2001 From: mlevkov Date: Sat, 4 Jul 2026 02:16:04 -0700 Subject: [PATCH 18/20] fix(ci): raise MSRV to 1.93 and fix rustdoc private-item link The re-enabled CI (the workflow had been auto-disabled for repo inactivity, which is why no PR run had ever exercised this branch) surfaced two failures: - MSRV: iggy 0.10's compio-buf 0.8 uses MaybeUninit slice APIs stabilized in Rust 1.93 and declares no rust-version, so the earlier metadata-based MSRV sweep could not see it and cargo cannot enforce it at resolution time. Verified empirically: 1.90/1.91/1.92 fail, 1.93.0 compiles and passes all 159 lib tests. rust-version, CI matrix, README badge/prerequisites, and CLAUDE.md updated; recorded as Breaking in the CHANGELOG. A downgrade was not possible: iggy_common 0.10 hard-requires the compio 0.18 line. - docs: public extract_client_ip_with_validation doc linked the private rightmost_untrusted_xff (rustdoc::private-intra-doc-links under -D warnings); demoted to plain code formatting. Verified with RUSTDOCFLAGS='-D warnings' cargo doc locally. --- .github/workflows/ci.yml | 4 ++-- CHANGELOG.md | 3 +++ CLAUDE.md | 2 +- Cargo.toml | 2 +- README.md | 4 ++-- src/middleware/ip.rs | 2 +- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b723a3e..222fd38 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ env: CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 # Minimum supported Rust version - MSRV: "1.90.0" + MSRV: "1.93.0" # Cancel in-progress runs for the same branch concurrency: @@ -70,7 +70,7 @@ jobs: include: # MSRV check on Ubuntu only - os: ubuntu-latest - rust: "1.90.0" + rust: "1.93.0" steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master diff --git a/CHANGELOG.md b/CHANGELOG.md index 211c407..d09bdcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated Apache Iggy Rust SDK from 0.8.0 to 0.10.0 (latest stable); no source changes required — the `Client` trait API is unchanged +- **Breaking**: MSRV raised 1.90 → 1.93: iggy 0.10's `compio-buf` + dependency uses APIs stabilized in Rust 1.93 (and declares no + rust-version, so cargo cannot catch this at resolution time) - Pinned the `apache/iggy` server image to 0.8.0 (the release paired with the 0.10 SDK) in `docker-compose.yaml` and integration tests, replacing the floating `latest` tag diff --git a/CLAUDE.md b/CLAUDE.md index 1f486f6..83193af 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -380,7 +380,7 @@ environment: ## Development ### Prerequisites -- Rust 1.90+ (edition 2024, MSRV: 1.90.0) +- Rust 1.93+ (edition 2024, MSRV: 1.93.0) - Docker & Docker Compose ### Quick Start diff --git a/Cargo.toml b/Cargo.toml index fe391e5..8301531 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "iggy_sample" version = "0.1.0" edition = "2024" -rust-version = "1.90.0" +rust-version = "1.93.0" description = "A comprehensive demonstration of Apache Iggy message streaming with Axum" license = "MIT" repository = "https://github.com/mlevkov/iggy_sample" diff --git a/README.md b/README.md index 5954b22..3347a24 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![CI](https://github.com/mlevkov/iggy_sample/actions/workflows/ci.yml/badge.svg)](https://github.com/mlevkov/iggy_sample/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Rust](https://img.shields.io/badge/rust-1.90%2B-blue.svg)](https://www.rust-lang.org) +[![Rust](https://img.shields.io/badge/rust-1.93%2B-blue.svg)](https://www.rust-lang.org) A comprehensive demonstration of [Apache Iggy](https://github.com/apache/iggy) message streaming integrated with [Axum](https://github.com/tokio-rs/axum) web framework in Rust. @@ -78,7 +78,7 @@ Apache Iggy is capable of processing millions of messages per second with ultra- ## Prerequisites -- Rust 1.90+ (edition 2024, MSRV: 1.90.0) +- Rust 1.93+ (edition 2024, MSRV: 1.93.0) - Docker & Docker Compose - curl or httpie (for testing) diff --git a/src/middleware/ip.rs b/src/middleware/ip.rs index 4c3d989..382ec4a 100644 --- a/src/middleware/ip.rs +++ b/src/middleware/ip.rs @@ -223,7 +223,7 @@ fn rightmost_untrusted_xff(xff: &str, trusted_proxies: &TrustedProxyConfig) -> O /// the peer address is the client IP (spoofed headers from direct clients /// are inert). /// - Peer in a trusted range with `X-Forwarded-For`: resolved via the -/// rightmost-untrusted rule (see [`rightmost_untrusted_xff`]) so the +/// rightmost-untrusted rule (see `rightmost_untrusted_xff`) so the /// guarantee holds for both overwriting and appending proxies; an /// unparseable chain falls back to the peer address. /// - Peer in a trusted range with `X-Real-IP`: the value is used only if it From 02fcf3409e7361a8d1e966b0a5eab63b495d3f3c Mon Sep 17 00:00:00 2001 From: mlevkov Date: Sun, 5 Jul 2026 13:54:49 -0700 Subject: [PATCH 19/20] fix(ci): run the stress-test Iggy service container privileged The re-enabled weekly Extended Tests failed on main: the io_uring-based server (0.6+) panics with 'Cannot create runtime: Operation not permitted' under the default container seccomp profile. The GitHub services block predates the io_uring rewrite and never gained the flag that docker-compose (privileged: true) and the testcontainers fixture (.with_privileged(true)) already carry. Documentation Coverage and all other extended jobs passed. --- .github/workflows/extended-tests.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/extended-tests.yml b/.github/workflows/extended-tests.yml index 3a17230..6e39251 100644 --- a/.github/workflows/extended-tests.yml +++ b/.github/workflows/extended-tests.yml @@ -79,7 +79,12 @@ jobs: image: apache/iggy:0.8.0 ports: - 8090:8090 + # --privileged: the io_uring-based server (0.6+) cannot create its + # runtime under the default seccomp profile ("Operation not + # permitted"); mirrors docker-compose's privileged: true and the + # integration tests' .with_privileged(true) options: >- + --privileged --health-cmd "curl -f http://localhost:3000/health || exit 1" --health-interval 10s --health-timeout 5s From 8a51414db8cc9d87c3c82e426e0bd2ec793c8184 Mon Sep 17 00:00:00 2001 From: mlevkov Date: Sun, 5 Jul 2026 14:00:35 -0700 Subject: [PATCH 20/20] fix(ci): make the stress-test Iggy service container actually reachable Reproduced the health failure locally against apache/iggy:0.8.0: the image ships no curl (health-cmd could never succeed) AND the server binds 127.0.0.1 by default, so the runner could not reach 8090 either. - healthcheck switched to the bundled 'iggy ping' CLI (verified in the local container; mirrors docker-compose's healthcheck) - env sets 0.0.0.0 bind addresses and iggy/iggy root credentials, matching the stress step's connection string and compose --- .github/workflows/extended-tests.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/extended-tests.yml b/.github/workflows/extended-tests.yml index 6e39251..7ea9db4 100644 --- a/.github/workflows/extended-tests.yml +++ b/.github/workflows/extended-tests.yml @@ -79,13 +79,20 @@ jobs: image: apache/iggy:0.8.0 ports: - 8090:8090 + env: + # The server binds 127.0.0.1 by default - unreachable from the + # runner; credentials match the stress step's connection string + IGGY_ROOT_USERNAME: iggy + IGGY_ROOT_PASSWORD: iggy + IGGY_TCP_ADDRESS: 0.0.0.0:8090 + IGGY_HTTP_ADDRESS: 0.0.0.0:3000 # --privileged: the io_uring-based server (0.6+) cannot create its # runtime under the default seccomp profile ("Operation not - # permitted"); mirrors docker-compose's privileged: true and the - # integration tests' .with_privileged(true) + # permitted"). Healthcheck uses the bundled iggy CLI - the image + # ships no curl (mirrors docker-compose's healthcheck). options: >- --privileged - --health-cmd "curl -f http://localhost:3000/health || exit 1" + --health-cmd "iggy ping" --health-interval 10s --health-timeout 5s --health-retries 10