diff --git a/CHANGELOG.md b/CHANGELOG.md index 9aa7271..5924ef4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,29 @@ ## 0.34.0 +- Follow-up HOT cache path: clone response headers outside the shared store + mutex using immutable `Arc` metadata; preserve exact hit/LRU + accounting and per-response Age/transform flags. Fast-path direct no-Vary + keys and avoid per-HIT domain string allocations, inactive BAN locks and + repeated full retention sweeps (stale objects remain for revalidation). +- Add dedicated concurrent HOT hit, transformed header isolation, exact LRU, + BAN, stale-age tests and a manually ignored microbenchmark. No policy, + cache-file format or panel changes. New code remains uncompiled/unmeasured; + see docs/CACHE_HOTPATH_0.34.0.md. + +- Follow-up reverse-proxy RPS revision: reuse typed upstream HTTP origin URIs, + use thread-local cryptographic nonce generation for per-request IDs, + reduce smooth-WRR member-ID clones, and share compiled health policies with probes. +- Cache backend lease once for streaming uploads instead of locking it on each + DATA frame; move access-log events directly into history when there are no + external consumers; remove repeated route attribution in forwarding. +- Retain exact per-request pool-recycle count and age limits plus independent + supervisor rotation, and keep H1/H2 protocol telemetry. No altered upstream keepalive defaults or cache query policies. +- Add source-only invariants, Rust regression cases and reproducible live + before/after/nginx benchmarking instructions. This revision has not yet been + compiled or measured for RPS; see docs/PROXY_RPS_0.34.0.md. + + - Cache regression fix: interpret local max TTL as a cap on remaining freshness at receipt, not on the upstream Age clock. Preserve RFC 9111 Age and transformed response validators; prohibit private/no-store caching. - HOT hit optimization: update LRU and hit counts in place, without removing/re-indexing the complete cache entry and its Vary variants on every hit. - Expose both total HTTP Age and time resident in RustProxy HOT/COLD in the cache dashboard, with Polish/English labels. diff --git a/README.md b/README.md index d167a52..439037b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,18 @@ # RustProxy 0.34.0 +## 0.34.0: HOT HIT critical-section follow-up (source candidate) + +Immutable response headers are shared until a HIT; the mutable per-response +copy and Body are built outside the cache's shared HOT mutex. No-Vary lookups, +per-domain counters, repeated cache reasons and inactive BAN checks use +shorter paths. The existing stale-for-revalidation policy makes the previous +full HOT sweep unnecessary. Exact LRU, generation barriers, BAN, COLD, +transformation/ETag, TTL and cache bypass settings remain unchanged. + +This is **uncompiled source**; do not infer network RPS or production acceptance +from source checks. See +[`docs/CACHE_HOTPATH_0.34.0.md`](docs/CACHE_HOTPATH_0.34.0.md). + ## 0.34.0: reverse-proxy hot-path optimization revision (source candidate) This revision retains the **0.34.0** version and all prior scoped-cache, diff --git a/SHA256SUMS b/SHA256SUMS index 10fd6a9..6c9679b 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -2,7 +2,7 @@ 4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example 9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore 46bab1d2c2d6e0a80d57a6b232b1ba4c220884b95cd45ddccbafce2aa160cddb AGENTS.md -4e0a7371f060063a6a73ae1c1a2faba2de8d227118be2406c88ca432c90305eb CHANGELOG.md +688f666a676d77847aade6ef97fd82a565b88c9b314050ddeb313a1b3ec4663a CHANGELOG.md ca201e3cd3f9842eae0006d0b40a0a72bcd3d4da6e4e5ecaf200d1b9a1390db2 Cargo.lock fec5f1bae9a59ef1fabd27c5e45d31448dd8f811736eebfa8826b3806a7e3349 Cargo.toml 25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile @@ -56,6 +56,7 @@ e68d5a5cf0991de9308f78afb0264e02979f7463a5f395eb7a6e9ec2bd3a3d54 docs/PHP_FPM.m bc068a176056f5ea4d3b637071e644ed58a2140494c233ef22aa66e9700e3299 docs/PHP_FPM_ISOLATION.md ad6e3f003b820a4f45e30e39577d120a69d167c70910bda4891971759458c3dd docs/PROMETHEUS.md deea89aed410c0619bdbdb12437edc7aff9dca2fec7fdc059e918c649fc36b51 docs/PROXY_HOTPATH_0.34.0.md +f92715fc95ca3b837ff8352e3c899569cdb0f72bcb45c09b050cf1f1ff7ccfe5 docs/PROXY_RPS_0.34.0.md 887d1fdcc3b546b024afcd18b1716b8e3a74a3800012bf4bda7a122d2eabe233 docs/README.md 6e46a137b9c88dac8cb9e3fdca877430092392de9913d746278dbff8652460c2 docs/REDIRECTS.md 0dfd0d431c5aedc67abac1ce5de040ffa790ec9578fd7d6f91a987c333d73358 docs/REFERENCES.md @@ -69,7 +70,7 @@ ec7e32fd19e6ba6ed1732f1f3dd92ac0860609e8c0bbd3c1998840cdfb24bf54 docs/SECURITY. 4dcf3243a13d9ae1827b5cefdbb9e27bd6310321c50d4d5934527d44437df6ef docs/SETTINGS.md 8fc112cb45a2f35ef5b0dd4371aa6f43aca4df85db62c6ae02e56d74a3b702ae docs/STREAMS.md 95148bfa1d2f8ff52b591c9acefc6c8992dadc4ebda43a195101d9b31d67a576 docs/TEST_REPORT.md -4c82db7fab8c8fe8cd1508fbb85751b7f72071edd913ee7def4a9ffde8efa13e docs/TEST_REPORT_0.34.0.md +ec1bf1690edc056068cbdec7244e3a98a2da2da953e0b4399301d76585a69be6 docs/TEST_REPORT_0.34.0.md cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md 751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md fe3ec6ca16ff933239c12dacf39dd7b45717c0551a8b9f6ce601bf78136c9e37 docs/TRANSPORT.md @@ -129,7 +130,7 @@ a641bd35400c56f3b68b0a92e97b602d06090f03972ca8f01fa744e90dd97403 scripts/deploy 2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migration/migrate_npm.py a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migration/migrate_npm_api.py 31ccad3ec3c880119f3b7002f0c6652f8b43634edad8ab3763cb0e4497d0bb1f scripts/migration/migrate_npm_core.py -e4fd7fa94d661d4610d7d86fcdeaae9d6deb220e95b8d3bbd67ec15fbd017ccb scripts/performance/README.md +c79b709fb6f806a772464b709e652e6dfcf498b850281df5e1c594c98392cb23 scripts/performance/README.md 0380406a059ee9ec27473f8a46435f638f73a6dc729382ae1561c08dbf7ccb53 scripts/performance/benchmark.py 9ac220163256e08513597c397c016a48345b114d7876b439259cc310b36c83fc scripts/performance/checks_cache.py f09125d081d0eed9a44fdca75de14e6bbf09dcb00fe1f027ba2270f89e3ff192 scripts/performance/checks_policy.py @@ -157,8 +158,8 @@ e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/runtim 046b4282550f5cb4e53906665c91c7442be70fc68fd0743466fd832582f7c979 scripts/test_stream_tcp.py f0b48ec0cc4411755cc0b4128eebce710375afa99344cb76198cf31074f8eb63 scripts/test_stream_udp.py fafd577ccbb58485c47de35fc125e5c64a3a7628ae40d01d4cd924fce2f414db scripts/tests/cache_age_policy_034_model.py -972d79127309071a15e231ca45d24c9a8347c25115d0ec4fd91a74ca56b3f665 scripts/tests/cache_age_transform_034_live.py -955c150f8d0490069cb14b659d4fb4e0b529a28b87c27bd6697935c23e7ef192 scripts/tests/cache_scope_034_static_test.py +b75a381d1b41bfb2ea7d2ce09728249b5e1465f76d57e2c3f151af72a5ae4d03 scripts/tests/cache_age_transform_034_live.py +6cb79535f742d1a41685f486688dc2f1330ea362e3da1a15d4fa32561094282f scripts/tests/cache_scope_034_static_test.py efb7a1fde2c74f30cd4a1abb88ae07c1cac7378bb33685dd09d52bb30849daac scripts/tests/dashboard_chart_headers_0335_browser_test.py eb2c9e5c968938b6fbe4675b547ba1f537519878b9095e1fea133f4d83f96fc8 scripts/tests/dashboard_chart_typography_0337_browser_test.py 6a908fd434d2a8a80adfd06a66d4b83c4174e448d453a8c10b1812475d036677 scripts/tests/dashboard_chart_width_0333_browser_test.py @@ -178,6 +179,7 @@ fa42ec0475e4398d647b3c094309b4332cd2b5649db223cae0b8e821d6b44a28 scripts/tests/ b0136544af717b75df2cb107900ea65c3f945fdd0040369dbb93919008fb954c scripts/tests/panel_html.py 8d1903e2b17b3d59cd0a5bec3e298e468b3d88ed2dda8b724239cc45d8353c59 scripts/tests/progressbar_0332_browser_test.py f94612eaefb4098e1a4efc0d970ce52a656db239aaa0a012df47567527bf3708 scripts/tests/proxy_hotpath_034_static_test.py +9d28d523aeed722f6ed9217d6eb30530d4fb103e036983d680909a9011dba8de scripts/tests/rps_hotpath_034_static_test.py 5be5cdc60324f4f0f6638640000b888a5babb0a9bbf29511c824a0ba38878c2a scripts/tests/sendfile_034_panel_browser_test.py 978789bfe89d43034dfebddba49b9b17daedb1be031732734f95375e56c78306 scripts/tests/sendfile_034_static_test.py 1bb99b148d0297235b707f16865ca077329246fc19ecbb20a143bdeb56a23f37 scripts/tests/sidebar_nav_0333_browser_test.py @@ -191,26 +193,26 @@ f0a2e3daa0a49aac54723cb5a281360e49a97c076281372c5cca2a85e096c02c scripts/tests/ f55efa99245bbdbc98fe3202e52952d224ac3200612561580d52f6e251810049 scripts/tests/web_server_0330_ui_test.py 0dd1cef06a1c3e1546e2fb9b085bf369dfbed02107b9ab01b4fb4f3b894099c0 scripts/tests/web_server_0334_browser_test.py 22e880e98c7b9d700c7a4c02b9b1d543e5dbd1f4ecdca42813a1083165b32a19 scripts/version.py -55485a513e8c4da2a58daa7abcb15b32ed408a5fc062cf23599fb83d16b90cfe src/acl.rs -63efab1e95d41134fc30cc57320c32810bbae9f81a640bdb24b1be2059e63d8a src/acl/dynamic.rs 4803e44de39b872a5b3b55f9ca9e81c066ba52cb8f6adb879fd57c5f94b93e89 src/acl/dynamic/tests.rs +63efab1e95d41134fc30cc57320c32810bbae9f81a640bdb24b1be2059e63d8a src/acl/dynamic.rs a22e5ea6381a1eacafe650be0f5cd6e653773e31f6491a562fe0459a309fb7cc src/acl/tests.rs -6f5d3137bf24451da05c1db9f0f9e2a451305db17b6bc2590815ad54a6511829 src/acme.rs +55485a513e8c4da2a58daa7abcb15b32ed408a5fc062cf23599fb83d16b90cfe src/acl.rs 587097620b2f25ce7c6f59f907b47660184b095e3040c7615e74509a82e7cdd8 src/acme/jobs.rs 3f158f46e80b403b658b4e9b1e3710e38c70e47b2d476b8223d9c394ffeeba95 src/acme/model.rs -c5334de6c732d76b2bea7bfc17b3ac305a250bbaad31a5420d6a7d7abb200449 src/acme/providers.rs d1dcb7bdfb7cfdc9c3b88cfb6908201c58532bc888744a13f45853079f214659 src/acme/providers/tests.rs +c5334de6c732d76b2bea7bfc17b3ac305a250bbaad31a5420d6a7d7abb200449 src/acme/providers.rs 29ff0647417b0f1d50a4cc075ab51afecff9ddf47db3eb7948d51db5a38f33f3 src/acme/runtime_bootstrap.rs 87f64198770ae026c03a242fa542db2590ca734b02e756f1e5c28d66236f753a src/acme/runtime_env.rs f8075a5ef9ae5651d72a33ac0f0fd5b44ad0ae30f6e2d2318412177e0ed837f5 src/acme/runtime_versions.rs e38331a1b186b49fe9d04051de52b603e1fa028def37e4e2f52e386b4d7b29ac src/acme/tests.rs +6f5d3137bf24451da05c1db9f0f9e2a451305db17b6bc2590815ad54a6511829 src/acme.rs b6f38f3fd75d6685bbc882ed1077a38bf495cfcf01c397f69ed9b98be68b446f src/admin/access.rs b4a2a068acfa1143b0032b9cd072c19cf3fdda2580f0b932d11d6368cd2ab3ad src/admin/certificate_api.rs 51f46a2cb3167c4e3a784dfbb59b8fe0e9d5d3d51365b26699498e31d0afb6a9 src/admin/certificates.rs b5f6d49f1add583cfc8cd4aa36ffe4db56dac4de3b151fe8239fe42f507019cc src/admin/dashboard_presets.rs -5e150d4ef0413a2f92db5b6631669933b84d4b4796633d01476c4b39188cd05c src/admin/documentation.rs 958546c43c05bf4bed8121b24db3b3096f9243f3c56a71e1762592b755684013 src/admin/documentation/documents.rs 67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs +5e150d4ef0413a2f92db5b6631669933b84d4b4796633d01476c4b39188cd05c src/admin/documentation.rs 1ce779b84aa8f47bdb3c34f83593c1f2f06d2235f0319847096cfcd035434168 src/admin/file_log_search.rs ab18acf6fb4ac86fb22635b9219a12dac76d24952c8f15538486267a9b4b7855 src/admin/file_logs.rs c6b552cd5acd9e743c28b1a5564420c214c619fe891ab0561b7d4499aae8d6c0 src/admin/file_logs_io.rs @@ -224,8 +226,8 @@ d9120f7dc61d27621c5ac27ea49815dc7945c44d5b99d208da503818c198f649 src/admin/oper 92b3e05f04711d8001b56816405859631d9e917868e15be965c7e76ef4ddef99 src/admin/preferences.rs 4a5cb4c3a3aa8b9a519020854506ce4be5e688a6c4298260322ae641dded7fe9 src/admin/preferences_columns.rs 9a3f13e2477769ae680c03d63336880c3b7b7d5aba4a91e44390a976608b9b1d src/admin/preferences_tests.rs -4a7a2017ba57c9e7a8b29e73764d439f6d03653403909ef198ded8c48d152784 src/admin/public.rs 7a00e4ab943f74d2b1bdcbfab7f9cd3f584e9f59536b4aca6c4c31f5f5508d12 src/admin/public/tests.rs +4a7a2017ba57c9e7a8b29e73764d439f6d03653403909ef198ded8c48d152784 src/admin/public.rs 9926827131254677a848c178c80850b3ca1993c05b4bf913687dc0c7992ad048 src/admin/refresh_preferences.rs daef5e663c1bc99a22b3b8fa4481ae5e4916b91db132c104bc050f51b1fdf77f src/admin/router.rs a40adfbaa908da3ea726d7aa3d45394a70a6a55b6fc0063270ec4d2a2f2026a3 src/admin/session.rs @@ -302,45 +304,44 @@ b3239e67901f1796d7a974ade0796f2e3590ff8064628a5629b78c7f6ce2f32e src/admin/web/ bb93e2017f40e3a45ff7f4ca92e4188bb464f432704b6d2c39da9d9621b7984c src/admin/web/vhosts/render.js 4dfdeb7c73c2775eacaca65fd8ef1dc80b275421b15522d319d338f5b96288cc src/admin/web/web-server-profiles.js fd476445a88eb40270e706814eee50cbfbba4069a26dd843c095367e55a147a1 src/admin/web/web-server.js -53237ae8b08d261ce21e0aa6fd225bf4f3bb17a37a80529e2a4039d45b13f7c5 src/auth/files.rs b2f948bcc3dbd24952c7b1b02d41d06757f49f0c38a78caf51f8f8a687d8480d src/auth/files/tests.rs +53237ae8b08d261ce21e0aa6fd225bf4f3bb17a37a80529e2a4039d45b13f7c5 src/auth/files.rs 775178ee5781e64574bdd7c1fb7d64e7e283977fbd9c98c99896532a7f1df612 src/auth/mod.rs c96a87e82ff0c1c9ce3d0cd5135903bb53af78d1c4fb6ebfe5fea503bf5644ea src/auth/store.rs 745226cabb007819370fc6aa680995a03a8a13fa057dacfab0fcc21d0c43bbe4 src/auth/tests.rs -09c3a622a7d2169e49e3369ea5f0f816eab2f717eaf1675c625d518c8f34d367 src/backup.rs 76aa2afe9e7302be39b3512f30d0448d9134300f8ae930d64ada40cf3ad4a8a3 src/backup/archive.rs 732cd4f9ed38ae69f8bbae61b1db8baaa6f86b07ca21c41f24a206a30f0a96fc src/backup/ownership.rs 076350506215faec3cfe7d9de4e60baf29672cc22a41eabe601dcc510c257799 src/backup/snapshot.rs 5c19490d834d9971166b6b784a75b081bb95fd50c8c634ef3c87924dfffd4a19 src/backup/tests.rs f5afae218f3a6f91bc2f1f5a90094630ce5b7ca574de61807b5a317b13937ec2 src/backup/validation.rs -426a946b2c9e244f09f6cbc5f9664b8ad0737529a9bb5a3d549d27196a1f8043 src/balance.rs +09c3a622a7d2169e49e3369ea5f0f816eab2f717eaf1675c625d518c8f34d367 src/backup.rs ca1e03b56a872dca37ed9d05ead41c1538a808e80b09fb9494823c443cd51c61 src/balance/health.rs -59d90e709bfc2be6cd48576f1c1d222891523853a46f2561f0562d74d01f60a5 src/balance/lease_runtime.rs +109c55f9af8918d4f314f0796e19de06f3129eefee3ef405770b99fc368f2aee src/balance/lease_runtime.rs 554b730c39a935b7da46bfb76b474b99cee07f8c482cb0490f2cf81ba7a51cf2 src/balance/model.rs -9a10649c02eeb2c05e0ca520ef6ab701f79d46591190b4432977c2d5dbc6d7f5 src/balance/pool_core.rs +8550e5d64aa6fee442cea3e8e243fc4cfc1e8aa39726bc5cc6b2afa9307e3d05 src/balance/pool_core.rs 477132dd381e23f1be197c32ff58a0925f41d3e6aa63fe77c10973cefb2b76f6 src/balance/pool_selection.rs 8abb6048303ee8c022546a528d5693167766e7450efd8e20f37ec6fd0fa9ef5e src/balance/pool_view.rs 842318f21a919055522f8bd9dae67bf4ec1c8c32dc52314cc048ba1d54a0b379 src/balance/tests.rs -5049d2bd01a9c866f3bad2288d8d594df96bba6045dfe15cd22b4173562b7388 src/basic_auth.rs +426a946b2c9e244f09f6cbc5f9664b8ad0737529a9bb5a3d549d27196a1f8043 src/balance.rs 9a8858b7ff461a4d961ba701ca3ed6eaec15c3f7f05eef8e0e30edc63617605a src/basic_auth/tests.rs -7def03ace07dc38a4806277d75b6ce673d33ddaaf1c4855fa0a3372be1cf9489 src/cache/cold.rs +5049d2bd01a9c866f3bad2288d8d594df96bba6045dfe15cd22b4173562b7388 src/basic_auth.rs a9eeb93519aae2de7a1282296ced91d2eb8307cc966f8fbf6085af0fd21b0ad2 src/cache/cold/cleanup.rs 8354ae90c1a0857773a0c6d79427925e0115d94ccfafb809ccc1ebf4ef10f9b9 src/cache/cold/demotion.rs 2eaba3261da2a547e9af91330319a3abd3a8ad8b5b44a337a84040720209017d src/cache/cold/startup.rs e7c7fd71727d542c2df3e03a4a1cba9d839c516963f66bbfde1c29ec81053500 src/cache/cold/worker.rs +7def03ace07dc38a4806277d75b6ce673d33ddaaf1c4855fa0a3372be1cf9489 src/cache/cold.rs d51fa4f320be56c572f767617428c3f3838543cb0eeb7d1f8ae791e1a1ce9a71 src/cache/collapse.rs 7e8741bc21e5ca75f3bf8c5e57c59b6814dc1e5e2e0548963fe35089256772bb src/cache/dashboard.rs 780c2654c67dbe176d8310678e6deec8022bcebfd85b95df7262069ed41f57b1 src/cache/fill.rs -675d18afb53f25c56cba49bbb431fa39ecd9d7bdd192c7b39b0698225de7a2a2 src/cache/invalidation.rs c1357a13658e1ed5158084d3a41ba9ea906512774a5863ce9c56cb508d3fa6c8 src/cache/invalidation/ban.rs 0cc31d8e70fb3774c4ce3713b94ed79c184a8d8f0d38ce66a97724410733e17e src/cache/invalidation/invalidate.rs +675d18afb53f25c56cba49bbb431fa39ecd9d7bdd192c7b39b0698225de7a2a2 src/cache/invalidation.rs 373031e18d0c23d006a31822ef977015c0263637e3da70c1442407f7bfed1033 src/cache/lifecycle.rs -bd831807ca4720b3a41a6ea1d9a6c0ed2d935225e2dacbd9313be064f2e4a20e src/cache/lookup.rs 5aa0d9ae53f0350f5cfc4d1694a6faf9eea893460d421344d3bc5d1c4ce66529 src/cache/lookup/hot.rs 1779abcee77600c13050a2882f896a5b35625b1ae53737c2554179492ce0e40d src/cache/lookup/insert.rs 107a589974b9689009a9bbf088818dfaec71cd1aab8676bbc6d0118359a54a31 src/cache/lookup/tiered.rs +bd831807ca4720b3a41a6ea1d9a6c0ed2d935225e2dacbd9313be064f2e4a20e src/cache/lookup.rs 58c03fde09e5eab40968679793b364f4f1cf5fd832bd79ebcdc92bb23871ad82 src/cache/mod.rs -0c8b0d550e94d4474dcdf991aa0340228dc5f2f904f39350e6e666a16f16fe87 src/cache/model.rs 875116e319c5bb9d281817fed626a94e468e9e3305f21dac99d3f2535e2fb157 src/cache/model/cache.rs 0ec4f6d0adea1ed80a3ca17c68c9c85502491da2904d9d690b4169ab9b025b21 src/cache/model/cold_metadata.rs 29bfce96b2ffd102d9c848c7b717a45fedb9aa44511f62f7a1f28a53dafd063a src/cache/model/entry.rs @@ -348,80 +349,80 @@ bd831807ca4720b3a41a6ea1d9a6c0ed2d935225e2dacbd9313be064f2e4a20e src/cache/look d890524999dc50aeb1dbda90264b46a1d53f08ac923d831b89ba82e4a564d923 src/cache/model/maintenance.rs 5441fa3a384add28b6fdd15300cf2648f8237dbfc211cad9117754203be76911 src/cache/model/settings.rs 55dcc0ad84d6b73689ba43e759d5685c406419c7733e23e78d5b072891ab17d7 src/cache/model/views.rs -e26dce02df9c043329ca164fed215d271781bce8fb03240f528331bf3df6b9d7 src/cache/policy.rs +0c8b0d550e94d4474dcdf991aa0340228dc5f2f904f39350e6e666a16f16fe87 src/cache/model.rs 989df11e246123f760ef31689370f3500e0913fcc6b70d7ac0c2c95e2f412fb0 src/cache/policy/request.rs d6051d55168fe1db97980a54c22c708f4f325ba72bfc36f35a1851eb5dcc051d src/cache/policy/response.rs +e26dce02df9c043329ca164fed215d271781bce8fb03240f528331bf3df6b9d7 src/cache/policy.rs 7e2bcfbb2b57732cdc704d9df7a9298fb80e735e37984431bc936630e41e7f8a src/cache/range.rs -7ad96b3baae36ec49c90d03b380e749944fb3d54abb045c90db3a35f6b89b1c1 src/cache/tests.rs 9183fa8e6086e92fb148fc278fd866d44e3dd988c848a071e32066e44a1cbf0a src/cache/tests/cold_tier_tests.rs 3842cdcf7c3414b9c30f5fbed927b93e835eec3ef96d4c6ee44250038b834276 src/cache/tests/dashboard_tests.rs d7a02eb7bad37e6a5524600aabb5e60ccf50e26c7a487e2806891c9d8acd48f3 src/cache/tests/http_invalidation_tests.rs 1659ffd8d227d2ec8b98e6a5e06b6143e6ac4ac06ab423c184980abcb47085da src/cache/tests/scope_tests.rs d4429cf4f35fd8d0082108fb62602fde72c9c2fc6edfadfe3268c6a8214a8f89 src/cache/tests/tests.rs af28534625859ff66011e10c54cff8f82c2cfa1c173c4d99b89c27c1437f0659 src/cache/tests/v022_cache_tests.rs +7ad96b3baae36ec49c90d03b380e749944fb3d54abb045c90db3a35f6b89b1c1 src/cache/tests.rs 22f8f16cf7383c579b19810819030072fc827681478e17068dddc85da8dc53ad src/cache/tickets.rs -e0146bf5d2e2e33aedcccd9205e5bd457559c9d77dc7ce266f8c165d1b867f0f src/compression.rs 4bdbde197d135fee8be20e93427eba8ca7b60b801717ed7ca734319bedb6ef05 src/compression/tests.rs -7bc8285cb2012506fd1a38a71dffa561f885f4ab65a9b2a7c8344319e3cd1cf6 src/config.rs -dbd57ef3307b937614cdfdb6c1df82acb80cbbef39f09d19fe2edfc24334aa58 src/config/backend.rs +e0146bf5d2e2e33aedcccd9205e5bd457559c9d77dc7ce266f8c165d1b867f0f src/compression.rs +56b3d37e7447995bec2fdda1c17dcc74b60958a63ca88b88ff57e56347d8b0b9 src/config/backend.rs beb831a208c4e5f7ebe8327686b1e0edc66608c7032f895cb4fc3366b189aae0 src/config/compile.rs dfa89a9e214d0b33b4efce8fa678f1fec17d17656e203acf84dfd09ce33868e0 src/config/internal_routing.rs -70741af46ad6f712b0c0e029c066d04c14a689eae0c831af264b09f6e5c41bae src/config/model.rs +40e09627e655d9319b576592f8d635e8c2da12905e5cee5952f7b6596010eb8c src/config/model.rs fc993c356386a9ff17379d48a70ef388df5b2b10ca9ee04f06342f68f9dbbaae src/config/routing.rs a7ccffdeeb258adae41c616022ed0181cf590d708d305d48c63317e4a3f68699 src/config/snapshot.rs 41a4fa4f9a919a8c848ee01d01423d82a136790c9d74f27319c1e5a6d9f18097 src/config/tests.rs -23541660151d9005555a07db6d491455d61b185f0cc70c3ec3a0bee2aef08dc3 src/db.rs +7bc8285cb2012506fd1a38a71dffa561f885f4ab65a9b2a7c8344319e3cd1cf6 src/config.rs 30f0b7269224a64e3f1ec9e6fbe9e27ddb1d308dc84cc9882e50545cf612695d src/db/certificate_api.rs 20ba52d3755c359d670968efa53b0604ff7cbed63a416f9f765b50a1f45074ac src/db/dashboard_presets.rs 2ff744223934685439374572b83fd639bb3eabdbac168bea4a6501a6e9ce0ee0 src/db/history.rs c05a02618d476e4670b4f9930555f199cac776cdf09edb8fcc1a675695b8f99f src/db/tests.rs +23541660151d9005555a07db6d491455d61b185f0cc70c3ec3a0bee2aef08dc3 src/db.rs 19ab6c3ae37009c544291cb0b65c1bb9c721e168f95c1ed3fa88155aca7463ad src/demo.rs e69031875ffec32c9f548c43bed5f1a5c4c626e9568c9173e3b093f9a8b73036 src/diagnostics.rs -b45d6040810c93ae2acb04ef6b0a59c189751edf9008cd81a4521c9e04e7f07d src/docker.rs 93a8a3f6ba12761ddcbd19f22f32fb2c0e6948b660fb774ddd7ebdba9b6562a5 src/docker/client.rs 4c2c988191a97f81568a8df64af772463d010844f1abf327871901cdb32d4336 src/docker/model.rs 5f4610c59b2d4ff6a205a2d9ae11bc84be689b2a68302459f546b3f43e53593c src/docker/reconcile.rs a12c47df4b90b6fc36c978a42477e6ddffbbbf69aed7252101c2fd829709bd6e src/docker/tests.rs -2a02ea2e010cde1b1fe99b2e6baa80ea344ea6b332dbb3c4be795b71541f64a7 src/forwarding.rs +b45d6040810c93ae2acb04ef6b0a59c189751edf9008cd81a4521c9e04e7f07d src/docker.rs 057e652f216a1478638cbb53b1c64ac88c7f413a65825caacbf893c5af004a63 src/forwarding/tests.rs -af7a4e91c32ffbe4701c8c06e48ba3f8bbbe55e8b9e877aecb30ccdf858e9e8f src/geoip.rs +2a02ea2e010cde1b1fe99b2e6baa80ea344ea6b332dbb3c4be795b71541f64a7 src/forwarding.rs 80850f7acd1ed4c0048b065e9e81740869a8efbb0d9b5d2e83d8ec7fbc494703 src/geoip/mmdb.rs 06aa056d789742cc11c8826c338d985e6a6fb22da6c3530937eec38e35478034 src/geoip/mmdb_tests.rs d3dc58628683472ef4d205fa5c42f0b7ef83cf90846146d96941cd3b36a40766 src/geoip/tests.rs 70091bdf6f38f03b6df8c8c4407c300677e57f293130122503f936500963d37c src/geoip/update.rs c1b49bef28d87712e4c46138e70b52c4bcfb1a00196579031fac043d0b4aaa7b src/geoip/update_http.rs 9a912dad0b8d1e10410fd95d0da3684656092ef85cf8b23eda4ffe7fb92921d8 src/geoip/update_tests.rs -f6ebafafa82391f8cb3b5a09331f3762b6ca5a9fb64bafb5772cde3ab814dfcf src/headers.rs +af7a4e91c32ffbe4701c8c06e48ba3f8bbbe55e8b9e877aecb30ccdf858e9e8f src/geoip.rs e4f07bfb7eb75b6897ef812870caff158679a3046d64582428abe8c6df490dbe src/headers/tests.rs -9e21edc67a14ef8a2ce3dda0ed18784d0fd0be305973e5d216058b7ac802c6cb src/http3.rs +f6ebafafa82391f8cb3b5a09331f3762b6ca5a9fb64bafb5772cde3ab814dfcf src/headers.rs eb57117938a447d9a6c2ec1806ebd9fecc640a68c88950a8b35fa9bad14d0dc6 src/http3/body.rs ed25da264a2d25b40c4fcddc97c626a3a43d4f3a9be6c6d31af58b9e4925c198 src/http3/diagnostics.rs 37f54961d8f52b79a75fb2be31e81f4fdb1e66b06350345c4483d6197b8a0bbc src/http3/identity.rs f2a8c2a98f4ccfb708f9d75a48ec7b351232be9b8a4364b7e63ba14d28eead7c src/http3/settings.rs a31db7eac12b578b1eb5a74fae98bbe36312da95b7d7173135dbc41fc454da21 src/http3/tests.rs +9e21edc67a14ef8a2ce3dda0ed18784d0fd0be305973e5d216058b7ac802c6cb src/http3.rs 68fb5ab6a889995a66ad12cb6acf88c5d04f0aca572e2ae15f1a9cd171a5f24b src/lib.rs -2551189a3e5701371026c9abd2c5d44d37495f28fb67e220180421bfd315e3dd src/logging.rs 18c8d74c21a47f234be93551d7264b1f8f8c00d1bbfbe555d91618c58cea0740 src/logging/error_runtime.rs -33b4b7f69560c2acc0de0eac62604161d38dbd7044c5789a1352f0cd3882ea70 src/logging/format.rs +bc3873c6f22d1ee7fa76ae586547c9867e6eeaec8266e3ed1f529c87108df2b6 src/logging/format.rs f5815d778cc1785723faed4abb15f9880f22b6524b5289fe1e7158905aac0986 src/logging/model.rs 42fef80af50735cd566c6801dad86ba81be98cf4a8ec7d45c2ea26f53f556a22 src/logging/runtime.rs -584782215ea78a88526cdb5515031d8d5b87910875e26c0024b93a06680f6791 src/logging/tests.rs -3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs +7ee1eb1d3b52b4c14303f8ca14a1924d4d6bbe75b2e43ce3ca47d7958f792c7d src/logging/tests.rs +2551189a3e5701371026c9abd2c5d44d37495f28fb67e220180421bfd315e3dd src/logging.rs fc8e99eac83c083d65101f6402aacbcc8392d66508c31566a50de3ebcb12e7da src/main/admin_runtime.rs 42a9197475823f67c1ca762951ede5d7a2f10124bd6ed4d1a3365f4297be894c src/main/bootstrap.rs 5a98d0e249338e73aa57ae2e352dc16db2f79c2d042c1544ce1c2022da76a31c src/main/proxy_runtime.rs -4dd7c1084a6214c0f2513ebf86fe8ef144470695557772bfe25979edd64491ab src/main/shutdown.rs 0a6578a0f4023b9f8d73043bf942dc5e096ef9afb8c0d2f131c14f65d08ac01d src/main/shutdown/tests.rs +4dd7c1084a6214c0f2513ebf86fe8ef144470695557772bfe25979edd64491ab src/main/shutdown.rs e5c2e9189777725f4afa1c3c052e4f75047ec3f2f486630d990f5d3c194bcc96 src/main/tasks.rs -49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs +3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs 201ab702722d6d21585fe8bb49939150d61bfac7ed8dc198b98c2cc6098ffb5f src/metrics/global.rs -49b11df65b4fb0b5b727f9f867436584680b45e3f4fbd53f74bc0cab8b9a88ec src/metrics/vhost.rs a54b5b34ea65f29723b89b81bf7968a238e8247b9c9760f3430d3504cb1017c9 src/metrics/vhost/tests.rs -a4702c95c7e5077c2ccff86e2e3216e1da6ff9ee4383ae199a0aff2f78db325f src/not_found.rs +49b11df65b4fb0b5b727f9f867436584680b45e3f4fbd53f74bc0cab8b9a88ec src/metrics/vhost.rs +49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs c915d30bbd7032d867e7916b4cd45f3bb7e7604d3f4558f4383c59394b4300c6 src/not_found/tests.rs -d66936cc15fd5d56566d40fa8cab93f80d94b05f10ac44478a30f63572a95e32 src/pages.rs +a4702c95c7e5077c2ccff86e2e3216e1da6ff9ee4383ae199a0aff2f78db325f src/not_found.rs da40e8ece87245efc49a0c4e750526b1d19fd97565856bd5caf641fcc1dfe18d src/pages/tests.rs -0dba7b6a33866cc7bbe05509b01961e545892645d1dad4eb88ac8a4bec65e3bf src/prometheus.rs +d66936cc15fd5d56566d40fa8cab93f80d94b05f10ac44478a30f63572a95e32 src/pages.rs cc6fb14351b009a16d07ed0f412c2bcc68cd7e551421aea03d8d97ceec9bee40 src/prometheus/core.rs c46882ed26742d93806a976257674326e105b8ca1fc23f5ca81e317b268057aa src/prometheus/docker_misc.rs d3c6b08088df0003144544936a55e2a3661aac85292821b594f00adfd19a2951 src/prometheus/model.rs @@ -429,139 +430,140 @@ d3c6b08088df0003144544936a55e2a3661aac85292821b594f00adfd19a2951 src/prometheus 028e1dd9f2ec2afaf699a6b21c2d4b0d96e76f11f5e4ed009c233dadf0f3be51 src/prometheus/traffic.rs e03f0d6b3f35dae6b98c394837aa0d17e4d73a8306767b4c607868bc00c30286 src/prometheus/vhosts_backends.rs fb2d8d45cd2e947f783eb981b37fa418c39fcb327a3af17a648fadea7f37f888 src/prometheus/web_php.rs -4c7199c04e6d314e89825f80feaab35a2efb3b893511f70da0cda46ab1d0bc9e src/proxy/body.rs +0dba7b6a33866cc7bbe05509b01961e545892645d1dad4eb88ac8a4bec65e3bf src/prometheus.rs f0f3e21350e4fb15fa2ae282da6b290857dcd6716e62b183e9ecb728b7c24c63 src/proxy/body/tests.rs +ea38488f8507609dbf6e5ae6e9c10f4045bc424a42e8eb11b608538a2d55d6ee src/proxy/body.rs b88d1d7a6837a2a470e5863f1f3247878daee62791fc3d8e72baeb66469bdfbf src/proxy/cache_helpers.rs aa49c9928bb91e3d5bae0d340d88396a0417b9edec6192d41e1403533319a897 src/proxy/cache_http_invalidation.rs -676c1b325fd8e55bcdd86451c7d1d8b8d347ca00e8da6b3e5b148a58c376a9b4 src/proxy/cache_plan.rs 3404b03096862de1050088f62cdd811ea1ab61d191f902501173107f81f23b9b src/proxy/cache_plan/tests.rs +676c1b325fd8e55bcdd86451c7d1d8b8d347ca00e8da6b3e5b148a58c376a9b4 src/proxy/cache_plan.rs a8848cbdf85368548b003af2b43823c453866c472af4c7d2ff0ec7da14c44c8d src/proxy/error_fallback.rs bf2e6bbd64fb45fceed54af0bff1c1e00da22b79c64dd8f5c23d2af0dbab0b7d src/proxy/forward/cache_lookup.rs cfa941b60b5d167ec9d8e2566ea3342b588d1a9078a6ca711ad6062ddf00fec9 src/proxy/forward/response.rs -8168a42c3992d002e87fdd5d52a37f53dcdf69cbc859221350d888e94c305ab8 src/proxy/forward/route_cache.rs +7a5f43fe65e5a746eb84881da0bf784215081ead66d18faa7780533ad5df9c8c src/proxy/forward/route_cache.rs 96c97928986ca89c2d6d415d5b107b9cc7228410bae93a2c08c24974324f4d8d src/proxy/forward/success_finish.rs 0a921474ef2b93eabdaf3888651cf29130eace5ccb1e0f5d6bc25e6aa69280be src/proxy/forward/success_prepare.rs bb24da9c9ab986a9c4d4144d4d1be0f661561d4afc755f298b58bc679b182850 src/proxy/forward/upstream.rs 11dccc239ac8a8e9ea06e884c2f2658f86b7807d14a59b8e052bc7dd75e86d63 src/proxy/forward/web_source.rs facde401ba5bcbafdcea5524f236cfae37eec11144ceeea879e83314c84bc97e src/proxy/forward_entry.rs 9b25f5e3ea4443a63ee6f45d12e1957dd316fe8e85325fc00ea416faaab1025a src/proxy/h2_graceful.rs -c438066616527a3108f2ad53e3698ae9b2a80961714394cdd0a450c92f99d992 src/proxy/headers.rs 5cb7522ceb9a9f083a15596ac4898559295e38d1138c80099577211de8a868ec src/proxy/headers/tests.rs +c438066616527a3108f2ad53e3698ae9b2a80961714394cdd0a450c92f99d992 src/proxy/headers.rs 3f25429418941eb72c3847125340db74684ad316eb278ecf658953026fd41f4f src/proxy/mod.rs 55133ceace2e5262d9c54fa41bea099a27b817d1e09c1cd0e2493fd9cee4da38 src/proxy/redirect_helpers.rs -c61738672f210e97333c8c89f9b35f8db454cbc1c36955288dfa4cfff2bbbc04 src/proxy/request_id.rs -6ff45990f6beb914e89924bec2c41022f3240a14357cf66ec4f8ada414a49767 src/proxy/request_id/tests.rs -8ad3619359f3befa14ea71ba43c638fea8f4d8a859cd07e4ed505731b8e32e72 src/proxy/tests/authority_tests.rs +d2df55b6ccfc7ceb8a3d2c49f664acd183448e3d932d5399d6692e87f79a094b src/proxy/request_id/tests.rs +96da894a86c05f8f467bf83cc23e56627ed3f9f490ffc936872d70126c8ecb41 src/proxy/request_id.rs +e56c65c85e42ee4fe6adf3655f3b2d176798e37c648db6bfb8e1602300b723eb src/proxy/tests/authority_tests.rs 1aa9bbbfb2659facdaeaf185e11221b1c8cd5fe6e23c7c1dacd3aa55e3db4fcb src/proxy/tests/cache_fast_path_tests.rs 0f2eacadf1447db8aa1c1a78005aa262e0f747096bfe7e0a0e41a2cd31a0d17f src/proxy/tests/h3_navigation_tests.rs d134b0eb617f82f9971b3f3461379ea37c57bd0079bf54748d0c434b2ef1b475 src/proxy/tests/http_invalidation_tests.rs -d5717fac0e7d2cb2939626eb95f4520241b677fe1f3e045df12c100315302a5a src/proxy/types.rs -8d7848645c25932589d0dfc54149bb71a51edd7188aed60dc4d6ca8179b361ab src/proxy/upstream_helpers.rs -2d9142e3ae95895b5271b4009ab48236a3a2074ce51179c021193b35777560e4 src/proxy/web_cache_revalidation.rs +7789a80202d606dcea3776345464a5487bf6124e4878bf8c94db91b67f0d4eb8 src/proxy/types.rs +1cbca5fbe91f40447e9eadc349e2d56f220119d14504f07ce90ed70f945a767e src/proxy/upstream_helpers.rs d7b4b809b12ee89293df45e9a64b1c9981fb3f89deaab3762f4a3ad3992dd7b1 src/proxy/web_cache_revalidation/tests.rs -69f775f6746b4c6d729dc10961dbef54168846f4ce9cf7fe453ab5a2b879c9b8 src/proxy/websocket.rs +2d9142e3ae95895b5271b4009ab48236a3a2074ce51179c021193b35777560e4 src/proxy/web_cache_revalidation.rs d866e7e72dee6ff54c2418c34e9899cbcd7e4e027d9cbe2b625044dc9062e4ee src/proxy/websocket/handshake.rs 7e9c4d4c207badabf90630223964bb2ad641f06d4149843cab751f192b8b4adf src/proxy/websocket/tests.rs a44421714aab37f7ceda19e3ec2512c645d2a3ce5dcb3da5bc7a0d87a442d6a8 src/proxy/websocket/tunnel.rs 15fabd38576b61f650c039acdbf7ac5a5bd24330062475b94315285b2dc258b6 src/proxy/websocket/validation.rs -545247eb00b0116efbd4cc913cc251195a55feded1530e0d8d8945abbae78039 src/rate_limit.rs +69f775f6746b4c6d729dc10961dbef54168846f4ce9cf7fe453ab5a2b879c9b8 src/proxy/websocket.rs e7e3a898e2c769b1ef11e58a13c1053104ada93dd694f8a03ea4b647c44c7d69 src/rate_limit/tests.rs -d40ad63939097d2b91339127d5835a74b618d83b7ba31db0c512f05a82107d6e src/redirect.rs +545247eb00b0116efbd4cc913cc251195a55feded1530e0d8d8945abbae78039 src/rate_limit.rs 811c83ae9fd0f5a13d35b5443cd37843f82b9d600bee4bb136ba9b87732a5347 src/redirect/tests.rs -d0939e1b0829c25356a0b67ecb3a178c74936dd07baa15fee24eb32544db6cad src/runtime.rs +d40ad63939097d2b91339127d5835a74b618d83b7ba31db0c512f05a82107d6e src/redirect.rs 8d0d7b01871c8c588c40a54330a1053c85024655fbe38516ca6c94c3c19a8956 src/runtime/certificates.rs 2ccafa8a5a8462417f65d05a0c6c42f294afa4ad1067ea0b93623181546d1f96 src/runtime/constructors.rs 92b4b284f1b69dca58e4e3f76199f0aecc397eed26d10123b427af1bb8a4a57e src/runtime/docker.rs ee2050b9f8efb45a209c91d76043a1ba29c4289978896aaf7e7d344d48fdc369 src/runtime/tests.rs 779137f320133ec96f73c8b3d357ad4f0b825ee2367e9e8300f4f143b38eecb7 src/runtime/types.rs 5947afaf7d070ea2047eb7969a8e0d84f981e91ace283f6506cf7685fb8afab9 src/runtime/updates.rs -208a378b14ca80e1412ffa19ef1b140594182971e2e797ee0bb95bcdb8404da7 src/secret_store.rs +d0939e1b0829c25356a0b67ecb3a178c74936dd07baa15fee24eb32544db6cad src/runtime.rs 6dca8da072a3bd610704a391bb6e25c9042971f4ebc60edb47665ce1d74cbc58 src/secret_store/tests.rs -6fa1d8c352bce7a16ff0208bc4cdbe80404715df0aff4ed26910a5df57cbbcd4 src/security.rs +208a378b14ca80e1412ffa19ef1b140594182971e2e797ee0bb95bcdb8404da7 src/secret_store.rs 0e5fc2aff856bf6fd959cba12b5ddd1e3b3cbeb0fbaadde906c811cdfafdaae7 src/security/tests.rs -98c52aa3b50d2e211ab752ccaf353c9a8c1500a87f9c1684b5dd0ba9d10e4619 src/server.rs +6fa1d8c352bce7a16ff0208bc4cdbe80404715df0aff4ed26910a5df57cbbcd4 src/security.rs f2f6196632b2fdb2444189624f961edaf5bdfff57f700b0207b57e2491c1daa5 src/server/http.rs 050c48ea1127aa87dba92c074ab84e8346e65dbe253621ea53f8ca2f68d791ab src/server/http1_connection.rs 8c6ca5eaffb9acab57199d0349722703b903cfff3752baad2cf2596bd51188de src/server/listeners.rs 4424a99b0b688d1119ee25626778b4804aedfaa0799a4a9bc0ee99bc5c7ba96b src/server/sendfile.rs -71899a9b1897fa22ab9e8eb6a670054a6d382bad78714006b9b218d07ba99902 src/server/serve.rs e9e478cf45b88994eaffd115f51764b6b07852ee3c941cafdabb20584d3dabc0 src/server/serve/tests.rs +71899a9b1897fa22ab9e8eb6a670054a6d382bad78714006b9b218d07ba99902 src/server/serve.rs 960a82e02f09d75141fe3c1eecca915b66f17fdc8167196cd2fe21d5e6859376 src/server/tls_runtime.rs -73ec453ac2850f6963f5bc33401cb8c92aa26d5123f15d68e6633860d91f265e src/settings.rs +98c52aa3b50d2e211ab752ccaf353c9a8c1500a87f9c1684b5dd0ba9d10e4619 src/server.rs 240ecdd70e8662ff3b84c21830dc0cbbbacadd755d56146e54214bc2887f068c src/settings/public_origin.rs 1f1121c3562c8f8f3fdee2113be2afffb8849be2d303e4c4409f87478b6e3307 src/settings/tests.rs -9dbf814870b8636770f579c3d36d16f5b6afe8bc66adfad356c26d7ae7dbce45 src/stream.rs +73ec453ac2850f6963f5bc33401cb8c92aa26d5123f15d68e6633860d91f265e src/settings.rs 254840126be2310426f6332f5efbe98147d9d8de1999731f40a3d13325c1dc6c src/stream/health.rs bdc56be7dae85a97a4d5b8d46ef31ec4177292b1cadd507fe423b6d4ee1403bd src/stream/model.rs de138a4ac68bcddba3a4f17ceba008bab643359d76a68726a755069f19334bed src/stream/pool.rs 6ec42b8647c95a0c2025462e0be29c71481ce1c3b1f4d4193926471f1cf06be0 src/stream/runtime.rs 9346cbd71e006e41a7188f28eb86f9b00bac822e4cb31c76c8d6d2ec5d4c4711 src/stream/tests.rs -3070d78a7b88c2e47ed59960fa35e7d220a4cd7d78e48f6c6d2bcc5ff77f9620 src/system_stats/linux.rs +9dbf814870b8636770f579c3d36d16f5b6afe8bc66adfad356c26d7ae7dbce45 src/stream.rs d9123c623e2a97d09e41edaa29993b4b2c8cba39a3abdf2b48a205aa9a9c2939 src/system_stats/linux/tests.rs +3070d78a7b88c2e47ed59960fa35e7d220a4cd7d78e48f6c6d2bcc5ff77f9620 src/system_stats/linux.rs 751d59582fb92c84e0c0cc7c0855c9d0a545805c9ba3be61826e50fb96c297ed src/system_stats/mod.rs 050aa831b1012c4aaade63863f433f01bfacf5d3f95787e57975572116a30ab2 src/system_stats/tests.rs -7467d83066bb6986d8c9103597f7a7617ac677579ed46d573c573ff6ca57bbd9 src/tinyauth.rs 0e1d87554db55e724f30c41ec4a5c80bdb8fc9c8cba8700ac509864d4abf8871 src/tinyauth/tests.rs -1084cccd9de28acd6cb0f0813de7c0953910f51726f73f4acdb2e3ada4593976 src/tls.rs +7467d83066bb6986d8c9103597f7a7617ac677579ed46d573c573ff6ca57bbd9 src/tinyauth.rs 4972a436f8878dec5f9eb8ff17eb82670b0bfd9e94e821c516575479f5fc1919 src/tls/certificates.rs 436575144aa352498a4696f12c2258bef3a898f3f50e6fbc6d67c3b93afa099f src/tls/external_paths.rs 3e6947dde167f82a5efc407ccf9bd4c9f5b26cc8633d1418a10284bc1a1503d4 src/tls/server_config.rs -31c0f51a3a0bab433f438c3ba1b1cdaf3c63fae6086e7d99b3464a537f75a5e1 src/tls/tests.rs 2af2164ecdf2b7fd9eef7c6e358dec053dfac9aedaa501dbb75df742d18638b2 src/tls/tests/acme.rs 93b359e9defd62506729bc90e56c96783e2f14dd477dd67f337ca83149da7168 src/tls/tests/external_paths.rs +31c0f51a3a0bab433f438c3ba1b1cdaf3c63fae6086e7d99b3464a537f75a5e1 src/tls/tests.rs 10c4a187e3aeb0c72a8c8968ea0cc8bfae4e3cb2f3d834ebd3da1056c4171ec5 src/tls/types.rs -c2f802d95da4332fb8480e83f0c7502334457b3408141c1c4516c9ba320a0463 src/traffic.rs +1084cccd9de28acd6cb0f0813de7c0953910f51726f73f4acdb2e3ada4593976 src/tls.rs 36301bcbd20b557dd3d58e77a9175f3581947f577c0bb0b972899cec6786b03f src/traffic/tests.rs +c2f802d95da4332fb8480e83f0c7502334457b3408141c1c4516c9ba320a0463 src/traffic.rs d88c92f5a2a7773bc3e715d24039db35da4df53881f1a0859eec819b96f06d3b src/transform/apply.rs 87f8af5261d79072f8215f1fb8a90715a58e7626544fd8176ce840e3111bdb92 src/transform/compile.rs b2c5236589093454ec60984b785ad6a4af6149d47f6db37d8e08c17fec7a2c1b src/transform/css.rs -5478670cef87777b14a2dab308f5a9182df56d7ea2f5625bdc94aa7e3d661503 src/transform/html.rs aa99b0d3029ef92c78371cb90e11f2026cce890d1be8e10ebd7fa9bc5cb13287 src/transform/html/tests.rs -a1d2fec07617c8a8dc425bfaeaf77c844152a0a174568a88a5baf4c317cc6c5c src/transform/js.rs +5478670cef87777b14a2dab308f5a9182df56d7ea2f5625bdc94aa7e3d661503 src/transform/html.rs cf5195ab6bacccae0082da7b3f1bd0382b73fdeb3607f4ae569ca6daab9d10d0 src/transform/js/tests.rs -226ae561dc7b0243c04e55e28cdfd2b76acd48cf614d1919b81c196660b03cfb src/transform/json.rs +a1d2fec07617c8a8dc425bfaeaf77c844152a0a174568a88a5baf4c317cc6c5c src/transform/js.rs 216e24e29ea87be4a124f95f898e155f127fd959d7d5d9b30f8cd0b2b5018f30 src/transform/json/tests.rs +226ae561dc7b0243c04e55e28cdfd2b76acd48cf614d1919b81c196660b03cfb src/transform/json.rs 6a8c28be93fb590942493c075b29ace1527981542bd183f232aa82b62e951eca src/transform/mod.rs fde2fa3aa94b30d8536afec1c11348cfbd922d0b5ff89e91d34266e8e3863e26 src/transform/model.rs -a4e1d2aa2913587c250f251356431097b76995aee6e8bf9e35c4978a95491de1 src/transform/svg.rs 0f53c64f442390ee20a59087a10f8f9ffb58053263e9aa848ddabfae1a6f3f51 src/transform/svg/tests.rs +a4e1d2aa2913587c250f251356431097b76995aee6e8bf9e35c4978a95491de1 src/transform/svg.rs d1f3f53fedae0d8ef69eeee457f7d30ed5a670a1ed4ced60683400ee46680260 src/transform/tests.rs -6146c59f36cf419623747d497b3de685da6c84964df43317bebdcb21deb9ee09 src/update_check.rs c42d000d1d576721af3ffe930589388693001b8113554a7658f6dcbc665b5d1b src/update_check/tests.rs -92efcdaafb9f5a8514daf63d7fcde29be977d7bdcd90162eda0ce00d6967ffbd src/upstream.rs +6146c59f36cf419623747d497b3de685da6c84964df43317bebdcb21deb9ee09 src/update_check.rs ca695e246bee5389b3757fe8588e48bff5d0ad473bc8588ad4ba7b52c99d7dfa src/upstream/client.rs -ad29f3fc15c9b627b5d752af5adc9f99ee1364fec2df63f1e4509cb223988efb src/upstream/client_tests.rs +56c8a9d1c801be0f41b35c75cb42716c46976951948009c8707122c0a6a1eb58 src/upstream/client_tests.rs 75fef0a488d3f3dabfa03c2eabc5995442dfb3c9a1fde8eb6653a124dda25d43 src/upstream/protocol_tests.rs 8a112154b9631f62a54584302a3ff6a64e4f177ce79aba678b4cb23e466c6ea3 src/upstream/tests.rs -a6b4905c245a22eafb1aa535d303d0fa24e4353db94e1d67fd42b825bf8c5adb src/user_agent.rs +92efcdaafb9f5a8514daf63d7fcde29be977d7bdcd90162eda0ce00d6967ffbd src/upstream.rs 6d7cb73de7d8c6f3780736480f5f81ef08813f4626fd5ed494aca0552cf214a1 src/user_agent/tests.rs -3aa1b07cd26d4a5b9e5e94d1d6a712ef1397d827e7f1e8c01f7195b98d2c5f67 src/vhost.rs +a6b4905c245a22eafb1aa535d303d0fa24e4353db94e1d67fd42b825bf8c5adb src/user_agent.rs 6678192de56bc10c5cf82bdff8da14eece7685f409592450c07478333aa3b99c src/vhost/error_fallback.rs 63e9fc7f7be78dc3e16a89f015a56a2da6ba3aff0197b70ff48321dfb17529bb src/vhost/model.rs 35a0aca3eee8d47d1bab27c83d4532cd572fc3e17b5f0d4a8d4515a8f85b3d96 src/vhost/redirect.rs 9b16b5307e140db97363f56c9fac8319e56df846bb1c919d3bc06091ab6ce69c src/vhost/tests.rs b7d0f7f5d0bbd74add3fda9b1308350c4f70f8b6debf979fb977a4f2f659c32f src/vhost/validation.rs -8d1eff4b24f01d137414ef5419f75dd281879228adb70409645b11b3c78c20e9 src/web_server/config.rs +3aa1b07cd26d4a5b9e5e94d1d6a712ef1397d827e7f1e8c01f7195b98d2c5f67 src/vhost.rs 9744e8c29ae72a4d9fbc9a76166a97f8292c4c312295ea40d1cb699e03d80a9d src/web_server/config/compile.rs bcaa1ae15134d85668b05a88d8abea01e5d188f535ad83cd4459732fd56f2f44 src/web_server/config/model.rs a863ad1337fbc353c0f42caea4ac8b3644792e2aa08c37f992b7882b4b8dc6d5 src/web_server/config/validation.rs -86c744216d7132d8294a6e90ba9be5fd871646006313d1c62506d40c81db9b12 src/web_server/fastcgi.rs +8d1eff4b24f01d137414ef5419f75dd281879228adb70409645b11b3c78c20e9 src/web_server/config.rs 200cba8cb3482e49db73d2c60cb60786175177c7d4be2a566cd281fec7ba7fca src/web_server/fastcgi/protocol.rs cef44ccdf882d84892a64bdbfdc1ba01380fce5e5f86d2829b0f9613f738b964 src/web_server/fastcgi/response.rs +86c744216d7132d8294a6e90ba9be5fd871646006313d1c62506d40c81db9b12 src/web_server/fastcgi.rs e15361622683268c5606a39a9e5db3938e630999767898391226e94ad2e421e8 src/web_server/filesystem.rs -d4fec3682532b0f2d11a0aefcd024f69fb623a39922ddb6c528a2d2de265347d src/web_server/htaccess.rs ca3cd376b313af1ce0ee65081b3327eb213f000d5a215aa4ee6b764de9702126 src/web_server/htaccess/model.rs 76de1d11df75f4a64556b2821fd0a1774d26c9f0ca0e5f2b5fcfad0722d43d47 src/web_server/htaccess/parse.rs 66260d159986d41eab0dfde2cb9f0713fc1cc209c2fb47e9023bd24fd37919e8 src/web_server/htaccess/rewrite.rs 8ca74d9cc4470045301f70eb4a9fbabd0dbfe74434429148d2230df9f996c06c src/web_server/htaccess/tests.rs +d4fec3682532b0f2d11a0aefcd024f69fb623a39922ddb6c528a2d2de265347d src/web_server/htaccess.rs 3341264f6e8041ac5a5d3c4b1601a69787386116019b5febc1a5e22bdb1186f9 src/web_server/metrics.rs 0dbef9b71869fc52cf01e9cb400ffd48fb433ae625383edee6c6c2d735608a76 src/web_server/mod.rs 67ab8bef567452f123c4b9b46a942b39c93f1f765c128edca7ee808ff1c5e304 src/web_server/request.rs -d98c80663847d6d83073df074ebff65952755415574708abcc153bd8d3adfaa9 src/web_server/response.rs 833c9fea744b9649046e1a64d6fc6972fbf6cea640e1943b0db52b7ccfa9faad src/web_server/response/file.rs -ba793a140699956a9aef6bc79b0c731c481af8ab7ac3d816a28695743febcb30 src/web_server/response/file_body.rs c2e54fd803a10f74490f183e8c030fd0e3e72f5de4e79c55bc393ab57d90ab9a src/web_server/response/file_body/tests.rs +ba793a140699956a9aef6bc79b0c731c481af8ab7ac3d816a28695743febcb30 src/web_server/response/file_body.rs 20509f9302b79d3a7be76dda6edaf5711826d3ddf47d19ebe07cb289427ca328 src/web_server/response/headers.rs +d98c80663847d6d83073df074ebff65952755415574708abcc153bd8d3adfaa9 src/web_server/response.rs 305c0109b4f869a99381a142853f9e2205f5c7c4f904b2dfa4643274bfc542af src/web_server/routing.rs 400e1c8aa2fe857d96036a64499fbc3de44c1e3a53811c5b25101fb843359792 src/web_server/security.rs 54cf72a82e023fe46513b9cf3130adfef1cd96c40f790c533016b4825d542e06 src/web_server/serve.rs diff --git a/docs/CACHE_HOTPATH_0.34.0.md b/docs/CACHE_HOTPATH_0.34.0.md new file mode 100644 index 0000000..448f6eb --- /dev/null +++ b/docs/CACHE_HOTPATH_0.34.0.md @@ -0,0 +1,81 @@ +# RustProxy 0.34.0 HOT HIT follow-up (source candidate) + +## What changed + +The *existing* optimized HOT cache no longer removes/reinserts full entries at +HIT time. This follow-up further shortens the critical section without changing +cache policy or accepting an approximate eviction algorithm: + +1. Store each published response's immutable headers in `Arc`. + Snapshot the Arc and the shared `Bytes` body while locked, release the HOT + mutex, then clone the headers and build the per-request response. Header + cloning is **not eliminated**, because per-request `Age`, `Cache-Status` and + transformation annotations must not mutate the stored representation. +2. Preserve exact per-HIT sequence, hit count and LRU ordering. Bypass the Vary + index traversal for the common direct-key/no-`Vary` representation; continue + selecting keyed variants for other representations. +3. Replace the per-domain flat pair-key map with a nested ordered map so a + known `(scope, domain)` is updated using borrowed keys. Preserve the 4096 + distinct-domain limit and the dashboard wire format. Update cached-byte + accounting without allocating `String` keys. +4. When there are no active BAN rules, a cached response does not acquire the + BAN mutex or clone any BAN rules. Active rules still perform expiration, + matching and removal. Publication/removal of BAN rules updates an atomic + count; BAN and PURGE security checks remain enabled. +5. Remove repeated full HOT-entry `sweep()` scans. The existing + `retention_deadline()` explicitly returns `u64::MAX`: entries beyond their + freshness TTL are retained for conditional revalidation and `max-stale`. + Capacity/idle eviction and cold-tier GC remain responsible for memory/disk + retention. HTTP freshness checks and `Age` are unchanged. +6. Avoid allocating the static cache-reason text on each repeat occurrence: + previously registered reasons update by borrowed lookup. Recorded dashboard + fields, counters and reason labels are unchanged. + +No new configuration, cache key, COLD on-disk format, migration, panel toggle or +feature flag was added. Cache query bypass, transformed representations, origin +validators, TTL, hot-to-cold demotion, backend selection and `sendfile` behavior +are intentionally unchanged. + +## Tests and verification + +Native Rust test file: `src/cache/tests/hot_hit_034_tests.rs`. + +- Multi-threaded HOT access checks monotonic, unique hit counters and bounded + LRU/index growth. +- Transformed response HIT retains its ETag/extension; changes to the response + header map never mutate cached headers. Memory accounting and per-domain + saved-byte counters remain exact. +- Exact LRU eviction, selective BAN, stale revalidation retention and + conditional client `max-stale` are covered. +- The manual **ignored** in-process lookup microbenchmark measures the whole + `get_hot()` response construction, not network RPS. + +```bash +./dev.sh check +CARGO_INCREMENTAL=0 ./dev.sh test +CARGO_INCREMENTAL=0 cargo build --release --locked +python3 scripts/tests/cache_hot_034_static_test.py +cargo test --release --locked --lib manual_hot_lookup_microbenchmark -- --ignored --nocapture +``` + +For real HTTP RPS use the supplied process regression and same-host nginx +benchmark (`scripts/performance/README.md`) with **identical** configs. The +standard nginx comparator benchmark explicitly disables the cache; it cannot +measure this HOT-specific improvement. Compare two compiled RustProxy 0.34.0 +binaries with the same cache-enabled, cache-warmed vhost and fixed no-query URL. +Check `X-Cache: HIT`, body SHA-256, exact `Cache-Status`, transform marker, Age, +ETag, p95/p99, CPU/request, memory and warm-up/eviction counts while measuring. + +## Remaining performance bottlenecks + +The HOT store still uses a shared mutex to provide exact LRU, per-key +generation checks and publication guarantees. Per-response `HeaderMap` cloning +is still required and now occurs outside this lock. Sharding the store or +making LRU eventually consistent would require a separate, extensively tested +cache architecture revision: doing that without compilation and live soak +would risk invalidation, accounting, BAN and tier-demotion regressions. + +**Acceptance gate open:** the changed Rust source was not compiled in this +environment, new Rust tests were not run and no HOT HIT RPS improvement has been +measured. Source-only guards, SQL and upgrade tests do not establish a safe +production build. diff --git a/docs/PROXY_RPS_0.34.0.md b/docs/PROXY_RPS_0.34.0.md new file mode 100644 index 0000000..d0044cf --- /dev/null +++ b/docs/PROXY_RPS_0.34.0.md @@ -0,0 +1,77 @@ +# RustProxy 0.34.0: hot-path throughput revision + +**Status: uncompiled source candidate.** This revision does not change the +application version, public API, dashboard controls, cache policy or tuning +defaults. It must not be labelled production-ready until the new Rust binary +passes compile, unit tests, live regressions and paired performance comparisons. + +## Code changes and intended cost reductions + +| Focus | Change | Kept behavior / risk to verify | +|---|---|---| +| Per-request identity | Draw a 256-bit nonce from the OS-seeded/reseeded thread-local CSPRNG instead of asking `getrandom` on every request. Preserve SHA-256 of nonce+monotonic sequence and the existing 64-hex ID format. | IDs remain non-predictable. Check ID uniqueness and raw upstream headers. | +| Upstream HTTP request target | Compile the validated scheme/authority `http::Uri` once with each immutable backend; parse only the request path+query and rebuild using typed `Uri::from_parts`. | Never accept a client-supplied authority, bypass DNS/address policies or change SNI. Test H1, H2, IPv6, query and percent-encoding. | +| Load balancing | Do not clone the member ID string for an existing smooth-WRR scoreboard entry. Continue the previously introduced no-mutex single-member non-cookie path. | Sticky-cookie modes, capacity limits, retry exclusions, health and drain remain unchanged. | +| Active health | Use already-compiled `Arc` when dispatching probes, avoiding a clone of its path/headers for each scheduled probe. | Same 250-ms supervisor, timeout, enabled/claim gate and health state transitions. | +| Per-frame streaming | Capture the leased backend once at first streamed request DATA (response already captured at wrapper construction). Do not take `Exchange.backend` mutex on every upload chunk. | Request/response byte counters, mid-stream failures, idle timeouts and lease lifetime remain active. | +| Pool recycling | Preserve exact request-count and elapsed-time recycling checks per request, with the existing independent 250-ms maintenance cycle. The pool factory/connection policy is unchanged. | Exact lifecycle semantics are more important than saving one cheap clock read; tune idle/age limits only after profiling. | +| Log history | Move completed `LogEntry` directly into bounded history when there are no live viewers or enabled external sinks, instead of deeply cloning its strings. Continue cloning and delivering to live viewers/sinks when enabled. | Same JSON fields, history size, drop counters, file/Loki/Logstash forwarding. | +| Route/HTTP instrumentation | Remove redundant route-ID mutex/write in forwarding and collapse H1/H2 version tallying into one branch. | Route is recorded once before forwarding; H1/H2 counters remain. | + +These changes build on the previous 0.34.0 route-Arc, single-backend LB, +conditional retry/fallback-header cloning, atomic exchange flags and cache HOT +LRU changes. There are no new UI toggles. Pool defaults remain `max_idle_per_host` +4 and `pool_recycle_requests` 25,000; tune these only in separately labelled +trials. `sendfile: auto` remains opt-in and applies only to eligible Linux +plaintext H1 static WWW responses, not reverse proxy or TLS/H2/H3. + +## Cache transform/query policy + +An enabled `bypass_query` is a deliberate policy: `/theme.css?v=...` is a +`BYPASS` even when the origin sends `Cache-Control: public`. Turning the bypass +off permits a transformed CSS object to follow `MISS -> HIT` and appear in the +cache dashboard. An existing transformed HOT row with hit count and TTL is +positive evidence of this. This throughput revision does not modify the +cache eligibility logic or discard Age/ETag/Cache-Control headers. + +## Verification already possible here + +- **Old/reproducible reference executable only:** 110 passed, 0 failed, 1 skipped + (`compression.codec.zstd` skipped because the optional decoder was missing), + from live proxy/policy/cache/transform loopback regression. This does **not** + exercise the newly modified Rust source. +- **Old/reproducible reference executable only:** query bypass enabled gives + `BYPASS -> BYPASS`, disabled gives `MISS -> HIT` and a transformed representation; + the older Age regression is also reproducible on this old executable. +- Source-only invariants: `scripts/tests/rps_hotpath_034_static_test.py` plus + existing route/cache/sendfile checks. SQL migration and upgrade scripts pass. +- **Not performed:** Rust compilation or newly added Rust unit tests, live + behavior of the optimized binary, HTTP/2 live TLS, comparative RPS, CPU + profiling and long-duration memory/health soak. The environment does not + include `cargo`, `rustc` or `wrk`. No post-change RPS gain is claimed. + +## Acceptance commands on a Rust-capable Linux host + +```bash +./dev.sh check +CARGO_INCREMENTAL=0 ./dev.sh test +CARGO_INCREMENTAL=0 cargo build --release --locked +python3 scripts/tests/rps_hotpath_034_static_test.py +python3 scripts/tests/cache_age_transform_034_live.py \ + --binary "$(realpath target/release/rust-proxy)" \ + --output /tmp/rustproxy034-cache-after +python3 scripts/performance/regression.py \ + --binary optimized="$(realpath target/release/rust-proxy)" \ + --out /tmp/rustproxy034-http-regression +``` + +Run `scripts/performance/benchmark.py` from the documented reproducible +before/after/nginx command in `scripts/performance/README.md`. Compare the +unchanged old 0.34.0 binary with the newly compiled source, not a baseline +with different feature flags. Record RPS at 1,040 B, 64 KiB, 1 MiB, p95/p99, +CPU/request, RSS, connection counts, errors and exact response-body hashes. +Use `perf record -g -p ` during a stable request interval and inspect +`perf report --stdio` to identify the next true bottleneck. + +Do not interpret Python origin self-tests, source inspections, or speculative +Amdahl models as measured RustProxy throughput. diff --git a/docs/TEST_REPORT_0.34.0.md b/docs/TEST_REPORT_0.34.0.md index 00b1b04..f136270 100644 --- a/docs/TEST_REPORT_0.34.0.md +++ b/docs/TEST_REPORT_0.34.0.md @@ -22,6 +22,26 @@ for compilation and live HTTP, health, capacity, sticky, and H2 retry tests. No RPS improvement, corrected nginx gap, or full functional parity has been measured for this new revision. Details: `docs/PROXY_HOTPATH_0.34.0.md`. +## RPS-focused follow-up source revision (not yet compiled) + +See `docs/PROXY_RPS_0.34.0.md` for detailed changes in request IDs, validated +upstream URI construction, LB scheduling, probes, streaming byte accounting, +unchanged pool generation lifetimes and log-history ownership. Source-only invariants pass. +A live test of the PREVIOUS executable (not this source revision) completed +110 passing cases, 0 failing cases and 1 skipped optional zstd decoder in the +proxy/policy/cache/transform harness. The old executable also confirms query- +string bypass is deliberate and reproduces the older Age/TTL bug. The new +binary has NOT been built or tested and its RPS gain is unknown. + +## HOT HIT follow-up source revision (not yet compiled) + +See `docs/CACHE_HOTPATH_0.34.0.md`. Response header cloning has moved outside +the global HOT lock, and redundant bounded stats key allocation, empty BAN +checks and full HOT TTL sweeps were removed. A new Rust concurrency/regression +suite and an ignored microbenchmark have been added. Existing COLD and Vary +behavior, exact LRU and cache semantics remain. This source has not been built +or run under Rust in this environment; no RPS gain is claimed. + ## Changes and correctness boundaries - Operational per-vhost saves (upstream keepalive/recycling, connection limits, diff --git a/scripts/performance/README.md b/scripts/performance/README.md index 55e98bd..44c1c3c 100644 --- a/scripts/performance/README.md +++ b/scripts/performance/README.md @@ -18,6 +18,17 @@ shutdown). Treat `--max-idle` tuning and recycles as separately labelled experiments rather than mixing them into the primary comparison. There are no new measured RPS figures in the source candidate. +## Follow-up RPS candidate on 0.34.0 + +`docs/PROXY_RPS_0.34.0.md` tracks the code changes and their regressions. +Compile before/after binaries from the two 0.34.0 source revisions and use +the paired benchmark below; a live run against the older binary cannot +establish a measured RPS improvement for the uncompiled candidate. Keep +`max_idle_per_host`, recycling, HTTP1/H2 settings and all application security +and logging features equal between runs. Treat any configuration tuning as a +separate experiment. Profile the resulting binary with `perf` only after +correctness checks pass. + ## Offline cache-retention model for 0.34.0 `python3 scripts/performance/simulate_cache_034.py` compares cache invalidation diff --git a/scripts/tests/cache_age_transform_034_live.py b/scripts/tests/cache_age_transform_034_live.py index b50ef00..10d7050 100755 --- a/scripts/tests/cache_age_transform_034_live.py +++ b/scripts/tests/cache_age_transform_034_live.py @@ -65,7 +65,7 @@ def request(port, path, encoding='identity'): 'x_cache_hits': headers.get('x-cache-hits'), 'x_cache_resident_age': headers.get('x-cache-resident-age'), 'cache_status': headers.get('cache-status'), 'age': headers.get('age'), 'body': body.decode('utf-8', 'replace'), 'content_length': headers.get('content-length'), - 'etag': headers.get('etag'), 'encoding': headers.get('content-encoding'), 'origin_count': COUNTS[path]} + 'etag': headers.get('etag'), 'encoding': headers.get('content-encoding'), 'origin_count': COUNTS[path.split('?', 1)[0]]} def dash(rust): @@ -133,6 +133,25 @@ def run(binary: Path, output: Path, expect_old_age_bug: bool): aged_br = find(snapshot, '/aged-br.css') assert aged_br['transformed'] and aged_br['remaining_ttl_seconds'] >= 86_000, aged_br + # Query-string bypass is an explicit administrator rule. Even a + # transformed CSS with public max-age stays BYPASS while it is ON, + # then the *same* URL must become MISS -> HIT after disabling it. + # This is not a transform regression: it is a request policy. + cfg['cache']['bypass_query'] = True + rust.apply(cfg, 'query-bypass-enabled') + query_path = '/normal.css?v=transformed-public' + before = COUNTS['/normal.css'] + q_bypass = [request(rust.reverse_port, query_path) for _ in range(2)] + assert [row['x_cache'] for row in q_bypass] == ['BYPASS', 'BYPASS'], q_bypass + assert COUNTS['/normal.css'] - before == 2, q_bypass + cfg['cache']['bypass_query'] = False + rust.apply(cfg, 'query-bypass-disabled') + q_cached = [request(rust.reverse_port, query_path) for _ in range(2)] + assert [row['x_cache'] for row in q_cached] == ['MISS', 'HIT'], q_cached + assert q_cached[1]['cache_status'] and 'transformed' in q_cached[1]['cache_status'] + assert COUNTS['/normal.css'] - before == 3, q_cached + outcome['tests'].append({'query_bypass_policy': {'bypass': q_bypass, 'cached': q_cached}}) + # Operational LB tuning must not invalidate a cached representation. before = COUNTS['/normal.css'] cfg['vhosts'][0]['load_balancing']['keepalive'] = {'max_idle_per_host': 16} diff --git a/scripts/tests/cache_hot_034_static_test.py b/scripts/tests/cache_hot_034_static_test.py new file mode 100755 index 0000000..74352c3 --- /dev/null +++ b/scripts/tests/cache_hot_034_static_test.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +"""Source-only hot-cache safety guards; NOT a Rust compile, concurrency test or RPS result.""" +from pathlib import Path +ROOT = Path(__file__).resolve().parents[2] +read = lambda path: (ROOT / path).read_text() +hot = read("src/cache/lookup/hot.rs") +entry = read("src/cache/model/cache.rs") +model = read("src/cache/model/maintenance.rs") +inv = read("src/cache/invalidation/invalidate.rs") +dash = read("src/cache/dashboard.rs") +assert 'headers: Arc' in entry +assert 'Arc::clone(&entry.headers)' in hot +assert 'drop(store);' in hot +assert hot.index('drop(store);\n\n let mut response') < hot.index('headers.as_ref().clone()') +assert 'store.saved_body_hit(&ticket.scope, &ticket.domain, body_bytes)' in hot +assert 'store.lru.remove(&old_sequence)' in hot and 'store.lru.insert(sequence, storage_key)' in hot +assert '.sweep()' not in hot +assert 'fn sweep(' not in entry +assert 'fn saved_body_hit(' in entry +assert 'domains: BTreeMap>' in entry +assert 'if self.ban_rule_count.load(Ordering::Acquire) == 0' in inv +assert 'bans.iter().any(|rule| rule.matches_hot' in inv +assert 'fn matching_hot_key(' in model and 'entry.vary.is_empty()' in model +assert 'store.domain_count = 0' in dash +assert 'c.reasons.get_mut(reason)' in dash +assert 'include!("tests/hot_hit_034_tests.rs")' in read('src/cache/tests.rs') +for path in (ROOT / 'src').rglob('*.rs'): + if '/tests/' not in path.as_posix() and path.name != 'tests.rs': + assert len(path.read_text().splitlines()) <= 500, path +print('PASS HOT path static invariants (no Rust compilation or throughput claim)') diff --git a/scripts/tests/cache_scope_034_static_test.py b/scripts/tests/cache_scope_034_static_test.py index 35fc424..d9bb8d1 100755 --- a/scripts/tests/cache_scope_034_static_test.py +++ b/scripts/tests/cache_scope_034_static_test.py @@ -28,7 +28,8 @@ assert "scope_revision: draft.scope_revision" in read("src/cache/cold/worker.rs" body = read("src/proxy/body.rs") assert "response_backend: Option>" in body assert "lease.bytes(count, false);" in body -assert "exchange.backend_bytes(count, true)" in body +assert "request_backend: Option>" in body +assert "lease.bytes(count, true);" in body assert "cold_scope_fingerprint_preserves_unmodified_vhost" in read("src/cache/tests/cold_tier_tests.rs") assert "stale_snapshot_cannot_issue_cache_ticket" in read("src/cache/tests/scope_tests.rs") for file in (root / 'src').rglob('*.rs'): diff --git a/scripts/tests/rps_hotpath_034_static_test.py b/scripts/tests/rps_hotpath_034_static_test.py new file mode 100644 index 0000000..88f18cb --- /dev/null +++ b/scripts/tests/rps_hotpath_034_static_test.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Source invariants for 0.34.0 RPS optimizations (NOT a Rust build or RPS test). + +These guards reject accidental removal of important branch/safety conditions. +They must be paired with ``cargo test`` and live black-box HTTP acceptance. +""" +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] + + +def source(path): + return (ROOT / path).read_text(encoding="utf-8") + + +# Random request identifiers retain CSPRNG entropy, SHA-256, format and sequence. +identifier = source("src/proxy/request_id.rs") +assert "rand::random()" in identifier +assert "hash.update(sequence.to_be_bytes());" in identifier +assert "hash.update(nonce);" in identifier +assert "request_id_hex(&hash.finalize())" in identifier +assert "SysRng.try_fill_bytes" not in identifier + +# Compiled/validated target authority survives request URI construction. +config = source("src/config/backend.rs") +model = source("src/config/model.rs") +request = source("src/proxy/types.rs") +assert "let request_origin_uri: http::Uri = request_origin.parse()?;" in config +assert "request_origin_uri," in config +assert "pub request_origin_uri: http::Uri," in model +assert "let path: http::uri::PathAndQuery = suffix.parse().ok()?;" in request +assert "let mut parts = backend.request_origin_uri.clone().into_parts();" in request +assert "parts.path_and_query = Some(path);" in request +assert "Uri::from_parts(parts).ok()" in request + +# No new unbounded caches or disabled LB safety gates. +pool = source("src/balance/pool_core.rs") +assert "if self.members.len() == 1 && !self.cookie_affinity()" in pool +assert "try_acquire_active(member.definition.max_concurrent_requests)" in pool +assert "m.state.healthy()" in pool +assert "scheduling.get_mut(member.definition.id.as_str())" in pool +assert "scheduling.entry(member.definition.id.clone())" in pool +health = source("src/balance/lease_runtime.rs") +assert "async fn probe(member: Arc, settings: Arc)" in health +assert "checks.spawn(probe(Arc::clone(member), health.clone()))" in health +assert "tokio::time::timeout" in health + +# Configurable keep-alive policy, exact recycle-request threshold and age backup. +upstream = source("src/upstream/client.rs") +assert "self.current.load_full()" in upstream +assert "request_due || age_due" in upstream +assert "let now = monotonic_millis();" in upstream +assert "let age_ms = now.saturating_sub(self.generation_started_ms.load(Ordering::Relaxed));" in upstream +assert "fn maintain_rotation_at(" in upstream +assert "self.factory.keepalive.pool_recycle_requests" in upstream +assert "self.factory.keepalive.pool_recycle_seconds" in upstream +assert "pool_max_idle_per_host(if keepalive.enabled" in upstream +assert "retry_canceled_requests(false)" in upstream +assert "observe_protocol" in upstream + +# Request/response streamed payloads retain accounting and timeout/cancellation. +body = source("src/proxy/body.rs") +assert "request_backend: Option>" in body +assert "this.request_backend = exchange.backend" in body +assert "lease.bytes(count, true)" in body +assert "lease.bytes(count, false)" in body +assert "exchange.request_bytes.fetch_add(count" in body +assert "exchange.response_bytes.fetch_add(count" in body +assert "get_or_insert_with(|| Box::pin(sleep_until(this.deadline)))" in body +assert "this.finish();" in body + +# Persist history and preserve live streaming/sink drop accounting. +logs = source("src/logging/format.rs") +assert "let has_viewers = self.live.receiver_count() != 0;" in logs +assert "let has_sink = self.settings.load().enabled();" in logs +assert "external.take().expect(\"entry\")" in logs +assert "self.live.send(entry.clone())" in logs +assert "self.sink.try_send(entry)" in logs +assert "self.sink_dropped.fetch_add(1" in logs + +# Never weaken routing, identity, HTTP version and cache eligibility while tuning. +route = source("src/proxy/forward/route_cache.rs") +assert "admin_session_passthrough" in route +assert "cache_plan.request_policy" in route +assert "exchange.set_cache_enabled" in route +assert "exchange.set_transform_enabled" in route +assert "upstream_uri(&backend, &origin_suffix)" in route +assert "headers::strip_hop_by_hop" in route +assert "pool.select_wait(context.client_ip" in route or ".select_wait(context.client_ip" in route +assert "pool.select_member_wait(id)" in route +assert "max_idle_per_host: 4" in source("src/balance/model.rs") +assert "pool_recycle_requests: 25_000" in source("src/balance/model.rs") +assert 'version = "0.34.0"' in source("Cargo.toml") + +for file in (ROOT / "src").rglob("*.rs"): + if "/tests/" in file.as_posix() or file.name == "tests.rs": + continue + assert len(file.read_text(encoding="utf-8").splitlines()) <= 500, file +print("PASS RPS hot-path source invariants (STATIC ONLY; new binary not compiled)") diff --git a/src/balance/lease_runtime.rs b/src/balance/lease_runtime.rs index 4b266ac..8193eac 100644 --- a/src/balance/lease_runtime.rs +++ b/src/balance/lease_runtime.rs @@ -296,7 +296,7 @@ impl Pool { text } } -async fn probe(member: Arc, settings: HealthCheck) { +async fn probe(member: Arc, settings: Arc) { let start = Instant::now(); let result = tokio::time::timeout(Duration::from_millis(settings.timeout_ms), async { match settings.mode { @@ -388,8 +388,12 @@ pub async fn health_loop(app: Weak, shutdown: CancellationT let snapshot = app.config.load_full(); // Own an Arc for each candidate. JoinSet tasks require an // owned, 'static member rather than a reference into snapshot. + // Health settings were already compiled into each member. + // Reuse the immutable Arc instead of cloning each HealthCheck + // and recalculating per-member overrides on every 250ms tick. let members: Vec<_> = snapshot.pools.iter().flat_map(|pool| - pool.members.iter().map(move |member| (Arc::clone(member), pool.settings.health_for(&member.definition)))).collect(); + pool.members.iter().map(|member| + (Arc::clone(member), Arc::clone(&member.health_settings)))).collect(); for (member, _) in &members { member.state.sample_traffic(); // Time-based pool recycling must not depend on receiving a diff --git a/src/balance/pool_core.rs b/src/balance/pool_core.rs index a4cf7eb..7d1bf4f 100644 --- a/src/balance/pool_core.rs +++ b/src/balance/pool_core.rs @@ -279,9 +279,13 @@ impl Pool { let weight = member .state .weight(member.definition.weight, self.settings.slow_start_seconds); - let current = scheduling - .entry(member.definition.id.clone()) - .or_insert(0.0); + let current = if let Some(current) = + scheduling.get_mut(member.definition.id.as_str()) + { + current + } else { + scheduling.entry(member.definition.id.clone()).or_insert(0.0) + }; *current += weight; total += weight; if *current > best { diff --git a/src/cache/dashboard.rs b/src/cache/dashboard.rs index 8f54c65..fe9364c 100644 --- a/src/cache/dashboard.rs +++ b/src/cache/dashboard.rs @@ -6,9 +6,6 @@ impl Cache { .unwrap_or_else(|e| e.into_inner()) .clone(); let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner()); - self.counters - .expired - .fetch_add(store.sweep() as u64, Ordering::Relaxed); let n = |value: &AtomicU64| value.load(Ordering::Relaxed); let hits = n(&self.counters.hits); let misses = n(&self.counters.misses); @@ -180,6 +177,7 @@ impl Cache { store.revision = revision; self.revision.store(revision, Ordering::Relaxed); store.domains.clear(); + store.domain_count = 0; } drop(store); self.cancel_all_fill_flights(); @@ -247,9 +245,6 @@ impl Cache { } pub fn scope_view(&self, scope: &str) -> ScopeView { let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner()); - self.counters - .expired - .fetch_add(store.sweep() as u64, Ordering::Relaxed); let mut view = ScopeView { entries: 0, resident_bytes: 0, @@ -276,26 +271,32 @@ impl Cache { "MISS" => c.misses += 1, _ => c.bypasses += 1, } - *c.reasons.entry(reason.into()).or_default() += 1; + if let Some(count) = c.reasons.get_mut(reason) { + *count = count.saturating_add(1); + } else { + c.reasons.insert(reason.to_owned(), 1); + } } } pub fn record_reason(&self, scope: &str, domain: &str, reason: &'static str) { let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner()); if let Some(c) = store.domain(scope, domain) { - *c.reasons.entry(reason.into()).or_default() += 1; + if let Some(count) = c.reasons.get_mut(reason) { + *count = count.saturating_add(1); + } else { + c.reasons.insert(reason.to_owned(), 1); + } } } pub fn dashboard(&self, scope: Option<&str>, offset: usize, limit: usize) -> Dashboard { let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner()); - self.counters - .expired - .fetch_add(store.sweep() as u64, Ordering::Relaxed); let revision = store.revision; let mut groups: BTreeMap<(String, String), DomainView> = store .domains .iter() - .filter(|((vhost, _), _)| scope.is_none_or(|s| s == vhost)) - .map(|((vhost, domain), counters)| { + .filter(|(vhost, _)| scope.is_none_or(|s| s == vhost.as_str())) + .flat_map(|(vhost, domains)| domains.iter().map(move |(domain, counters)| (vhost, domain, counters))) + .map(|(vhost, domain, counters)| { ( (vhost.clone(), domain.clone()), DomainView { diff --git a/src/cache/fill.rs b/src/cache/fill.rs index 702dde5..da3a884 100644 --- a/src/cache/fill.rs +++ b/src/cache/fill.rs @@ -160,7 +160,7 @@ impl Candidate { created_at, body, body_allocated: body_allocation, - headers: self.headers, + headers: Arc::new(self.headers), received: self.plan.received, last_access: Instant::now(), resident_since: Instant::now(), diff --git a/src/cache/invalidation/invalidate.rs b/src/cache/invalidation/invalidate.rs index 756bd9b..ed32449 100644 --- a/src/cache/invalidation/invalidate.rs +++ b/src/cache/invalidation/invalidate.rs @@ -1,17 +1,24 @@ impl Cache { fn active_bans(&self) -> Vec { + if self.ban_rule_count.load(Ordering::Acquire) == 0 { + return Vec::new(); + } let now = SystemTime::now(); let mut bans = self.bans.lock().unwrap_or_else(|e| e.into_inner()); bans.retain(|rule| !rule.expired(now)); + self.ban_rule_count.store(bans.len() as u64, Ordering::Release); bans.clone() } fn hot_entry_banned(&self, key: &Key, entry: &Entry) -> bool { + if self.ban_rule_count.load(Ordering::Acquire) == 0 { + return false; + } let now = SystemTime::now(); - let banned = self - .active_bans() - .iter() - .any(|rule| rule.matches_hot(key, entry, now)); + let mut bans = self.bans.lock().unwrap_or_else(|e| e.into_inner()); + bans.retain(|rule| !rule.expired(now)); + self.ban_rule_count.store(bans.len() as u64, Ordering::Release); + let banned = bans.iter().any(|rule| rule.matches_hot(key, entry, now)); if banned { self.counters.ban_matches.fetch_add(1, Ordering::Relaxed); } @@ -19,11 +26,14 @@ impl Cache { } fn cold_entry_banned(&self, key: &Key, entry: &ColdEntry) -> bool { + if self.ban_rule_count.load(Ordering::Acquire) == 0 { + return false; + } let now = SystemTime::now(); - let banned = self - .active_bans() - .iter() - .any(|rule| rule.matches_cold(key, entry, now)); + let mut bans = self.bans.lock().unwrap_or_else(|e| e.into_inner()); + bans.retain(|rule| !rule.expired(now)); + self.ban_rule_count.store(bans.len() as u64, Ordering::Release); + let banned = bans.iter().any(|rule| rule.matches_cold(key, entry, now)); if banned { self.counters.ban_matches.fetch_add(1, Ordering::Relaxed); } @@ -31,10 +41,14 @@ impl Cache { } fn entry_banned_before_insert(&self, key: &Key, entry: &Entry) -> bool { + if self.ban_rule_count.load(Ordering::Acquire) == 0 { + return false; + } let now = SystemTime::now(); - self.active_bans() - .iter() - .any(|rule| rule.matches_hot(key, entry, now)) + let mut bans = self.bans.lock().unwrap_or_else(|e| e.into_inner()); + bans.retain(|rule| !rule.expired(now)); + self.ban_rule_count.store(bans.len() as u64, Ordering::Release); + bans.iter().any(|rule| rule.matches_hot(key, entry, now)) } pub fn bans(&self) -> Vec { @@ -46,6 +60,7 @@ impl Cache { let before = bans.len(); bans.retain(|rule| rule.id != id); let removed = bans.len() != before; + self.ban_rule_count.store(bans.len() as u64, Ordering::Release); if removed { self.counters.bans_removed.fetch_add(1, Ordering::Relaxed); } @@ -82,6 +97,7 @@ impl Cache { created_at: now, expires_at: now.checked_add(Duration::from_secs(ttl)).unwrap_or(now), }); + self.ban_rule_count.store(bans.len() as u64, Ordering::Release); self.counters.bans_created.fetch_add(1, Ordering::Relaxed); Ok(InvalidationResult { operation: request.operation, diff --git a/src/cache/lifecycle.rs b/src/cache/lifecycle.rs index 4cdd942..adf8536 100644 --- a/src/cache/lifecycle.rs +++ b/src/cache/lifecycle.rs @@ -25,6 +25,7 @@ impl Cache { fill_flight_sequence: AtomicU64::new(0), collapse_waiters: AtomicU64::new(0), bans: Mutex::new(Vec::new()), + ban_rule_count: AtomicU64::new(0), ban_sequence: AtomicU64::new(0), proactive_scheduled_keys: Mutex::new(HashSet::new()), proactive_inflight: AtomicU64::new(0), @@ -75,6 +76,7 @@ impl Cache { fill_flight_sequence: AtomicU64::new(0), collapse_waiters: AtomicU64::new(0), bans: Mutex::new(Vec::new()), + ban_rule_count: AtomicU64::new(0), ban_sequence: AtomicU64::new(0), proactive_scheduled_keys: Mutex::new(HashSet::new()), proactive_inflight: AtomicU64::new(0), diff --git a/src/cache/lookup/hot.rs b/src/cache/lookup/hot.rs index ddcf66f..75cb017 100644 --- a/src/cache/lookup/hot.rs +++ b/src/cache/lookup/hot.rs @@ -1,29 +1,20 @@ impl Cache { fn get_hot(&self, ticket: &Ticket, count_miss: bool) -> Option { + // Only metadata, hit counters and exact LRU ordering require the store + // mutex. Materializing an HTTP response may allocate (headers, Body, + // Age formatting) and must be done *after* dropping that mutex. let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner()); - let expired = store.sweep(); - self.counters - .expired - .fetch_add(expired as u64, Ordering::Relaxed); if store.revision != ticket.revision || store.generation != ticket.generation - || store - .scope_generations - .get(&ticket.scope) - .copied() - .unwrap_or(0) + || store.scope_generations.get(&ticket.scope).copied().unwrap_or(0) != ticket.scope_generation - || store.key_generations.get(&ticket.key).copied().unwrap_or(0) != ticket.key_generation + || store.key_generations.get(&ticket.key).copied().unwrap_or(0) + != ticket.key_generation { return None; } if let Some(storage_key) = matching_hot_key(&store, ticket) { - // Don't remove and reinsert the entire Entry + variant index on - // every HOT hit. A cache hit changes only the LRU sequence, hits - // counter, and last-access clock; body/headers remain immutable. - let banned = store - .entries - .get(&storage_key) + let banned = store.entries.get(&storage_key) .is_some_and(|entry| self.hot_entry_banned(&storage_key, entry)); if banned { store.remove(&storage_key); @@ -34,14 +25,9 @@ impl Cache { release_allocator_memory(); return None; } - let Some(entry) = store.entries.get(&storage_key) else { - return None; - }; + let entry = store.entries.get(&storage_key)?; let resident_age_seconds = hot_resident_age(entry).as_secs(); - let age = entry - .initial_age - .saturating_add(entry.received.elapsed()) - .as_secs(); + let age = entry.initial_age.saturating_add(entry.received.elapsed()).as_secs(); let state = cache_state( age, entry.ttl, @@ -53,48 +39,54 @@ impl Cache { ); if state.servable() { let sequence = store.next_sequence(); - let (old_sequence, scope, domain, body_bytes, result) = { + let (old_sequence, headers, body, hits, transformed, skip_compression, + remaining_ttl, body_bytes) = { let entry = store.entries.get_mut(&storage_key)?; let old_sequence = entry.sequence; entry.hits = entry.hits.saturating_add(1); entry.last_access = Instant::now(); entry.sequence = sequence; - let mut response = Response::new(Body::from(entry.body.clone())); - *response.headers_mut() = entry.headers.clone(); - if entry.transformed { - response - .extensions_mut() - .insert(crate::transform::CachedTransformed); - } - if entry.skip_compression { - response.extensions_mut().insert(crate::compression::Skip); - } - response.headers_mut().insert( - "age", - HeaderValue::from_str(&age.to_string()).expect("integer header"), - ); - let result = Hit { - response, - body: entry.body.clone(), - hits: entry.hits, - transformed: entry.transformed, - remaining_ttl: entry.ttl.saturating_sub(age), - resident_age_seconds, - stale: state.stale(), - background_revalidate: state.background_revalidate(), - }; - (old_sequence, entry.scope.clone(), entry.domain.clone(), entry.body.len(), result) + ( + old_sequence, + Arc::clone(&entry.headers), + entry.body.clone(), + entry.hits, + entry.transformed, + entry.skip_compression, + entry.ttl.saturating_sub(age), + entry.body.len(), + ) }; store.lru.remove(&old_sequence); store.lru.insert(sequence, storage_key); - // `store.bytes`, `store.logical_bytes` and the Vary schema index - // did not change. In particular, this path does not allocate a - // replacement variants Vec for every response. - if let Some(counter) = store.domain(&scope, &domain) { - counter.saved_body_bytes = counter.saved_body_bytes.saturating_add(body_bytes as u64); - } + // Preserve the exact LRU and domain-byte counters, without + // allocating a replacement variant index or String keys. + store.saved_body_hit(&ticket.scope, &ticket.domain, body_bytes); self.counters.hits.fetch_add(1, Ordering::Relaxed); - return Some(result); + drop(store); + + let mut response = Response::new(Body::from(body.clone())); + *response.headers_mut() = headers.as_ref().clone(); + if transformed { + response.extensions_mut().insert(crate::transform::CachedTransformed); + } + if skip_compression { + response.extensions_mut().insert(crate::compression::Skip); + } + response.headers_mut().insert( + "age", + HeaderValue::from_str(&age.to_string()).expect("integer header"), + ); + return Some(Hit { + response, + body, + hits, + transformed, + remaining_ttl, + resident_age_seconds, + stale: state.stale(), + background_revalidate: state.background_revalidate(), + }); } } if count_miss { @@ -243,7 +235,7 @@ impl Cache { target: entry.target.clone(), tags: entry.tags.clone(), created_at: entry.created_at, - headers: metadata.headers.clone(), + headers: Arc::new(metadata.headers.clone()), body: body.clone(), body_allocated, received: Instant::now(), diff --git a/src/cache/lookup/insert.rs b/src/cache/lookup/insert.rs index 08c124b..8664f34 100644 --- a/src/cache/lookup/insert.rs +++ b/src/cache/lookup/insert.rs @@ -51,9 +51,6 @@ impl Cache { store.remove(&key); } store.remove(&storage_key); - self.counters - .expired - .fetch_add(store.sweep() as u64, Ordering::Relaxed); let revision = store.revision; let generation = store.generation; let mut released_hot = false; diff --git a/src/cache/lookup/tiered.rs b/src/cache/lookup/tiered.rs index fc11760..969ca81 100644 --- a/src/cache/lookup/tiered.rs +++ b/src/cache/lookup/tiered.rs @@ -77,7 +77,7 @@ impl Cache { target: ticket.target.clone(), tags, created_at: ticket.created_at, - headers: headers.clone(), + headers: Arc::new(headers.clone()), body: cached.body.clone(), body_allocated: cached.body_allocated, received: plan.received, diff --git a/src/cache/model/cache.rs b/src/cache/model/cache.rs index bf527ea..b6b96bb 100644 --- a/src/cache/model/cache.rs +++ b/src/cache/model/cache.rs @@ -111,7 +111,7 @@ struct Entry { target: String, tags: Vec, created_at: SystemTime, - headers: HeaderMap, + headers: Arc, body: Bytes, /// Backing allocation retained by `body`; can exceed `body.len()`. body_allocated: usize, @@ -181,6 +181,9 @@ fn hot_entry_cost( .sum::(); body_allocated .saturating_add(std::mem::size_of::()) + // Arc owns the actual map on a separate allocation. + .saturating_add(std::mem::size_of::()) + .saturating_add(2 * std::mem::size_of::()) .saturating_add(index_heap) .saturating_add(header_heap) .saturating_add(string_vec_heap(vary)) @@ -333,8 +336,8 @@ struct Store { control_revision: u64, scope_generations: HashMap, key_generations: HashMap, - domains: BTreeMap<(String, String), DomainCounters>, - last_sweep: Option, + domains: BTreeMap>, + domain_count: usize, } impl Store { fn index_variant(&mut self, base_key: Key, key: Key) { @@ -360,39 +363,24 @@ impl Store { Some(entry) } fn domain(&mut self, scope: &str, domain: &str) -> Option<&mut DomainCounters> { - let key = (scope.to_owned(), domain.to_owned()); - // Cardinality is bounded independently of resident cache capacity. - if self.domains.len() >= 4096 && !self.domains.contains_key(&key) { + // The common path is a borrowed lookup: no allocation of scope/domain + // strings for every cache hit or log-reason counter update. + if self.domains.get(scope).is_some_and(|d| d.contains_key(domain)) { + return self.domains.get_mut(scope)?.get_mut(domain); + } + if self.domain_count >= 4096 { return None; } - Some(self.domains.entry(key).or_default()) + self.domain_count += 1; + Some(self.domains.entry(scope.to_owned()).or_default() + .entry(domain.to_owned()).or_default()) } - fn sweep(&mut self) -> usize { - if self - .last_sweep - .is_some_and(|v| v.elapsed() < Duration::from_secs(1)) - { - return 0; + fn saved_body_hit(&mut self, scope: &str, domain: &str, bytes: usize) { + // Every admitted HOT insertion registered its domain. If the bounded + // stats table is full, skip accounting for an unregistered domain. + if let Some(c) = self.domains.get_mut(scope).and_then(|d| d.get_mut(domain)) { + c.saved_body_bytes = c.saved_body_bytes.saturating_add(bytes as u64); } - self.last_sweep = Some(Instant::now()); - let stale: Vec<_> = self - .entries - .iter() - .filter(|(_, e)| { - let age = e.initial_age.saturating_add(e.received.elapsed()).as_secs(); - age >= retention_deadline( - e.ttl, - e.stale_while_revalidate, - e.stale_if_error, - e.origin_etag.is_some() || e.origin_last_modified.is_some(), - ) - }) - .map(|(k, _)| *k) - .collect(); - for key in &stale { - self.remove(key); - } - stale.len() } fn next_sequence(&mut self) -> u64 { // No practical wrapping, but a wrap must not corrupt LRU bookkeeping. diff --git a/src/cache/model/maintenance.rs b/src/cache/model/maintenance.rs index 9628bb0..d531161 100644 --- a/src/cache/model/maintenance.rs +++ b/src/cache/model/maintenance.rs @@ -73,6 +73,7 @@ pub struct Cache { fill_flight_sequence: AtomicU64, collapse_waiters: AtomicU64, bans: Mutex>, + ban_rule_count: AtomicU64, ban_sequence: AtomicU64, proactive_scheduled_keys: Mutex>, proactive_inflight: AtomicU64, @@ -347,6 +348,9 @@ fn hot_resident_age(entry: &Entry) -> Duration { } fn matching_hot_key(store: &Store, ticket: &Ticket) -> Option { + if store.entries.get(&ticket.key).is_some_and(|entry| entry.vary.is_empty()) { + return Some(ticket.key); + } store .variants .get(&ticket.key)? diff --git a/src/cache/tests.rs b/src/cache/tests.rs index 38337dd..8c66460 100644 --- a/src/cache/tests.rs +++ b/src/cache/tests.rs @@ -19,3 +19,6 @@ include!("tests/cold_tier_tests.rs"); #[cfg(test)] include!("tests/http_invalidation_tests.rs"); + +#[cfg(test)] +include!("tests/hot_hit_034_tests.rs"); diff --git a/src/cache/tests/dashboard_tests.rs b/src/cache/tests/dashboard_tests.rs index b81bc1d..5fdbf3f 100644 --- a/src/cache/tests/dashboard_tests.rs +++ b/src/cache/tests/dashboard_tests.rs @@ -84,7 +84,6 @@ mod dashboard_tests { for entry in store.entries.values_mut() { entry.received = Instant::now() - Duration::from_secs(2 * 60 * 60); } - store.last_sweep = None; } let dashboard = cache.dashboard(None, 0, 50); assert_eq!(dashboard.matching_objects, 1); diff --git a/src/cache/tests/hot_hit_034_tests.rs b/src/cache/tests/hot_hit_034_tests.rs new file mode 100644 index 0000000..f6d1f41 --- /dev/null +++ b/src/cache/tests/hot_hit_034_tests.rs @@ -0,0 +1,172 @@ +mod hot_hit_034_tests { + use super::*; + use http_body_util::BodyExt; + + fn settings() -> Settings { + Settings { enabled: true, ..Settings::default() } + } + + async fn publish( + cache: &Arc, + settings: &Settings, + path: &str, + body: &'static str, + transformed: bool, + ) -> Ticket { + let ticket = cache.ticket_scoped( + 34, "app", "http", "cache.example", "origin@127.0.0.1:8080", + path, &HeaderMap::new(), + ).expect("cache ticket"); + let mut response = Response::new(Body::from(body)); + response.headers_mut().insert( + "cache-control", HeaderValue::from_static("public, max-age=31536000"), + ); + response.headers_mut().insert( + "etag", HeaderValue::from_static("\"cached-representation\""), + ); + if transformed { + response.extensions_mut().insert(crate::transform::Applied { + kind: crate::transform::Kind::Css, + original_bytes: body.len(), + transformed_bytes: body.len(), + }); + } + let policy = plan(StatusCode::OK, response.headers(), settings, Duration::ZERO) + .expect("public static object cacheable"); + cache.wrap(response, ticket.clone(), policy, settings) + .into_body().collect().await.unwrap(); + ticket + } + + #[tokio::test] + async fn hot_hit_headers_are_independent_and_lru_counters_remain_exact() { + let cache = Cache::new(34); + let settings = settings(); + let ticket = publish(&cache, &settings, "/theme.css", "body{color:red}", true).await; + let initial = cache.view(); + let before = cache.dashboard(Some("app"), 0, 10); + assert_eq!(before.matching_objects, 1); + assert!(before.objects[0].transformed); + + for i in 1..=500 { + let hit = cache.get(&ticket).expect("HOT cache hit"); + assert_eq!(hit.hits, i); + assert!(hit.transformed); + assert!(hit.response.extensions().get::().is_some()); + assert_eq!(hit.response.headers()["etag"], "\"cached-representation\""); + let mut independent = hit.response; + independent.headers_mut().insert("etag", HeaderValue::from_static("\"client-only\"")); + if i == 500 { + assert_eq!(independent.into_body().collect().await.unwrap().to_bytes(), Bytes::from_static(b"body{color:red}")); + } + } + let after = cache.view(); + assert_eq!(after.entries, 1); + assert_eq!(after.hot_allocated_bytes, initial.hot_allocated_bytes); + assert_eq!(after.hot_logical_bytes, initial.hot_logical_bytes); + assert_eq!(after.hits.saturating_sub(initial.hits), 500); + let store = cache.store.lock().unwrap(); + assert_eq!(store.entries.len(), 1); + assert_eq!(store.lru.len(), 1); + assert_eq!(store.variants.len(), 1); + drop(store); + let dashboard = cache.dashboard(Some("app"), 0, 10); + assert_eq!(dashboard.objects[0].hits, 500); + assert_eq!(dashboard.domains[0].counters.saved_body_bytes, 500 * 15); + assert_eq!(cache.get(&ticket).unwrap().response.headers()["etag"], "\"cached-representation\""); + } + + #[tokio::test] + async fn concurrent_hot_hits_have_unique_monotonic_counts_and_bounded_indexes() { + let cache = Cache::new(34); + let ticket = publish(&cache, &settings(), "/shared.css", "cachebody", false).await; + let workers = 6; + let per_worker = 300; + let mut threads = Vec::new(); + for _ in 0..workers { + let cache = Arc::clone(&cache); + let ticket = ticket.clone(); + threads.push(std::thread::spawn(move || { + let mut hits = Vec::with_capacity(per_worker); + for _ in 0..per_worker { + let hit = cache.get(&ticket).expect("concurrent HOT hit"); + assert_eq!(hit.response.headers()["etag"], "\"cached-representation\""); + hits.push(hit.hits); + } + hits + })); + } + let mut counts: Vec<_> = threads.into_iter().flat_map(|t| t.join().unwrap()).collect(); + counts.sort_unstable(); + assert_eq!(counts, (1..=(workers * per_worker) as u64).collect::>()); + assert_eq!(cache.view().hits, (workers * per_worker) as u64); + let store = cache.store.lock().unwrap(); + assert_eq!(store.lru.len(), 1); + assert_eq!(store.entries.len(), 1); + } + + #[tokio::test] + async fn hot_lru_eviction_and_ban_are_preserved() { + let cache = Cache::new(34); + let settings = Settings { max_entries: 2, ..settings() }; + let a = publish(&cache, &settings, "/a.css", "AAAA", false).await; + let b = publish(&cache, &settings, "/b.css", "BBBB", false).await; + assert!(cache.get(&a).is_some(), "A becomes most recently used"); + let c = publish(&cache, &settings, "/c.css", "CCCC", false).await; + assert!(cache.get(&a).is_some()); + assert!(cache.get(&b).is_none(), "B is least recently used"); + assert!(cache.get(&c).is_some()); + + let ban = cache.invalidate(InvalidationRequest { + operation: InvalidationOperation::Ban, + selector: InvalidationSelector::Exact, + value: "/a.css".into(), + host: Some("cache.example".into()), + vhost: Some("app".into()), + ttl_seconds: Some(3600), + confirm_wide: false, + }).unwrap(); + assert_eq!(cache.ban_rule_count.load(Ordering::Acquire), 1); + assert!(cache.get(&a).is_none(), "BAN must block cached A"); + assert!(cache.remove_ban(ban.ban_id.unwrap())); + assert_eq!(cache.ban_rule_count.load(Ordering::Acquire), 0); + assert!(cache.get(&c).is_some(), "unrelated C must remain cached"); + } + + #[tokio::test] + #[ignore = "manual in-process timing only; NOT HTTP RPS"] + async fn manual_hot_lookup_microbenchmark() { + let cache = Cache::new(34); + let ticket = publish(&cache, &settings(), "/bench.css", "smallbody", true).await; + for _ in 0..1_000 { + std::hint::black_box(cache.get(&ticket).unwrap()); + } + let iterations = 100_000; + let start = Instant::now(); + for _ in 0..iterations { + std::hint::black_box(cache.get(&ticket).unwrap()); + } + let duration = start.elapsed(); + eprintln!("HOT lookup incl. response materialization: {:.1} ns/op ({} hits); NOT network RPS", + duration.as_secs_f64() * 1e9 / iterations as f64, iterations); + assert_eq!(cache.view().hits, iterations + 1_000); + } + + #[tokio::test] + async fn stale_object_remains_available_for_explicit_max_stale() { + let cache = Cache::new(34); + let ticket = publish(&cache, &settings(), "/old.css", "oldbody", false).await; + { + let mut store = cache.store.lock().unwrap(); + store.entries.values_mut().next().unwrap().received = + Instant::now() - Duration::from_secs(700_000); + } + assert!(cache.get(&ticket).is_none(), "expired freshness is not an unconditional HIT"); + assert_eq!(cache.view().entries, 1, "stale representation must survive for revalidation"); + let mut stale_ticket = ticket; + stale_ticket.request_headers.insert( + "cache-control", HeaderValue::from_static("max-stale"), + ); + assert!(cache.get(&stale_ticket).unwrap().stale); + } +} diff --git a/src/cache/tickets.rs b/src/cache/tickets.rs index 455fb0a..a6c8e62 100644 --- a/src/cache/tickets.rs +++ b/src/cache/tickets.rs @@ -109,10 +109,6 @@ impl Cache { } pub fn revalidation(&self, ticket: &Ticket) -> Option { let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner()); - let expired = store.sweep(); - self.counters - .expired - .fetch_add(expired as u64, Ordering::Relaxed); if store.revision != ticket.revision || store.generation != ticket.generation || store @@ -142,7 +138,7 @@ impl Cache { .saturating_add(entry.received.elapsed()) .as_secs(); Some(Revalidation { - headers: entry.headers.clone(), + headers: entry.headers.as_ref().clone(), body: entry.body.clone(), body_allocated: entry.body_allocated, _transient_leases: Vec::new(), diff --git a/src/config/backend.rs b/src/config/backend.rs index c572bde..364f963 100644 --- a/src/config/backend.rs +++ b/src/config/backend.rs @@ -94,11 +94,14 @@ pub(crate) fn compile_backend_with_timeout( connect_timeout, )? }; + let request_origin = format!("{}://{request_authority}", url.scheme()); + let request_origin_uri: http::Uri = request_origin.parse()?; Ok(CompiledBackend { address_policy, id: backend.id.clone(), origin: format!("{}://{authority}", url.scheme()), - request_origin: format!("{}://{request_authority}", url.scheme()), + request_origin, + request_origin_uri, host_header: authority.parse()?, preserve_host: backend.preserve_host, protocol: backend.protocol, diff --git a/src/config/model.rs b/src/config/model.rs index 15aeba4..e27d095 100644 --- a/src/config/model.rs +++ b/src/config/model.rs @@ -174,6 +174,8 @@ pub struct CompiledBackend { pub origin: String, /// HTTP request origin. For HTTPS this may carry a separate verified TLS name. pub request_origin: String, + /// Parsed once at configuration publish; reused for every upstream request. + pub request_origin_uri: http::Uri, pub host_header: HeaderValue, pub preserve_host: bool, pub protocol: UpstreamProtocol, diff --git a/src/logging/format.rs b/src/logging/format.rs index 02c2f2e..b16fc13 100644 --- a/src/logging/format.rs +++ b/src/logging/format.rs @@ -136,8 +136,17 @@ impl Logs { self.settings.load().capture_country } pub fn push(&self, entry: LogEntry) { - // String cloning and eviction cleanup do not need the history mutex. - let history_entry = entry.clone(); + let has_viewers = self.live.receiver_count() != 0; + let has_sink = self.settings.load().enabled(); + // The usual history-only configuration needs no deep copy at all. + // When external consumers are present, clone exactly for history and + // keep the original entry available for streaming/sink delivery. + let mut external = Some(entry); + let history_entry = if has_viewers || has_sink { + external.as_ref().expect("entry").clone() + } else { + external.take().expect("entry") + }; let evicted = if let Ok(mut entries) = self.entries.try_lock() { let evicted = if entries.len() == HISTORY_CAPACITY { entries.pop_front() @@ -150,13 +159,18 @@ impl Logs { self.dropped.fetch_add(1, Ordering::Relaxed); None }; + // Destroy evicted strings outside of the history lock. drop(evicted); - // No live viewers means no reason to allocate/clone a second LogEntry. - // History retention and external sinks remain unchanged. - if self.live.receiver_count() != 0 { - let _ = self.live.send(entry.clone()); + if let Some(entry) = external { + if has_viewers { + let _ = self.live.send(entry.clone()); + } + // Snapshot the sink choice once. This preserves sink drop + // accounting and avoids a second ArcSwap load per request. + if has_sink && self.sink.try_send(entry).is_err() { + self.sink_dropped.fetch_add(1, Ordering::Relaxed); + } } - self.push_sink(entry); } fn push_sink(&self, entry: LogEntry) { if self.settings.load().enabled() && self.sink.try_send(entry).is_err() { diff --git a/src/logging/tests.rs b/src/logging/tests.rs index ebdf85a..cc80609 100644 --- a/src/logging/tests.rs +++ b/src/logging/tests.rs @@ -422,4 +422,40 @@ mod tests { assert_eq!(stats.sent, 1); assert_eq!(stats.errors, 0); } + #[test] + fn history_only_mode_preserves_exact_fields_without_any_live_or_sink_copies() { + let logs = Logs::default(); + let item = entry(); + let expected = serde_json::to_value(&item).unwrap(); + logs.push(item); + let saved = logs.recent(); + assert_eq!(saved.len(), 1); + assert_eq!(serde_json::to_value(&saved[0]).unwrap(), expected); + assert_eq!(logs.stats().queue_dropped, 0); + } + + #[test] + fn live_viewers_receive_same_event_as_persisted_history() { + let logs = Logs::default(); + let mut live = logs.subscribe(); + let item = entry(); + let expected = serde_json::to_value(&item).unwrap(); + logs.push(item); + assert_eq!(serde_json::to_value(live.try_recv().unwrap()).unwrap(), expected); + assert_eq!(serde_json::to_value(&logs.recent()[0]).unwrap(), expected); + } + + #[test] + fn enabled_external_sink_retains_existing_queue_drop_accounting() { + // The default test Logs has a closed sink receiver. Delivering to a + // configured external sink must still account for its failure. + let logs = Logs::default(); + let mut settings = Settings::default(); + settings.file.enabled = true; + logs.configure(settings); + logs.push(entry()); + assert_eq!(logs.recent().len(), 1); + assert_eq!(logs.stats().queue_dropped, 1); + } + } diff --git a/src/proxy/body.rs b/src/proxy/body.rs index a8e59cb..0596d57 100644 --- a/src/proxy/body.rs +++ b/src/proxy/body.rs @@ -301,6 +301,7 @@ pub struct MeteredBody { /// For a response, backend selection is final before this wrapper is made. /// Keep the selected lease alive and avoid locking Exchange.backend per frame. response_backend: Option>, + request_backend: Option>, direction: Direction, timer: Option>>, deadline: tokio::time::Instant, @@ -317,6 +318,7 @@ impl MeteredBody { inner, exchange: Some(exchange), response_backend, + request_backend: None, direction, // Ready/empty bodies never need a timer allocation. Keep the // original deadline so a delayed first poll still times out. @@ -330,6 +332,7 @@ impl MeteredBody { self.finished = true; self.timer.take(); self.response_backend.take(); + self.request_backend.take(); self.exchange.take(); } } @@ -350,7 +353,17 @@ impl HttpBody for MeteredBody { if let (Some(data), Some(exchange)) = (frame.data_ref(), this.exchange.as_ref()) { let count = data.len() as u64; match this.direction { - Direction::Request => exchange.backend_bytes(count, true), + Direction::Request => { + // Backend is selected before a non-empty upstream + // upload is polled. No retry of a streamed write. + if this.request_backend.is_none() { + this.request_backend = exchange.backend + .lock().unwrap_or_else(|e| e.into_inner()).clone(); + } + if let Some(lease) = &this.request_backend { + lease.bytes(count, true); + } + } Direction::Response => { if let Some(lease) = &this.response_backend { lease.bytes(count, false); diff --git a/src/proxy/forward/route_cache.rs b/src/proxy/forward/route_cache.rs index 3c19453..a980dd7 100644 --- a/src/proxy/forward/route_cache.rs +++ b/src/proxy/forward/route_cache.rs @@ -125,7 +125,9 @@ let cache_settings = route let transform_settings = route.frontend.as_ref().map(|f| &f.transform); let scope = route.frontend.as_ref().map(|f| f.id.as_str()).unwrap_or(""); if let Some(exchange) = request.extensions().get::>() { - *exchange.route.lock().unwrap_or_else(|e| e.into_inner()) = Some(route.id.clone()); + // The resolved route was already recorded before local response, LB and + // ACL decisions. Do not acquire the route mutex and copy its ID twice on + // every reverse-proxy request. exchange.set_cache_enabled(cache_settings.active_for(route.cache)); exchange.set_transform_enabled(transform_settings.is_some_and(|settings| settings.enabled)); } diff --git a/src/proxy/request_id.rs b/src/proxy/request_id.rs index f245d45..2d8f66d 100644 --- a/src/proxy/request_id.rs +++ b/src/proxy/request_id.rs @@ -1,11 +1,10 @@ fn request_id_hash(sequence: u64) -> String { - use rand::TryRng; use sha2::{Digest, Sha256}; - let mut nonce = [0u8; 32]; - let mut rng = rand::rngs::SysRng; - rng.try_fill_bytes(&mut nonce) - .expect("secure operating system random source unavailable"); + // Thread-local CSPRNG is seeded (and periodically reseeded) from the OS. + // Do not make a getrandom syscall on the hot path for every HTTP request. + // Keep hashing the per-process sequence as before for the same ID format. + let nonce: [u8; 32] = rand::random(); let mut hash = Sha256::new(); hash.update(sequence.to_be_bytes()); hash.update(nonce); diff --git a/src/proxy/request_id/tests.rs b/src/proxy/request_id/tests.rs index 62a66ce..701d2f5 100644 --- a/src/proxy/request_id/tests.rs +++ b/src/proxy/request_id/tests.rs @@ -25,7 +25,7 @@ mod request_id_tests { .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))); assert!( seen.insert(id), - "fixed sequence must still use independent OS entropy" + "fixed sequence must still use independent CSPRNG output" ); } } diff --git a/src/proxy/tests/authority_tests.rs b/src/proxy/tests/authority_tests.rs index 22a266c..d49c122 100644 --- a/src/proxy/tests/authority_tests.rs +++ b/src/proxy/tests/authority_tests.rs @@ -595,4 +595,23 @@ mod graceful_h2_tests { assert!(!is_graceful_h2_goaway(&protocol)); assert!(!is_graceful_h2_goaway(&local)); } + #[test] + fn compiled_request_origin_matches_previous_uri_parser() { + let mut config = crate::config::demo_config("127.0.0.1:9000".parse().unwrap()); + config.vhosts[0].backends[0].url = "https://127.0.0.1:9000".into(); + config.vhosts[0].backends[0].tls_server_name = Some("backend.example".into()); + let snapshot = crate::config::compile(config, 1, &validation_policy()).unwrap(); + let backend = &snapshot.pools[0].members[0].backend; + assert_eq!(backend.request_origin_uri.to_string(), backend.request_origin); + for suffix in [ + "/", "/static/main.css", "/path?x=1&y=2", "/p%2Fq?url=https%3A//example.org/a", + "/?q=%C5%82&empty=", "/double//slash", "/item#frag", + ] { + let previous = format!("{}{suffix}", backend.request_origin).parse::().ok(); + assert_eq!(upstream_uri(backend, suffix), previous, "{suffix}"); + } + assert!(upstream_uri(backend, "http://attacker.invalid/").is_none()); + assert_eq!(backend.origin, "https://127.0.0.1:9000"); + } + } diff --git a/src/proxy/types.rs b/src/proxy/types.rs index 7581cfb..f2896d1 100644 --- a/src/proxy/types.rs +++ b/src/proxy/types.rs @@ -152,7 +152,13 @@ fn retry_safe_method(method: &Method) -> bool { } fn upstream_uri(backend: &crate::config::CompiledBackend, suffix: &str) -> Option { - format!("{}{suffix}", backend.request_origin).parse().ok() + // Reuse the validated scheme and authority from the immutable compiled + // backend. Only the untrusted path/query is parsed per request. This also + // prevents accidental authority changes from rewritten paths. + let path: http::uri::PathAndQuery = suffix.parse().ok()?; + let mut parts = backend.request_origin_uri.clone().into_parts(); + parts.path_and_query = Some(path); + Uri::from_parts(parts).ok() } fn legacy_websocket_origin( diff --git a/src/proxy/upstream_helpers.rs b/src/proxy/upstream_helpers.rs index 3dab465..b45caaa 100644 --- a/src/proxy/upstream_helpers.rs +++ b/src/proxy/upstream_helpers.rs @@ -56,14 +56,11 @@ async fn observe(State(state): State>, mut request: Request, next: Next || (request.version() == Version::HTTP_3 && request.method() == Method::CONNECT && websocket::h3_protocol(&request) == Some("websocket")); - if request.version() == Version::HTTP_11 { - state.metrics.http1_requests.fetch_add(1, Ordering::Relaxed); - } - if request.version() == Version::HTTP_2 { - state.metrics.http2_requests.fetch_add(1, Ordering::Relaxed); - } - if request.version() == Version::HTTP_3 { - state.metrics.http3_requests.fetch_add(1, Ordering::Relaxed); + match request.version() { + Version::HTTP_11 => { state.metrics.http1_requests.fetch_add(1, Ordering::Relaxed); } + Version::HTTP_2 => { state.metrics.http2_requests.fetch_add(1, Ordering::Relaxed); } + Version::HTTP_3 => { state.metrics.http3_requests.fetch_add(1, Ordering::Relaxed); } + _ => {} } let id = state.metrics.requests.fetch_add(1, Ordering::Relaxed) + 1; let request_id = request_id_hash(id); diff --git a/src/upstream/client_tests.rs b/src/upstream/client_tests.rs index aa63d4e..aedca3d 100644 --- a/src/upstream/client_tests.rs +++ b/src/upstream/client_tests.rs @@ -96,5 +96,21 @@ mod tests { } assert_eq!(client.rotation_stats().rotations, 0); } + #[test] + fn request_recycling_still_has_exact_threshold_after_clock_sampling() { + let backend = backend("requests-boundary", 18085); + let mut keepalive = crate::balance::Keepalive::default(); + keepalive.pool_recycle_requests = 1_000; + keepalive.pool_recycle_seconds = 0; + let client = client_with_keepalive( + &backend, AddressPolicy::new(&backend).unwrap(), false, &keepalive, + ).unwrap(); + for _ in 0..1_000 { client.inner.client_for_request(); } + assert_eq!(client.rotation_stats().rotations, 0); + client.inner.client_for_request(); + assert_eq!(client.rotation_stats().rotations, 1); + assert_eq!(client.rotation_stats().generation_requests, 1); + } + }