v0.34.0_1

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

No files matched your search

+33
View File
@@ -6,6 +6,26 @@ controllable test **upstream**, used to verify streaming, non-replayable writes,
headers, cache policy and WebSocket frames. Performance measurements use nginx
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.
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""Deterministic analytical/synthetic cache-retention scenario, not a RustProxy load test.
Model: 20 vhosts with 100 HOT + 100 COLD uniformly requested objects each.
A new configuration is saved 1,000 times. Eighty percent of saves edit only
keepalive/LB operational controls, and 20% edit content policy on one vhost.
Every save is followed by 500 uniform GETs; every object is public/cacheable and
has infinite TTL. A miss immediately inserts the object in its original tier.
Capacity is assumed adequate and there is no origin latency or concurrent IO.
The baseline flushes both tiers globally on every save, as in 0.33.8.
The candidate changes only the affected vhost after content edits.
"""
import argparse
import json
import random
def simulate(vhosts=20, objects_per_tier=100, saves=1000, requests_per_save=500, seed=3400):
if min(vhosts, objects_per_tier, saves, requests_per_save) < 1:
raise ValueError("all counts must be positive")
rng = random.Random(seed)
objects = vhosts * objects_per_tier * 2
old = set(range(objects))
new = set(range(objects))
old_hits = new_hits = old_misses = new_misses = 0
old_evicted = new_evicted = 0
edits_operational = edits_content = 0
for step in range(saves):
changed_vhost = rng.randrange(vhosts)
content_edit = step % 5 == 0
if content_edit:
edits_content += 1
else:
edits_operational += 1
old_evicted += len(old)
old.clear()
if content_edit:
low = changed_vhost * objects_per_tier * 2
high = low + objects_per_tier * 2
removed = set(range(low, high)) & new
new_evicted += len(removed)
new.difference_update(removed)
for _ in range(requests_per_save):
object_id = rng.randrange(objects)
if object_id in old:
old_hits += 1
else:
old_misses += 1
old.add(object_id)
if object_id in new:
new_hits += 1
else:
new_misses += 1
new.add(object_id)
total = saves * requests_per_save
assert old_hits + old_misses == total
assert new_hits + new_misses == total
assert new_evicted <= old_evicted
assert new_misses <= old_misses
return {
"scenario": {
"vhosts": vhosts,
"hot_objects_per_vhost": objects_per_tier,
"cold_objects_per_vhost": objects_per_tier,
"saves": saves,
"operational_saves": edits_operational,
"single_vhost_content_saves": edits_content,
"gets_per_save": requests_per_save,
"total_gets": total,
"seed": seed,
"assumptions": "All objects public/cacheable, infinite TTL, instantly refilled, uniform GETs; no bandwidth/CPU/origin timings",
},
"legacy_global_purge": {
"invalidated_objects": old_evicted,
"misses": old_misses,
"hits": old_hits,
"hit_ratio_percent": round(100 * old_hits / total, 3),
},
"scoped_cache": {
"invalidated_objects": new_evicted,
"misses": new_misses,
"hits": new_hits,
"hit_ratio_percent": round(100 * new_hits / total, 3),
},
"improvement": {
"fewer_invalidations_percent": round(100 * (1 - new_evicted / old_evicted), 3) if old_evicted else 0,
"fewer_cache_misses_percent": round(100 * (1 - new_misses / old_misses), 3) if old_misses else 0,
"hit_ratio_gain_percentage_points": round(100 * (new_hits - old_hits) / total, 3),
"observed_rps_gain_percent": None,
},
}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--vhosts", type=int, default=20)
parser.add_argument("--objects-per-tier", type=int, default=100)
parser.add_argument("--saves", type=int, default=1000)
parser.add_argument("--gets-per-save", type=int, default=500)
parser.add_argument("--seed", type=int, default=3400)
parser.add_argument("--output", help="optional JSON results file")
args = parser.parse_args()
result = simulate(args.vhosts, args.objects_per_tier, args.saves, args.gets_per_save, args.seed)
encoded = json.dumps(result, indent=2, sort_keys=True)
if args.output:
from pathlib import Path
Path(args.output).write_text(encoded + "\n")
print(encoded)
if __name__ == "__main__":
main()
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Amdahl sensitivity study only: 0.33.8 measured baseline, 0.34.0 not measured.
No synthetic RPS is described as benchmarked RustProxy throughput. Amdahl uses
RPS_new = RPS_old / (1 - eligible_cost_fraction * fraction_eliminated).
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
# Verified medians reproduced from the supplied 0.33.8 evidence. Cache OFF.
BASELINE = [
("reverse_proxy", 1040, 29334.12, 170610.16, False),
("reverse_proxy", 65536, 20735.07, 35393.29, False),
("reverse_proxy", 1048576, 4461.19, 2730.23, False),
("www", 1040, 24906.20, 351605.13, False),
("www", 65536, 20856.67, 203709.91, True),
("www", 1048576, 2833.71, 12611.49, True),
]
# WWW scenarios: share of per-request service time due to file copy/read;
# assume sendfile removes HALF of that part, not the full service time.
WWW_SHARES = {"low": 0.20, "middle": 0.40, "high": 0.60}
COPY_COST_REMOVED = 0.50
# Proxy independent optimization: hypothetical reduction in *total* request
# service time, modeling removal of redundant response-frame mutex, nothing else.
PROXY_TIME_SAVED = {"low": 0.01, "middle": 0.03, "high": 0.05}
def run() -> dict:
items = []
for kind, payload_bytes, rust_rps, nginx_rps, eligible in BASELINE:
baseline_ratio = rust_rps / nginx_rps
row = {
"kind": kind,
"bytes": payload_bytes,
"baseline_version": "0.33.8",
"baseline_rust_rps": rust_rps,
"baseline_nginx_rps": nginx_rps,
"baseline_of_nginx_pct": round(100 * baseline_ratio, 2),
"factor_to_match_nginx": round(nginx_rps / rust_rps, 2),
"sendfile_eligible_only_if_plain_linux_http1_1_no_cache_no_compression": eligible,
"scenarios": {},
}
for level in ("low", "middle", "high"):
if kind == "www" and eligible:
saved_fraction = WWW_SHARES[level] * COPY_COST_REMOVED
elif kind == "reverse_proxy":
saved_fraction = PROXY_TIME_SAVED[level]
else:
saved_fraction = 0.0
simulated_rps = rust_rps / (1.0 - saved_fraction)
row["scenarios"][level] = {
"modeled_total_time_saved_pct": round(100 * saved_fraction, 2),
"simulated_rps": round(simulated_rps),
"simulated_rps_gain_pct": round(100 * (simulated_rps / rust_rps - 1), 2),
"simulated_of_nginx_pct": round(100 * simulated_rps / nginx_rps, 2),
"simulated_remaining_gap_to_nginx_pct_points": round(
100 * (1 - simulated_rps / nginx_rps), 2
),
}
items.append(row)
return {
"kind": "HYPOTHETICAL_AMDAHL_SENSITIVITY_NOT_A_BENCHMARK",
"comparison": "Existing 0.33.8 measurements (5 runs, cache off) vs measured nginx",
"not_measured": "0.34.0 server performance and sendfile syscall/runtime correctness",
"conditions": [
"Identical CPU and network assumptions to supplied 0.33.8 baseline",
"WWW zero-copy is limited to Linux, cleartext HTTP/1.1, GET >=64KiB, no cache, range, transform, compression or TLS",
"WWW low/middle/high: file-copy contributes 20%/40%/60% of total request time; modeled sendfile eliminates 50% of that component",
"Reverse proxy low/middle/high: removing response-frame mutex saves a hypothetical 1%/3%/5% of total service time",
"These fractions are chosen sensitivity parameters; not measured or inferred from profiling",
"No change is assumed for WWW 1040B or out-of-scope protocol paths",
"No network saturation, TLS record processing, queueing, origin latency, filesystem cache misses, CPU contention or concurrency effects modeled",
"No additional speedup due to cache-retention changes is assumed",
],
"formula": "RPS_sim = RPS_0.33.8 / (1 - fraction_of_total_service_time_eliminated)",
"entries": items,
}
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, help="write JSON to this file")
args = parser.parse_args()
payload = json.dumps(run(), indent=2, ensure_ascii=False) + "\n"
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(payload)
print(f"wrote {args.output}")
else:
print(payload)
if __name__ == "__main__":
main()
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""0.34.0 source-wiring checks only; do not mistake these for Rust tests."""
from pathlib import Path
import tomllib
root = Path(__file__).resolve().parents[2]
read = lambda path: (root / path).read_text()
assert tomllib.loads(read("Cargo.toml"))["package"]["version"] == "0.34.0"
assert 'version = "0.34.0"' in read("Cargo.lock")
app = read("src/runtime/updates.rs")
assert "CacheInvalidation::Scopes(scopes)" in app
assert "this.cache.purge_scope(&scope)" in app
assert "this.cache.set_control_revision(envelope.revision)" in app
assert app.index("this.cache.set_control_revision(envelope.revision)") < app.index("match invalidation")
source = read("src/runtime/types.rs")
assert "view.load_balancing = Default::default();" in source
assert "member.max_concurrent_requests = 0;" in source
assert "fn vhost_cache_revision" in source
assert "&web_services" in source
assert "service.sendfile = crate::web_server::SendfileMode::Off;" in source
assert "&config.php_fpm_profiles" in source
for filename in ("src/proxy/forward/route_cache.rs", "src/proxy/forward/web_source.rs"):
assert "ticket_scoped_for_config(" in read(filename)
assert "snapshot.envelope.revision" in read(filename)
assert "expected_control_revision" in read("src/cache/tickets.rs")
assert "file.scope_revision != expected_scope_revision" in read("src/cache/cold/startup.rs")
assert "scope_revision: draft.scope_revision" in read("src/cache/cold/worker.rs")
body = read("src/proxy/body.rs")
assert "response_backend: Option<Arc<crate::balance::Lease>>" in body
assert "lease.bytes(count, false);" in body
assert "exchange.backend_bytes(count, true)" in body
assert "cold_scope_fingerprint_preserves_unmodified_vhost" in read("src/cache/tests/cold_tier_tests.rs")
assert "stale_snapshot_cannot_issue_cache_ticket" in read("src/cache/tests/scope_tests.rs")
for file in (root / 'src').rglob('*.rs'):
if '/tests/' in file.as_posix() or file.name == 'tests.rs':
continue
if len(file.read_text().splitlines()) > 500:
raise AssertionError(f"production Rust source exceeds 500 lines: {file}")
print('PASS source wiring for scoped invalidation, cold fingerprints, stale-snapshot barrier, response accounting and <=500-line modules (STATIC ONLY)')
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""Source-only guardrails for the uncompiled 0.34.0 hot-path candidate."""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def src(path):
return (ROOT / path).read_text()
snapshot = src("src/config/snapshot.rs")
assert "fn host_patterns(host: &str) -> impl Iterator" in snapshot
assert "pub fn select_on_arc(" in snapshot
assert "self.select_on_arc(host, path, local, tls).map(Arc::as_ref)" in snapshot
assert "Vec<Arc<CompiledRoute>>" in src("src/config/model.rs")
assert "route: Option<Arc<crate::config::CompiledRoute>>" in src("src/proxy/types.rs")
assert "request.extensions_mut().insert(SelectedRouteContext" in src("src/proxy/upstream_helpers.rs")
assert "selection.route.as_deref()" in src("src/proxy/forward_entry.rs")
assert "let fallback_request = selected" in src("src/proxy/forward_entry.rs")
assert "filter(|route| route.error_fallback.is_some())" in src("src/proxy/forward_entry.rs")
assert "let Some(next_request) = replay_headers" in src("src/proxy/forward/upstream.rs") or "replay_headers.as_ref().and_then" in src("src/proxy/forward/upstream.rs")
assert "let replay_headers = ((pool_retry_possible || h2_replay_possible)" in src("src/proxy/forward/upstream.rs")
pool = src("src/balance/pool_core.rs")
assert "if self.members.len() == 1 && !self.cookie_affinity()" in pool
assert "let mut scheduling = lock(&self.scheduling);" in pool
assert "fn candidates_excluding" in pool
assert "health: member.health_settings.clone()" in pool
assert "health: selected.health_settings.clone()" in pool
assert "health_settings: Arc<HealthCheck>" in src("src/balance/health.rs")
assert "healthy_cached: AtomicBool" in src("src/balance/model.rs")
assert "self.healthy_cached.store(h.healthy, Ordering::Release)" in src("src/balance/health.rs")
exchange = src("src/proxy/body.rs")
for field in ("source", "cache_effective_enabled", "transform_effective_enabled"):
assert f"pub {field}: AtomicU8" in exchange
assert f"pub {field}: Mutex" not in exchange
assert "exchange.set_source(" in src("src/proxy/forward/route_cache.rs")
assert "exchange.set_cache_enabled(" in src("src/proxy/forward/web_source.rs")
assert "self.live.receiver_count() != 0" in src("src/logging/format.rs")
assert "fn route_handles_reuse_snapshot_ownership_without_cloning_compiled_routes" in src("src/config/tests.rs")
assert "fn single_backend_fast_path_observes_capacity_and_health_without_affinity_cookie" in src("src/balance/tests.rs")
assert "fn atomic_exchange_tags_keep_original_observability_fields" in src("src/proxy/body/tests.rs")
for path in (ROOT / "src").rglob("*.rs"):
if "/tests/" in path.as_posix() or path.name == "tests.rs" or path.is_relative_to(ROOT / "tests"):
continue
assert len(path.read_text().splitlines()) <= 500, path
assert 'version = "0.34.0"' in src("Cargo.toml")
assert "max_idle_per_host: 4" in src("src/balance/model.rs")
assert "pool_recycle_requests: 25_000" in src("src/balance/model.rs")
print("PASS 0.34.0 proxy hot-path source wiring, observability, preserved defaults and source-size limits (STATIC ONLY)")
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""Offline Chromium test: sendfile toggle serializes in real admin bundle.
It uses simulated config and a mocked saveSection; never claims native
sendfile execution, Rust compilation or live server acceptance.
"""
from pathlib import Path
import json
import re
from playwright.sync_api import sync_playwright
from panel_html import render_admin_index
ROOT = Path(__file__).resolve().parents[2]
ROUTER = (ROOT / "src/admin/router.rs").read_text()
start = re.search(r'fn admin_app_javascript\([^)]*\)[^{]*\{', ROUTER)
assert start
array = ROUTER[start.end():]
array = array[:re.search(r'\]\s*\.join\("\\n"\)', array).start()]
chunks = []
for line in array.splitlines():
line = line.strip()
match = re.search(r'include_str!\("([^"]+)"\)', line)
if match:
chunks.append((ROOT / "src/admin" / match.group(1)).read_text())
elif line == 'admin_vhosts_javascript(),':
chunks.append('(() => {\n' + '\n'.join(
(ROOT / 'src/admin/web/vhosts' / (f + '.js')).read_text()
for f in ('core', 'protocol', 'health', 'rates', 'editor', 'render')) + '\n})();')
elif line.startswith('"') and line.endswith('",'):
chunks.append(json.loads(line[:-1]))
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, executable_path='/usr/bin/chromium', args=['--no-sandbox'])
for width in (375, 1440):
page = browser.new_page(viewport={'width': width, 'height': 950})
page.set_default_timeout(4500)
errors = []
page.on('pageerror', lambda err: errors.append(str(err)))
page.set_content(render_admin_index(ROOT))
page.add_style_tag(content=(ROOT / 'src/admin/web/style.css').read_text())
page.add_script_tag(content='\n'.join(chunks))
page.evaluate('''() => {
envelope = {revision: 10, config: {web_services: [{
id: 'site-A', name: 'Static Site', document_root: '/srv/static',
index: ['index.html'], php: {enabled: false}
}], vhosts: [{id: 'vhost-A', keepalive: {max_connections: 8}}], php_fpm_profiles: []}};
currentPanel = 'web-server';
document.documentElement.classList.remove('app-loading', 'app-startup-failed');
document.getElementById('login').hidden = true;
document.getElementById('workspace').hidden = false;
document.getElementById('panel-web-server').hidden = false;
window.__saved = [];
saveSection = async (section, value) => {
window.__saved.push({section, value: structuredClone(value)});
if (section === 'web-services') envelope.config.web_services = structuredClone(value);
return true;
};
window.rustProxyWebServer.render();
}''')
page.evaluate("document.querySelector('#web-service-list .entity-actions button').click()")
assert page.locator('#web-service-sendfile').input_value() == 'off'
page.evaluate("document.getElementById('web-service-sendfile').value = 'auto'")
page.evaluate('''() => { const form = document.getElementById('web-service-form'); if (!form.checkValidity()) throw new Error('invalid fields: ' + [...form.elements].filter(e=>!e.checkValidity()).map(e=>e.id).join(', ')); form.requestSubmit(); }''')
page.wait_for_function('window.__saved.length === 1')
result = page.evaluate('''() => ({
saved: window.__saved[0],
cacheStillUnrelated: envelope.config.vhosts[0].keepalive.max_connections === 8,
closed: !document.getElementById('web-service-dialog').open
})''')
assert result['saved']['section'] == 'web-services', result
assert result['saved']['value'][0]['sendfile'] == 'auto', result
assert result['saved']['value'][0]['document_root'] == '/srv/static', result
assert result['cacheStillUnrelated'] and result['closed'], result
page.evaluate("document.querySelector('#web-service-list .entity-actions button').click()")
assert page.locator('#web-service-sendfile').input_value() == 'auto'
page.evaluate("document.getElementById('web-service-sendfile').value = 'off'")
page.evaluate('''() => { const form = document.getElementById('web-service-form'); if (!form.checkValidity()) throw new Error('invalid fields: ' + [...form.elements].filter(e=>!e.checkValidity()).map(e=>e.id).join(', ')); form.requestSubmit(); }''')
page.wait_for_function('window.__saved.length === 2')
assert page.evaluate('envelope.config.web_services[0].sendfile') == 'off'
assert not errors, errors
print(f'PASS simulated panel sendfile off -> auto -> off config save at {width}px')
browser.close()
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""RustProxy 0.34.0 sendfile source-wiring assertions ONLY, not native tests."""
from pathlib import Path
import json
import re
root = Path(__file__).resolve().parents[2]
get = lambda path: (root / path).read_text()
model = get('src/web_server/config/model.rs')
assert 'pub enum SendfileMode' in model and 'sendfile: SendfileMode::Off' in model
assert 'pub sendfile: SendfileMode' in model
assert 'sendfile: self.sendfile' in get('src/web_server/config/compile.rs')
assert 'pub sendfile: SendfileMode' in get('src/web_server/config/validation.rs')
assert 'service.sendfile = crate::web_server::SendfileMode::Off;' in get('src/runtime/types.rs')
source = get('src/proxy/forward/web_source.rs')
for guard in ('!enabled && identity_request', 'request_method == Method::GET',
'response.status() == StatusCode::OK', 'content-encoding',
'transfer-encoding', 'content-range', 'candidate.length',
'http::Version::HTTP_11'):
assert guard in source, guard
assert 'content_encoding.is_none()' in get('src/web_server/response/file.rs')
assert 'single_range.is_none()' in get('src/web_server/response/file.rs')
assert 'opened.file.try_clone()' in get('src/web_server/response/file.rs')
bridge = get('src/server/sendfile.rs')
for item in ('libc::sendfile(', 'MAX_PENDING: usize = 8', 'MIN_SIZE: u64 = 64 * 1024',
'registry.slots.is_empty()', 'native_sendfile_transfers_exact_file',
'sendfile placeholder did not match', 'UnexpectedEof'):
assert item in bridge, item
assert 'pub(crate) mod sendfile' in get('src/server.rs')
assert 'SendfileIo::new(stream)' in get('src/server/tls_runtime.rs')
assert 'sendfile_slots' in get('src/server/http1_connection.rs')
js = get('src/admin/web/web-server.js')
html = get('src/admin/web/index.html')
assert '"web-service-sendfile"' in js and 'sendfile: "off"' in js
assert 'sendfile: $("web-service-sendfile").value' in js
assert html.count('id="web-service-sendfile"') == 1
assert 'value="auto"' in html
for locale in ('en_US', 'pl_pl'):
keys = json.loads(get(f'langs/{locale}.json'))['translations']
assert all(k in keys for k in ('webServiceSendfile', 'webServiceSendfileHelp', 'webServiceSendfileAuto'))
api = json.loads(get('src/admin/openapi.json'))
assert 'sendfile' in json.dumps(api)
print('PASS 0.34.0 sendfile source guards, default OFF, Linux syscall, UI, translations, API and scoped cache (STATIC ONLY)')