Skip to content

v0.10.0 — gRPC (HTTP/2 + HTTP/3), reactor-pool streaming, HTTP/3 streaming bodies

Choose a tag to compare

@EdmondDantes EdmondDantes released this 10 Jul 11:50
· 271 commits to main since this release

gRPC on HTTP/2 and HTTP/3, a streaming reverse path under the reactor pool, and HTTP/3 streaming request bodies — the async server now speaks full-duplex gRPC across both transports and streams worker responses across the reactor/worker split with credit-based backpressure. Plus RFC 9110 header combining, RFC 7692 window-bits negotiation, three ASAN-found use-after-frees fixed, and H3 hot-path work.

Added

  • gRPC over HTTP/2 and HTTP/3 (#4)HttpServer::addGrpcHandler(); all four RPC shapes (unary, server/client-streaming, full-duplex bidi) with incremental readMessage() / writeMessage(), real HTTP trailers (grpc-status/grpc-message), grpc-web (binary + text), per-message gzip, grpc-timeout. Works under the reactor pool.
  • Reactor-pool streaming reverse path (#80)send() / writeMessage() / SSE cross the thread boundary in FIFO order with native trailers at EOF; per-stream credit-based backpressure parks the producer over 1 MiB un-acked so a slow client cannot flood the shared reactor.
  • HTTP/3 streaming request bodies (#26)setBodyStreamingEnabled(true) applies the H2 Content-Length policy on H3, so readBody() and incremental readMessage() work over QUIC with deferred flow-control credit.
  • WebSocket permessage-deflate honours server_max_window_bits (RFC 7692).

Fixed

  • HTTP/3 uploads larger than the initial stream window (256 KiB) no longer stall — h3_recv_data_cb returns QUIC credit as it consumes DATA.
  • Three latent use-after-frees found under ASAN (WebSocket dispose, http_log_server_stop self-freeing write, H1 parser-borrowed request on reject/spawn-fail).
  • Duplicate request headers combined per RFC 9110 §5.3 across H1/H2/H3 (Cookie with "; ", others ", ").
  • Inbound WebSocket message FIFO bounded (8× max_message_size) → close 1013 on overflow.
  • Reactor-spawned listeners get their own per-listener counters slice (telemetry race).

Performance

  • HTTP/3 reactor-pool hot paths: by-value mailbox commands, O(1) intrusive stream unlink, cached per-family sockaddr, thread-local CID-steering cipher, per-worker H3 static-delivery budget.
  • H3 reactor-mode request bodies built persistent from the first byte; wire body adopted instead of re-copied.
  • gRPC readMessage reassembly compaction amortized.

Changed

  • gRPC call-lifecycle policy extracted out of the transports into src/grpc/grpc_call.c (#4).
  • Configure fails fast on a non-ZTS (NTS) PHP — the threaded worker pool requires ZTS by design.

gRPC in practice

A unary greeter — addGrpcHandler() deframes the request with readMessage() and frames the reply with writeMessage() (the 5-byte length prefixes are handled for you); a clean return defaults grpc-status: 0. gRPC shares the listener with plain HTTP, so a fallback HTTP handler is still registered:

<?php
use TrueAsync\HttpServer;
use TrueAsync\HttpServerConfig;
use TrueAsync\HttpRequest;
use TrueAsync\HttpResponse;

$server = new HttpServer(
    (new HttpServerConfig())->addListener('127.0.0.1', 8080)
);

// Unary RPC: one request message in, one reply message out.
// Messages are your protobuf payloads.
$server->addGrpcHandler(function (HttpRequest $req, HttpResponse $resp) {
    $name = $req->readMessage() ?? '';       // deframed request message
    $resp->writeMessage("hello, {$name}");   // framed reply; grpc-status:0 on clean return
});

// gRPC shares the listener with plain HTTP; a fallback handler is still required.
$server->addHttpHandler(function (HttpRequest $req, HttpResponse $res) {
    $res->setStatusCode(426)->setBody("This endpoint speaks gRPC.\n");
});

$server->start();

Call it with any gRPC client, or raw over h2c (\x00 + 4-byte length + payload):

printf '\x00\x00\x00\x00\x05world' | curl --http2-prior-knowledge -s \
  -H 'content-type: application/grpc' -H 'te: trailers' --data-binary @- \
  http://127.0.0.1:8080/helloworld.Greeter/SayHello | tail -c +6; echo
# → hello, world

The same handler covers the other three RPC shapes (server/client-streaming, full-duplex bidi) through incremental readMessage() / writeMessage(), and runs unchanged over HTTP/3.


Full changelog: v0.9.3...v0.10.0