perf(d-3): dedicated trust-header rewrite + mTLS handshake scenarios (standards#99)#22
Merged
hyperpolymath merged 2 commits intoMay 27, 2026
Conversation
Phase D-2 (PR #14) left two cost surfaces folded into the "exact route allow (proxy 200)" scenario: the Phase A contract-header construction in Proxy.build_backend_headers/1, and the transport-level mTLS handshake. A baseline collected from D-2 would have attributed both to the proxy-200 number, so a regression in either would only show up as an aggregate slowdown -- the comparator couldn't tell us which leg moved. Phase D-3 (this change, hyperpolymath/standards#99 sub-deliverable) pulls each into its own Benchee scenario so D-4 baseline collection attributes them independently: * "trust-header rewrite (Proxy.build_backend_headers)" - direct call to a new @doc false Proxy.__benchmark_build_backend_headers__/1 seam over the existing private build_backend_headers/1. No policy lookup, no Gateway.call/2 pipeline, no network I/O. Isolates the cost surface the Phase A contract invariant lives on: the gateway sets X-Trust-Level / X-Request-ID as authoritative values that shadow any client-supplied header, and the cost of building that final header map is what this scenario measures. * "mTLS handshake (test CA)" - raw :ssl acceptor + :ssl.connect/4 + immediate :ssl.close/1, using a test CA chain generated in-memory at bench startup via :public_key.pkix_test_data/1. Each iteration is one fresh handshake (verify_peer + cert chain validation + key exchange) closed before any application bytes are exchanged. Bounds the connection-spike SLO Phase E rollout has to budget for. Why in-memory rather than reusing the committed Phase B fixture in test/fixtures/mtls/: the fixture ships only *.crt files -- *.key is gitignored at the repo root. Reusing the committed certs would require committing the matching keys (or carving a fixture exception in .gitignore), which the estate security posture prefers we don't do for a bench fixture. The chain shape (test CA -> server peer + test CA -> client peer, verify_peer enforced) is identical to Phase B's fixture; only the key material differs, and it never touches disk. bench/baseline.json gains the two new scenario keys (both TODO; the _status stays "scaffold-placeholder" because D-3 still doesn't collect real numbers -- D-4 does, via the `perf: rebaseline` ritual in docs/perf-contract.md). _schema_version bumps 0.1.0 -> 0.2.0 to signal the shape change to anyone diffing the file or running compare.exs against an older results.json. docs/perf-contract.md moves both items from "Out of scope" to in scope, adds them to the scenarios table and the targets table, and re-anchors the remaining out-of-scope list on D-4 (baseline collection), Phase E (historical dashboard), and a possible follow-up amortised-handshake scenario if D-4 numbers show the proxy-200 + handshake bracket is loose. Phase D-3 is the last harness-shape change before D-4 collection: scenario keys are now stable, the comparator's per-scenario diff stays correctly keyed across the flip from scaffold to active. Unblocks: standards#99 D-4 (real baseline collection + gate arming) and, downstream, the remaining items in section 1.1 of boj-server's docs/integration/hcg-tier2-rollout-runbook.md. Refs hyperpolymath/standards#91 Refs hyperpolymath/standards#99 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…over-keep-alive)
Closes the bracket scenarios 3 + 5 left loose: per-iteration cost is
(handshake + N * request) / N which approximates the per-request cost
the Phase E rollout sees once the handshake is amortised across a
kept-alive pool.
Adds:
- bench/gateway_latency.exs: scenario 6
"mTLS amortised (test CA, N requests over kept-alive)" plus a
dedicated kept-alive acceptor on port 19_879 running an echo loop
(separate listener from scenario 5 so its close-on-handshake shape
doesn't kill the kept-alive connection). N=16; payload is a tiny
8-byte ASCII frame.
- bench/baseline.json: new scenario key with TODO placeholders;
_schema_version bumped to 0.3.0-scaffold.
- docs/perf-contract.md: scenarios table + targets table now list six
scenarios; the previously deferred amortised follow-up is moved
out of "Out of scope" into in-scope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase D-3 of the single-lane HCG tier-2 channel (
standards#91). Phase D-2 (PR #14) left two cost surfaces folded into theexact route allow (proxy 200)scenario: the Phase A contract-header construction inProxy.build_backend_headers/1, and the transport-level mTLS handshake. A baseline collected from D-2 would have attributed both to the proxy-200 number, so a regression in either would only show up as an aggregate slowdown — the comparator couldn't tell us which leg moved.This PR pulls each into its own Benchee scenario so D-4 baseline collection attributes them independently.
Refs hyperpolymath/standards#91Refs hyperpolymath/standards#99(NOT Closes — joint-close is owner-only, and D-4 baseline collection is still pending under #99).What changed
bench/gateway_latency.exs— two new Benchee scenarios:"trust-header rewrite (Proxy.build_backend_headers)"— direct call to a new@doc falseProxy.__benchmark_build_backend_headers__/1seam over the existing privatebuild_backend_headers/1. No policy lookup, noGateway.call/2pipeline, no network I/O. Pre-built conn carriesassigns[:trust_level] = :authenticatedandassigns[:request_id], so the measurement is the contract-header construction cost in isolation."mTLS handshake (test CA)"— raw:sslacceptor +:ssl.connect/4+ immediate:ssl.close/1against an in-memory test CA chain. Each iteration is one fresh handshake (verify_peer + cert chain validation + key exchange) closed before any application bytes flow. Bounds the connection-spike SLO Phase E has to budget for.lib/http_capability_gateway/proxy.ex— added the@doc false__benchmark_build_backend_headers__/1hook. Thin pass-through to the existing privatebuild_backend_headers/1; named so the bench seam is explicit and grep-discoverable, not a production entry point.forward/2remains the only supported caller.bench/baseline.json— two new scenario keys (bothTODO);_schema_versionbumps0.1.0-scaffold→0.2.0-scaffoldto signal the shape change._statusstaysscaffold-placeholder— D-3 still does not collect real numbers; D-4 does.docs/perf-contract.md— status updated D-2 → D-3; both new scenarios moved from "Out of scope" to in-scope (scenarios table + targets table); out-of-scope list re-anchored on D-4 (baseline collection), Phase E (historical dashboard), and a possible follow-up amortised-handshake scenario if D-4 reveals the proxy-200 + handshake bracket is too loose.Why in-memory cert/key instead of reusing the Phase B fixture
The committed Phase B fixture in
test/fixtures/mtls/ships only*.crtfiles — matching*.keyis gitignored at the repo root (.gitignore: *.key). The existingtest/mtls_test.exsworks around this by parsing DER from the.crtfiles directly, but a real TLS handshake needs the private keys.Two options here:
*.keyfiles (carving an exception in.gitignore).:public_key.pkix_test_data/1.I went with (2): the estate security posture prefers no key material on disk even for test fixtures, and the chain shape (test CA → server peer + test CA → client peer with
verify_peerenforced) is identical to Phase B's fixture. If D-4 baseline shows the handshake cost is sensitive to RSA key size or chain length, a follow-up pins{rsa, Size, Exp}explicitly (currently using OTP defaults).What's deliberately deferred
_statustoactive— Phase D-4 (dedicatedperf: rebaselinePR perdocs/perf-contract.md).mTLS handshake (test CA)+exact route allow (proxy 200)for D-4; if the bracket is too loose, a follow-up adds the explicit amortised scenario.standards#100).Downstream unblock
This is the last harness-shape change before D-4 baseline collection. With D-3 landed, scenario keys are stable, the comparator's per-scenario diff stays correctly keyed across the eventual flip from scaffold to active, and the rollout-prerequisite checklist in
boj-server/docs/integration/hcg-tier2-rollout-runbook.md§1.1 can tick its D-3 box.Test plan
Perf Regressionworkflow runs the five-scenario harness end-to-end (proxy-200 hits the loopback backend from D-2; mTLS handshake hits the in-memory acceptor introduced here) and posts a scaffold-mode markdown report (still non-blocking — baseline not yet populated).mix teststill passes — the@doc falseProxy.__benchmark_build_backend_headers__/1hook is a thin pass-through over the existing private function with no behaviour change.just benchon a developer machine runs the harness, the two new scenarios succeed (handshake completes against the in-memory acceptor; rewrite returns the expected header map), prints the scaffold-mode banner.bench/results.jsonfor the new scenarios reports non-trivial p50/p95/p99.:ssl.close(listen_socket)+Process.exit(acceptor_pid, :shutdown)teardown in theafterblock leaves no orphan listener on port 19_878 (noeaddrinuseon a second run).Owner merges; not for admin-merge.
🤖 Generated with Claude Code
Generated by Claude Code