v0.34.0_1
This commit is contained in:
1 parent
9cfc4588cc
commit
f20c8da718
25 files changed
+694
-117
No files matched your search
@@ -6,6 +6,24 @@ 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.
|
||||
|
||||
## DEV 1 KiB histogram writes (source candidate)
|
||||
|
||||
`VhostMetrics::complete` now writes one exclusive histogram bin for each
|
||||
completed exchange instead of up to 11 cumulative atomics. Both Prometheus
|
||||
exporters reconstruct `le` and `+Inf` at scrape time. Unit tests cover boundary
|
||||
semantics. This change has not been compiled or benchmarked: see
|
||||
`docs/PROXY_1KB_COUNTERS_DEV_0.34.0.md`.
|
||||
|
||||
## DEV 1 KiB cache counter follow-up
|
||||
|
||||
See `docs/PROXY_1KB_COUNTERS_DEV_0.34.0.md`. Cache domain metrics no longer
|
||||
lock the HOT store on BYPASS; analytics cloning is deferred until the queue
|
||||
has capacity. The new **source** remains uncompiled in the editing environment.
|
||||
For quick 1,040-byte loopback diagnostics with ApacheBench (not a production
|
||||
acceptance benchmark), use `python3 scripts/performance/ab_1kb_dev.py --help`.
|
||||
The authoritative long-run comparison remains `benchmark.py` with pinned
|
||||
CPU sets, an explicitly equal upstream idle pool and multiple `wrk` repeats.
|
||||
|
||||
## DEV 1 KiB follow-up on 0.34.0 (newer source)
|
||||
|
||||
See `docs/PROXY_1KB_HOTPATH_DEV_0.34.0.md`. The previous executable's 4-vs-64
|
||||
|
||||
Executable
+108
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Short, same-host ab diagnostic for 1,040-byte HTTP/1 proxy responses.
|
||||
|
||||
Requires an existing executable and nginx; keeps the same explicitly supplied
|
||||
idle-pool limit across variants. Not a substitute for wrk/CPU-isolated acceptance.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
from common import Fixture, http, write_json
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--binary', action='append', required=True, metavar='LABEL=/abs/path')
|
||||
parser.add_argument('--nginx', default='/usr/sbin/nginx', type=Path)
|
||||
parser.add_argument('--ab', default='/usr/bin/ab', type=Path)
|
||||
parser.add_argument('--out', required=True, type=Path)
|
||||
parser.add_argument('--concurrency', default='16,64')
|
||||
parser.add_argument('--duration', type=int, default=4)
|
||||
parser.add_argument('--repeats', type=int, default=2)
|
||||
parser.add_argument('--workers', type=int, default=4)
|
||||
parser.add_argument('--max-idle', type=int, default=64)
|
||||
args = parser.parse_args()
|
||||
binaries = dict(item.split('=', 1) for item in args.binary)
|
||||
if len(binaries) != len(args.binary) or not all(Path(x).is_absolute() for x in binaries.values()):
|
||||
parser.error('binary labels must be distinct and paths must be absolute')
|
||||
if args.duration < 2 or args.repeats < 1:
|
||||
parser.error('duration >=2 seconds and repeats >=1 required')
|
||||
concurrency = [int(v) for v in args.concurrency.split(',')]
|
||||
args.out.mkdir(parents=True, exist_ok=True)
|
||||
records = []
|
||||
write_json(args.out / 'metadata.json', {
|
||||
'simulation': False, 'binary_labels': list(binaries), 'nginx': str(args.nginx),
|
||||
'max_idle_for_every_rust_variant': args.max_idle, 'duration_s': args.duration,
|
||||
'repeats': args.repeats, 'concurrency': concurrency, 'workers': args.workers,
|
||||
'limitations': 'same-host shared CPU and closed-loop ab; not build-level before/after of uncompiled source'
|
||||
})
|
||||
with Fixture(args.out, workers=args.workers) as fixture:
|
||||
nginx = fixture.nginx(args.nginx, frontend=True)
|
||||
instances = {name: fixture.rust(Path(binary), name, max_idle=args.max_idle)
|
||||
for name, binary in binaries.items()}
|
||||
for name, instance in instances.items():
|
||||
instance.login()
|
||||
print('Waiting for quiescence:', name, flush=True)
|
||||
instance.wait_quiescent(timeout=150, quiet_seconds=3)
|
||||
sources = {**{name: r.reverse_port for name, r in instances.items()}, 'nginx': nginx['reverse_port']}
|
||||
payload = (fixture.site / 'small.html').read_bytes()
|
||||
for name, port in sources.items():
|
||||
code, _, body = http(port, '/small.html', headers={'Accept-Encoding': 'identity'})
|
||||
if code != 200 or body != payload:
|
||||
raise AssertionError(f'{name} precheck failed: {code}, {len(body)} bytes')
|
||||
write_json(args.out / 'payload-check.json', {
|
||||
'status': 200, 'bytes': len(payload), 'sha256': hashlib.sha256(payload).hexdigest(),
|
||||
'all_variants_verified': True
|
||||
})
|
||||
labels = list(sources)
|
||||
for c in concurrency:
|
||||
for repeat in range(args.repeats):
|
||||
order = labels[repeat % len(labels):] + labels[:repeat % len(labels)]
|
||||
if repeat % 2:
|
||||
order.reverse()
|
||||
for name in order:
|
||||
port = sources[name]
|
||||
cmd = [str(args.ab), '-k', '-t', str(args.duration), '-n', '1000000', '-c', str(c),
|
||||
'-H', 'Accept-Encoding: identity', f'http://127.0.0.1:{port}/small.html']
|
||||
prefix = f'c{c}-rep{repeat+1}-{name}'
|
||||
subprocess.run([str(args.ab), '-k', '-n', '256', '-c', str(c),
|
||||
f'http://127.0.0.1:{port}/small.html'],
|
||||
capture_output=True, timeout=25, check=True)
|
||||
p = subprocess.run(cmd, capture_output=True, text=True, timeout=args.duration + 45)
|
||||
raw = p.stdout + '\n' + p.stderr
|
||||
(args.out / f'{prefix}.txt').write_text(raw)
|
||||
if p.returncode:
|
||||
raise RuntimeError(f'{prefix} ab failed: {p.returncode} {p.stderr}')
|
||||
def field(label):
|
||||
m = re.search(r'^' + re.escape(label) + r':\s*(\d+(?:\.\d+)?)', raw, re.M)
|
||||
return float(m.group(1)) if m else None
|
||||
# ApacheBench tracks both network failures and bad body lengths.
|
||||
if not re.search(r'^Failed requests:\s+0\s*$', raw, re.M):
|
||||
raise RuntimeError(f'{prefix} failed requests: {raw[-1600:]}')
|
||||
if not re.search(r'^Non-2xx responses:\s*0\s*$', raw, re.M) and 'Non-2xx responses:' in raw:
|
||||
raise RuntimeError(f'{prefix} received non-2xx')
|
||||
code, _, body = http(port, '/small.html', headers={'Accept-Encoding': 'identity'})
|
||||
if code != 200 or body != payload:
|
||||
raise AssertionError(f'{prefix} postcheck failed')
|
||||
rps = field('Requests per second')
|
||||
if rps is None:
|
||||
raise RuntimeError(f'{prefix} missing RPS')
|
||||
sample = {'variant': name, 'concurrency': c, 'repeat': repeat+1,
|
||||
'rps': rps, 'completed': field('Complete requests'), 'failed': 0,
|
||||
'expected_sha256': hashlib.sha256(payload).hexdigest(), 'status': 200}
|
||||
records.append(sample)
|
||||
write_json(args.out / 'samples.json', records)
|
||||
print(sample, flush=True)
|
||||
write_json(args.out / 'complete.json', {'complete': True, 'samples': len(records)})
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -12,17 +12,21 @@ assert 'headers: Arc<HeaderMap>' in entry
|
||||
assert 'Arc::clone(&entry.headers)' in hot
|
||||
assert 'drop(store);' in hot
|
||||
assert hot.index('drop(store);\n\n let mut response') < hot.index('headers.as_ref().clone()')
|
||||
assert 'store.saved_body_hit(&ticket.scope, &ticket.domain, body_bytes)' in hot
|
||||
assert 'self.domain_stats.saved_body_hit(&ticket.scope, &ticket.domain, body_bytes)' in hot
|
||||
assert 'store.lru.remove(&old_sequence)' in hot and 'store.lru.insert(sequence, storage_key)' in hot
|
||||
assert '.sweep()' not in hot
|
||||
assert 'fn sweep(' not in entry
|
||||
assert 'fn saved_body_hit(' in entry
|
||||
assert 'domains: BTreeMap<String, BTreeMap<String, DomainCounters>>' in entry
|
||||
stats = read('src/cache/stats.rs')
|
||||
assert 'fn saved_body_hit(' in stats
|
||||
assert 'const DOMAIN_STATS_LIMIT: usize = 4096' in stats
|
||||
assert 'ArcSwap<PublishedDomains>' in stats
|
||||
assert 'fn record_reason(&self, reason:' in stats
|
||||
assert 'self.store.lock()' not in stats
|
||||
assert 'if self.ban_rule_count.load(Ordering::Acquire) == 0' in inv
|
||||
assert 'bans.iter().any(|rule| rule.matches_hot' in inv
|
||||
assert 'fn matching_hot_key(' in model and 'entry.vary.is_empty()' in model
|
||||
assert 'store.domain_count = 0' in dash
|
||||
assert 'c.reasons.get_mut(reason)' in dash
|
||||
assert 'self.domain_stats.clear()' in dash
|
||||
assert 'self.domain_stats.record(scope, domain, outcome, reason);' in dash
|
||||
assert 'include!("tests/hot_hit_034_tests.rs")' in read('src/cache/tests.rs')
|
||||
for path in (ROOT / 'src').rglob('*.rs'):
|
||||
if '/tests/' not in path.as_posix() and path.name != 'tests.rs':
|
||||
|
||||
@@ -107,6 +107,19 @@ assert "if !headers.contains_key(http::header::COOKIE)" in source("src/proxy/hea
|
||||
assert "if !headers.contains_key(http::header::SET_COOKIE)" in source("src/proxy/headers.rs")
|
||||
assert 'version = "0.34.0"' in source("Cargo.toml")
|
||||
|
||||
# Full exchange duration histogram still exports cumulative Prometheus series,
|
||||
# but only one exclusive bucket is updated per request; per-vhost and Docker
|
||||
# exporters must reconstruct bucket counts from exactly the same interface.
|
||||
histogram = source("src/metrics/vhost.rs")
|
||||
exporter = source("src/prometheus/docker_misc.rs")
|
||||
assert "let bucket = DURATION_BUCKETS_US.partition_point" in histogram
|
||||
assert "self.buckets[bucket].fetch_add(1, Ordering::Relaxed);" in histogram
|
||||
assert "pub fn duration_histogram(&self)" in histogram
|
||||
assert exporter.count("let (buckets, count) = m.duration_histogram();") == 2
|
||||
assert "rust_proxy_vhost_exchange_duration_seconds_bucket" in exporter
|
||||
assert "rust_proxy_docker_vhost_exchange_duration_seconds_bucket" in exporter
|
||||
assert "exclusive_bins_reconstruct_exact_prometheus_cumulative_boundaries" in source("src/metrics/vhost/tests.rs")
|
||||
|
||||
for file in (ROOT / "src").rglob("*.rs"):
|
||||
if "/tests/" in file.as_posix() or file.name == "tests.rs":
|
||||
continue
|
||||
|
||||
Reference in new issue
Block a user