v0.34.0_1

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-17 20:34:13 +02:00
1 parent 9b3d6731cc
commit 00f29f92c8
68 files changed
+2599 -513

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes. Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes.
The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for
compatibility. Current source version: 0.33.8. compatibility. Current source version: 0.34.0.
## Delivery ## Delivery
+50
View File
@@ -3,6 +3,56 @@
> Operator-facing release history. Historical engineering notes are kept in > Operator-facing release history. Historical engineering notes are kept in
> [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md). > [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md).
## 0.34.0
- Optimize reverse-proxy hot path with shared immutable compiled route handles,
reusing the observer's host/location selection in forwarding without changing
ACL, rate, canonical authority or protocol enforcement.
- Avoid wildcard allocations for exact-host hits; preserve host precedence,
listener bindings, regex and internal named-location fallback behavior.
- Add allocation-free, scheduler-lock-free single-backend LB selection for
non-cookie pools. Keep health/drain/capacity admission, retry and lease
lifetime; preserve multi-backend algorithms and cookie-affinity semantics.
- Make healthy-backend selection lock-free using a synchronized atomic status
tag; share compiled health/passive policies between leases rather than
deep-cloning their strings per request. Preserve recovery state machine.
- Prepare error-fallback and retry request headers only when the operation is
configured/eligible. Keep error-response interception and safe-method replay.
- Replace exchange source, cache and transform telemetry mutexes with atomic
tags without changing log/event field names or tri-state flags. Do not clone
live-stream log entries when there are no subscribers and move terminal
exchange fields into the final log record; history and sinks stay.
- Add routing/health/LB/telemetry regression cases and source-only guardrails.
No new configuration fields, migrations, LB switches, or changed keepalive
defaults. Actual Rust compilation and benchmark are required before rollout.
- Separate global cache representation compatibility from per-vhost scopes.
Keep HOT and COLD entries across vhost keepalive/recycle/LB diagnostics saves;
invalidate only modified content-bearing scopes and cancel their in-flight fills.
- Persist a content fingerprint alongside COLD entries so interrupted scoped
purges cannot resurrect old responses after restart; retain COLD format v3.
- Remove per-frame `Exchange.backend` mutex acquisition from response body
metering by retaining the final backend lease with the body; request metering,
lease lifetime, counters and error handling retain their semantics.
- Guard cache-ticket creation against in-flight requests using an obsolete
configuration snapshot after a successful save, without evicting unchanged
cache entries.
- Add regression tests for operational saves, scoped content changes and COLD
restart compatibility. No LB UI changes or new database migration.
- Add `sendfile: off | auto` to Web Service configuration and Delivery editor.
For eligible Linux cleartext HTTP/1.1 GET >=64 KiB, transfer already-authorized
static file descriptors using native `sendfile(2)` behind a bounded connection
bridge. Keep normal streaming for TLS/H2/H3, range, transforms, compressed
bodies, active object cache and other operating systems. Disabled by default.
- Keep the kernel transport switch outside global cache identity; toggling it
must not invalidate HOT or COLD. Include native transport/fallback regression
test cases and panel save/reload tests (Rust tests still need execution).
- Include an explicitly hypothetical Amdahl model of nginx parity using the
existing 0.33.8 benchmark, with no claim of measured 0.34.0 RPS improvement.
- Actual Rust compilation, protocol/regression execution and comparative RPS
benchmarking are required before deployment; do not infer throughput gains
from the source-level changes alone.
## 0.33.8 ## 0.33.8
- Skip cache-only policy maps, request snapshots and ticket work when caching - Skip cache-only policy maps, request snapshots and ticket work when caching
Generated
+1 -1
View File
@@ -1881,7 +1881,7 @@ dependencies = [
[[package]] [[package]]
name = "rust-proxy" name = "rust-proxy"
version = "0.33.8" version = "0.34.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arc-swap", "arc-swap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rust-proxy" name = "rust-proxy"
version = "0.33.8" version = "0.34.0"
edition = "2021" edition = "2021"
rust-version = "1.99" rust-version = "1.99"
description = "RustProxy - independent reverse proxy with local administration" description = "RustProxy - independent reverse proxy with local administration"
+63 -1
View File
@@ -1,4 +1,66 @@
# RustProxy 0.33.8 # RustProxy 0.34.0
## 0.34.0: reverse-proxy hot-path optimization revision (source candidate)
This revision retains the **0.34.0** version and all prior scoped-cache,
`sendfile`, security, logging and vhost/administration controls. In the
uncached reverse-proxy hot path, request observation now passes the immutable
selected route/frontend directly to forwarding; compiled locations are stored
as shared `Arc` handles instead of cloning their headers, regexes and policies.
Exact-host routing avoids wildcard temporary strings unless needed.
Single-member non-cookie load balancers bypass candidate vector allocation
and the shared weighted round-robin scheduler mutex, but still enforce
health, drain, pool eligibility, capacity and leases. Multi-member policies,
sticky cookies and weighted RR retain their existing selection rules. Healthy
backend observations use an atomic fast path; resolved per-member health
settings are shared across leases. Failure/quarantine/recovery transitions
retain their existing locked state machine.
Requests without configured error fallbacks no longer clone request headers
for error replay. Safe/empty request retry headers are saved only if a
multi-backend retry or upstream H2 close replay is possible. Exchange source,
cache-enabled and transform-enabled log flags move from mutexes to atomic tags
with **unchanged emitted log fields**. Live log records are cloned for
subscribers only while subscribers exist; history and external sinks remain.
These changes have **not** been compiled or timed in this environment, and
there is no claimed 0.34.0 RPS improvement. Build and run the regression and
nginx benchmark matrix before deployment. See
[`docs/PROXY_HOTPATH_0.34.0.md`](docs/PROXY_HOTPATH_0.34.0.md) for invariants,
checks and exact measurement commands.
## 0.34.0: scoped cache, proxy accounting and opt-in Linux sendfile
An operational vhost save (including every upstream keepalive/recycle knob,
LB health/weight, backend concurrency, diagnostic controls and timeouts)
no longer purges the HOT or COLD cache. Origin/content policy changes purge
only the affected vhost(s); global cache/transform/headers/forwarding and
Web Service changes still trigger the conservative global invalidation.
Persistent COLD entries carry a vhost content fingerprint, preventing stale
reloading after an interrupted scoped purge. The global cache identity changed
for this release, so existing 0.33.8 COLD entries may require one-time refilling.
Reverse-proxy response metering now snapshots the selected backend lease after
routing and avoids locking backend selection for each response DATA frame.
Request-body metering, timeouts, cancellation, accounting and HTTP transport
behaviors stay unchanged. This is a source-level optimization candidate:
**build and live RPS comparison are required before claiming a performance gain**.
There are no new LB controls. In **Web Server -> Edit service -> Delivery**,
`sendfile: off | auto` is an opt-in Linux `sendfile(2)` transport bridge for
full static GET responses of at least 64 KiB over plain HTTP/1.1. The already
validated file descriptor is sent directly to the TCP socket; cache-enabled,
compressed, ranged, transformed, TLS/H2/H3 and non-Linux requests retain the
existing streaming path. The default is `off`; toggling this transport option
does not purge HOT/COLD cache. **Native compilation and live protocol tests
remain pending; do not deploy this candidate without them.**
An offline cache-retention simulation for a 20-vhost workload shows 92.123%
fewer invalidated objects and 92.113% fewer cache misses than the old global
purge policy **under its stated synthetic assumptions**. These are not RPS
figures; real 0.34.0 performance remains unmeasured.
See `docs/TEST_REPORT_0.34.0.md` for verification scope and remaining gates.
## 0.33.8: request-path performance and compatibility verification ## 0.33.8: request-path performance and compatibility verification
+30
View File
@@ -1,5 +1,35 @@
# RustProxy roadmap # RustProxy roadmap
## 0.34.0 - Scoped cache correctness and proxy body hot path
- [x] Share compiled route handles and reuse middleware routing in forwarding;
avoid exact-host wildcard allocation and full-route cloning.
- [x] Fast-path single-backend LB; share immutable member health settings
between leases; reduce health locks and fallback/retry header snapshots.
- [x] Convert three exchange metadata locks to atomic tags; avoid unused live
log clones and add Rust regression fixtures for modified semantics.
- [ ] Compile this hot-path revision, run Rust tests and verify live LB health,
sticky/session behavior and HTTP/2 graceful shutdown retries.
- [ ] Profile CPU flamegraphs and measure real gains vs 0.33.8/nginx; tune
idle pool/recycle only as a separately labelled experiment.
- [x] Disconnect keepalive/LB/diagnostic-only vhost saves from cache revisioning.
- [x] Invalidate only a vhost scope when its origin/representation policy changes.
- [x] Persist per-scope COLD fingerprints; old metadata cannot revive after an
interrupted content update or restart.
- [x] Reduce response-body per-frame backend mutex operations without losing metrics.
- [x] Add Rust regression cases and preserve unchanged LB controls/API.
- [ ] Compile, run the full Rust suites and repeat live HTTP/TLS/H2/H3/WS/LB
regressions with a compatible Rust toolchain.
- [ ] Run paired 0.33.8 vs 0.34.0 vs nginx benchmarks; record real RPS/p95/p99,
CPU, RSS, cache retention after saves, and rollback on failed acceptance.
- [x] Implement opt-in Linux HTTP/1.1 `sendfile(2)` and a per-Web-Service
Delivery toggle, with TLS/H2/H3/range/cache/compression fallback and bounded
per-connection slots. Implementation is source-only until native tests run.
- [ ] Compile and validate native `sendfile`/fallback functionality and safety
on the target Linux OS; compare live WWW RPS against nginx.
- [ ] Complete long-running soak, external dependency integration and staged rollout.
## 0.33.8 - Performance with retained functionality ## 0.33.8 - Performance with retained functionality
- [x] Reduce cache-disabled request-policy work, header copies, timer allocation - [x] Reduce cache-disabled request-policy work, header copies, timer allocation
+129 -118
View File
@@ -1,13 +1,13 @@
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore 61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example 4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore 9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
293fdbc2fc7318a9f544543c2ad518061ba1377edab1c73e93bd3c881d916195 AGENTS.md 46bab1d2c2d6e0a80d57a6b232b1ba4c220884b95cd45ddccbafce2aa160cddb AGENTS.md
38124997eb546aa2fb54c1b7801a127994a7911c469ae93b04b96e9816dec842 CHANGELOG.md 37ed26e41820a69fe6417e7600c9ec5a2eeab69fd11e5a389be912a2a3cd7642 CHANGELOG.md
fcbd81ae693bf052a67a8815e227221a236e0486669e2b4668eece1f4e73bee7 Cargo.lock ca201e3cd3f9842eae0006d0b40a0a72bcd3d4da6e4e5ecaf200d1b9a1390db2 Cargo.lock
8e72d66bf61579ceb5237d9195a4c09baf936abc9c9b541f17454c7946acdb58 Cargo.toml fec5f1bae9a59ef1fabd27c5e45d31448dd8f811736eebfa8826b3806a7e3349 Cargo.toml
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile 25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
bcf49d2ed52433772e249d9d991fdaf7cc82636da4de30f098193c08c4b0d7b8 README.md 4dba0f83d900cbfb131eed06768084f18fbe018952cecfd4857533b36bccb747 README.md
61870c209a50f1c683d221b3a11b18507b084c3b602111ef62f8bd90f285f51a ROADMAP.md 1119f4de537b90a6263d93eb1ce2b6b0f233fd7a398f32d758c892ba16ea7dd6 ROADMAP.md
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md
@@ -48,13 +48,15 @@ d8829cc68d982c933d04207cf7bc78cc918044c72e024e7ecad5220953bb3a79 docs/MEMORY_ST
e36247774051aa9c83c1a2fda91c1f7857991dcaa4fa90320ca5b492e31dbcf8 docs/MIGRATION.md e36247774051aa9c83c1a2fda91c1f7857991dcaa4fa90320ca5b492e31dbcf8 docs/MIGRATION.md
5f090a190000c6badab2a74e5d4968e70c34de43b59c1452f5b05503b5c5c18a docs/MOCK_AUTH.md 5f090a190000c6badab2a74e5d4968e70c34de43b59c1452f5b05503b5c5c18a docs/MOCK_AUTH.md
1f85fac0e06e432ef6f744f0b5f9c1b7a27a1e9e1ad30dc1a218eab96b1caffe docs/MOCK_SERVER.md 1f85fac0e06e432ef6f744f0b5f9c1b7a27a1e9e1ad30dc1a218eab96b1caffe docs/MOCK_SERVER.md
6cd54c6e2c3e5c23ae0104e09e88ab6d93bde26bd92b23c1a078fe7c4ee04b55 docs/NGINX_GAP_0.34.0.json
be53a58afbeddbd7deb4b5f6447f2dc74db0327839bd83407c20915df98dcfa0 docs/NOT_FOUND_HOSTS.md be53a58afbeddbd7deb4b5f6447f2dc74db0327839bd83407c20915df98dcfa0 docs/NOT_FOUND_HOSTS.md
89a1af6c9fe10c572b211f9632ce256166e67990b5dacacf2ae07265d99a5172 docs/NPM_MIGRATION.md 89a1af6c9fe10c572b211f9632ce256166e67990b5dacacf2ae07265d99a5172 docs/NPM_MIGRATION.md
02126c23264b3afdf1e639c0334d621520e7538ff92d60eb6398c5fb10a23f2e docs/PERFORMANCE_0.33.8.md 02126c23264b3afdf1e639c0334d621520e7538ff92d60eb6398c5fb10a23f2e docs/PERFORMANCE_0.33.8.md
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
bb15713876184682cf1907d8c5edd18e657735b6de6cc5f8ca976dbdec4dd936 docs/README.md deea89aed410c0619bdbdb12437edc7aff9dca2fec7fdc059e918c649fc36b51 docs/PROXY_HOTPATH_0.34.0.md
887d1fdcc3b546b024afcd18b1716b8e3a74a3800012bf4bda7a122d2eabe233 docs/README.md
6e46a137b9c88dac8cb9e3fdca877430092392de9913d746278dbff8652460c2 docs/REDIRECTS.md 6e46a137b9c88dac8cb9e3fdca877430092392de9913d746278dbff8652460c2 docs/REDIRECTS.md
0dfd0d431c5aedc67abac1ce5de040ffa790ec9578fd7d6f91a987c333d73358 docs/REFERENCES.md 0dfd0d431c5aedc67abac1ce5de040ffa790ec9578fd7d6f91a987c333d73358 docs/REFERENCES.md
d85c5567fc6a7714b96ad7cfe4ee9ff2085f84c4f31534a6484340d03a73e0f7 docs/RENEWAL.md d85c5567fc6a7714b96ad7cfe4ee9ff2085f84c4f31534a6484340d03a73e0f7 docs/RENEWAL.md
@@ -66,12 +68,13 @@ 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
4c82db7fab8c8fe8cd1508fbb85751b7f72071edd913ee7def4a9ffde8efa13e docs/TEST_REPORT_0.34.0.md
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md 751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md
fe3ec6ca16ff933239c12dacf39dd7b45717c0551a8b9f6ce601bf78136c9e37 docs/TRANSPORT.md fe3ec6ca16ff933239c12dacf39dd7b45717c0551a8b9f6ce601bf78136c9e37 docs/TRANSPORT.md
711c69c9e05761ba5cb96dadc3c368eb2961e2d2287ca039eb49c7fbca0dd63c docs/VHOSTS.md 711c69c9e05761ba5cb96dadc3c368eb2961e2d2287ca039eb49c7fbca0dd63c docs/VHOSTS.md
09b60fb7b40db4d0139fa2a9e69a2aa9f28d4e61b8643521cfc7e936d7df8774 docs/VHOST_ACCESS.md 09b60fb7b40db4d0139fa2a9e69a2aa9f28d4e61b8643521cfc7e936d7df8774 docs/VHOST_ACCESS.md
4257729a4c1b1f3949538410af78a12067bd3417c70525a15bd01411155d25fd docs/WEB_SERVER.md ca5d1d1d7e8aa372dcd3e2bd56bb7a9b8977953482ece560cfef58a19314242e docs/WEB_SERVER.md
c336f998027330c58d5afbbbb3a3a682fd44ae3997643882fe342633822f8bb3 docs/screenshots/README.md c336f998027330c58d5afbbbb3a3a682fd44ae3997643882fe342633822f8bb3 docs/screenshots/README.md
5502e8ce44276213803b66c58e3a4d804a1354f5ed5d83f47ee805e8c44ab088 examples/acl-headers.json 5502e8ce44276213803b66c58e3a4d804a1354f5ed5d83f47ee805e8c44ab088 examples/acl-headers.json
2431dd88a2d9ad2cf3cd4b8fcdfa602563ab1013c612d8917fddbe7febfb2dc3 examples/cache-dashboard.json 2431dd88a2d9ad2cf3cd4b8fcdfa602563ab1013c612d8917fddbe7febfb2dc3 examples/cache-dashboard.json
@@ -97,8 +100,8 @@ cb01fdef286f38b442dd9252c23c4f8e44eb72e9ecdd99adb926e8e9f6318bb1 integrations/r
96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh 96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh 3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
13189fa546c3488dddd8efc80c47f6da5241e5fc28b79cbfe795414ea05c7a4f langs/README.md 13189fa546c3488dddd8efc80c47f6da5241e5fc28b79cbfe795414ea05c7a4f langs/README.md
1a69ed38b730c3b379b8b14b5707d8ae923320a0c66d084c7ee478e6a01eb53c langs/en_US.json 462339bc35535fa69ebd95aaf43e9405913d940406bfa14b27d15afb62accef5 langs/en_US.json
6513435292eb947935f50eeadf9ef2ee0995f4b57e989699dfc9cda686f21685 langs/pl_pl.json 22efd93e26f71bd78c1e6cf83a9f0aaab3a8a5a4adb98822cbf73ba6e22cb991 langs/pl_pl.json
a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql 20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
@@ -125,7 +128,7 @@ 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
afea7479fd10b7d723f147f6605283174ef81ff5e482f65819db4f01ffb02fe5 scripts/performance/README.md e4fd7fa94d661d4610d7d86fcdeaae9d6deb220e95b8d3bbd67ec15fbd017ccb scripts/performance/README.md
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
@@ -138,6 +141,8 @@ c1d8c184811ae8fb1960d1ada08d8a4a95405c3b83dd18e1a27d711b596140be scripts/perfor
830a360609e2b8ebbb1ef6f3d89e3e6151e8fefb36c3673d20df68ff8932eabb scripts/performance/profile_syscalls.py 830a360609e2b8ebbb1ef6f3d89e3e6151e8fefb36c3673d20df68ff8932eabb scripts/performance/profile_syscalls.py
ceb26650fc8b63d0107eb9100ccd572e5c70f7a0ce5cc533a94083e20485095d scripts/performance/regression.py ceb26650fc8b63d0107eb9100ccd572e5c70f7a0ce5cc533a94083e20485095d scripts/performance/regression.py
d6480962a23f55d19a54aba27648ca1b28b50f03afe2decbfa272efd142df756 scripts/performance/resources.py d6480962a23f55d19a54aba27648ca1b28b50f03afe2decbfa272efd142df756 scripts/performance/resources.py
2dcd4c7e39dd58fd77c3c713d9bdee5025cace7191ca5f12bd6e7fe28f333358 scripts/performance/simulate_cache_034.py
4a5af2d3870d9517b4ad9c008789490cfb07affe98ffd629e51f7674ff54a642 scripts/performance/simulate_nginx_gap_034.py
f85dd0c29d5a96ad59a71e035ac4303c7b6a70a8d61280a15cdb966b32a218c7 scripts/performance/soak.py f85dd0c29d5a96ad59a71e035ac4303c7b6a70a8d61280a15cdb966b32a218c7 scripts/performance/soak.py
fe6ff5bfd76ac7a2752a8a2e1e45c63e549c4a61c753008671e478df3ebff304 scripts/performance/telemetry.py fe6ff5bfd76ac7a2752a8a2e1e45c63e549c4a61c753008671e478df3ebff304 scripts/performance/telemetry.py
7c1ba46c623e1ade47b872fcdf4526a48ca85c56515dffeac626188b0357d23c scripts/performance/transport_clients.py 7c1ba46c623e1ade47b872fcdf4526a48ca85c56515dffeac626188b0357d23c scripts/performance/transport_clients.py
@@ -150,6 +155,7 @@ e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/runtim
174c8059625e3315a3d9fefe74c17f78ed850ff25ee15bfdabc148107dd344eb scripts/start_stream_backends.sh 174c8059625e3315a3d9fefe74c17f78ed850ff25ee15bfdabc148107dd344eb scripts/start_stream_backends.sh
046b4282550f5cb4e53906665c91c7442be70fc68fd0743466fd832582f7c979 scripts/test_stream_tcp.py 046b4282550f5cb4e53906665c91c7442be70fc68fd0743466fd832582f7c979 scripts/test_stream_tcp.py
f0b48ec0cc4411755cc0b4128eebce710375afa99344cb76198cf31074f8eb63 scripts/test_stream_udp.py f0b48ec0cc4411755cc0b4128eebce710375afa99344cb76198cf31074f8eb63 scripts/test_stream_udp.py
955c150f8d0490069cb14b659d4fb4e0b529a28b87c27bd6697935c23e7ef192 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
6a908fd434d2a8a80adfd06a66d4b83c4174e448d453a8c10b1812475d036677 scripts/tests/dashboard_chart_width_0333_browser_test.py 6a908fd434d2a8a80adfd06a66d4b83c4174e448d453a8c10b1812475d036677 scripts/tests/dashboard_chart_width_0333_browser_test.py
@@ -168,6 +174,9 @@ fa42ec0475e4398d647b3c094309b4332cd2b5649db223cae0b8e821d6b44a28 scripts/tests/
72185f59f31bf04a4e88d53fe2fca52509f3663cb5f40b1d3bc319a5f945ef23 scripts/tests/panel_html.mjs 72185f59f31bf04a4e88d53fe2fca52509f3663cb5f40b1d3bc319a5f945ef23 scripts/tests/panel_html.mjs
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
f94612eaefb4098e1a4efc0d970ce52a656db239aaa0a012df47567527bf3708 scripts/tests/proxy_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 1bb99b148d0297235b707f16865ca077329246fc19ecbb20a143bdeb56a23f37 scripts/tests/sidebar_nav_0333_browser_test.py
12f2b18b0ed2171c49eb8cde2c2b88fc627054c2f574c04bd9847821b00cd4c1 scripts/tests/system_ram_meter_0332_browser_test.py 12f2b18b0ed2171c49eb8cde2c2b88fc627054c2f574c04bd9847821b00cd4c1 scripts/tests/system_ram_meter_0332_browser_test.py
046b4282550f5cb4e53906665c91c7442be70fc68fd0743466fd832582f7c979 scripts/tests/test_stream_tcp.py 046b4282550f5cb4e53906665c91c7442be70fc68fd0743466fd832582f7c979 scripts/tests/test_stream_tcp.py
@@ -179,41 +188,41 @@ f0a2e3daa0a49aac54723cb5a281360e49a97c076281372c5cca2a85e096c02c scripts/tests/
f55efa99245bbdbc98fe3202e52952d224ac3200612561580d52f6e251810049 scripts/tests/web_server_0330_ui_test.py f55efa99245bbdbc98fe3202e52952d224ac3200612561580d52f6e251810049 scripts/tests/web_server_0330_ui_test.py
0dd1cef06a1c3e1546e2fb9b085bf369dfbed02107b9ab01b4fb4f3b894099c0 scripts/tests/web_server_0334_browser_test.py 0dd1cef06a1c3e1546e2fb9b085bf369dfbed02107b9ab01b4fb4f3b894099c0 scripts/tests/web_server_0334_browser_test.py
22e880e98c7b9d700c7a4c02b9b1d543e5dbd1f4ecdca42813a1083165b32a19 scripts/version.py 22e880e98c7b9d700c7a4c02b9b1d543e5dbd1f4ecdca42813a1083165b32a19 scripts/version.py
55485a513e8c4da2a58daa7abcb15b32ed408a5fc062cf23599fb83d16b90cfe src/acl.rs
63efab1e95d41134fc30cc57320c32810bbae9f81a640bdb24b1be2059e63d8a src/acl/dynamic.rs
4803e44de39b872a5b3b55f9ca9e81c066ba52cb8f6adb879fd57c5f94b93e89 src/acl/dynamic/tests.rs 4803e44de39b872a5b3b55f9ca9e81c066ba52cb8f6adb879fd57c5f94b93e89 src/acl/dynamic/tests.rs
63efab1e95d41134fc30cc57320c32810bbae9f81a640bdb24b1be2059e63d8a src/acl/dynamic.rs
a22e5ea6381a1eacafe650be0f5cd6e653773e31f6491a562fe0459a309fb7cc src/acl/tests.rs a22e5ea6381a1eacafe650be0f5cd6e653773e31f6491a562fe0459a309fb7cc src/acl/tests.rs
6f5d3137bf24451da05c1db9f0f9e2a451305db17b6bc2590815ad54a6511829 src/acme.rs 55485a513e8c4da2a58daa7abcb15b32ed408a5fc062cf23599fb83d16b90cfe src/acl.rs
587097620b2f25ce7c6f59f907b47660184b095e3040c7615e74509a82e7cdd8 src/acme/jobs.rs 587097620b2f25ce7c6f59f907b47660184b095e3040c7615e74509a82e7cdd8 src/acme/jobs.rs
3f158f46e80b403b658b4e9b1e3710e38c70e47b2d476b8223d9c394ffeeba95 src/acme/model.rs 3f158f46e80b403b658b4e9b1e3710e38c70e47b2d476b8223d9c394ffeeba95 src/acme/model.rs
c5334de6c732d76b2bea7bfc17b3ac305a250bbaad31a5420d6a7d7abb200449 src/acme/providers.rs
d1dcb7bdfb7cfdc9c3b88cfb6908201c58532bc888744a13f45853079f214659 src/acme/providers/tests.rs d1dcb7bdfb7cfdc9c3b88cfb6908201c58532bc888744a13f45853079f214659 src/acme/providers/tests.rs
c5334de6c732d76b2bea7bfc17b3ac305a250bbaad31a5420d6a7d7abb200449 src/acme/providers.rs
29ff0647417b0f1d50a4cc075ab51afecff9ddf47db3eb7948d51db5a38f33f3 src/acme/runtime_bootstrap.rs 29ff0647417b0f1d50a4cc075ab51afecff9ddf47db3eb7948d51db5a38f33f3 src/acme/runtime_bootstrap.rs
87f64198770ae026c03a242fa542db2590ca734b02e756f1e5c28d66236f753a src/acme/runtime_env.rs 87f64198770ae026c03a242fa542db2590ca734b02e756f1e5c28d66236f753a src/acme/runtime_env.rs
f8075a5ef9ae5651d72a33ac0f0fd5b44ad0ae30f6e2d2318412177e0ed837f5 src/acme/runtime_versions.rs f8075a5ef9ae5651d72a33ac0f0fd5b44ad0ae30f6e2d2318412177e0ed837f5 src/acme/runtime_versions.rs
e38331a1b186b49fe9d04051de52b603e1fa028def37e4e2f52e386b4d7b29ac src/acme/tests.rs e38331a1b186b49fe9d04051de52b603e1fa028def37e4e2f52e386b4d7b29ac src/acme/tests.rs
6f5d3137bf24451da05c1db9f0f9e2a451305db17b6bc2590815ad54a6511829 src/acme.rs
b6f38f3fd75d6685bbc882ed1077a38bf495cfcf01c397f69ed9b98be68b446f src/admin/access.rs b6f38f3fd75d6685bbc882ed1077a38bf495cfcf01c397f69ed9b98be68b446f src/admin/access.rs
b4a2a068acfa1143b0032b9cd072c19cf3fdda2580f0b932d11d6368cd2ab3ad src/admin/certificate_api.rs b4a2a068acfa1143b0032b9cd072c19cf3fdda2580f0b932d11d6368cd2ab3ad src/admin/certificate_api.rs
51f46a2cb3167c4e3a784dfbb59b8fe0e9d5d3d51365b26699498e31d0afb6a9 src/admin/certificates.rs 51f46a2cb3167c4e3a784dfbb59b8fe0e9d5d3d51365b26699498e31d0afb6a9 src/admin/certificates.rs
b5f6d49f1add583cfc8cd4aa36ffe4db56dac4de3b151fe8239fe42f507019cc src/admin/dashboard_presets.rs b5f6d49f1add583cfc8cd4aa36ffe4db56dac4de3b151fe8239fe42f507019cc src/admin/dashboard_presets.rs
5e150d4ef0413a2f92db5b6631669933b84d4b4796633d01476c4b39188cd05c src/admin/documentation.rs
958546c43c05bf4bed8121b24db3b3096f9243f3c56a71e1762592b755684013 src/admin/documentation/documents.rs 958546c43c05bf4bed8121b24db3b3096f9243f3c56a71e1762592b755684013 src/admin/documentation/documents.rs
67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs 67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs
5e150d4ef0413a2f92db5b6631669933b84d4b4796633d01476c4b39188cd05c src/admin/documentation.rs
1ce779b84aa8f47bdb3c34f83593c1f2f06d2235f0319847096cfcd035434168 src/admin/file_log_search.rs 1ce779b84aa8f47bdb3c34f83593c1f2f06d2235f0319847096cfcd035434168 src/admin/file_log_search.rs
ab18acf6fb4ac86fb22635b9219a12dac76d24952c8f15538486267a9b4b7855 src/admin/file_logs.rs ab18acf6fb4ac86fb22635b9219a12dac76d24952c8f15538486267a9b4b7855 src/admin/file_logs.rs
c6b552cd5acd9e743c28b1a5564420c214c619fe891ab0561b7d4499aae8d6c0 src/admin/file_logs_io.rs c6b552cd5acd9e743c28b1a5564420c214c619fe891ab0561b7d4499aae8d6c0 src/admin/file_logs_io.rs
c720a7cad6a15c57b1f2ae7b3e5fbf30a97c64afb9137915f37b23f4e576bdc5 src/admin/file_logs_query.rs c720a7cad6a15c57b1f2ae7b3e5fbf30a97c64afb9137915f37b23f4e576bdc5 src/admin/file_logs_query.rs
74636c6560618ee95757335452baf8db03394e1ad18acd54252f54b15746b8d5 src/admin/logging_api.rs 74636c6560618ee95757335452baf8db03394e1ad18acd54252f54b15746b8d5 src/admin/logging_api.rs
e552c4b565e3198cc2224d1efa3bef21db02a9872cb80cd96f446181035f19fb src/admin/mod.rs e552c4b565e3198cc2224d1efa3bef21db02a9872cb80cd96f446181035f19fb src/admin/mod.rs
28dd33ef19d7eab08cfa243b253785012d6c731bc4c56b4f69f28edd3d9e07a7 src/admin/openapi.json 93f63f7bbde1ed84fc6cfbbb7cf6b96b7d1a1411fd6dbf6d90a3a9070ed9f5f8 src/admin/openapi.json
d9120f7dc61d27621c5ac27ea49815dc7945c44d5b99d208da503818c198f649 src/admin/operations.rs d9120f7dc61d27621c5ac27ea49815dc7945c44d5b99d208da503818c198f649 src/admin/operations.rs
30895e05c5d5d0a26be29f667e0f6d72f16a96a5c8a98e09cd11d8ef2d7b4763 src/admin/panel_assets.rs 30895e05c5d5d0a26be29f667e0f6d72f16a96a5c8a98e09cd11d8ef2d7b4763 src/admin/panel_assets.rs
75bbedd54fdabb2b489642a642ee7c6533f69cdacb85e251763a56ba11174d5c src/admin/panel_content.rs 75bbedd54fdabb2b489642a642ee7c6533f69cdacb85e251763a56ba11174d5c src/admin/panel_content.rs
92b3e05f04711d8001b56816405859631d9e917868e15be965c7e76ef4ddef99 src/admin/preferences.rs 92b3e05f04711d8001b56816405859631d9e917868e15be965c7e76ef4ddef99 src/admin/preferences.rs
4a5cb4c3a3aa8b9a519020854506ce4be5e688a6c4298260322ae641dded7fe9 src/admin/preferences_columns.rs 4a5cb4c3a3aa8b9a519020854506ce4be5e688a6c4298260322ae641dded7fe9 src/admin/preferences_columns.rs
9a3f13e2477769ae680c03d63336880c3b7b7d5aba4a91e44390a976608b9b1d src/admin/preferences_tests.rs 9a3f13e2477769ae680c03d63336880c3b7b7d5aba4a91e44390a976608b9b1d src/admin/preferences_tests.rs
4a7a2017ba57c9e7a8b29e73764d439f6d03653403909ef198ded8c48d152784 src/admin/public.rs
7a00e4ab943f74d2b1bdcbfab7f9cd3f584e9f59536b4aca6c4c31f5f5508d12 src/admin/public/tests.rs 7a00e4ab943f74d2b1bdcbfab7f9cd3f584e9f59536b4aca6c4c31f5f5508d12 src/admin/public/tests.rs
4a7a2017ba57c9e7a8b29e73764d439f6d03653403909ef198ded8c48d152784 src/admin/public.rs
9926827131254677a848c178c80850b3ca1993c05b4bf913687dc0c7992ad048 src/admin/refresh_preferences.rs 9926827131254677a848c178c80850b3ca1993c05b4bf913687dc0c7992ad048 src/admin/refresh_preferences.rs
daef5e663c1bc99a22b3b8fa4481ae5e4916b91db132c104bc050f51b1fdf77f src/admin/router.rs daef5e663c1bc99a22b3b8fa4481ae5e4916b91db132c104bc050f51b1fdf77f src/admin/router.rs
a40adfbaa908da3ea726d7aa3d45394a70a6a55b6fc0063270ec4d2a2f2026a3 src/admin/session.rs a40adfbaa908da3ea726d7aa3d45394a70a6a55b6fc0063270ec4d2a2f2026a3 src/admin/session.rs
@@ -267,7 +276,7 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg 0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.js 7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.js
b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js
6684e7de2540499f9129b78c790f9a820b0e6c5de33e3b60c318d9a1afc931a5 src/admin/web/index.html 7ffdda1b657c613f2bd3b7a9c7721d67d3d7a159351aaeccbde2a213597b77f7 src/admin/web/index.html
03ad9572bb14c34059636d612cf6b876d6361ce601817cff674c57b715dc65dd src/admin/web/integrations.js 03ad9572bb14c34059636d612cf6b876d6361ce601817cff674c57b715dc65dd src/admin/web/integrations.js
7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json 7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json
cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/login.html cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/login.html
@@ -289,127 +298,126 @@ b3239e67901f1796d7a974ade0796f2e3590ff8064628a5629b78c7f6ce2f32e src/admin/web/
2170cfddf5c611672de34b0525d36ade8fcbe459cc21269281d6b615d8601b93 src/admin/web/vhosts/rates.js 2170cfddf5c611672de34b0525d36ade8fcbe459cc21269281d6b615d8601b93 src/admin/web/vhosts/rates.js
bb93e2017f40e3a45ff7f4ca92e4188bb464f432704b6d2c39da9d9621b7984c src/admin/web/vhosts/render.js bb93e2017f40e3a45ff7f4ca92e4188bb464f432704b6d2c39da9d9621b7984c src/admin/web/vhosts/render.js
4dfdeb7c73c2775eacaca65fd8ef1dc80b275421b15522d319d338f5b96288cc src/admin/web/web-server-profiles.js 4dfdeb7c73c2775eacaca65fd8ef1dc80b275421b15522d319d338f5b96288cc src/admin/web/web-server-profiles.js
0049e5c2a35e3ec25f3c1590879684a7b15165584e96eb1b022fb60d3c548f2c src/admin/web/web-server.js fd476445a88eb40270e706814eee50cbfbba4069a26dd843c095367e55a147a1 src/admin/web/web-server.js
53237ae8b08d261ce21e0aa6fd225bf4f3bb17a37a80529e2a4039d45b13f7c5 src/auth/files.rs
b2f948bcc3dbd24952c7b1b02d41d06757f49f0c38a78caf51f8f8a687d8480d src/auth/files/tests.rs b2f948bcc3dbd24952c7b1b02d41d06757f49f0c38a78caf51f8f8a687d8480d src/auth/files/tests.rs
53237ae8b08d261ce21e0aa6fd225bf4f3bb17a37a80529e2a4039d45b13f7c5 src/auth/files.rs
775178ee5781e64574bdd7c1fb7d64e7e283977fbd9c98c99896532a7f1df612 src/auth/mod.rs 775178ee5781e64574bdd7c1fb7d64e7e283977fbd9c98c99896532a7f1df612 src/auth/mod.rs
c96a87e82ff0c1c9ce3d0cd5135903bb53af78d1c4fb6ebfe5fea503bf5644ea src/auth/store.rs c96a87e82ff0c1c9ce3d0cd5135903bb53af78d1c4fb6ebfe5fea503bf5644ea src/auth/store.rs
745226cabb007819370fc6aa680995a03a8a13fa057dacfab0fcc21d0c43bbe4 src/auth/tests.rs 745226cabb007819370fc6aa680995a03a8a13fa057dacfab0fcc21d0c43bbe4 src/auth/tests.rs
09c3a622a7d2169e49e3369ea5f0f816eab2f717eaf1675c625d518c8f34d367 src/backup.rs
76aa2afe9e7302be39b3512f30d0448d9134300f8ae930d64ada40cf3ad4a8a3 src/backup/archive.rs 76aa2afe9e7302be39b3512f30d0448d9134300f8ae930d64ada40cf3ad4a8a3 src/backup/archive.rs
732cd4f9ed38ae69f8bbae61b1db8baaa6f86b07ca21c41f24a206a30f0a96fc src/backup/ownership.rs 732cd4f9ed38ae69f8bbae61b1db8baaa6f86b07ca21c41f24a206a30f0a96fc src/backup/ownership.rs
076350506215faec3cfe7d9de4e60baf29672cc22a41eabe601dcc510c257799 src/backup/snapshot.rs 076350506215faec3cfe7d9de4e60baf29672cc22a41eabe601dcc510c257799 src/backup/snapshot.rs
5c19490d834d9971166b6b784a75b081bb95fd50c8c634ef3c87924dfffd4a19 src/backup/tests.rs 5c19490d834d9971166b6b784a75b081bb95fd50c8c634ef3c87924dfffd4a19 src/backup/tests.rs
f5afae218f3a6f91bc2f1f5a90094630ce5b7ca574de61807b5a317b13937ec2 src/backup/validation.rs f5afae218f3a6f91bc2f1f5a90094630ce5b7ca574de61807b5a317b13937ec2 src/backup/validation.rs
426a946b2c9e244f09f6cbc5f9664b8ad0737529a9bb5a3d549d27196a1f8043 src/balance.rs 09c3a622a7d2169e49e3369ea5f0f816eab2f717eaf1675c625d518c8f34d367 src/backup.rs
d3257fa5222dd4f70a82400f5060d3cbd33c13862a9320931361fe0e1c5bd452 src/balance/health.rs ca1e03b56a872dca37ed9d05ead41c1538a808e80b09fb9494823c443cd51c61 src/balance/health.rs
59d90e709bfc2be6cd48576f1c1d222891523853a46f2561f0562d74d01f60a5 src/balance/lease_runtime.rs 59d90e709bfc2be6cd48576f1c1d222891523853a46f2561f0562d74d01f60a5 src/balance/lease_runtime.rs
aed9ed896a122d7adcbedf0897cfd5acf62be3c5b63a71a4a425c8fe8474c0eb src/balance/model.rs 554b730c39a935b7da46bfb76b474b99cee07f8c482cb0490f2cf81ba7a51cf2 src/balance/model.rs
41c3c020a60c372dbf965bd277c330dd9cf2ab079f6608b472adf72a656f4cb3 src/balance/pool_core.rs 9a10649c02eeb2c05e0ca520ef6ab701f79d46591190b4432977c2d5dbc6d7f5 src/balance/pool_core.rs
477132dd381e23f1be197c32ff58a0925f41d3e6aa63fe77c10973cefb2b76f6 src/balance/pool_selection.rs 477132dd381e23f1be197c32ff58a0925f41d3e6aa63fe77c10973cefb2b76f6 src/balance/pool_selection.rs
8abb6048303ee8c022546a528d5693167766e7450efd8e20f37ec6fd0fa9ef5e src/balance/pool_view.rs 8abb6048303ee8c022546a528d5693167766e7450efd8e20f37ec6fd0fa9ef5e src/balance/pool_view.rs
f1a92d188628ab3fb587375e80956e1552301647f2732cafb55e1471c7f743e7 src/balance/tests.rs 842318f21a919055522f8bd9dae67bf4ec1c8c32dc52314cc048ba1d54a0b379 src/balance/tests.rs
5049d2bd01a9c866f3bad2288d8d594df96bba6045dfe15cd22b4173562b7388 src/basic_auth.rs 426a946b2c9e244f09f6cbc5f9664b8ad0737529a9bb5a3d549d27196a1f8043 src/balance.rs
9a8858b7ff461a4d961ba701ca3ed6eaec15c3f7f05eef8e0e30edc63617605a src/basic_auth/tests.rs 9a8858b7ff461a4d961ba701ca3ed6eaec15c3f7f05eef8e0e30edc63617605a src/basic_auth/tests.rs
7def03ace07dc38a4806277d75b6ce673d33ddaaf1c4855fa0a3372be1cf9489 src/cache/cold.rs 5049d2bd01a9c866f3bad2288d8d594df96bba6045dfe15cd22b4173562b7388 src/basic_auth.rs
a9eeb93519aae2de7a1282296ced91d2eb8307cc966f8fbf6085af0fd21b0ad2 src/cache/cold/cleanup.rs a9eeb93519aae2de7a1282296ced91d2eb8307cc966f8fbf6085af0fd21b0ad2 src/cache/cold/cleanup.rs
e022003be144598100389cf38d76f509b5dbcc035d5bae556bb9b94cc8bd30ba src/cache/cold/demotion.rs 8354ae90c1a0857773a0c6d79427925e0115d94ccfafb809ccc1ebf4ef10f9b9 src/cache/cold/demotion.rs
9fc18368e2ce278ef0ad2b73ff016aa24936c2186be371ca4010a1f6f63bb0ed src/cache/cold/startup.rs 2eaba3261da2a547e9af91330319a3abd3a8ad8b5b44a337a84040720209017d src/cache/cold/startup.rs
ea319b6e200b0c6ca93da5e2af7a3f86e3667cef918716f54f82b8012111b9bd src/cache/cold/worker.rs e7c7fd71727d542c2df3e03a4a1cba9d839c516963f66bbfde1c29ec81053500 src/cache/cold/worker.rs
7def03ace07dc38a4806277d75b6ce673d33ddaaf1c4855fa0a3372be1cf9489 src/cache/cold.rs
d51fa4f320be56c572f767617428c3f3838543cb0eeb7d1f8ae791e1a1ce9a71 src/cache/collapse.rs d51fa4f320be56c572f767617428c3f3838543cb0eeb7d1f8ae791e1a1ce9a71 src/cache/collapse.rs
284f1cc56113fbcc3b1e617103d75a7b70fd7d43e536d6017c15912154921424 src/cache/dashboard.rs 284f1cc56113fbcc3b1e617103d75a7b70fd7d43e536d6017c15912154921424 src/cache/dashboard.rs
780c2654c67dbe176d8310678e6deec8022bcebfd85b95df7262069ed41f57b1 src/cache/fill.rs 780c2654c67dbe176d8310678e6deec8022bcebfd85b95df7262069ed41f57b1 src/cache/fill.rs
675d18afb53f25c56cba49bbb431fa39ecd9d7bdd192c7b39b0698225de7a2a2 src/cache/invalidation.rs
c1357a13658e1ed5158084d3a41ba9ea906512774a5863ce9c56cb508d3fa6c8 src/cache/invalidation/ban.rs c1357a13658e1ed5158084d3a41ba9ea906512774a5863ce9c56cb508d3fa6c8 src/cache/invalidation/ban.rs
0cc31d8e70fb3774c4ce3713b94ed79c184a8d8f0d38ce66a97724410733e17e src/cache/invalidation/invalidate.rs 0cc31d8e70fb3774c4ce3713b94ed79c184a8d8f0d38ce66a97724410733e17e src/cache/invalidation/invalidate.rs
b74945c7460b76ed7afcb02a45ede09d76f08e0703c47920b9b9d794d4b74980 src/cache/lifecycle.rs 675d18afb53f25c56cba49bbb431fa39ecd9d7bdd192c7b39b0698225de7a2a2 src/cache/invalidation.rs
bd831807ca4720b3a41a6ea1d9a6c0ed2d935225e2dacbd9313be064f2e4a20e src/cache/lookup.rs 373031e18d0c23d006a31822ef977015c0263637e3da70c1442407f7bfed1033 src/cache/lifecycle.rs
88f6210c03d7a8e7a35be1f123c9c15675af6e6a599c50db927d99866851b634 src/cache/lookup/hot.rs 88f6210c03d7a8e7a35be1f123c9c15675af6e6a599c50db927d99866851b634 src/cache/lookup/hot.rs
1779abcee77600c13050a2882f896a5b35625b1ae53737c2554179492ce0e40d src/cache/lookup/insert.rs 1779abcee77600c13050a2882f896a5b35625b1ae53737c2554179492ce0e40d src/cache/lookup/insert.rs
107a589974b9689009a9bbf088818dfaec71cd1aab8676bbc6d0118359a54a31 src/cache/lookup/tiered.rs 107a589974b9689009a9bbf088818dfaec71cd1aab8676bbc6d0118359a54a31 src/cache/lookup/tiered.rs
bd831807ca4720b3a41a6ea1d9a6c0ed2d935225e2dacbd9313be064f2e4a20e src/cache/lookup.rs
58c03fde09e5eab40968679793b364f4f1cf5fd832bd79ebcdc92bb23871ad82 src/cache/mod.rs 58c03fde09e5eab40968679793b364f4f1cf5fd832bd79ebcdc92bb23871ad82 src/cache/mod.rs
0c8b0d550e94d4474dcdf991aa0340228dc5f2f904f39350e6e666a16f16fe87 src/cache/model.rs a7b6dfeb63510e4710eb11a4939b360b8863a4b3f71de263d8f3e50dfd006c57 src/cache/model/cache.rs
ba94226addfd6cdab67d666dc15b8412c4d117111e44639e503e89a95175d0a2 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
09a4417f82984eca8c040ea28eb82b3f129c347d951d4b4fab704bb194d9b985 src/cache/model/maintenance.rs d890524999dc50aeb1dbda90264b46a1d53f08ac923d831b89ba82e4a564d923 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 0c8b0d550e94d4474dcdf991aa0340228dc5f2f904f39350e6e666a16f16fe87 src/cache/model.rs
989df11e246123f760ef31689370f3500e0913fcc6b70d7ac0c2c95e2f412fb0 src/cache/policy/request.rs 989df11e246123f760ef31689370f3500e0913fcc6b70d7ac0c2c95e2f412fb0 src/cache/policy/request.rs
d426d341a7678f8ec70406d86052c97d0f49804bbcd8402951103d61a735b9da src/cache/policy/response.rs d426d341a7678f8ec70406d86052c97d0f49804bbcd8402951103d61a735b9da src/cache/policy/response.rs
e26dce02df9c043329ca164fed215d271781bce8fb03240f528331bf3df6b9d7 src/cache/policy.rs
7e2bcfbb2b57732cdc704d9df7a9298fb80e735e37984431bc936630e41e7f8a src/cache/range.rs 7e2bcfbb2b57732cdc704d9df7a9298fb80e735e37984431bc936630e41e7f8a src/cache/range.rs
7ad96b3baae36ec49c90d03b380e749944fb3d54abb045c90db3a35f6b89b1c1 src/cache/tests.rs 291d0dcc3a07926eadbc9419b8fc1740b4023ef01c0521269ae2d83f4a5acb6d src/cache/tests/cold_tier_tests.rs
a1c750a48b4562d88d904b45dee5c783d4bf0ae1a62e67719df6bb2a7374465b src/cache/tests/cold_tier_tests.rs
3842cdcf7c3414b9c30f5fbed927b93e835eec3ef96d4c6ee44250038b834276 src/cache/tests/dashboard_tests.rs 3842cdcf7c3414b9c30f5fbed927b93e835eec3ef96d4c6ee44250038b834276 src/cache/tests/dashboard_tests.rs
d7a02eb7bad37e6a5524600aabb5e60ccf50e26c7a487e2806891c9d8acd48f3 src/cache/tests/http_invalidation_tests.rs d7a02eb7bad37e6a5524600aabb5e60ccf50e26c7a487e2806891c9d8acd48f3 src/cache/tests/http_invalidation_tests.rs
1db70ea5f3cc1f1543504a68498ba23cafce5f9bb48c4616a4bf70d9a536790e src/cache/tests/scope_tests.rs 1659ffd8d227d2ec8b98e6a5e06b6143e6ac4ac06ab423c184980abcb47085da src/cache/tests/scope_tests.rs
ddbcf27a667d779065d7a654d8094910e92958d2b8862a2ec2a85ca41b7f21e5 src/cache/tests/tests.rs ddbcf27a667d779065d7a654d8094910e92958d2b8862a2ec2a85ca41b7f21e5 src/cache/tests/tests.rs
af28534625859ff66011e10c54cff8f82c2cfa1c173c4d99b89c27c1437f0659 src/cache/tests/v022_cache_tests.rs af28534625859ff66011e10c54cff8f82c2cfa1c173c4d99b89c27c1437f0659 src/cache/tests/v022_cache_tests.rs
a8a0ce446eb53ffdb8f8d97efa3e1344410dc3798b25f62b8817e8e160d28940 src/cache/tickets.rs 7ad96b3baae36ec49c90d03b380e749944fb3d54abb045c90db3a35f6b89b1c1 src/cache/tests.rs
e0146bf5d2e2e33aedcccd9205e5bd457559c9d77dc7ce266f8c165d1b867f0f src/compression.rs 22f8f16cf7383c579b19810819030072fc827681478e17068dddc85da8dc53ad src/cache/tickets.rs
4bdbde197d135fee8be20e93427eba8ca7b60b801717ed7ca734319bedb6ef05 src/compression/tests.rs 4bdbde197d135fee8be20e93427eba8ca7b60b801717ed7ca734319bedb6ef05 src/compression/tests.rs
7bc8285cb2012506fd1a38a71dffa561f885f4ab65a9b2a7c8344319e3cd1cf6 src/config.rs e0146bf5d2e2e33aedcccd9205e5bd457559c9d77dc7ce266f8c165d1b867f0f src/compression.rs
34be288c858a037c7174f6c4dee23178b01b565aa9d65f0951f351e1685847df src/config/backend.rs dbd57ef3307b937614cdfdb6c1df82acb80cbbef39f09d19fe2edfc24334aa58 src/config/backend.rs
beb831a208c4e5f7ebe8327686b1e0edc66608c7032f895cb4fc3366b189aae0 src/config/compile.rs beb831a208c4e5f7ebe8327686b1e0edc66608c7032f895cb4fc3366b189aae0 src/config/compile.rs
490d05d84b2fe349ddde54eae44880993fab70811652484dd9ba38b5069d603b src/config/internal_routing.rs dfa89a9e214d0b33b4efce8fa678f1fec17d17656e203acf84dfd09ce33868e0 src/config/internal_routing.rs
a19fc675720c7c2dbf4bba706e1b1c30c83c6df1da3f29e3cbb3b82442e7246a src/config/model.rs 70741af46ad6f712b0c0e029c066d04c14a689eae0c831af264b09f6e5c41bae src/config/model.rs
f66c2388f31b19142c12469df23d43451e0f7c48b158d4996920282c06d32076 src/config/routing.rs fc993c356386a9ff17379d48a70ef388df5b2b10ca9ee04f06342f68f9dbbaae src/config/routing.rs
b25e212a493c6d9f6cd1d1136935182504163c7b8d7e776364389451ca2247aa src/config/snapshot.rs a7ccffdeeb258adae41c616022ed0181cf590d708d305d48c63317e4a3f68699 src/config/snapshot.rs
f5431467dec55540f1f881541bf13d9402753680c7f83e3deb9d0e1fa747313e src/config/tests.rs 41a4fa4f9a919a8c848ee01d01423d82a136790c9d74f27319c1e5a6d9f18097 src/config/tests.rs
23541660151d9005555a07db6d491455d61b185f0cc70c3ec3a0bee2aef08dc3 src/db.rs 7bc8285cb2012506fd1a38a71dffa561f885f4ab65a9b2a7c8344319e3cd1cf6 src/config.rs
30f0b7269224a64e3f1ec9e6fbe9e27ddb1d308dc84cc9882e50545cf612695d src/db/certificate_api.rs 30f0b7269224a64e3f1ec9e6fbe9e27ddb1d308dc84cc9882e50545cf612695d src/db/certificate_api.rs
20ba52d3755c359d670968efa53b0604ff7cbed63a416f9f765b50a1f45074ac src/db/dashboard_presets.rs 20ba52d3755c359d670968efa53b0604ff7cbed63a416f9f765b50a1f45074ac src/db/dashboard_presets.rs
2ff744223934685439374572b83fd639bb3eabdbac168bea4a6501a6e9ce0ee0 src/db/history.rs 2ff744223934685439374572b83fd639bb3eabdbac168bea4a6501a6e9ce0ee0 src/db/history.rs
c05a02618d476e4670b4f9930555f199cac776cdf09edb8fcc1a675695b8f99f src/db/tests.rs c05a02618d476e4670b4f9930555f199cac776cdf09edb8fcc1a675695b8f99f src/db/tests.rs
23541660151d9005555a07db6d491455d61b185f0cc70c3ec3a0bee2aef08dc3 src/db.rs
19ab6c3ae37009c544291cb0b65c1bb9c721e168f95c1ed3fa88155aca7463ad src/demo.rs 19ab6c3ae37009c544291cb0b65c1bb9c721e168f95c1ed3fa88155aca7463ad src/demo.rs
e69031875ffec32c9f548c43bed5f1a5c4c626e9568c9173e3b093f9a8b73036 src/diagnostics.rs e69031875ffec32c9f548c43bed5f1a5c4c626e9568c9173e3b093f9a8b73036 src/diagnostics.rs
b45d6040810c93ae2acb04ef6b0a59c189751edf9008cd81a4521c9e04e7f07d src/docker.rs
93a8a3f6ba12761ddcbd19f22f32fb2c0e6948b660fb774ddd7ebdba9b6562a5 src/docker/client.rs 93a8a3f6ba12761ddcbd19f22f32fb2c0e6948b660fb774ddd7ebdba9b6562a5 src/docker/client.rs
4c2c988191a97f81568a8df64af772463d010844f1abf327871901cdb32d4336 src/docker/model.rs 4c2c988191a97f81568a8df64af772463d010844f1abf327871901cdb32d4336 src/docker/model.rs
5f4610c59b2d4ff6a205a2d9ae11bc84be689b2a68302459f546b3f43e53593c src/docker/reconcile.rs 5f4610c59b2d4ff6a205a2d9ae11bc84be689b2a68302459f546b3f43e53593c src/docker/reconcile.rs
a12c47df4b90b6fc36c978a42477e6ddffbbbf69aed7252101c2fd829709bd6e src/docker/tests.rs a12c47df4b90b6fc36c978a42477e6ddffbbbf69aed7252101c2fd829709bd6e src/docker/tests.rs
2a02ea2e010cde1b1fe99b2e6baa80ea344ea6b332dbb3c4be795b71541f64a7 src/forwarding.rs b45d6040810c93ae2acb04ef6b0a59c189751edf9008cd81a4521c9e04e7f07d src/docker.rs
057e652f216a1478638cbb53b1c64ac88c7f413a65825caacbf893c5af004a63 src/forwarding/tests.rs 057e652f216a1478638cbb53b1c64ac88c7f413a65825caacbf893c5af004a63 src/forwarding/tests.rs
af7a4e91c32ffbe4701c8c06e48ba3f8bbbe55e8b9e877aecb30ccdf858e9e8f src/geoip.rs 2a02ea2e010cde1b1fe99b2e6baa80ea344ea6b332dbb3c4be795b71541f64a7 src/forwarding.rs
80850f7acd1ed4c0048b065e9e81740869a8efbb0d9b5d2e83d8ec7fbc494703 src/geoip/mmdb.rs 80850f7acd1ed4c0048b065e9e81740869a8efbb0d9b5d2e83d8ec7fbc494703 src/geoip/mmdb.rs
06aa056d789742cc11c8826c338d985e6a6fb22da6c3530937eec38e35478034 src/geoip/mmdb_tests.rs 06aa056d789742cc11c8826c338d985e6a6fb22da6c3530937eec38e35478034 src/geoip/mmdb_tests.rs
d3dc58628683472ef4d205fa5c42f0b7ef83cf90846146d96941cd3b36a40766 src/geoip/tests.rs d3dc58628683472ef4d205fa5c42f0b7ef83cf90846146d96941cd3b36a40766 src/geoip/tests.rs
70091bdf6f38f03b6df8c8c4407c300677e57f293130122503f936500963d37c src/geoip/update.rs 70091bdf6f38f03b6df8c8c4407c300677e57f293130122503f936500963d37c src/geoip/update.rs
c1b49bef28d87712e4c46138e70b52c4bcfb1a00196579031fac043d0b4aaa7b src/geoip/update_http.rs c1b49bef28d87712e4c46138e70b52c4bcfb1a00196579031fac043d0b4aaa7b src/geoip/update_http.rs
9a912dad0b8d1e10410fd95d0da3684656092ef85cf8b23eda4ffe7fb92921d8 src/geoip/update_tests.rs 9a912dad0b8d1e10410fd95d0da3684656092ef85cf8b23eda4ffe7fb92921d8 src/geoip/update_tests.rs
f6ebafafa82391f8cb3b5a09331f3762b6ca5a9fb64bafb5772cde3ab814dfcf src/headers.rs af7a4e91c32ffbe4701c8c06e48ba3f8bbbe55e8b9e877aecb30ccdf858e9e8f src/geoip.rs
e4f07bfb7eb75b6897ef812870caff158679a3046d64582428abe8c6df490dbe src/headers/tests.rs e4f07bfb7eb75b6897ef812870caff158679a3046d64582428abe8c6df490dbe src/headers/tests.rs
9e21edc67a14ef8a2ce3dda0ed18784d0fd0be305973e5d216058b7ac802c6cb src/http3.rs f6ebafafa82391f8cb3b5a09331f3762b6ca5a9fb64bafb5772cde3ab814dfcf src/headers.rs
eb57117938a447d9a6c2ec1806ebd9fecc640a68c88950a8b35fa9bad14d0dc6 src/http3/body.rs eb57117938a447d9a6c2ec1806ebd9fecc640a68c88950a8b35fa9bad14d0dc6 src/http3/body.rs
ed25da264a2d25b40c4fcddc97c626a3a43d4f3a9be6c6d31af58b9e4925c198 src/http3/diagnostics.rs ed25da264a2d25b40c4fcddc97c626a3a43d4f3a9be6c6d31af58b9e4925c198 src/http3/diagnostics.rs
37f54961d8f52b79a75fb2be31e81f4fdb1e66b06350345c4483d6197b8a0bbc src/http3/identity.rs 37f54961d8f52b79a75fb2be31e81f4fdb1e66b06350345c4483d6197b8a0bbc src/http3/identity.rs
f2a8c2a98f4ccfb708f9d75a48ec7b351232be9b8a4364b7e63ba14d28eead7c src/http3/settings.rs f2a8c2a98f4ccfb708f9d75a48ec7b351232be9b8a4364b7e63ba14d28eead7c src/http3/settings.rs
a31db7eac12b578b1eb5a74fae98bbe36312da95b7d7173135dbc41fc454da21 src/http3/tests.rs a31db7eac12b578b1eb5a74fae98bbe36312da95b7d7173135dbc41fc454da21 src/http3/tests.rs
9e21edc67a14ef8a2ce3dda0ed18784d0fd0be305973e5d216058b7ac802c6cb src/http3.rs
68fb5ab6a889995a66ad12cb6acf88c5d04f0aca572e2ae15f1a9cd171a5f24b src/lib.rs 68fb5ab6a889995a66ad12cb6acf88c5d04f0aca572e2ae15f1a9cd171a5f24b src/lib.rs
2551189a3e5701371026c9abd2c5d44d37495f28fb67e220180421bfd315e3dd src/logging.rs
18c8d74c21a47f234be93551d7264b1f8f8c00d1bbfbe555d91618c58cea0740 src/logging/error_runtime.rs 18c8d74c21a47f234be93551d7264b1f8f8c00d1bbfbe555d91618c58cea0740 src/logging/error_runtime.rs
87948a511b680e1c274eb46d13b938b45792e63bd6cff1621afe3477708ad2a8 src/logging/format.rs 33b4b7f69560c2acc0de0eac62604161d38dbd7044c5789a1352f0cd3882ea70 src/logging/format.rs
f5815d778cc1785723faed4abb15f9880f22b6524b5289fe1e7158905aac0986 src/logging/model.rs f5815d778cc1785723faed4abb15f9880f22b6524b5289fe1e7158905aac0986 src/logging/model.rs
42fef80af50735cd566c6801dad86ba81be98cf4a8ec7d45c2ea26f53f556a22 src/logging/runtime.rs 42fef80af50735cd566c6801dad86ba81be98cf4a8ec7d45c2ea26f53f556a22 src/logging/runtime.rs
584782215ea78a88526cdb5515031d8d5b87910875e26c0024b93a06680f6791 src/logging/tests.rs 584782215ea78a88526cdb5515031d8d5b87910875e26c0024b93a06680f6791 src/logging/tests.rs
3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs 2551189a3e5701371026c9abd2c5d44d37495f28fb67e220180421bfd315e3dd src/logging.rs
fc8e99eac83c083d65101f6402aacbcc8392d66508c31566a50de3ebcb12e7da src/main/admin_runtime.rs fc8e99eac83c083d65101f6402aacbcc8392d66508c31566a50de3ebcb12e7da src/main/admin_runtime.rs
42a9197475823f67c1ca762951ede5d7a2f10124bd6ed4d1a3365f4297be894c src/main/bootstrap.rs 42a9197475823f67c1ca762951ede5d7a2f10124bd6ed4d1a3365f4297be894c src/main/bootstrap.rs
5a98d0e249338e73aa57ae2e352dc16db2f79c2d042c1544ce1c2022da76a31c src/main/proxy_runtime.rs 5a98d0e249338e73aa57ae2e352dc16db2f79c2d042c1544ce1c2022da76a31c src/main/proxy_runtime.rs
4dd7c1084a6214c0f2513ebf86fe8ef144470695557772bfe25979edd64491ab src/main/shutdown.rs
0a6578a0f4023b9f8d73043bf942dc5e096ef9afb8c0d2f131c14f65d08ac01d src/main/shutdown/tests.rs 0a6578a0f4023b9f8d73043bf942dc5e096ef9afb8c0d2f131c14f65d08ac01d src/main/shutdown/tests.rs
4dd7c1084a6214c0f2513ebf86fe8ef144470695557772bfe25979edd64491ab src/main/shutdown.rs
e5c2e9189777725f4afa1c3c052e4f75047ec3f2f486630d990f5d3c194bcc96 src/main/tasks.rs e5c2e9189777725f4afa1c3c052e4f75047ec3f2f486630d990f5d3c194bcc96 src/main/tasks.rs
49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs 3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs
201ab702722d6d21585fe8bb49939150d61bfac7ed8dc198b98c2cc6098ffb5f src/metrics/global.rs 201ab702722d6d21585fe8bb49939150d61bfac7ed8dc198b98c2cc6098ffb5f src/metrics/global.rs
49b11df65b4fb0b5b727f9f867436584680b45e3f4fbd53f74bc0cab8b9a88ec src/metrics/vhost.rs
a54b5b34ea65f29723b89b81bf7968a238e8247b9c9760f3430d3504cb1017c9 src/metrics/vhost/tests.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 c915d30bbd7032d867e7916b4cd45f3bb7e7604d3f4558f4383c59394b4300c6 src/not_found/tests.rs
d66936cc15fd5d56566d40fa8cab93f80d94b05f10ac44478a30f63572a95e32 src/pages.rs a4702c95c7e5077c2ccff86e2e3216e1da6ff9ee4383ae199a0aff2f78db325f src/not_found.rs
da40e8ece87245efc49a0c4e750526b1d19fd97565856bd5caf641fcc1dfe18d src/pages/tests.rs da40e8ece87245efc49a0c4e750526b1d19fd97565856bd5caf641fcc1dfe18d src/pages/tests.rs
0dba7b6a33866cc7bbe05509b01961e545892645d1dad4eb88ac8a4bec65e3bf src/prometheus.rs d66936cc15fd5d56566d40fa8cab93f80d94b05f10ac44478a30f63572a95e32 src/pages.rs
cc6fb14351b009a16d07ed0f412c2bcc68cd7e551421aea03d8d97ceec9bee40 src/prometheus/core.rs cc6fb14351b009a16d07ed0f412c2bcc68cd7e551421aea03d8d97ceec9bee40 src/prometheus/core.rs
c46882ed26742d93806a976257674326e105b8ca1fc23f5ca81e317b268057aa src/prometheus/docker_misc.rs c46882ed26742d93806a976257674326e105b8ca1fc23f5ca81e317b268057aa src/prometheus/docker_misc.rs
d3c6b08088df0003144544936a55e2a3661aac85292821b594f00adfd19a2951 src/prometheus/model.rs d3c6b08088df0003144544936a55e2a3661aac85292821b594f00adfd19a2951 src/prometheus/model.rs
@@ -417,141 +425,144 @@ d3c6b08088df0003144544936a55e2a3661aac85292821b594f00adfd19a2951 src/prometheus
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
fb02fb31ab62ac47c998d418fb8fe2f1d810a451b68805f31d6ecc244ae20820 src/proxy/body.rs 0dba7b6a33866cc7bbe05509b01961e545892645d1dad4eb88ac8a4bec65e3bf src/prometheus.rs
b8fb54359a7c60e5a8bd8ca45a5a64641633c595f0deb84d6d906c31fbbc1312 src/proxy/body/tests.rs f0f3e21350e4fb15fa2ae282da6b290857dcd6716e62b183e9ecb728b7c24c63 src/proxy/body/tests.rs
c3dcd88f4b00a9dbcbb65f6fa70ee2ab98c2fba20e9a2dde3b8852306111836f src/proxy/cache_helpers.rs 4c7199c04e6d314e89825f80feaab35a2efb3b893511f70da0cda46ab1d0bc9e src/proxy/body.rs
b88d1d7a6837a2a470e5863f1f3247878daee62791fc3d8e72baeb66469bdfbf src/proxy/cache_helpers.rs
aa49c9928bb91e3d5bae0d340d88396a0417b9edec6192d41e1403533319a897 src/proxy/cache_http_invalidation.rs aa49c9928bb91e3d5bae0d340d88396a0417b9edec6192d41e1403533319a897 src/proxy/cache_http_invalidation.rs
676c1b325fd8e55bcdd86451c7d1d8b8d347ca00e8da6b3e5b148a58c376a9b4 src/proxy/cache_plan.rs
3404b03096862de1050088f62cdd811ea1ab61d191f902501173107f81f23b9b src/proxy/cache_plan/tests.rs 3404b03096862de1050088f62cdd811ea1ab61d191f902501173107f81f23b9b src/proxy/cache_plan/tests.rs
771cc1a38927f96e593b144dbab66cd64a5dc0ed42d54b1615c359845b2b7223 src/proxy/error_fallback.rs 676c1b325fd8e55bcdd86451c7d1d8b8d347ca00e8da6b3e5b148a58c376a9b4 src/proxy/cache_plan.rs
a8848cbdf85368548b003af2b43823c453866c472af4c7d2ff0ec7da14c44c8d src/proxy/error_fallback.rs
bf2e6bbd64fb45fceed54af0bff1c1e00da22b79c64dd8f5c23d2af0dbab0b7d src/proxy/forward/cache_lookup.rs bf2e6bbd64fb45fceed54af0bff1c1e00da22b79c64dd8f5c23d2af0dbab0b7d src/proxy/forward/cache_lookup.rs
cfa941b60b5d167ec9d8e2566ea3342b588d1a9078a6ca711ad6062ddf00fec9 src/proxy/forward/response.rs cfa941b60b5d167ec9d8e2566ea3342b588d1a9078a6ca711ad6062ddf00fec9 src/proxy/forward/response.rs
b4be19f2dccc97270f3596d7e56dc60237b7d4d254cd9362a355812026d90295 src/proxy/forward/route_cache.rs 8168a42c3992d002e87fdd5d52a37f53dcdf69cbc859221350d888e94c305ab8 src/proxy/forward/route_cache.rs
96c97928986ca89c2d6d415d5b107b9cc7228410bae93a2c08c24974324f4d8d src/proxy/forward/success_finish.rs 96c97928986ca89c2d6d415d5b107b9cc7228410bae93a2c08c24974324f4d8d src/proxy/forward/success_finish.rs
0a921474ef2b93eabdaf3888651cf29130eace5ccb1e0f5d6bc25e6aa69280be src/proxy/forward/success_prepare.rs 0a921474ef2b93eabdaf3888651cf29130eace5ccb1e0f5d6bc25e6aa69280be src/proxy/forward/success_prepare.rs
a064cf54ee62a374c1753f7a64940990a591888efc4516a07942c52c4e0842e2 src/proxy/forward/upstream.rs bb24da9c9ab986a9c4d4144d4d1be0f661561d4afc755f298b58bc679b182850 src/proxy/forward/upstream.rs
1f36225516caefc756a49bbdf37b227a7ea97b72ab78df021c9234e5c3dd41fc src/proxy/forward/web_source.rs 11dccc239ac8a8e9ea06e884c2f2658f86b7807d14a59b8e052bc7dd75e86d63 src/proxy/forward/web_source.rs
a9c011061585c067f04f7885215d53e6c40474bdced15d2be5a5ba65190910a0 src/proxy/forward_entry.rs facde401ba5bcbafdcea5524f236cfae37eec11144ceeea879e83314c84bc97e src/proxy/forward_entry.rs
9b25f5e3ea4443a63ee6f45d12e1957dd316fe8e85325fc00ea416faaab1025a src/proxy/h2_graceful.rs 9b25f5e3ea4443a63ee6f45d12e1957dd316fe8e85325fc00ea416faaab1025a src/proxy/h2_graceful.rs
c438066616527a3108f2ad53e3698ae9b2a80961714394cdd0a450c92f99d992 src/proxy/headers.rs
5cb7522ceb9a9f083a15596ac4898559295e38d1138c80099577211de8a868ec src/proxy/headers/tests.rs 5cb7522ceb9a9f083a15596ac4898559295e38d1138c80099577211de8a868ec src/proxy/headers/tests.rs
c438066616527a3108f2ad53e3698ae9b2a80961714394cdd0a450c92f99d992 src/proxy/headers.rs
3f25429418941eb72c3847125340db74684ad316eb278ecf658953026fd41f4f src/proxy/mod.rs 3f25429418941eb72c3847125340db74684ad316eb278ecf658953026fd41f4f src/proxy/mod.rs
55133ceace2e5262d9c54fa41bea099a27b817d1e09c1cd0e2493fd9cee4da38 src/proxy/redirect_helpers.rs 55133ceace2e5262d9c54fa41bea099a27b817d1e09c1cd0e2493fd9cee4da38 src/proxy/redirect_helpers.rs
c61738672f210e97333c8c89f9b35f8db454cbc1c36955288dfa4cfff2bbbc04 src/proxy/request_id.rs
6ff45990f6beb914e89924bec2c41022f3240a14357cf66ec4f8ada414a49767 src/proxy/request_id/tests.rs 6ff45990f6beb914e89924bec2c41022f3240a14357cf66ec4f8ada414a49767 src/proxy/request_id/tests.rs
c61738672f210e97333c8c89f9b35f8db454cbc1c36955288dfa4cfff2bbbc04 src/proxy/request_id.rs
8ad3619359f3befa14ea71ba43c638fea8f4d8a859cd07e4ed505731b8e32e72 src/proxy/tests/authority_tests.rs 8ad3619359f3befa14ea71ba43c638fea8f4d8a859cd07e4ed505731b8e32e72 src/proxy/tests/authority_tests.rs
1aa9bbbfb2659facdaeaf185e11221b1c8cd5fe6e23c7c1dacd3aa55e3db4fcb src/proxy/tests/cache_fast_path_tests.rs 1aa9bbbfb2659facdaeaf185e11221b1c8cd5fe6e23c7c1dacd3aa55e3db4fcb src/proxy/tests/cache_fast_path_tests.rs
0f2eacadf1447db8aa1c1a78005aa262e0f747096bfe7e0a0e41a2cd31a0d17f src/proxy/tests/h3_navigation_tests.rs 0f2eacadf1447db8aa1c1a78005aa262e0f747096bfe7e0a0e41a2cd31a0d17f src/proxy/tests/h3_navigation_tests.rs
d134b0eb617f82f9971b3f3461379ea37c57bd0079bf54748d0c434b2ef1b475 src/proxy/tests/http_invalidation_tests.rs d134b0eb617f82f9971b3f3461379ea37c57bd0079bf54748d0c434b2ef1b475 src/proxy/tests/http_invalidation_tests.rs
591b47fc40cbcb36d7c71998933dc873340dfcf2657e16c561ab64361c4dbab6 src/proxy/types.rs d5717fac0e7d2cb2939626eb95f4520241b677fe1f3e045df12c100315302a5a src/proxy/types.rs
8b053027bbdbbebb624164d9c1b15b2b8f5a13b8a23a9e1477ef4e840ce0334f src/proxy/upstream_helpers.rs 8d7848645c25932589d0dfc54149bb71a51edd7188aed60dc4d6ca8179b361ab src/proxy/upstream_helpers.rs
2d9142e3ae95895b5271b4009ab48236a3a2074ce51179c021193b35777560e4 src/proxy/web_cache_revalidation.rs
d7b4b809b12ee89293df45e9a64b1c9981fb3f89deaab3762f4a3ad3992dd7b1 src/proxy/web_cache_revalidation/tests.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 d866e7e72dee6ff54c2418c34e9899cbcd7e4e027d9cbe2b625044dc9062e4ee src/proxy/websocket/handshake.rs
7e9c4d4c207badabf90630223964bb2ad641f06d4149843cab751f192b8b4adf src/proxy/websocket/tests.rs 7e9c4d4c207badabf90630223964bb2ad641f06d4149843cab751f192b8b4adf src/proxy/websocket/tests.rs
a44421714aab37f7ceda19e3ec2512c645d2a3ce5dcb3da5bc7a0d87a442d6a8 src/proxy/websocket/tunnel.rs a44421714aab37f7ceda19e3ec2512c645d2a3ce5dcb3da5bc7a0d87a442d6a8 src/proxy/websocket/tunnel.rs
15fabd38576b61f650c039acdbf7ac5a5bd24330062475b94315285b2dc258b6 src/proxy/websocket/validation.rs 15fabd38576b61f650c039acdbf7ac5a5bd24330062475b94315285b2dc258b6 src/proxy/websocket/validation.rs
545247eb00b0116efbd4cc913cc251195a55feded1530e0d8d8945abbae78039 src/rate_limit.rs 69f775f6746b4c6d729dc10961dbef54168846f4ce9cf7fe453ab5a2b879c9b8 src/proxy/websocket.rs
e7e3a898e2c769b1ef11e58a13c1053104ada93dd694f8a03ea4b647c44c7d69 src/rate_limit/tests.rs e7e3a898e2c769b1ef11e58a13c1053104ada93dd694f8a03ea4b647c44c7d69 src/rate_limit/tests.rs
d40ad63939097d2b91339127d5835a74b618d83b7ba31db0c512f05a82107d6e src/redirect.rs 545247eb00b0116efbd4cc913cc251195a55feded1530e0d8d8945abbae78039 src/rate_limit.rs
811c83ae9fd0f5a13d35b5443cd37843f82b9d600bee4bb136ba9b87732a5347 src/redirect/tests.rs 811c83ae9fd0f5a13d35b5443cd37843f82b9d600bee4bb136ba9b87732a5347 src/redirect/tests.rs
d0939e1b0829c25356a0b67ecb3a178c74936dd07baa15fee24eb32544db6cad src/runtime.rs d40ad63939097d2b91339127d5835a74b618d83b7ba31db0c512f05a82107d6e src/redirect.rs
8d0d7b01871c8c588c40a54330a1053c85024655fbe38516ca6c94c3c19a8956 src/runtime/certificates.rs 8d0d7b01871c8c588c40a54330a1053c85024655fbe38516ca6c94c3c19a8956 src/runtime/certificates.rs
8d2443df8dab1ce6f11878e991b4928046b7362c417c5703f6d1f3a977646cbe src/runtime/constructors.rs 2ccafa8a5a8462417f65d05a0c6c42f294afa4ad1067ea0b93623181546d1f96 src/runtime/constructors.rs
92b4b284f1b69dca58e4e3f76199f0aecc397eed26d10123b427af1bb8a4a57e src/runtime/docker.rs 92b4b284f1b69dca58e4e3f76199f0aecc397eed26d10123b427af1bb8a4a57e src/runtime/docker.rs
6b2b57888863a396461fff202bfdaa6d8e869b2628cc171c0823f31f388e5178 src/runtime/tests.rs ee2050b9f8efb45a209c91d76043a1ba29c4289978896aaf7e7d344d48fdc369 src/runtime/tests.rs
4036b48925f5b25de70e21dfbb278488c20b39248dcda4cad4d19d5d7aed2b8d src/runtime/types.rs 779137f320133ec96f73c8b3d357ad4f0b825ee2367e9e8300f4f143b38eecb7 src/runtime/types.rs
e1d172acd2b230b8f4dd671cae8862a76f28416cbf0e43f86214a796edd90fab src/runtime/updates.rs 5947afaf7d070ea2047eb7969a8e0d84f981e91ace283f6506cf7685fb8afab9 src/runtime/updates.rs
208a378b14ca80e1412ffa19ef1b140594182971e2e797ee0bb95bcdb8404da7 src/secret_store.rs d0939e1b0829c25356a0b67ecb3a178c74936dd07baa15fee24eb32544db6cad src/runtime.rs
6dca8da072a3bd610704a391bb6e25c9042971f4ebc60edb47665ce1d74cbc58 src/secret_store/tests.rs 6dca8da072a3bd610704a391bb6e25c9042971f4ebc60edb47665ce1d74cbc58 src/secret_store/tests.rs
6fa1d8c352bce7a16ff0208bc4cdbe80404715df0aff4ed26910a5df57cbbcd4 src/security.rs 208a378b14ca80e1412ffa19ef1b140594182971e2e797ee0bb95bcdb8404da7 src/secret_store.rs
0e5fc2aff856bf6fd959cba12b5ddd1e3b3cbeb0fbaadde906c811cdfafdaae7 src/security/tests.rs 0e5fc2aff856bf6fd959cba12b5ddd1e3b3cbeb0fbaadde906c811cdfafdaae7 src/security/tests.rs
53a4bed4d1233aa434712b946857ea2050acaaf5de011c6dadca5b5c22155a83 src/server.rs 6fa1d8c352bce7a16ff0208bc4cdbe80404715df0aff4ed26910a5df57cbbcd4 src/security.rs
f2f6196632b2fdb2444189624f961edaf5bdfff57f700b0207b57e2491c1daa5 src/server/http.rs f2f6196632b2fdb2444189624f961edaf5bdfff57f700b0207b57e2491c1daa5 src/server/http.rs
050c48ea1127aa87dba92c074ab84e8346e65dbe253621ea53f8ca2f68d791ab src/server/http1_connection.rs
8c6ca5eaffb9acab57199d0349722703b903cfff3752baad2cf2596bd51188de src/server/listeners.rs 8c6ca5eaffb9acab57199d0349722703b903cfff3752baad2cf2596bd51188de src/server/listeners.rs
71899a9b1897fa22ab9e8eb6a670054a6d382bad78714006b9b218d07ba99902 src/server/serve.rs 4424a99b0b688d1119ee25626778b4804aedfaa0799a4a9bc0ee99bc5c7ba96b src/server/sendfile.rs
e9e478cf45b88994eaffd115f51764b6b07852ee3c941cafdabb20584d3dabc0 src/server/serve/tests.rs e9e478cf45b88994eaffd115f51764b6b07852ee3c941cafdabb20584d3dabc0 src/server/serve/tests.rs
048f2c7a8ef698a172ba7ed844558f4118b367b2cf340748c5c28da7723d8e85 src/server/tls_runtime.rs 71899a9b1897fa22ab9e8eb6a670054a6d382bad78714006b9b218d07ba99902 src/server/serve.rs
73ec453ac2850f6963f5bc33401cb8c92aa26d5123f15d68e6633860d91f265e src/settings.rs 960a82e02f09d75141fe3c1eecca915b66f17fdc8167196cd2fe21d5e6859376 src/server/tls_runtime.rs
98c52aa3b50d2e211ab752ccaf353c9a8c1500a87f9c1684b5dd0ba9d10e4619 src/server.rs
240ecdd70e8662ff3b84c21830dc0cbbbacadd755d56146e54214bc2887f068c src/settings/public_origin.rs 240ecdd70e8662ff3b84c21830dc0cbbbacadd755d56146e54214bc2887f068c src/settings/public_origin.rs
1f1121c3562c8f8f3fdee2113be2afffb8849be2d303e4c4409f87478b6e3307 src/settings/tests.rs 1f1121c3562c8f8f3fdee2113be2afffb8849be2d303e4c4409f87478b6e3307 src/settings/tests.rs
9dbf814870b8636770f579c3d36d16f5b6afe8bc66adfad356c26d7ae7dbce45 src/stream.rs 73ec453ac2850f6963f5bc33401cb8c92aa26d5123f15d68e6633860d91f265e src/settings.rs
254840126be2310426f6332f5efbe98147d9d8de1999731f40a3d13325c1dc6c src/stream/health.rs 254840126be2310426f6332f5efbe98147d9d8de1999731f40a3d13325c1dc6c src/stream/health.rs
bdc56be7dae85a97a4d5b8d46ef31ec4177292b1cadd507fe423b6d4ee1403bd src/stream/model.rs bdc56be7dae85a97a4d5b8d46ef31ec4177292b1cadd507fe423b6d4ee1403bd src/stream/model.rs
de138a4ac68bcddba3a4f17ceba008bab643359d76a68726a755069f19334bed src/stream/pool.rs de138a4ac68bcddba3a4f17ceba008bab643359d76a68726a755069f19334bed src/stream/pool.rs
6ec42b8647c95a0c2025462e0be29c71481ce1c3b1f4d4193926471f1cf06be0 src/stream/runtime.rs 6ec42b8647c95a0c2025462e0be29c71481ce1c3b1f4d4193926471f1cf06be0 src/stream/runtime.rs
9346cbd71e006e41a7188f28eb86f9b00bac822e4cb31c76c8d6d2ec5d4c4711 src/stream/tests.rs 9346cbd71e006e41a7188f28eb86f9b00bac822e4cb31c76c8d6d2ec5d4c4711 src/stream/tests.rs
3070d78a7b88c2e47ed59960fa35e7d220a4cd7d78e48f6c6d2bcc5ff77f9620 src/system_stats/linux.rs 9dbf814870b8636770f579c3d36d16f5b6afe8bc66adfad356c26d7ae7dbce45 src/stream.rs
d9123c623e2a97d09e41edaa29993b4b2c8cba39a3abdf2b48a205aa9a9c2939 src/system_stats/linux/tests.rs d9123c623e2a97d09e41edaa29993b4b2c8cba39a3abdf2b48a205aa9a9c2939 src/system_stats/linux/tests.rs
3070d78a7b88c2e47ed59960fa35e7d220a4cd7d78e48f6c6d2bcc5ff77f9620 src/system_stats/linux.rs
751d59582fb92c84e0c0cc7c0855c9d0a545805c9ba3be61826e50fb96c297ed src/system_stats/mod.rs 751d59582fb92c84e0c0cc7c0855c9d0a545805c9ba3be61826e50fb96c297ed src/system_stats/mod.rs
050aa831b1012c4aaade63863f433f01bfacf5d3f95787e57975572116a30ab2 src/system_stats/tests.rs 050aa831b1012c4aaade63863f433f01bfacf5d3f95787e57975572116a30ab2 src/system_stats/tests.rs
7467d83066bb6986d8c9103597f7a7617ac677579ed46d573c573ff6ca57bbd9 src/tinyauth.rs
0e1d87554db55e724f30c41ec4a5c80bdb8fc9c8cba8700ac509864d4abf8871 src/tinyauth/tests.rs 0e1d87554db55e724f30c41ec4a5c80bdb8fc9c8cba8700ac509864d4abf8871 src/tinyauth/tests.rs
1084cccd9de28acd6cb0f0813de7c0953910f51726f73f4acdb2e3ada4593976 src/tls.rs 7467d83066bb6986d8c9103597f7a7617ac677579ed46d573c573ff6ca57bbd9 src/tinyauth.rs
4972a436f8878dec5f9eb8ff17eb82670b0bfd9e94e821c516575479f5fc1919 src/tls/certificates.rs 4972a436f8878dec5f9eb8ff17eb82670b0bfd9e94e821c516575479f5fc1919 src/tls/certificates.rs
436575144aa352498a4696f12c2258bef3a898f3f50e6fbc6d67c3b93afa099f src/tls/external_paths.rs 436575144aa352498a4696f12c2258bef3a898f3f50e6fbc6d67c3b93afa099f src/tls/external_paths.rs
3e6947dde167f82a5efc407ccf9bd4c9f5b26cc8633d1418a10284bc1a1503d4 src/tls/server_config.rs 3e6947dde167f82a5efc407ccf9bd4c9f5b26cc8633d1418a10284bc1a1503d4 src/tls/server_config.rs
31c0f51a3a0bab433f438c3ba1b1cdaf3c63fae6086e7d99b3464a537f75a5e1 src/tls/tests.rs
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
31c0f51a3a0bab433f438c3ba1b1cdaf3c63fae6086e7d99b3464a537f75a5e1 src/tls/tests.rs
10c4a187e3aeb0c72a8c8968ea0cc8bfae4e3cb2f3d834ebd3da1056c4171ec5 src/tls/types.rs 10c4a187e3aeb0c72a8c8968ea0cc8bfae4e3cb2f3d834ebd3da1056c4171ec5 src/tls/types.rs
c2f802d95da4332fb8480e83f0c7502334457b3408141c1c4516c9ba320a0463 src/traffic.rs 1084cccd9de28acd6cb0f0813de7c0953910f51726f73f4acdb2e3ada4593976 src/tls.rs
36301bcbd20b557dd3d58e77a9175f3581947f577c0bb0b972899cec6786b03f src/traffic/tests.rs 36301bcbd20b557dd3d58e77a9175f3581947f577c0bb0b972899cec6786b03f src/traffic/tests.rs
c2f802d95da4332fb8480e83f0c7502334457b3408141c1c4516c9ba320a0463 src/traffic.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
5478670cef87777b14a2dab308f5a9182df56d7ea2f5625bdc94aa7e3d661503 src/transform/html.rs
aa99b0d3029ef92c78371cb90e11f2026cce890d1be8e10ebd7fa9bc5cb13287 src/transform/html/tests.rs aa99b0d3029ef92c78371cb90e11f2026cce890d1be8e10ebd7fa9bc5cb13287 src/transform/html/tests.rs
a1d2fec07617c8a8dc425bfaeaf77c844152a0a174568a88a5baf4c317cc6c5c src/transform/js.rs 5478670cef87777b14a2dab308f5a9182df56d7ea2f5625bdc94aa7e3d661503 src/transform/html.rs
cf5195ab6bacccae0082da7b3f1bd0382b73fdeb3607f4ae569ca6daab9d10d0 src/transform/js/tests.rs cf5195ab6bacccae0082da7b3f1bd0382b73fdeb3607f4ae569ca6daab9d10d0 src/transform/js/tests.rs
226ae561dc7b0243c04e55e28cdfd2b76acd48cf614d1919b81c196660b03cfb src/transform/json.rs a1d2fec07617c8a8dc425bfaeaf77c844152a0a174568a88a5baf4c317cc6c5c src/transform/js.rs
216e24e29ea87be4a124f95f898e155f127fd959d7d5d9b30f8cd0b2b5018f30 src/transform/json/tests.rs 216e24e29ea87be4a124f95f898e155f127fd959d7d5d9b30f8cd0b2b5018f30 src/transform/json/tests.rs
226ae561dc7b0243c04e55e28cdfd2b76acd48cf614d1919b81c196660b03cfb src/transform/json.rs
6a8c28be93fb590942493c075b29ace1527981542bd183f232aa82b62e951eca src/transform/mod.rs 6a8c28be93fb590942493c075b29ace1527981542bd183f232aa82b62e951eca src/transform/mod.rs
fde2fa3aa94b30d8536afec1c11348cfbd922d0b5ff89e91d34266e8e3863e26 src/transform/model.rs fde2fa3aa94b30d8536afec1c11348cfbd922d0b5ff89e91d34266e8e3863e26 src/transform/model.rs
a4e1d2aa2913587c250f251356431097b76995aee6e8bf9e35c4978a95491de1 src/transform/svg.rs
0f53c64f442390ee20a59087a10f8f9ffb58053263e9aa848ddabfae1a6f3f51 src/transform/svg/tests.rs 0f53c64f442390ee20a59087a10f8f9ffb58053263e9aa848ddabfae1a6f3f51 src/transform/svg/tests.rs
a4e1d2aa2913587c250f251356431097b76995aee6e8bf9e35c4978a95491de1 src/transform/svg.rs
d1f3f53fedae0d8ef69eeee457f7d30ed5a670a1ed4ced60683400ee46680260 src/transform/tests.rs d1f3f53fedae0d8ef69eeee457f7d30ed5a670a1ed4ced60683400ee46680260 src/transform/tests.rs
6146c59f36cf419623747d497b3de685da6c84964df43317bebdcb21deb9ee09 src/update_check.rs
c42d000d1d576721af3ffe930589388693001b8113554a7658f6dcbc665b5d1b src/update_check/tests.rs c42d000d1d576721af3ffe930589388693001b8113554a7658f6dcbc665b5d1b src/update_check/tests.rs
92efcdaafb9f5a8514daf63d7fcde29be977d7bdcd90162eda0ce00d6967ffbd src/upstream.rs 6146c59f36cf419623747d497b3de685da6c84964df43317bebdcb21deb9ee09 src/update_check.rs
ca695e246bee5389b3757fe8588e48bff5d0ad473bc8588ad4ba7b52c99d7dfa src/upstream/client.rs ca695e246bee5389b3757fe8588e48bff5d0ad473bc8588ad4ba7b52c99d7dfa src/upstream/client.rs
ad29f3fc15c9b627b5d752af5adc9f99ee1364fec2df63f1e4509cb223988efb src/upstream/client_tests.rs ad29f3fc15c9b627b5d752af5adc9f99ee1364fec2df63f1e4509cb223988efb src/upstream/client_tests.rs
75fef0a488d3f3dabfa03c2eabc5995442dfb3c9a1fde8eb6653a124dda25d43 src/upstream/protocol_tests.rs 75fef0a488d3f3dabfa03c2eabc5995442dfb3c9a1fde8eb6653a124dda25d43 src/upstream/protocol_tests.rs
8a112154b9631f62a54584302a3ff6a64e4f177ce79aba678b4cb23e466c6ea3 src/upstream/tests.rs 8a112154b9631f62a54584302a3ff6a64e4f177ce79aba678b4cb23e466c6ea3 src/upstream/tests.rs
a6b4905c245a22eafb1aa535d303d0fa24e4353db94e1d67fd42b825bf8c5adb src/user_agent.rs 92efcdaafb9f5a8514daf63d7fcde29be977d7bdcd90162eda0ce00d6967ffbd src/upstream.rs
6d7cb73de7d8c6f3780736480f5f81ef08813f4626fd5ed494aca0552cf214a1 src/user_agent/tests.rs 6d7cb73de7d8c6f3780736480f5f81ef08813f4626fd5ed494aca0552cf214a1 src/user_agent/tests.rs
3aa1b07cd26d4a5b9e5e94d1d6a712ef1397d827e7f1e8c01f7195b98d2c5f67 src/vhost.rs a6b4905c245a22eafb1aa535d303d0fa24e4353db94e1d67fd42b825bf8c5adb src/user_agent.rs
6678192de56bc10c5cf82bdff8da14eece7685f409592450c07478333aa3b99c src/vhost/error_fallback.rs 6678192de56bc10c5cf82bdff8da14eece7685f409592450c07478333aa3b99c src/vhost/error_fallback.rs
63e9fc7f7be78dc3e16a89f015a56a2da6ba3aff0197b70ff48321dfb17529bb src/vhost/model.rs 63e9fc7f7be78dc3e16a89f015a56a2da6ba3aff0197b70ff48321dfb17529bb src/vhost/model.rs
35a0aca3eee8d47d1bab27c83d4532cd572fc3e17b5f0d4a8d4515a8f85b3d96 src/vhost/redirect.rs 35a0aca3eee8d47d1bab27c83d4532cd572fc3e17b5f0d4a8d4515a8f85b3d96 src/vhost/redirect.rs
9b16b5307e140db97363f56c9fac8319e56df846bb1c919d3bc06091ab6ce69c src/vhost/tests.rs 9b16b5307e140db97363f56c9fac8319e56df846bb1c919d3bc06091ab6ce69c src/vhost/tests.rs
b7d0f7f5d0bbd74add3fda9b1308350c4f70f8b6debf979fb977a4f2f659c32f src/vhost/validation.rs b7d0f7f5d0bbd74add3fda9b1308350c4f70f8b6debf979fb977a4f2f659c32f src/vhost/validation.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
8d1eff4b24f01d137414ef5419f75dd281879228adb70409645b11b3c78c20e9 src/web_server/config.rs 8d1eff4b24f01d137414ef5419f75dd281879228adb70409645b11b3c78c20e9 src/web_server/config.rs
aa38e89e84f928bd1952d3e2cb67e48ef2e92c5f444c095b6288034adc5e3d3d src/web_server/config/compile.rs
7853f6ec127a91163a4418ca45b93a92f424d5c618f27b29f9d526598a26f7ef src/web_server/config/model.rs
d9178aa934ee1a7236cb04d864fa842faf64156e9103786e3103c92bc4e18fde src/web_server/config/validation.rs
86c744216d7132d8294a6e90ba9be5fd871646006313d1c62506d40c81db9b12 src/web_server/fastcgi.rs
200cba8cb3482e49db73d2c60cb60786175177c7d4be2a566cd281fec7ba7fca src/web_server/fastcgi/protocol.rs 200cba8cb3482e49db73d2c60cb60786175177c7d4be2a566cd281fec7ba7fca src/web_server/fastcgi/protocol.rs
cef44ccdf882d84892a64bdbfdc1ba01380fce5e5f86d2829b0f9613f738b964 src/web_server/fastcgi/response.rs cef44ccdf882d84892a64bdbfdc1ba01380fce5e5f86d2829b0f9613f738b964 src/web_server/fastcgi/response.rs
86c744216d7132d8294a6e90ba9be5fd871646006313d1c62506d40c81db9b12 src/web_server/fastcgi.rs
e15361622683268c5606a39a9e5db3938e630999767898391226e94ad2e421e8 src/web_server/filesystem.rs e15361622683268c5606a39a9e5db3938e630999767898391226e94ad2e421e8 src/web_server/filesystem.rs
d4fec3682532b0f2d11a0aefcd024f69fb623a39922ddb6c528a2d2de265347d src/web_server/htaccess.rs
ca3cd376b313af1ce0ee65081b3327eb213f000d5a215aa4ee6b764de9702126 src/web_server/htaccess/model.rs ca3cd376b313af1ce0ee65081b3327eb213f000d5a215aa4ee6b764de9702126 src/web_server/htaccess/model.rs
76de1d11df75f4a64556b2821fd0a1774d26c9f0ca0e5f2b5fcfad0722d43d47 src/web_server/htaccess/parse.rs 76de1d11df75f4a64556b2821fd0a1774d26c9f0ca0e5f2b5fcfad0722d43d47 src/web_server/htaccess/parse.rs
66260d159986d41eab0dfde2cb9f0713fc1cc209c2fb47e9023bd24fd37919e8 src/web_server/htaccess/rewrite.rs 66260d159986d41eab0dfde2cb9f0713fc1cc209c2fb47e9023bd24fd37919e8 src/web_server/htaccess/rewrite.rs
8ca74d9cc4470045301f70eb4a9fbabd0dbfe74434429148d2230df9f996c06c src/web_server/htaccess/tests.rs 8ca74d9cc4470045301f70eb4a9fbabd0dbfe74434429148d2230df9f996c06c src/web_server/htaccess/tests.rs
d4fec3682532b0f2d11a0aefcd024f69fb623a39922ddb6c528a2d2de265347d src/web_server/htaccess.rs
3341264f6e8041ac5a5d3c4b1601a69787386116019b5febc1a5e22bdb1186f9 src/web_server/metrics.rs 3341264f6e8041ac5a5d3c4b1601a69787386116019b5febc1a5e22bdb1186f9 src/web_server/metrics.rs
7b945065e636ca74147048aa53cc5b34028cced72a780fbaf0aa2499b00718fa src/web_server/mod.rs 0dbef9b71869fc52cf01e9cb400ffd48fb433ae625383edee6c6c2d735608a76 src/web_server/mod.rs
67ab8bef567452f123c4b9b46a942b39c93f1f765c128edca7ee808ff1c5e304 src/web_server/request.rs 67ab8bef567452f123c4b9b46a942b39c93f1f765c128edca7ee808ff1c5e304 src/web_server/request.rs
d98c80663847d6d83073df074ebff65952755415574708abcc153bd8d3adfaa9 src/web_server/response.rs 833c9fea744b9649046e1a64d6fc6972fbf6cea640e1943b0db52b7ccfa9faad src/web_server/response/file.rs
c89e412584cf524cd1cd332ce555ff922a9e5549cd5d20b2e9a29393456be889 src/web_server/response/file.rs
ba793a140699956a9aef6bc79b0c731c481af8ab7ac3d816a28695743febcb30 src/web_server/response/file_body.rs
c2e54fd803a10f74490f183e8c030fd0e3e72f5de4e79c55bc393ab57d90ab9a src/web_server/response/file_body/tests.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 20509f9302b79d3a7be76dda6edaf5711826d3ddf47d19ebe07cb289427ca328 src/web_server/response/headers.rs
d98c80663847d6d83073df074ebff65952755415574708abcc153bd8d3adfaa9 src/web_server/response.rs
305c0109b4f869a99381a142853f9e2205f5c7c4f904b2dfa4643274bfc542af src/web_server/routing.rs 305c0109b4f869a99381a142853f9e2205f5c7c4f904b2dfa4643274bfc542af src/web_server/routing.rs
400e1c8aa2fe857d96036a64499fbc3de44c1e3a53811c5b25101fb843359792 src/web_server/security.rs 400e1c8aa2fe857d96036a64499fbc3de44c1e3a53811c5b25101fb843359792 src/web_server/security.rs
54cf72a82e023fe46513b9cf3130adfef1cd96c40f790c533016b4825d542e06 src/web_server/serve.rs 54cf72a82e023fe46513b9cf3130adfef1cd96c40f790c533016b4825d542e06 src/web_server/serve.rs
bda82f531f61251772d3bb4bc67d98ca613aebd51b489b7cac1fd5086c653b1b src/web_server/tests.rs 23904fc0ca6d5e816b332303b148a2ec5ea3b226b5ff8f68db05c42954472a3e src/web_server/tests.rs
f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json
1725c10331745dc04a07675784d385260da7b14567b8dba3cbae5e891eb5dadf tests/http2.rs 1725c10331745dc04a07675784d385260da7b14567b8dba3cbae5e891eb5dadf tests/http2.rs
347a4ff9e028a1e999ad129590c355b5734cf928dd81e5099aea0c8aca8d5908 tests/http3.rs 347a4ff9e028a1e999ad129590c355b5734cf928dd81e5099aea0c8aca8d5908 tests/http3.rs
+216
View File
@@ -0,0 +1,216 @@
{
"kind": "HYPOTHETICAL_AMDAHL_SENSITIVITY_NOT_A_BENCHMARK",
"comparison": "Existing 0.33.8 measurements (5 runs, cache off) vs measured nginx",
"not_measured": "0.34.0 server performance and sendfile syscall/runtime correctness",
"conditions": [
"Identical CPU and network assumptions to supplied 0.33.8 baseline",
"WWW zero-copy is limited to Linux, cleartext HTTP/1.1, GET >=64KiB, no cache, range, transform, compression or TLS",
"WWW low/middle/high: file-copy contributes 20%/40%/60% of total request time; modeled sendfile eliminates 50% of that component",
"Reverse proxy low/middle/high: removing response-frame mutex saves a hypothetical 1%/3%/5% of total service time",
"These fractions are chosen sensitivity parameters; not measured or inferred from profiling",
"No change is assumed for WWW 1040B or out-of-scope protocol paths",
"No network saturation, TLS record processing, queueing, origin latency, filesystem cache misses, CPU contention or concurrency effects modeled",
"No additional speedup due to cache-retention changes is assumed"
],
"formula": "RPS_sim = RPS_0.33.8 / (1 - fraction_of_total_service_time_eliminated)",
"entries": [
{
"kind": "reverse_proxy",
"bytes": 1040,
"baseline_version": "0.33.8",
"baseline_rust_rps": 29334.12,
"baseline_nginx_rps": 170610.16,
"baseline_of_nginx_pct": 17.19,
"factor_to_match_nginx": 5.82,
"sendfile_eligible_only_if_plain_linux_http1_1_no_cache_no_compression": false,
"scenarios": {
"low": {
"modeled_total_time_saved_pct": 1.0,
"simulated_rps": 29630,
"simulated_rps_gain_pct": 1.01,
"simulated_of_nginx_pct": 17.37,
"simulated_remaining_gap_to_nginx_pct_points": 82.63
},
"middle": {
"modeled_total_time_saved_pct": 3.0,
"simulated_rps": 30241,
"simulated_rps_gain_pct": 3.09,
"simulated_of_nginx_pct": 17.73,
"simulated_remaining_gap_to_nginx_pct_points": 82.27
},
"high": {
"modeled_total_time_saved_pct": 5.0,
"simulated_rps": 30878,
"simulated_rps_gain_pct": 5.26,
"simulated_of_nginx_pct": 18.1,
"simulated_remaining_gap_to_nginx_pct_points": 81.9
}
}
},
{
"kind": "reverse_proxy",
"bytes": 65536,
"baseline_version": "0.33.8",
"baseline_rust_rps": 20735.07,
"baseline_nginx_rps": 35393.29,
"baseline_of_nginx_pct": 58.58,
"factor_to_match_nginx": 1.71,
"sendfile_eligible_only_if_plain_linux_http1_1_no_cache_no_compression": false,
"scenarios": {
"low": {
"modeled_total_time_saved_pct": 1.0,
"simulated_rps": 20945,
"simulated_rps_gain_pct": 1.01,
"simulated_of_nginx_pct": 59.18,
"simulated_remaining_gap_to_nginx_pct_points": 40.82
},
"middle": {
"modeled_total_time_saved_pct": 3.0,
"simulated_rps": 21376,
"simulated_rps_gain_pct": 3.09,
"simulated_of_nginx_pct": 60.4,
"simulated_remaining_gap_to_nginx_pct_points": 39.6
},
"high": {
"modeled_total_time_saved_pct": 5.0,
"simulated_rps": 21826,
"simulated_rps_gain_pct": 5.26,
"simulated_of_nginx_pct": 61.67,
"simulated_remaining_gap_to_nginx_pct_points": 38.33
}
}
},
{
"kind": "reverse_proxy",
"bytes": 1048576,
"baseline_version": "0.33.8",
"baseline_rust_rps": 4461.19,
"baseline_nginx_rps": 2730.23,
"baseline_of_nginx_pct": 163.4,
"factor_to_match_nginx": 0.61,
"sendfile_eligible_only_if_plain_linux_http1_1_no_cache_no_compression": false,
"scenarios": {
"low": {
"modeled_total_time_saved_pct": 1.0,
"simulated_rps": 4506,
"simulated_rps_gain_pct": 1.01,
"simulated_of_nginx_pct": 165.05,
"simulated_remaining_gap_to_nginx_pct_points": -65.05
},
"middle": {
"modeled_total_time_saved_pct": 3.0,
"simulated_rps": 4599,
"simulated_rps_gain_pct": 3.09,
"simulated_of_nginx_pct": 168.45,
"simulated_remaining_gap_to_nginx_pct_points": -68.45
},
"high": {
"modeled_total_time_saved_pct": 5.0,
"simulated_rps": 4696,
"simulated_rps_gain_pct": 5.26,
"simulated_of_nginx_pct": 172.0,
"simulated_remaining_gap_to_nginx_pct_points": -72.0
}
}
},
{
"kind": "www",
"bytes": 1040,
"baseline_version": "0.33.8",
"baseline_rust_rps": 24906.2,
"baseline_nginx_rps": 351605.13,
"baseline_of_nginx_pct": 7.08,
"factor_to_match_nginx": 14.12,
"sendfile_eligible_only_if_plain_linux_http1_1_no_cache_no_compression": false,
"scenarios": {
"low": {
"modeled_total_time_saved_pct": 0.0,
"simulated_rps": 24906,
"simulated_rps_gain_pct": 0.0,
"simulated_of_nginx_pct": 7.08,
"simulated_remaining_gap_to_nginx_pct_points": 92.92
},
"middle": {
"modeled_total_time_saved_pct": 0.0,
"simulated_rps": 24906,
"simulated_rps_gain_pct": 0.0,
"simulated_of_nginx_pct": 7.08,
"simulated_remaining_gap_to_nginx_pct_points": 92.92
},
"high": {
"modeled_total_time_saved_pct": 0.0,
"simulated_rps": 24906,
"simulated_rps_gain_pct": 0.0,
"simulated_of_nginx_pct": 7.08,
"simulated_remaining_gap_to_nginx_pct_points": 92.92
}
}
},
{
"kind": "www",
"bytes": 65536,
"baseline_version": "0.33.8",
"baseline_rust_rps": 20856.67,
"baseline_nginx_rps": 203709.91,
"baseline_of_nginx_pct": 10.24,
"factor_to_match_nginx": 9.77,
"sendfile_eligible_only_if_plain_linux_http1_1_no_cache_no_compression": true,
"scenarios": {
"low": {
"modeled_total_time_saved_pct": 10.0,
"simulated_rps": 23174,
"simulated_rps_gain_pct": 11.11,
"simulated_of_nginx_pct": 11.38,
"simulated_remaining_gap_to_nginx_pct_points": 88.62
},
"middle": {
"modeled_total_time_saved_pct": 20.0,
"simulated_rps": 26071,
"simulated_rps_gain_pct": 25.0,
"simulated_of_nginx_pct": 12.8,
"simulated_remaining_gap_to_nginx_pct_points": 87.2
},
"high": {
"modeled_total_time_saved_pct": 30.0,
"simulated_rps": 29795,
"simulated_rps_gain_pct": 42.86,
"simulated_of_nginx_pct": 14.63,
"simulated_remaining_gap_to_nginx_pct_points": 85.37
}
}
},
{
"kind": "www",
"bytes": 1048576,
"baseline_version": "0.33.8",
"baseline_rust_rps": 2833.71,
"baseline_nginx_rps": 12611.49,
"baseline_of_nginx_pct": 22.47,
"factor_to_match_nginx": 4.45,
"sendfile_eligible_only_if_plain_linux_http1_1_no_cache_no_compression": true,
"scenarios": {
"low": {
"modeled_total_time_saved_pct": 10.0,
"simulated_rps": 3149,
"simulated_rps_gain_pct": 11.11,
"simulated_of_nginx_pct": 24.97,
"simulated_remaining_gap_to_nginx_pct_points": 75.03
},
"middle": {
"modeled_total_time_saved_pct": 20.0,
"simulated_rps": 3542,
"simulated_rps_gain_pct": 25.0,
"simulated_of_nginx_pct": 28.09,
"simulated_remaining_gap_to_nginx_pct_points": 71.91
},
"high": {
"modeled_total_time_saved_pct": 30.0,
"simulated_rps": 4048,
"simulated_rps_gain_pct": 42.86,
"simulated_of_nginx_pct": 32.1,
"simulated_remaining_gap_to_nginx_pct_points": 67.9
}
}
}
]
}
+84
View File
@@ -0,0 +1,84 @@
# RustProxy 0.34.0 — reverse-proxy hot-path candidate
## What was changed
1. **Routes:** `Snapshot.routes` and `fallbacks` now hold
`Arc<CompiledRoute>` handles. The same compiled location is shared across
hostname aliases. `observe` resolves vhost/location once from the accepted
listener/TLS-state and stores a request-local handle. `forward_request`
consumes the same immutable snapshot/route; internal error fallbacks still
resolve the explicitly configured named location independently.
2. **Host matching:** wildcard candidate strings are generated lazily only
after an exact host failed, without changing longest-specific-suffix order.
3. **LB:** single-backend non-cookie pools use a direct admitted lease without
temporary candidate vectors or scheduler mutex. Multi-backend weighted
round-robin retains its ordering and mutex; least-connection/traffic,
adaptive, IP hash and cookie-affinity retain their behavior. IP hash avoids
repeated canonicalization/temporary `format!` strings for each candidate.
4. **Health:** a `healthy_cached` atomic allows checking healthy backends
without the health mutex; all state transitions remain inside the existing
state machine, including passive quarantine and half-open recovery.
Resolved active/passive per-member policies are held in shared immutable
`Arc` handles so a new request lease does not deep-clone health paths,
headers or optional host strings.
5. **Fallback/retry:** request body+HeaderMap replay templates are made only
for configured error fallbacks. Retry headers are cloned only for eligible
safe replay requests where another member or H2 graceful close makes retry
possible; writes, streaming bodies and WebSockets are not replayed.
6. **Telemetry:** three per-request mutex-backed scalar tags are now atomic;
their original event strings and `Option<bool>` log fields are decoded at
exchange completion. Request ID, method, host and path are moved into the
terminal log record instead of deep-cloned in Drop; the traffic sample
still receives its own required copies. Logs avoid a broadcast clone if
there are no subscribers, preserving history, sink and counters.
## Preserved boundaries
All vhost, ACL, GeoIP, Basic Auth, Tinyauth, WebSocket, retry, cache,
transform, compression, log, metrics, `sendfile`, PHP-FPM, HTTPS/H2/H3 and
admin panel configuration paths are intended to remain compatible. Defaults
and SQLite schema are unchanged; no new performance toggle was added. The
optimization reuses route objects within one immutable snapshot but does not
allow stale snapshot cache fills (existing revision ticket barrier remains).
Sticky cookies deliberately retain the general selection path, and upstream
request-count recycling is unchanged (default 25,000 per generation).
## Locally executed checks vs required checks
- **Executed locally:** scoped-cache/sendfile/hot-path *source-only* guards,
Python syntax, SQLite migration and upgrade preservation tests, selected JS
panel scripts and simulated editor tests. These do **not** prove Rust builds.
- **Prepared, NOT executed:** Rust unit tests for shared routing, single-member
LB capacity/health, passive ejection/recovery, atomic log fields, retained
sendfile, cache and protocol behavior.
- **Unmeasured:** 0.34.0 requests/sec, latency, CPU cost, memory and nginx gap.
Do not infer a throughput gain from fewer source-code locks/allocations.
## Build and acceptance
Run on the intended Linux builder with exactly the same compiler, dependencies,
release flags and workload as the 0.33.8 reference:
```bash
./dev.sh doctor
./dev.sh check
CARGO_INCREMENTAL=0 ./dev.sh test
CARGO_INCREMENTAL=0 cargo build --release --locked
python3 scripts/tests/proxy_hotpath_034_static_test.py
python3 scripts/tests/cache_scope_034_static_test.py
python3 scripts/tests/sendfile_034_static_test.py
python3 scripts/tests/dashboard_presets_sql_test.py
python3 scripts/tests/upgrade_performance_test.py
```
Next run `scripts/performance/regression.py`, `lb_regression.py`,
`transport_regression.py` and `concurrency_regression.py` against both binary
versions. After functional acceptance, run the paired
`scripts/performance/benchmark.py` suite against the unmodified baseline,
this candidate and nginx; use the instructions in
`scripts/performance/README.md` including separate CPU affinities.
Measure RPS/p95/p99, CPU microseconds per request, RSS, FDs, pool rotations,
health transitions, cache HIT ratio and logger drops. For each payload report
`(0.34.0 RPS / 0.33.8 RPS - 1)*100` and `0.34.0 RPS/nginx RPS*100` from real
medians. A source-only revision is **not production-accepted**.
+1
View File
@@ -42,6 +42,7 @@ This is the canonical index for the current project documentation. The authentic
- [MOCK_SERVER](MOCK_SERVER.md) - local Python mock origin used by development/integration checks. - [MOCK_SERVER](MOCK_SERVER.md) - local Python mock origin used by development/integration checks.
- [MOCK_AUTH](MOCK_AUTH.md) - local fake Tinyauth peer for development only. - [MOCK_AUTH](MOCK_AUTH.md) - local fake Tinyauth peer for development only.
- [MEMORY_STRESS](MEMORY_STRESS.md) - sustained-memory audit, offline load simulations, real RustProxy staging stress and RSS measurement procedure. - [MEMORY_STRESS](MEMORY_STRESS.md) - sustained-memory audit, offline load simulations, real RustProxy staging stress and RSS measurement procedure.
- [TEST_REPORT_0.34.0](TEST_REPORT_0.34.0.md) - 0.34.0 source-stage test evidence, simulated cache retention and required acceptance gates.
## Technical documentation ## Technical documentation
+173
View File
@@ -0,0 +1,173 @@
# RustProxy 0.34.0 verification report
## Acceptance status
**Source candidate, not verified production release.** Rust (`cargo`, `rustc`) is
not installed in this execution environment, and the runtime has no access to
its dependency registry. Consequently the modified Rust source was **not
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
UI/cache tests do not close the runtime acceptance gate.
## Later 0.34.0 reverse-proxy optimization revision
The refreshed source archive additionally reuses immutable route handles,
limits lazy host wildcard allocation, fast-paths one-backend non-cookie LB,
atomically caches healthy state and exchange scalar log fields, shares
compiled per-member health policies across leases, and avoids
unneeded fallback/retry header clones and empty-subscriber broadcast clones.
The existing cache/sendfile work and all panel controls remain. Added Rust
regression cases and the source-only hot-path guard are **not a substitute**
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`.
## Changes and correctness boundaries
- Operational per-vhost saves (upstream keepalive/recycling, connection limits,
load-balancing/health policy, timeouts, request gate and diagnostics) retain
HOT and COLD entries. Backend origin, routing, content, cache and transform
changes retain conservative invalidation. Vhost-specific changes purge only
that vhost; global cache, transform, forwarding, headers and Web Service or
PHP-FPM profile changes may conservatively purge all tiers.
- Scope-specific cold-cache records persist an origin/content compatibility
fingerprint. COLD startup rejects stale scope records and deletes their cache
artifacts. Legacy COLD metadata defaults its fingerprint to zero; production
vhosts have a nonzero SHA-derived fingerprint. A one-time cold refill can
occur on the 0.33.8 -> 0.34.0 upgrade because the global identity changed.
- A separate administrative configuration revision acts as a cache-ticket
publication barrier: a request bound to a pre-save routing snapshot cannot
create a new cache ticket after the save. The data revision stays stable for
operational-only saves, so previously cached objects remain available.
- Reverse-proxy response-body byte metering captures the backend lease once at
wrapper creation instead of acquiring a backend-selection mutex per DATA
frame. Request-body accounting, error paths and exchange ownership remain.
- An **opt-in `sendfile: auto`** setting is wired through Web Service serde,
compiled config, the Delivery UI and documented API. The Linux HTTP/1.1
TCP transport adapter calls `libc::sendfile` for eligible static files;
cache-enabled, ranged, compressed, transformed and TLS/H2/H3 paths retain
existing streaming. It is **disabled by default** and must pass native
compilation, correctness, transport and load tests before use. No persisted
SQLite schema migration or LB UI change is required.
## Local verification
| Check | Outcome | Coverage / caveat |
|---|---|---|
| `./dev.sh doctor` | PASS | Project source layout and tool inventory |
| SQL migration/preset test | PASS | All ten migrations re-applied, preset access and limits |
| Upgrade test | PASS | Stop/lock guards, SQLite/.env/language preservation, symlink rejection |
| Python compileall | PASS | Python syntax only |
| Vhost LB/Cache Chromium test | PASS | Simulated browser quick-view, 320/375/768/1440px |
| Other Chromium scripts | 15 PASS | Simulated API/CSS/JS, not a live Rust server |
| Sidebar navigation Chromium test | INCOMPLETE | Execution time budget; not counted as a pass |
| Three Node dashboard scripts | PASS | Simulated UI logic |
| `./dev.sh check` and `./dev.sh test` | BLOCKED | `cargo`/`rustc` unavailable |
| Rust unit/integration tests (including new scoped COLD regression) | NOT RUN | Requires Rust toolchain |
| Live HTTP/protocol/LB regressions | NOT RUN | Requires built 0.34.0 binary |
| 0.33.8 / 0.34.0 / nginx paired RPS benchmarks | NOT RUN | No verified 0.34.0 binary |
| Zero-copy sendfile WWW setting | SOURCE IMPLEMENTED / NOT RUNTIME VERIFIED | Linux plain H1 static GET only, `off` by default |
| Sendfile editor Chromium save/reload | PASS | 375/1440px, simulated admin API, off -> auto -> off |
| Sendfile Rust adapter + fallback test code | ADDED / NOT RUN | TCP marker replacement, persistent socket, HEAD, range, conditional; compiler unavailable |
| Amdahl sendfile/nginx sensitivity study | PASS MODEL | No runtime evidence for simulated improvements |
The 0.33.8 report in the supplied evidence archive recorded **706 passing
Rust tests**, **152 HTTP assertions**, **111 transport assertions** and
**17 LB assertions**. Those are *prior-version* results and are **not evidence
that 0.34.0 passes**. This report never transfers previous pass counts.
## Synthetic cache-retention scenario (not an RPS measurement)
Run:
```bash
python3 scripts/performance/simulate_cache_034.py
```
Assume 20 vhosts, 100 HOT + 100 COLD objects each, all public/cacheable with
infinite TTL and adequate cache capacity; 1,000 configuration saves (800
operational-only, 200 content changes to one vhost), followed by 500 uniformly
random GETs per save. A miss immediately reloads an object into its original
tier. No networking, filesystem, CPU timing, origin delay or concurrency is
modeled. Results with seed 3400:
| Synthetic metric | Old global purge | 0.34.0 scoped retention |
|---|---:|---:|
| Object invalidations | 473,540 | 37,300 |
| GET misses | 470,006 | 37,069 |
| GET hits | 29,994 | 462,931 |
| HIT ratio | 5.999% | 92.586% |
The modeled change yields **92.123% fewer invalidations**, **92.113% fewer
misses**, and **+86.587 percentage points** of synthetic cache HIT ratio.
**Measured 0.34.0 requests/s gain: unknown.** These values must not be
presented as a real speedup or a projection against nginx.
## Previously measured comparison (0.33.8, not this candidate)
From the attached 0.33.8 evidence, five paired benchmark runs with cache OFF,
median RPS:
| Mode / payload | 0.33.8 RPS | nginx RPS | 0.33.8 / nginx |
|---|---:|---:|---:|
| Reverse proxy 1,040 B | 29,334.12 | 170,610.16 | 17.19% |
| Reverse proxy 64 KiB | 20,735.07 | 35,393.29 | 58.58% |
| Reverse proxy 1 MiB | 4,461.19 | 2,730.23 | 163.40% |
| WWW 1,040 B | 24,906.20 | 351,605.13 | 7.08% |
| WWW 64 KiB | 20,856.67 | 203,709.91 | 10.24% |
| WWW 1 MiB | 2,833.71 | 12,611.49 | 22.47% |
The 0.33.8 1 MiB proxy path was **2.77% slower than rebuilt 0.33.7**.
Removing a per-response-frame mutex is a performance candidate but is **not**
a measured correction of that regression. No throughput gains are attributed
here to the new source changes.
## Hypothetical 0.34.0/nginx gap sensitivity (NOT MEASURED)
Run `python3 scripts/performance/simulate_nginx_gap_034.py`. Using the **real
0.33.8 measured medians** above, assume zero-copy removes 50% of the
read/copy part of per-request cost; that part hypothetically accounts for
20/40/60% of *total* cost for eligible static files. Model **1/3/5%** total
service-time savings from removed proxy-frame mutexes, independently.
Amdahl: `RPS_sim = RPS_0.33.8 / (1 - hypothetical_fraction_saved)`. The
result is a sensitivity calculation, **not a prediction or 0.34.0 benchmark**.
| Workload | Measured 0.33.8/nginx | Modeled low | Modeled middle | Modeled high |
|---|---:|---:|---:|---:|
| Proxy 1040 B | 17.19% | 17.37% | 17.73% | 18.10% |
| Proxy 64 KiB | 58.58% | 59.18% | 60.40% | 61.67% |
| Proxy 1 MiB | 163.40% | 165.05% | 168.45% | 172.00% |
| WWW 1040 B | 7.08% | 7.08% | 7.08% | 7.08% |
| WWW 64 KiB | 10.24% | 11.38% | 12.80% | 14.63% |
| WWW 1 MiB | 22.47% | 24.97% | 28.09% | 32.10% |
These ratios apply only to eligible Linux plain HTTP/1.1 static workloads
with cache OFF. TLS, HTTP/2, HTTP/3, ranged, compressed and object-cached
responses are **not** accelerated by this bridge. Bandwidth, contention,
queueing, realistic filesystem and actual CPU profiles are not modeled.
Even the favorable modeled 1 MiB case remains ~3.1x slower than nginx WWW;
small WWW remains ~14.1x slower. Proxy 1 MiB already exceeds nginx in the
**prior baseline**, not as a demonstrated new 0.34.0 improvement.
## Required rollout gates
1. On a trusted Linux build worker with the pinned Rust toolchain, run
`./dev.sh check`, `CARGO_INCREMENTAL=0 ./dev.sh test`, and a locked release
build. Resolve any compiler/test issues before a deployment.
2. Follow `scripts/performance/README.md` to run `regression.py`,
`transport_regression.py`, `lb_regression.py`, and
`concurrency_regression.py` against verified 0.33.8 and 0.34.0 binaries.
3. Exercise administration saves during simultaneous requests: save keepalive
values and confirm unchanged HOT/COLD entries and hashes; change one backend
origin and confirm only its scope is purged, both before/after restart. Check
that old snapshots cannot recreate invalidated objects.
4. Run the five-repeat, 15-second paired benchmark against 0.33.8, 0.34.0 and
nginx on the *same host and affinity*, including large proxy responses, CPU,
RSS, resource limits and response correctness.
5. Compile the source-level Linux `sendfile` bridge and validate on a running
RustProxy process: plain HTTP/1 sendfile path, headers/body exactness,
keepalive/pipelining, slow clients, cancellation, HEAD, range, cache,
compression, TLS/H2/H3 fallback, symlink security and observability. Do
**not** activate Auto on production before that gate passes.
6. Complete a mixed-traffic soak and staged rollback rehearsal. Until then,
do not describe 0.34.0 as production-accepted.
+34 -1
View File
@@ -99,12 +99,45 @@ The Web Service editor is split into stable configuration tabs so later features
- **.htaccess** - bounded migration-oriented Apache compatibility, recursive scan and strict-mode behavior. - **.htaccess** - bounded migration-oriented Apache compatibility, recursive scan and strict-mode behavior.
- **MIME** - per-extension MIME overrides and fallback MIME type. - **MIME** - per-extension MIME overrides and fallback MIME type.
- **Cache & headers** - default `Cache-Control`, ordered cache rules and fixed response headers. - **Cache & headers** - default `Cache-Control`, ordered cache rules and fixed response headers.
- **Delivery** - precompressed `.br`/`.gz` sidecars, forced-download extensions and Range/streaming information. - **Delivery** - opt-in Linux `sendfile`, precompressed `.br`/`.gz` sidecars, forced-download extensions and Range/streaming information.
- **PHP-FPM** - attach a shared PHP-FPM profile through the selector, open the profile-management modal to expand/edit/create profiles, or choose local configuration for all FastCGI tuning options. - **PHP-FPM** - attach a shared PHP-FPM profile through the selector, open the profile-management modal to expand/edit/create profiles, or choose local configuration for all FastCGI tuning options.
- **Security** - symlink policy, request/response hardening summary and filesystem-confinement notes. - **Security** - symlink policy, request/response hardening summary and filesystem-confinement notes.
After the Web Service is saved, attach it under **Vhosts** by selecting `Web Server` and the service ID for the main source or a custom location. After the Web Service is saved, attach it under **Vhosts** by selecting `Web Server` and the service ID for the main source or a custom location.
### Optional native Linux sendfile (0.34.0 source candidate)
Set **Web Server -> Edit service -> Delivery -> Linux sendfile** to **Auto**
(or `web_services[].sendfile: "auto"`). The default is `"off"` for full
backward compatibility. This option changes only transport; switching it
never invalidates the HOT or COLD object cache. No database migration is needed.
**Platforms other than Linux (FreeBSD, OpenBSD, NetBSD, DragonFly BSD, macOS and Windows):** `sendfile: "auto"` is accepted as configuration but does **not** activate kernel zero-copy in RustProxy 0.34.0. The Linux-specific bridge is compiled only on Linux; on other targets ordinary Tokio `ReaderStream`/Hyper streaming serves the exact file content, including HEAD, ranges, validators, PHP and existing authorization/cache policies. `"off"` and `"auto"` behave the same on those targets. This describes the `sendfile` feature only and is **not** a claim that the entire application currently passes a cross-platform compile/test suite. FreeBSD/macOS native `sendfile` and Windows `TransmitFile` require separate implementations and runtime tests; Linux `libc::sendfile` cannot be reused directly.
The kernel `sendfile(2)` bridge is selected **only** if all conditions hold:
- Linux, plain TCP **HTTP/1.1** (no TLS, HTTP/2, HTTP/3 or QUIC);
- static **GET** of an already-constrained file, full **200 OK** response;
- file at least **64 KiB**, exact `Content-Length`, no `Range`/`If-Range`;
- no negotiated compression/precompressed variant, response transform or
object cache participation, and no rewritten content/encoding headers.
The file descriptor is duplicated from the already-authorized confined open
file (not re-resolved by path), then attached to a bounded HTTP/1.1 TCP
connection bridge. The bridge sends the physical file bytes using Linux
`sendfile(2)` and never sends the internal marker bytes to the client. If
conditions are not met, RustProxy uses the existing stream/counters unchanged.
Only new cleartext connections pick up a newly enabled adapter; this option
does not change the listener protocol configuration.
**Release acceptance:** This source candidate has not been compiled or
tested against a running 0.34.0 RustProxy binary in the current environment.
The included Rust unit/integration test and real-network test cases must pass
on a Linux build worker before enabling in production. Sendfile cannot by
itself close the small-response gap to nginx. See
`docs/TEST_REPORT_0.34.0.md` and
`scripts/performance/simulate_nginx_gap_034.py`.
### Intercept application HTTP errors (0.29.7) ### Intercept application HTTP errors (0.29.7)
Open **Vhosts → Edit vhost → Pages** (not the Web Service PHP tab), enable Open **Vhosts → Edit vhost → Pages** (not the Web Service PHP tab), enable
+3
View File
@@ -1903,6 +1903,9 @@
"webServiceRewritesHelp": "One regex rule per line. Use pattern => /target for an internal rewrite or pattern => 301:/target for a redirect.", "webServiceRewritesHelp": "One regex rule per line. Use pattern => /target for an internal rewrite or pattern => 301:/target for a redirect.",
"webServiceRewritesInvalid": "Rewrite rules must use pattern => /target or pattern => 301:/target (301/302/307/308).", "webServiceRewritesInvalid": "Rewrite rules must use pattern => /target or pattern => 301:/target (301/302/307/308).",
"webServiceCompression": "Dynamic compression", "webServiceCompression": "Dynamic compression",
"webServiceSendfile": "Linux sendfile (zero-copy)",
"webServiceSendfileAuto": "Auto (eligible plain HTTP/1.1)",
"webServiceSendfileHelp": "Opt-in for static GET files >=64 KiB on Linux plain HTTP/1.1 with cache and transforms bypassed and no negotiated compression. TLS, HTTP/2, HTTP/3, ranges and sidecars keep streaming. Does not clear cache.",
"webServiceCompressionHelp": "Inherits the vhost/global policy by default. Individual codecs can be filtered here; globally disabled codecs remain unavailable and minimum size stays global.", "webServiceCompressionHelp": "Inherits the vhost/global policy by default. Individual codecs can be filtered here; globally disabled codecs remain unavailable and minimum size stays global.",
"webServicePrecompressed": "Serve precompressed sidecars", "webServicePrecompressed": "Serve precompressed sidecars",
"webServicePrecompressedBr": "Brotli (.br)", "webServicePrecompressedBr": "Brotli (.br)",
+3
View File
@@ -1903,6 +1903,9 @@
"webServiceRewritesHelp": "Jedna reguła regex na linię. Użyj wzorzec => /cel dla rewrite wewnętrznego albo wzorzec => 301:/cel dla przekierowania.", "webServiceRewritesHelp": "Jedna reguła regex na linię. Użyj wzorzec => /cel dla rewrite wewnętrznego albo wzorzec => 301:/cel dla przekierowania.",
"webServiceRewritesInvalid": "Reguły rewrite muszą mieć format wzorzec => /cel albo wzorzec => 301:/cel (301/302/307/308).", "webServiceRewritesInvalid": "Reguły rewrite muszą mieć format wzorzec => /cel albo wzorzec => 301:/cel (301/302/307/308).",
"webServiceCompression": "Kompresja dynamiczna", "webServiceCompression": "Kompresja dynamiczna",
"webServiceSendfile": "Linux sendfile (zero-copy)",
"webServiceSendfileAuto": "Auto (HTTP/1.1 bez TLS)",
"webServiceSendfileHelp": "Dla statycznych plików GET od 64 KiB w Linuksie przez HTTP/1.1 bez TLS, przy wyłączonym cache, transformacjach i bez kompresji. TLS, HTTP/2, HTTP/3, Range i pliki skompresowane korzystają ze standardowego strumieniowania. Zmiana opcji nie czyści cache.",
"webServiceCompressionHelp": "Domyślnie dziedziczy politykę vhosta/globalną. Tutaj można ograniczyć poszczególne kodeki; kodeki wyłączone globalnie pozostają niedostępne, a minimalny rozmiar pozostaje globalny.", "webServiceCompressionHelp": "Domyślnie dziedziczy politykę vhosta/globalną. Tutaj można ograniczyć poszczególne kodeki; kodeki wyłączone globalnie pozostają niedostępne, a minimalny rozmiar pozostaje globalny.",
"webServicePrecompressed": "Serwuj gotowe pliki skompresowane", "webServicePrecompressed": "Serwuj gotowe pliki skompresowane",
"webServicePrecompressedBr": "Brotli (.br)", "webServicePrecompressedBr": "Brotli (.br)",
+33
View File
@@ -6,6 +6,26 @@ 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.
## Reverse-proxy optimization revision 0.34.0
After compiling the refreshed candidate, measure the same unmodified 0.33.8
binary and nginx reference as before, without turning off logging, request
IDs, ACL, cache-policy enforcement or LB health. Profile request-size 1,040 B
first. Before RPS runs execute full proxy, LB, transport and concurrency
regressions (including single-member capacity, recovery, sticky cookies,
replays on safe methods, rejected non-replayable writes and H2 connection
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.
## Offline cache-retention model for 0.34.0
`python3 scripts/performance/simulate_cache_034.py` compares cache invalidation
policies under a deterministic synthetic sequence of vhost saves and uniform
cacheable GETs. The model contains **no RustProxy server**, no timing and no
network traffic; its percentages are **not measured RPS gains**. See
`docs/TEST_REPORT_0.34.0.md` for assumptions and the live acceptance gap.
## Requirements ## Requirements
- Linux with readable `/proc`, Python 3.10+, executable RustProxy binaries. - Linux with readable `/proc`, Python 3.10+, executable RustProxy binaries.
@@ -274,3 +294,16 @@ are not presented as proven server-side cancellations.
Memory trend is descriptive: switching caching and allocator retention affect Memory trend is descriptive: switching caching and allocator retention affect
RSS. This is a 30-minute resource gate, not a 24-hour leak proof. A short RSS. This is a 30-minute resource gate, not a 24-hour leak proof. A short
`--exploratory --duration ...` sanity run stays explicitly outside that gate. `--exploratory --duration ...` sanity run stays explicitly outside that gate.
## 0.34.0 hypothesis-only sendfile/nginx gap simulator
```bash
python3 scripts/performance/simulate_nginx_gap_034.py --output /tmp/nginx-gap.json
```
This is **not** a benchmark of RustProxy 0.34.0. It reproduces measured
0.33.8/nginx medians with cache OFF and applies explicitly hypothetical
Amdahl service-time reductions (WWW and reverse proxy separately). A real
RustProxy binary is required to validate the code path, detect regressions
and measure latency/RPS against nginx under identical conditions.
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""Deterministic analytical/synthetic cache-retention scenario, not a RustProxy load test.
Model: 20 vhosts with 100 HOT + 100 COLD uniformly requested objects each.
A new configuration is saved 1,000 times. Eighty percent of saves edit only
keepalive/LB operational controls, and 20% edit content policy on one vhost.
Every save is followed by 500 uniform GETs; every object is public/cacheable and
has infinite TTL. A miss immediately inserts the object in its original tier.
Capacity is assumed adequate and there is no origin latency or concurrent IO.
The baseline flushes both tiers globally on every save, as in 0.33.8.
The candidate changes only the affected vhost after content edits.
"""
import argparse
import json
import random
def simulate(vhosts=20, objects_per_tier=100, saves=1000, requests_per_save=500, seed=3400):
if min(vhosts, objects_per_tier, saves, requests_per_save) < 1:
raise ValueError("all counts must be positive")
rng = random.Random(seed)
objects = vhosts * objects_per_tier * 2
old = set(range(objects))
new = set(range(objects))
old_hits = new_hits = old_misses = new_misses = 0
old_evicted = new_evicted = 0
edits_operational = edits_content = 0
for step in range(saves):
changed_vhost = rng.randrange(vhosts)
content_edit = step % 5 == 0
if content_edit:
edits_content += 1
else:
edits_operational += 1
old_evicted += len(old)
old.clear()
if content_edit:
low = changed_vhost * objects_per_tier * 2
high = low + objects_per_tier * 2
removed = set(range(low, high)) & new
new_evicted += len(removed)
new.difference_update(removed)
for _ in range(requests_per_save):
object_id = rng.randrange(objects)
if object_id in old:
old_hits += 1
else:
old_misses += 1
old.add(object_id)
if object_id in new:
new_hits += 1
else:
new_misses += 1
new.add(object_id)
total = saves * requests_per_save
assert old_hits + old_misses == total
assert new_hits + new_misses == total
assert new_evicted <= old_evicted
assert new_misses <= old_misses
return {
"scenario": {
"vhosts": vhosts,
"hot_objects_per_vhost": objects_per_tier,
"cold_objects_per_vhost": objects_per_tier,
"saves": saves,
"operational_saves": edits_operational,
"single_vhost_content_saves": edits_content,
"gets_per_save": requests_per_save,
"total_gets": total,
"seed": seed,
"assumptions": "All objects public/cacheable, infinite TTL, instantly refilled, uniform GETs; no bandwidth/CPU/origin timings",
},
"legacy_global_purge": {
"invalidated_objects": old_evicted,
"misses": old_misses,
"hits": old_hits,
"hit_ratio_percent": round(100 * old_hits / total, 3),
},
"scoped_cache": {
"invalidated_objects": new_evicted,
"misses": new_misses,
"hits": new_hits,
"hit_ratio_percent": round(100 * new_hits / total, 3),
},
"improvement": {
"fewer_invalidations_percent": round(100 * (1 - new_evicted / old_evicted), 3) if old_evicted else 0,
"fewer_cache_misses_percent": round(100 * (1 - new_misses / old_misses), 3) if old_misses else 0,
"hit_ratio_gain_percentage_points": round(100 * (new_hits - old_hits) / total, 3),
"observed_rps_gain_percent": None,
},
}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--vhosts", type=int, default=20)
parser.add_argument("--objects-per-tier", type=int, default=100)
parser.add_argument("--saves", type=int, default=1000)
parser.add_argument("--gets-per-save", type=int, default=500)
parser.add_argument("--seed", type=int, default=3400)
parser.add_argument("--output", help="optional JSON results file")
args = parser.parse_args()
result = simulate(args.vhosts, args.objects_per_tier, args.saves, args.gets_per_save, args.seed)
encoded = json.dumps(result, indent=2, sort_keys=True)
if args.output:
from pathlib import Path
Path(args.output).write_text(encoded + "\n")
print(encoded)
if __name__ == "__main__":
main()
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Amdahl sensitivity study only: 0.33.8 measured baseline, 0.34.0 not measured.
No synthetic RPS is described as benchmarked RustProxy throughput. Amdahl uses
RPS_new = RPS_old / (1 - eligible_cost_fraction * fraction_eliminated).
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
# Verified medians reproduced from the supplied 0.33.8 evidence. Cache OFF.
BASELINE = [
("reverse_proxy", 1040, 29334.12, 170610.16, False),
("reverse_proxy", 65536, 20735.07, 35393.29, False),
("reverse_proxy", 1048576, 4461.19, 2730.23, False),
("www", 1040, 24906.20, 351605.13, False),
("www", 65536, 20856.67, 203709.91, True),
("www", 1048576, 2833.71, 12611.49, True),
]
# WWW scenarios: share of per-request service time due to file copy/read;
# assume sendfile removes HALF of that part, not the full service time.
WWW_SHARES = {"low": 0.20, "middle": 0.40, "high": 0.60}
COPY_COST_REMOVED = 0.50
# Proxy independent optimization: hypothetical reduction in *total* request
# service time, modeling removal of redundant response-frame mutex, nothing else.
PROXY_TIME_SAVED = {"low": 0.01, "middle": 0.03, "high": 0.05}
def run() -> dict:
items = []
for kind, payload_bytes, rust_rps, nginx_rps, eligible in BASELINE:
baseline_ratio = rust_rps / nginx_rps
row = {
"kind": kind,
"bytes": payload_bytes,
"baseline_version": "0.33.8",
"baseline_rust_rps": rust_rps,
"baseline_nginx_rps": nginx_rps,
"baseline_of_nginx_pct": round(100 * baseline_ratio, 2),
"factor_to_match_nginx": round(nginx_rps / rust_rps, 2),
"sendfile_eligible_only_if_plain_linux_http1_1_no_cache_no_compression": eligible,
"scenarios": {},
}
for level in ("low", "middle", "high"):
if kind == "www" and eligible:
saved_fraction = WWW_SHARES[level] * COPY_COST_REMOVED
elif kind == "reverse_proxy":
saved_fraction = PROXY_TIME_SAVED[level]
else:
saved_fraction = 0.0
simulated_rps = rust_rps / (1.0 - saved_fraction)
row["scenarios"][level] = {
"modeled_total_time_saved_pct": round(100 * saved_fraction, 2),
"simulated_rps": round(simulated_rps),
"simulated_rps_gain_pct": round(100 * (simulated_rps / rust_rps - 1), 2),
"simulated_of_nginx_pct": round(100 * simulated_rps / nginx_rps, 2),
"simulated_remaining_gap_to_nginx_pct_points": round(
100 * (1 - simulated_rps / nginx_rps), 2
),
}
items.append(row)
return {
"kind": "HYPOTHETICAL_AMDAHL_SENSITIVITY_NOT_A_BENCHMARK",
"comparison": "Existing 0.33.8 measurements (5 runs, cache off) vs measured nginx",
"not_measured": "0.34.0 server performance and sendfile syscall/runtime correctness",
"conditions": [
"Identical CPU and network assumptions to supplied 0.33.8 baseline",
"WWW zero-copy is limited to Linux, cleartext HTTP/1.1, GET >=64KiB, no cache, range, transform, compression or TLS",
"WWW low/middle/high: file-copy contributes 20%/40%/60% of total request time; modeled sendfile eliminates 50% of that component",
"Reverse proxy low/middle/high: removing response-frame mutex saves a hypothetical 1%/3%/5% of total service time",
"These fractions are chosen sensitivity parameters; not measured or inferred from profiling",
"No change is assumed for WWW 1040B or out-of-scope protocol paths",
"No network saturation, TLS record processing, queueing, origin latency, filesystem cache misses, CPU contention or concurrency effects modeled",
"No additional speedup due to cache-retention changes is assumed",
],
"formula": "RPS_sim = RPS_0.33.8 / (1 - fraction_of_total_service_time_eliminated)",
"entries": items,
}
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, help="write JSON to this file")
args = parser.parse_args()
payload = json.dumps(run(), indent=2, ensure_ascii=False) + "\n"
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(payload)
print(f"wrote {args.output}")
else:
print(payload)
if __name__ == "__main__":
main()
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""0.34.0 source-wiring checks only; do not mistake these for Rust tests."""
from pathlib import Path
import tomllib
root = Path(__file__).resolve().parents[2]
read = lambda path: (root / path).read_text()
assert tomllib.loads(read("Cargo.toml"))["package"]["version"] == "0.34.0"
assert 'version = "0.34.0"' in read("Cargo.lock")
app = read("src/runtime/updates.rs")
assert "CacheInvalidation::Scopes(scopes)" in app
assert "this.cache.purge_scope(&scope)" in app
assert "this.cache.set_control_revision(envelope.revision)" in app
assert app.index("this.cache.set_control_revision(envelope.revision)") < app.index("match invalidation")
source = read("src/runtime/types.rs")
assert "view.load_balancing = Default::default();" in source
assert "member.max_concurrent_requests = 0;" in source
assert "fn vhost_cache_revision" in source
assert "&web_services" in source
assert "service.sendfile = crate::web_server::SendfileMode::Off;" in source
assert "&config.php_fpm_profiles" in source
for filename in ("src/proxy/forward/route_cache.rs", "src/proxy/forward/web_source.rs"):
assert "ticket_scoped_for_config(" in read(filename)
assert "snapshot.envelope.revision" in read(filename)
assert "expected_control_revision" in read("src/cache/tickets.rs")
assert "file.scope_revision != expected_scope_revision" in read("src/cache/cold/startup.rs")
assert "scope_revision: draft.scope_revision" in read("src/cache/cold/worker.rs")
body = read("src/proxy/body.rs")
assert "response_backend: Option<Arc<crate::balance::Lease>>" in body
assert "lease.bytes(count, false);" in body
assert "exchange.backend_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'):
if '/tests/' in file.as_posix() or file.name == 'tests.rs':
continue
if len(file.read_text().splitlines()) > 500:
raise AssertionError(f"production Rust source exceeds 500 lines: {file}")
print('PASS source wiring for scoped invalidation, cold fingerprints, stale-snapshot barrier, response accounting and <=500-line modules (STATIC ONLY)')
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""Source-only guardrails for the uncompiled 0.34.0 hot-path candidate."""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def src(path):
return (ROOT / path).read_text()
snapshot = src("src/config/snapshot.rs")
assert "fn host_patterns(host: &str) -> impl Iterator" in snapshot
assert "pub fn select_on_arc(" in snapshot
assert "self.select_on_arc(host, path, local, tls).map(Arc::as_ref)" in snapshot
assert "Vec<Arc<CompiledRoute>>" in src("src/config/model.rs")
assert "route: Option<Arc<crate::config::CompiledRoute>>" in src("src/proxy/types.rs")
assert "request.extensions_mut().insert(SelectedRouteContext" in src("src/proxy/upstream_helpers.rs")
assert "selection.route.as_deref()" in src("src/proxy/forward_entry.rs")
assert "let fallback_request = selected" in src("src/proxy/forward_entry.rs")
assert "filter(|route| route.error_fallback.is_some())" in src("src/proxy/forward_entry.rs")
assert "let Some(next_request) = replay_headers" in src("src/proxy/forward/upstream.rs") or "replay_headers.as_ref().and_then" in src("src/proxy/forward/upstream.rs")
assert "let replay_headers = ((pool_retry_possible || h2_replay_possible)" in src("src/proxy/forward/upstream.rs")
pool = src("src/balance/pool_core.rs")
assert "if self.members.len() == 1 && !self.cookie_affinity()" in pool
assert "let mut scheduling = lock(&self.scheduling);" in pool
assert "fn candidates_excluding" in pool
assert "health: member.health_settings.clone()" in pool
assert "health: selected.health_settings.clone()" in pool
assert "health_settings: Arc<HealthCheck>" in src("src/balance/health.rs")
assert "healthy_cached: AtomicBool" in src("src/balance/model.rs")
assert "self.healthy_cached.store(h.healthy, Ordering::Release)" in src("src/balance/health.rs")
exchange = src("src/proxy/body.rs")
for field in ("source", "cache_effective_enabled", "transform_effective_enabled"):
assert f"pub {field}: AtomicU8" in exchange
assert f"pub {field}: Mutex" not in exchange
assert "exchange.set_source(" in src("src/proxy/forward/route_cache.rs")
assert "exchange.set_cache_enabled(" in src("src/proxy/forward/web_source.rs")
assert "self.live.receiver_count() != 0" in src("src/logging/format.rs")
assert "fn route_handles_reuse_snapshot_ownership_without_cloning_compiled_routes" in src("src/config/tests.rs")
assert "fn single_backend_fast_path_observes_capacity_and_health_without_affinity_cookie" in src("src/balance/tests.rs")
assert "fn atomic_exchange_tags_keep_original_observability_fields" in src("src/proxy/body/tests.rs")
for path in (ROOT / "src").rglob("*.rs"):
if "/tests/" in path.as_posix() or path.name == "tests.rs" or path.is_relative_to(ROOT / "tests"):
continue
assert len(path.read_text().splitlines()) <= 500, path
assert 'version = "0.34.0"' in src("Cargo.toml")
assert "max_idle_per_host: 4" in src("src/balance/model.rs")
assert "pool_recycle_requests: 25_000" in src("src/balance/model.rs")
print("PASS 0.34.0 proxy hot-path source wiring, observability, preserved defaults and source-size limits (STATIC ONLY)")
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""Offline Chromium test: sendfile toggle serializes in real admin bundle.
It uses simulated config and a mocked saveSection; never claims native
sendfile execution, Rust compilation or live server acceptance.
"""
from pathlib import Path
import json
import re
from playwright.sync_api import sync_playwright
from panel_html import render_admin_index
ROOT = Path(__file__).resolve().parents[2]
ROUTER = (ROOT / "src/admin/router.rs").read_text()
start = re.search(r'fn admin_app_javascript\([^)]*\)[^{]*\{', ROUTER)
assert start
array = ROUTER[start.end():]
array = array[:re.search(r'\]\s*\.join\("\\n"\)', array).start()]
chunks = []
for line in array.splitlines():
line = line.strip()
match = re.search(r'include_str!\("([^"]+)"\)', line)
if match:
chunks.append((ROOT / "src/admin" / match.group(1)).read_text())
elif line == 'admin_vhosts_javascript(),':
chunks.append('(() => {\n' + '\n'.join(
(ROOT / 'src/admin/web/vhosts' / (f + '.js')).read_text()
for f in ('core', 'protocol', 'health', 'rates', 'editor', 'render')) + '\n})();')
elif line.startswith('"') and line.endswith('",'):
chunks.append(json.loads(line[:-1]))
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, executable_path='/usr/bin/chromium', args=['--no-sandbox'])
for width in (375, 1440):
page = browser.new_page(viewport={'width': width, 'height': 950})
page.set_default_timeout(4500)
errors = []
page.on('pageerror', lambda err: errors.append(str(err)))
page.set_content(render_admin_index(ROOT))
page.add_style_tag(content=(ROOT / 'src/admin/web/style.css').read_text())
page.add_script_tag(content='\n'.join(chunks))
page.evaluate('''() => {
envelope = {revision: 10, config: {web_services: [{
id: 'site-A', name: 'Static Site', document_root: '/srv/static',
index: ['index.html'], php: {enabled: false}
}], vhosts: [{id: 'vhost-A', keepalive: {max_connections: 8}}], php_fpm_profiles: []}};
currentPanel = 'web-server';
document.documentElement.classList.remove('app-loading', 'app-startup-failed');
document.getElementById('login').hidden = true;
document.getElementById('workspace').hidden = false;
document.getElementById('panel-web-server').hidden = false;
window.__saved = [];
saveSection = async (section, value) => {
window.__saved.push({section, value: structuredClone(value)});
if (section === 'web-services') envelope.config.web_services = structuredClone(value);
return true;
};
window.rustProxyWebServer.render();
}''')
page.evaluate("document.querySelector('#web-service-list .entity-actions button').click()")
assert page.locator('#web-service-sendfile').input_value() == 'off'
page.evaluate("document.getElementById('web-service-sendfile').value = 'auto'")
page.evaluate('''() => { const form = document.getElementById('web-service-form'); if (!form.checkValidity()) throw new Error('invalid fields: ' + [...form.elements].filter(e=>!e.checkValidity()).map(e=>e.id).join(', ')); form.requestSubmit(); }''')
page.wait_for_function('window.__saved.length === 1')
result = page.evaluate('''() => ({
saved: window.__saved[0],
cacheStillUnrelated: envelope.config.vhosts[0].keepalive.max_connections === 8,
closed: !document.getElementById('web-service-dialog').open
})''')
assert result['saved']['section'] == 'web-services', result
assert result['saved']['value'][0]['sendfile'] == 'auto', result
assert result['saved']['value'][0]['document_root'] == '/srv/static', result
assert result['cacheStillUnrelated'] and result['closed'], result
page.evaluate("document.querySelector('#web-service-list .entity-actions button').click()")
assert page.locator('#web-service-sendfile').input_value() == 'auto'
page.evaluate("document.getElementById('web-service-sendfile').value = 'off'")
page.evaluate('''() => { const form = document.getElementById('web-service-form'); if (!form.checkValidity()) throw new Error('invalid fields: ' + [...form.elements].filter(e=>!e.checkValidity()).map(e=>e.id).join(', ')); form.requestSubmit(); }''')
page.wait_for_function('window.__saved.length === 2')
assert page.evaluate('envelope.config.web_services[0].sendfile') == 'off'
assert not errors, errors
print(f'PASS simulated panel sendfile off -> auto -> off config save at {width}px')
browser.close()
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""RustProxy 0.34.0 sendfile source-wiring assertions ONLY, not native tests."""
from pathlib import Path
import json
import re
root = Path(__file__).resolve().parents[2]
get = lambda path: (root / path).read_text()
model = get('src/web_server/config/model.rs')
assert 'pub enum SendfileMode' in model and 'sendfile: SendfileMode::Off' in model
assert 'pub sendfile: SendfileMode' in model
assert 'sendfile: self.sendfile' in get('src/web_server/config/compile.rs')
assert 'pub sendfile: SendfileMode' in get('src/web_server/config/validation.rs')
assert 'service.sendfile = crate::web_server::SendfileMode::Off;' in get('src/runtime/types.rs')
source = get('src/proxy/forward/web_source.rs')
for guard in ('!enabled && identity_request', 'request_method == Method::GET',
'response.status() == StatusCode::OK', 'content-encoding',
'transfer-encoding', 'content-range', 'candidate.length',
'http::Version::HTTP_11'):
assert guard in source, guard
assert 'content_encoding.is_none()' in get('src/web_server/response/file.rs')
assert 'single_range.is_none()' in get('src/web_server/response/file.rs')
assert 'opened.file.try_clone()' in get('src/web_server/response/file.rs')
bridge = get('src/server/sendfile.rs')
for item in ('libc::sendfile(', 'MAX_PENDING: usize = 8', 'MIN_SIZE: u64 = 64 * 1024',
'registry.slots.is_empty()', 'native_sendfile_transfers_exact_file',
'sendfile placeholder did not match', 'UnexpectedEof'):
assert item in bridge, item
assert 'pub(crate) mod sendfile' in get('src/server.rs')
assert 'SendfileIo::new(stream)' in get('src/server/tls_runtime.rs')
assert 'sendfile_slots' in get('src/server/http1_connection.rs')
js = get('src/admin/web/web-server.js')
html = get('src/admin/web/index.html')
assert '"web-service-sendfile"' in js and 'sendfile: "off"' in js
assert 'sendfile: $("web-service-sendfile").value' in js
assert html.count('id="web-service-sendfile"') == 1
assert 'value="auto"' in html
for locale in ('en_US', 'pl_pl'):
keys = json.loads(get(f'langs/{locale}.json'))['translations']
assert all(k in keys for k in ('webServiceSendfile', 'webServiceSendfileHelp', 'webServiceSendfileAuto'))
api = json.loads(get('src/admin/openapi.json'))
assert 'sendfile' in json.dumps(api)
print('PASS 0.34.0 sendfile source guards, default OFF, Linux syscall, UI, translations, API and scoped cache (STATIC ONLY)')
+6
View File
@@ -5415,6 +5415,12 @@
"type": "boolean", "type": "boolean",
"default": false "default": false
}, },
"sendfile": {
"type": "string",
"enum": ["off", "auto"],
"default": "off",
"description": "Linux TCP HTTP/1.1 kernel sendfile for eligible unmodified static GET responses. Transport-only; off by default."
},
"mime_types": { "mime_types": {
"type": "object", "type": "object",
"maxProperties": 64, "maxProperties": 64,
+5
View File
@@ -3567,6 +3567,11 @@
responses are never stored.</p> responses are never stored.</p>
</div> </div>
<div class="vh-grid web-service-page" data-web-service-page="delivery" hidden> <div class="vh-grid web-service-page" data-web-service-page="delivery" hidden>
<label class="vh-wide"><span data-i18n="webServiceSendfile">Linux sendfile (zero-copy)</span>
<select id="web-service-sendfile">
<option value="off" data-i18n="notEnabled">Disabled</option>
<option value="auto" data-i18n="webServiceSendfileAuto">Auto (eligible plain HTTP/1.1)</option>
</select><small class="fine" data-i18n="webServiceSendfileHelp">Opt-in for static GET files at least 64 KiB on Linux HTTP/1.1 without TLS, cache, compression or transform. Other requests use safe streaming; switching this setting never clears the cache.</small></label>
<label><span data-i18n="webServiceCompression">Dynamic compression</span><select <label><span data-i18n="webServiceCompression">Dynamic compression</span><select
id="web-service-compression"> id="web-service-compression">
<option value="" data-i18n="inherit">Inherit</option> <option value="" data-i18n="inherit">Inherit</option>
+3 -1
View File
@@ -199,7 +199,7 @@
function defaults() { function defaults() {
return { return {
id: "", name: "", document_root: "", index: ["index.html", "index.htm"], directory_listing: false, id: "", name: "", document_root: "", index: ["index.html", "index.htm"], directory_listing: false, sendfile: "off",
mime_types: {}, default_mime_type: "application/octet-stream", cache_control: null, cache_rules: [], response_headers: {}, mime_types: {}, default_mime_type: "application/octet-stream", cache_control: null, cache_rules: [], response_headers: {},
try_files: [], rewrites: [], compression: { enabled: null, gzip: null, br: null, zstd: null, deflate: null }, precompressed: { enabled: false, brotli: true, gzip: true }, download_extensions: [], try_files: [], rewrites: [], compression: { enabled: null, gzip: null, br: null, zstd: null, deflate: null }, precompressed: { enabled: false, brotli: true, gzip: true }, download_extensions: [],
symlink_policy: "deny", php_profile_id: null, php: { symlink_policy: "deny", php_profile_id: null, php: {
@@ -236,6 +236,7 @@
function populateSettings(item) { function populateSettings(item) {
$("web-service-index").value = (item.index || []).join("\n"); $("web-service-index").value = (item.index || []).join("\n");
$("web-service-listing").checked = item.directory_listing === true; $("web-service-listing").checked = item.directory_listing === true;
$("web-service-sendfile").value = item.sendfile === "auto" ? "auto" : "off";
$("web-service-try-files").value = (item.try_files || []).join("\n"); $("web-service-try-files").value = (item.try_files || []).join("\n");
$("web-service-rewrites").value = rewriteLines(item.rewrites); $("web-service-rewrites").value = rewriteLines(item.rewrites);
$("web-service-mime-types").value = mimeLines(item.mime_types); $("web-service-mime-types").value = mimeLines(item.mime_types);
@@ -467,6 +468,7 @@
document_root: $("web-service-root").value.trim(), document_root: $("web-service-root").value.trim(),
index: lines($("web-service-index").value), index: lines($("web-service-index").value),
directory_listing: $("web-service-listing").checked, directory_listing: $("web-service-listing").checked,
sendfile: $("web-service-sendfile").value,
mime_types: parseMimeTypes($("web-service-mime-types").value), mime_types: parseMimeTypes($("web-service-mime-types").value),
default_mime_type: $("web-service-default-mime").value.trim() || "application/octet-stream", default_mime_type: $("web-service-default-mime").value.trim() || "application/octet-stream",
cache_control: $("web-service-cache-control").value.trim() || null, cache_control: $("web-service-cache-control").value.trim() || null,
+19 -2
View File
@@ -2,6 +2,7 @@ impl BackendState {
fn new(mode: HealthMode) -> Self { fn new(mode: HealthMode) -> Self {
let now = Instant::now(); let now = Instant::now();
Self { Self {
healthy_cached: AtomicBool::new(mode == HealthMode::Off),
health: Mutex::new(Health { health: Mutex::new(Health {
healthy: mode == HealthMode::Off, healthy: mode == HealthMode::Off,
active_healthy: mode == HealthMode::Off, active_healthy: mode == HealthMode::Off,
@@ -111,6 +112,11 @@ impl BackendState {
} }
} }
pub fn healthy(&self) -> bool { pub fn healthy(&self) -> bool {
// No lock/clock acquisition for a healthy backend. Only an unhealthy
// backend enters the quarantine/cooldown recovery path below.
if self.healthy_cached.load(Ordering::Acquire) {
return true;
}
let mut h = lock(&self.health); let mut h = lock(&self.health);
// Passive-only monitoring has no active probes to recover an ejected // Passive-only monitoring has no active probes to recover an ejected
// backend. After a bounded quarantine, allow one new traffic attempt. // backend. After a bounded quarantine, allow one new traffic attempt.
@@ -125,6 +131,12 @@ impl BackendState {
} }
h.healthy h.healthy
} }
#[cfg(test)]
fn set_healthy_for_test(&self, healthy: bool) {
lock(&self.health).healthy = healthy;
self.healthy_cached.store(healthy, Ordering::Release);
}
fn clear_passive_ejection(h: &mut Health) { fn clear_passive_ejection(h: &mut Health) {
h.passive_healthy = true; h.passive_healthy = true;
h.passive_failures = 0; h.passive_failures = 0;
@@ -195,6 +207,7 @@ impl BackendState {
fn finish_transition(&self, h: &mut Health, previous: bool) { fn finish_transition(&self, h: &mut Health, previous: bool) {
h.healthy = h.active_healthy && h.passive_healthy; h.healthy = h.active_healthy && h.passive_healthy;
self.healthy_cached.store(h.healthy, Ordering::Release);
if previous != h.healthy { if previous != h.healthy {
h.transitions = h.transitions.saturating_add(1); h.transitions = h.transitions.saturating_add(1);
if h.healthy { if h.healthy {
@@ -446,6 +459,10 @@ pub struct Member {
pub backend: Arc<CompiledBackend>, pub backend: Arc<CompiledBackend>,
pub definition: PoolMember, pub definition: PoolMember,
pub state: Arc<BackendState>, pub state: Arc<BackendState>,
// Immutable effective per-backend policies are compiled once per pool
// generation, not deep-cloned for every request/lease.
health_settings: Arc<HealthCheck>,
passive_settings: Arc<PassiveHealth>,
token: String, token: String,
} }
#[derive(Debug)] #[derive(Debug)]
@@ -461,7 +478,7 @@ pub struct Pool {
#[derive(Debug)] #[derive(Debug)]
pub struct Lease { pub struct Lease {
pub member: Arc<Member>, pub member: Arc<Member>,
health: HealthCheck, health: Arc<HealthCheck>,
passive_health: PassiveHealth, passive_health: Arc<PassiveHealth>,
error_recorded: AtomicBool, error_recorded: AtomicBool,
} }
+3
View File
@@ -386,6 +386,9 @@ impl Default for FailureObservation {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct BackendState { pub struct BackendState {
// Lock-free healthy lookups on the hot request-selection path. The mutex
// remains authoritative for transitions, probes and quarantine expiry.
healthy_cached: AtomicBool,
health: Mutex<Health>, health: Mutex<Health>,
traffic: Mutex<Traffic>, traffic: Mutex<Traffic>,
last_failure: Mutex<FailureObservation>, last_failure: Mutex<FailureObservation>,
+78 -46
View File
@@ -20,10 +20,14 @@ impl Pool {
.cloned() .cloned()
.zip(backends) .zip(backends)
.map(|(definition, backend)| { .map(|(definition, backend)| {
let health_settings = Arc::new(settings.health_for(&definition));
let passive_settings = Arc::new(settings.passive_for(&definition));
Arc::new(Member { Arc::new(Member {
state: Arc::new(BackendState::new(settings.health_for(&definition).mode)), state: Arc::new(BackendState::new(health_settings.mode)),
definition, definition,
backend, backend,
health_settings,
passive_settings,
token: rand::rng() token: rand::rng()
.sample_iter(Alphanumeric) .sample_iter(Alphanumeric)
.take(32) .take(32)
@@ -88,7 +92,7 @@ impl Pool {
} }
} }
fn candidates_excluding(&self, excluded: &[String]) -> Vec<&Arc<Member>> { fn candidates_excluding(&self, excluded: &[String]) -> Vec<&Arc<Member>> {
let all: Vec<_> = self let mut eligible: Vec<_> = self
.members .members
.iter() .iter()
.filter(|m| { .filter(|m| {
@@ -100,10 +104,10 @@ impl Pool {
&& !excluded.iter().any(|id| id == &m.definition.id) && !excluded.iter().any(|id| id == &m.definition.id)
}) })
.collect(); .collect();
let primary = all.iter().any(|m| !m.definition.backup); if eligible.iter().any(|m| !m.definition.backup) {
all.into_iter() eligible.retain(|m| !m.definition.backup);
.filter(|m| !primary || !m.definition.backup) }
.collect() eligible
} }
pub fn ready(&self) -> bool { pub fn ready(&self) -> bool {
!self.eligible_healthy_excluding(&[]).is_empty() !self.eligible_healthy_excluding(&[]).is_empty()
@@ -197,8 +201,8 @@ impl Pool {
Some(Selection { Some(Selection {
lease: Arc::new(Lease { lease: Arc::new(Lease {
member: member.clone(), member: member.clone(),
health: self.settings.health_for(&member.definition), health: member.health_settings.clone(),
passive_health: self.settings.passive_for(&member.definition), passive_health: member.passive_settings.clone(),
error_recorded: AtomicBool::new(false), error_recorded: AtomicBool::new(false),
}), }),
cookie: None, cookie: None,
@@ -211,7 +215,32 @@ impl Pool {
tls: bool, tls: bool,
excluded: &[String], excluded: &[String],
) -> Option<Selection> { ) -> Option<Selection> {
let mut scheduling = lock(&self.scheduling); // Most deployments have one healthy backend. For non-cookie modes it
// needs neither candidate Vecs, weight scoring nor the shared scheduler
// mutex. Still use the same atomic capacity lease and health gate.
if self.members.len() == 1 && !self.cookie_affinity() {
let member = &self.members[0];
if !member.definition.pool_eligible
|| !member.definition.enabled
|| member.definition.drain
|| !member.state.healthy()
|| excluded.iter().any(|id| id == &member.definition.id)
|| !member
.state
.try_acquire_active(member.definition.max_concurrent_requests)
{
return None;
}
return Some(Selection {
lease: Arc::new(Lease {
member: member.clone(),
health: member.health_settings.clone(),
passive_health: member.passive_settings.clone(),
error_recorded: AtomicBool::new(false),
}),
cookie: None,
});
}
let members = self.candidates_excluding(excluded); let members = self.candidates_excluding(excluded);
let existing = if self.cookie_affinity() { let existing = if self.cookie_affinity() {
self.affinity(headers) self.affinity(headers)
@@ -237,27 +266,24 @@ impl Pool {
let selected = if let Some(member) = matched { let selected = if let Some(member) = matched {
member member
} else { } else {
let weights: Vec<f64> = members
.iter()
.map(|m| {
m.state
.weight(m.definition.weight, self.settings.slow_start_seconds)
})
.collect();
if self.settings.algorithm == Algorithm::RoundRobin if self.settings.algorithm == Algorithm::RoundRobin
&& self.settings.sticky.mode != StickyMode::IpHash && self.settings.sticky.mode != StickyMode::IpHash
{ {
// Smooth weighted round-robin: distribute weighted requests, // Smooth weighted round-robin keeps its historical state and
// rather than sending long consecutive bursts to one member. // global ordering. Only that algorithm takes the mutex.
let mut scheduling = lock(&self.scheduling);
let mut chosen = members[0]; let mut chosen = members[0];
let mut best = f64::NEG_INFINITY; let mut best = f64::NEG_INFINITY;
let mut total = 0.0; let mut total = 0.0;
for (member, weight) in members.iter().zip(&weights) { for member in &members {
let weight = member
.state
.weight(member.definition.weight, self.settings.slow_start_seconds);
let current = scheduling let current = scheduling
.entry(member.definition.id.clone()) .entry(member.definition.id.clone())
.or_insert(0.0); .or_insert(0.0);
*current += *weight; *current += weight;
total += *weight; total += weight;
if *current > best { if *current > best {
best = *current; best = *current;
chosen = *member; chosen = *member;
@@ -270,43 +296,49 @@ impl Pool {
} else { } else {
let mut chosen = members[(cursor as usize) % members.len()]; let mut chosen = members[(cursor as usize) % members.len()];
let mut best = f64::INFINITY; let mut best = f64::INFINITY;
let algorithm = if self.settings.sticky.mode == StickyMode::IpHash {
Algorithm::IpHash
} else {
self.settings.algorithm
};
// Canonicalize the address once, not for every candidate. The
// SHA-256 input and rendezvous scoring remain bit-for-bit equal.
let hash_ip = (algorithm == Algorithm::IpHash)
.then(|| crate::vhost::canonical_ip(peer).to_string());
for offset in 0..members.len() { for offset in 0..members.len() {
let index = ((cursor as usize) % members.len() + offset) % members.len(); let index = ((cursor as usize) % members.len() + offset) % members.len();
let member = members[index]; let member = members[index];
let active = member.state.active.load(Ordering::Relaxed) as f64; let active = member.state.active.load(Ordering::Relaxed) as f64;
let algorithm = if self.settings.sticky.mode == StickyMode::IpHash {
Algorithm::IpHash
} else {
self.settings.algorithm
};
let score = match algorithm { let score = match algorithm {
Algorithm::IpHash => { Algorithm::IpHash => {
let digest = Sha256::digest( let mut hash = Sha256::new();
format!( hash.update(self.id.as_bytes());
"{}|{}|{}", hash.update(b"|");
self.id, hash.update(hash_ip.as_deref().expect("hash address").as_bytes());
crate::vhost::canonical_ip(peer), hash.update(b"|");
member.definition.id hash.update(member.definition.id.as_bytes());
) let digest = hash.finalize();
.as_bytes(),
);
let n = u64::from_be_bytes([ let n = u64::from_be_bytes([
digest[0], digest[1], digest[2], digest[3], digest[4], digest[5], digest[0], digest[1], digest[2], digest[3], digest[4], digest[5],
digest[6], digest[7], digest[6], digest[7],
]); ]);
// Weighted rendezvous: stable across requests, no client-controlled forwarding headers.
let unit = (n as f64 + 1.0) / (u64::MAX as f64 + 2.0); let unit = (n as f64 + 1.0) / (u64::MAX as f64 + 2.0);
-unit.max(f64::MIN_POSITIVE).ln() / member.definition.weight as f64 -unit.max(f64::MIN_POSITIVE).ln() / member.definition.weight as f64
} }
Algorithm::LeastTraffic => { _ => {
(member.state.traffic_rate() + (active + 1.0) * 1024.0) / weights[index] let weight = member.state.weight(
member.definition.weight,
self.settings.slow_start_seconds,
);
match algorithm {
Algorithm::LeastTraffic =>
(member.state.traffic_rate() + (active + 1.0) * 1024.0) / weight,
Algorithm::Adaptive =>
((active + 1.0) * member.state.latency()
+ member.state.traffic_rate() / 16_384.0) / weight,
_ => (active + 1.0) / weight,
}
} }
Algorithm::Adaptive => {
((active + 1.0) * member.state.latency()
+ member.state.traffic_rate() / 16_384.0)
/ weights[index]
}
_ => (active + 1.0) / weights[index],
}; };
if score < best { if score < best {
best = score; best = score;
@@ -338,8 +370,8 @@ impl Pool {
Some(Selection { Some(Selection {
lease: Arc::new(Lease { lease: Arc::new(Lease {
member: selected.clone(), member: selected.clone(),
health: self.settings.health_for(&selected.definition), health: selected.health_settings.clone(),
passive_health: self.settings.passive_for(&selected.definition), passive_health: selected.passive_settings.clone(),
error_recorded: AtomicBool::new(false), error_recorded: AtomicBool::new(false),
}), }),
cookie, cookie,
+43 -5
View File
@@ -533,7 +533,7 @@ mod tests {
headers.insert("cookie", format!("{cookie}; {cookie}").parse().unwrap()); headers.insert("cookie", format!("{cookie}; {cookie}").parse().unwrap());
assert!(pool.affinity(&headers).is_none()); assert!(pool.affinity(&headers).is_none());
headers.insert("cookie", cookie.parse().unwrap()); headers.insert("cookie", cookie.parse().unwrap());
lock(&first.lease.member.state.health).healthy = false; first.lease.member.state.set_healthy_for_test(false);
let next = pool let next = pool
.select("127.0.0.1".parse().unwrap(), &headers, false) .select("127.0.0.1".parse().unwrap(), &headers, false)
.unwrap(); .unwrap();
@@ -542,7 +542,7 @@ mod tests {
first.lease.member.definition.id first.lease.member.definition.id
); );
assert!(next.cookie.is_some()); assert!(next.cookie.is_some());
lock(&next.lease.member.state.health).healthy = false; next.lease.member.state.set_healthy_for_test(false);
assert!(pool assert!(pool
.select("127.0.0.1".parse().unwrap(), &headers, false) .select("127.0.0.1".parse().unwrap(), &headers, false)
.is_none()); .is_none());
@@ -611,9 +611,9 @@ mod tests {
for _ in 0..8 { for _ in 0..8 {
assert_eq!(select(&pool).lease.member.definition.id, "a"); assert_eq!(select(&pool).lease.member.definition.id, "a");
} }
lock(&pool.members[0].state.health).healthy = false; pool.members[0].state.set_healthy_for_test(false);
assert_eq!(select(&pool).lease.member.definition.id, "b"); assert_eq!(select(&pool).lease.member.definition.id, "b");
lock(&pool.members[0].state.health).healthy = true; pool.members[0].state.set_healthy_for_test(true);
assert_eq!(select(&pool).lease.member.definition.id, "a"); assert_eq!(select(&pool).lease.member.definition.id, "a");
} }
#[tokio::test] #[tokio::test]
@@ -635,7 +635,7 @@ mod tests {
.select("127.0.0.1".parse().unwrap(), &headers, false) .select("127.0.0.1".parse().unwrap(), &headers, false)
.unwrap(); .unwrap();
assert_eq!(existing.lease.member.definition.id, "a"); assert_eq!(existing.lease.member.definition.id, "a");
lock(&pool.members[0].state.health).healthy = false; pool.members[0].state.set_healthy_for_test(false);
assert_eq!( assert_eq!(
pool.select("127.0.0.1".parse().unwrap(), &headers, false) pool.select("127.0.0.1".parse().unwrap(), &headers, false)
.unwrap() .unwrap()
@@ -764,4 +764,42 @@ mod tests {
assert!(state.healthy()); assert!(state.healthy());
} }
#[test]
fn single_backend_fast_path_observes_capacity_and_health_without_affinity_cookie() {
let pool = capacity_pool(1, 0);
assert_eq!(pool.members.len(), 1);
let peer = "127.0.0.1".parse().unwrap();
let headers = HeaderMap::new();
let first = pool.select(peer, &headers, false).expect("healthy backend");
assert!(first.cookie.is_none());
assert!(pool.select(peer, &headers, false).is_none());
drop(first);
pool.members[0].state.set_healthy_for_test(false);
assert!(pool.select(peer, &headers, false).is_none());
pool.members[0].state.set_healthy_for_test(true);
assert!(pool.select(peer, &headers, false).is_some());
}
#[test]
fn leases_reuse_immutable_backend_health_policies() {
let pool = capacity_pool(0, 0);
let first = select(&pool);
let second = select(&pool);
assert!(Arc::ptr_eq(&first.lease.health, &second.lease.health));
assert!(Arc::ptr_eq(&first.lease.passive_health, &second.lease.passive_health));
}
#[test]
fn healthy_fast_path_tracks_active_and_passive_ejections() {
let state = BackendState::new(HealthMode::Off);
assert!(state.healthy());
let active = settings(HealthMode::Off);
let passive = PassiveHealth { enabled: true, fall: Some(1), rise: Some(1), ..Default::default() };
state.observe_passive(&active, &passive, "connect_error", Duration::from_millis(2));
assert!(!state.healthy());
// Preserve quarantine rules; an active probe is not invented here.
state.observe_passive(&active, &passive, "ok", Duration::from_millis(2));
assert!(state.healthy());
}
} // mod tests } // mod tests
+8
View File
@@ -51,11 +51,15 @@ impl Cache {
.fetch_add(1, Ordering::Relaxed); .fetch_add(1, Ordering::Relaxed);
return false; return false;
}; };
let scope_revision = self.scope_revisions
.lock().unwrap_or_else(|e| e.into_inner())
.get(&entry.scope).copied().unwrap_or(0);
self.cold_queue_items.fetch_add(1, Ordering::Relaxed); self.cold_queue_items.fetch_add(1, Ordering::Relaxed);
if tx if tx
.try_send(ColdCommand::Store(ColdDraft { .try_send(ColdCommand::Store(ColdDraft {
key, key,
revision, revision,
scope_revision,
generation, generation,
scope_generation, scope_generation,
key_generation, key_generation,
@@ -186,9 +190,13 @@ impl Cache {
}), }),
} }
}); });
let scope_revision = self.scope_revisions
.lock().unwrap_or_else(|e| e.into_inner())
.get(&entry.scope).copied().unwrap_or(0);
ColdDraft { ColdDraft {
key, key,
revision, revision,
scope_revision,
generation, generation,
scope_generation, scope_generation,
key_generation, key_generation,
+7 -1
View File
@@ -72,7 +72,13 @@ impl Cache {
continue; continue;
}; };
let body_path = shard.join(&file.body_file); let body_path = shard.join(&file.body_file);
if file.format != COLD_FORMAT_VERSION || file.revision != revision { let expected_scope_revision = self.scope_revisions
.lock().unwrap_or_else(|e| e.into_inner())
.get(&file.scope).copied().unwrap_or(0);
if file.format != COLD_FORMAT_VERSION
|| file.revision != revision
|| file.scope_revision != expected_scope_revision
{
let _ = std::fs::remove_file(&path); let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_file(&body_path); let _ = std::fs::remove_file(&body_path);
continue; continue;
+1
View File
@@ -121,6 +121,7 @@ impl Cache {
base_key: key_hex(&draft.entry.base_key), base_key: key_hex(&draft.entry.base_key),
vary: draft.entry.vary.clone(), vary: draft.entry.vary.clone(),
revision: draft.revision, revision: draft.revision,
scope_revision: draft.scope_revision,
scope: draft.entry.scope.clone(), scope: draft.entry.scope.clone(),
scheme: draft.entry.scheme.clone(), scheme: draft.entry.scheme.clone(),
authority: draft.entry.authority.clone(), authority: draft.entry.authority.clone(),
+23
View File
@@ -8,6 +8,7 @@ impl Cache {
revision: AtomicU64::new(revision), revision: AtomicU64::new(revision),
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()),
data_root: None, data_root: None,
cold_tx: None, cold_tx: None,
cold_delete_fallbacks: Arc::new(Semaphore::new(COLD_DELETE_FALLBACKS)), cold_delete_fallbacks: Arc::new(Semaphore::new(COLD_DELETE_FALLBACKS)),
@@ -37,6 +38,16 @@ impl Cache {
data_root: Option<PathBuf>, data_root: Option<PathBuf>,
settings: Settings, settings: Settings,
shutdown: CancellationToken, shutdown: CancellationToken,
) -> Arc<Self> {
Self::start_with_scope_revisions(revision, data_root, settings, shutdown, HashMap::new())
}
pub fn start_with_scope_revisions(
revision: u64,
data_root: Option<PathBuf>,
settings: Settings,
shutdown: CancellationToken,
scope_revisions: HashMap<String, u64>,
) -> Arc<Self> { ) -> Arc<Self> {
let (tx, rx) = mpsc::channel(COLD_QUEUE); let (tx, rx) = mpsc::channel(COLD_QUEUE);
let cache = Arc::new(Self { let cache = Arc::new(Self {
@@ -47,6 +58,7 @@ impl Cache {
revision: AtomicU64::new(revision), revision: AtomicU64::new(revision),
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),
data_root, data_root,
cold_tx: Some(tx), cold_tx: Some(tx),
cold_delete_fallbacks: Arc::new(Semaphore::new(COLD_DELETE_FALLBACKS)), cold_delete_fallbacks: Arc::new(Semaphore::new(COLD_DELETE_FALLBACKS)),
@@ -291,6 +303,17 @@ impl Cache {
} }
} }
/// Block requests bound to an old immutable routing snapshot from creating
/// cache tickets during a concurrent configuration publication. This does
/// not clear HOT or COLD and does not modify the representation revision.
pub fn set_control_revision(&self, revision: u64) {
self.store.lock().unwrap_or_else(|e| e.into_inner()).control_revision = revision;
}
pub fn replace_scope_revisions(&self, scopes: HashMap<String, u64>) {
*self.scope_revisions.lock().unwrap_or_else(|e| e.into_inner()) = scopes;
}
pub fn configure(&self, settings: Settings) { pub fn configure(&self, settings: Settings) {
*self.tier_settings.lock().unwrap_or_else(|e| e.into_inner()) = settings.clone(); *self.tier_settings.lock().unwrap_or_else(|e| e.into_inner()) = settings.clone();
// Hot-applied limit reductions take effect immediately for published HOT // Hot-applied limit reductions take effect immediately for published HOT
+7
View File
@@ -201,6 +201,9 @@ struct ColdFile {
#[serde(default)] #[serde(default)]
vary: Vec<String>, vary: Vec<String>,
revision: u64, revision: u64,
/// 0 denotes legacy/unscoped entries; production vhosts require an exact match.
#[serde(default)]
scope_revision: u64,
scope: String, scope: String,
#[serde(default = "default_cache_scheme")] #[serde(default = "default_cache_scheme")]
scheme: String, scheme: String,
@@ -296,6 +299,7 @@ struct ColdStore {
struct ColdDraft { struct ColdDraft {
key: Key, key: Key,
revision: u64, revision: u64,
scope_revision: u64,
generation: u64, generation: u64,
scope_generation: u64, scope_generation: u64,
key_generation: u64, key_generation: u64,
@@ -321,6 +325,9 @@ struct Store {
sequence: u64, sequence: u64,
generation: u64, generation: u64,
revision: u64, revision: u64,
/// Current persisted administrative config revision. Distinct from cache
/// representation revision: operational saves do not evict data.
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, String), DomainCounters>, domains: BTreeMap<(String, String), DomainCounters>,
+2
View File
@@ -55,6 +55,8 @@ pub struct Cache {
revision: AtomicU64, revision: AtomicU64,
cold: Arc<Mutex<ColdStore>>, cold: Arc<Mutex<ColdStore>>,
tier_settings: Mutex<Settings>, tier_settings: Mutex<Settings>,
/// Per-vhost content compatibility used by persisted COLD metadata.
scope_revisions: Mutex<HashMap<String, u64>>,
data_root: Option<PathBuf>, data_root: Option<PathBuf>,
cold_tx: Option<mpsc::Sender<ColdCommand>>, cold_tx: Option<mpsc::Sender<ColdCommand>>,
cold_delete_fallbacks: Arc<Semaphore>, cold_delete_fallbacks: Arc<Semaphore>,
+35
View File
@@ -774,4 +774,39 @@ fn cold_path_cannot_escape_data_directory() {
settings.cold_path = "cache/cold".into(); settings.cold_path = "cache/cold".into();
assert!(settings.validate().is_ok()); assert!(settings.validate().is_ok());
} }
#[tokio::test]
async fn cold_scope_fingerprint_preserves_unmodified_vhost_and_rejects_stale_restart() {
let dir = tempfile::tempdir().unwrap();
let settings = cold_settings();
let shutdown = CancellationToken::new();
let mut fingerprints = HashMap::new();
fingerprints.insert("cold-test".to_owned(), 77);
let cache = Cache::start_with_scope_revisions(
11, Some(dir.path().to_path_buf()), settings.clone(), shutdown.clone(),
fingerprints.clone(),
);
fill(&cache, &settings, "/one", "stable-contents").await;
fill(&cache, &settings, "/two", "second-contents").await;
wait_for_cold(&cache, 1).await;
shutdown.cancel();
drop(cache);
let restart = CancellationToken::new();
let same = Cache::start_with_scope_revisions(
11, Some(dir.path().to_path_buf()), settings.clone(), restart.clone(),
fingerprints.clone(),
);
assert_eq!(same.view().cold_entries, 1, "operational saves retain COLD");
restart.cancel();
drop(same);
fingerprints.insert("cold-test".to_owned(), 78);
let restart = CancellationToken::new();
let changed = Cache::start_with_scope_revisions(
11, Some(dir.path().to_path_buf()), settings.clone(), restart.clone(), fingerprints,
);
assert_eq!(changed.view().cold_entries, 0, "old COLD must never survive a content revision");
restart.cancel();
}
} }
+16
View File
@@ -54,6 +54,22 @@ mod scope_tests {
fill(&cache, ticket("a")).await; fill(&cache, ticket("a")).await;
assert_eq!(cache.scope_view("a").entries, 1); assert_eq!(cache.scope_view("a").entries, 1);
} }
#[test]
fn stale_snapshot_cannot_issue_cache_ticket_after_config_commit() {
let cache = Cache::new(1);
cache.set_control_revision(7);
let ticket = |revision| cache.ticket_scoped_for_config(
revision, 1, "vhost-a", "http", "same.test", "main", "/resource", &HeaderMap::new()
);
assert!(ticket(7).is_some(), "the current snapshot can cache");
// An operational save only advances the config publication barrier.
// It must not change the representation revision or clear any objects.
cache.set_control_revision(8);
assert_eq!(cache.revision(), 1);
assert!(ticket(7).is_none(), "late request from the previous snapshot must bypass");
assert!(ticket(8).is_some(), "the new snapshot can reuse the same cache identity");
}
#[test] #[test]
fn per_vhost_settings_inherit_and_cannot_expand_shared_capacity() { fn per_vhost_settings_inherit_and_cannot_expand_shared_capacity() {
let global = Settings::default(); let global = Settings::default();
+36 -1
View File
@@ -22,6 +22,39 @@ impl Cache {
_route: &str, _route: &str,
target: &str, target: &str,
headers: &HeaderMap, headers: &HeaderMap,
) -> Option<Ticket> {
self.ticket_scoped_impl(None, revision, scope, scheme, authority, _route, target, headers)
}
/// Production requests bind the cache ticket to the immutable routing
/// configuration used to authorize/route the request. After a config save,
/// stale in-flight snapshots cannot re-fill a scope after its purge.
#[allow(clippy::too_many_arguments)]
pub fn ticket_scoped_for_config(
&self,
control_revision: u64,
revision: u64,
scope: &str,
scheme: &str,
authority: &str,
route: &str,
target: &str,
headers: &HeaderMap,
) -> Option<Ticket> {
self.ticket_scoped_impl(Some(control_revision), revision, scope, scheme, authority, route, target, headers)
}
#[allow(clippy::too_many_arguments)]
fn ticket_scoped_impl(
&self,
expected_control_revision: Option<u64>,
revision: u64,
scope: &str,
scheme: &str,
authority: &str,
_route: &str,
target: &str,
headers: &HeaderMap,
) -> Option<Ticket> { ) -> Option<Ticket> {
if target.len() > 8192 || scope.len() > 64 { if target.len() > 8192 || scope.len() > 64 {
return None; return None;
@@ -50,7 +83,9 @@ impl Cache {
} }
let key: Key = digest.finalize().into(); let key: Key = digest.finalize().into();
let store = self.store.lock().unwrap_or_else(|e| e.into_inner()); let store = self.store.lock().unwrap_or_else(|e| e.into_inner());
(store.revision == revision).then_some(Ticket { (store.revision == revision
&& expected_control_revision.is_none_or(|expected| expected == store.control_revision))
.then_some(Ticket {
scheme: hash_scheme, scheme: hash_scheme,
authority: hash_authority, authority: hash_authority,
domain, domain,
+2 -2
View File
@@ -207,8 +207,8 @@ fn compile_local_hosts(config: &Config) -> Result<LocalHostBuild> {
} }
struct RoutingBuild { struct RoutingBuild {
routes: HashMap<String, Vec<CompiledRoute>>, routes: HashMap<String, Vec<Arc<CompiledRoute>>>,
fallbacks: HashMap<String, Vec<CompiledRoute>>, fallbacks: HashMap<String, Vec<Arc<CompiledRoute>>>,
frontends: HashMap<String, Vec<Arc<CompiledFrontend>>>, frontends: HashMap<String, Vec<Arc<CompiledFrontend>>>,
redirects: HashMap<String, Vec<Arc<crate::redirect::CompiledHost>>>, redirects: HashMap<String, Vec<Arc<crate::redirect::CompiledHost>>>,
not_found_hosts: HashMap<String, Vec<Arc<crate::not_found::CompiledHost>>>, not_found_hosts: HashMap<String, Vec<Arc<crate::not_found::CompiledHost>>>,
+14 -2
View File
@@ -10,8 +10,20 @@ impl Snapshot {
local: SocketAddr, local: SocketAddr,
tls: bool, tls: bool,
) -> Option<&CompiledRoute> { ) -> Option<&CompiledRoute> {
self.select_internal_on_arc(host, vhost_id, target, local, tls)
.map(Arc::as_ref)
}
pub fn select_internal_on_arc(
&self,
host: &str,
vhost_id: &str,
target: &str,
local: SocketAddr,
tls: bool,
) -> Option<&Arc<CompiledRoute>> {
for pattern in Self::host_patterns(host) { for pattern in Self::host_patterns(host) {
let owns_host = self.fallbacks.get(&pattern).is_some_and(|routes| { let owns_host = self.fallbacks.get(pattern.as_ref()).is_some_and(|routes| {
routes.iter().any(|route| { routes.iter().any(|route| {
route.frontend.as_ref().is_some_and(|frontend| frontend.id == vhost_id) route.frontend.as_ref().is_some_and(|frontend| frontend.id == vhost_id)
&& route.bindings.iter().any(|binding| binding.matches(local, tls)) && route.bindings.iter().any(|binding| binding.matches(local, tls))
@@ -20,7 +32,7 @@ impl Snapshot {
if !owns_host { if !owns_host {
continue; continue;
} }
return self.routes.get(&pattern).and_then(|routes| { return self.routes.get(pattern.as_ref()).and_then(|routes| {
routes.iter().find(|route| { routes.iter().find(|route| {
route.internal route.internal
&& route.name == target && route.name == target
+2 -2
View File
@@ -289,9 +289,9 @@ pub struct Snapshot {
frontends: HashMap<String, Vec<Arc<CompiledFrontend>>>, frontends: HashMap<String, Vec<Arc<CompiledFrontend>>>,
pub tls: Option<Arc<rustls::ServerConfig>>, pub tls: Option<Arc<rustls::ServerConfig>>,
pub envelope: Envelope, pub envelope: Envelope,
routes: HashMap<String, Vec<CompiledRoute>>, routes: HashMap<String, Vec<Arc<CompiledRoute>>>,
/// Main vhost backend fallback, kept separate from user Custom locations. /// Main vhost backend fallback, kept separate from user Custom locations.
fallbacks: HashMap<String, Vec<CompiledRoute>>, fallbacks: HashMap<String, Vec<Arc<CompiledRoute>>>,
redirects: HashMap<String, Vec<Arc<crate::redirect::CompiledHost>>>, redirects: HashMap<String, Vec<Arc<crate::redirect::CompiledHost>>>,
not_found_hosts: HashMap<String, Vec<Arc<crate::not_found::CompiledHost>>>, not_found_hosts: HashMap<String, Vec<Arc<crate::not_found::CompiledHost>>>,
host_acls: HashMap<String, Arc<CompiledAcl>>, host_acls: HashMap<String, Arc<CompiledAcl>>,
+6 -6
View File
@@ -9,8 +9,8 @@ fn compile_routing(
effective_tls: &crate::tls::Settings, effective_tls: &crate::tls::Settings,
tls: &Option<Arc<rustls::ServerConfig>>, tls: &Option<Arc<rustls::ServerConfig>>,
) -> Result<RoutingBuild> { ) -> Result<RoutingBuild> {
let mut routes: HashMap<String, Vec<CompiledRoute>> = HashMap::new(); let mut routes: HashMap<String, Vec<Arc<CompiledRoute>>> = HashMap::new();
let mut fallbacks: HashMap<String, Vec<CompiledRoute>> = HashMap::new(); let mut fallbacks: HashMap<String, Vec<Arc<CompiledRoute>>> = HashMap::new();
let mut frontends: HashMap<String, Vec<Arc<CompiledFrontend>>> = HashMap::new(); let mut frontends: HashMap<String, Vec<Arc<CompiledFrontend>>> = HashMap::new();
let (redirects, not_found_hosts, mut claimed_hosts) = compile_local_hosts(config)?; let (redirects, not_found_hosts, mut claimed_hosts) = compile_local_hosts(config)?;
let mut pools = Vec::new(); let mut pools = Vec::new();
@@ -247,7 +247,7 @@ fn compile_routing(
None, None,
), ),
}; };
let fallback = CompiledRoute { let fallback = Arc::new(CompiledRoute {
backend_id: None, backend_id: None,
bindings: frontend.bindings.clone(), bindings: frontend.bindings.clone(),
frontend: Some(frontend.clone()), frontend: Some(frontend.clone()),
@@ -273,7 +273,7 @@ fn compile_routing(
method_policy: crate::vhost::MethodPolicy::default(), method_policy: crate::vhost::MethodPolicy::default(),
headers: crate::headers::CompiledHeaders::default(), headers: crate::headers::CompiledHeaders::default(),
unavailable: host.transient_unavailable && host.source.is_reverse_proxy(), unavailable: host.transient_unavailable && host.source.is_reverse_proxy(),
}; });
for name in &routing_hosts { for name in &routing_hosts {
fallbacks fallbacks
.entry(name.clone()) .entry(name.clone())
@@ -389,7 +389,7 @@ fn compile_routing(
), ),
} }
}; };
let compiled = CompiledRoute { let compiled = Arc::new(CompiledRoute {
backend_id: route.backend_id.clone(), backend_id: route.backend_id.clone(),
bindings: frontend.bindings.clone(), bindings: frontend.bindings.clone(),
frontend: Some(frontend.clone()), frontend: Some(frontend.clone()),
@@ -415,7 +415,7 @@ fn compile_routing(
method_policy: route.method_policy.clone(), method_policy: route.method_policy.clone(),
headers, headers,
unavailable: false, unavailable: false,
}; });
for name in &routing_hosts { for name in &routing_hosts {
routes routes
.entry(name.clone()) .entry(name.clone())
+31 -21
View File
@@ -1,17 +1,16 @@
impl Snapshot { impl Snapshot {
fn host_patterns(host: &str) -> Vec<String> { // Exact host: no allocation. Wildcards: lazy and most-specific-first.
let mut patterns = vec![host.to_owned()]; fn host_patterns(host: &str) -> impl Iterator<Item = std::borrow::Cow<'_, str>> {
for (index, _) in host.match_indices('.') { std::iter::once(std::borrow::Cow::Borrowed(host)).chain(
let suffix = &host[index + 1..]; host.match_indices('.').filter_map(move |(index, _)| {
if suffix.contains('.') { let suffix = &host[index + 1..];
patterns.push(format!("*.{suffix}")); suffix.contains('.').then(|| std::borrow::Cow::Owned(format!("*.{suffix}")))
} }),
} )
patterns
} }
pub fn host_allowed(&self, host: &str, peer: IpAddr) -> bool { pub fn host_allowed(&self, host: &str, peer: IpAddr) -> bool {
for pattern in Self::host_patterns(host) { for pattern in Self::host_patterns(host) {
if let Some(acl) = self.host_acls.get(&pattern) { if let Some(acl) = self.host_acls.get(pattern.as_ref()) {
return acl.allows(peer); return acl.allows(peer);
} }
} }
@@ -53,8 +52,19 @@ impl Snapshot {
local: SocketAddr, local: SocketAddr,
tls: bool, tls: bool,
) -> Option<&CompiledRoute> { ) -> Option<&CompiledRoute> {
self.select_on_arc(host, path, local, tls).map(Arc::as_ref)
}
/// Shared compiled-route handle for middleware and forward().
pub fn select_on_arc(
&self,
host: &str,
path: &str,
local: SocketAddr,
tls: bool,
) -> Option<&Arc<CompiledRoute>> {
for pattern in Self::host_patterns(host) { for pattern in Self::host_patterns(host) {
let fallback = self.fallbacks.get(&pattern).and_then(|routes| { let fallback = self.fallbacks.get(pattern.as_ref()).and_then(|routes| {
routes routes
.iter() .iter()
.find(|route| route.bindings.iter().any(|l| l.matches(local, tls))) .find(|route| route.bindings.iter().any(|l| l.matches(local, tls)))
@@ -65,7 +75,7 @@ impl Snapshot {
if let Some(fallback) = fallback { if let Some(fallback) = fallback {
return self return self
.routes .routes
.get(&pattern) .get(pattern.as_ref())
.and_then(|routes| { .and_then(|routes| {
select_route(routes, path, |r| { select_route(routes, path, |r| {
r.bindings.iter().any(|l| l.matches(local, tls)) r.bindings.iter().any(|l| l.matches(local, tls))
@@ -76,7 +86,6 @@ impl Snapshot {
} }
None None
} }
pub fn frontend( pub fn frontend(
&self, &self,
host: &str, host: &str,
@@ -84,7 +93,7 @@ impl Snapshot {
tls: bool, tls: bool,
) -> Option<&Arc<CompiledFrontend>> { ) -> Option<&Arc<CompiledFrontend>> {
for pattern in Self::host_patterns(host) { for pattern in Self::host_patterns(host) {
if let Some(frontend) = self.frontends.get(&pattern).and_then(|frontends| { if let Some(frontend) = self.frontends.get(pattern.as_ref()).and_then(|frontends| {
frontends frontends
.iter() .iter()
.find(|f| f.bindings.iter().any(|l| l.matches(local, tls))) .find(|f| f.bindings.iter().any(|l| l.matches(local, tls)))
@@ -155,7 +164,7 @@ impl Snapshot {
&crate::redirect::CompiledRule, &crate::redirect::CompiledRule,
)> { )> {
for pattern in Self::host_patterns(host) { for pattern in Self::host_patterns(host) {
if let Some((redirect, rule)) = self.redirects.get(&pattern).and_then(|redirects| { if let Some((redirect, rule)) = self.redirects.get(pattern.as_ref()).and_then(|redirects| {
redirects.iter().find_map(|redirect| { redirects.iter().find_map(|redirect| {
if !redirect if !redirect
.bindings .bindings
@@ -184,7 +193,7 @@ impl Snapshot {
tls: bool, tls: bool,
) -> Option<&Arc<crate::not_found::CompiledHost>> { ) -> Option<&Arc<crate::not_found::CompiledHost>> {
for pattern in Self::host_patterns(host) { for pattern in Self::host_patterns(host) {
if let Some(item) = self.not_found_hosts.get(&pattern).and_then(|items| { if let Some(item) = self.not_found_hosts.get(pattern.as_ref()).and_then(|items| {
items.iter().find(|item| { items.iter().find(|item| {
item.bindings item.bindings
.iter() .iter()
@@ -202,14 +211,15 @@ impl Snapshot {
for pattern in Self::host_patterns(host) { for pattern in Self::host_patterns(host) {
if let Some(fallback) = self if let Some(fallback) = self
.fallbacks .fallbacks
.get(&pattern) .get(pattern.as_ref())
.and_then(|routes| routes.first()) .and_then(|routes| routes.first())
{ {
return self return self
.routes .routes
.get(&pattern) .get(pattern.as_ref())
.and_then(|routes| select_route(routes, path, |_| true)) .and_then(|routes| select_route(routes, path, |_| true))
.or(Some(fallback)); .or(Some(fallback))
.map(Arc::as_ref);
} }
} }
None None
@@ -217,10 +227,10 @@ impl Snapshot {
} }
fn select_route<'a, F>( fn select_route<'a, F>(
routes: &'a [CompiledRoute], routes: &'a [Arc<CompiledRoute>],
path: &str, path: &str,
allowed: F, allowed: F,
) -> Option<&'a CompiledRoute> ) -> Option<&'a Arc<CompiledRoute>>
where where
F: Fn(&CompiledRoute) -> bool, F: Fn(&CompiledRoute) -> bool,
{ {
+13
View File
@@ -9,6 +9,19 @@ fn policy() -> ValidationPolicy {
admin_addr: "127.0.0.1:9090".parse().unwrap(), admin_addr: "127.0.0.1:9090".parse().unwrap(),
} }
} }
#[test]
fn route_handles_reuse_snapshot_ownership_without_cloning_compiled_routes() {
let snapshot = compile(demo_config("127.0.0.1:9000".parse().unwrap()), 1, &policy()).unwrap();
let local = "127.0.0.1:8080".parse().unwrap();
let first = snapshot.select_on_arc("localhost", "/", local, false).unwrap();
let second = snapshot.select_on_arc("localhost", "/child", local, false).unwrap();
// Both selectors point into one immutable compiled snapshot.
assert!(std::sync::Arc::ptr_eq(first, second));
let legacy = snapshot.select_on("localhost", "/", local, false).unwrap();
assert!(std::ptr::eq(first.as_ref(), legacy));
assert!(snapshot.select_on_arc("not-this-host", "/", local, false).is_none());
}
#[test] #[test]
fn missing_http2_settings_use_release_defaults() { fn missing_http2_settings_use_release_defaults() {
let config: Config = serde_json::from_value(serde_json::json!({})).unwrap(); let config: Config = serde_json::from_value(serde_json::json!({})).unwrap();
+5 -1
View File
@@ -151,7 +151,11 @@ impl Logs {
None None
}; };
drop(evicted); drop(evicted);
let _ = self.live.send(entry.clone()); // 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());
}
self.push_sink(entry); self.push_sink(entry);
} }
fn push_sink(&self, entry: LogEntry) { fn push_sink(&self, entry: LogEntry) {
+71 -20
View File
@@ -10,7 +10,7 @@ use std::{
future::Future, future::Future,
pin::Pin, pin::Pin,
sync::{ sync::{
atomic::{AtomicBool, AtomicU16, AtomicU64, Ordering}, atomic::{AtomicBool, AtomicU16, AtomicU64, AtomicU8, Ordering},
Arc, Mutex, Arc, Mutex,
}, },
task::{Context, Poll}, task::{Context, Poll},
@@ -21,13 +21,44 @@ use tokio::{
time::{sleep_until, Sleep}, time::{sleep_until, Sleep},
}; };
/// Compact source tags avoid a mutex on every completed exchange.
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum ExchangeSource {
Unresolved = 0,
LocalResponse = 1,
LocalRedirect = 2,
WebServer = 3,
ReverseProxy = 4,
}
impl ExchangeSource {
fn name(value: u8) -> &'static str {
match value {
1 => "local_response",
2 => "local_redirect",
3 => "web_server",
4 => "reverse_proxy",
_ => "unresolved",
}
}
}
// 0 = unknown, 1 = Some(false), 2 = Some(true). Preserve log schema.
fn read_effective(value: u8) -> Option<bool> {
match value {
1 => Some(false),
2 => Some(true),
_ => None,
}
}
pub struct Exchange { pub struct Exchange {
pub vhost_metrics: Option<Arc<crate::metrics::VhostMetrics>>, pub vhost_metrics: Option<Arc<crate::metrics::VhostMetrics>>,
pub vhost: Option<String>, pub vhost: Option<String>,
pub route: Mutex<Option<String>>, pub route: Mutex<Option<String>>,
pub source: Mutex<&'static str>, pub source: AtomicU8,
pub cache_effective_enabled: Mutex<Option<bool>>, pub cache_effective_enabled: AtomicU8,
pub transform_effective_enabled: Mutex<Option<bool>>, pub transform_effective_enabled: AtomicU8,
pub backend: Mutex<Option<Arc<crate::balance::Lease>>>, pub backend: Mutex<Option<Arc<crate::balance::Lease>>>,
pub id: u64, pub id: u64,
pub request_id: String, pub request_id: String,
@@ -59,6 +90,17 @@ pub struct Exchange {
pub _permit: OwnedSemaphorePermit, pub _permit: OwnedSemaphorePermit,
} }
impl Exchange { impl Exchange {
pub fn set_source(&self, source: ExchangeSource) {
self.source.store(source as u8, Ordering::Relaxed);
}
pub fn set_cache_enabled(&self, enabled: bool) {
self.cache_effective_enabled
.store(if enabled { 2 } else { 1 }, Ordering::Relaxed);
}
pub fn set_transform_enabled(&self, enabled: bool) {
self.transform_effective_enabled
.store(if enabled { 2 } else { 1 }, Ordering::Relaxed);
}
pub fn websocket_phase(&self, phase: &'static str) { pub fn websocket_phase(&self, phase: &'static str) {
if self.websocket_bootstrap.is_some() { if self.websocket_bootstrap.is_some() {
*self *self
@@ -145,15 +187,9 @@ impl Drop for Exchange {
.lock() .lock()
.unwrap_or_else(|e| e.into_inner()); .unwrap_or_else(|e| e.into_inner());
let route = self.route.lock().unwrap_or_else(|e| e.into_inner()).clone(); let route = self.route.lock().unwrap_or_else(|e| e.into_inner()).clone();
let source = *self.source.lock().unwrap_or_else(|e| e.into_inner()); let source = ExchangeSource::name(self.source.load(Ordering::Relaxed));
let cache_effective_enabled = *self let cache_effective_enabled = read_effective(self.cache_effective_enabled.load(Ordering::Relaxed));
.cache_effective_enabled let transform_effective_enabled = read_effective(self.transform_effective_enabled.load(Ordering::Relaxed));
.lock()
.unwrap_or_else(|e| e.into_inner());
let transform_effective_enabled = *self
.transform_effective_enabled
.lock()
.unwrap_or_else(|e| e.into_inner());
let client_ip = self let client_ip = self
.client_ip .client_ip
.lock() .lock()
@@ -213,17 +249,17 @@ impl Drop for Exchange {
authority: None, authority: None,
matched_by: None, matched_by: None,
cache_reason: *self.cache_reason.lock().unwrap_or_else(|e| e.into_inner()), cache_reason: *self.cache_reason.lock().unwrap_or_else(|e| e.into_inner()),
vhost: self.vhost.clone(), vhost: self.vhost.take(),
route, route,
cache_effective_enabled, cache_effective_enabled,
transform_effective_enabled, transform_effective_enabled,
backend, backend,
id: self.id, id: self.id,
request_id: self.request_id.clone(), request_id: std::mem::take(&mut self.request_id),
time_ms: now_ms(), time_ms: now_ms(),
method: self.method.clone(), method: std::mem::take(&mut self.method),
host: self.host.clone(), host: std::mem::take(&mut self.host),
path: self.path.clone(), path: std::mem::take(&mut self.path),
http_version: self.http_version, http_version: self.http_version,
transport: self.transport, transport: self.transport,
websocket_bootstrap: self.websocket_bootstrap, websocket_bootstrap: self.websocket_bootstrap,
@@ -243,7 +279,7 @@ impl Drop for Exchange {
.unwrap_or_else(|e| e.into_inner()) .unwrap_or_else(|e| e.into_inner())
.clone(), .clone(),
client_ip, client_ip,
user_agent: self.user_agent.clone(), user_agent: self.user_agent.take(),
country, country,
status, status,
duration_ms, duration_ms,
@@ -262,6 +298,9 @@ pub enum Direction {
pub struct MeteredBody { pub struct MeteredBody {
inner: Body, inner: Body,
exchange: Option<Arc<Exchange>>, exchange: Option<Arc<Exchange>>,
/// 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<Arc<crate::balance::Lease>>,
direction: Direction, direction: Direction,
timer: Option<Pin<Box<Sleep>>>, timer: Option<Pin<Box<Sleep>>>,
deadline: tokio::time::Instant, deadline: tokio::time::Instant,
@@ -271,9 +310,13 @@ pub struct MeteredBody {
impl MeteredBody { impl MeteredBody {
pub fn new(inner: Body, exchange: Arc<Exchange>, direction: Direction, idle: Duration) -> Self { pub fn new(inner: Body, exchange: Arc<Exchange>, direction: Direction, idle: Duration) -> Self {
let finished = inner.is_end_stream(); let finished = inner.is_end_stream();
let response_backend = matches!(direction, Direction::Response)
.then(|| exchange.backend.lock().unwrap_or_else(|e| e.into_inner()).clone())
.flatten();
Self { Self {
inner, inner,
exchange: Some(exchange), exchange: Some(exchange),
response_backend,
direction, direction,
// Ready/empty bodies never need a timer allocation. Keep the // Ready/empty bodies never need a timer allocation. Keep the
// original deadline so a delayed first poll still times out. // original deadline so a delayed first poll still times out.
@@ -286,6 +329,7 @@ impl MeteredBody {
fn finish(&mut self) { fn finish(&mut self) {
self.finished = true; self.finished = true;
self.timer.take(); self.timer.take();
self.response_backend.take();
self.exchange.take(); self.exchange.take();
} }
} }
@@ -305,7 +349,14 @@ impl HttpBody for MeteredBody {
Poll::Ready(Some(Ok(frame))) => { Poll::Ready(Some(Ok(frame))) => {
if let (Some(data), Some(exchange)) = (frame.data_ref(), this.exchange.as_ref()) { if let (Some(data), Some(exchange)) = (frame.data_ref(), this.exchange.as_ref()) {
let count = data.len() as u64; let count = data.len() as u64;
exchange.backend_bytes(count, matches!(this.direction, Direction::Request)); match this.direction {
Direction::Request => exchange.backend_bytes(count, true),
Direction::Response => {
if let Some(lease) = &this.response_backend {
lease.bytes(count, false);
}
}
}
match this.direction { match this.direction {
Direction::Request => { Direction::Request => {
exchange.request_bytes.fetch_add(count, Ordering::Relaxed); exchange.request_bytes.fetch_add(count, Ordering::Relaxed);
+27 -3
View File
@@ -11,9 +11,9 @@ mod tests {
vhost_metrics: None, vhost_metrics: None,
vhost: None, vhost: None,
route: Mutex::new(None), route: Mutex::new(None),
source: Mutex::new("unresolved"), source: AtomicU8::new(ExchangeSource::Unresolved as u8),
cache_effective_enabled: Mutex::new(None), cache_effective_enabled: AtomicU8::new(0),
transform_effective_enabled: Mutex::new(None), transform_effective_enabled: AtomicU8::new(0),
backend: Mutex::new(None), backend: Mutex::new(None),
id: 1, id: 1,
request_id: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".into(), request_id: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".into(),
@@ -225,4 +225,28 @@ mod tests {
); );
} }
} }
#[tokio::test]
async fn atomic_exchange_tags_keep_original_observability_fields() {
let (exchange, slots) = observation().await;
let logs = exchange.logs.clone();
exchange.set_source(ExchangeSource::ReverseProxy);
exchange.set_cache_enabled(false);
exchange.set_transform_enabled(true);
drop(exchange);
assert_eq!(slots.available_permits(), 1);
let entries = logs.recent();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].cache_effective_enabled, Some(false));
assert_eq!(entries[0].transform_effective_enabled, Some(true));
assert_eq!(entries[0].cache_reason, "not_proxy_response");
assert_eq!(entries[0].method, "POST");
assert_eq!(entries[0].host, "localhost");
assert_eq!(entries[0].path, "/echo");
assert_eq!(entries[0].request_id.len(), 64);
assert_eq!(ExchangeSource::name(ExchangeSource::ReverseProxy as u8), "reverse_proxy");
assert_eq!(read_effective(0), None);
assert_eq!(read_effective(1), Some(false));
assert_eq!(read_effective(2), Some(true));
}
} }
+2 -2
View File
@@ -159,7 +159,7 @@ fn response_context(
request: &Request, request: &Request,
) -> ( ) -> (
Option<Arc<crate::config::CompiledFrontend>>, Option<Arc<crate::config::CompiledFrontend>>,
Option<crate::config::CompiledRoute>, Option<Arc<crate::config::CompiledRoute>>,
) { ) {
let tls = request let tls = request
.extensions() .extensions()
@@ -174,7 +174,7 @@ fn response_context(
let frontend = snapshot.frontend(&host, local, tls).cloned(); let frontend = snapshot.frontend(&host, local, tls).cloned();
let route = crate::acl::routing_path(request.uri().path()) let route = crate::acl::routing_path(request.uri().path())
.ok() .ok()
.and_then(|path| snapshot.select_on(&host, &path, local, tls).cloned()); .and_then(|path| snapshot.select_on_arc(&host, &path, local, tls).cloned());
Some((frontend, route)) Some((frontend, route))
})(); })();
context.unwrap_or((None, None)) context.unwrap_or((None, None))
+4 -3
View File
@@ -6,7 +6,7 @@ struct InternalFallbackRoute {
} }
#[derive(Clone)] #[derive(Clone)]
struct FinalRouteContext(crate::config::CompiledRoute); struct FinalRouteContext(Arc<crate::config::CompiledRoute>);
/// The named location has already produced the terminal HTTP response. /// The named location has already produced the terminal HTTP response.
/// In particular, do not replace its 404 HTML with a global/vhost error page. /// In particular, do not replace its 404 HTML with a global/vhost error page.
@@ -135,20 +135,21 @@ async fn execute_error_fallback(
tls: bool, tls: bool,
route: &crate::config::CompiledRoute, route: &crate::config::CompiledRoute,
status: StatusCode, status: StatusCode,
template: &ErrorFallbackRequest, template: &Option<ErrorFallbackRequest>,
body: &ErrorFallbackBody, body: &ErrorFallbackBody,
) -> Option<Response> { ) -> Option<Response> {
let fallback = route let fallback = route
.error_fallback .error_fallback
.as_ref() .as_ref()
.filter(|fallback| fallback.matches(status))?; .filter(|fallback| fallback.matches(status))?;
let template = template.as_ref()?;
let frontend = route.frontend.as_ref()?; let frontend = route.frontend.as_ref()?;
if template.depth >= MAX_ERROR_FALLBACK_DEPTH { if template.depth >= MAX_ERROR_FALLBACK_DEPTH {
tracing::warn!(vhost = %frontend.id, route = %route.id, target = %fallback.target, "error fallback depth limit reached"); tracing::warn!(vhost = %frontend.id, route = %route.id, target = %fallback.target, "error fallback depth limit reached");
return None; return None;
} }
let Some(target) = snapshot let Some(target) = snapshot
.select_internal_on(host, &frontend.id, &fallback.target, local.0, tls) .select_internal_on_arc(host, &frontend.id, &fallback.target, local.0, tls)
.cloned() .cloned()
else { else {
tracing::warn!( tracing::warn!(
+10 -11
View File
@@ -4,16 +4,16 @@
// allow-only country entries and explicit IP allows form one policy scope. // allow-only country entries and explicit IP allows form one policy scope.
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() { if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
*exchange.route.lock().unwrap_or_else(|e| e.into_inner()) = Some(route.id.clone()); *exchange.route.lock().unwrap_or_else(|e| e.into_inner()) = Some(route.id.clone());
*exchange.source.lock().unwrap_or_else(|e| e.into_inner()) = if route.response.is_some() { exchange.set_source(if route.response.is_some() {
"local_response" ExchangeSource::LocalResponse
} else if route.redirect.is_some() { } else if route.redirect.is_some() {
"local_redirect" ExchangeSource::LocalRedirect
} else { } else {
match &route.source { match &route.source {
crate::config::CompiledSource::WebServer(_) => "web_server", crate::config::CompiledSource::WebServer(_) => ExchangeSource::WebServer,
crate::config::CompiledSource::ReverseProxy => "reverse_proxy", crate::config::CompiledSource::ReverseProxy => ExchangeSource::ReverseProxy,
} }
}; });
} }
// Local location responses intentionally still pass host/profile/location ACLs // Local location responses intentionally still pass host/profile/location ACLs
// and the outer response-header/security pipeline, but never select/open an upstream. // and the outer response-header/security pipeline, but never select/open an upstream.
@@ -126,10 +126,8 @@ 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(""); let scope = route.frontend.as_ref().map(|f| f.id.as_str()).unwrap_or("");
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() { if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
*exchange.route.lock().unwrap_or_else(|e| e.into_inner()) = Some(route.id.clone()); *exchange.route.lock().unwrap_or_else(|e| e.into_inner()) = Some(route.id.clone());
*exchange.cache_effective_enabled.lock().unwrap_or_else(|e| e.into_inner()) = exchange.set_cache_enabled(cache_settings.active_for(route.cache));
Some(cache_settings.active_for(route.cache)); exchange.set_transform_enabled(transform_settings.is_some_and(|settings| settings.enabled));
*exchange.transform_effective_enabled.lock().unwrap_or_else(|e| e.into_inner()) =
Some(transform_settings.is_some_and(|settings| settings.enabled));
} }
let policy_reason = if admin_control_plane { let policy_reason = if admin_control_plane {
Some("admin_control_plane") Some("admin_control_plane")
@@ -304,7 +302,8 @@ let key_headers = cache_key_headers
.as_ref() .as_ref()
.unwrap_or_else(|| request.headers()); .unwrap_or_else(|| request.headers());
let cache_ticket = if effective_cacheable { let cache_ticket = if effective_cacheable {
state.cache.ticket_scoped( state.cache.ticket_scoped_for_config(
snapshot.envelope.revision,
state.cache.revision(), state.cache.revision(),
route.frontend.as_ref().map(|f| f.id.as_str()).unwrap_or(""), route.frontend.as_ref().map(|f| f.id.as_str()).unwrap_or(""),
context.scheme, context.scheme,
+32 -15
View File
@@ -60,15 +60,36 @@ if pending_revalidation {
if request.method() == Method::HEAD && cache_ticket.is_some() { if request.method() == Method::HEAD && cache_ticket.is_some() {
*request.method_mut() = Method::GET; *request.method_mut() = Method::GET;
} }
// Preserve a pre-normalization Host snapshot for pool retries. Each next // Save the pre-normalization headers only for replayable requests that may
// backend gets its OWN target and Host/:authority, never the previous peer's. // actually be retried. The common one-backend HTTP/1 route has no retry peer
let retry_headers = request.headers().clone(); // or H2 GOAWAY handling, so copying HeaderMap for every request was wasted.
// Never replay writes, streaming uploads, auth or WebSocket requests.
let retry_method = request.method().clone();
let retry_policy = route.pool.as_ref().map(|pool| &pool.settings.retry);
let pool_retry_possible = route.backend_id.is_none()
&& !admin_control_plane
&& handshake.is_none()
&& retry_safe_method(&retry_method)
&& route.pool.as_ref().is_some_and(|pool| pool.members.len() > 1)
&& retry_policy.is_some_and(|policy| policy.max_retries > 0);
let h2_replay_possible = !admin_control_plane
&& handshake.is_none()
&& retry_safe_method(&retry_method)
&& matches!(backend.protocol,
crate::config::UpstreamProtocol::Http2
| crate::config::UpstreamProtocol::H2
| crate::config::UpstreamProtocol::Auto);
let replay_headers = ((pool_retry_possible || h2_replay_possible)
&& (request.body().size_hint().exact() == Some(0)
|| request.version() == Version::HTTP_3))
.then(|| request.headers().clone());
let expected_backend_host = if backend.preserve_host { let expected_backend_host = if backend.preserve_host {
&original_host &original_host
} else { } else {
&backend.host_header &backend.host_header
}; };
let retry_host_follows_backend = retry_headers.get(HOST) let retry_host_follows_backend = replay_headers.as_ref()
.and_then(|headers| headers.get(HOST))
.is_some_and(|value| value == expected_backend_host); .is_some_and(|value| value == expected_backend_host);
let Some(upstream_target) = upstream_request_target( let Some(upstream_target) = upstream_request_target(
&uri, request.headers_mut(), &backend, &uri, request.headers_mut(), &backend,
@@ -125,14 +146,7 @@ let proactive_base_headers = if cache_ticket.is_some() && cache_settings.proacti
let response_header_timeout = frontend let response_header_timeout = frontend
.and_then(|frontend| frontend.proxy_timeouts.response_header_timeout()) .and_then(|frontend| frontend.proxy_timeouts.response_header_timeout())
.unwrap_or(state.header_timeout); .unwrap_or(state.header_timeout);
let retry_method = request.method().clone(); let retry_enabled = pool_retry_possible && request_body_empty && replay_headers.is_some();
let retry_policy = route.pool.as_ref().map(|pool| pool.settings.retry.clone());
let retry_enabled = route.backend_id.is_none()
&& !admin_control_plane
&& handshake.is_none()
&& request_body_empty
&& retry_safe_method(&retry_method)
&& retry_policy.as_ref().is_some_and(|policy| policy.max_retries > 0);
let upstream_started = Instant::now(); let upstream_started = Instant::now();
let mut attempt_started = Instant::now(); let mut attempt_started = Instant::now();
let mut active_backend = backend.clone(); let mut active_backend = backend.clone();
@@ -165,15 +179,15 @@ let upstream_result = loop {
&& (is_graceful_h2_goaway(cause) && (is_graceful_h2_goaway(cause)
|| is_related_h2_close(cause, &active_backend)) || is_related_h2_close(cause, &active_backend))
{ {
if let Some(next_request) = retry_request( if let Some(next_request) = replay_headers.as_ref().and_then(|headers| retry_request(
&retry_method, &retry_method,
upstream_request_version(&active_backend, false), upstream_request_version(&active_backend, false),
&retry_headers, headers,
&active_backend, &active_backend,
&original_host, &original_host,
&origin_suffix, &origin_suffix,
retry_host_follows_backend, retry_host_follows_backend,
) { )) {
graceful_retry_done = true; graceful_retry_done = true;
if let Some(lease) = &active_lease { if let Some(lease) = &active_lease {
lease.retry_attempted(); lease.retry_attempted();
@@ -201,6 +215,9 @@ let upstream_result = loop {
break result; break result;
} }
attempted_backends.push(active_backend.id.clone()); attempted_backends.push(active_backend.id.clone());
let Some(retry_headers) = replay_headers.as_ref() else {
break result;
};
let Some(pool) = route.pool.as_ref() else { let Some(pool) = route.pool.as_ref() else {
break result; break result;
}; };
+39 -4
View File
@@ -11,6 +11,14 @@
Err(_) => return error(StatusCode::BAD_REQUEST, Err(_) => return error(StatusCode::BAD_REQUEST,
"RustProxy: invalid Web Server rewrite path\n"), "RustProxy: invalid Web Server rewrite path\n"),
}; };
#[cfg(target_os = "linux")]
let kernel_sendfile = request.extensions()
.get::<crate::server::sendfile::SendfileSlots>().cloned();
#[cfg(target_os = "linux")]
let identity_request = request.version() == http::Version::HTTP_11
&& !request.headers().contains_key("accept-encoding")
&& !request.headers().contains_key("range")
&& !request.headers().contains_key("if-range");
let cache_settings = route.frontend.as_ref() let cache_settings = route.frontend.as_ref()
.map(|frontend| &frontend.cache) .map(|frontend| &frontend.cache)
.unwrap_or(&snapshot.envelope.config.cache); .unwrap_or(&snapshot.envelope.config.cache);
@@ -19,9 +27,8 @@
let enabled = cache_settings.active_for(route.cache) let enabled = cache_settings.active_for(route.cache)
&& frontend.is_none_or(|frontend| frontend.basic_auth.is_none() && frontend.tinyauth.is_none()); && frontend.is_none_or(|frontend| frontend.basic_auth.is_none() && frontend.tinyauth.is_none());
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() { if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
*exchange.cache_effective_enabled.lock().unwrap_or_else(|e| e.into_inner()) = Some(enabled); exchange.set_cache_enabled(enabled);
*exchange.transform_effective_enabled.lock().unwrap_or_else(|e| e.into_inner()) = exchange.set_transform_enabled(transform_settings.is_some_and(|settings| settings.enabled));
Some(transform_settings.is_some_and(|settings| settings.enabled));
} }
// The authority is already validated by effective_host. HTTP/2 and HTTP/3 // The authority is already validated by effective_host. HTTP/2 and HTTP/3
// requests commonly have :authority but no Host field, which otherwise // requests commonly have :authority but no Host field, which otherwise
@@ -64,7 +71,8 @@
Some(headers) Some(headers)
} else { None }; } else { None };
let cache_ticket = if eligible { let cache_ticket = if eligible {
state.cache.ticket_scoped(state.cache.revision(), scope, context.scheme, state.cache.ticket_scoped_for_config(snapshot.envelope.revision,
state.cache.revision(), scope, context.scheme,
original_host.to_str().unwrap_or(""), &route.id, &public_suffix, original_host.to_str().unwrap_or(""), &route.id, &public_suffix,
key_headers.as_ref().unwrap_or(&request_headers)) key_headers.as_ref().unwrap_or(&request_headers))
} else { None }; } else { None };
@@ -280,5 +288,32 @@
cache_settings.annotate_reason(response.headers_mut(), outcome, 0, 0, reason); cache_settings.annotate_reason(response.headers_mut(), outcome, 0, 0, reason);
let transformed = response.extensions().get::<crate::transform::Applied>().is_some(); let transformed = response.extensions().get::<crate::transform::Applied>().is_some();
cache_settings.annotate_transformed(response.headers_mut(), transformed); cache_settings.annotate_transformed(response.headers_mut(), transformed);
// Register only AFTER the vhost response policy, cache and transformation
// decisions are final. Never feed synthetic transport placeholders into
// cache, compression or the transformation engine. The ordinary body
// remains the fallback on every other protocol/response and when the
// per-connection bridge has no capacity.
#[cfg(target_os = "linux")]
if !enabled && identity_request && request_method == Method::GET
&& !transform_settings.is_some_and(|settings| settings.enabled)
&& response.status() == StatusCode::OK
&& !response.headers().contains_key("content-encoding")
&& !response.headers().contains_key("transfer-encoding")
&& !response.headers().contains_key("content-range")
{
if let (Some(slots), Some(candidate)) = (
kernel_sendfile,
response.extensions().get::<crate::server::sendfile::SendfileCandidate>().cloned(),
) {
let correct_length = response.headers().get("content-length")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok()) == Some(candidate.length);
if correct_length {
if let Some(body) = slots.create_body(candidate, service.web_metrics.clone()) {
*response.body_mut() = Body::new(body);
}
}
}
}
response response
} }
+15 -2
View File
@@ -107,7 +107,14 @@ async fn forward_request(
"RustProxy: ambiguous request path\n", "RustProxy: ambiguous request path\n",
); );
}; };
let frontend = snapshot.frontend(&host, local.0, tls); let selected_context = request.extensions().get::<SelectedRouteContext>().cloned();
let frontend = if let Some(selection) = &selected_context {
selection.frontend.as_ref()
} else {
// Internal error fallback calls forward_request directly, without
// rerunning the observation middleware.
snapshot.frontend(&host, local.0, tls)
};
if snapshot.user_agent_blocking.blocks(request.headers()) if snapshot.user_agent_blocking.blocks(request.headers())
|| frontend.is_some_and(|frontend| frontend.user_agent_blocking.blocks(request.headers())) || frontend.is_some_and(|frontend| frontend.user_agent_blocking.blocks(request.headers()))
{ {
@@ -307,6 +314,8 @@ async fn forward_request(
} else { } else {
snapshot.select_internal_on(&host, &dispatch.vhost_id, &dispatch.target, local.0, tls) snapshot.select_internal_on(&host, &dispatch.vhost_id, &dispatch.target, local.0, tls)
} }
} else if let Some(selection) = &selected_context {
selection.route.as_deref()
} else { } else {
snapshot.select_on(&host, &path, local.0, tls) snapshot.select_on(&host, &path, local.0, tls)
}; };
@@ -343,7 +352,11 @@ async fn forward_request(
); );
} }
} }
let fallback_request = ErrorFallbackRequest::capture(&request, &snapshot); // Normal routes almost never have an internal error fallback. Do not copy
// the complete HeaderMap and request metadata when no fallback exists.
let fallback_request = selected
.filter(|route| route.error_fallback.is_some())
.map(|_| ErrorFallbackRequest::capture(&request, &snapshot));
// The admin cookie is never ordinary application state. Strip it BEFORE // The admin cookie is never ordinary application state. Strip it BEFORE
// auth/cache and keep only the removed fragment locally. A verified self-admin // auth/cache and keep only the removed fragment locally. A verified self-admin
// backend may restore it after the actual upstream has been selected. // backend may restore it after the actual upstream has been selected.
+9 -2
View File
@@ -13,7 +13,7 @@ use axum::{
response::{IntoResponse, Response}, response::{IntoResponse, Response},
Extension, Router, Extension, Router,
}; };
use body::{Direction, Exchange, MeteredBody}; use body::{Direction, Exchange, ExchangeSource, MeteredBody};
use bytes::Bytes; use bytes::Bytes;
use http::{ use http::{
header::{HeaderValue, HOST, USER_AGENT}, header::{HeaderValue, HOST, USER_AGENT},
@@ -21,6 +21,13 @@ use http::{
}; };
use http_body::Body as HttpBody; use http_body::Body as HttpBody;
use http_body_util::BodyExt; use http_body_util::BodyExt;
/// A route resolved against the immutable snapshot in the observation layer.
/// The internal error-fallback dispatcher must resolve its target independently.
#[derive(Clone)]
struct SelectedRouteContext {
frontend: Option<Arc<crate::config::CompiledFrontend>>,
route: Option<Arc<crate::config::CompiledRoute>>,
}
#[derive(Clone)] #[derive(Clone)]
struct ResponseRulesApplied; struct ResponseRulesApplied;
#[derive(Clone)] #[derive(Clone)]
@@ -42,7 +49,7 @@ struct OriginBackend(String);
use std::{ use std::{
sync::{ sync::{
atomic::{AtomicBool, AtomicU16, AtomicU64, Ordering}, atomic::{AtomicBool, AtomicU16, AtomicU64, AtomicU8, Ordering},
Arc, Mutex, Arc, Mutex,
}, },
time::{Duration, Instant}, time::{Duration, Instant},
+14 -6
View File
@@ -23,6 +23,12 @@ fn should_replace_error_page(response: &Response, minimal_welcome: bool) -> bool
async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next) -> Response { async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next) -> Response {
let snapshot = state.config.load_full(); let snapshot = state.config.load_full();
let (frontend_context, route_context) = response_context(&snapshot, &request); let (frontend_context, route_context) = response_context(&snapshot, &request);
// The same snapshot is inserted into the request. No second host/regex
// selection or expensive full CompiledRoute clone is necessary in forward().
request.extensions_mut().insert(SelectedRouteContext {
frontend: frontend_context.clone(),
route: route_context.clone(),
});
let local = request.extensions().get::<LocalAddr>().map(|v| v.0); let local = request.extensions().get::<LocalAddr>().map(|v| v.0);
let transport_tls = request let transport_tls = request
.extensions() .extensions()
@@ -90,7 +96,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
response.headers_mut(), response.headers_mut(),
&snapshot, &snapshot,
frontend_context.as_deref(), frontend_context.as_deref(),
route_context.as_ref(), route_context.as_deref(),
false, false,
); );
finish_headers( finish_headers(
@@ -106,7 +112,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
response.headers_mut(), response.headers_mut(),
&snapshot, &snapshot,
frontend_context.as_deref(), frontend_context.as_deref(),
route_context.as_ref(), route_context.as_deref(),
); );
return response; return response;
}; };
@@ -153,9 +159,9 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
vhost_metrics: frontend_context.as_ref().map(|f| f.metrics.clone()), vhost_metrics: frontend_context.as_ref().map(|f| f.metrics.clone()),
vhost: frontend_context.as_ref().map(|f| f.id.clone()), vhost: frontend_context.as_ref().map(|f| f.id.clone()),
route: Mutex::new(None), route: Mutex::new(None),
source: Mutex::new("unresolved"), source: AtomicU8::new(ExchangeSource::Unresolved as u8),
cache_effective_enabled: Mutex::new(None), cache_effective_enabled: AtomicU8::new(0),
transform_effective_enabled: Mutex::new(None), transform_effective_enabled: AtomicU8::new(0),
backend: Mutex::new(None), backend: Mutex::new(None),
id, id,
request_id: request_id.clone(), request_id: request_id.clone(),
@@ -210,7 +216,9 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
.extensions() .extensions()
.get::<FinalRouteContext>() .get::<FinalRouteContext>()
.map(|context| context.0.clone()); .map(|context| context.0.clone());
let effective_route_context = final_route_context.as_ref().or(route_context.as_ref()); let effective_route_context = final_route_context
.as_deref()
.or_else(|| route_context.as_deref());
let minimal_welcome = response let minimal_welcome = response
.extensions() .extensions()
.get::<crate::pages::MinimalWelcome>() .get::<crate::pages::MinimalWelcome>()
+3 -1
View File
@@ -98,12 +98,14 @@ impl App {
.as_ref() .as_ref()
.and_then(|path| path.parent()) .and_then(|path| path.parent())
.map(std::path::Path::to_path_buf); .map(std::path::Path::to_path_buf);
let cache = crate::cache::Cache::start( let cache = crate::cache::Cache::start_with_scope_revisions(
cache_revision(&snapshot.envelope.config), cache_revision(&snapshot.envelope.config),
cache_data_root, cache_data_root,
snapshot.envelope.config.cache.clone(), snapshot.envelope.config.cache.clone(),
shutdown.clone(), shutdown.clone(),
vhost_cache_revisions(&snapshot.envelope.config),
); );
cache.set_control_revision(snapshot.envelope.revision);
let active_listeners = snapshot.listeners.clone(); let active_listeners = snapshot.listeners.clone();
let active_streams = snapshot.envelope.config.streams.clone(); let active_streams = snapshot.envelope.config.streams.clone();
let stream_metrics = active_streams let stream_metrics = active_streams
+48 -1
View File
@@ -7,6 +7,17 @@ mod cache_revision_tests {
let base = config::demo_config("127.0.0.1:9000".parse().unwrap()); let base = config::demo_config("127.0.0.1:9000".parse().unwrap());
let identity = cache_revision(&base); let identity = cache_revision(&base);
let mut file_transport = base.clone();
file_transport.web_services.push(crate::web_server::Service {
id: "static-test".into(), name: "Static Test".into(),
document_root: "/var/www/static-test".into(),
..Default::default()
});
let mut zero_copy = file_transport.clone();
zero_copy.web_services[0].sendfile = crate::web_server::SendfileMode::Auto;
assert_eq!(cache_revision(&file_transport), cache_revision(&zero_copy));
assert!(matches!(cache_invalidation(&file_transport, &zero_copy), CacheInvalidation::None));
let mut logging = base.clone(); let mut logging = base.clone();
logging.logging.file.path = "/tmp/rustproxy-test-access.jsonl".into(); logging.logging.file.path = "/tmp/rustproxy-test-access.jsonl".into();
assert_eq!(cache_revision(&logging), identity); assert_eq!(cache_revision(&logging), identity);
@@ -107,8 +118,44 @@ mod cache_revision_tests {
CacheInvalidation::All(value) if value == cache_identity CacheInvalidation::All(value) if value == cache_identity
)); ));
let mut operational = base.clone();
operational.vhosts[0].load_balancing.keepalive.enabled = false;
operational.vhosts[0].load_balancing.keepalive.idle_timeout_seconds = 90;
operational.vhosts[0].load_balancing.keepalive.max_idle_per_host = 32;
operational.vhosts[0].load_balancing.keepalive.tcp_keepalive_seconds = 90;
operational.vhosts[0].load_balancing.keepalive.pool_recycle_requests = 1_000;
operational.vhosts[0].load_balancing.keepalive.pool_recycle_seconds = 1_800;
operational.vhosts[0].load_balancing.keepalive.http1_max_buffer_bytes = 65_536;
operational.vhosts[0].backends[0].weight += 1;
operational.vhosts[0].backends[0].max_concurrent_requests = 16;
operational.vhosts[0].backends[0].queue_timeout_ms = 100;
operational.vhosts[0].timing_headers = true;
operational.vhosts[0].acl_profile_id = Some("test-only-acl".into());
operational.vhosts[0].user_agent_blocking = Default::default();
operational.vhosts[0].proxy_timeouts.read_timeout_seconds = Some(180);
assert_eq!(cache_revision(&operational), identity);
assert_eq!(vhost_cache_revision(&operational.vhosts[0]), vhost_cache_revision(&base.vhosts[0]));
assert!(matches!(cache_invalidation(&base, &operational), CacheInvalidation::None));
assert!(matches!(cache_invalidation(&operational, &base), CacheInvalidation::None));
let mut vhost = base.clone(); let mut vhost = base.clone();
vhost.vhosts[0].backends[0].url = "http://127.0.0.1:9001".into(); vhost.vhosts[0].backends[0].url = "http://127.0.0.1:9001".into();
assert_ne!(cache_revision(&vhost), identity); assert_eq!(cache_revision(&vhost), identity);
let affected_id = base.vhosts[0].id.clone();
assert!(matches!(cache_invalidation(&base, &vhost), CacheInvalidation::Scopes(ids) if ids == vec![affected_id]));
// A content change must not invalidate another independent vhost.
let mut multiple = base.clone();
let mut another = multiple.vhosts[0].clone();
another.id = "other-scope".into();
another.frontend.hosts = vec!["other.example.test".into()];
multiple.vhosts.push(another);
let mut changed_one = multiple.clone();
changed_one.vhosts[0].backends[0].url = "http://127.0.0.1:9002".into();
assert!(matches!(cache_invalidation(&multiple, &changed_one), CacheInvalidation::Scopes(ids)
if ids == vec![base.vhosts[0].id.clone()]));
let mut changed_global = multiple.clone();
changed_global.transform.max_body_bytes += 1;
assert!(matches!(cache_invalidation(&multiple, &changed_global), CacheInvalidation::All(_)));
} }
} }
+86 -30
View File
@@ -78,25 +78,19 @@ impl IntoResponse for ApiError {
enum CacheInvalidation { enum CacheInvalidation {
None, None,
All(u64), All(u64),
Scopes(Vec<String>),
} }
/// Stable cache compatibility identity. Unlike the configuration revision, this /// Global compatibility identity for HOT/COLD cache entries. Vhost-specific
/// changes only when a setting can alter an origin request or cached response. /// representation changes use individual scope generations and COLD fingerprints.
/// Keeping it stable across logging/TLS/admin saves lets the persistent cold /// Connection-pool/LB knobs do not enter either identity.
/// tier survive a restart after unrelated control-plane edits.
fn cache_revision(config: &config::Config) -> u64 { fn cache_revision(config: &config::Config) -> u64 {
// Preset fields are UI intent metadata. Persist them, but do not rotate the
// cache compatibility identity (and purge HOT/COLD) when only the label
// changes while the effective cache limits stay identical.
let mut cache = config.cache.clone(); let mut cache = config.cache.clone();
cache.memory_preset = None; cache.memory_preset = None;
cache.cold_preset = None; cache.cold_preset = None;
// Request bypass switches and response-only diagnostic headers do not alter // These are request admission or response diagnostics, never stored objects.
// already cached representations. Keep cache objects across these policy
// edits; the new behavior applies immediately on the next request.
cache.bypass_query = false; cache.bypass_query = false;
cache.bypass_prefixes.clear(); cache.bypass_prefixes.clear();
// Remote maintenance permissions cannot change stored object representations.
cache.http_invalidation_allowed_cidrs.clear(); cache.http_invalidation_allowed_cidrs.clear();
cache.expose_status = false; cache.expose_status = false;
cache.expose_hits = false; cache.expose_hits = false;
@@ -104,43 +98,105 @@ fn cache_revision(config: &config::Config) -> u64 {
cache.expose_resident_age = false; cache.expose_resident_age = false;
cache.expose_only_hits = false; cache.expose_only_hits = false;
cache.via_header = None; cache.via_header = None;
// Resource-only transform controls decide how much optimization work may
// happen concurrently, but do not change the transformed representation.
// Keep those out of the cache identity while including representation and
// eligibility controls such as enabled kinds, extensions and max body.
let mut transform = config.transform.clone(); let mut transform = config.transform.clone();
transform.preset = None; transform.preset = None;
transform.memory_budget_bytes = 0; transform.memory_budget_bytes = 0;
transform.max_inflight = 0; transform.max_inflight = 0;
// A per-vhost diagnostics override is also presentation-only and must not // Web services and FPM profiles can change the bytes served by a location.
// invalidate cached objects when switched from inherited to HIT-only. // Until their dependency graph is split into scopes, invalidate globally.
let mut vhosts = config.vhosts.clone(); let mut web_services = config.web_services.clone();
for vhost in &mut vhosts { for service in &mut web_services {
vhost.cache.expose_only_hits = None; // Kernel write strategy never changes the HTTP representation.
// Frontend ALPN/QUIC switches decide how a client reaches a vhost, service.sendfile = crate::web_server::SendfileMode::Off;
// not which representation the origin returns. Hot and cold entries
// must survive HTTP/2 or HTTP/3 opt-in/out saves.
vhost.http2_enabled = true;
vhost.http3_enabled = true;
} }
let encoded = serde_json::to_vec(&( cache_digest(&(
&cache, &cache,
&transform, &transform,
&config.headers, &config.headers,
&config.forwarding, &config.forwarding,
&vhosts, &web_services,
&config.php_fpm_profiles,
)) ))
.expect("serializable cache compatibility view"); }
fn cache_digest<T: serde::Serialize>(value: &T) -> u64 {
let encoded = serde_json::to_vec(value).expect("serializable cache compatibility view");
let digest = Sha256::digest(encoded); let digest = Sha256::digest(encoded);
u64::from_be_bytes(digest[..8].try_into().expect("sha256 prefix")) u64::from_be_bytes(digest[..8].try_into().expect("sha256 prefix"))
} }
/// Only effective origin/content policies contribute to this scope identity.
/// A vhost keepalive or load-balancer control save must retain both cache tiers.
fn vhost_cache_revision(vhost: &crate::vhost::Vhost) -> u64 {
let mut view = vhost.clone();
view.websockets = false;
view.websocket_legacy_support = false;
view.websocket_rfc8441 = false;
view.websocket_rfc9220 = false;
view.http2_enabled = true;
view.http3_enabled = true;
view.enabled = true;
view.force_ssl = false;
view.www_redirect = false;
view.www_redirect_target = None;
view.timing_headers = false;
view.backend_header = false;
view.backend_timing_header = false;
view.certificate_id = None;
view.tls_policy = None;
view.server_header = None;
view.diagnostic_headers = None;
view.rate_limit = Default::default();
view.method_policy = Default::default();
view.basic_auth = Default::default();
view.tinyauth = Default::default();
view.acl = None;
view.acl_profile_id = None;
view.user_agent_blocking = Default::default();
view.proxy_timeouts = Default::default();
view.frontend.listener_ids.clear();
view.cache.expose_only_hits = None;
// The complete LB policy (keepalive, recycling, weights, retries, health,
// affinity and queues) governs connection selection, not cache identity.
// Sticky authentication checks and bypass still run before the cache lookup.
view.load_balancing = Default::default();
for member in &mut view.backends {
member.weight = 1;
member.backup = false;
member.drain = false;
member.health_check_enabled = true;
member.health_check = None;
member.max_concurrent_requests = 0;
member.queue_timeout_ms = 0;
}
cache_digest(&view)
}
fn vhost_cache_revisions(config: &config::Config) -> HashMap<String, u64> {
config.vhosts.iter().map(|vhost| (vhost.id.clone(), vhost_cache_revision(vhost))).collect()
}
fn cache_invalidation(previous: &config::Config, next: &config::Config) -> CacheInvalidation { fn cache_invalidation(previous: &config::Config, next: &config::Config) -> CacheInvalidation {
let before = cache_revision(previous); let before = cache_revision(previous);
let after = cache_revision(next); let after = cache_revision(next);
if before == after { if before != after {
return CacheInvalidation::All(after);
}
let old = vhost_cache_revisions(previous);
let new = vhost_cache_revisions(next);
let mut changed: Vec<_> = old
.iter()
.filter(|(id, revision)| new.get(*id) != Some(*revision))
.map(|(id, _)| id.clone())
.collect();
// New scopes do not have previous cache objects, but include the set for
// completeness if an ID is ever reused within a single config transaction.
changed.extend(new.keys().filter(|id| !old.contains_key(*id)).cloned());
changed.sort();
changed.dedup();
if changed.is_empty() {
CacheInvalidation::None CacheInvalidation::None
} else { } else {
CacheInvalidation::All(after) CacheInvalidation::Scopes(changed)
} }
} }
+12
View File
@@ -147,12 +147,24 @@ impl App {
// intentionally decoupled from the configuration revision, so unrelated // intentionally decoupled from the configuration revision, so unrelated
// control-plane saves do not invalidate hot/cold objects. // control-plane saves do not invalidate hot/cold objects.
let envelope = prepared.envelope.clone(); let envelope = prepared.envelope.clone();
// After commit, reject late cache tickets from the old routing
// snapshot before any scope purge. Existing unaffected objects stay.
this.cache.set_control_revision(envelope.revision);
match invalidation { match invalidation {
CacheInvalidation::All(cache_revision) => { CacheInvalidation::All(cache_revision) => {
this.cache.purge(Some(cache_revision)); this.cache.purge(Some(cache_revision));
} }
CacheInvalidation::Scopes(scopes) => {
for scope in scopes {
this.cache.purge_scope(&scope);
}
}
CacheInvalidation::None => {} CacheInvalidation::None => {}
} }
// A COLD entry is usable after restart only if its scope still has
// the same origin/content policy. Publish the fingerprints after
// the cache generation barriers but before the routing snapshot.
this.cache.replace_scope_revisions(vhost_cache_revisions(&envelope.config));
this.cache.configure(envelope.config.cache.clone()); this.cache.configure(envelope.config.cache.clone());
this.rate_limiter.reset(); this.rate_limiter.reset();
this.logs.configure(envelope.config.logging.clone()); this.logs.configure(envelope.config.logging.clone());
+7
View File
@@ -3,6 +3,13 @@
//! a validated HTTP/1.1 `Upgrade: h2c` bootstrap. //! a validated HTTP/1.1 `Upgrade: h2c` bootstrap.
include!("server/listeners.rs"); include!("server/listeners.rs");
#[cfg(target_os = "linux")]
pub(crate) mod sendfile;
#[cfg(target_os = "linux")]
type TransportSendfileSlots = Option<sendfile::SendfileSlots>;
#[cfg(not(target_os = "linux"))]
type TransportSendfileSlots = ();
include!("server/http.rs"); include!("server/http.rs");
include!("server/tls_runtime.rs"); include!("server/tls_runtime.rs");
include!("server/http1_connection.rs");
include!("server/serve.rs"); include!("server/serve.rs");
+207
View File
@@ -0,0 +1,207 @@
// HTTP/1 dispatcher and upgrade flow; split from TLS listener runtime.
#[allow(clippy::too_many_arguments)]
async fn connection<I>(
stream: I,
router: Router,
peer: SocketAddr,
local: SocketAddr,
transport: Transport,
tls_sni: Option<String>,
h2: bool,
extended_connect: bool,
h2c_upgrade: bool,
frontend_state: Option<Arc<App>>,
lease: Arc<ConnectionLease>,
stop: CancellationToken,
grace: Duration,
http2_settings: Http2Connections,
sendfile_slots: TransportSendfileSlots,
) where
I: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
if h2 {
http2_connection(
stream,
router,
peer,
local,
transport,
tls_sni.clone(),
extended_connect,
frontend_state.clone(),
lease,
stop,
grace,
http2_settings,
)
.await;
return;
}
let request_stop = stop.clone();
let service_frontend_state = frontend_state.clone();
let service = service_fn(move |mut request: Request<Incoming>| {
let router = router.clone();
let request_stop = request_stop.clone();
let request_lease = lease.clone();
let upgrade_state = service_frontend_state.clone();
#[cfg(target_os = "linux")]
let sendfile_slots = sendfile_slots.clone();
async move {
request.extensions_mut().insert(PeerAddr(peer));
request.extensions_mut().insert(LocalAddr(local));
request.extensions_mut().insert(transport);
request.extensions_mut().insert(request_lease.clone());
#[cfg(target_os = "linux")]
if let Some(slots) = sendfile_slots {
request.extensions_mut().insert(slots);
}
request
.extensions_mut()
.insert(ServerShutdown(request_stop.clone()));
if h2c_attempt(&request) {
if !h2c_upgrade {
let (authority, host) = request_log_identity(&request);
log_protocol_error_with_identity(
upgrade_state.as_ref(),
"h2c_upgrade_rejected",
"h2c upgrade is disabled on this listener",
peer,
local,
"tcp",
"1.1",
400,
authority,
host,
None,
);
return Ok::<_, Infallible>(
Response::builder()
.status(http::StatusCode::BAD_REQUEST)
.header(http::header::CACHE_CONTROL, "no-store")
.body(Body::from("h2c upgrade is disabled on this listener"))
.expect("static h2c disabled response"),
);
}
let injected_headers = match h2_headers_frame(&request) {
Ok(frame) => frame,
Err(message) => {
let (authority, host) = request_log_identity(&request);
log_protocol_error_with_identity(
upgrade_state.as_ref(),
"h2c_upgrade_invalid",
message,
peer,
local,
"tcp",
"1.1",
400,
authority,
host,
None,
);
return Ok::<_, Infallible>(
Response::builder()
.status(http::StatusCode::BAD_REQUEST)
.header(http::header::CACHE_CONTROL, "no-store")
.body(Body::from(message))
.expect("static h2c validation response"),
);
}
};
let on_upgrade = hyper::upgrade::on(&mut request);
let upgrade_router = router.clone();
let upgrade_stop = request_stop.clone();
let upgrade_lease = request_lease.clone();
let upgrade_state_for_task = upgrade_state.clone();
let task = async move {
match on_upgrade.await {
Ok(upgraded) => {
if let Some(state) = &upgrade_state_for_task {
state
.metrics
.http2_connections
.fetch_add(1, Ordering::Relaxed);
}
let extended_connect =
upgrade_state_for_task.as_ref().is_some_and(|state| {
state.config.load().websocket_rfc8441_on(local, false)
});
let io = H2cUpgradeIo::new(TokioIo::new(upgraded), injected_headers);
http2_connection(
io,
upgrade_router,
peer,
local,
Transport { tls: false },
None,
extended_connect,
upgrade_state_for_task.clone(),
upgrade_lease,
upgrade_stop,
grace,
http2_settings,
)
.await;
}
Err(error) => {
log_protocol_error(
upgrade_state_for_task.as_ref(),
"h2c_transport_error",
&error,
peer,
local,
"tcp",
"2",
0,
None,
);
tracing::debug!(%error, "h2c upgrade transport failed");
}
}
};
if let Some(state) = upgrade_state {
state.tunnels.spawn(task);
} else {
tokio::spawn(task);
}
return Ok::<_, Infallible>(
Response::builder()
.status(http::StatusCode::SWITCHING_PROTOCOLS)
.header(http::header::CONNECTION, "Upgrade")
.header(http::header::UPGRADE, "h2c")
.body(Body::empty())
.expect("static h2c switching response"),
);
}
router.oneshot(request.map(Body::new)).await
}
});
let mut builder = http1::Builder::new();
builder
.timer(TokioTimer::new())
.keep_alive(true)
.max_headers(64)
.max_buf_size(65_536)
.header_read_timeout(Duration::from_secs(10));
let conn = builder
.serve_connection(TokioIo::new(stream), service)
.with_upgrades();
tokio::pin!(conn);
tokio::select! {
result = &mut conn => {
if let Err(error) = result {
log_protocol_error(
frontend_state.as_ref(), "http1_connection_error", &error, peer, local,
if transport.tls { "tls" } else { "tcp" }, "1.1", 400, tls_sni.clone(),
);
tracing::debug!(%error, "HTTP/1 connection closed");
}
},
_ = stop.cancelled() => {
conn.as_mut().graceful_shutdown(); let _ = tokio::time::timeout(grace, &mut conn).await;
},
}
}
+301
View File
@@ -0,0 +1,301 @@
//! Linux HTTP/1 kernel sendfile bridge.
//!
//! Hyper's Body uses Bytes, not file descriptors. This opt-in bridge conveys a
//! uniquely tagged, exact-length placeholder through the ordinary metering
//! pipeline; a TCP-only AsyncWrite adapter replaces those bytes with the
//! original, already-validated file via Linux sendfile(2). An unmatched marker
//! or short file is a hard I/O error, never silently returned as user content.
//! Never register a placeholder for cached, compressed, transformed, TLS, H2,
//! H3, ranged or non-GET responses.
use bytes::Bytes;
use http_body::{Body as HttpBody, Frame, SizeHint};
use std::{
fs::File,
io,
os::fd::{AsRawFd, RawFd},
pin::Pin,
sync::{Arc, Mutex},
sync::atomic::Ordering,
task::{Context, Poll},
};
use tokio::{io::{AsyncRead, AsyncWrite, Interest, ReadBuf}, net::TcpStream};
const TAG_LEN: usize = 32;
const FRAME_LEN: usize = 64 * 1024;
const MAX_PENDING: usize = 8;
const MIN_SIZE: u64 = 64 * 1024;
#[derive(Clone, Debug)]
pub(crate) struct SendfileCandidate {
pub file: Arc<File>,
pub length: u64,
}
struct Slot {
tag: [u8; TAG_LEN],
file: Arc<File>,
length: u64,
written: u64,
}
#[derive(Default)]
struct Registry {
slots: Vec<Slot>,
active: Option<[u8; TAG_LEN]>,
}
#[derive(Clone, Default)]
pub(crate) struct SendfileSlots(Arc<Mutex<Registry>>);
pub(crate) struct SendfileBody {
bytes: Bytes,
remaining: u64,
web_metrics: Arc<crate::web_server::metrics::PhpMetrics>,
}
// Web Server counters count emitted body frames before any downstream encoder.
// Atomic reference is supplied by the Web Server that originally opened the file.
impl SendfileSlots {
pub fn create_body(
&self,
candidate: SendfileCandidate,
web_metrics: Arc<crate::web_server::metrics::PhpMetrics>,
) -> Option<SendfileBody> {
if !(MIN_SIZE..=i64::MAX as u64).contains(&candidate.length) {
return None;
}
let mut registry = self.0.lock().unwrap_or_else(|e| e.into_inner());
if registry.slots.len() >= MAX_PENDING { return None; }
let mut tag = [0u8; TAG_LEN];
tag[0] = 0; // A valid HTTP/1 response header never contains NUL.
tag[1..].copy_from_slice(&rand::random::<[u8; TAG_LEN - 1]>());
if registry.slots.iter().any(|slot| slot.tag == tag) { return None; }
let mut data = vec![0u8; FRAME_LEN];
for chunk in data.chunks_exact_mut(TAG_LEN) { chunk.copy_from_slice(&tag); }
registry.slots.push(Slot {
tag, file: candidate.file, length: candidate.length, written: 0,
});
Some(SendfileBody {
bytes: Bytes::from(data), remaining: candidate.length, web_metrics,
})
}
fn plan(&self, buf: &[u8]) -> WritePlan {
let registry = self.0.lock().unwrap_or_else(|e| e.into_inner());
// Critical for mixed reverse-proxy traffic: almost all socket writes
// have no pending static file; do not inspect their payload at all.
if registry.slots.is_empty() { return WritePlan::Plain(buf.len()); }
if let Some(tag) = registry.active {
if let Some(slot) = registry.slots.iter().find(|s| s.tag == tag) {
return plan_file(slot, buf);
}
return WritePlan::Invalid;
}
// A marker is an opaque 32-byte random tag, searched only on a
// connection that has a registered response. Headers preceding a
// marker are written intact; each subsequent body byte is validated.
for start in 0..buf.len() {
if buf[start] != 0 { continue; }
for slot in &registry.slots {
let available = buf.len() - start;
let compare = available.min(TAG_LEN);
if buf[start..start + compare] == slot.tag[..compare] {
if start > 0 { return WritePlan::Plain(start); }
return plan_file(slot, buf);
}
}
}
WritePlan::Plain(buf.len())
}
fn commit(&self, tag: [u8; TAG_LEN], count: usize) {
let mut registry = self.0.lock().unwrap_or_else(|e| e.into_inner());
registry.active = Some(tag);
if let Some(slot) = registry.slots.iter_mut().find(|s| s.tag == tag) {
slot.written += count as u64;
if slot.written == slot.length {
registry.slots.retain(|s| s.tag != tag);
registry.active = None;
}
}
}
}
enum WritePlan {
Plain(usize),
File { tag: [u8; TAG_LEN], fd: RawFd, file: Arc<File>, offset: u64, count: usize },
Invalid,
}
fn plan_file(slot: &Slot, buf: &[u8]) -> WritePlan {
let expected = (slot.length - slot.written).min(buf.len() as u64) as usize;
let matched = buf[..expected].iter().enumerate()
.take_while(|(n, byte)| **byte == slot.tag[(slot.written as usize + n) % TAG_LEN])
.count();
if matched == 0 { return WritePlan::Invalid; }
WritePlan::File {
tag: slot.tag, fd: slot.file.as_raw_fd(), file: slot.file.clone(),
offset: slot.written, count: matched,
}
}
impl HttpBody for SendfileBody {
type Data = Bytes;
type Error = io::Error;
fn poll_frame(mut self: Pin<&mut Self>, _cx: &mut Context<'_>)
-> Poll<Option<Result<Frame<Bytes>, io::Error>>>
{
if self.remaining == 0 { return Poll::Ready(None); }
let len = self.remaining.min(FRAME_LEN as u64) as usize;
self.remaining -= len as u64;
self.web_metrics.response_bytes.fetch_add(len as u64, Ordering::Relaxed);
Poll::Ready(Some(Ok(Frame::data(self.bytes.slice(0..len)))))
}
fn size_hint(&self) -> SizeHint { SizeHint::with_exact(self.remaining) }
fn is_end_stream(&self) -> bool { self.remaining == 0 }
}
pub(crate) struct SendfileIo {
stream: TcpStream,
slots: SendfileSlots,
}
impl SendfileIo {
pub fn new(stream: TcpStream) -> (Self, SendfileSlots) {
let slots = SendfileSlots::default();
(Self { stream, slots: slots.clone() }, slots)
}
}
impl AsyncRead for SendfileIo {
fn poll_read(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.get_mut().stream).poll_read(cx, buf)
}
}
impl AsyncWrite for SendfileIo {
fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll<io::Result<usize>> {
if buf.is_empty() { return Poll::Ready(Ok(0)); }
match self.slots.plan(buf) {
WritePlan::Plain(count) => Pin::new(&mut self.stream).poll_write(cx, &buf[..count]),
WritePlan::Invalid => Poll::Ready(Err(io::Error::new(io::ErrorKind::InvalidData,
"sendfile placeholder did not match its registered descriptor"))),
WritePlan::File { tag, fd, file, offset, count } => {
let _keep_open = file;
let socket: RawFd = self.stream.as_raw_fd();
let mut at: libc::off_t = offset as libc::off_t;
match self.stream.poll_write_ready(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
Poll::Ready(Ok(())) => {
let result = self.stream.try_io(Interest::WRITABLE, || {
let n = unsafe { libc::sendfile(socket, fd, &mut at, count) };
if n < 0 { Err(io::Error::last_os_error()) }
else if n == 0 { Err(io::Error::new(io::ErrorKind::UnexpectedEof,
"static source truncated during sendfile")) }
else { Ok(n as usize) }
});
match result {
Ok(n) => { self.slots.commit(tag, n); Poll::Ready(Ok(n)) },
Err(e) if e.kind() == io::ErrorKind::WouldBlock => Poll::Pending,
Err(e) => Poll::Ready(Err(e)),
}
}
}
}
}
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.stream).poll_flush(cx)
}
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.stream).poll_shutdown(cx)
}
fn is_write_vectored(&self) -> bool { self.stream.is_write_vectored() }
fn poll_write_vectored(mut self: Pin<&mut Self>, cx: &mut Context<'_>, bufs: &[io::IoSlice<'_>]) -> Poll<io::Result<usize>> {
// Preserve zero-extra-syscall vectored I/O whenever no sendfile body
// has been prepared on this connection.
if self.slots.0.lock().unwrap_or_else(|e| e.into_inner()).slots.is_empty() {
return Pin::new(&mut self.stream).poll_write_vectored(cx, bufs);
}
match bufs.iter().find(|part| !part.is_empty()) {
Some(part) => self.poll_write(cx, part),
None => Poll::Ready(Ok(0)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use http_body::Body;
use tempfile::tempfile;
use std::io::Write;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[test]
fn placeholder_has_exact_frame_lengths_and_recovers_fd() {
let mut file = tempfile().unwrap();
file.write_all(&vec![42u8; 100_000]).unwrap();
let slots = SendfileSlots::default();
let counter = Arc::new(crate::web_server::metrics::PhpMetrics::default());
let mut body = slots.create_body(SendfileCandidate { file: Arc::new(file), length: 100_000 }, counter.clone()).unwrap();
let first = match Pin::new(&mut body).poll_frame(&mut Context::from_waker(std::task::Waker::noop())) {
Poll::Ready(Some(Ok(frame))) => frame.into_data().unwrap(), _ => panic!("frame missing"),
};
assert_eq!(first.len(), FRAME_LEN);
match slots.plan(&first) { WritePlan::File { count, .. } => assert_eq!(count, FRAME_LEN), _ => panic!("missing kernel file plan") }
let tag = slots.0.lock().unwrap().slots[0].tag;
slots.commit(tag, FRAME_LEN);
let second = match Pin::new(&mut body).poll_frame(&mut Context::from_waker(std::task::Waker::noop())) {
Poll::Ready(Some(Ok(frame))) => frame.into_data().unwrap(), _ => panic!("frame missing"),
};
assert_eq!(second.len(), 34_464);
match slots.plan(&second) { WritePlan::File { count, offset, .. } => { assert_eq!(offset, 65_536); assert_eq!(count, 34_464); }, _ => panic!("second plan missing") }
slots.commit(tag, second.len());
assert!(slots.0.lock().unwrap().slots.is_empty());
assert!(body.is_end_stream());
assert_eq!(counter.response_bytes.load(Ordering::Relaxed), 100_000);
}
#[tokio::test]
async fn native_sendfile_transfers_exact_file_and_keeps_socket_reusable() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (tcp, _) = listener.accept().await.unwrap();
let (mut socket, slots) = SendfileIo::new(tcp);
for round in 0..2 {
let expected = if round == 0 { vec![0, 1, 2, 3].repeat(25_000) }
else { vec![9u8; 100_003] };
let mut file = tempfile().unwrap();
file.write_all(&expected).unwrap();
let web = Arc::new(crate::web_server::metrics::PhpMetrics::default());
let body = slots.create_body(SendfileCandidate {
file: Arc::new(file), length: expected.len() as u64,
}, web.clone()).unwrap();
let first = format!("HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n", expected.len());
// Combine header and first marker into one socket write: the
// bridge must send the header before enabling kernel transfer.
let mut body = body;
let first_frame = http_body_util::BodyExt::frame(&mut body).await.unwrap().unwrap().into_data().unwrap();
let mut joined = first.into_bytes();
joined.extend_from_slice(&first_frame);
socket.write_all(&joined).await.unwrap();
while let Some(frame) = http_body_util::BodyExt::frame(&mut body).await {
socket.write_all(&frame.unwrap().into_data().unwrap()).await.unwrap();
}
assert_eq!(web.response_bytes.load(Ordering::Relaxed), expected.len() as u64);
}
socket.shutdown().await.unwrap();
});
let mut client = tokio::net::TcpStream::connect(address).await.unwrap();
let mut actual = Vec::new();
tokio::time::timeout(std::time::Duration::from_secs(10), client.read_to_end(&mut actual)).await.unwrap().unwrap();
server.await.unwrap();
let mut wanted = Vec::new();
for expected in [vec![0, 1, 2, 3].repeat(25_000), vec![9u8; 100_003]] {
wanted.extend_from_slice(format!("HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n", expected.len()).as_bytes());
wanted.extend_from_slice(&expected);
}
assert_eq!(actual, wanted, "kernel sendfile must never leak placeholder markers");
}
}
+20 -199
View File
@@ -134,6 +134,7 @@ async fn serve_inner(
stop, stop,
grace, grace,
http2_settings, http2_settings,
Default::default(),
) )
.await; .await;
}, },
@@ -178,6 +179,7 @@ async fn serve_inner(
stop, stop,
grace, grace,
Http2Connections::default(), Http2Connections::default(),
Default::default(),
) )
.await; .await;
return; return;
@@ -208,6 +210,7 @@ async fn serve_inner(
stop, stop,
grace, grace,
Http2Connections::default(), Http2Connections::default(),
Default::default(),
) )
.await; .await;
} }
@@ -242,6 +245,22 @@ async fn serve_inner(
&& frontend_state && frontend_state
.as_ref() .as_ref()
.is_some_and(|state| state.config.load().websocket_rfc8441_on(local, false)); .is_some_and(|state| state.config.load().websocket_rfc8441_on(local, false));
// The existing Hyper transport remains untouched when the
// option is disabled. Connections opened after a hot config
// change acquire a new bridge automatically.
#[cfg(target_os = "linux")]
if !prior_h2 && frontend_state.as_ref().is_some_and(|state| {
state.config.load().web_php_services.values()
.any(|service| service.sendfile == crate::web_server::SendfileMode::Auto)
}) {
let (io, slots) = sendfile::SendfileIo::new(stream);
connection(
io, router, peer, local, Transport { tls: false }, None,
false, false, h2c, frontend_state, lease, stop, grace,
http2_settings, Some(slots),
).await;
return;
}
connection( connection(
stream, stream,
router, router,
@@ -257,6 +276,7 @@ async fn serve_inner(
stop, stop,
grace, grace,
http2_settings, http2_settings,
Default::default(),
) )
.await; .await;
} }
@@ -273,205 +293,6 @@ async fn serve_inner(
while tasks.join_next().await.is_some() {} while tasks.join_next().await.is_some() {}
Ok(()) Ok(())
} }
#[allow(clippy::too_many_arguments)]
async fn connection<I>(
stream: I,
router: Router,
peer: SocketAddr,
local: SocketAddr,
transport: Transport,
tls_sni: Option<String>,
h2: bool,
extended_connect: bool,
h2c_upgrade: bool,
frontend_state: Option<Arc<App>>,
lease: Arc<ConnectionLease>,
stop: CancellationToken,
grace: Duration,
http2_settings: Http2Connections,
) where
I: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
if h2 {
http2_connection(
stream,
router,
peer,
local,
transport,
tls_sni.clone(),
extended_connect,
frontend_state.clone(),
lease,
stop,
grace,
http2_settings,
)
.await;
return;
}
let request_stop = stop.clone();
let service_frontend_state = frontend_state.clone();
let service = service_fn(move |mut request: Request<Incoming>| {
let router = router.clone();
let request_stop = request_stop.clone();
let request_lease = lease.clone();
let upgrade_state = service_frontend_state.clone();
async move {
request.extensions_mut().insert(PeerAddr(peer));
request.extensions_mut().insert(LocalAddr(local));
request.extensions_mut().insert(transport);
request.extensions_mut().insert(request_lease.clone());
request
.extensions_mut()
.insert(ServerShutdown(request_stop.clone()));
if h2c_attempt(&request) {
if !h2c_upgrade {
let (authority, host) = request_log_identity(&request);
log_protocol_error_with_identity(
upgrade_state.as_ref(),
"h2c_upgrade_rejected",
"h2c upgrade is disabled on this listener",
peer,
local,
"tcp",
"1.1",
400,
authority,
host,
None,
);
return Ok::<_, Infallible>(
Response::builder()
.status(http::StatusCode::BAD_REQUEST)
.header(http::header::CACHE_CONTROL, "no-store")
.body(Body::from("h2c upgrade is disabled on this listener"))
.expect("static h2c disabled response"),
);
}
let injected_headers = match h2_headers_frame(&request) {
Ok(frame) => frame,
Err(message) => {
let (authority, host) = request_log_identity(&request);
log_protocol_error_with_identity(
upgrade_state.as_ref(),
"h2c_upgrade_invalid",
message,
peer,
local,
"tcp",
"1.1",
400,
authority,
host,
None,
);
return Ok::<_, Infallible>(
Response::builder()
.status(http::StatusCode::BAD_REQUEST)
.header(http::header::CACHE_CONTROL, "no-store")
.body(Body::from(message))
.expect("static h2c validation response"),
);
}
};
let on_upgrade = hyper::upgrade::on(&mut request);
let upgrade_router = router.clone();
let upgrade_stop = request_stop.clone();
let upgrade_lease = request_lease.clone();
let upgrade_state_for_task = upgrade_state.clone();
let task = async move {
match on_upgrade.await {
Ok(upgraded) => {
if let Some(state) = &upgrade_state_for_task {
state
.metrics
.http2_connections
.fetch_add(1, Ordering::Relaxed);
}
let extended_connect =
upgrade_state_for_task.as_ref().is_some_and(|state| {
state.config.load().websocket_rfc8441_on(local, false)
});
let io = H2cUpgradeIo::new(TokioIo::new(upgraded), injected_headers);
http2_connection(
io,
upgrade_router,
peer,
local,
Transport { tls: false },
None,
extended_connect,
upgrade_state_for_task.clone(),
upgrade_lease,
upgrade_stop,
grace,
http2_settings,
)
.await;
}
Err(error) => {
log_protocol_error(
upgrade_state_for_task.as_ref(),
"h2c_transport_error",
&error,
peer,
local,
"tcp",
"2",
0,
None,
);
tracing::debug!(%error, "h2c upgrade transport failed");
}
}
};
if let Some(state) = upgrade_state {
state.tunnels.spawn(task);
} else {
tokio::spawn(task);
}
return Ok::<_, Infallible>(
Response::builder()
.status(http::StatusCode::SWITCHING_PROTOCOLS)
.header(http::header::CONNECTION, "Upgrade")
.header(http::header::UPGRADE, "h2c")
.body(Body::empty())
.expect("static h2c switching response"),
);
}
router.oneshot(request.map(Body::new)).await
}
});
let mut builder = http1::Builder::new();
builder
.timer(TokioTimer::new())
.keep_alive(true)
.max_headers(64)
.max_buf_size(65_536)
.header_read_timeout(Duration::from_secs(10));
let conn = builder
.serve_connection(TokioIo::new(stream), service)
.with_upgrades();
tokio::pin!(conn);
tokio::select! {
result = &mut conn => {
if let Err(error) = result {
log_protocol_error(
frontend_state.as_ref(), "http1_connection_error", &error, peer, local,
if transport.tls { "tls" } else { "tcp" }, "1.1", 400, tls_sni.clone(),
);
tracing::debug!(%error, "HTTP/1 connection closed");
}
},
_ = stop.cancelled() => {
conn.as_mut().graceful_shutdown(); let _ = tokio::time::timeout(grace, &mut conn).await;
},
}
}
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
enum H2ShutdownCause { enum H2ShutdownCause {
Idle, Idle,
+1
View File
@@ -426,6 +426,7 @@ impl Service {
web_metrics: Arc::new(crate::web_server::metrics::PhpMetrics::default()), web_metrics: Arc::new(crate::web_server::metrics::PhpMetrics::default()),
id: self.id.clone(), id: self.id.clone(),
name: self.name.clone(), name: self.name.clone(),
sendfile: self.sendfile,
root, root,
index: self.index.clone(), index: self.index.clone(),
directory_listing: self.directory_listing, directory_listing: self.directory_listing,
+13
View File
@@ -216,6 +216,16 @@ pub enum SymlinkPolicy {
InsideRoot, InsideRoot,
} }
/// Opt-in kernel zero-copy for suitable Linux plain HTTP/1.1 static GETs.
/// Other requests continue through the original streaming implementation.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum SendfileMode {
#[default]
Off,
Auto,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default, deny_unknown_fields)] #[serde(default, deny_unknown_fields)]
pub struct RewriteRule { pub struct RewriteRule {
@@ -338,6 +348,8 @@ pub struct Service {
pub document_root: PathBuf, pub document_root: PathBuf,
pub index: Vec<String>, pub index: Vec<String>,
pub directory_listing: bool, pub directory_listing: bool,
/// Transport-only preference: it must not rotate cache identity.
pub sendfile: SendfileMode,
/// Optional per-extension MIME overrides. Keys are extensions without a leading dot. /// Optional per-extension MIME overrides. Keys are extensions without a leading dot.
pub mime_types: BTreeMap<String, String>, pub mime_types: BTreeMap<String, String>,
/// Fallback MIME type used when neither an override nor a built-in mapping exists. /// Fallback MIME type used when neither an override nor a built-in mapping exists.
@@ -382,6 +394,7 @@ impl Default for Service {
document_root: PathBuf::new(), document_root: PathBuf::new(),
index: default_index(), index: default_index(),
directory_listing: false, directory_listing: false,
sendfile: SendfileMode::Off,
mime_types: BTreeMap::new(), mime_types: BTreeMap::new(),
default_mime_type: "application/octet-stream".into(), default_mime_type: "application/octet-stream".into(),
cache_control: None, cache_control: None,
+1
View File
@@ -47,6 +47,7 @@ pub struct CompiledService {
pub id: String, pub id: String,
pub(crate) web_metrics: Arc<crate::web_server::metrics::PhpMetrics>, pub(crate) web_metrics: Arc<crate::web_server::metrics::PhpMetrics>,
pub name: String, pub name: String,
pub sendfile: SendfileMode,
pub(super) root: PathBuf, pub(super) root: PathBuf,
pub(super) index: Vec<String>, pub(super) index: Vec<String>,
pub(super) directory_listing: bool, pub(super) directory_listing: bool,
+1 -1
View File
@@ -17,7 +17,7 @@ mod serve;
pub use config::{ pub use config::{
CacheRule, CompiledService, Hardening, Php, PhpFpmProfile, Precompressed, RewriteRule, Service, CacheRule, CompiledService, Hardening, Php, PhpFpmProfile, Precompressed, RewriteRule, Service,
SymlinkPolicy, SymlinkPolicy, SendfileMode,
}; };
pub use htaccess::Config as HtaccessConfig; pub use htaccess::Config as HtaccessConfig;
pub use request::RequestContext; pub use request::RequestContext;
+16
View File
@@ -162,6 +162,18 @@ async fn file_response_inner(
return response; return response;
} }
// Clone the already confined, validated file descriptor. Never reopen
// the user-controlled path for kernel sendfile (TOCTOU/symlink escape).
// When no HTTP/1 TCP bridge accepts it, the regular reader remains used.
#[cfg(target_os = "linux")]
let sendfile_candidate = if service.sendfile == super::SendfileMode::Auto
&& !multipart && single_range.is_none() && content_encoding.is_none()
&& body_length >= 64 * 1024
{
opened.file.try_clone().ok().map(|file| crate::server::sendfile::SendfileCandidate {
file: std::sync::Arc::new(file), length: body_length,
})
} else { None };
let mut file = tokio::fs::File::from_std(opened.file); let mut file = tokio::fs::File::from_std(opened.file);
if let Some(boundary) = boundary { if let Some(boundary) = boundary {
let ranges = ranges.expect("multipart ranges"); let ranges = ranges.expect("multipart ranges");
@@ -211,6 +223,10 @@ async fn file_response_inner(
} }
let stream = ReaderStream::with_capacity(file.take(body_length), stream_capacity); let stream = ReaderStream::with_capacity(file.take(body_length), stream_capacity);
*response.body_mut() = Body::new(file_body::FileBody::new(stream, body_length)); *response.body_mut() = Body::new(file_body::FileBody::new(stream, body_length));
#[cfg(target_os = "linux")]
if let Some(candidate) = sendfile_candidate {
response.extensions_mut().insert(candidate);
}
response response
} }
+51
View File
@@ -2225,3 +2225,54 @@ async fn cancelled_static_and_multipart_bodies_release_files_and_count_only_emit
assert_eq!(service.web_metrics.view()["response_bytes"], accounted); assert_eq!(service.web_metrics.view()["response_bytes"], accounted);
} }
} }
#[cfg(target_os = "linux")]
#[tokio::test]
async fn sendfile_auto_without_tcp_bridge_preserves_full_head_range_and_validators() {
let tmp = TempDir::new().unwrap();
let original = vec![0x5au8; 128 * 1024 + 3];
std::fs::write(tmp.path().join("large.bin"), &original).unwrap();
let service = Service {
id: "sendfile-fallback".into(),
name: "Sendfile fallback".into(),
document_root: tmp.path().to_path_buf(),
sendfile: SendfileMode::Auto,
..Default::default()
}.compile().unwrap();
let get = serve_request(&service, &request(Method::GET, "/large.bin"), "/large.bin").await;
assert_eq!(get.status(), StatusCode::OK);
assert_eq!(get.headers()[CONTENT_LENGTH].to_str().unwrap(), original.len().to_string());
// No transport adapter here. Even if a candidate extension was produced,
// its fallback Body must still stream the exact original representation.
let etag = get.headers()[ETAG].clone();
assert_eq!(get.into_body().collect().await.unwrap().to_bytes().to_vec(), original);
let head = serve_request(&service, &request(Method::HEAD, "/large.bin"), "/large.bin").await;
assert_eq!(head.status(), StatusCode::OK);
assert_eq!(head.headers()[CONTENT_LENGTH].to_str().unwrap(), original.len().to_string());
assert!(head.into_body().collect().await.unwrap().to_bytes().is_empty());
let mut range = request(Method::GET, "/large.bin");
range.headers_mut().insert(RANGE, HeaderValue::from_static("bytes=100-199"));
let ranged = serve_request(&service, &range, "/large.bin").await;
assert_eq!(ranged.status(), StatusCode::PARTIAL_CONTENT);
assert_eq!(ranged.headers()[CONTENT_RANGE].to_str().unwrap(), format!("bytes 100-199/{}", original.len()));
assert_eq!(ranged.into_body().collect().await.unwrap().to_bytes().to_vec(), original[100..200]);
let mut conditional = request(Method::GET, "/large.bin");
conditional.headers_mut().insert(IF_NONE_MATCH, etag);
let unchanged = serve_request(&service, &conditional, "/large.bin").await;
assert_eq!(unchanged.status(), StatusCode::NOT_MODIFIED);
assert!(unchanged.into_body().collect().await.unwrap().to_bytes().is_empty());
}
#[test]
fn sendfile_mode_is_backward_compatible_and_rejects_unknown_values() {
assert_eq!(SendfileMode::default(), SendfileMode::Off);
assert_eq!(serde_json::from_str::<SendfileMode>("\"auto\"").unwrap(), SendfileMode::Auto);
assert_eq!(serde_json::from_str::<SendfileMode>("\"off\"").unwrap(), SendfileMode::Off);
assert!(serde_json::from_str::<SendfileMode>("\"unsafe\"").is_err());
let legacy = serde_json::json!({"id":"legacy", "name":"Legacy", "document_root":"/tmp"});
assert_eq!(serde_json::from_value::<Service>(legacy).unwrap().sendfile, SendfileMode::Off);
}