docs(quic): OnClosed, and what ~ClientQUIC and SendSync now promise

The README's QUIC section gains how to own accepted connections: the
OnClosed callback, the thread it runs on, what not to do in it and the
std::stop_callback rule that makes deleting a connection under a lock
its callback takes a deadlock. The consumer notes give the same
contract with the map-under-a-mutex pattern for a server that also
deletes on shutdown, list OnClosed among what runs on msquic's workers
rather than the pool, say that ~ClientQUIC now waits up to 1 s so the
peer hears the close, and that SendSync throws only for its own send.
The agent notes count the three new tests, add OnClosed's thread and
the destructor's new wait to the gotchas, and the forking tests and
their alarm() watchdog to the testing conventions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-11-01 02:20:04 +01:00
commit 3be8642d32
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
3 changed files with 30 additions and 8 deletions

View file

@ -108,6 +108,21 @@ The port-only constructor listens on every address of both families. To keep a l
Crafter::ListenerQUIC local("127.0.0.1", 0, "myproto/1", creds, onConnect);
```
#### Owning accepted connections
Each connection the listener accepts reaches the callback as a heap-allocated `ClientQUIC*` that the callback owns. `OnClosed` says when to delete it: it fires once when the connection has ended for any reason but its own destruction (the peer closed it, the idle timeout or a transport error ended it, or `Stop()` was called), on msquic's worker thread, and deleting the connection from inside it is safe.
```cpp
Crafter::ListenerQUIC listener("127.0.0.1", 0, "myproto/1", creds, [](Crafter::ClientQUIC* peer) {
peer->OnClosed([peer] { delete peer; });
peer->OnStream([](Crafter::QUICStream stream) { /* ... */ });
});
```
Keep the callback short: it runs on the msquic worker, so no `*Sync` call and no deleting a *different* connection from inside it. If the connection had already closed when `OnClosed` was called, it fires as a pool task instead, never inside `OnClosed`. Once `~ClientQUIC` has begun it never fires, and the destructor waits for one already running on another thread, as `std::stop_callback` does, so don't destroy a connection while holding a lock its callback takes. A client-side `ClientQUIC` has `OnClosed` too.
`~ClientQUIC` waits, up to 1 s and about a round trip when the peer is there, for its close to reach the peer, so a server learns of a client that went away at once rather than at its idle timeout.
### HTTP/3 Components
HTTP/3 runs over QUIC, which always requires TLS. Pass server credentials when constructing the listener (or set `selfSigned = true` for a development-only ephemeral cert) and matching client credentials when constructing the client (`insecureNoServerValidation = true` for self-signed servers).
@ -342,7 +357,10 @@ The library includes tests covering:
- QUIC listener ports (`ShouldReportListenerPortQUIC`) — a listener on port 0 reports the port it got and is reachable there, a second one gets another, and a fixed port reads back as itself
- QUIC loopback listeners (`ShouldBindLoopbackQUIC`) — a listener on `127.0.0.1` serves loopback clients and refuses, at once, clients through the host's LAN address or `::1`; `::1` works too, and a name is refused
- The QUIC development certificate (`ShouldShareSelfSignedCertificateQUIC`) — six processes start `selfSigned` listeners together from an empty cache, with no `openssl` on `PATH`, and all serve one certificate; its directory is private, a later process reuses it, and a mismatched pair on disk is replaced
- QUIC connection teardown (`ShouldSurviveConnectionChurn`) — 25 build-up/tear-down cycles with reader threads parked inside `RecieveSync`, the shape a reconnect loop has; the rest of the QUIC tests end in `std::_Exit(0)` and so never run a `ClientQUIC` destructor
- QUIC connection teardown (`ShouldSurviveConnectionChurn`) — 25 build-up/tear-down cycles with reader threads parked inside `RecieveSync`, the shape a reconnect loop has; most of the other QUIC tests end in `std::_Exit(0)`
- Closed-connection notices (`ShouldNotifyClosedConnectionQUIC`) — a server deletes each of 100 accepted connections from its `OnClosed` and the process then exits normally; a peer that exits or freezes, a server-side `Stop()`, a callback set after the close and an owner that deletes first each behave as documented
- The peer hears a close (`ShouldTellPeerOfCloseQUIC`) — the server side of each of 20 connections learns at once that the client destroyed its `ClientQUIC`, rather than at the idle timeout
- `SendSync` under a fast peer (`ShouldReturnFromSendSyncQUIC`) — 2000 sends to a peer that answers and finishes the stream at once all return
- WebTransport echo (`ShouldEchoWebTransport`) — extended-CONNECT acceptance, draft-02 SETTINGS, bidi data stream framing (`WT_STREAM 0x41` + session-id varint), and byte-for-byte echo
- HTTP/1.1 wire format (`ShouldParseHTTP1`) — serialisation, incremental parsing fed one byte at a time, chunked bodies with trailers, pipelining, interim 1xx, HTTP/1.0 and HEAD framing, and the malformed inputs the parser is required to reject
- HTTP/1.1 round-trip (`ShouldSendRecieveHTTP1`) — routing, query strings, HEAD, 404 and a throwing handler