Skip to content

bench: generate both columns of every comparison from one shape table - #35

Open
shreemaan-abhishek wants to merge 2 commits into
mainfrom
bench/harness-rewrite
Open

bench: generate both columns of every comparison from one shape table#35
shreemaan-abhishek wants to merge 2 commits into
mainfrom
bench/harness-rewrite

Conversation

@shreemaan-abhishek

@shreemaan-abhishek shreemaan-abhishek commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Benchmark harness rewrite, plus the results records it produced. No library code changes — nothing under src/.

Companion to #33, which carries the TLS trust-store fix this harness found. That PR touches only src/; this one should land first, since the fix's numbers are reproduced with the cases added here.

Why

The old harness hand-wrote one target location per client, so the two sides drifted into different API shapes and the published ratio credited that drift to the C-vs-Lua difference.

Every case is now a driver plus a shape, and both columns of a pair are generated from the same shape table — so they send the same headers, size the same pool and consume the body the same way by construction rather than by review. A fairness audit diffs the raw request each client actually put on the wire (via a /mock/echo upstream returning ngx.req.raw_header()) and fails the run if the caller-supplied headers disagree.

benchmark/cases.txt is the single source of truth: run.sh generates one location per name and the smoke check diffs it against the case table loaded in the worker, so the two cannot drift apart.

What's in it

  • Matrix: read mode, connection lifetime, method, transport, peer address, response framing, response header count, request header count — 16 shapes across both clients.
  • Repeats with median reporting (BENCH_REPEATS, default 5) and a separate BENCH_LATENCY_REPEATS, because a single 30 s window is not evidence for a tail number.
  • Saturation gate: any window where the target worker was not CPU-bound is discarded rather than reported. This is what caught the earlier 2-core results being generator-limited.
  • Separate fixed-rate latency phase. Latency measured at saturation is queue depth, not latency, so the pair is re-run at a fixed fraction of the slower side's throughput — the only rate at which both are answering the same question.
  • Connection-reuse accounting taken from the upstream, so it stays off the measured worker's hot path. A keepalive case that quietly stopped reusing would otherwise look like a win.
  • CSV output plus fold.sh to render it as markdown.
  • Trailers are benchmarked against a raw TCP mock in a stream {} block, since nginx's http server cannot emit them.
  • New tlsverify / tlsverifyshort cases. The matrix had never exercised ssl_verify = true — all four call sites hardcoded false — so the verifying path was entirely unmeasured.

Results records

Two findings worth calling out from it:

  • A previously published ffi.stateful p99.9 claim of 20.94 ms is disproven. It came from a single 30 s window and was flagged at the time as not-yet-evidence. Three repeats give 2.720 / 2.730 / 2.730 ms against resty.stateful's 3.320 ms — the FFI client wins the tail, it does not lose it.
  • The one genuinely large tail in the matrix belongs to lua-resty-http: resty.trailers p99.9 of 109.570 ms against ffi.trailers at 2.600 ms.

Known gaps

  • README.md and benchmark/README.md still carry pre-fix numbers and the disproven tail claim above. Corrected data for the whole matrix is in benchmark/results-full.md; re-baselining the READMEs is deliberately left to a follow-up rather than bundled here.
  • The fixed-rate latency phase shows windows that miss their target rate by ~2%, which coordinated-omission correction turns into a ~7x apparent latency blowup. It affects both clients (documented in benchmark/results-full.md), so it looks like a harness or environment property rather than a client defect — but it is not yet diagnosed, and no latency claim should be published for the short-lived TLS shapes until it is.

Summary by CodeRabbit

  • New Features

    • Added a comprehensive HTTP/1.1 benchmark suite covering throughput, latency, TLS, streaming, framing, headers, connection reuse, and multiple client implementations.
    • Added configurable benchmark runs with warmups, repeats, auditing, garbage-collection reporting, and CSV/Markdown result generation.
    • Added expanded mock endpoints and benchmark scenarios, including trailers, chunked responses, cookies, and request-body handling.
  • Documentation

    • Updated benchmark methodology, results, configuration, analysis, and AI proxy integration guidance.
    • Added a canonical benchmark case catalog.
  • Tests

    • Expanded benchmark validation for generated configurations, case coverage, fairness checks, and result processing.

The old harness hand-wrote one target location per client, so the two sides
drifted into different API shapes and the published ratio credited that drift
to the C-vs-Lua difference. Every case is now a driver plus a shape, and both
columns of a pair are built from the same shape table, so they send the same
headers, size the same pool and consume the body the same way by construction
rather than by review. A fairness audit diffs the raw request each client
actually put on the wire and fails the run if the caller headers disagree.

benchmark/cases.txt is the single source of truth: run.sh generates one
location per name and the smoke check diffs it against the table loaded in the
worker, so the two cannot drift. The matrix covers read mode, connection
lifetime, method, transport, peer address, response framing and header count.

Also adds repeats with median reporting, a saturation gate that discards any
window where the target worker was not CPU-bound, CSV output, fold.sh to render
it, a separate fixed-rate latency phase (latency measured at saturation is
queue depth, not latency), and connection-reuse accounting taken from the
upstream so it stays off the measured worker's hot path.
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR replaces the benchmark with a configurable HTTP/1.1 matrix for FFI and lua-resty-http. It adds case-driven Lua clients, generated routes, mock services, throughput and latency phases, CSV folding, benchmark results, validation tests, and APISIX integration documentation.

Changes

Benchmark matrix

Layer / File(s) Summary
Case model and client drivers
benchmark/cases.txt, benchmark/lua/bench.lua
Defines paired benchmark cases and FFI or lua-resty-http drivers for request, connection, transport, framing, header, body, and TLS variants.
Mock services and generated routing
benchmark/conf/*.nginx.conf, benchmark/run.sh
Adds reusable upstream fixtures, counters, TLS and trailer services, initialized target routes, generated case locations, and readiness checks.
Benchmark execution and result folding
benchmark/run.sh, benchmark/fold.sh, .gitignore
Adds configurable warmups, repeats, audits, throughput and latency phases, resource sampling, CSV output, filtering, aggregation, and broader run-directory ignores.
Benchmark documentation, results, and tests
README.md, benchmark/README.md, benchmark/results-full.md, t/003-benchmark-runner.t
Documents the benchmark methodology and matrix, records full and TLS results, and validates cases, generated configuration, phases, mocks, folding, and dry-run isolation.

APISIX integration assessment

Layer / File(s) Summary
APISIX compatibility assessment
docs/ai-proxy-integration.md
Documents the APISIX ai-proxy transport contract, compatible behavior, unsupported interfaces, request semantics, and testing prerequisites.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Runner
  participant TargetNginx
  participant BenchLua
  participant UpstreamNginx
  participant FoldScript
  Runner->>TargetNginx: Generate and load benchmark case routes
  Runner->>TargetNginx: Execute throughput or latency cases
  TargetNginx->>BenchLua: Run selected case
  BenchLua->>UpstreamNginx: Send HTTP or TLS request
  UpstreamNginx-->>BenchLua: Return response and counters
  BenchLua-->>TargetNginx: Return fixed benchmark response
  TargetNginx-->>Runner: Produce wrk2 metrics
  Runner->>FoldScript: Process CSV results
  FoldScript-->>Runner: Emit Markdown benchmark tables
Loading

Possibly related PRs

Suggested reviewers: membphis

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main benchmark harness change: generating both comparison columns from one shared shape table.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (7)
benchmark/README.md (1)

249-249: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fix the markdownlint warnings.

Two code fences have no language (lines 249 and 333), and ## Results appears twice (lines 20 and 327). Use text for both fences and rename the second heading, for example ## Result files.

Also applies to: 327-327, 333-333

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmark/README.md` at line 249, Fix the markdownlint issues in
benchmark/README.md by adding the text language to the code fences at the
referenced locations and renaming the second “## Results” heading to a distinct
heading such as “## Result files,” while preserving the surrounding content.

Source: Linters/SAST tools

benchmark/results-full.md (1)

240-240: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a language to the perf diff fence.

markdownlint reports MD040 for this fence. Use text.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmark/results-full.md` at line 240, Update the fenced code block in
benchmark/results-full.md to declare the text language by adding `text` after
the opening fence, resolving markdownlint MD040 without changing the block
contents.

Source: Linters/SAST tools

benchmark/fold.sh (1)

147-148: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the dead loop and the stray printf argument.

Line 218 is an empty loop body and has no effect. Line 147 passes an extra empty argument to a format string that has no conversion specifier.

♻️ Proposed cleanup
-        printf "The outbound cost ratio subtracts that baseline from both ",
-            ""
+        printf "The outbound cost ratio subtracts that baseline from both "
         printf "client paths and compares what is left.\n"
-        for (i = 0; i < n; i++) { }
         for (name in discarded)

Also applies to: 218-218

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmark/fold.sh` around lines 147 - 148, Remove the unused empty argument
from the printf call near the outbound cost ratio message, and delete the empty
loop body at the referenced loop location. Keep the surrounding output and loop
behavior unchanged.
t/003-benchmark-runner.t (1)

99-103: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add the remaining shapes to the axes list.

The loop covers 12 shapes. cases.txt defines 16. Add the four missing names so a removal from cases.txt fails here.

💚 Proposed change
-for my $shape (qw(oneshot stateful stream short post tls dns chunked trailers
-                  hdr40 cookies req30)) {
+for my $shape (qw(oneshot stateful stream readbody short post tls tlsshort
+                  tlsverify tlsverifyshort dns chunked trailers
+                  hdr40 cookies req30)) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@t/003-benchmark-runner.t` around lines 99 - 103, Add the four shapes defined
in cases.txt but absent from the qw(...) list in the matrix coverage loop,
preserving the existing 12 names and ordering where practical. Keep the
assertions keyed through %by_shape so removing any defined shape from cases.txt
causes this coverage check to fail.
benchmark/run.sh (2)

169-169: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document the intentional word splitting.

BENCH_CASES is a space-separated list, so the unquoted expansion is required here. Add a directive so the intent is explicit and shellcheck stays quiet.

♻️ Proposed change
+    # BENCH_CASES is a space-separated list; splitting is the point
+    # shellcheck disable=SC2086
     printf '%s\n' $requested
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmark/run.sh` at line 169, Add a ShellCheck directive immediately before
the unquoted $requested expansion in the printf statement to explicitly suppress
the intentional word-splitting warning, while preserving the existing
space-separated BENCH_CASES behavior.

Source: Linters/SAST tools


755-767: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the shape dedupe loop into one helper.

fairness_audit and latency_phase contain the same loop that strips the driver prefix and deduplicates shapes. One helper keeps the two phases from drifting when a new driver prefix is added.

♻️ Proposed helper
# shapes present in CASES, in first-seen order
case_shapes() {
    local name shape seen s
    local -a shapes=()

    for name in "${CASES[@]}"; do
        [[ "$name" == "baseline" ]] && continue
        shape="${name#*.}"

        seen=0
        for s in "${shapes[@]:-}"; do
            [[ "$s" == "$shape" ]] && seen=1
        done
        (( seen )) || shapes+=("$shape")
    done

    printf '%s\n' "${shapes[@]:-}"
}

Both phases then read mapfile -t shapes < <(case_shapes).

Also applies to: 886-898

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmark/run.sh` around lines 755 - 767, Extract the duplicated shape-prefix
stripping and first-seen deduplication logic from fairness_audit and
latency_phase into a shared case_shapes helper. Have the helper iterate over
CASES and emit shapes in order, then update both phases to populate their shapes
arrays from its output via mapfile, preserving baseline exclusion and ordering.
benchmark/conf/target.nginx.conf (1)

103-124: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a silent case invocation for /gcdelta.

bench.audit writes one line per call, so this endpoint returns 21 audit lines followed by the delta. The runner depends on tail -n 1 to recover the value. A failing case inside the loop also cannot set its 502 status, because the first ngx.say already flushed the headers.

A bench.probe(name) helper that runs the case and returns without writing would make the response one line and let this location report failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmark/conf/target.nginx.conf` around lines 103 - 124, Add a silent
bench.probe helper and update the /gcdelta handler to use it for the warm-up and
repeated measurements instead of bench.audit, preserving the single delta line
response. Ensure probe failures propagate before any ngx.say call so the case
can set its 502 status.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@benchmark/fold.sh`:
- Around line 119-142: Sort the headline rows generated from seen_tp before
iterating, replacing the unordered for (name in seen_tp) traversal with the same
alphabetical ordering approach used by the detail and latency tables. Also apply
deterministic sorting to the discarded list iteration at lines 219-221,
preserving the existing filtering and output behavior.

In `@benchmark/README.md`:
- Around line 196-210: Update the benchmark documentation matrix to include the
tlsverify and tlsverifyshort shapes with their TLS verification behaviors, and
add BENCH_LATENCY_REPEATS to the configuration table using its default value of
3. Keep the entries aligned with the definitions in cases.txt, bench.lua, and
run.sh.
- Line 90: Reconcile the benchmark entries for tlsshort and ffi.stateful p99.9
in the README with the authoritative results in results-full.md: replace the
withdrawn figures and recommendations with the corrected trust-store and rerun
results, or clearly mark both entries as superseded and direct readers to
results-full.md. Ensure the affected sections, including lines 94–114, no longer
present the outdated claims as current.

In `@benchmark/run.sh`:
- Around line 626-639: Extend the case exclusion pattern in the non-keepalive
warning logic to also match the verify-short benchmarks `ffi.tlsverifyshort` and
`resty.tlsverifyshort`, while preserving the existing `*.short` and `*.tlsshort`
exclusions. This prevents the reused-almost-nothing warning for these
deliberately non-keepalive cases.

In `@docs/ai-proxy-integration.md`:
- Around line 10-15: Update the APISIX file references in the documentation to
use one repository-relative path convention, removing the extra “apisix/” prefix
from the `ai-transport/http.lua` reference so it matches
`apisix/plugins/ai-drivers/openai-base.lua`.
- Around line 18-31: Update both fenced code blocks in the documentation: add
the lua language identifier to the APISIX request trace block and add http or
text to the raw headers block, including the additional block referenced by the
review. Leave the block contents unchanged.
- Around line 111-118: Update the proxy handling around httpc:connect and
client.connect so configured proxy_opts cannot be silently ignored: either
reject proxy_opts before c_connect or clearly document proxy-configured
deployments as unsupported, with rejection preferred. Add coverage that
distinguishes a proxied connection from a direct connection and verifies the
configured behavior.

---

Nitpick comments:
In `@benchmark/conf/target.nginx.conf`:
- Around line 103-124: Add a silent bench.probe helper and update the /gcdelta
handler to use it for the warm-up and repeated measurements instead of
bench.audit, preserving the single delta line response. Ensure probe failures
propagate before any ngx.say call so the case can set its 502 status.

In `@benchmark/fold.sh`:
- Around line 147-148: Remove the unused empty argument from the printf call
near the outbound cost ratio message, and delete the empty loop body at the
referenced loop location. Keep the surrounding output and loop behavior
unchanged.

In `@benchmark/README.md`:
- Line 249: Fix the markdownlint issues in benchmark/README.md by adding the
text language to the code fences at the referenced locations and renaming the
second “## Results” heading to a distinct heading such as “## Result files,”
while preserving the surrounding content.

In `@benchmark/results-full.md`:
- Line 240: Update the fenced code block in benchmark/results-full.md to declare
the text language by adding `text` after the opening fence, resolving
markdownlint MD040 without changing the block contents.

In `@benchmark/run.sh`:
- Line 169: Add a ShellCheck directive immediately before the unquoted
$requested expansion in the printf statement to explicitly suppress the
intentional word-splitting warning, while preserving the existing
space-separated BENCH_CASES behavior.
- Around line 755-767: Extract the duplicated shape-prefix stripping and
first-seen deduplication logic from fairness_audit and latency_phase into a
shared case_shapes helper. Have the helper iterate over CASES and emit shapes in
order, then update both phases to populate their shapes arrays from its output
via mapfile, preserving baseline exclusion and ordering.

In `@t/003-benchmark-runner.t`:
- Around line 99-103: Add the four shapes defined in cases.txt but absent from
the qw(...) list in the matrix coverage loop, preserving the existing 12 names
and ordering where practical. Keep the assertions keyed through %by_shape so
removing any defined shape from cases.txt causes this coverage check to fail.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 17d513a6-9263-449f-add0-e08583f5d186

📥 Commits

Reviewing files that changed from the base of the PR and between f13fcfa and 6377154.

📒 Files selected for processing (12)
  • .gitignore
  • README.md
  • benchmark/README.md
  • benchmark/cases.txt
  • benchmark/conf/target.nginx.conf
  • benchmark/conf/upstream.nginx.conf
  • benchmark/fold.sh
  • benchmark/lua/bench.lua
  • benchmark/results-full.md
  • benchmark/run.sh
  • docs/ai-proxy-integration.md
  • t/003-benchmark-runner.t

Comment thread benchmark/fold.sh
Comment on lines +119 to +142
for (name in seen_tp) {
if (name == "baseline") continue
# not split(name, parts, ".") -- awk reads the separator as a regex, so
# "." matches every character and every field comes back empty
if (substr(name, 1, 4) != "ffi.") continue
shape = substr(name, 5)

f = med_of("qps:ffi." shape)
r = med_of("qps:resty." shape)
if (f == "" || r == "") continue

printf "| `%s` | `%.2f` | `%.2f` | `%.2fx` |", shape, f, r, f / r

if (base != "" && f > 0 && r > 0) {
bus = 1000000 / base
fus = 1000000 / f - bus
rus = 1000000 / r - bus
if (fus > 0 && rus > 0) printf " `%.2fx` |", rus / fus
else printf " n/a |"
} else if (base != "") {
printf " n/a |"
}
printf "\n"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Sort the headline rows so the output is reproducible.

for (name in seen_tp) iterates an awk associative array, and that order is unspecified. The detail table at lines 158-162 and the latency table at lines 193-198 both sort first, so only the headline table is nondeterministic. benchmark/results-full.md lines 45-58 show the effect: the recorded headline table is in arbitrary shape order while the detail table below it is alphabetical.

The output of this script is pasted into README.md and benchmark/README.md, so an unstable row order produces noisy diffs and makes two folds of the same CSV disagree. The discarded list at lines 219-221 has the same problem.

🐛 Proposed fix to sort the headline shapes
-    for (name in seen_tp) {
-        if (name == "baseline") continue
+    ns = 0
+    for (name in seen_tp) headline[ns++] = name
+    for (i = 0; i < ns; i++)
+        for (j = i + 1; j < ns; j++)
+            if (headline[j] < headline[i]) {
+                t = headline[i]; headline[i] = headline[j]; headline[j] = t
+            }
+
+    for (k = 0; k < ns; k++) {
+        name = headline[k]
+        if (name == "baseline") continue
         # not split(name, parts, ".") -- awk reads the separator as a regex, so
         # "." matches every character and every field comes back empty
         if (substr(name, 1, 4) != "ffi.") continue
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (name in seen_tp) {
if (name == "baseline") continue
# not split(name, parts, ".") -- awk reads the separator as a regex, so
# "." matches every character and every field comes back empty
if (substr(name, 1, 4) != "ffi.") continue
shape = substr(name, 5)
f = med_of("qps:ffi." shape)
r = med_of("qps:resty." shape)
if (f == "" || r == "") continue
printf "| `%s` | `%.2f` | `%.2f` | `%.2fx` |", shape, f, r, f / r
if (base != "" && f > 0 && r > 0) {
bus = 1000000 / base
fus = 1000000 / f - bus
rus = 1000000 / r - bus
if (fus > 0 && rus > 0) printf " `%.2fx` |", rus / fus
else printf " n/a |"
} else if (base != "") {
printf " n/a |"
}
printf "\n"
}
ns = 0
for (name in seen_tp) headline[ns++] = name
for (i = 0; i < ns; i++)
for (j = i + 1; j < ns; j++)
if (headline[j] < headline[i]) {
t = headline[i]; headline[i] = headline[j]; headline[j] = t
}
for (k = 0; k < ns; k++) {
name = headline[k]
if (name == "baseline") continue
# not split(name, parts, ".") -- awk reads the separator as a regex, so
# "." matches every character and every field comes back empty
if (substr(name, 1, 4) != "ffi.") continue
shape = substr(name, 5)
f = med_of("qps:ffi." shape)
r = med_of("qps:resty." shape)
if (f == "" || r == "") continue
printf "| `%s` | `%.2f` | `%.2f` | `%.2fx` |", shape, f, r, f / r
if (base != "" && f > 0 && r > 0) {
bus = 1000000 / base
fus = 1000000 / f - bus
rus = 1000000 / r - bus
if (fus > 0 && rus > 0) printf " `%.2fx` |", rus / fus
else printf " n/a |"
} else if (base != "") {
printf " n/a |"
}
printf "\n"
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmark/fold.sh` around lines 119 - 142, Sort the headline rows generated
from seen_tp before iterating, replacing the unordered for (name in seen_tp)
traversal with the same alphabetical ordering approach used by the detail and
latency tables. Also apply deterministic sorting to the discarded list iteration
at lines 219-221, preserving the existing filtering and output behavior.

Comment thread benchmark/README.md
| `readbody` | `26051.07` | `15462.52` | `1.68x` | `1.99x` |
| `tls` | `17732.35` | `11861.95` | `1.49x` | `1.63x` |
| `short` | `8275.94` | `6973.64` | `1.19x` | `1.21x` |
| `tlsshort` | `872.96` | `1108.38` | `0.79x` | `0.79x` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Reconcile the tlsshort and ffi.stateful claims with results-full.md.

This PR now publishes two contradictory results for the same two claims.

  • tlsshort: this file reports 0.79x and recommends lua-resty-http for fresh handshakes. benchmark/results-full.md lines 203-210 and 283-286 state that the trust-store fix supersedes the tls and tlsshort numbers, and that tlsshort is now 1.10x, a win.
  • ffi.stateful p99.9: this file reports 20.940 ms and asks for a re-run with BENCH_LATENCY_REPEATS=3. benchmark/results-full.md lines 169-184 report that re-run and record the claim as disproven, with samples of 2.720, 2.730 and 2.730 ms.

Either update these two sections or add an explicit pointer to benchmark/results-full.md that marks both claims as superseded. As written, the withdrawn figures are the ones a reader of this file finds.

Also applies to: 94-114

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmark/README.md` at line 90, Reconcile the benchmark entries for tlsshort
and ffi.stateful p99.9 in the README with the authoritative results in
results-full.md: replace the withdrawn figures and recommendations with the
corrected trust-store and rerun results, or clearly mark both entries as
superseded and direct readers to results-full.md. Ensure the affected sections,
including lines 94–114, no longer present the outdated claims as current.

Comment thread benchmark/README.md
Comment on lines +196 to +210
| shape | axis it prices |
| --- | --- |
| `oneshot`, `stateful` | API shape; the headline pair |
| `readbody` | buffering through the Lua chunk loop instead of the C preread fast path |
| `stream` | `res.body_reader`, chunk by chunk |
| `short` | connection setup, with no keepalive pool |
| `post` | a 4KB request body |
| `tls` | TLS with a pooled connection |
| `tlsshort` | a fresh TLS handshake per request |
| `dns` | resolving the peer by hostname instead of an IP literal |
| `chunked` | chunked response framing |
| `trailers` | chunked framing with a trailer section |
| `hdr40` | 40 response headers |
| `cookies` | ten repeated `Set-Cookie` headers, which fold into an array |
| `req30` | 30 request headers, which prices per-request header validation |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the two new TLS verify shapes and BENCH_LATENCY_REPEATS.

benchmark/cases.txt lines 36-39 add tlsverify and tlsverifyshort, and benchmark/lua/bench.lua lines 427-429 register them. The case matrix table does not list either shape, although line 192 states that cases.txt is the single source of truth for the matrix.

The configuration table also omits BENCH_LATENCY_REPEATS, which benchmark/run.sh line 37 defines with default 3 and which line 113 of this file already tells the reader to set.

📝 Proposed additions
 | `tls` | TLS with a pooled connection |
 | `tlsshort` | a fresh TLS handshake per request |
+| `tlsverify` | pooled TLS with certificate verification enabled |
+| `tlsverifyshort` | a fresh verified TLS handshake per request |
 | `BENCH_LATENCY_FRACTION` | `0.6` | fraction of the slower path's QPS for that phase |
+| `BENCH_LATENCY_REPEATS` | `3` | repeats per case in the latency phase |
 | `BENCH_LATENCY_MODE` | `corrected` | `corrected` or `uncorrected` percentiles |

Also applies to: 376-408

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmark/README.md` around lines 196 - 210, Update the benchmark
documentation matrix to include the tlsverify and tlsverifyshort shapes with
their TLS verification behaviors, and add BENCH_LATENCY_REPEATS to the
configuration table using its default value of 3. Keep the entries aligned with
the definitions in cases.txt, bench.lua, and run.sh.

Comment thread benchmark/run.sh
Comment on lines +626 to +639
# a keepalive case that opens roughly one connection per request is not
# exercising the pool, whatever else the row says
case "$name" in
*.short|*.tlsshort) ;;
*)
if [[ "$rpc" != "inf" && -n "$rpc" ]] &&
awk -v v="$rpc" 'BEGIN { exit !(v + 0 < 2) }'
then
warn "$name reused almost nothing (${rpc} requests per" \
"connection); this is not a keepalive result"
fi
;;
esac
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add the new verify-short cases to the non-keepalive exclusion list.

The pattern list excludes *.short and *.tlsshort. ffi.tlsverifyshort and resty.tlsverifyshort end in short but not in .short, so they match neither pattern. Both shapes set keepalive = false in benchmark/lua/bench.lua lines 428-429, so their expected ratio is exactly 1.0 request per connection. The runner therefore prints "reused almost nothing ... this is not a keepalive result" for every window of both cases, which is a false warning on a deliberately non-keepalive shape.

benchmark/results-full.md line 223 records requests_per_connection as 1.0 for all four short-lived cases, which confirms the shape behaviour.

🐛 Proposed fix for the exclusion pattern
         case "$name" in
-            *.short|*.tlsshort) ;;
+            # every non-keepalive shape ends in "short"
+            *.short|*.tlsshort|*.tlsverifyshort) ;;
             *)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# a keepalive case that opens roughly one connection per request is not
# exercising the pool, whatever else the row says
case "$name" in
*.short|*.tlsshort) ;;
*)
if [[ "$rpc" != "inf" && -n "$rpc" ]] &&
awk -v v="$rpc" 'BEGIN { exit !(v + 0 < 2) }'
then
warn "$name reused almost nothing (${rpc} requests per" \
"connection); this is not a keepalive result"
fi
;;
esac
fi
# a keepalive case that opens roughly one connection per request is not
# exercising the pool, whatever else the row says
case "$name" in
# every non-keepalive shape ends in "short"
*.short|*.tlsshort|*.tlsverifyshort) ;;
*)
if [[ "$rpc" != "inf" && -n "$rpc" ]] &&
awk -v v="$rpc" 'BEGIN { exit !(v + 0 < 2) }'
then
warn "$name reused almost nothing (${rpc} requests per" \
"connection); this is not a keepalive result"
fi
;;
esac
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmark/run.sh` around lines 626 - 639, Extend the case exclusion pattern
in the non-keepalive warning logic to also match the verify-short benchmarks
`ffi.tlsverifyshort` and `resty.tlsverifyshort`, while preserving the existing
`*.short` and `*.tlsshort` exclusions. This prevents the reused-almost-nothing
warning for these deliberately non-keepalive cases.

Comment on lines +10 to +15
The plan assumes `apisix/apisix/plugins/ai-transport/http.lua`, "already a seam"
exposing `_M.request(params, timeout)` and `_M.set_keepalive(res, ...)`, behind
which an alternate transport is "roughly 30 lines".

There is no `ai-transport` directory in APISIX at `636100ec`. The outbound call
lives inline in `apisix/plugins/ai-drivers/openai-base.lua`, in `_M.request()`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use one repository-relative APISIX path convention.

Line 10 uses apisix/apisix/plugins/ai-transport/http.lua, while Line 15 uses apisix/plugins/ai-drivers/openai-base.lua. Remove the extra apisix/ prefix or explain the different root. Readers need one convention to locate the referenced files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/ai-proxy-integration.md` around lines 10 - 15, Update the APISIX file
references in the documentation to use one repository-relative path convention,
removing the extra “apisix/” prefix from the `ai-transport/http.lua` reference
so it matches `apisix/plugins/ai-drivers/openai-base.lua`.

Comment on lines +18 to +31
```
_M.request(self, ctx, conf, request_table, extra_opts)
http.new() / set_timeout
fetch_gcp_access_token -- auth, may return early
url.parse(endpoint) -- scheme/host/port/path/query derivation
construct_forward_headers
self.request_filter(...) -- per-driver hook that mutates params
httpc:connect(params)
core.json.encode(params.body)
httpc:request(params)
if status == 429 or 5xx: return -- retry-relevant, before the body is read
read_response(...) -- SSE loop or buffered read
httpc:set_keepalive(conf.keepalive_timeout, conf.keepalive_pool)
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add language identifiers to fenced code blocks.

markdownlint-cli2 reports MD040 for both blocks. Use lua for the APISIX trace and http or text for the raw headers.

Proposed fix
-```
+```lua
...
-```
+```http

Also applies to: 134-137

🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 18-18: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/ai-proxy-integration.md` around lines 18 - 31, Update both fenced code
blocks in the documentation: add the lua language identifier to the APISIX
request trace block and add http or text to the raw headers block, including the
additional block referenced by the review. Leave the block contents unchanged.

Source: Linters/SAST tools

Comment on lines +111 to +118
### 3. No proxy support

`lua-resty-http` has `set_proxy_options` and honours `proxy_opts` in `connect`
and `request_uri`. This library has none.

ai-proxy's `openai-base.lua` does not use it, but `request_filter` hooks are
free to set it and APISIX deployments behind a corporate proxy will want it.
This is the largest single missing feature relative to `lua-resty-http`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 8 'proxy_opts|set_proxy_options' \
  apisix/plugins/ai-drivers/openai-base.lua || true

rg -n -C 12 'proxy_opts|set_proxy_options|function.*connect|connect' \
  lib/resty/ngx_http_ffi_client.lua || true

Repository: api7/ngx_http_ffi_client

Length of output: 13329


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(openai-base\.lua|ngx_http_ffi_client\.lua|ai-proxy-integration\.md)$'

printf '%s\n' '--- proxy references in tracked Lua/docs files ---'
rg -n -C 8 'proxy_opts|set_proxy_options|request_filter|openai-base|connect\s*\(' \
  --glob '*.lua' --glob '*.md' . || true

Repository: api7/ngx_http_ffi_client

Length of output: 13011


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

client = Path("lib/resty/ngx_http_ffi_client.lua").read_text()
doc = Path("docs/ai-proxy-integration.md").read_text()

connect = client[client.index("function client.connect"):
                client.index("\n\n\nfunction client.request", client.index("function client.connect"))]
connect_fields = re.search(r'-- connect\{([^}]*)\}', client, re.S).group(1)
assigned = set(re.findall(r'\bcp\.([A-Za-z_][A-Za-z0-9_]*)\s*=', connect))

print("connect_signature_fields:", " ".join(connect_fields.split()))
print("connect_struct_assignments:", sorted(assigned))
print("proxy_opts_in_connect:", "proxy_opts" in connect)
print("set_proxy_options_in_client:", "set_proxy_options" in client)

sequence = doc[doc.index("self.request_filter"):
               doc.index("self.request_filter") + 180]
print("documented_hook_sequence:")
print(sequence)
PY

Repository: api7/ngx_http_ffi_client

Length of output: 267


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '540,670p' lib/resty/ngx_http_ffi_client.lua
printf '%s\n' '--- documented call sequence ---'
sed -n '19,31p' docs/ai-proxy-integration.md

Repository: api7/ngx_http_ffi_client

Length of output: 4356


Security Misconfiguration (CWE-16)

Reachability: Internal · Exploitability: Theoretical

Fail closed when proxy_opts is configured.

When request_filter adds proxy_opts, httpc:connect(params) passes it to client.connect, which ignores it before calling c_connect. The request can bypass the configured proxy. Document proxy-configured deployments as unsupported, or reject proxy_opts before connecting. Add a test that distinguishes proxy use from a direct connection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/ai-proxy-integration.md` around lines 111 - 118, Update the proxy
handling around httpc:connect and client.connect so configured proxy_opts cannot
be silently ignored: either reject proxy_opts before c_connect or clearly
document proxy-configured deployments as unsupported, with rejection preferred.
Add coverage that distinguishes a proxied connection from a direct connection
and verifies the configured behavior.

@shreemaan-abhishek shreemaan-abhishek self-assigned this Aug 5, 2026
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