Stinky 11 — Lane R2: the data plane, restored
Lane R2 consumes stinky-01 (the data-plane audit), stinky-08 (the archaeology) and stinky-10 (the benchmark baseline) and RESOLVES them, in the order of cheapness the archaeology gave. Branch
stinky/r2-dataplane, offstinky/bench@a5ef6e6— that merge is not optional, it carries the fix without whichcargo build -p xtask --example nornir-benchdies with 10x E0063.Every row below is MEASURED on a named box, before and after, from the benchmark harness. Builds on loki (Ryzen 9 7900, 12 cores), measurements on oden (Threadripper PRO 3975WX, 32 cores), release, host toolchain — the same road stinky-10 took, because a comparison across two roads is void.
The four, and what each one cost and bought
| # | thing | verdict | number |
| 2 | six reqwest manifests without default-features = false |
DONE | holger reached native-tls + openssl-sys; now neither package is in its graph at all |
| 3 | the request-path bench, dropped 2026-05-31 | DONE | 3 new arms; the serving door measured at 332 627 ops/s against get_file's 420 738 on the same corpus — the door costs 21 % |
| 4 | HotArchive under the writable put |
DONE | 8 000 one-at-a-time puts: 42.148 s → 1.042 s, 189.8 → 7 673.9 puts/s, 40.4× |
| 1 | the Vec<u8> → Bytes migration, written and deleted unexecuted |
DONE | 50 signatures; flat on every existing arm, and the reason is stated rather than hidden |
Commits, in order: b18627f · 4dada1a · fc671dc · 39aac92 · 5f7aed5 · 22476b7 · 49b9374.
2 · The six reqwest lines — the one that was already burning
server/lib:121 was always right and even ships the proof command. Six others forgot two words, so cargo unified reqwest's default-tls → hyper-tls → native-tls → openssl-sys across the graph, and §6c's "the ring provider is pinned process-wide, no OpenSSL" quietly stopped being true. All six date to b5a435c (2026-05-30): sloppy from birth, not degraded. A binary shipped to the live box the same morning died on libcrypto.so.3: version OPENSSL_3.3.0 not found — a C library nobody asked for, resolved against a host that had another.
MEASURED, loki, same worktree, before and after (cargo tree -p <p> -i <dep>):
| crate | before | after |
holger (the systemd binary) |
reaches native-tls AND openssl-sys |
no such package in the graph |
holger-bench-scenarios |
reaches openssl-sys |
gone |
holger-rust-znippy-repository — a repository backend, not a bench crate |
reaches openssl-sys |
gone |
What native-tls is left in the workspace comes through nornir (hf-hub, fantoccini — xtask's dev-dependency) and mannequin's cert-helper, which uses openssl on purpose. Neither is holger's reqwest.
znippy-rust also gained rustls-tls by name: its line asked for blocking + json and NO tls feature, so dropping the defaults would have left it with no provider rather than the rustls one it means.
3 · The serving path has a number again
8aa6159 (2026-05-22) benched handle_http2_request beside get_file; cd7636d (2026-05-31) kept only get_file. For the next four months nothing measured the route that answers a client — and that is the route stinky-01 measured as the slow one. Three arms, all NEW names (bench_runs keys history by metric name):
holger.rust_crate_http_st/holger.rust_crate_http_mt32— same corpus, same loop shape as the read twins. The difference between the twins IS the door.holger.rust_crate_put— N one-at-a-timeputs, fixed N (HOLGER_BENCH_PUTS), because a quadratic hides inside a time-boxed loop by simply completing fewer operations. Payloads are built before the clock starts. It carries an ANSWER floor: the last put is read back throughhandle_http2_request, so a write path that produces an archive the read path cannot serve fails the arm.
MEASURED, oden, quick scale (the run that also serves as the before for #4):
| arm | ops/s | MB/s |
rust_crate_st (get_file) |
420 738 | 250.81 |
rust_crate_http_st (the whole door) |
332 627 | 198.29 |
rust_crate_mt32 |
4 196 834 | 2 501.83 |
rust_crate_http_mt32 |
3 962 156 | 2 361.93 |
The door costs 21 % single-threaded and 5.6 % across 32 — which agrees with the archaeology lane's 19 % on loki, from a different corpus size on a different box. The parse is not the problem, and now there is a number that will say so out loud if it ever becomes one.
4 · HotArchive: a quadratic, stated as a rate
The writable dev-mode backend (/sparring, the /cache primary) did two things per operation that grew with the archive: put re-sealed the whole Arrow index through append_files, and every fetch/contains/list re-opened the file through ZnippyArchive::open. Neither needed inventing — znippy shipped HotArchive on 2026-08-04 for exactly this, and holger's backend (2026-06-19) simply predates it. znippy-common 0.9.14 was already in the lock.
MEASURED, oden, both binaries built and run back to back in the same minute:
| N puts | before, wall | before, puts/s | after, wall | after, puts/s | ratio |
| 500 | 0.361 s | 1 386.3 | 0.077 s | 6 467.2 | 4.7× |
| 2 000 | 3.405 s | 587.4 | 0.265 s | 7 554.4 | 12.8× |
| 8 000 | 42.148 s | 189.8 | 1.042 s | 7 673.9 | 40.4× |
Read the rate columns, not the ratio. Before, the rate FALLS — 1386 → 587 → 190 — which is the quadratic stated as a rate. After, it is flat and rising — 6467 → 7554 → 7674. The ratio therefore keeps growing with N, exactly as znippy's own table (oden 2026-08-04, 26.7× wall at 8 000 appends) says it must, because only one side of it grows.
Three things came out of doing it, and all three are in the commits:
- A second writer.
cargo publishwrites TWO entries per version — the.crateand a{name}-{version}.idx.jsonwith the real deps.putmoved to the handle;store_index_metawas still onappend_files, re-sealing the archive the handle was appending to. The SUITE caught it, not reasoning:sparse_index_serves_real_published_deps, "deps must NOT be empty". Moving one writer onto a held handle means finding every writer, because the ones left behind do not fail — they disagree. - A held handle still has to notice a swap. The old per-request
ZnippyArchive::openwas also what made a REPLACED archive fail the request (format_version_gate.rscopies a future-format archive over a live repo and requires a 503 naming the version, with the other repos still serving). So the handle stamps(len, mtime)as it leaves the file and compares on every lookup — onestat, which is what the old memoised gate cost — and a mismatch drops the handle and re-runs the gate. The 503 still happens, for the same reason, reached by noticing instead of by re-parsing. - One test assertion was restating the old design ("znippy's append writer records a format version"), which a hot archive does not: its journal is its index and it records nothing until
seal. Restated, not deleted, and it now asserts both halves — live ⇒ hot, sealed copy ⇒ a version this build supports.
Known limit, stated: a swapped-in archive this build CAN read is not adopted; the journal this process holds still describes the repo until it restarts. Replacing a live writable archive with a good one was never supported; replacing it with an unreadable one is refused, which is the half that mattered.
Read arms in the same run, unchanged as they must be — the static read path was never touched: rust_crate_st 420 738 → 471 932, rust_crate_http_st 332 627 → 372 451 ops/s. Shared box, not a claim. The swap detector's stat costs ~3 % of the put rate (7 554 → 7 242 at N=2 000) and is worth it.
1 · The Bytes migration, executed
81d8e24 wrote it on 2026-06-28; be80f18 deleted it on 2026-06-30 in a "prune dated/scratch notes" pass, unexecuted and unrejected — swept up because its name had a date in it. Restored verbatim (fc671dc) and then carried out (49b9374): 50 signatures, bytes named once in traits/Cargo.toml and re-exported as traits::Bytes so no repository manifest moved, one Bytes::from per backend at the point its extractor hands over.
The plan assumed one thing the tree did not have. It says the unary gRPC field "is already prost::bytes::Bytes" — only ArtifactChunk.data was. Left alone, the seam would have BOUGHT a copy rather than removed one: a Bytes from the archive to_vec()'d back into the response. cargo xtask gen-proto now declares FetchArtifactResponse.data and PutArtifactRequest.data as bytes too.
MEASURED, oden, both binaries back to back on a busy box (loadavg ~48):
| arm | before | after |
rust_crate_st |
443 405 ops/s | 439 893 |
rust_crate_http_st |
342 943 ops/s | 350 811 |
rust_crate_mt32 |
3 549 568 ops/s | 3 577 768 |
rust_crate_http_mt32 |
3 260 308 ops/s | 3 255 644 |
Flat, and that is the honest result. These four arms drive get_file/handle_http2_request in process, where the buffer was already moved rather than cloned. The hops this seam pays off on — the proxy's cache-and-forward, replication, and the gRPC unary and stream doors — have no bench arm in holger at all. Naming that is worth more than quoting a number from the wrong workload.
What is left
| item | state |
the plan's phase 7 — handle_http2_request body/return and ConnectorTrait::download_asset |
open. The plan itself files them as "not on the fetch hot path"; they are the whole-body collect stinky-01 §2.2 measured (sha256 at 0.61 GB/s = 1.75 s of blocked thread per GiB) |
| a bench arm on the proxy / replication / gRPC doors | open, and now the blocking gap — the Bytes seam cannot be shown to pay without one |
| gatling in the server (stinky-01 §1.1, §3c) | untouched. The engine is linked twice and called zero times from the server; hashing and the fan-out loops are still one thread |
the two holger-rust-znippy-repository 0.6.10 in the lock (archaeology §3d) |
untouched. Two bodies of code answer to one version string; it makes measurements unreproducible and should be resolved |
HotArchive for the other writable znippy backends |
untouched. Only the rust one was named and only it was changed |
| holger's CI | still cargo audit alone — no cargo build, no cargo test. Nothing but a person will catch the next regression |