0.34 #2
No files matched your search
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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**.
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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)",
|
||||||
|
|||||||
@@ -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)",
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
@@ -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()
|
||||||
@@ -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)')
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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
@@ -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
|
||||||
Vendored
+8
@@ -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,
|
||||||
|
|||||||
Vendored
+7
-1
@@ -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;
|
||||||
|
|||||||
Vendored
+1
@@ -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(),
|
||||||
|
|||||||
Vendored
+23
@@ -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
|
||||||
|
|||||||
Vendored
+7
@@ -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>,
|
||||||
|
|||||||
Vendored
+2
@@ -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>,
|
||||||
|
|||||||
Vendored
+35
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Vendored
+16
@@ -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();
|
||||||
|
|||||||
Vendored
+36
-1
@@ -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,
|
||||||
|
|||||||
@@ -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>>>,
|
||||||
|
|||||||
@@ -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
@@ -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>>,
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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))
|
||||||
|
|||||||
@@ -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!(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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},
|
||||||
|
|||||||
@@ -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>()
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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());
|
||||||
|
|||||||
@@ -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");
|
||||||
@@ -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;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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 ®istry.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
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user