v0.34.0_1
This commit is contained in:
1 parent
9b3d6731cc
commit
00f29f92c8
68 files changed
+2599
-513
No files matched your search
@@ -6,6 +6,26 @@ controllable test **upstream**, used to verify streaming, non-replayable writes,
|
||||
headers, cache policy and WebSocket frames. Performance measurements use nginx
|
||||
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
|
||||
|
||||
- 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
|
||||
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.
|
||||
|
||||
## 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)')
|
||||
Reference in new issue
Block a user