0.34 #2
No files matched your search
@@ -5,6 +5,21 @@
|
|||||||
|
|
||||||
## 0.34.0
|
## 0.34.0
|
||||||
|
|
||||||
|
- DEV 1 KiB accounting follow-up: move per-domain HIT/MISS/BYPASS, reason,
|
||||||
|
stored-entry and saved-body statistics outside the HOT cache's shared mutex.
|
||||||
|
Publish bounded domain/reason indexes and update counters atomically while
|
||||||
|
preserving the 4,096-domain cap, scoped/global reset behavior and dashboard
|
||||||
|
labels. Avoid creating analytics samples when the existing bounded queue is
|
||||||
|
full, preserving its drop counter and downstream processing. Change the
|
||||||
|
per-vhost latency histogram from up to 11 cumulative atomic increments per
|
||||||
|
exchange to 1 exclusive bucket increment; reconstruct unchanged cumulative
|
||||||
|
Prometheus and Docker-vhost histograms when scraped.
|
||||||
|
- Extend Rust regressions for parallel BYPASS/HIT counting, reset semantics and
|
||||||
|
saturated analytics. The supplied **previous** DEV executable passes 151
|
||||||
|
actual HTTP checks; these newest source changes are uncompiled and their RPS
|
||||||
|
uplift unmeasured. See `docs/PROXY_1KB_COUNTERS_DEV_0.34.0.md`.
|
||||||
|
|
||||||
|
|
||||||
- DEV 1 KiB reverse-proxy throughput follow-up: identify HTTP/1.1 upstream
|
- DEV 1 KiB reverse-proxy throughput follow-up: identify HTTP/1.1 upstream
|
||||||
idle-pool capacity as a bottleneck using 4-vs-64 live tuning trials on the
|
idle-pool capacity as a bottleneck using 4-vs-64 live tuning trials on the
|
||||||
*supplied, unmodified executable*. Change the new-vhost default from 4 to 64
|
*supplied, unmodified executable*. Change the new-vhost default from 4 to 64
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
# RustProxy 0.34.0
|
# RustProxy 0.34.0
|
||||||
|
|
||||||
|
## DEV: cache counter contention and saturated analytics (source candidate)
|
||||||
|
|
||||||
|
Per-domain cache outcome/reason accounting now uses bounded immutable indexes
|
||||||
|
and atomic counters rather than the HOT cache's global store mutex on every
|
||||||
|
request. The analytics producer reserves a queue slot before cloning fields;
|
||||||
|
full-queue/drop behavior and its counter remain unchanged. Per-vhost
|
||||||
|
latency histograms now update one exclusive atomic bin per completion and
|
||||||
|
reconstruct unchanged cumulative Prometheus buckets during scraping. The supplied
|
||||||
|
previous DEV binary passed 151 live HTTP assertions, but this newest code
|
||||||
|
**has not been built**. No version bump, no measured RPS claim. See
|
||||||
|
[`docs/PROXY_1KB_COUNTERS_DEV_0.34.0.md`](docs/PROXY_1KB_COUNTERS_DEV_0.34.0.md).
|
||||||
|
|
||||||
## 0.34.0: HOT HIT critical-section follow-up (source candidate)
|
## 0.34.0: HOT HIT critical-section follow-up (source candidate)
|
||||||
|
|
||||||
Immutable response headers are shared until a HIT; the mutable per-response
|
Immutable response headers are shared until a HIT; the mutable per-response
|
||||||
|
|||||||
+10
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
## 0.34.0 - Scoped cache correctness and proxy body hot path
|
## 0.34.0 - Scoped cache correctness and proxy body hot path
|
||||||
|
|
||||||
|
- [x] DEV 1 KiB: remove shared HOT store mutex from cache outcome/reason
|
||||||
|
accounting; preserve domain cap, exact quiescent totals and reset rules.
|
||||||
|
- [x] Reserve bounded analytics queue capacity before allocating dropped samples;
|
||||||
|
preserve queue-drop accounting and every downstream feature.
|
||||||
|
- [x] Move per-vhost histogram aggregation to scrape time; retain the same
|
||||||
|
cumulative Prometheus and Docker-vhost bucket semantics while one exclusive
|
||||||
|
atomic bin is updated per completed exchange.
|
||||||
|
- [ ] Build/test revised counters source; repeat 1 KiB source A/B and pinned
|
||||||
|
nginx benchmark (100k RPS / 80% target still open).
|
||||||
|
|
||||||
- [x] Share compiled route handles and reuse middleware routing in forwarding;
|
- [x] Share compiled route handles and reuse middleware routing in forwarding;
|
||||||
avoid exact-host wildcard allocation and full-route cloning.
|
avoid exact-host wildcard allocation and full-route cloning.
|
||||||
- [x] Fast-path single-backend LB; share immutable member health settings
|
- [x] Fast-path single-backend LB; share immutable member health settings
|
||||||
|
|||||||
+24
-21
@@ -2,12 +2,12 @@
|
|||||||
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
||||||
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
||||||
46bab1d2c2d6e0a80d57a6b232b1ba4c220884b95cd45ddccbafce2aa160cddb AGENTS.md
|
46bab1d2c2d6e0a80d57a6b232b1ba4c220884b95cd45ddccbafce2aa160cddb AGENTS.md
|
||||||
364809f8807bb1fefcf1fdb80abf706e022d54415250b79178fcc9d7b68a3302 CHANGELOG.md
|
03d0a320df729cc98a119f4296e3b9805aece0c8591fa6b90003df2b58f8608c CHANGELOG.md
|
||||||
ca201e3cd3f9842eae0006d0b40a0a72bcd3d4da6e4e5ecaf200d1b9a1390db2 Cargo.lock
|
ca201e3cd3f9842eae0006d0b40a0a72bcd3d4da6e4e5ecaf200d1b9a1390db2 Cargo.lock
|
||||||
fec5f1bae9a59ef1fabd27c5e45d31448dd8f811736eebfa8826b3806a7e3349 Cargo.toml
|
fec5f1bae9a59ef1fabd27c5e45d31448dd8f811736eebfa8826b3806a7e3349 Cargo.toml
|
||||||
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
|
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
|
||||||
646eeb8d0d421923010f14201de5b8864753511ef715e38af8aad1c78068c45d README.md
|
5533499655d41f1faaf666dfa630375cf6770dff37f6cd7ef5a4e5bbd58cb258 README.md
|
||||||
1119f4de537b90a6263d93eb1ce2b6b0f233fd7a398f32d758c892ba16ea7dd6 ROADMAP.md
|
aee9bbb367e061aeb79916e38d121e7767f6f9d0c16a2dcc4b28f091586a33b1 ROADMAP.md
|
||||||
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
|
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
|
||||||
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
|
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
|
||||||
ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md
|
ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md
|
||||||
@@ -56,6 +56,7 @@ be53a58afbeddbd7deb4b5f6447f2dc74db0327839bd83407c20915df98dcfa0 docs/NOT_FOUND
|
|||||||
e68d5a5cf0991de9308f78afb0264e02979f7463a5f395eb7a6e9ec2bd3a3d54 docs/PHP_FPM.md
|
e68d5a5cf0991de9308f78afb0264e02979f7463a5f395eb7a6e9ec2bd3a3d54 docs/PHP_FPM.md
|
||||||
bc068a176056f5ea4d3b637071e644ed58a2140494c233ef22aa66e9700e3299 docs/PHP_FPM_ISOLATION.md
|
bc068a176056f5ea4d3b637071e644ed58a2140494c233ef22aa66e9700e3299 docs/PHP_FPM_ISOLATION.md
|
||||||
ad6e3f003b820a4f45e30e39577d120a69d167c70910bda4891971759458c3dd docs/PROMETHEUS.md
|
ad6e3f003b820a4f45e30e39577d120a69d167c70910bda4891971759458c3dd docs/PROMETHEUS.md
|
||||||
|
9a19e57e30b2d2818322fcd5b490380281014e2ed43279c2251f09acbfd23130 docs/PROXY_1KB_COUNTERS_DEV_0.34.0.md
|
||||||
f13ee0005d57d2576ccc970745f17069096df4afa23d712ea0f748da85433f7d docs/PROXY_1KB_HOTPATH_DEV_0.34.0.md
|
f13ee0005d57d2576ccc970745f17069096df4afa23d712ea0f748da85433f7d docs/PROXY_1KB_HOTPATH_DEV_0.34.0.md
|
||||||
deea89aed410c0619bdbdb12437edc7aff9dca2fec7fdc059e918c649fc36b51 docs/PROXY_HOTPATH_0.34.0.md
|
deea89aed410c0619bdbdb12437edc7aff9dca2fec7fdc059e918c649fc36b51 docs/PROXY_HOTPATH_0.34.0.md
|
||||||
a7d6189b0e8cb854918456e0063101ee77bfb2419a9ba0430eb0e3f8678c0f09 docs/PROXY_RPS_0.34.0.md
|
a7d6189b0e8cb854918456e0063101ee77bfb2419a9ba0430eb0e3f8678c0f09 docs/PROXY_RPS_0.34.0.md
|
||||||
@@ -72,7 +73,7 @@ ec7e32fd19e6ba6ed1732f1f3dd92ac0860609e8c0bbd3c1998840cdfb24bf54 docs/SECURITY.
|
|||||||
4dcf3243a13d9ae1827b5cefdbb9e27bd6310321c50d4d5934527d44437df6ef docs/SETTINGS.md
|
4dcf3243a13d9ae1827b5cefdbb9e27bd6310321c50d4d5934527d44437df6ef docs/SETTINGS.md
|
||||||
8fc112cb45a2f35ef5b0dd4371aa6f43aca4df85db62c6ae02e56d74a3b702ae docs/STREAMS.md
|
8fc112cb45a2f35ef5b0dd4371aa6f43aca4df85db62c6ae02e56d74a3b702ae docs/STREAMS.md
|
||||||
95148bfa1d2f8ff52b591c9acefc6c8992dadc4ebda43a195101d9b31d67a576 docs/TEST_REPORT.md
|
95148bfa1d2f8ff52b591c9acefc6c8992dadc4ebda43a195101d9b31d67a576 docs/TEST_REPORT.md
|
||||||
9bc103492b1ddeae5d5489a3e47a1c21ff14ec0e98372dc14c6ccb412dfb7846 docs/TEST_REPORT_0.34.0.md
|
47e3cf646b2884db18684385ca7ff18d929b6de6a5206ed5e7ad27a71d350052 docs/TEST_REPORT_0.34.0.md
|
||||||
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
|
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
|
||||||
751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md
|
751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md
|
||||||
e6d270894cafe96f8aa9cf58c9609223d03c9bcf6f872adfcbfb767a7b313f0a docs/TRANSPORT.md
|
e6d270894cafe96f8aa9cf58c9609223d03c9bcf6f872adfcbfb767a7b313f0a docs/TRANSPORT.md
|
||||||
@@ -132,7 +133,8 @@ a641bd35400c56f3b68b0a92e97b602d06090f03972ca8f01fa744e90dd97403 scripts/deploy
|
|||||||
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migration/migrate_npm.py
|
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migration/migrate_npm.py
|
||||||
a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migration/migrate_npm_api.py
|
a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migration/migrate_npm_api.py
|
||||||
31ccad3ec3c880119f3b7002f0c6652f8b43634edad8ab3763cb0e4497d0bb1f scripts/migration/migrate_npm_core.py
|
31ccad3ec3c880119f3b7002f0c6652f8b43634edad8ab3763cb0e4497d0bb1f scripts/migration/migrate_npm_core.py
|
||||||
76009d786638720efb996401a8752340d00c21b93c81657fc2d5364431d7a1a4 scripts/performance/README.md
|
2f5206ddea7a0816924986bfe663734f9fee7771fb25c04e0374642329b06c35 scripts/performance/README.md
|
||||||
|
939287dadf7309689d29d2171757c2d80e91d898c3f9112454b943d08bd611fb scripts/performance/ab_1kb_dev.py
|
||||||
0380406a059ee9ec27473f8a46435f638f73a6dc729382ae1561c08dbf7ccb53 scripts/performance/benchmark.py
|
0380406a059ee9ec27473f8a46435f638f73a6dc729382ae1561c08dbf7ccb53 scripts/performance/benchmark.py
|
||||||
9ac220163256e08513597c397c016a48345b114d7876b439259cc310b36c83fc scripts/performance/checks_cache.py
|
9ac220163256e08513597c397c016a48345b114d7876b439259cc310b36c83fc scripts/performance/checks_cache.py
|
||||||
f09125d081d0eed9a44fdca75de14e6bbf09dcb00fe1f027ba2270f89e3ff192 scripts/performance/checks_policy.py
|
f09125d081d0eed9a44fdca75de14e6bbf09dcb00fe1f027ba2270f89e3ff192 scripts/performance/checks_policy.py
|
||||||
@@ -161,7 +163,7 @@ e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/runtim
|
|||||||
f0b48ec0cc4411755cc0b4128eebce710375afa99344cb76198cf31074f8eb63 scripts/test_stream_udp.py
|
f0b48ec0cc4411755cc0b4128eebce710375afa99344cb76198cf31074f8eb63 scripts/test_stream_udp.py
|
||||||
fafd577ccbb58485c47de35fc125e5c64a3a7628ae40d01d4cd924fce2f414db scripts/tests/cache_age_policy_034_model.py
|
fafd577ccbb58485c47de35fc125e5c64a3a7628ae40d01d4cd924fce2f414db scripts/tests/cache_age_policy_034_model.py
|
||||||
b75a381d1b41bfb2ea7d2ce09728249b5e1465f76d57e2c3f151af72a5ae4d03 scripts/tests/cache_age_transform_034_live.py
|
b75a381d1b41bfb2ea7d2ce09728249b5e1465f76d57e2c3f151af72a5ae4d03 scripts/tests/cache_age_transform_034_live.py
|
||||||
45a2f4c271b03f413fd96aa2e8360c7895b9a55d5d8f2d2a0fedf0118933086e scripts/tests/cache_hot_034_static_test.py
|
dbab8d5457546a1ffe8233514ee94225a73974fe5d6f3b1f5fadd06885fa228c scripts/tests/cache_hot_034_static_test.py
|
||||||
6cb79535f742d1a41685f486688dc2f1330ea362e3da1a15d4fa32561094282f scripts/tests/cache_scope_034_static_test.py
|
6cb79535f742d1a41685f486688dc2f1330ea362e3da1a15d4fa32561094282f scripts/tests/cache_scope_034_static_test.py
|
||||||
efb7a1fde2c74f30cd4a1abb88ae07c1cac7378bb33685dd09d52bb30849daac scripts/tests/dashboard_chart_headers_0335_browser_test.py
|
efb7a1fde2c74f30cd4a1abb88ae07c1cac7378bb33685dd09d52bb30849daac scripts/tests/dashboard_chart_headers_0335_browser_test.py
|
||||||
eb2c9e5c968938b6fbe4675b547ba1f537519878b9095e1fea133f4d83f96fc8 scripts/tests/dashboard_chart_typography_0337_browser_test.py
|
eb2c9e5c968938b6fbe4675b547ba1f537519878b9095e1fea133f4d83f96fc8 scripts/tests/dashboard_chart_typography_0337_browser_test.py
|
||||||
@@ -182,7 +184,7 @@ fa42ec0475e4398d647b3c094309b4332cd2b5649db223cae0b8e821d6b44a28 scripts/tests/
|
|||||||
b0136544af717b75df2cb107900ea65c3f945fdd0040369dbb93919008fb954c scripts/tests/panel_html.py
|
b0136544af717b75df2cb107900ea65c3f945fdd0040369dbb93919008fb954c scripts/tests/panel_html.py
|
||||||
8d1903e2b17b3d59cd0a5bec3e298e468b3d88ed2dda8b724239cc45d8353c59 scripts/tests/progressbar_0332_browser_test.py
|
8d1903e2b17b3d59cd0a5bec3e298e468b3d88ed2dda8b724239cc45d8353c59 scripts/tests/progressbar_0332_browser_test.py
|
||||||
2a6e75c05f3860acf65ea8132f0083904478b690145115ec017c120d70f26cca scripts/tests/proxy_hotpath_034_static_test.py
|
2a6e75c05f3860acf65ea8132f0083904478b690145115ec017c120d70f26cca scripts/tests/proxy_hotpath_034_static_test.py
|
||||||
e2a698093c6e0125562eade1ce8098be11105ec24b8fdb0b120b831762072ccb scripts/tests/rps_hotpath_034_static_test.py
|
6fa2f22816996e989dad9dc7884adbf24f02f0ca6a6634b06e219eb08526c7fe scripts/tests/rps_hotpath_034_static_test.py
|
||||||
5be5cdc60324f4f0f6638640000b888a5babb0a9bbf29511c824a0ba38878c2a scripts/tests/sendfile_034_panel_browser_test.py
|
5be5cdc60324f4f0f6638640000b888a5babb0a9bbf29511c824a0ba38878c2a scripts/tests/sendfile_034_panel_browser_test.py
|
||||||
978789bfe89d43034dfebddba49b9b17daedb1be031732734f95375e56c78306 scripts/tests/sendfile_034_static_test.py
|
978789bfe89d43034dfebddba49b9b17daedb1be031732734f95375e56c78306 scripts/tests/sendfile_034_static_test.py
|
||||||
1bb99b148d0297235b707f16865ca077329246fc19ecbb20a143bdeb56a23f37 scripts/tests/sidebar_nav_0333_browser_test.py
|
1bb99b148d0297235b707f16865ca077329246fc19ecbb20a143bdeb56a23f37 scripts/tests/sidebar_nav_0333_browser_test.py
|
||||||
@@ -334,32 +336,33 @@ a9eeb93519aae2de7a1282296ced91d2eb8307cc966f8fbf6085af0fd21b0ad2 src/cache/cold
|
|||||||
2eaba3261da2a547e9af91330319a3abd3a8ad8b5b44a337a84040720209017d src/cache/cold/startup.rs
|
2eaba3261da2a547e9af91330319a3abd3a8ad8b5b44a337a84040720209017d src/cache/cold/startup.rs
|
||||||
e7c7fd71727d542c2df3e03a4a1cba9d839c516963f66bbfde1c29ec81053500 src/cache/cold/worker.rs
|
e7c7fd71727d542c2df3e03a4a1cba9d839c516963f66bbfde1c29ec81053500 src/cache/cold/worker.rs
|
||||||
d51fa4f320be56c572f767617428c3f3838543cb0eeb7d1f8ae791e1a1ce9a71 src/cache/collapse.rs
|
d51fa4f320be56c572f767617428c3f3838543cb0eeb7d1f8ae791e1a1ce9a71 src/cache/collapse.rs
|
||||||
0b9b3ff7ff6089bedf1a101c3a015a1a217b8221c6cac9c07a4a8df75f0fbf6b src/cache/dashboard.rs
|
36febf4d27a2f5bbd72dab7f45d8b8dd64fb4f7b95060b9e0ebd5709d19749fa src/cache/dashboard.rs
|
||||||
7eaf3947cfc89cc6ef55a64fef0b3fedf1d9a1c33322aba2b5170c0ea34a5f2c src/cache/fill.rs
|
7eaf3947cfc89cc6ef55a64fef0b3fedf1d9a1c33322aba2b5170c0ea34a5f2c src/cache/fill.rs
|
||||||
675d18afb53f25c56cba49bbb431fa39ecd9d7bdd192c7b39b0698225de7a2a2 src/cache/invalidation.rs
|
675d18afb53f25c56cba49bbb431fa39ecd9d7bdd192c7b39b0698225de7a2a2 src/cache/invalidation.rs
|
||||||
c1357a13658e1ed5158084d3a41ba9ea906512774a5863ce9c56cb508d3fa6c8 src/cache/invalidation/ban.rs
|
c1357a13658e1ed5158084d3a41ba9ea906512774a5863ce9c56cb508d3fa6c8 src/cache/invalidation/ban.rs
|
||||||
3b8f77bde71253a3ca600be5438b7cc72f7da258f3afa5de2cf64553e49922d1 src/cache/invalidation/invalidate.rs
|
3b8f77bde71253a3ca600be5438b7cc72f7da258f3afa5de2cf64553e49922d1 src/cache/invalidation/invalidate.rs
|
||||||
6764e2a20038fb2f75d1b821b5fbe683325d32ed78bc547f064e6e8a245ceec7 src/cache/lifecycle.rs
|
f3c5ec56f8d81a285877f6a07239081c2b1b064fc9143ba3bf36d616b0239d86 src/cache/lifecycle.rs
|
||||||
bd831807ca4720b3a41a6ea1d9a6c0ed2d935225e2dacbd9313be064f2e4a20e src/cache/lookup.rs
|
bd831807ca4720b3a41a6ea1d9a6c0ed2d935225e2dacbd9313be064f2e4a20e src/cache/lookup.rs
|
||||||
b52ba2f8a54fdff986bd9026639e7c6d084016046072b698d0278ddfd32e3ebb src/cache/lookup/hot.rs
|
bbd5659a3197e443b3b5b726d39d7b7934bb9aeaedcdc04285bbf4321d1359d4 src/cache/lookup/hot.rs
|
||||||
7c5c7cd2788c09e801ef8aa9598b41d1a9c3991007801a06e962c4368a60a0ea src/cache/lookup/insert.rs
|
5c3f749b3028e436a99614c22dbd05907b63c12a5c7c6baeda24c8f506db4f65 src/cache/lookup/insert.rs
|
||||||
b38f93fa2eac694aac4dd2f1d69b55688928b53b78bf095d479843956c78e605 src/cache/lookup/tiered.rs
|
b38f93fa2eac694aac4dd2f1d69b55688928b53b78bf095d479843956c78e605 src/cache/lookup/tiered.rs
|
||||||
58c03fde09e5eab40968679793b364f4f1cf5fd832bd79ebcdc92bb23871ad82 src/cache/mod.rs
|
5d4338ecccdbe9d78476e28491d92d4a1f96ab3c1d40590a7cfd1ba131b3ee12 src/cache/mod.rs
|
||||||
0c8b0d550e94d4474dcdf991aa0340228dc5f2f904f39350e6e666a16f16fe87 src/cache/model.rs
|
0c8b0d550e94d4474dcdf991aa0340228dc5f2f904f39350e6e666a16f16fe87 src/cache/model.rs
|
||||||
4862b330040664b663da7540a694da573010eba188b770d27a72bf2b35b327fe src/cache/model/cache.rs
|
9704265423d53e87c04cbac11effdcc3792439699494138dc86466ab50656993 src/cache/model/cache.rs
|
||||||
0ec4f6d0adea1ed80a3ca17c68c9c85502491da2904d9d690b4169ab9b025b21 src/cache/model/cold_metadata.rs
|
0ec4f6d0adea1ed80a3ca17c68c9c85502491da2904d9d690b4169ab9b025b21 src/cache/model/cold_metadata.rs
|
||||||
29bfce96b2ffd102d9c848c7b717a45fedb9aa44511f62f7a1f28a53dafd063a src/cache/model/entry.rs
|
29bfce96b2ffd102d9c848c7b717a45fedb9aa44511f62f7a1f28a53dafd063a src/cache/model/entry.rs
|
||||||
88d74fc1b7b9ca15a54a8f4893ad3993105952148bcfee099e9bc9727b622aa3 src/cache/model/http_invalidation.rs
|
88d74fc1b7b9ca15a54a8f4893ad3993105952148bcfee099e9bc9727b622aa3 src/cache/model/http_invalidation.rs
|
||||||
ff4dab5a6591b1afc359c6aa2b7e0f5f2be013836bb3c67a24db2eb418ce7bd5 src/cache/model/maintenance.rs
|
5ea7d8e72c714e5c5330dd81b5a6e2c8ed79e4367af85f133e63a756ad9788a3 src/cache/model/maintenance.rs
|
||||||
5441fa3a384add28b6fdd15300cf2648f8237dbfc211cad9117754203be76911 src/cache/model/settings.rs
|
5441fa3a384add28b6fdd15300cf2648f8237dbfc211cad9117754203be76911 src/cache/model/settings.rs
|
||||||
55dcc0ad84d6b73689ba43e759d5685c406419c7733e23e78d5b072891ab17d7 src/cache/model/views.rs
|
55dcc0ad84d6b73689ba43e759d5685c406419c7733e23e78d5b072891ab17d7 src/cache/model/views.rs
|
||||||
e26dce02df9c043329ca164fed215d271781bce8fb03240f528331bf3df6b9d7 src/cache/policy.rs
|
e26dce02df9c043329ca164fed215d271781bce8fb03240f528331bf3df6b9d7 src/cache/policy.rs
|
||||||
989df11e246123f760ef31689370f3500e0913fcc6b70d7ac0c2c95e2f412fb0 src/cache/policy/request.rs
|
989df11e246123f760ef31689370f3500e0913fcc6b70d7ac0c2c95e2f412fb0 src/cache/policy/request.rs
|
||||||
d6051d55168fe1db97980a54c22c708f4f325ba72bfc36f35a1851eb5dcc051d src/cache/policy/response.rs
|
d6051d55168fe1db97980a54c22c708f4f325ba72bfc36f35a1851eb5dcc051d src/cache/policy/response.rs
|
||||||
7e2bcfbb2b57732cdc704d9df7a9298fb80e735e37984431bc936630e41e7f8a src/cache/range.rs
|
7e2bcfbb2b57732cdc704d9df7a9298fb80e735e37984431bc936630e41e7f8a src/cache/range.rs
|
||||||
|
6d7a4c22a7776a6538fb22d78806bec72efbe11aa5a26fb3ecf575ffd266ada0 src/cache/stats.rs
|
||||||
93bd3e9e5c1a045c784cee0e1fdb9e1c5d862ed70d5d91c54c5a769bca8fd949 src/cache/tests.rs
|
93bd3e9e5c1a045c784cee0e1fdb9e1c5d862ed70d5d91c54c5a769bca8fd949 src/cache/tests.rs
|
||||||
9183fa8e6086e92fb148fc278fd866d44e3dd988c848a071e32066e44a1cbf0a src/cache/tests/cold_tier_tests.rs
|
9183fa8e6086e92fb148fc278fd866d44e3dd988c848a071e32066e44a1cbf0a src/cache/tests/cold_tier_tests.rs
|
||||||
334893037a278ca2a6f0b8b1a32fe0e2d286665f24db78d787bfe9425dc5a88d src/cache/tests/dashboard_tests.rs
|
25ed7635bfd5e34de55b319bf163db9a0af26642664ce7685ebe1241bab52cac src/cache/tests/dashboard_tests.rs
|
||||||
a0a23e7ec1baa403524811f8096bbc026b1027b7b1b79357ad920b0a4be770b7 src/cache/tests/hot_hit_034_tests.rs
|
a0a23e7ec1baa403524811f8096bbc026b1027b7b1b79357ad920b0a4be770b7 src/cache/tests/hot_hit_034_tests.rs
|
||||||
d7a02eb7bad37e6a5524600aabb5e60ccf50e26c7a487e2806891c9d8acd48f3 src/cache/tests/http_invalidation_tests.rs
|
d7a02eb7bad37e6a5524600aabb5e60ccf50e26c7a487e2806891c9d8acd48f3 src/cache/tests/http_invalidation_tests.rs
|
||||||
1659ffd8d227d2ec8b98e6a5e06b6143e6ac4ac06ab423c184980abcb47085da src/cache/tests/scope_tests.rs
|
1659ffd8d227d2ec8b98e6a5e06b6143e6ac4ac06ab423c184980abcb47085da src/cache/tests/scope_tests.rs
|
||||||
@@ -421,21 +424,21 @@ fc8e99eac83c083d65101f6402aacbcc8392d66508c31566a50de3ebcb12e7da src/main/admin
|
|||||||
e5c2e9189777725f4afa1c3c052e4f75047ec3f2f486630d990f5d3c194bcc96 src/main/tasks.rs
|
e5c2e9189777725f4afa1c3c052e4f75047ec3f2f486630d990f5d3c194bcc96 src/main/tasks.rs
|
||||||
49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs
|
49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs
|
||||||
201ab702722d6d21585fe8bb49939150d61bfac7ed8dc198b98c2cc6098ffb5f src/metrics/global.rs
|
201ab702722d6d21585fe8bb49939150d61bfac7ed8dc198b98c2cc6098ffb5f src/metrics/global.rs
|
||||||
49b11df65b4fb0b5b727f9f867436584680b45e3f4fbd53f74bc0cab8b9a88ec src/metrics/vhost.rs
|
78aa428a3ed8f9a11d69ec2a6783679fb27ea99a74bc18d866c65c5391d61293 src/metrics/vhost.rs
|
||||||
a54b5b34ea65f29723b89b81bf7968a238e8247b9c9760f3430d3504cb1017c9 src/metrics/vhost/tests.rs
|
e9cd8d9aff1de7c92b3381f7646e5c577f857d960bbb276e0736b29973b97319 src/metrics/vhost/tests.rs
|
||||||
a4702c95c7e5077c2ccff86e2e3216e1da6ff9ee4383ae199a0aff2f78db325f src/not_found.rs
|
a4702c95c7e5077c2ccff86e2e3216e1da6ff9ee4383ae199a0aff2f78db325f src/not_found.rs
|
||||||
c915d30bbd7032d867e7916b4cd45f3bb7e7604d3f4558f4383c59394b4300c6 src/not_found/tests.rs
|
c915d30bbd7032d867e7916b4cd45f3bb7e7604d3f4558f4383c59394b4300c6 src/not_found/tests.rs
|
||||||
d66936cc15fd5d56566d40fa8cab93f80d94b05f10ac44478a30f63572a95e32 src/pages.rs
|
d66936cc15fd5d56566d40fa8cab93f80d94b05f10ac44478a30f63572a95e32 src/pages.rs
|
||||||
da40e8ece87245efc49a0c4e750526b1d19fd97565856bd5caf641fcc1dfe18d src/pages/tests.rs
|
da40e8ece87245efc49a0c4e750526b1d19fd97565856bd5caf641fcc1dfe18d src/pages/tests.rs
|
||||||
0dba7b6a33866cc7bbe05509b01961e545892645d1dad4eb88ac8a4bec65e3bf src/prometheus.rs
|
0dba7b6a33866cc7bbe05509b01961e545892645d1dad4eb88ac8a4bec65e3bf src/prometheus.rs
|
||||||
cc6fb14351b009a16d07ed0f412c2bcc68cd7e551421aea03d8d97ceec9bee40 src/prometheus/core.rs
|
cc6fb14351b009a16d07ed0f412c2bcc68cd7e551421aea03d8d97ceec9bee40 src/prometheus/core.rs
|
||||||
c46882ed26742d93806a976257674326e105b8ca1fc23f5ca81e317b268057aa src/prometheus/docker_misc.rs
|
74822d8293fe1ce64b18d8363a67e9f0ed2ee9d181e2668e5ca6bd258f98285c src/prometheus/docker_misc.rs
|
||||||
d3c6b08088df0003144544936a55e2a3661aac85292821b594f00adfd19a2951 src/prometheus/model.rs
|
d3c6b08088df0003144544936a55e2a3661aac85292821b594f00adfd19a2951 src/prometheus/model.rs
|
||||||
064b2be7fed2f6acf526b1cce070c1bb1b943d36f08fa50bbfcbdf3cf1c9318c src/prometheus/tests.rs
|
064b2be7fed2f6acf526b1cce070c1bb1b943d36f08fa50bbfcbdf3cf1c9318c src/prometheus/tests.rs
|
||||||
028e1dd9f2ec2afaf699a6b21c2d4b0d96e76f11f5e4ed009c233dadf0f3be51 src/prometheus/traffic.rs
|
028e1dd9f2ec2afaf699a6b21c2d4b0d96e76f11f5e4ed009c233dadf0f3be51 src/prometheus/traffic.rs
|
||||||
e03f0d6b3f35dae6b98c394837aa0d17e4d73a8306767b4c607868bc00c30286 src/prometheus/vhosts_backends.rs
|
e03f0d6b3f35dae6b98c394837aa0d17e4d73a8306767b4c607868bc00c30286 src/prometheus/vhosts_backends.rs
|
||||||
fb2d8d45cd2e947f783eb981b37fa418c39fcb327a3af17a648fadea7f37f888 src/prometheus/web_php.rs
|
fb2d8d45cd2e947f783eb981b37fa418c39fcb327a3af17a648fadea7f37f888 src/prometheus/web_php.rs
|
||||||
fc234886805226f744306c83df859805c444edf102dc4eb14f443afaa4b3362a src/proxy/body.rs
|
2c97367686b54989573ce7fb81b2c6dde72adb254df9522bc05ded74f1fbffb6 src/proxy/body.rs
|
||||||
c9b76abd91f0dfe81e07073e89d35faa033c19e11fa88b223f075428542284f1 src/proxy/body/tests.rs
|
c9b76abd91f0dfe81e07073e89d35faa033c19e11fa88b223f075428542284f1 src/proxy/body/tests.rs
|
||||||
b88d1d7a6837a2a470e5863f1f3247878daee62791fc3d8e72baeb66469bdfbf src/proxy/cache_helpers.rs
|
b88d1d7a6837a2a470e5863f1f3247878daee62791fc3d8e72baeb66469bdfbf src/proxy/cache_helpers.rs
|
||||||
aa49c9928bb91e3d5bae0d340d88396a0417b9edec6192d41e1403533319a897 src/proxy/cache_http_invalidation.rs
|
aa49c9928bb91e3d5bae0d340d88396a0417b9edec6192d41e1403533319a897 src/proxy/cache_http_invalidation.rs
|
||||||
@@ -516,8 +519,8 @@ d9123c623e2a97d09e41edaa29993b4b2c8cba39a3abdf2b48a205aa9a9c2939 src/system_sta
|
|||||||
2af2164ecdf2b7fd9eef7c6e358dec053dfac9aedaa501dbb75df742d18638b2 src/tls/tests/acme.rs
|
2af2164ecdf2b7fd9eef7c6e358dec053dfac9aedaa501dbb75df742d18638b2 src/tls/tests/acme.rs
|
||||||
93b359e9defd62506729bc90e56c96783e2f14dd477dd67f337ca83149da7168 src/tls/tests/external_paths.rs
|
93b359e9defd62506729bc90e56c96783e2f14dd477dd67f337ca83149da7168 src/tls/tests/external_paths.rs
|
||||||
10c4a187e3aeb0c72a8c8968ea0cc8bfae4e3cb2f3d834ebd3da1056c4171ec5 src/tls/types.rs
|
10c4a187e3aeb0c72a8c8968ea0cc8bfae4e3cb2f3d834ebd3da1056c4171ec5 src/tls/types.rs
|
||||||
c2f802d95da4332fb8480e83f0c7502334457b3408141c1c4516c9ba320a0463 src/traffic.rs
|
0d2ce0a7f5200437a1f965c62f72c91a681949a0bdec8ef4ccdb837a55626ed7 src/traffic.rs
|
||||||
36301bcbd20b557dd3d58e77a9175f3581947f577c0bb0b972899cec6786b03f src/traffic/tests.rs
|
aa6cea1b22cb5805934e1f819e9c5254a7b4f745025842144f6e6ae671eb170d src/traffic/tests.rs
|
||||||
d88c92f5a2a7773bc3e715d24039db35da4df53881f1a0859eec819b96f06d3b src/transform/apply.rs
|
d88c92f5a2a7773bc3e715d24039db35da4df53881f1a0859eec819b96f06d3b src/transform/apply.rs
|
||||||
87f8af5261d79072f8215f1fb8a90715a58e7626544fd8176ce840e3111bdb92 src/transform/compile.rs
|
87f8af5261d79072f8215f1fb8a90715a58e7626544fd8176ce840e3111bdb92 src/transform/compile.rs
|
||||||
b2c5236589093454ec60984b785ad6a4af6149d47f6db37d8e08c17fec7a2c1b src/transform/css.rs
|
b2c5236589093454ec60984b785ad6a4af6149d47f6db37d8e08c17fec7a2c1b src/transform/css.rs
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# RustProxy 0.34.0 DEV — 1 KiB lock contention follow-up
|
||||||
|
|
||||||
|
## Goal and baseline
|
||||||
|
|
||||||
|
**Priority 1: reverse proxy, HTTP/1.1 keep-alive, uncompressed 1,040-byte
|
||||||
|
responses, cache OFF.** Aiming for approximately 100,000 RPS and at least 80%
|
||||||
|
of nginx under the same pinned-CPU benchmark. Priority 2: local static WWW,
|
||||||
|
measured separately. No feature removals and **no version bump**.
|
||||||
|
|
||||||
|
The user supplied a **new executable** (SHA-256
|
||||||
|
`1a84c58b20945bba9fdd1bc1a44d9e9d6ac7659d714343a46a1a8511a78331fa`,
|
||||||
|
`rust-proxy 0.34.0`), apparently built from the prior DEV revision. It passed
|
||||||
|
151 actual black-box HTTP tests with 0 failures and 1 skipped optional zstd
|
||||||
|
decoder. This runtime coverage belongs to the **supplied executable only**,
|
||||||
|
not to the freshly edited source below. Neither `cargo` nor `rustc` is
|
||||||
|
available in the editing environment; the new revision has NOT been compiled.
|
||||||
|
|
||||||
|
## Modified source
|
||||||
|
|
||||||
|
### `Cache::record` and `Cache::record_reason`
|
||||||
|
|
||||||
|
Before, every HIT, MISS and BYPASS acquired the same `Store` mutex used by the
|
||||||
|
HOT object store, exact LRU and cache inventory. With cache disabled, a 1 KiB
|
||||||
|
request still contested that mutex to increment `cache_disabled`.
|
||||||
|
|
||||||
|
`src/cache/stats.rs` now holds bounded per-(scope,domain) statistics separately:
|
||||||
|
|
||||||
|
- An `ArcSwap`-published immutable, borrowed-lookup BTreeMap of domain handles.
|
||||||
|
**The normal path does not lock or allocate**, even for BYPASS. New domains
|
||||||
|
alone use a registration mutex and a copy-on-first-visit registry update.
|
||||||
|
- Atomic counts for HIT, MISS, BYPASS, stored objects and saved body bytes.
|
||||||
|
The common static reasons (`cache_disabled`, `cache_hit`, `cache_miss`,
|
||||||
|
`request_cookie`) use direct atomic counters. Other reason names use another
|
||||||
|
immutable published index and atomic counters; they are registered once.
|
||||||
|
- Hard cardinality remains **4096 (scope, domain) pairs**. When full, new
|
||||||
|
unregistered domains are skipped as before. Scope purge preserves stats.
|
||||||
|
Only a revision-bearing global purge resets the per-domain registry.
|
||||||
|
- The dashboard still exposes the same `DomainCounters`, reason strings,
|
||||||
|
saved byte counts, ratio and per-vhost/object views. Counter snapshots are
|
||||||
|
quiescent-exact, but under concurrent requests individual fields can reflect
|
||||||
|
slightly different instants, consistent with other atomic metrics.
|
||||||
|
- HOT object and COLD representation/LRU access, barriers, memory accounting,
|
||||||
|
cache statuses, hit policy, retry and backend security paths are unchanged.
|
||||||
|
|
||||||
|
Trade-offs: a first-visit registration copies the bounded domain index and
|
||||||
|
allocates one counter set; this is not an adaptive, unbounded cache. The hot
|
||||||
|
path's ArcSwap guard and lookup still cost CPU. Measure that cost in profiles;
|
||||||
|
compare under high-cardinality traffic to identify any excessive registration
|
||||||
|
cost before production acceptance.
|
||||||
|
|
||||||
|
### Traffic analytics queue saturation
|
||||||
|
|
||||||
|
Previously `Exchange::drop` cloned the vhost, route, method, path, IP, user
|
||||||
|
agent and country before trying to enqueue the `Sample`. If the bounded
|
||||||
|
2,048-element MPSC queue was full, all those clones were discarded.
|
||||||
|
|
||||||
|
`Analytics::push_lazy` now acquires the same bounded Tokio queue permit with
|
||||||
|
`try_reserve` first and creates the sample only after a successful reservation.
|
||||||
|
Rejections increment `queue_dropped` exactly as before. Neither the analytics
|
||||||
|
worker, counters/top lists, reset behavior nor request log path is removed.
|
||||||
|
This improves allocation costs **on overflow** but does not turn dropped
|
||||||
|
analytics into delivered samples; monitor the existing drop counter.
|
||||||
|
|
||||||
|
### Per-vhost duration histogram atomics
|
||||||
|
|
||||||
|
Previously `VhostMetrics::complete` incremented every cumulative latency bucket
|
||||||
|
whose threshold covered the observation and the `+Inf` bucket. For an ordinary
|
||||||
|
short response this meant **11 shared atomic RMWs** for the histogram alone,
|
||||||
|
on top of existing exchange/status/byte/latency metrics.
|
||||||
|
|
||||||
|
It now increments **exactly one exclusive bucket** chosen from the same 10
|
||||||
|
latency boundaries. Both the regular vhost and Docker-vhost Prometheus exporters
|
||||||
|
compute the original cumulative `le` buckets, `+Inf` and `_count` from the 11
|
||||||
|
exclusive buckets **at scrape time**. The metric names, labels, boundaries,
|
||||||
|
body-lifetime timing and measurement collection remain intact. The cumulative
|
||||||
|
snapshot may be concurrent with request completions, as before. Added boundary
|
||||||
|
unit tests for exact thresholds and long-duration `+Inf`.
|
||||||
|
|
||||||
|
This is source-only: neither the throughput benefit nor compiled Rust behavior
|
||||||
|
has been verified in the editing environment. Review exporter parity and
|
||||||
|
under-load histogram correctness on the newly compiled executable.
|
||||||
|
|
||||||
|
## Verification and untested claims
|
||||||
|
|
||||||
|
- PASS: source preflight, cache/HOT/LB/RPS/histogram/static source guards and <=500-line
|
||||||
|
production Rust modules.
|
||||||
|
- PASS: SQLite migration + preset constraints, upgrade/lock and source
|
||||||
|
replacement tests.
|
||||||
|
- PASS: compiled user-supplied binary: 151 real HTTP checks, 0 failed,
|
||||||
|
1 optional zstd skip. This binary predates the source changes above.
|
||||||
|
- NOT RUN: Rust compile, Rust unit tests, native regressions or RPS after
|
||||||
|
rebuilding the new source. No numerical speedup attributed to this patch.
|
||||||
|
- Current diagnostic A/B on the user-supplied binary may use
|
||||||
|
`scripts/performance/ab_1kb_dev.py` and must remain labelled *old vs already
|
||||||
|
supplied binary*, **not** old vs newly edited source. The `wrk` 30 s x 7
|
||||||
|
pinned-CPU benchmark remains the production acceptance gate.
|
||||||
|
|
||||||
|
## Repeat on a compiler-enabled benchmark host
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./dev.sh doctor
|
||||||
|
./dev.sh check
|
||||||
|
CARGO_INCREMENTAL=0 ./dev.sh test
|
||||||
|
CARGO_INCREMENTAL=0 cargo build --release --locked
|
||||||
|
python3 scripts/performance/regression.py \
|
||||||
|
--binary previous=/absolute/path/to/supplied-rust-proxy \
|
||||||
|
--binary edited="$(realpath target/release/rust-proxy)" \
|
||||||
|
--out /tmp/rustproxy034-counter-http
|
||||||
|
python3 scripts/performance/lb_regression.py \
|
||||||
|
--binary previous=/absolute/path/to/supplied-rust-proxy \
|
||||||
|
--binary edited="$(realpath target/release/rust-proxy)" \
|
||||||
|
--out /tmp/rustproxy034-counter-lb
|
||||||
|
python3 scripts/performance/transport_regression.py \
|
||||||
|
--binary previous=/absolute/path/to/supplied-rust-proxy \
|
||||||
|
--binary edited="$(realpath target/release/rust-proxy)" \
|
||||||
|
--out /tmp/rustproxy034-counter-transport
|
||||||
|
python3 scripts/performance/benchmark.py \
|
||||||
|
--binary previous=/absolute/path/to/supplied-rust-proxy \
|
||||||
|
--binary edited="$(realpath target/release/rust-proxy)" \
|
||||||
|
--nginx /usr/sbin/nginx --wrk /absolute/path/to/wrk \
|
||||||
|
--out /tmp/rustproxy034-counter-benchmark \
|
||||||
|
--modes proxy --sizes small --concurrency 16,64,128 \
|
||||||
|
--duration 30 --warmup 5 --repeats 7
|
||||||
|
```
|
||||||
|
|
||||||
|
Explicitly set `max_idle_per_host=64` on **both** before/after vhosts, with the
|
||||||
|
same TLS/H1/H2/health/cache/logging/settings. Verify 1,040 bytes and SHA-256,
|
||||||
|
no failures, latency percentiles, per-request CPU, RSS, file descriptors,
|
||||||
|
backend connects and all log/traffic drop counters. Test concurrent global
|
||||||
|
purges and per-vhost purges during traffic and verify the stats reset behavior
|
||||||
|
and cache HIT/MISS/BYPASS/reason distribution. Run long soaks and both cold/HOT
|
||||||
|
cache tests. The 100k RPS / nginx 80% goal is still **OPEN**.
|
||||||
@@ -9,6 +9,21 @@ compiled**, the new Rust tests were **not executed**, and no 0.34.0 RustProxy
|
|||||||
binary or live throughput figures are supplied. Source inspection and simulated
|
binary or live throughput figures are supplied. Source inspection and simulated
|
||||||
UI/cache tests do not close the runtime acceptance gate.
|
UI/cache tests do not close the runtime acceptance gate.
|
||||||
|
|
||||||
|
## DEV 1 KiB counters follow-up (source remains uncompiled)
|
||||||
|
|
||||||
|
The newly supplied binary `rust-proxy(1)` identifies as 0.34.0 and has
|
||||||
|
SHA-256 `1a84c58b20945bba9fdd1bc1a44d9e9d6ac7659d714343a46a1a8511a78331fa`.
|
||||||
|
It completed **151 passing real HTTP tests, 0 failures, 1 optional zstd
|
||||||
|
skip**. These tests were run on the supplied binary, not on the subsequently
|
||||||
|
modified source. The latest source moves per-domain cache statistics outside
|
||||||
|
HOT store mutexes and lazily allocates analytics samples only after a queue
|
||||||
|
permit is available. Histogram completion now increments one exclusive
|
||||||
|
latency bin instead of up to 11 cumulative bins; both Prometheus exporters
|
||||||
|
convert them to the same cumulative format at scrape time. Static
|
||||||
|
source/SQL/upgrade checks pass; **Rust compile,
|
||||||
|
Rust unit tests and live new-source RPS remain OPEN**. See
|
||||||
|
`docs/PROXY_1KB_COUNTERS_DEV_0.34.0.md` and the separate evidence archive.
|
||||||
|
|
||||||
## DEV 1 KiB reverse-proxy follow-up (October 2026; source only)
|
## DEV 1 KiB reverse-proxy follow-up (October 2026; source only)
|
||||||
|
|
||||||
The supplied **old executable** passed loopback HTTP/1.1 1,040-byte content
|
The supplied **old executable** passed loopback HTTP/1.1 1,040-byte content
|
||||||
|
|||||||
@@ -6,6 +6,24 @@ controllable test **upstream**, used to verify streaming, non-replayable writes,
|
|||||||
headers, cache policy and WebSocket frames. Performance measurements use nginx
|
headers, cache policy and WebSocket frames. Performance measurements use nginx
|
||||||
as the shared upstream instead of Python.
|
as the shared upstream instead of Python.
|
||||||
|
|
||||||
|
## DEV 1 KiB histogram writes (source candidate)
|
||||||
|
|
||||||
|
`VhostMetrics::complete` now writes one exclusive histogram bin for each
|
||||||
|
completed exchange instead of up to 11 cumulative atomics. Both Prometheus
|
||||||
|
exporters reconstruct `le` and `+Inf` at scrape time. Unit tests cover boundary
|
||||||
|
semantics. This change has not been compiled or benchmarked: see
|
||||||
|
`docs/PROXY_1KB_COUNTERS_DEV_0.34.0.md`.
|
||||||
|
|
||||||
|
## DEV 1 KiB cache counter follow-up
|
||||||
|
|
||||||
|
See `docs/PROXY_1KB_COUNTERS_DEV_0.34.0.md`. Cache domain metrics no longer
|
||||||
|
lock the HOT store on BYPASS; analytics cloning is deferred until the queue
|
||||||
|
has capacity. The new **source** remains uncompiled in the editing environment.
|
||||||
|
For quick 1,040-byte loopback diagnostics with ApacheBench (not a production
|
||||||
|
acceptance benchmark), use `python3 scripts/performance/ab_1kb_dev.py --help`.
|
||||||
|
The authoritative long-run comparison remains `benchmark.py` with pinned
|
||||||
|
CPU sets, an explicitly equal upstream idle pool and multiple `wrk` repeats.
|
||||||
|
|
||||||
## DEV 1 KiB follow-up on 0.34.0 (newer source)
|
## DEV 1 KiB follow-up on 0.34.0 (newer source)
|
||||||
|
|
||||||
See `docs/PROXY_1KB_HOTPATH_DEV_0.34.0.md`. The previous executable's 4-vs-64
|
See `docs/PROXY_1KB_HOTPATH_DEV_0.34.0.md`. The previous executable's 4-vs-64
|
||||||
|
|||||||
Executable
+108
@@ -0,0 +1,108 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Short, same-host ab diagnostic for 1,040-byte HTTP/1 proxy responses.
|
||||||
|
|
||||||
|
Requires an existing executable and nginx; keeps the same explicitly supplied
|
||||||
|
idle-pool limit across variants. Not a substitute for wrk/CPU-isolated acceptance.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
from common import Fixture, http, write_json
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument('--binary', action='append', required=True, metavar='LABEL=/abs/path')
|
||||||
|
parser.add_argument('--nginx', default='/usr/sbin/nginx', type=Path)
|
||||||
|
parser.add_argument('--ab', default='/usr/bin/ab', type=Path)
|
||||||
|
parser.add_argument('--out', required=True, type=Path)
|
||||||
|
parser.add_argument('--concurrency', default='16,64')
|
||||||
|
parser.add_argument('--duration', type=int, default=4)
|
||||||
|
parser.add_argument('--repeats', type=int, default=2)
|
||||||
|
parser.add_argument('--workers', type=int, default=4)
|
||||||
|
parser.add_argument('--max-idle', type=int, default=64)
|
||||||
|
args = parser.parse_args()
|
||||||
|
binaries = dict(item.split('=', 1) for item in args.binary)
|
||||||
|
if len(binaries) != len(args.binary) or not all(Path(x).is_absolute() for x in binaries.values()):
|
||||||
|
parser.error('binary labels must be distinct and paths must be absolute')
|
||||||
|
if args.duration < 2 or args.repeats < 1:
|
||||||
|
parser.error('duration >=2 seconds and repeats >=1 required')
|
||||||
|
concurrency = [int(v) for v in args.concurrency.split(',')]
|
||||||
|
args.out.mkdir(parents=True, exist_ok=True)
|
||||||
|
records = []
|
||||||
|
write_json(args.out / 'metadata.json', {
|
||||||
|
'simulation': False, 'binary_labels': list(binaries), 'nginx': str(args.nginx),
|
||||||
|
'max_idle_for_every_rust_variant': args.max_idle, 'duration_s': args.duration,
|
||||||
|
'repeats': args.repeats, 'concurrency': concurrency, 'workers': args.workers,
|
||||||
|
'limitations': 'same-host shared CPU and closed-loop ab; not build-level before/after of uncompiled source'
|
||||||
|
})
|
||||||
|
with Fixture(args.out, workers=args.workers) as fixture:
|
||||||
|
nginx = fixture.nginx(args.nginx, frontend=True)
|
||||||
|
instances = {name: fixture.rust(Path(binary), name, max_idle=args.max_idle)
|
||||||
|
for name, binary in binaries.items()}
|
||||||
|
for name, instance in instances.items():
|
||||||
|
instance.login()
|
||||||
|
print('Waiting for quiescence:', name, flush=True)
|
||||||
|
instance.wait_quiescent(timeout=150, quiet_seconds=3)
|
||||||
|
sources = {**{name: r.reverse_port for name, r in instances.items()}, 'nginx': nginx['reverse_port']}
|
||||||
|
payload = (fixture.site / 'small.html').read_bytes()
|
||||||
|
for name, port in sources.items():
|
||||||
|
code, _, body = http(port, '/small.html', headers={'Accept-Encoding': 'identity'})
|
||||||
|
if code != 200 or body != payload:
|
||||||
|
raise AssertionError(f'{name} precheck failed: {code}, {len(body)} bytes')
|
||||||
|
write_json(args.out / 'payload-check.json', {
|
||||||
|
'status': 200, 'bytes': len(payload), 'sha256': hashlib.sha256(payload).hexdigest(),
|
||||||
|
'all_variants_verified': True
|
||||||
|
})
|
||||||
|
labels = list(sources)
|
||||||
|
for c in concurrency:
|
||||||
|
for repeat in range(args.repeats):
|
||||||
|
order = labels[repeat % len(labels):] + labels[:repeat % len(labels)]
|
||||||
|
if repeat % 2:
|
||||||
|
order.reverse()
|
||||||
|
for name in order:
|
||||||
|
port = sources[name]
|
||||||
|
cmd = [str(args.ab), '-k', '-t', str(args.duration), '-n', '1000000', '-c', str(c),
|
||||||
|
'-H', 'Accept-Encoding: identity', f'http://127.0.0.1:{port}/small.html']
|
||||||
|
prefix = f'c{c}-rep{repeat+1}-{name}'
|
||||||
|
subprocess.run([str(args.ab), '-k', '-n', '256', '-c', str(c),
|
||||||
|
f'http://127.0.0.1:{port}/small.html'],
|
||||||
|
capture_output=True, timeout=25, check=True)
|
||||||
|
p = subprocess.run(cmd, capture_output=True, text=True, timeout=args.duration + 45)
|
||||||
|
raw = p.stdout + '\n' + p.stderr
|
||||||
|
(args.out / f'{prefix}.txt').write_text(raw)
|
||||||
|
if p.returncode:
|
||||||
|
raise RuntimeError(f'{prefix} ab failed: {p.returncode} {p.stderr}')
|
||||||
|
def field(label):
|
||||||
|
m = re.search(r'^' + re.escape(label) + r':\s*(\d+(?:\.\d+)?)', raw, re.M)
|
||||||
|
return float(m.group(1)) if m else None
|
||||||
|
# ApacheBench tracks both network failures and bad body lengths.
|
||||||
|
if not re.search(r'^Failed requests:\s+0\s*$', raw, re.M):
|
||||||
|
raise RuntimeError(f'{prefix} failed requests: {raw[-1600:]}')
|
||||||
|
if not re.search(r'^Non-2xx responses:\s*0\s*$', raw, re.M) and 'Non-2xx responses:' in raw:
|
||||||
|
raise RuntimeError(f'{prefix} received non-2xx')
|
||||||
|
code, _, body = http(port, '/small.html', headers={'Accept-Encoding': 'identity'})
|
||||||
|
if code != 200 or body != payload:
|
||||||
|
raise AssertionError(f'{prefix} postcheck failed')
|
||||||
|
rps = field('Requests per second')
|
||||||
|
if rps is None:
|
||||||
|
raise RuntimeError(f'{prefix} missing RPS')
|
||||||
|
sample = {'variant': name, 'concurrency': c, 'repeat': repeat+1,
|
||||||
|
'rps': rps, 'completed': field('Complete requests'), 'failed': 0,
|
||||||
|
'expected_sha256': hashlib.sha256(payload).hexdigest(), 'status': 200}
|
||||||
|
records.append(sample)
|
||||||
|
write_json(args.out / 'samples.json', records)
|
||||||
|
print(sample, flush=True)
|
||||||
|
write_json(args.out / 'complete.json', {'complete': True, 'samples': len(records)})
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
@@ -12,17 +12,21 @@ assert 'headers: Arc<HeaderMap>' in entry
|
|||||||
assert 'Arc::clone(&entry.headers)' in hot
|
assert 'Arc::clone(&entry.headers)' in hot
|
||||||
assert 'drop(store);' in hot
|
assert 'drop(store);' in hot
|
||||||
assert hot.index('drop(store);\n\n let mut response') < hot.index('headers.as_ref().clone()')
|
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 'self.domain_stats.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 'store.lru.remove(&old_sequence)' in hot and 'store.lru.insert(sequence, storage_key)' in hot
|
||||||
assert '.sweep()' not in hot
|
assert '.sweep()' not in hot
|
||||||
assert 'fn sweep(' not in entry
|
assert 'fn sweep(' not in entry
|
||||||
assert 'fn saved_body_hit(' in entry
|
stats = read('src/cache/stats.rs')
|
||||||
assert 'domains: BTreeMap<String, BTreeMap<String, DomainCounters>>' in entry
|
assert 'fn saved_body_hit(' in stats
|
||||||
|
assert 'const DOMAIN_STATS_LIMIT: usize = 4096' in stats
|
||||||
|
assert 'ArcSwap<PublishedDomains>' in stats
|
||||||
|
assert 'fn record_reason(&self, reason:' in stats
|
||||||
|
assert 'self.store.lock()' not in stats
|
||||||
assert 'if self.ban_rule_count.load(Ordering::Acquire) == 0' in inv
|
assert 'if self.ban_rule_count.load(Ordering::Acquire) == 0' in inv
|
||||||
assert 'bans.iter().any(|rule| rule.matches_hot' 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 'fn matching_hot_key(' in model and 'entry.vary.is_empty()' in model
|
||||||
assert 'store.domain_count = 0' in dash
|
assert 'self.domain_stats.clear()' in dash
|
||||||
assert 'c.reasons.get_mut(reason)' in dash
|
assert 'self.domain_stats.record(scope, domain, outcome, reason);' in dash
|
||||||
assert 'include!("tests/hot_hit_034_tests.rs")' in read('src/cache/tests.rs')
|
assert 'include!("tests/hot_hit_034_tests.rs")' in read('src/cache/tests.rs')
|
||||||
for path in (ROOT / 'src').rglob('*.rs'):
|
for path in (ROOT / 'src').rglob('*.rs'):
|
||||||
if '/tests/' not in path.as_posix() and path.name != 'tests.rs':
|
if '/tests/' not in path.as_posix() and path.name != 'tests.rs':
|
||||||
|
|||||||
@@ -107,6 +107,19 @@ assert "if !headers.contains_key(http::header::COOKIE)" in source("src/proxy/hea
|
|||||||
assert "if !headers.contains_key(http::header::SET_COOKIE)" in source("src/proxy/headers.rs")
|
assert "if !headers.contains_key(http::header::SET_COOKIE)" in source("src/proxy/headers.rs")
|
||||||
assert 'version = "0.34.0"' in source("Cargo.toml")
|
assert 'version = "0.34.0"' in source("Cargo.toml")
|
||||||
|
|
||||||
|
# Full exchange duration histogram still exports cumulative Prometheus series,
|
||||||
|
# but only one exclusive bucket is updated per request; per-vhost and Docker
|
||||||
|
# exporters must reconstruct bucket counts from exactly the same interface.
|
||||||
|
histogram = source("src/metrics/vhost.rs")
|
||||||
|
exporter = source("src/prometheus/docker_misc.rs")
|
||||||
|
assert "let bucket = DURATION_BUCKETS_US.partition_point" in histogram
|
||||||
|
assert "self.buckets[bucket].fetch_add(1, Ordering::Relaxed);" in histogram
|
||||||
|
assert "pub fn duration_histogram(&self)" in histogram
|
||||||
|
assert exporter.count("let (buckets, count) = m.duration_histogram();") == 2
|
||||||
|
assert "rust_proxy_vhost_exchange_duration_seconds_bucket" in exporter
|
||||||
|
assert "rust_proxy_docker_vhost_exchange_duration_seconds_bucket" in exporter
|
||||||
|
assert "exclusive_bins_reconstruct_exact_prometheus_cumulative_boundaries" in source("src/metrics/vhost/tests.rs")
|
||||||
|
|
||||||
for file in (ROOT / "src").rglob("*.rs"):
|
for file in (ROOT / "src").rglob("*.rs"):
|
||||||
if "/tests/" in file.as_posix() or file.name == "tests.rs":
|
if "/tests/" in file.as_posix() or file.name == "tests.rs":
|
||||||
continue
|
continue
|
||||||
|
|||||||
Vendored
+11
-32
@@ -176,8 +176,7 @@ impl Cache {
|
|||||||
if let Some(revision) = revision {
|
if let Some(revision) = revision {
|
||||||
store.revision = revision;
|
store.revision = revision;
|
||||||
self.revision.store(revision, Ordering::Relaxed);
|
self.revision.store(revision, Ordering::Relaxed);
|
||||||
store.domains.clear();
|
self.domain_stats.clear();
|
||||||
store.domain_count = 0;
|
|
||||||
}
|
}
|
||||||
drop(store);
|
drop(store);
|
||||||
self.cancel_all_fill_flights();
|
self.cancel_all_fill_flights();
|
||||||
@@ -264,44 +263,24 @@ impl Cache {
|
|||||||
view
|
view
|
||||||
}
|
}
|
||||||
pub fn record(&self, scope: &str, domain: &str, outcome: &str, reason: &'static str) {
|
pub fn record(&self, scope: &str, domain: &str, outcome: &str, reason: &'static str) {
|
||||||
let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner());
|
self.domain_stats.record(scope, domain, outcome, reason);
|
||||||
if let Some(c) = store.domain(scope, domain) {
|
|
||||||
match outcome {
|
|
||||||
"HIT" => c.hits += 1,
|
|
||||||
"MISS" => c.misses += 1,
|
|
||||||
_ => c.bypasses += 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) {
|
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());
|
self.domain_stats.record_reason(scope, domain, reason);
|
||||||
if let Some(c) = store.domain(scope, domain) {
|
|
||||||
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 {
|
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());
|
let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
let revision = store.revision;
|
let revision = store.revision;
|
||||||
let mut groups: BTreeMap<(String, String), DomainView> = store
|
let mut groups: BTreeMap<(String, String), DomainView> = self
|
||||||
.domains
|
.domain_stats
|
||||||
.iter()
|
.snapshot(scope)
|
||||||
.filter(|(vhost, _)| scope.is_none_or(|s| s == vhost.as_str()))
|
.into_iter()
|
||||||
.flat_map(|(vhost, domains)| domains.iter().map(move |(domain, counters)| (vhost, domain, counters)))
|
.map(|((vhost, domain), counters)| {
|
||||||
.map(|(vhost, domain, counters)| {
|
|
||||||
(
|
(
|
||||||
(vhost.clone(), domain.clone()),
|
(vhost.clone(), domain.clone()),
|
||||||
DomainView {
|
DomainView {
|
||||||
vhost: vhost.clone(),
|
vhost,
|
||||||
domain: domain.clone(),
|
domain,
|
||||||
entries: 0,
|
entries: 0,
|
||||||
logical_bytes: 0,
|
logical_bytes: 0,
|
||||||
resident_bytes: 0,
|
resident_bytes: 0,
|
||||||
@@ -313,7 +292,7 @@ impl Cache {
|
|||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
},
|
},
|
||||||
counters: counters.clone(),
|
counters,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
Vendored
+2
@@ -6,6 +6,7 @@ impl Cache {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
})),
|
})),
|
||||||
revision: AtomicU64::new(revision),
|
revision: AtomicU64::new(revision),
|
||||||
|
domain_stats: DomainStatsStore::default(),
|
||||||
cold: Arc::new(Mutex::new(ColdStore::default())),
|
cold: Arc::new(Mutex::new(ColdStore::default())),
|
||||||
tier_settings: Mutex::new(Settings::default()),
|
tier_settings: Mutex::new(Settings::default()),
|
||||||
scope_revisions: Mutex::new(HashMap::new()),
|
scope_revisions: Mutex::new(HashMap::new()),
|
||||||
@@ -57,6 +58,7 @@ impl Cache {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
})),
|
})),
|
||||||
revision: AtomicU64::new(revision),
|
revision: AtomicU64::new(revision),
|
||||||
|
domain_stats: DomainStatsStore::default(),
|
||||||
cold: Arc::new(Mutex::new(ColdStore::default())),
|
cold: Arc::new(Mutex::new(ColdStore::default())),
|
||||||
tier_settings: Mutex::new(settings),
|
tier_settings: Mutex::new(settings),
|
||||||
scope_revisions: Mutex::new(scope_revisions),
|
scope_revisions: Mutex::new(scope_revisions),
|
||||||
|
|||||||
Vendored
+2
-7
@@ -61,7 +61,6 @@ impl Cache {
|
|||||||
store.lru.insert(sequence, storage_key);
|
store.lru.insert(sequence, storage_key);
|
||||||
// Preserve the exact LRU and domain-byte counters, without
|
// Preserve the exact LRU and domain-byte counters, without
|
||||||
// allocating a replacement variant index or String keys.
|
// 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);
|
self.counters.hits.fetch_add(1, Ordering::Relaxed);
|
||||||
drop(store);
|
drop(store);
|
||||||
|
|
||||||
@@ -77,6 +76,7 @@ impl Cache {
|
|||||||
"age",
|
"age",
|
||||||
HeaderValue::from_str(&age.to_string()).expect("integer header"),
|
HeaderValue::from_str(&age.to_string()).expect("integer header"),
|
||||||
);
|
);
|
||||||
|
self.domain_stats.saved_body_hit(&ticket.scope, &ticket.domain, body_bytes);
|
||||||
return Some(Hit {
|
return Some(Hit {
|
||||||
response,
|
response,
|
||||||
body,
|
body,
|
||||||
@@ -274,12 +274,7 @@ impl Cache {
|
|||||||
"age",
|
"age",
|
||||||
HeaderValue::from_str(&age.to_string()).expect("integer header"),
|
HeaderValue::from_str(&age.to_string()).expect("integer header"),
|
||||||
);
|
);
|
||||||
{
|
self.domain_stats.saved_body_hit(&ticket.scope, &ticket.domain, entry.body_bytes);
|
||||||
let mut store = self.store.lock().unwrap_or_else(|e| e.into_inner());
|
|
||||||
if let Some(c) = store.domain(&ticket.scope, &ticket.domain) {
|
|
||||||
c.saved_body_bytes = c.saved_body_bytes.saturating_add(entry.body_bytes as u64);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.counters.hits.fetch_add(1, Ordering::Relaxed);
|
self.counters.hits.fetch_add(1, Ordering::Relaxed);
|
||||||
self.counters.cold_hits.fetch_add(1, Ordering::Relaxed);
|
self.counters.cold_hits.fetch_add(1, Ordering::Relaxed);
|
||||||
self.counters
|
self.counters
|
||||||
|
|||||||
Vendored
+1
-3
@@ -108,9 +108,7 @@ impl Cache {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if count_stored {
|
if count_stored {
|
||||||
if let Some(c) = store.domain(&entry.scope, &entry.domain) {
|
self.domain_stats.stored(&entry.scope, &entry.domain);
|
||||||
c.stored += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
entry.last_access = Instant::now();
|
entry.last_access = Instant::now();
|
||||||
entry.sequence = store.next_sequence();
|
entry.sequence = store.next_sequence();
|
||||||
|
|||||||
Vendored
+1
@@ -5,6 +5,7 @@
|
|||||||
//! policy and streaming-fill code can be reviewed independently.
|
//! policy and streaming-fill code can be reviewed independently.
|
||||||
|
|
||||||
include!("model.rs");
|
include!("model.rs");
|
||||||
|
include!("stats.rs");
|
||||||
include!("lifecycle.rs");
|
include!("lifecycle.rs");
|
||||||
include!("cold.rs");
|
include!("cold.rs");
|
||||||
include!("dashboard.rs");
|
include!("dashboard.rs");
|
||||||
|
|||||||
Vendored
-22
@@ -336,8 +336,6 @@ struct Store {
|
|||||||
control_revision: u64,
|
control_revision: u64,
|
||||||
scope_generations: HashMap<String, u64>,
|
scope_generations: HashMap<String, u64>,
|
||||||
key_generations: HashMap<Key, u64>,
|
key_generations: HashMap<Key, u64>,
|
||||||
domains: BTreeMap<String, BTreeMap<String, DomainCounters>>,
|
|
||||||
domain_count: usize,
|
|
||||||
}
|
}
|
||||||
impl Store {
|
impl Store {
|
||||||
fn index_variant(&mut self, base_key: Key, key: Key) {
|
fn index_variant(&mut self, base_key: Key, key: Key) {
|
||||||
@@ -362,26 +360,6 @@ impl Store {
|
|||||||
}
|
}
|
||||||
Some(entry)
|
Some(entry)
|
||||||
}
|
}
|
||||||
fn domain(&mut self, scope: &str, domain: &str) -> Option<&mut DomainCounters> {
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
self.domain_count += 1;
|
|
||||||
Some(self.domains.entry(scope.to_owned()).or_default()
|
|
||||||
.entry(domain.to_owned()).or_default())
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn next_sequence(&mut self) -> u64 {
|
fn next_sequence(&mut self) -> u64 {
|
||||||
// No practical wrapping, but a wrap must not corrupt LRU bookkeeping.
|
// No practical wrapping, but a wrap must not corrupt LRU bookkeeping.
|
||||||
if self.sequence == u64::MAX {
|
if self.sequence == u64::MAX {
|
||||||
|
|||||||
Vendored
+1
@@ -52,6 +52,7 @@ impl Drop for TransientLeaseInner {
|
|||||||
|
|
||||||
pub struct Cache {
|
pub struct Cache {
|
||||||
store: Arc<Mutex<Store>>,
|
store: Arc<Mutex<Store>>,
|
||||||
|
domain_stats: DomainStatsStore,
|
||||||
revision: AtomicU64,
|
revision: AtomicU64,
|
||||||
cold: Arc<Mutex<ColdStore>>,
|
cold: Arc<Mutex<ColdStore>>,
|
||||||
tier_settings: Mutex<Settings>,
|
tier_settings: Mutex<Settings>,
|
||||||
|
|||||||
Vendored
+187
@@ -0,0 +1,187 @@
|
|||||||
|
// Lock-free read path for bounded, per-domain cache dashboard counters.
|
||||||
|
//
|
||||||
|
// Cache-disabled proxy requests still contribute to BYPASS and reason totals,
|
||||||
|
// but must never lock the HOT object/LRU store. Published domain/reason indexes
|
||||||
|
// are immutable; individual counters are atomics. Only first sight of a new
|
||||||
|
// domain/reason acquires the registration mutex.
|
||||||
|
|
||||||
|
use arc_swap::ArcSwap;
|
||||||
|
|
||||||
|
const DOMAIN_STATS_LIMIT: usize = 4096;
|
||||||
|
const FAST_REASONS: [&str; 4] = ["cache_disabled", "cache_hit", "cache_miss", "request_cookie"];
|
||||||
|
|
||||||
|
type ReasonIndex = BTreeMap<&'static str, Arc<AtomicU64>>;
|
||||||
|
type DomainIndex = BTreeMap<String, BTreeMap<String, Arc<DomainStats>>>;
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
struct PublishedDomains {
|
||||||
|
index: DomainIndex,
|
||||||
|
count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DomainStats {
|
||||||
|
hits: AtomicU64,
|
||||||
|
misses: AtomicU64,
|
||||||
|
bypasses: AtomicU64,
|
||||||
|
stored: AtomicU64,
|
||||||
|
saved_body_bytes: AtomicU64,
|
||||||
|
fast_reasons: [AtomicU64; FAST_REASONS.len()],
|
||||||
|
reasons: ArcSwap<ReasonIndex>,
|
||||||
|
register_reason: Mutex<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DomainStats {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
hits: AtomicU64::new(0),
|
||||||
|
misses: AtomicU64::new(0),
|
||||||
|
bypasses: AtomicU64::new(0),
|
||||||
|
stored: AtomicU64::new(0),
|
||||||
|
saved_body_bytes: AtomicU64::new(0),
|
||||||
|
fast_reasons: std::array::from_fn(|_| AtomicU64::new(0)),
|
||||||
|
reasons: ArcSwap::from_pointee(ReasonIndex::new()),
|
||||||
|
register_reason: Mutex::new(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DomainStats {
|
||||||
|
fn record(&self, outcome: &str, reason: &'static str) {
|
||||||
|
match outcome {
|
||||||
|
"HIT" => &self.hits,
|
||||||
|
"MISS" => &self.misses,
|
||||||
|
_ => &self.bypasses,
|
||||||
|
}.fetch_add(1, Ordering::Relaxed);
|
||||||
|
self.record_reason(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_reason(&self, reason: &'static str) {
|
||||||
|
// Cache OFF is the highest-volume case; avoid reason map lookup and
|
||||||
|
// any shared lock/allocator activity for its dashboard reason.
|
||||||
|
if let Some(index) = FAST_REASONS.iter().position(|label| *label == reason) {
|
||||||
|
self.fast_reasons[index].fetch_add(1, Ordering::Relaxed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let published = self.reasons.load();
|
||||||
|
if let Some(counter) = published.get(reason) {
|
||||||
|
counter.fetch_add(1, Ordering::Relaxed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drop(published);
|
||||||
|
let _guard = self.register_reason.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
let published = self.reasons.load_full();
|
||||||
|
if let Some(counter) = published.get(reason) {
|
||||||
|
counter.fetch_add(1, Ordering::Relaxed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut next = (*published).clone();
|
||||||
|
next.insert(reason, Arc::new(AtomicU64::new(1)));
|
||||||
|
self.reasons.store(Arc::new(next));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snapshot(&self) -> DomainCounters {
|
||||||
|
let load = |value: &AtomicU64| value.load(Ordering::Relaxed);
|
||||||
|
let mut reasons = BTreeMap::new();
|
||||||
|
for (label, counter) in FAST_REASONS.iter().zip(&self.fast_reasons) {
|
||||||
|
let value = load(counter);
|
||||||
|
if value > 0 {
|
||||||
|
reasons.insert((*label).to_owned(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (label, counter) in self.reasons.load().iter() {
|
||||||
|
reasons.insert((*label).to_owned(), load(counter));
|
||||||
|
}
|
||||||
|
DomainCounters {
|
||||||
|
hits: load(&self.hits),
|
||||||
|
misses: load(&self.misses),
|
||||||
|
bypasses: load(&self.bypasses),
|
||||||
|
stored: load(&self.stored),
|
||||||
|
saved_body_bytes: load(&self.saved_body_bytes),
|
||||||
|
reasons,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DomainStatsStore {
|
||||||
|
domains: ArcSwap<PublishedDomains>,
|
||||||
|
register: Mutex<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DomainStatsStore {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
domains: ArcSwap::from_pointee(PublishedDomains::default()),
|
||||||
|
register: Mutex::new(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DomainStatsStore {
|
||||||
|
fn with(&self, scope: &str, domain: &str, action: impl FnOnce(&DomainStats)) {
|
||||||
|
let published = self.domains.load();
|
||||||
|
if let Some(counters) = published.index.get(scope).and_then(|hosts| hosts.get(domain)) {
|
||||||
|
action(counters);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drop(published);
|
||||||
|
// Rare path: register only 4096 distinct (scope, domain) pairs. The
|
||||||
|
// published map is cloned on first insertion, never on a request hit.
|
||||||
|
let _guard = self.register.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
let published = self.domains.load_full();
|
||||||
|
if let Some(counters) = published.index.get(scope).and_then(|hosts| hosts.get(domain)) {
|
||||||
|
action(counters);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if published.count >= DOMAIN_STATS_LIMIT {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut next = (*published).clone();
|
||||||
|
let counters = Arc::new(DomainStats::default());
|
||||||
|
next.index.entry(scope.to_owned()).or_default()
|
||||||
|
.insert(domain.to_owned(), Arc::clone(&counters));
|
||||||
|
next.count += 1;
|
||||||
|
self.domains.store(Arc::new(next));
|
||||||
|
action(&counters);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record(&self, scope: &str, domain: &str, outcome: &str, reason: &'static str) {
|
||||||
|
self.with(scope, domain, |counters| counters.record(outcome, reason));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_reason(&self, scope: &str, domain: &str, reason: &'static str) {
|
||||||
|
self.with(scope, domain, |counters| counters.record_reason(reason));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stored(&self, scope: &str, domain: &str) {
|
||||||
|
self.with(scope, domain, |counters| {
|
||||||
|
counters.stored.fetch_add(1, Ordering::Relaxed);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn saved_body_hit(&self, scope: &str, domain: &str, bytes: usize) {
|
||||||
|
// COLD entries loaded from persisted metadata may be served before a
|
||||||
|
// domain is registered. Preserve first-hit byte accounting as well.
|
||||||
|
self.with(scope, domain, |counters| {
|
||||||
|
counters.saved_body_bytes.fetch_add(bytes as u64, Ordering::Relaxed);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear(&self) {
|
||||||
|
let _guard = self.register.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
self.domains.store(Arc::new(PublishedDomains::default()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snapshot(&self, scope: Option<&str>) -> BTreeMap<(String, String), DomainCounters> {
|
||||||
|
let published = self.domains.load();
|
||||||
|
let mut counters = BTreeMap::new();
|
||||||
|
for (vhost, domains) in &published.index {
|
||||||
|
if !scope.is_none_or(|selected| selected == vhost) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (domain, metric) in domains {
|
||||||
|
counters.insert((vhost.clone(), domain.clone()), metric.snapshot());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
counters
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+51
@@ -215,6 +215,57 @@ mod dashboard_tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cache_domain_metrics_do_not_lock_hot_store_and_preserve_reasons() {
|
||||||
|
let cache = Cache::new(7);
|
||||||
|
// A BYPASS must be recordable even while the HOT/LRU mutex is held.
|
||||||
|
let store = cache.store.lock().unwrap();
|
||||||
|
cache.record("alpha", "a.example", "BYPASS", "cache_disabled");
|
||||||
|
cache.record_reason("alpha", "a.example", "background_refresh_failure");
|
||||||
|
drop(store);
|
||||||
|
let workers = 8;
|
||||||
|
let each = 2000;
|
||||||
|
let mut tasks = Vec::new();
|
||||||
|
for _ in 0..workers {
|
||||||
|
let cache = Arc::clone(&cache);
|
||||||
|
tasks.push(std::thread::spawn(move || {
|
||||||
|
for _ in 0..each {
|
||||||
|
cache.record("alpha", "a.example", "BYPASS", "cache_disabled");
|
||||||
|
cache.record("alpha", "a.example", "HIT", "cache_hit");
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
for task in tasks {
|
||||||
|
task.join().expect("domain counter worker");
|
||||||
|
}
|
||||||
|
let scoped = cache.dashboard(Some("alpha"), 0, 1);
|
||||||
|
assert_eq!(scoped.domains.len(), 1);
|
||||||
|
let counters = &scoped.domains[0].counters;
|
||||||
|
assert_eq!(counters.bypasses, 1 + workers * each);
|
||||||
|
assert_eq!(counters.hits, workers * each);
|
||||||
|
assert_eq!(counters.reasons["cache_disabled"], 1 + workers * each);
|
||||||
|
assert_eq!(counters.reasons["cache_hit"], workers * each);
|
||||||
|
assert_eq!(counters.reasons["background_refresh_failure"], 1);
|
||||||
|
assert_eq!(cache.purge_scope("alpha"), 0);
|
||||||
|
assert_eq!(cache.dashboard(Some("alpha"), 0, 1).domains[0].counters.hits, workers * each);
|
||||||
|
cache.purge(Some(8));
|
||||||
|
assert!(cache.dashboard(None, 0, 1).domains.is_empty());
|
||||||
|
cache.record("alpha", "a.example", "MISS", "cache_miss");
|
||||||
|
let after_reset = cache.dashboard(Some("alpha"), 0, 1);
|
||||||
|
assert_eq!(after_reset.domains[0].counters.misses, 1);
|
||||||
|
assert_eq!(after_reset.domains[0].counters.hits, 0);
|
||||||
|
assert_eq!(after_reset.statistics_revision, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_hit_saved_bytes_registers_domain_even_without_prior_record() {
|
||||||
|
let cache = Cache::new(7);
|
||||||
|
cache.domain_stats.saved_body_hit("cold", "restored.example", 1040);
|
||||||
|
let result = cache.dashboard(Some("cold"), 0, 1);
|
||||||
|
assert_eq!(result.domains.len(), 1);
|
||||||
|
assert_eq!(result.domains[0].counters.saved_body_bytes, 1040);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn per_domain_counter_cardinality_is_bounded() {
|
fn per_domain_counter_cardinality_is_bounded() {
|
||||||
let cache = Cache::new(7);
|
let cache = Cache::new(7);
|
||||||
|
|||||||
+17
-4
@@ -62,12 +62,25 @@ impl VhostMetrics {
|
|||||||
Err(observed) => current = observed,
|
Err(observed) => current = observed,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (index, threshold) in DURATION_BUCKETS_US.iter().enumerate() {
|
// Write exactly one exclusive histogram bin per exchange. Prometheus
|
||||||
if micros <= *threshold {
|
// cumulative le=... buckets are reconstructed only when scraped.
|
||||||
self.buckets[index].fetch_add(1, Ordering::Relaxed);
|
// This removes up to 11 contended atomics for short 1 KiB responses.
|
||||||
|
let bucket = DURATION_BUCKETS_US.partition_point(|threshold| micros > *threshold);
|
||||||
|
self.buckets[bucket].fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A scrape-consistent cumulative representation of exclusive bins. The
|
||||||
|
/// sum of atomic snapshots is monotonic and the final count includes +Inf.
|
||||||
|
pub fn duration_histogram(&self) -> ([u64; DURATION_BUCKETS_US.len()], u64) {
|
||||||
|
let mut cumulative = [0u64; DURATION_BUCKETS_US.len()];
|
||||||
|
let mut count = 0u64;
|
||||||
|
for (index, bin) in self.buckets.iter().enumerate() {
|
||||||
|
count = count.saturating_add(bin.load(Ordering::Relaxed));
|
||||||
|
if index < DURATION_BUCKETS_US.len() {
|
||||||
|
cumulative[index] = count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.buckets[DURATION_BUCKETS_US.len()].fetch_add(1, Ordering::Relaxed);
|
(cumulative, count)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view(&self) -> VhostMetricsView {
|
pub fn view(&self) -> VhostMetricsView {
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
mod vhost_tests {
|
mod vhost_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn exclusive_bins_reconstruct_exact_prometheus_cumulative_boundaries() {
|
||||||
|
let m = VhostMetrics::default();
|
||||||
|
m.active.store(5, Ordering::Relaxed);
|
||||||
|
for micros in [1, 5_000, 5_001, 25_000, 5_000_001] {
|
||||||
|
m.complete(200, false, std::time::Duration::from_micros(micros), 0, 1040);
|
||||||
|
}
|
||||||
|
let (cumulative, total) = m.duration_histogram();
|
||||||
|
assert_eq!(total, 5);
|
||||||
|
assert_eq!(cumulative, [2, 3, 4, 4, 4, 4, 4, 4, 4, 4]);
|
||||||
|
assert_eq!(n(&m.buckets[0]), 2);
|
||||||
|
assert_eq!(n(&m.buckets[1]), 1);
|
||||||
|
assert_eq!(n(&m.buckets[2]), 1);
|
||||||
|
assert_eq!(n(&m.buckets[10]), 1);
|
||||||
|
assert_eq!(n(&m.completed), 5);
|
||||||
|
assert_eq!(n(&m.active), 0);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn streaming_completion_and_histogram() {
|
fn streaming_completion_and_histogram() {
|
||||||
let m = VhostMetrics::default();
|
let m = VhostMetrics::default();
|
||||||
@@ -10,7 +28,12 @@ mod vhost_tests {
|
|||||||
assert_eq!(n(&m.status[4]), 1);
|
assert_eq!(n(&m.status[4]), 1);
|
||||||
assert_eq!(n(&m.buckets[1]), 0);
|
assert_eq!(n(&m.buckets[1]), 0);
|
||||||
assert_eq!(n(&m.buckets[2]), 1);
|
assert_eq!(n(&m.buckets[2]), 1);
|
||||||
assert_eq!(n(&m.buckets[10]), 1);
|
assert_eq!(n(&m.buckets[10]), 0); // exclusive +Inf bin
|
||||||
|
let (buckets, count) = m.duration_histogram();
|
||||||
|
assert_eq!(buckets[1], 0);
|
||||||
|
assert_eq!(buckets[2], 1);
|
||||||
|
assert_eq!(buckets[9], 1);
|
||||||
|
assert_eq!(count, 1);
|
||||||
let view = m.view();
|
let view = m.view();
|
||||||
assert_eq!(view.requests, 0);
|
assert_eq!(view.requests, 0);
|
||||||
assert_eq!(view.completed, 1);
|
assert_eq!(view.completed, 1);
|
||||||
|
|||||||
@@ -240,16 +240,9 @@ fn render_histograms(
|
|||||||
"rust_proxy_vhost_exchange_duration_seconds_sum{{{l}}} {:.6}",
|
"rust_proxy_vhost_exchange_duration_seconds_sum{{{l}}} {:.6}",
|
||||||
m.duration_us.load(Ordering::Relaxed) as f64 / 1e6
|
m.duration_us.load(Ordering::Relaxed) as f64 / 1e6
|
||||||
);
|
);
|
||||||
// Read the cumulative last bucket once for +Inf and count.
|
let (buckets, count) = m.duration_histogram();
|
||||||
let count =
|
|
||||||
m.buckets[crate::metrics::DURATION_BUCKETS_US.len()].load(Ordering::Relaxed);
|
|
||||||
let mut previous = 0;
|
|
||||||
for (i, threshold) in crate::metrics::DURATION_BUCKETS_US.iter().enumerate() {
|
for (i, threshold) in crate::metrics::DURATION_BUCKETS_US.iter().enumerate() {
|
||||||
let value = m.buckets[i]
|
let value = buckets[i];
|
||||||
.load(Ordering::Relaxed)
|
|
||||||
.max(previous)
|
|
||||||
.min(count);
|
|
||||||
previous = value;
|
|
||||||
let _ = writeln!(
|
let _ = writeln!(
|
||||||
out,
|
out,
|
||||||
"rust_proxy_vhost_exchange_duration_seconds_bucket{{{l},le=\"{}\"}} {value}",
|
"rust_proxy_vhost_exchange_duration_seconds_bucket{{{l},le=\"{}\"}} {value}",
|
||||||
@@ -275,15 +268,9 @@ fn render_histograms(
|
|||||||
"rust_proxy_docker_vhost_exchange_duration_seconds_sum{{{l}}} {:.6}",
|
"rust_proxy_docker_vhost_exchange_duration_seconds_sum{{{l}}} {:.6}",
|
||||||
m.duration_us.load(Ordering::Relaxed) as f64 / 1e6
|
m.duration_us.load(Ordering::Relaxed) as f64 / 1e6
|
||||||
);
|
);
|
||||||
let count =
|
let (buckets, count) = m.duration_histogram();
|
||||||
m.buckets[crate::metrics::DURATION_BUCKETS_US.len()].load(Ordering::Relaxed);
|
|
||||||
let mut previous = 0;
|
|
||||||
for (i, threshold) in crate::metrics::DURATION_BUCKETS_US.iter().enumerate() {
|
for (i, threshold) in crate::metrics::DURATION_BUCKETS_US.iter().enumerate() {
|
||||||
let value = m.buckets[i]
|
let value = buckets[i];
|
||||||
.load(Ordering::Relaxed)
|
|
||||||
.max(previous)
|
|
||||||
.min(count);
|
|
||||||
previous = value;
|
|
||||||
let _ = writeln!(
|
let _ = writeln!(
|
||||||
out,
|
out,
|
||||||
"rust_proxy_docker_vhost_exchange_duration_seconds_bucket{{{l},le=\"{}\"}} {value}",
|
"rust_proxy_docker_vhost_exchange_duration_seconds_bucket{{{l},le=\"{}\"}} {value}",
|
||||||
|
|||||||
+3
-3
@@ -218,9 +218,9 @@ impl Drop for Exchange {
|
|||||||
} else {
|
} else {
|
||||||
"complete"
|
"complete"
|
||||||
};
|
};
|
||||||
if let Some(vhost) = self.vhost.clone() {
|
if let Some(vhost) = self.vhost.as_ref() {
|
||||||
self.traffic.push(crate::traffic::Sample {
|
self.traffic.push_lazy(|| crate::traffic::Sample {
|
||||||
vhost,
|
vhost: vhost.clone(),
|
||||||
route: route.clone(),
|
route: route.clone(),
|
||||||
method: self.method.clone(),
|
method: self.method.clone(),
|
||||||
path: self.path.clone(),
|
path: self.path.clone(),
|
||||||
|
|||||||
+12
-2
@@ -243,8 +243,18 @@ impl Analytics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn push(&self, sample: Sample) {
|
pub fn push(&self, sample: Sample) {
|
||||||
if self.tx.try_send(sample).is_err() {
|
self.push_lazy(|| sample);
|
||||||
self.dropped.fetch_add(1, Ordering::Relaxed);
|
}
|
||||||
|
|
||||||
|
/// Acquire queue capacity before constructing owned analytics fields.
|
||||||
|
/// Overflow accounting is identical to `push`, and the queue remains
|
||||||
|
/// bounded. In saturated traffic the rejected sample allocates nothing.
|
||||||
|
pub fn push_lazy(&self, make_sample: impl FnOnce() -> Sample) {
|
||||||
|
match self.tx.try_reserve() {
|
||||||
|
Ok(permit) => permit.send(make_sample()),
|
||||||
|
Err(_) => {
|
||||||
|
self.dropped.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,28 @@ mod tests {
|
|||||||
assert_eq!(view.client_ips[0].count, 1);
|
assert_eq!(view.client_ips[0].count, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn saturated_analytics_queue_does_not_build_discarded_samples() {
|
||||||
|
let analytics = Analytics::start(CancellationToken::new());
|
||||||
|
let made = AtomicU64::new(0);
|
||||||
|
// No .await until the queue is filled: on this current-thread test
|
||||||
|
// runtime the consumer has not removed a sample in the background.
|
||||||
|
for _ in 0..(QUEUE_CAPACITY + 5) {
|
||||||
|
analytics.push_lazy(|| {
|
||||||
|
made.fetch_add(1, Ordering::Relaxed);
|
||||||
|
Sample {
|
||||||
|
vhost: "site".into(), route: None, method: "GET".into(),
|
||||||
|
path: "/".into(), client_ip: None, status: 200,
|
||||||
|
duration_ms: 1, request_bytes: 0, response_bytes: 1040,
|
||||||
|
cache: "BYPASS", source: "reverse_proxy",
|
||||||
|
user_agent: None, country: None,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
assert_eq!(made.load(Ordering::Relaxed), QUEUE_CAPACITY as u64);
|
||||||
|
assert_eq!(analytics.dropped.load(Ordering::Relaxed), 5);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn prometheus_dimensions_are_fixed_cardinality() {
|
fn prometheus_dimensions_are_fixed_cardinality() {
|
||||||
assert_eq!(method_family("GET"), "GET");
|
assert_eq!(method_family("GET"), "GET");
|
||||||
|
|||||||
Reference in new issue
Block a user