Skip to content

drpcquic: add DRPC-over-QUIC transport#79

Closed
eshwarsriramoju wants to merge 18 commits into
cockroachdb:mainfrom
eshwarsriramoju:feat/drpc-quic-2
Closed

drpcquic: add DRPC-over-QUIC transport#79
eshwarsriramoju wants to merge 18 commits into
cockroachdb:mainfrom
eshwarsriramoju:feat/drpc-quic-2

Conversation

@eshwarsriramoju

Copy link
Copy Markdown

drpcquic: add DRPC-over-QUIC transport

Add drpcquic, a QUIC-based transport for DRPC.

DRPC multiplexes logical streams above the transport. Over TCP, all logical
streams share one ordered byte stream, so a single lost segment stalls every
stream on the connection (transport-layer head-of-line blocking), which cannot
be fixed in application code over one TCP connection. QUIC provides independent,
flow-controlled streams, so mapping each drpc stream onto its own QUIC stream
removes this by construction: loss on one stream cannot stall the others.

Design: one drpc.Stream maps to one native quic.Stream, with no manager. The TCP
path uses drpcmanager to demux frames by stream id over a shared reader/writer;
QUIC already demultiplexes streams, so that machinery is bypassed. QuicConn wraps
*quic.Conn and implements drpc.Conn; QuicStream wraps *quic.Stream and
implements drpc.Stream directly.

Includes:

  • Client: Dial/DialWithOptions (one QUIC connection per peer, one stream per
    Invoke), forcing the drpc-quic ALPN over a caller-supplied TLS config.
  • Server: drpcserver.ServeQuic plus a drpcquic.Server with Serve(ctx, lis),
    Listen, and ListenPacket, reusing the standard drpc handler registration.
  • Wire: options, ALPN/TLS 1.3 helper, quic-go error mapping, context deadline to
    codes.DeadlineExceeded, peer-cert injection into the served context, and
    clean-shutdown handling.
  • QUIC config tuning (MaxIncomingStreams, keepalive / idle timeout).
  • End-to-end tests (streaming, concurrency, cancellation, server death, no-log,
    goroutine-leak), a cmd/quicdemo, and an examples/drpc-quic echo service.

Depends on quic-go v0.59.1 and requires Go 1.25.

This is an experimental transport.

eshwarsriramoju and others added 18 commits June 15, 2026 08:30
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…st harness

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a QUIC Server type whose Serve(ctx, *Listener) method mirrors
(*drpcserver.Server).Serve, so it fits callers that hold a server and
call Serve(ctx, lis). The free Serve stays as a wrapper. Add ListenPacket
for a caller-owned UDP socket.
The accept loop read each stream's invoke before accepting the next, so a
stream slow to send its invoke blocked every later stream on the
connection. Split AcceptStream (loop) from the invoke read (per-stream
goroutine), isolate per-stream parse errors, and order shutdown
Cancel->Close->Wait so a stream stalled mid-invoke can't hang drain.
mapQUICError let context.DeadlineExceeded fall into the net.Error timeout
catch-all, wrapping it as ClosedError so ToRPCErr returned Unavailable.
Return the bare sentinel before the catch-all; ToRPCErr matches it by
value and yields codes.DeadlineExceeded.
…rns nil

Mirror ServeOne's TLS peer-info: read the verified chain off the QUIC
connection and set drpcctx.PeerConnectionInfo before ServeMultiplexed so
handlers (auth) see peer identity. Serve now returns nil on clean stop.
Rewrite DRPC-over-QUIC from the "pipe adapter" design to the "application
adapter" design: instead of wrapping each QUIC stream as a drpc.Transport and
driving it through a QUICManager + drpcstream state machine, a QuicConn
implements drpc.Conn and a QuicStream implements drpc.Stream directly over
quic-go. drpc's multiplexing manager is bypassed entirely; only its framing,
encoding, error and metadata helpers are reused.

Each RPC opens its own native QUIC stream, so there is no shared reader, no
semaphore, no demux, and no per-RPC readLoop/watcher goroutines. Cancellation
uses the QUIC stream's native context on the server and a single context.AfterFunc
on the client.

Removed pipe adapter:
- drpc.MultiplexedTransport
- drpcmanager/quic_manager.go
- drpcquic transport/server/listener/options/error/tls adapter files
- drpcstream NewForReader/readLoop/watcher
- drpcconn NewFromMultiplexed
- drpcserver/multiplexed.go

Added:
- drpcquic/{doc,wire,conn,stream}.go
- drpcserver/server_quic.go

quic_test.go covers unary, server-streaming, error-code survival, metadata,
20-way concurrency, server-observes-client-cancel, and a >4 MiB message.
go build/vet clean; drpcquic tests pass under -race; TCP-path suites still pass.
Dial and Listen passed a nil *quic.Config, so quic-go's defaults applied —
most importantly MaxIncomingStreams=100. Because every drpc RPC opens its own
QUIC stream, a connection carrying more than 100 concurrent RPCs makes the
peer's OpenStreamSync block on stream credits, stalling latency-sensitive RPCs
(e.g. connection heartbeats) until they time out and the connection is torn
down and re-dialed. Under CockroachDB node-to-node load this shows up as
"conn heartbeat timed out" churn and "Application error 0x0" on in-flight RPCs.

Add quicConfig() and pass it from Dial/DialWithOptions/Listen/ListenPacket:
- MaxIncomingStreams 1<<16 (well above realistic per-connection concurrency),
- KeepAlivePeriod 15s / MaxIdleTimeout 30s (the QUIC idle timeout is
  connection-scoped; keepalive stops a quiet period from dropping every
  multiplexed stream).

go build/vet clean; go test ./drpcquic/ -race passes.
A minimal end-to-end example of the stream-per-QUIC-stream API. The server
registers EchoService on a drpcmux and serves it with
drpcserver.(*Server).ServeQuic over a drpcquic.Listen listener; the client
dials with drpcquic.Dial (whose *QuicConn is itself a drpc.Conn, so no
drpcconn wrapping) and calls Echo. QUIC requires TLS, so the server loads a
self-signed cert and the client uses InsecureSkipVerify; the drpc-quic ALPN is
forced inside drpcquic.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants