Repository navigation
libuvcpp: subscribe the eight profiles the entry was missing - #1553
Conversation
…rver
Subscribes the entry to gateway-64, gateway-h3 and production-stack, which are
the three profiles the entry was missing that are orchestrated rather than
run as a single container. All three put a stock, unmodified caddy:2-alpine at
the edge -- the gateway rules exclude custom proxy implementations, so there is
no hand-written proxy anywhere in this entry, only a Caddyfile.
What the compose stacks need from the server, and what main.cc now answers:
* gateway-* forward /baseline2, /json/{n} and /async-db over plain h1 to
127.0.0.1:8080. /baseline2 was registered only on the 8443 listener before
this, so the gateway hop would have 404'd; it is now on h1 as well.
* production-stack forwards /public/* unauthenticated and /api/* only after
the shared authsvc sidecar has verified the bearer token and handed back
X-User-Id. That is /public/baseline, /public/json/{count}, /api/items/{id}
(GET and POST) and /api/me, all new.
Cache-aside is in-process, which the production-stack implementation guide
permits explicitly ("in-process only: no Redis for caching, just a local
hashmap with TTL"). Item entries live 1 s and user entries 30 s, both the
profile's ceilings. The map is process-global and sharded 64 ways rather than
one map per worker loop: the MISS-then-HIT the profile checks is two separate
connections, and a per-loop map would give each its own empty cache. The
`cache` service the profile requires is still in compose.production-stack.yml
and `server` still waits on it with `condition: service_healthy`, as required.
The server gets no /certs in any of the three compose files. main.cc opens its
8081 and 8443 listeners only when the certificate pair is readable, so without
it the server comes up on 8080 alone and leaves every TLS port to the edge.
The server also refuses /api/* outright when X-User-Id is absent, so an edge
that failed to authenticate cannot silently forward into private rows -- the
401 wall is checked as a status, on both the GET and the POST path.
Measured locally, no docker: the same statements the harness runs, against the
real server and a real Caddy 2.6.2.
gateway-64 / gateway-h3 shape, curl --http2 against https://localhost:8443:
HTTP/2 negotiation -> 2
/static/reset.css Content-Type -> text/css; charset=utf-8
/static/app.js Content-Type -> text/javascript; charset=utf-8
/static/app.js size (br) -> 47,275 bytes
/static/nonexistent.txt -> 404
/json/50 -> count 50, full schema, totals correct
/async-db?min=10&max=50 -> count 50; empty range -> 0
/baseline2?a=13&b=42 -> 55; randomized pair matched a+b
production-stack shape, same edge config plus forward_auth:
/public/baseline?a=13&b=42 -> 55; randomized pair matched a+b
/public/json/25 -> count 25, full schema, totals correct
GET /api/items/1 no token -> 401
GET /api/items/1 bad token -> 401
POST /api/items/1 no token -> 401
POST /api/items/1 bad token -> 401
GET /api/items/1 with token -> id 1
GET /api/items/7 twice -> X-Cache: MISS then HIT
POST /api/items/2 with token -> 204
warm /api/items/3, POST it, GET -> X-Cache: MISS
GET /api/me with token -> id 42
Postgres seeded from data/pgdb-seed.sql (100,000 items, 4 users).
Both Caddyfiles pass `caddy validate --adapter caddyfile`; the only error
either reports is the absent /certs, which exists inside the containers.
The stack runs on host networking, so the edge's upstream hop is loopback --
HTTP/1.1 with a keepalive pool rather than h2, so the many h2 streams arriving
from the client fan out across the server's per-core loops instead of being
re-serialized behind one connection's flow control. cpusets split all 64
logical CPUs in physical-core pairs: 0-15,64-79 / 16-31,80-95 for the two
gateway stacks, and 0-14,64-78 / 15,79 / 16-19,80-83 / 20-31,84-95 for the four
production-stack services.
… service
Subscribes the entry to baseline-h2c, json-h2c, unary-grpc and unary-grpc-tls --
the last four profiles it was missing that are plain request/response over a
protocol rather than an orchestrated stack.
Two new ways in, and one route that serves both gRPC profiles:
* 8082/tcp is the platform's dedicated h2c port (scripts/validate.sh sets
H2C_PORT=8082), so baseline-h2c and json-h2c get a listener of their own and
never share a port with anything. Both routes already existed elsewhere --
/baseline2 and /json/{n} are the same handlers, h2load is explicit about the
protocol, so a server that quietly answered HTTP/1.1 here would be caught
rather than scored.
* unary-grpc names http://localhost:8080, the h1 port. That port now splits on
the first 24 bytes of each connection: the RFC 9113 preface is byte-exact, so
this is a decision, not a guess, and it settles once per connection -- a
connection that starts with anything else is HTTP/1.1 for the rest of its
life and pays nothing further. set_h2c_enabled(true) is the documented switch
for it (uvcpp_http_server.h:377).
* /benchmark.BenchmarkService/GetSum is registered once on the h2c app and once
on the 8443 h2 app. A route is one row in the router regardless of the
connection it arrives on, so one handler answers both profiles.
The handler is the framework's, not the entry's:
grpc_parse_unary_body() -> unwraps the 5-byte length-prefixed frame
grpc_field_get_int32() -> walks the protobuf message
grpc_field_put_int32() -> builds the reply
grpc_web_send_unary() -> HEADERS, DATA, trailers, in that order
No framing, no varint and no trailer construction in main.cc -- the gRPC module
is the library's documented high-level API for this, and hand-rolling the encoder
here would be exactly the "build a faster path around the framework" the
standard-mode rules forbid. proto3 omits a zero-valued field and
grpc_field_get_int32() answers UV_ENOENT for an absent one, which is the default
and not an error, so the two defaulted locals are the right reading of the wire
format rather than a shortcut for the benchmark. A malformed body never reaches
the handler: the parser returns the status it decided on (INVALID_ARGUMENT for a
broken frame, RESOURCE_EXHAUSTED for an oversized one) and the sentence for
grpc-message, and the reply is Trailers-Only.
Measured locally, no docker, against the real server built from the library tree
that became 0528b4a (the rig's main.cc differs from this one only in four path
constants). The server was confirmed to hold 8080/8082/8443 itself before any of
this ran (port holder == recorded pid, and /proc/<pid>/maps showed the rig's own
libuvcpp.so -- the check that the earlier rig was missing):
curl --http2-prior-knowledge http://localhost:8082/baseline2?a=13&b=42
-> HTTP/2, 200, body 55
curl --http2-prior-knowledge http://localhost:8080/baseline2?a=13&b=42
-> HTTP/2, 200, body 55
curl --http1.1 http://localhost:8080/baseline11?a=13&b=42
-> HTTP/1.1, 200, body 55 (unchanged)
GetSum over h2c, frame 00 00000004 08 0d 10 2a (a=13, b=42)
-> HTTP/2 200, content-type: application/grpc, data frame
00 00000002 08 37, i.e. SumReply.result == 55 == 0x37
h2load -p h2c on that same URL, 5 s windows at -c 1 -m 1, -c 64 -m 1 and
-c 256 -m 100
-> 0 failed, 0 errored, 0 non-2xx in all six cells
The rc of the trailing HEADERS block is asserted where it can be asserted
exactly -- tests/functional/web_h2_grpc_func.cpp requires grpc-status to be
*absent* from the first block and == 0 in the end block -- rather than by
reading curl output, which does not print HTTP/2 trailers.
meta.json subscribes the four profiles; the entry now declares 26 tests.
The entry builds `0528b4a6` now instead of `3fdd6208` (`v1.6.1`), and subscribes
`static-h3` -- the last of the eight profiles it was missing. Nothing else about
the entry changes shape: the same five listeners, the same routes.
**The pin has to move, because four of the things these profiles need are in no
release.** `1c75d12` (cleartext h2), `c79ec95` (the h3 deferred reply),
`09237aa`..`e4fff33` (the gRPC module and its h2 trailer) and `745608c` (the
QUIC short-header CID fix) all landed after `v1.6.4`. The CID fix is the one the
h3 profile was actually scored on: on the platform, baseline-h3 answered 294
responses in its 5 s window -- 4.6 per connection, i.e. one initial PTO per
request -- while the same box served 3.37M rps over h2. The server was decoding
a short-header DCID with the *peer's* SCID length, so after the handshake every
1-RTT packet was read as an unknown CID and dropped. Also new in the pin and
used here: `4aab9e5`, the QUIC fanout, which is what `h3.set_loops(loops)` asks
for -- one socket per loop under SO_REUSEPORT -- and `23c21ed`'s tests for it.
**main.cc** gains the static handler on the h3 leg:
uvcpp_static_server h3_static(kStaticRoot, "/static");
h3_static.set_spa_fallback(false);
...
h3.on_request(h3_static.handler(&h3));
h3.set_loops(loops);
`on_request()` is the no-route-match handler, so `/baseline2` above it still
wins and everything else is looked up on disk -- which is what the profile asks
for. Three details are load-bearing. The handler is declared *before* the server
because it captures `this` and the destructor order of locals is the reverse of
the declaration's. `set_spa_fallback(false)` matches the h1/h2 legs:
`uvcpp_static_server` defaults it to true and `uvcpp_web_static_options` to
false, so left on, a miss would be `index.html` with a 200 on this leg and a 404
on the other two. And `set_loops()` has to come before `listen_quic()` -- it
answers UV_EBUSY once the endpoint exists -- which is why it sits above the call
rather than after it.
**meta.json** subscribes `static-h3` (26 tests) and its description now names
the five listeners. **README.md** gets the listener table, the `/static` row on
the h3 leg and the `/benchmark.BenchmarkService/GetSum` endpoint, and its zlib
paragraph now matches the Dockerfile. **Dockerfile**: the pin, the rationale
above, and `zlib1g-dev` dropped -- the install rules ship `include/zlib.h` and
`include/zconf.h` as of 1.6.3, which is the thing that package was compensating
for at the old pin.
Measured locally (2026-10-13), no docker: the entry built from this exact pin
with the Dockerfile's own flags and its own `g++ -std=c++11 -O2 -DNDEBUG` line,
running against the platform's own h2load (nghttp2 v1.69.0 with
`--enable-http3`), a stock Caddy 2 edge, and a Postgres seeded with the
profile's 100,000 rows. The rig's main.cc differs from this one in four path
constants only. Every load run is `-D 5s`:
check_all.sh -- the 15 h2c / json-h2c / gRPC / h1 assertions, including the
truncated-frame path (grpc-status: 3)
-> PASS=15 FAIL=0
baseline-h2 -c 64 -m 100 -t 64 -> 1,328,380 req/s, 6,641,900 2xx, 0 4xx, 0 5xx
baseline-h3 -c 64 -m 64 -t 64 --alpn-list=h3
-> 392,231 req/s, 1,961,339 2xx, 0 4xx, 0 5xx
that is 30,646 responses per connection where the platform measured 4.6
static-h2 -c 64 -m 32 -t 64 -> 22,620 req/s, 113,102 2xx, 0 failed
static-h3 -c 64 -m 64 -t 64 --alpn-list=h3
-> 5,189 req/s, 26,015 2xx, 0 4xx, 0 5xx -- a profile no release could serve
check_gw.sh -- _validate_gateway's 12 assertions through stock Caddy
-> PASS=12 FAIL=0; gateway-64 25,181 req/s, 125,960 2xx, 0 4xx, 0 5xx
gateway-h3 -c 64 -m 32 --alpn-list=h3, the same 20-URI mix
-> 14,156 req/s, 72,476 2xx, 0 4xx, 0 5xx
check_ps.sh -- _validate_production_stack's 15 assertions, including
cache-aside MISS then HIT and the POST-invalidation MISS
-> PASS=15 FAIL=0
Two of the same build's numbers for context, since both are honest about where
the remaining room is: h3 is 0.30 of h2 on the baseline shape (392k / 1,328k)
and 0.23 on static (5.2k / 22.6k), and `g++ -H` confirms `<zlib.h>` still
resolves out of the install prefix with `zlib1g-dev` gone.
The dataset ships a `.br` and a `.gz` beside 15 of its 20 static assets, and
every static profile sends `Accept-Encoding: br;q=1, gzip;q=0.8`. The entry was
ignoring them: each response was deflated live on the server. Now all three
static legs serve the shipped twin -- `uvcpp_web_static_options::precompressed`
on the two `uvcpp_web_app` legs (:8081 and :8443/tcp), and
`uvcpp_static_server::set_precompressed()` on the h3 leg, which is a bare
`uvcpp_http_server` and has no options struct to pass. Both servers run the same
selection rule, so the twin a request gets does not depend on which leg answers.
**It is the h3 leg that makes this worth a pin move.** `uvcpp_static_server` had
no compressed-variant cache at all -- `uvcpp_http_server`'s variant cache is
keyed on `resp.cache_tag`, which only `uvcpp_web_static` ever sets -- so it ran
zlib over the whole file on the loop thread once per request. Against the
entry's own `data/static` (20 files, 8 loops, `h2load -p http/1.1 -c 64 -m 2 -n
8000`, 20-URI rotation, three reps):
leg off on bytes / 8000 req
uvcpp_static_server 933.15 rps 82,451 rps 173,843,446 -> 131,194,904
uvcpp_web_app::serve_static 110,773 rps 185,280 rps 172,797,487 -> 131,865,720
88x on the h3 leg, +67% on the other two; 24% fewer bytes on both. The
`uvcpp_web_app` legs already cached their deflated variant, so they only see the
byte saving -- which is why the two multipliers differ so much. Note that "off"
is *not* the originals: it is the server's live gzip, and it costs more bytes
per round (426 KB) than the shipped `.gz` twins do (363,783 B).
The byte column is filesystem-independent; the rps column is not, and that is
worth recording. The dev box's copy of the dataset is on an ntfs-3g (FUSE)
mount where one `stat()` costs 45.9 us, against 1.35 us on ext4 -- 34x. Turning
the option on adds one `stat()` per request to probe the twin candidate, so on
ntfs-3g that probe is the entire bottleneck and the h3 leg measures *slower*
than off (32,448 rps). The numbers above are from an ext4 copy of the same 50
files. The platform mounts the dataset into the container, so the ext4 numbers
are the representative ones.
This is also what every other entry in the arena does -- nginx `gzip_static`,
Caddy `precompressed br gzip` (which this entry's own edge Caddyfiles already
ask for), actix `try_compressed()`. The entry neither produces nor caches those
files: it turns on a documented framework option and points it at the dataset as
mounted. A file with no twin, a request with no `Accept-Encoding`, and a `Range`
request all take the live path unchanged.
**The pin moves to `8cce146a` (`1.6.18-dev`)** because `precompressed` on
`uvcpp_static_server` is in no release. Everything the previous pin carried is
still in it; the Dockerfile comment gains the static family and the new
paragraph above.
Validated locally, bare-metal, against a library built from the same tree with
the Dockerfile's own flags (`_local_deps` standing in for the container):
- the entry compiles and starts; all five listeners come up
- all 20 files on :8081 (h1 over TLS) and :8443 (h2): 200, `content-encoding:
br` exactly when a twin exists, and the body byte-identical to `x.br` (or to
the original when there is no twin)
- the same 20 on :8443/udp (h3): 40 requests each, 0 non-2xx, bytes-per-request
matching the br twin to within h2load's constant per-request frame overhead
- `/static/app.js` with no `Accept-Encoding` returns the 204,800-byte original
with no `content-encoding`
- `scripts/validate-frag.py 127.0.0.1 8080` 9/9, `scripts/validate-ws.py` 7/7
The pin moves from `8cce146a` (`1.6.18-dev`) to `2d5fe74` (`1.6.20-dev`).
Nothing about the entry changes -- same five listeners, same routes, same
options. What changes is the library underneath, and two of the commits in
between are defect fixes on a path this entry takes.
**`reply_alive()` dereferenced the connection pointer it was handed.**
`uvcpp_http_server::reply_alive(const uvcpp_http_reply&)` exists precisely to be
called *after* a connection may already be gone: `uvcpp_static_server` --
the h3 leg's static handler, and the handler behind every `static-*` cache miss
-- asks it from the thread-pool completion before writing (`uvcpp_static_server
.cpp:394, 503, 532, 578`). It used to resolve the caller by reading
`client->loop_index()` and then looking up that loop's registration table. With
one loop there is a fast path that skips the read; with `set_loops(n > 1)` --
which this entry calls on all five listeners -- it is a read of freed memory.
Measured on Linux under ASan (`-fsanitize=address,undefined`,
RelWithDebInfo), 5/5 runs, from `tests/functional/web_static_server_func.cpp`'s
`multiloop_concurrency`:
heap-buffer-overflow ... READ of size 8
#0 ctxs_of(uvcpp_tcp_client*) const src/web/uvcpp_http_server.cpp:455
MDA2AV#2 connection_generation(...) src/web/uvcpp_http_server.cpp:2516
and on Windows the same code is what `CI · Windows · MinGW-w64` had been
reporting as `test_web_static_server_func ....***Exception: SegFault` -- the
Windows heap returns freed pages to the OS, so the same read is an access
violation there, while Linux and macOS keep the pages mapped and only a
sanitizer sees it.
The fix (`ebf988e`) replaces that lookup with a small registry written in
lockstep with the tables at the four register/erase sites, so the answer costs
no table access and no dereference at all; `reply_alive()`'s h1 branch now picks
its table from the credential's own loop index, the way the h3 branch always
did. The same commit adds a probe with a deliberately unmapped address
(`(uvcpp_tcp_client*)1`), so the contract fails loudly on plain CI builds too,
and fixes a second, pre-existing out-of-bounds read: both 413 sites passed a
hand-written length of 23 for the 21-character `"413 Payload Too Large"`
literal.
**`uvcpp_loop_index_of_this_thread()` was not exported.** `e3e033e` gives it and
its setter `UVCPP_API`; it is declared in an installed public header
(`src/net/uvcpp_loop_worker.h`), so on a Windows DLL build an external TU could
not link it -- which is why the library's own MSVC `http3` job had never built.
No effect on this entry, which does not call it.
Both of those left `CI · Windows · MSVC` red, and so did a third thing, which is
why the pin is the commit it is: the library's own `quic_stream_func` asserted
"20 serial QUIC echo round trips in under 500 ms" -- a budget calibrated on
Linux, where the 20 round trips take 43 ms. On the Windows runner they take
505-508 ms, because that budget is really the runner's timer granularity: each
round trip waits on `wait_until()`'s `sleep_for(1ms)`, which is ~1.06 ms here
and ~15.6 ms there. `2d5fe74` derives the budget from the granularity the
process measures for itself instead of writing a number down. It touches that
one test file and nothing else -- `git diff e3e033e0 2d5fe74 -- src/` is empty
-- so the library this Dockerfile builds is byte-for-byte the one `e3e033e0`
builds, and the two defect fixes above are the only behaviour changes that reach
this entry.
Neither fix changes behaviour on any input this entry produces: the registry
answers exactly what the table lookup used to, the 413 length only stops the
overread, and the test budget does not exist in the built library at all. All of
it was verified before the pin moved.
Local pre-validation of the entry against this pin (bare-metal copy of the
Dockerfile's flags, no Docker on this machine; `_local_deps` OpenSSL 3.5,
pgsql-dev, the dataset and certs mounted as the compose files do):
- builds clean, both rig binaries link against the new library;
- `check_all.sh` (h2c 8082, json-h2c, unary-grpc on 8080, the h1 regression and
the bad-frame boundary case): **PASS=15 FAIL=0**;
- `check_gw.sh` (gateway-64 through Caddy): **PASS=12 FAIL=0**;
- `check_ps.sh` (production-stack, edge + cache + authsvc): **PASS=15 FAIL=0**;
- `h2load` readings on the same profiles: `baseline-h2` 1,307,140 req/s
(6,535,700 requests, 0 failed, 0 5xx), `baseline-h3` 370,148 req/s
(1,850,743 requests, 0 5xx), `static-h2` 7,671 req/s (38,356 requests, all
2xx), `gateway-64` 24,805 req/s through Caddy (124,027 requests, 0 failed,
0 5xx).
The library's four CI workflows are green on `2d5fe74` -- every job `success`,
in runs 38331861230 (Linux·Ubuntu, 14 jobs), 38331861232 (macOS, 10),
38331861255 (Windows·MSVC, 11, including the `quic` and `http3` jobs the budget
above had been failing) and 38331861404 (Windows·MinGW-w64, 1) -- and its
doc/test gates (`check_doc_lines`, `check_doc_versions`, `check_docs`,
`check_doc_snippets`, `check_config_contract`, `check_capi_symbols`,
`check_ci_layout`) are green locally.
That is the A/B the tip is here for: the same four workflows on `e3e033e0` have
the same two MSVC legs red on this one assertion, with `done=20/20` and
`checks=94 failures=1`.
The pool was opened exactly once, before the listeners come up, and a
failure there was permanent: `g_db_ready` stayed false and every
database route answered its documented empty shape for the rest of the
run. The harness starts Postgres and then the entry, but the gate it
waits on runs *inside* the sidecar container (`docker exec ... pg_isready`
+ `psql -tAc`, validate.sh:281-291), so it talks to the Unix socket --
and the image's entrypoint runs initdb against a temporary server with
`listen_addresses` empty before it starts the one that listens on 5432.
Starting in that window is normal, and it landed on a healthy sidecar.
Measured on the 2026-10-13 `validate-gateway` run (38345997274): one
`cannot open the Postgres pool: ... port 5432 failed: Connection refused`
on stderr of `httparena-validate-libuvcpp` (`status=running exit=0`), and
the three production-stack assertions that need the database failing --
`/api/items/1` returned `{}` instead of `id=1`, the cache-aside MISS on
both reads, `/api/me` 503 -- while `/static/*`, `/public/*`, the 401s and
`POST -> 204` all passed. The standalone container keeps the host's 8080
and 8443 for the whole run, so the compose edge proxies the database
requests back into it: the same one lost race also explains the failing
`/async-db` result-set checks (gateway-64, gateway-h3, and the four
random parameter sets the standalone check tries).
Now the first attempt is unchanged, and a failure leaves a retry thread
behind: every 250 ms for the first 10 s, then every 2 s, for 120 s, then
it gives up with a line naming the budget. `init()` after a failed
`init()` is the pool's documented way back ("修好问题可以再 init 一次",
db/uvcpp_db_pool.h:103), and `g_db_ready` is now atomic because two
threads write it. The thread is joined on the way out of main, before
`g_db` -- a static -- is destroyed.
Criterion: with Postgres down when the process starts and started later,
`/async-db` stops answering the empty shape without restarting the
server. Measured on the dev box (compiled with the Dockerfile's exact
g++ line against the 1.6.20-dev prefix; `DATABASE_URL` at a stopped
port 55432):
* Postgres down, server up 3 s: stderr carries `cannot open the
Postgres pool: ... Connection refused` and the retry line, the
listener is up, `/async-db` answers `{"items":[],"count":0}`.
* Postgres started 15 s later (past the point where the interval
widens): `Postgres pool up on retry 46 (22000 ms after startup)`, and
`/async-db?min=10&max=50` answers `{"count":50,...}` with real rows
-- recovery in place was the point; nothing was restarted.
* Postgres never started: `giving up on the Postgres pool after 95
attempts / 120 s`, no further attempts in the log over the next 10 s,
and `/baseline11?a=1&b=2` still answers 3.
* Postgres already up: `Postgres pool up (max=8)` on the first attempt
and no retry line at all -- the benchmark path spends nothing on
this.
|
Quick note on this one: it's still in development. The new profiles it Opening it now so the compose stacks actually get exercised by CI; I'll |
The three compose stacks all exited at startup and the edge proxied to whatever else held their port. main.cc binds its cleartext listeners at boot and returns 1 if a bind fails, the stacks run on `network_mode: host`, and the harness starts an instance of this same image as a plain container for the isolated profiles -- `--network host` too, and left running for the whole validation run, gateway phases included. So the compose `server` lost 8080 and 8082 to that container before it ever answered, exited, and left Caddy's upstream hop landing on the harness's own container instead: a healthy backend replaced by one whose Postgres pool had failed, which is where the platform's /async-db, /api/items/1, cache-aside and /api/me failures came from. Reproduced locally without docker, by holding the two ports with a throwaway listener and running the entry against the same dataset: $ python3 decoy.py & # 0.0.0.0:8080 and 0.0.0.0:8082 $ ./server-gwtest # no H1_PORT / H2C_PORT libuvcpp: Postgres pool up (max=256) libuvcpp: cannot listen on 8080 (rc=-98) # -98 is UV_EADDRINUSE exit=1 which is also why the CI log never showed the error: validate.sh dumps the logs of the container it started itself, never the compose project's. `H1_PORT` / `H2C_PORT` now move the two cleartext listeners, and the three compose files set them to 8085 / 8086 with both Caddyfiles' upstreams moved to match. Unset, the entry binds 8080 / 8082 exactly as before -- every isolated profile and the benchmark's own `docker run` are untouched, and the benchmark never starts a standalone container alongside a compose stack, so nothing measured changes. Moving the backend rather than making a busy port non-fatal keeps a real bind failure loud. 8443 is deliberately not overridable: it is the edge's port in both stacks, and the compose `server` gets no /certs, so its TLS listeners never open anyway. $ H1_PORT=8085 H2C_PORT=8086 ./server-gwtest & # decoy still holding both libuvcpp: Postgres pool up (max=256) $ curl -s 'http://127.0.0.1:8085/public/baseline?a=13&b=42' 55 $ curl -s -o /dev/null -w '%{http_code}' -H 'X-User-Id: 42' \ http://127.0.0.1:8085/api/items/1 200 A TCP backend port is what the gateway profile asks for ("listens on an internal port (e.g. 8080)"); the port number is the entry's choice, and the profile's two-service, no-custom-proxy, static-on-the-edge rules are unchanged.
The gateway-64 checks that failed on the platform were answered by this
entry's own 8443 h2 listener rather than by Caddy. The edge binds
[::1]:8443 (`https://localhost:8443`) and this process binds
0.0.0.0:8443, so curl takes ::1 whenever the edge is already listening
there and falls back to 127.0.0.1 while a freshly built edge image is
still starting -- and the checks run inside that window: the proxy
container reported Started at 04:23:27.190 and all twelve checks were
over by 27.554.
The log says which of the two answered. `PASS [gateway /static/app.js
Content-Type] (application/javascript)` is this server's MIME; the same
check in the gateway-h3 phase, where the edge was up in time, reads
`text/javascript` -- Caddy's. And the two failures are the exact shape of
a 404 from a listener that has no such route:
FAIL [gateway /json]: count=0, schema=False, correct=False
FAIL [gateway /async-db]: count=0, rating=False, tags=False, active=False
8443 answered /baseline2 and /static and nothing else, so /json/50 and
/async-db fell through to a JSON 404 body that reads as an empty result
set. /json/{n} and /async-db are this server's endpoints whichever port
they are asked on; a listener that answers half of them is a listener
that 404s a request the deployment is supposed to serve.
Now served on 8443 as well: /json/{count}, /async-db, /public/baseline,
/public/json/{count}. /api/* stays off it deliberately -- those routes
need the edge's verified X-User-Id and the profile forbids the framework
from doing the JWT verification itself, so exposing them here could only
ever answer 401.
Measured against the entry's own gateway check bodies, run verbatim
(isolated from curl's ::1 preference by addressing 127.0.0.1 directly),
with 8080 and 8082 held by a decoy listener to reproduce the harness's
layout:
$ H1_PORT=8085 H2C_PORT=8086 ./server-final &
JSON : count=50 schema=True totals=True CT=application/json; charset=utf-8
ASYNC : count=50 rating=True tags=True active=True CT=application/json; charset=utf-8
EMPTY : count=0
STATIC: /static/reset.css text/css; charset=utf-8 8192 bytes
STATIC: /static/app.js application/javascript; charset=utf-8 204800 bytes
MISS : 404
BASE : /baseline2?a=13&b=42 -> 55
BASE : /baseline2?a=823&b=594 -> 1417
all twelve PASS conditions the harness checks in the gateway phase.
The previous commit moved the compose backend to 8085/8086 so its binds would
stop failing during a mixed entry's validation run. That is right for
validate.sh, but it leaves the benchmark with nothing on 8080, and
framework_wait_ready() has no gateway case: the three compose profiles fall
through to `http://localhost:8080/baseline11?a=1&b=1` (scripts/lib/framework.sh,
PORT=8080 from scripts/lib/common.sh), so benchmark.sh:243 would print "did not
come up for $profile; skipping" and publish no numbers for gateway-64,
gateway-h3 or production-stack at all.
The stack now also opens 8080, from H1_PROBE_PORT, best-effort: the 8080 leg's
own /baseline11 handler on a single worker loop. Best-effort because the two
runs disagree about who owns that port -- a validation run keeps the harness's
own container on it for its whole length, so there the bind fails, one line is
logged, and the stack serves the edge on 8085 exactly as the platform's last
validate-gateway run saw it pass. Nothing in any profile measures this
listener.
Measured on the bare-metal rig (no docker), compose-mode server with
H1_PORT=8085 H2C_PORT=8086 H1_PROBE_PORT=8080, no certs mounted:
- listening sockets: 8080, 8085, 8086 -- and nothing else
- `curl -sk -o /dev/null --max-time 2 http://localhost:8080/baseline11?a=1&b=1`
(the probe verbatim) exits 0; ?a=1&b=2 answers 3
- with a decoy holding 0.0.0.0:8080, i.e. the validation case: the process
stays up, logs ":8080 is not free for the readiness probe -- continuing
without it", and 8085 still answers /baseline2 with 200
- stock Caddy on the repo's proxy/Caddyfile (upstream 8085) in front:
check_gw.sh (the 12 assertions of validate.sh's _validate_gateway) PASS=12
FAIL=0, and the gateway-64 URI mix under `h2load -c 64 -m 32 -t 16 -D 3s`:
54365 done, 0 failed, 0 5xx
- production-stack's 15 assertions (check_ps.sh, shared authsvc + Caddy):
PASS=15 FAIL=0
- no environment at all (every isolated profile): listeners are 8080/8081/
8082/8443 and no probe listener is opened
|
/benchmark -f libuvcpp --save |
|
👋 Benchmark request received. A collaborator will review and approve the run. |
Benchmark ResultsFramework:
Full log |
The handler deep-copied every dataset item into a second DOM before dumping the wrapper, so a 50-item response re-allocated the whole nested item (rating object and tags array included) 50 times and then serialized a tree that existed only to carry one derived field. It now dumps the dataset item itself and splices `,"total":N` in ahead of the closing brace: the same nlohmann serializer, run per request over the live dataset, minus the deep copy and the throwaway DOM. Nothing is pre-rendered and no response is cached -- the profile rules forbid pre-computed / pre-serialized response caches "on either type", so a variant that rendered each item's bytes once at startup was written, measured and rejected: it is 54x rather than 1.7x, and it would have made the README's "nothing is pre-serialized or cached" false. Criterion: the served body must parse-equal the old one for the json-tls and json-comp round-robin pairs (25,4) (40,8) (50,6), the validator's pairs (1,1) (7,2) (23,11), and the edges count=0 / 50 / 51 / 100 with m=0; and the validator's own invariants must hold on the new path (count == route count, full item schema, integer total == price*quantity*m). Both hold. The served body is byte-count-identical (8397) with total last rather than in sorted position, which is the same JSON either way. Measured on /json/50?m=6, `h2load -c 64 -m 16 -t 4 -D 5s`, server pinned to 6 cores, 3 interleaved rounds, each instance identified by its own body before it was measured (a failed bind otherwise silently measures the other one): h2c 36.0k -> 72.5k rps, h1 35.7k -> 76.0k rps, +113% on both. Isolated serialization cost 57.4 -> 33.3 us per 50-item response (1.73x).
The pin was 2d5fe74, which predates `fix(web): ws 发送队列整批合并` (Antruly/libuvcpp 219b0da), and a round has already measured that absence: `echo-ws-pipeline` came back at 4.30M / 3.94M / 3.81M req/s (512 / 4096 / 16384) against 10.85M / 12.63M / 10.63M in the round before, while the single-depth `echo-ws` cases sat still at 2.79-2.89M -- the send path, not the frame path. The commit batches a whole read-dispatch's send queue into one `uv_write` and defers the pump until the dispatch is drained. Criterion, measured locally on both sides of the change: segments per frame on a 1500-byte veth, 8 connections x depth 16, counted from each side's /proc/net/snmp OutSegs -- 1.0000 before, 0.0625 after; end to end 524k-859k frames/s before, 4,111k-5,215k after, with a v1.6.1 control arm at 0.71 segments/frame and 1,014k frames/s. 219b0da is the only commit between the old pin and the new one.
|
/benchmark -f libuvcpp --save |
|
👋 Benchmark request received. A collaborator will review and approve the run. |
The handler no longer dumps each item with nlohmann and splices `total` in
ahead of the closing brace. It walks the live dataset through
`uvcpp_json_writer` -- libuvcpp's documented streaming constructor, the one
its `doc/json-guide.md` describes -- and computes `total` inside the loop
from the values the writer is reading. Same items, same DOM, same work per
request; what changes is how the bytes get appended on the way out. The
library commit this pins (`perf(json): 写出器的键与字符串值改成一次追加`,
c8950ae) builds a whole key in one `append` instead of four or five, which on
a 50-item response is 4073 appends down to 1536, byte-identical.
Criterion, measured on this entry before writing this commit:
server CPU per request, `/json/50?m=6`, server pinned to 6 single-threaded
cores and saturated at 100% of them, best-of-3 interleaved rounds:
plain 80.4 -> 63.2 us (a repeat run: 73.8 -> 59.2)
gzip, br 155.0 -> 141.1 us
end to end, h2load, best of 3:
h2c 73.7k -> 94.2k req/s
h1 81.0k -> 96.3k req/s
against `build_json_general` -- each item copied into a second DOM and the
payload dumped, the shape af78de0 replaced and this file still keeps as the
fallback for a value the writer cannot reproduce byte-for-byte:
176.0 -> 59.2 us
The bytes do not move: the body was compared against one captured from the
committed build, over h1 and h2c, and again after gzip. The harness's own
json rules were re-run locally against this build -- 4 random
(count, multiplier) pairs checked field by field against data/dataset.json,
Content-Type, and the gzip leg decompressed and compared -- all passing on
both the h1 and the h2c port.
The pin moves from 219b0da to c8950ae, which is the commit this handler was
rewritten onto.
Benchmark ResultsFramework:
Full log |
…uest
`/json/{count}` serialized out of the live nlohmann DOM: for each item, eight
`std::map` node visits to emit the fields plus two more `value()` lookups for
`price`/`quantity` -- per item, on every request. The dataset is fixed at
startup, so that walk is the same 50 trees every time.
`build_flat_dataset()` now reads it into one contiguous array (`flat_item`, a
plain scalar per field, plus one character pool), and the handler serializes out
of that. The serialized bytes do not change.
Measured, 9 interleaved rounds in one process, median of the per-round ratios
(the ratio cancels this host's frequency drift, which is worth ~30% between two
runs of the same binary), three independent repeats:
serialization of /json/50?m=6 31.0 -> 23.4 us ratios 0.751/0.769/0.793
and end to end, server CPU per request at the profile's shape (`/json/50?m=6`,
-c 64 -m 16, 11 paired rounds, two runs): 0.883 and 0.893 -- 10.8-11.7% less.
The two numbers differ because the in-process one isolates the step and the
end-to-end one is end to end; both are reported rather than the flattering one.
Deterministic criterion, not a reading: the flat form is only enabled after
`build_flat_dataset()` has serialized every item both ways and found them byte
for byte equal at m = 1, 7, 1000003. A dataset with an extra key, a float, or a
missing `rating` therefore stays on the reference path. On top of that, 96
(count, m, Accept-Encoding) combinations were compared against the pre-change
binary: identical, including the gzip responses. The startup line naming the
path taken is what makes that comparison mean anything -- a fast path that never
ran also answers identical bytes.
`load_dataset()` also moves to the framework's `uvcpp_json_parse` rather than
nlohmann's `parse` directly: same backend, no throw, and the entry then touches
no JSON API the library does not publish. Startup-only, once, before any
listener.
Not done, and deliberately: the plan's target for this step was 15 us, and the
serialization step gives 7.6. The 17 us estimate came from a breakdown that
covered "DOM walk plus per-item `dump()`"; the `dump()` half was already gone
(writer path, previous commit), so what the flattening can reach is the walk,
~140 ns of the ~590 ns each item costs. The remaining per-item cost is the ~26
writer calls it takes to emit ~170 bytes, which is not something the entry can
change without hand-rolling a serializer and giving up the documented one.
README: the JSON note records the shape, the numbers, and the self-check.
`uvcpp_web_static`'s default revalidation hands every request to the thread
pool: `uv_queue_work` out, the worker `stat`s, `after_work` back. One pool
round trip per request, on a path whose entire remaining work is a `stat`. The
library exposes both modes (1.6.25-dev / 1.6.26-dev); the entry was still on the
default `POOL`.
Measured locally, same binary pair, same shape (static-h2, 256 connections,
`-m 32`, `-t 8`, `-D 5s`, 3 repeats per arm, `{tp8, tp64}` pooled, reading is
CPU ticks / 2xx from `/proc/<pid>/stat`, median of six):
POOL static 109.66 us/req
LOOP_STAT static 88.31 us/req -19.5%
POOL baseline2 10.11 us/req
LOOP_STAT baseline2 9.91 us/req -2.0%, inside the run-to-run spread
The `baseline2` arm is the control: it exercises the same process, loop and
client but never enters the static handler, and it does not move. The gain is
in the static path, not in the machine drifting between arms.
What the entry pays for it, stated rather than assumed: the `stat` now runs on
the loop thread, so it blocks the other connections sharing that loop. That is
only affordable because the platform serves this from the container's own
filesystem -- `Dockerfile` copies the corpus in, so `stat` is a few
microseconds. The 46 us figure quoted in the library's header is a FUSE mount;
on that kind of mount this option should stay off, and the option's default
stays `POOL` for exactly that reason. Semantics are unchanged in either mode:
the next request still gets the bytes the disk has now, which is what
`validate.sh`'s staleness probe checks.
…s measuring `v1.6.1`, the revision the merged entry pins, decodes the short-header DCID from the *peer's* SCID length, so every 1-RTT packet after the handshake is dropped; `745608c` fixes it and is not in `v1.6.1`. That is the profile the board reads as 58 req/s, 4.2% CPU, rank 13 of 13. The pin moves 29 commits, from `c8950ae` (1.6.22-dev) to `6eede33` (1.6.51-dev) -- the tip of the library's `master`, and the first revision whose four CI legs (Linux, macOS, MSVC, MinGW-w64) are all green. Criterion, same box, same client, both libraries built with the platform's own flag set and both entries driven by the platform's own command lines, three runs each, median: profile (platform command line) merged @ v1.6.1 this entry @ 6eede33 baseline-h2 -c 64 -m 100 618,420 rps 724,800 rps baseline-h3 -c 64 -m 64, alpn h3 45 rps 200,999 rps static-h2 -c 64 -m 32 13,027 rps 28,937 rps static-h3 -c 64 -m 64, alpn h3 33 rps 14,223 rps Every 6eede33 row is 0 3xx / 0 4xx / 0 5xx across all three runs. The two h3 rows are not like-for-like: the merged entry has no HTTP/3 static leg at all, so its 33 req/s is 404s, and mounting `uvcpp_static_server` on the QUIC listener is what makes `static-h3` -- a profile `meta.json` subscribes to -- a profile the entry can serve. The whole pre-validation was re-run at this pin against the platform's flag set: `check_all.sh` 15/15, the 12 gateway assertions 12/12, the 15 production-stack assertions 15/15, and the loads above. The previous run of that rig was invalid and is not the basis for any number here -- a process left over from an earlier run was still bound to 8443 (the entry uses SO_REUSEPORT, so two processes share the port and the kernel hashes connections between them), which split the load across two different libraries; the rig now asserts, at every listener, that the port's sole owner is the process it just started. The Dockerfile comment carries the per-commit criteria for the rest of the batch, all of them from the library's own CHANGELOG.
|
/benchmark -f libuvcpp --save |
|
👋 Benchmark request received. A collaborator will review and approve the run. |
|
|
|
/benchmark -f libuvcpp --save |
|
👋 Benchmark request received. A collaborator will review and approve the run. |
|
|
|
/benchmark -f libuvcpp --save |
|
👋 Benchmark request received. A collaborator will review and approve the run. |
|
/benchmark -f libuvcpp --save |
|
👋 Benchmark request received. A collaborator will review and approve the run. |
Benchmark ResultsFramework:
Full log |
注释是唯一改动(`grep -v '^#'` 前后逐行相同,72 行),构建命令一行没动。 两处陈旧: 1. 开头那段还在说"pin 现在是 2d5fe74(1.6.20-dev)",而 ARG LIBUVCPP_REF 从 057bb45 起就是 6eede33(1.6.51-dev),下面 149 行附近那块也早就是这么写的 —— 上次搬 pin 时加了新块,旧块忘了改。 2. 顺带把 v1.7.0 与 6eede33 的关系写清:`git diff 6eede33 v1.7.0 -- src/ ':!src/db'` 只有一个 hunk(src/uvcpp/uvcpp_version.h 的版本宏);src/db/ 那段 是 MySQL/PG 后端改成默认 OFF 的 dlopen 接缝;其余是打包/构建/CI,其中只有 `--exclude-libs,ALL` 这一条够得着 ELF 构建(只改动态符号表,不改代码)。 所以板上这个条目的读数就是 1.7.0 的读数,pin 因此继续钉提交而不是标签。
|
The library released Three things changed since the description was written; the branch is otherwise untouched. The description now opens its numbers with the platform's own reading. The board ran 42 profiles against this entry on 2026-10-14. Against
All 42 profiles returned zero 5xx, the eight this PR subscribes included. The largest move is The pin stays on the commit, not the tag. If you'd rather the saved results carried the release revision literally, say so and I'll move the pin to the tag and re-run — a one-line change plus a The last push is comment-only. The Dockerfile's pin history was stale: it still described This is ready to merge. Nothing outstanding on my side, and nothing here needs another |
|
👋 Benchmark request received. A collaborator will review and approve the run. |
|
One correction: please ignore the benchmark request the bot just picked up from my previous comment. It matches the command name literally, and I wrote it in prose twice, so it queued a run that is now sitting at the runner approval gate. Nothing there needs approving — the branch is unchanged apart from the comment-only Dockerfile edit described above, and the numbers already in the description are the platform's own from the 2026-10-14 run. I can't cancel it from this account (that needs admin on the repo), so it will just stay queued. The only ask is to merge. |
Description
Subscribes
libuvcppto the eight profiles it wasn't being scored on:baseline-h2c,json-h2c,static-h3,unary-grpc,unary-grpc-tls,gateway-64,gateway-h3,production-stack. A family's score is the sum ofits profiles, so an unsubscribed profile gives up that one's whole score.
The entry doesn't change shape: one process, one build, five listeners. The pin
moves from
3fdd6208(v1.6.1) to6eede33, which is the revision the libraryreleased as
v1.7.0in everything that decides behaviour — see The pin.On the board below says what that move is worth.
What's new
h2c.
baseline-h2candjson-h2cget the platform's dedicated h2c port(8082), so their listener shares a port with nothing else; both routes are the
same handlers the h2 listener already had.
unary-grpcis prior-knowledge h2c on8080, which also carries HTTP/1.1 — a connection is h2c iff its first 24 bytes are
the RFC 9113 preface, decided once per connection. No
Upgrade: h2c(RFC 9113dropped it) and no sniffing.
gRPC. The 5-byte length-prefixed frame, the varint field walk and the
grpc-statustrailer are the library's documented API(
grpc_parse_unary_body,grpc_field_get_int32,grpc_web_send_unary,grpc_web_send_status), so the handler is about a dozen lines and nothing aboutthe wire format is hand-written in the entry.
unary-grpc-tlsis the same routeover h2+TLS on 8443.
Gateway and production-stack. One
compose.<profile>.ymleach, with a stock,unmodified
caddy:2-alpineat the edge — the rules exclude custom proxyimplementations and there isn't one here. The edge serves
/static/*off themounted disk itself (
file_server { precompressed br gzip }) and never forwardsit to the backend. The backend hop is plain HTTP/1.1 over loopback, so client h2
streams fan out across the server's per-core loops instead of queueing behind one
connection's flow control. The server gets no
/certsin these profiles, so itstays on 8080 and leaves 8443 to the edge, and
production-stackuses theshared, unmodified
frameworks/_shared/authsvc/.static-h3. Needed the library's h3 dispatch to accept a deferred reply; on
QUIC there's no
uvcpp_web_app, so that leg usesuvcpp_static_serveras itsfallback handler. Both static servers also grew the same
precompressedoption,so all three static legs serve the
.br/.gzsidecars the dataset alreadyships beside 15 of its 20 assets — what nginx
gzip_staticand Caddyprecompressed br gzip(which this entry's own Caddyfiles use) do. The entryneither produces nor caches those files.
Tests
This PR adds no test files: an entry has no suite of its own, and the only
harness that runs against one is
scripts/validate.sh. The tests for the librarychanges live in
Antruly/libuvcppat the pinned commit and run in that repo's CIon all four platforms. These are the ones that pin what the new profiles rely on:
web_h2_cleartext_func.cppgrpc_codec_func.cpp,web_h2_grpc_func.cpp,web_app_h2_grpc_func.cpphttp3_web_func.cpphttp3_multiloop_func.cpp,quic_server_multiloop_func.cpp,reuseport_probe.hquic_stream_func.cppreply_alive()reads nothing through its argumentweb_static_server_func.cpp(the probe passes an address that is not mapped)web_multiloop_func.cpp(taskset -c 0-3, 8 ways × 250)tests/tools/multiloop_mutation.pyfeeds mutations back through the tree — e.g.pinning the endpoint loop count to 1 — and asserts those cases go red. The
criterion there isn't "it passed", it's "it fails when broken".
Against this pin, on a bare-metal copy of the Dockerfile's own flags (no Docker
on this machine):
scripts/validate-frag.py 127.0.0.1 8080 60→ 9/9 (1047 fragment offsets);scripts/validate-ws.py 127.0.0.1 8080→ 7/7.scripts/validate.shdoes:h2c on 8082,
json-h2c,unary-grpcon 8080, the h1 regression and thebad-frame boundary case → 15 pass / 0 fail;
gateway-64through the Caddyedge → 12 / 0;
production-stack(edge + cache + authsvc) → 15 / 0.:8081and on:8443/tcp:200withcontent-encoding: brfor exactly the 15 that have a sidecar, bodybyte-identical to the
.brfile. The same sweep over:8443/udp (h3): zeronon-2xx, same bytes per response. With no
Accept-Encoding,app.jscomesback as the original 204,800 bytes and no
content-encoding.Numbers
On the board
The platform ran 42 profiles against this entry on 2026-10-14 — its machine, its
generators, one profile at a time. The comparison is against
v1.6.1, therevision the currently merged entry pins, and every reading is the one the
platform committed back to this branch:
v1.6.1baseline-h3-64(req/s)echo-ws-pipeline-512/-4096/-16384(req/s)async-32000(req/s)pipelined-4096(req/s)json-tls-4096(req/s)json-comp-4096(req/s)baseline-4096(req/s)static-h2-1024(resident)All 42 profiles returned zero 5xx; the 15 4xx are
production-stack's own healthprobes. The largest single move is
baseline-h3-64: 58 req/s at 4.2% CPU, whichmeans the server was waiting rather than computing — it decoded the short header
with the peer's connection-ID length, so every 1-RTT packet was dropped after
the handshake (
745608c). Thestatic-h2memory is a batch now capped at 64 KiBrather than as large as the peer's window allows (
46ba344).The ratios are the code and not a faster box.
cpu_per_req_us—(CPU%/100)/rps,which cancels out the machine — falls 17–62% on every profile that moved, while
the profiles pinned to something outside the server (
latency-*,8gbit,async-db) stay inside ±4%. The full 42-profile table, the per-profileattribution and the four-round comparison are in the library's
doc/benchmark.md§4.5.
On this machine
Every row is an A/B on this machine with the arms alternating round by round, and
each names what it was measured against:
745608c)h2load --alpn-list=h3against the entryh2load -p http/1.1 -c 64 -m 2 -n 8000, 20-URI rotationuvcpp_static_server933 → 82,451 rps;uvcpp_web_app110,773 → 185,280 rps; bytes −24.5%font/woffandfont/woff2out of the deflate table (ac6d216)static-h2round 43,756 → 52,752 rps (+20.6%)46c20c5)TCP_NODELAYon by default (0528b4a)-c 1 -m 1, request median8ed50fb)taskset -c 0-3, 8 ways × 250Two of those are honest about their reach. The
TCP_NODELAYrow is a latencyswitch, not a score lever: at the benchmark's own shapes (
-m 32/-m 100) thetwo arms land inside each other's round-to-round spread, and it was merged
because 41 ms is wrong rather than because it moves a number. And the callgrind
row is instructions, not time — this box's CPU governor drifts µs/request by
about 35% between sessions, so the counts are the reproducible unit.
Entry-side
h2loadreadings against this pin:baseline-h21,307,140 req/s(0 failed, 0 5xx),
baseline-h3370,148 req/s,static-h27,671 req/s (all 2xx),gateway-6424,805 req/s through Caddy.The pin
3fdd6208(v1.6.1) →2d5fe74(1.6.20-dev) →6eede33(1.6.51-dev), 29commits past
c8950ae. Each move was measured against the previous pin beforebeing taken; each commit's own criterion is in the library's
CHANGELOG.md, andwhat this entry's profiles do with them is in the Dockerfile's comments.
3fdd6208→2d5fe74carries cleartext h2 and gRPC; the h3 deferred reply; twinserving on all three static legs plus the two MIME exclusions; the QUIC
short-header CID fix; and the multi-loop teardown, chain-cache and ALPN fixes.
2d5fe74was the tip of the 1.6.x line rather than an earlier commit on itbecause two defects this entry can reach landed just below it:
connection_generation()/reply_alive()dereferenced the connection theywere handed. Both are documented to be called after it may be gone — which is
exactly the path every
static-*cache miss takes — and withset_loops(n > 1), which this entry calls, it was a read of freed memory. ASancaught it 5/5 on Linux; on Windows it is the
SegFaultMinGW had beenreporting.
(
LNK2019from a Windows DLL), which is why the library's own MSVChttp3jobhad never built.
That tip also touches one test file and nothing else —
git diff e3e033e0 2d5fe74 -- src/is empty, so the library built from it is byte-for-byte the onee3e033e0builds. It was taken because it is the revision whose CI is green onall four platforms: a wall-clock budget in the library's own
quic_stream_funchad been calibrated on Linux (43 ms) and came out 8 ms over on the Windows
runner, keeping
CI - Windows · MSVCred on two jobs.6eede33is a batch rather than one named change, so its evidence is per profileand lives in the Dockerfile's comments. It is the tip because it is the first
revision whose four CI legs — Linux, macOS, MSVC and MinGW-w64 — are all green,
and because it is what the library released as
v1.7.0:git diff 6eede33 v1.7.0 -- src/ ':!src/db'is one hunk, the version macros insrc/uvcpp/uvcpp_version.h, and nothing else.Everything else in that range is outside the library's behaviour:
src/db/is the MySQL and PostgreSQL backends moving behind adlopenseamthat is off by default, plus the status enum the seam added. A build that does
not ask for it is unaffected, and this entry does not ask for it.
build: an
--exclude-libs,ALLlink option that hides symbols arriving fromstatic archives, which leaves the library's own API exported. It changes the
dynamic symbol table, not the code.
So the numbers On the board are the release's numbers, and the pin stays on
the commit rather than moving to the tag: moving it would put the entry's source
a revision away from the revision the saved results came from, for a library that
is — where it counts — the same one.
No route, listener or option changes, and nothing about the already-scored
profiles changes with the pin.