Skip to content

perf(d-3): dedicated trust-header rewrite + mTLS handshake scenarios (standards#99)#22

Merged
hyperpolymath merged 2 commits into
mainfrom
claude/phase-d3-mtls-handshake-and-header-rewrite-scenarios
May 27, 2026
Merged

perf(d-3): dedicated trust-header rewrite + mTLS handshake scenarios (standards#99)#22
hyperpolymath merged 2 commits into
mainfrom
claude/phase-d3-mtls-handshake-and-header-rewrite-scenarios

Conversation

@hyperpolymath
Copy link
Copy Markdown
Owner

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 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.

This PR pulls each into its own Benchee scenario so D-4 baseline collection attributes them independently.

Refs hyperpolymath/standards#91
Refs 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 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. Pre-built conn carries assigns[:trust_level] = :authenticated and assigns[:request_id], so the measurement is the contract-header construction cost in isolation.
    • "mTLS handshake (test CA)" — raw :ssl acceptor + :ssl.connect/4 + immediate :ssl.close/1 against 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__/1 hook. Thin pass-through to the existing private build_backend_headers/1; named so the bench seam is explicit and grep-discoverable, not a production entry point. forward/2 remains the only supported caller.

  • bench/baseline.json — two new scenario keys (both TODO); _schema_version bumps 0.1.0-scaffold0.2.0-scaffold to signal the shape change. _status stays scaffold-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 *.crt files — matching *.key is gitignored at the repo root (.gitignore: *.key). The existing test/mtls_test.exs works around this by parsing DER from the .crt files directly, but a real TLS handshake needs the private keys.

Two options here:

  1. Commit the test *.key files (carving an exception in .gitignore).
  2. Generate the chain in-memory at bench startup via :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_peer enforced) 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

  • Real baseline numbers + flip _status to active — Phase D-4 (dedicated perf: rebaseline PR per docs/perf-contract.md).
  • A dedicated "mTLS amortised" scenario (N requests over one kept-alive TLS connection) — bracketed by 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.
  • Dashboard / historical-numbers publication — Phase E (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

  • CI green: Perf Regression workflow 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).
  • CI green: existing workflows (governance, hypatia-scan, dogfood-gate, codeql, scorecard) unaffected.
  • CI green: mix test still passes — the @doc false Proxy.__benchmark_build_backend_headers__/1 hook is a thin pass-through over the existing private function with no behaviour change.
  • Local: just bench on 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.
  • Manual: confirm bench/results.json for the new scenarios reports non-trivial p50/p95/p99.
  • Manual: confirm the :ssl.close(listen_socket) + Process.exit(acceptor_pid, :shutdown) teardown in the after block leaves no orphan listener on port 19_878 (no eaddrinuse on a second run).

Owner merges; not for admin-merge.

🤖 Generated with Claude Code


Generated by Claude Code

hyperpolymath and others added 2 commits May 27, 2026 06:19
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>
@hyperpolymath hyperpolymath marked this pull request as ready for review May 27, 2026 14:22
@hyperpolymath hyperpolymath merged commit c82be8d into main May 27, 2026
6 of 15 checks passed
@hyperpolymath hyperpolymath deleted the claude/phase-d3-mtls-handshake-and-header-rewrite-scenarios branch May 27, 2026 14:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant