Repository navigation
deps: re-pin loadgen v1.4.14, with churn-close on the default 5% error budget (probatorium#424) - #436
Conversation
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configurationConfiguration used: Repository: goceleris/probatorium/.coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: ⛔ Files ignored due to path filters (5)
📒 Files selected for processing (15)
Comment |
15d55bc to
5c63890
Compare
|
Review round 1 is pushed. The branch was force-pushed, so here is how the commits map:
The body's "Review round 1" table maps each finding to its fix. The main correction is that echo-h2 and gin-h2 do not change server-side code at this re-pin. Rebuilt at main and here, echo, gin and hertz have byte-identical code and data. iris differs only in x/net/html's The Status section now also says that a release-order guard must be on main before the loadgen release is re-run. CI is green at 5c63890. The PR stays a draft until the v1.4.14 tag exists. |
…fault 5% error budget (probatorium#424) loadgen v1.4.14 carries goceleris/loadgen#90 (-close no longer counts one error per request, #87) and #91 (the HTTP/2 client's real status and body bytes, no lifetime deadline, redial of dead connections: #85 #86 #88 #89). The v1.4.14 tag does not exist yet (the loadgen release workflow cannot create it, see the PR), so this pins the pseudo-version of the commit the tag will point at. The pin moves to v1.4.14 once the tag exists. loadgen v1.4.14 requires golang.org/x/net v0.59.0, so MVS moves x/net 0.58.0 -> 0.59.0 in the root module and in the four adapter modules that replace it (echo, gin, hertz, iris); go mod tidy in every other module changes nothing. No adapter serves HTTP/2 through x/net: echo, gin, chi, iris and stdhttp serve h2c with net/http's own HTTP/2, hertz with hertz-contrib/http2. churn-close carried ErrBudget 0.5, calibrated against a loadgen artifact: up to v1.4.13 every successful close-mode request was followed by one counted EOF error (goceleris/loadgen#87), so errors/(errors+requests) sat at ~0.5 on every server that closes as asked. With loadgen v1.4.14 the ratio is the genuine failure rate (0 errors in goceleris/loadgen#90's measurement; connect_errors was 0 in every published churn-close cell), and a 0.5 budget would publish a server that fails half its churn attempts as ok. The budget change and the loadgen bump are one commit, so no main commit has one without the other, whatever the merge method. - scenarios/static.go: drop the override and rewrite the comment; correct the H2 comment (4 streams per connection, not 100: the runner maps Workers to Connections and loadgen multiplies H2 workers by 4). - TestErrorBudgets: every registered scenario uses the default. - TestChurnCloseErrorBudget and two TestClassifyCompletedCell rows: the measured v1.4.13 artifact (26,753 requests, 26,746 errors) is suspect, the fixed client's clean run is ok; the churn-close rows take their budget from the registered scenario instead of a copy of 0.5. - TestAggregateCarriesLoadgenResultVerbatim: loadgen v1.4.14's new Result.CloseAborts reaches raw/<host>.json unchanged (the harness catch-up needs no code: the ingest keeps `result` as raw JSON).
… the h2c comments (probatorium#424) With churn-close on the default, no registered scenario raises its error budget, so every budget test ran at 0.05 and could not tell a working override from an ignored one. The override stays: it is #424's fallback for churn-close if the first post-bump run shows genuine refused dials above 5%. - TestErrorBudgets: a scenario declaring ErrBudget 0.2 must resolve to 0.2. - TestClassifyCompletedCell: a 0.3 ratio under a 0.5 budget is ok, and over a 0.5 budget the message and the connect-class overage attribution use 0.5 (600 connect errors cover the 500 past a 0.5 allowance, not the 1,850 past a 5% one). Mutants that make the gate, the attribution, the message or StaticScenario.ErrorBudget use the default all pass the previous tests and fail these. - scenarios/static.go: the churn-close comment says what loadgen counts in close mode (a failed dial, a reset, a truncated response or a timeout; never the expected close). - servers/{chi,echo,gin,iris}/server.go, servers/servers.go: the h2c mode is net/http's own HTTP/2 (Server.Protocols with UnencryptedHTTP2), not h2c.NewHandler. Comments only, same line count in the adapters.
…m#424) loadgen v1.4.14 is published: tag v1.4.14 -> 9f8993eb24b4, the commit the pseudo-version v1.4.14-0.20260927122822-9f8993eb24b4 named, and Go resolves the tag to the same commit (Origin.Hash). The module trees are identical (diff -r of the two module-cache directories is empty), so every suite and CI run on the pseudo-version tested this code. `go get github.com/goceleris/loadgen@v1.4.14 && go mod tidy` in the root changes only the root go.mod require line and the two go.sum lines; tidy is a no-op in every other module, and all 14 modules that resolve loadgen now resolve v1.4.14. benchmark_config.loadgen_version is read from the root require line, so published results will name v1.4.14.
5c63890 to
d434126
Compare
Status: ready for review. loadgen v1.4.14 is released and this PR pins v1.4.14. Squash merge only.
The tag exists and points at the commit this PR tested.
gh api repos/goceleris/loadgen/git/refs/tags/v1.4.14gives commit9f8993eb24b44e0d053b55a3796211794120c576, which is loadgen main 9f8993e (chore(release): v1.4.14 (#101)).check-tag.sh(finishing step 0) exits 0: git's peeled tag and Go'sOrigin.Hashare both 9f8993e (finish/check-tag.log, 14:09Z).gh release create v1.4.14 --target 9f8993e. That is therelease: publishedpath, the same way v1.4.13 was cut. It is not a re-run of the failed dispatch 36319142903.Origin.Hash9f8993e andRefrefs/tags/v1.4.14(finish/release-facts.log).The pin is
v1.4.14. Commit d434126 rango get github.com/goceleris/loadgen@v1.4.14 && go mod tidyin the root. It changes only the rootgo.modrequire line and the two loadgen lines ofgo.sum.go.mod/go.sumfindsv1.4.14-0.20260927122822-9f8993eb24b4(rootgo.mod:8,go.sum:18-19; that is the positive control). After it, the same grep, a grep for any loadgen pseudo-version and a grep of the whole tree all find nothing (finish/pseudo-grep-{before,after}.log).diff -rof the module-cache treesloadgen@v1.4.14andloadgen@v1.4.14-0.20260927122822-9f8993eb24b4is empty (68 files). As a control, v1.4.13 vs v1.4.14 differs in 33 files (finish/source-identity.log).go mod tidy -diffis clean in all 23 modules. All 14 modules that resolve loadgen now resolve v1.4.14: the root, the 12 servers/* adapters and fieldguard. The 9 refapp/debugvars modules do not depend on it.go mod verifypasses (finish/bump-proof-v1.4.14.txt,finish/tidy-diff-all.log).benchmark_config.loadgen_versionand the split index read the root require line (the three sites under "Why the pseudo-version could not merge"). A run from main after this merge therefore publishesv1.4.14.Rebased on main 02e4172 (#412 at 3263830, then #411 at 02e4172). The rebase was clean.
servers/servers.go: this PR changes one comment line there and feat(bench): make the SEND_ZC on/off A/B answerable: SUT /debug/vars, iowait-free CPU, per-scenario engine egress, per-column SUT env (celeris#585) #411 adds 14 lines elsewhere in the file.report.SchemaVersionto 5.17. This PR touches no schema constant and no test that pins one, so RULE 48 is not triggered.go build ./...,go vet ./...,mage -compileandgo vet -tags mage .all pass (finish/build-after-rebase.log).Merge with squash. The repo also allows rebase merges (ruleset 16196082). A rebase merge would put a4b2d0f and 53f0ed8, which pin the pseudo-version, on main as separate commits.
The release-order guard did not happen in the planned order. "Release-order gate" below says so plainly and lists what protects main until this PR merges.
Why the pseudo-version could not merge. Every place that records the loadgen version reads the root
go.modrequire line:benchmark_config.loadgen_versionfrom the mage ingest:mage_bench.go:1859(goModRequireVersion);benchmark_config.loadgen_versionfrom the runner:cmd/runner/main.go:1925(modRequireVersion, which readsgo.modat:1980);loadgen_version:mage_publish.go:360→report/split.go:98.What changes
go.modis the only module that requires loadgen. The 13 modules thatreplacethe root (servers/* and validation/properties/fieldguard) resolve it through the root. The 9 refapp/debugvars modules do not depend on it.go list -mper module is in the evidence (bump-proof-*.txt; at the tag pin,finish/bump-proof-v1.4.14.txt).golang.org/x/net v0.59.0, so MVS moves x/net from 0.58.0 to 0.59.0 in thego.modof the root and of the adapter modules echo, gin, hertz and iris.go mod tidyin the other 18 modules changes nothing: celeris, the refapps and debugvars were already on 0.59.0, and chi, fasthttp, fiber, gnet, gorilla_ws, nbio, stdhttp and fieldguard do not use x/net. In the module graphs of the seven adapters among them x/net moves too (through the root), but none builds an x/net package. hertz's graph also moves x/term 0.45.0 → 0.46.0 (x/net 0.59.0 requires it), and hertz builds no x/term package (round1/graph-diff.log).x/net/http2/hpack, imported by loadgen's client. Its source is identical in 0.58.0 and 0.59.0.Server.ProtocolswithSetUnencryptedHTTP2). That code isnet/http/internal/http2and comes with the Go toolchain, not from x/net. hertz-h2 serves through hertz-contrib/http2.x/net/http2/h2cforStartH2CServerandUseH2C. The adapters never call either, so the linker keeps only those packages' init functions and variables: 0x/net/http2.(*serverConn)symbols, 85net/http/internal/http2.(*serverConn)ones (round1/xnet-symbols.log).round1/servers-xnet-proof.log): echo, gin, hertz and iris built reproducibly (linux/amd64,-trimpath -buildvcs=false, empty build ID, no symbol table or DWARF) at main cd8dedf and at this head. Two strings must differ and have equal length: the x/net line of the build info and thegolang.org/x/net@v0.5x.0/file names. With those rewritten,.text,.rodata,.data,.noptrdata,.go.typeand.go.funcare byte-identical for echo, gin and hertz. Only the pc-to-line table (and the module data's offsets into it) differs, because 0.59.0 adds comment lines above linked init functions.unescapeEntity, where 0.59.0 writes anifasmin()with the same result. Built with 0.59.0 plus 0.58.0'shtml/escape.go, iris's code and data match the 0.58.0 build exactly. iris links x/net/html only through bluemonday, which iris calls only fromWriteMarkdownwithSanitize; the adapter never renders Markdown..text; a change in linked x/net code (h2c's init) changes code and data; a change in x/net's HTTP/2 server (serverConn.serve) leaves echo byte-identical.servers/{chi,echo,gin,iris}/server.goandservers/servers.go, which had described the h2c mode wrongly. Comments only: the four adapter binaries are byte-identical before and after (round1/comment-only-proof.log).scenarios/static.godropsErrBudget: 0.5, and the comment says why. TheErrorBudgeterandErrBudgetdocs now say that no scenario raises its budget.Workers = Connections(buildCellConfig), and loadgen multiplies HTTP/2 workers by 4 (bench.go,case cfg.HTTP2).TestErrorBudgets: every registered scenario gets the default budget.TestChurnCloseErrorBudget: churn-close's budget is checked against measured ratios.TestClassifyCompletedCellgets two new rows, the measured half-failing cell (suspect) and the fixed client's clean cell (ok). Its churn-close rows now take their budget from the registered scenario instead of a copy of 0.5.TestAggregateCarriesLoadgenResultVerbatimis the RULE 93 check (below).TestErrorBudgetsnow checks that a scenario declaringErrBudget: 0.2gets 0.2.TestClassifyCompletedCellgets two rows with a 0.5 budget: a 0.3 ratio is ok, and over the budget the message and the connect-class overage attribution use 0.5.Why the churn-close budget is 0.05
#424 names the choice: the default budget (0.05), or a churn-specific budget set from the first post-fix run's genuine refused-dial rates. No post-fix run exists, and the data we have puts the genuine rate at about 0:
suspectwith a delta of 100.000-100.006%. arm64 20260829 has 9 more coin-flipokcells with a delta of about 100.connect_errors["churn-close"]is omitted (0) in every published dataset (churn-close after loadgen#87: change ErrBudget 0.5 with the loadgen pin bump; lithium's churn-close becomes real churn; latency and teardown discontinuity #424 §3,r2/impact/published-churn-close-all.txt). So the "refused dials are inherent to churn" premise of the 0.5 never showed up as errors.okonce the artifact is gone. That is the case the new tests pin.Failing-first (evidence
control-*.log):ErrBudget: 0.5restoredTestErrorBudgets: churn-close = 0.5.TestChurnCloseErrorBudget: the 0.49993 row and the 0.10 row are not suspect.TestClassifyCompletedCell/churn-close_half-failing_cell_is_suspect: ErrMsg ""go.modvia-modfile)unknown field CloseAborts in struct literal of type loadgen.Resultresultthrough a typed mirror withoutclose_abortsclose_aborts = "", want 4499990. The other fields surviveRound 1, the non-default budget path (
round1/mutants.log;go test ./cmd/runner/ ./scenarios/in a copy of each tree, the worktree never mutated):main.go:1394)StaticScenario.ErrorBudgetignoresErrBudget(static.go:67)main.go:1401)main.go:1396)main.go:1225)RULE 93 (the harness catch-up): no probatorium code has to carry anything, and a test now proves it
What v1.4.14 changes in
loadgen.Result.git diff v1.4.13 9f8993e -- results.goadds exactly one field:CloseAborts uint64 json:"close_aborts,omitempty"(goceleris/loadgen#90).TimeseriesPointand every other type are unchanged. #91's status accounting (#85) and body bytes (#86) change the values of existing fields (requests,errors,throughput_bps), not the shape.Where the new field goes:
*loadgen.Resultin its per-cell JSON (cmd/runnercellResultFile.Result).resultas raw JSON (runnerCellFile.Result json.RawMessage→cellRecord.Loadgen,mage_bench.go), soraw/<host>.jsoncarries every field loadgen emits.results/(benchmark-tier.yml,path: results/).TestAggregateCarriesLoadgenResultVerbatimpins this; controls C and D above show it can fail. churn-close after loadgen#87: change ErrBudget 0.5 with the loadgen pin bump; lithium's churn-close becomes real churn; latency and teardown discontinuity #424's post-bump check readsclose_abortsfrom there: aboutrequestson lithium, about 0 elsewhere.summary.json, schema 5.17 since feat(bench): make the SEND_ZC on/off A/B answerable: SUT /debug/vars, iowait-free CPU, per-scenario engine egress, per-column SUT env (celeris#585) #411): not added, on purpose.dial_retriesandrecvq_highappear nowhere inreport/.report.Aggregatereads only named fields, and none changed shape.SchemaVersiondoes not move, and RULE 48 is not triggered. After the rebase onto feat(bench): make the SEND_ZC on/off A/B answerable: SUT /debug/vars, iowait-free CPU, per-scenario engine egress, per-column SUT env (celeris#585) #411 (5.17), this PR still touches no schema constant and no test that pins one.EngineMetrics→ debugvars →Snapshot. Novalidation/package imports loadgen:git grep -l goceleris/loadgenlists only cmd/loadgen, cmd/runner, interleave (test), the mage files, report and scenarios.tools/stresstallytalliesgo testoutput.DisallowUnknownFieldsin the repo.report/schema_v5.jsonvalidates the published document, which does not embed the loadgen result.Which published fields change: the churn-close and H2 series restart at this re-pin
Sources: #424 §2-§4 and its comment, goceleris/loadgen#90 "Impact on what probatorium publishes", and goceleris/loadgen#91 "What changes in probatorium's published numbers".
churn-close, on the 42-44 servers per dataset that close as asked:
sent_vs_handled_delta_pctfalls from about 100 to the genuine rate. The key is omitted at 0.cell_statusesmoves fromsuspecttook.latency_at_slo["churn-close"]andrated_mode_p99_at_target_rps["churn-close"]appear. Rated profiles run one more churn-close sweep per server.saturation_mode_rpsandloadgen_cpu_p95["churn-close"]change.saturation_mode_rpscan also come out low on a server whose full accept queue drops SYNs. loadgen's H1 dial cannot be cancelled, so a SYN retransmission that starts near the end of the window stretches the elapsed time: a 3 s stall after a 30 s window reads about 10% low (h1client: a dial in progress cannot be cancelled, so a SYN retransmission at the end of a run stretches Run's elapsed time and deflates requests_per_sec loadgen#95, open). After fix(publish): BenchAndValidate reads VALIDATE_TARGET to gate ValidateDiff #90 every churn-close request dials, so churn-close is where this shows.timeseries.json.gzfor churn-close: the per-seconderrorsfalls from aboutrpsto about 0.hdr_histogram_b64, the timeseriesp99_ms, and the new rated fields.connect_errorskeeps its meaning.throughput_bpsscales with successes. Its only published use,network_bound, cannot fire on this small body.lithium's churn-close becomes real churn. lithium ignores
Connection: close, so its published cell has been a keep-alive measurement: 458,510-481,817 rps on x86, 196,704 on arm64. It falls to churn values insaturation_mode_rps,latency_at_sloandrated_mode_p99_at_target_rps. This is not a lithium regression.resources["churn-close"].fd_hwmrises by about rate × 50 ms.H2:
get-json-h2andpost-4k-h2, 32 cells per arch (goceleris/loadgen#91)::statusis now an error. That movesrequests,errors,sent_vs_handled_delta_pctandsaturation_mode_rps, and can change the cell status. feat(diff): persist ValidateDiff findings as diff.txt + diff.json #91 expects no published change, since every adapter answers 200.throughput_bpschanges only for a server that splits bodies or ends them with an empty END_STREAM DATA frame. That is fastapi-h2, which counted 0 bytes. The only published use isnetwork_bound, and no H2 cell comes near it.saturation_mode_rpsrises on servers that end connections, the latency of the redialing request carries the dial, failed redials show inconnect_errors, and errors per second during an outage rise about 4×.loadgen_cpu_p95falls (a leaked writeLoop per GOAWAY), and the latency tail falls (no 5-10 ms sleep per redial).hdr_histogram_b64upper tail (p99.99, max) can rise. Responses that were missing come back, and misattributed ones go away.latency_at_sloandrated_mode_p99_at_target_rpsexist for H2 rows only while BenchTier rates every clean cell (~770 of 813) at 30 s passes, not the model's 388 rated cells: this is the whole gap between the 44.5 h / 26.6 h projections and the measured ~69 h / ~53 h runs #418 stands.Keep-alive H1: small. Only cells that already carry loadgen errors are affected: 15 driver-pg-write cells and one get-simple-1024c cell in ae6b949.
Do not compare these series across this commit.
diffBenchResults(BenchSince) and the markdown "Regression vs baseline" section do not readloadgen_version.BenchSince, and nothing setsreport.Meta.BaselinePath(only tests do). So the first post-bump run cannot trip or pass either comparison on a measurement change.errorsandp99_msseries, and the H2 rows. They need the discontinuity note from churn-close after loadgen#87: change ErrBudget 0.5 with the loadgen pin bump; lithium's churn-close becomes real churn; latency and teardown discontinuity #424's second to-do (docs).BenchSinceinto CI.Release-order gate
The tag reached the world before this PR merged, and before either guard reached main. Round 1 said a guard had to land on probatorium main before v1.4.14 existed (#424, goceleris/loadgen#97 item 2). The guard was either (a) a dependabot
ignorefor loadgen or (b) the budget change alone.ErrBudget: 0.5and has noignorefor loadgen.What protects main until this PR merges:
servers/nbioonly), ci: bump the all-actions group across 2 directories with 1 update #408 (actions), deps(maven): bump the all-maven-deps group across 1 directory with 2 updates #407 (maven), deps(js): bump uWebSockets.js from v20.70.0 to v20.71.0 in /servers/uws #406 and deps(js): bump fastify from 5.12.4 to 5.12.5 in /servers/fastify in the all-js-deps group across 1 directory #405 (js). None of their diffs has a loadgen line (finish/dependabot-gate-check.txt).go get github.com/goceleris/loadgen@v1.4.14 && go mod tidy, the root builds and vets, andgo test ./scenarios/ ./cmd/runner/(including main'sTestErrorBudgets) and the mage-tagged tests pass (finish/main-bump-sim.log). A green dependabot PR would prove nothing here.Proposed for later releases: an
ignorefor loadgen in.github/dependabot.yml(not added here). In thepackage-ecosystem: gomodentry:Verification
timing=none,-p 1, go 1.27.1;finish/suite-v1414-*.log):go test -count=1 -p 1 -race -v ./scenarios/ ./cmd/runner/ ./servers/: 108 PASS / 0 FAIL / 3 SKIP (the rust adapter binaries are not built on the Mac)go test -count=1 -p 1 -race -v -tags mage .: 129 PASS / 0 FAIL / 1 SKIP (114 at round 1; the new ones come with feat(bench): make the SEND_ZC on/off A/B answerable: SUT /debug/vars, iowait-free CPU, per-scenario engine egress, per-column SUT env (celeris#585) #411 and feat(validation): the celeris#588 capture control -- inject an h2c hang and a WS handshake failure at a known instant, and prove the artifact names them on every engine (probatorium#351) #412)go test -count=1 -p 1 -race -v ./...: 1232 PASS / 0 FAIL / 12 SKIP, every package ok (1143 at round 1; the new ones come with feat(bench): make the SEND_ZC on/off A/B answerable: SUT /debug/vars, iowait-free CPU, per-scenario engine egress, per-column SUT env (celeris#585) #411 and feat(validation): the celeris#588 capture control -- inject an h2c hang and a WS handshake failure at a known instant, and prove the artifact names them on every engine (probatorium#351) #412)go build,go vet ./...,go test -race ./...ok (no test files in these five)ansible loads every playbook and tasks filejob from feat(bench): make the SEND_ZC on/off A/B answerable: SUT /debug/vars, iowait-free CPU, per-scenario engine egress, per-column SUT env (celeris#585) #411, ci-ok), Lint (36325002657), Coverage (36325002610), CodeQL (36324999864). The PR-tier matrix is skipped (nocluster-oklabel).timing=none,-p 1, go 1.27.1), at 5c63890 with loadgen 9f8993e as the pseudo-version (round1/suite-r1-5c63890-*.log):go test -count=1 -p 1 -race -v ./scenarios/ ./cmd/runner/ ./servers/: 108 PASS / 0 FAIL / 3 SKIP (the rust adapter binaries are not built on the Mac)go test -count=1 -p 1 -race -v -tags mage .: 114 PASS / 0 FAIL / 1 SKIPgo test -count=1 -p 1 -race -v ./...: 1143 PASS / 0 FAIL / 12 SKIP, every package ok (round 0: 1141, plus the two new classifier rows)go build,go vet ./...,go test -race ./...ok (no test files in these five)round1/lint.log):go vet ./...,golangci-lint run(v2.13.2) 0 issues,gofmt -l .empty. In the adapter modules, golangci-lint is clean for chi, gin and iris; echo reports one SA1019 (middleware.LoggerWithConfigis deprecated) inchain_handlers.go, which this PR does not touch.cluster-oklabel).Review round 1
min()refactor, which the adapter never reaches. The stale adapter comments that said "h2c.NewHandler-wrapped" are fixed.TestErrorBudgetscheck at a non-default budget. They kill M1, M2, M3 and M5; M4 is listed below.check-tag.sh. Its controls at round 1: v1.4.14 exited 2 (no tag yet), v1.4.13 against aad6d6b exits 0, v1.4.13 against 9f8993e exits 1. At the finish, v1.4.14 exits 0 (Status).loadgen_versionReview round 2
Approved. Its release-order minor is answered in "Release-order gate": the tag reached the world before this PR merged, and that section lists what protects main until it does.
Not in this PR, so
Refs #424, notFixesconnect_errorsandclose_aborts, TIME_WAIT on both hosts,TcpExtTCPSynRetrans,TcpExtPAWSEstab,TcpExtTCPTimeWaitOverflow, and churn-close cells withrequests > 0andthroughput_bps == 0(h1client: the response framing parser leaves the connection at the wrong position for close-delimited bodies, HEAD/1xx/204/304, TE+CL, trailers and HTTP/1.0 loadgen#93).saturation_mode_rpswhen a SYN retransmission runs past the window. Until it is fixed, a churn-close RPS drop on a server that drops SYNs may be that, not the server.executeCellpassesscenarios.ErrorBudgetFor(cell.Scenario)to the classifier (cmd/runner/main.go:1225). It survives on main too. It changes nothing today, because every scenario is at the default, but it matters once a scenario raises its budget again. Covering it needs a seam inexecuteCell, so it is left for a follow-up issue.Refs #424