doq: use OpenStreamSync and retry on StreamLimitReachedError

Replace conn.OpenStream (non-blocking) with conn.OpenStreamSync so that
the resolver waits for the server's MAX_STREAMS credit replenishment frame
instead of immediately failing when the stream limit is temporarily
exhausted. Also retry on StreamLimitReachedError as defense-in-depth for
servers that are slow or fail to send MAX_STREAMS updates.
This commit is contained in:
Cuong Manh Le
2026-04-10 14:56:43 +07:00
committed by Cuong Manh Le
parent a92d20cef8
commit a767ebdaa5
+22 -10
View File
@@ -100,8 +100,9 @@ func newDOQConnPool(_ context.Context, uc *UpstreamConfig, addrs []string) *doqC
// Resolve performs a DNS query using a pooled QUIC connection. // Resolve performs a DNS query using a pooled QUIC connection.
func (p *doqConnPool) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) { func (p *doqConnPool) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
// Retry logic for transient errors: io.EOF (connection reset) and // Retry logic for transient errors: io.EOF (connection reset),
// IdleTimeoutError (stale pooled connection timed out). // IdleTimeoutError (stale pooled connection timed out), and
// StreamLimitReachedError (stream credit exhausted before server MAX_STREAMS arrived).
for range 5 { for range 5 {
answer, err := p.doResolve(ctx, msg) answer, err := p.doResolve(ctx, msg)
if err == io.EOF { if err == io.EOF {
@@ -111,6 +112,10 @@ func (p *doqConnPool) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, erro
if errors.As(err, &idleErr) { if errors.As(err, &idleErr) {
continue continue
} }
var streamLimitErr quic.StreamLimitReachedError
if errors.As(err, &streamLimitErr) {
continue
}
if err != nil { if err != nil {
return nil, wrapCertificateVerificationError(err) return nil, wrapCertificateVerificationError(err)
} }
@@ -135,18 +140,25 @@ func (p *doqConnPool) doResolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, er
return nil, err return nil, err
} }
// Open a new stream for this query // Ensure the context has a deadline before calling OpenStreamSync, which
stream, err := conn.OpenStream() // blocks until the server sends a MAX_STREAMS update. Without a deadline the
// call could block indefinitely when the server never sends the update.
deadline, ok := ctx.Deadline()
if !ok {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, 5*time.Second)
defer cancel()
deadline, _ = ctx.Deadline()
}
// OpenStreamSync blocks until the server's MAX_STREAMS credit arrives,
// avoiding the StreamLimitReachedError race that OpenStream (non-blocking)
// triggers when the credit replenishment frame is still in flight.
stream, err := conn.OpenStreamSync(ctx)
if err != nil { if err != nil {
p.putConn(conn, false) p.putConn(conn, false)
return nil, err return nil, err
} }
// Set deadline
deadline, ok := ctx.Deadline()
if !ok {
deadline = time.Now().Add(5 * time.Second)
}
_ = stream.SetDeadline(deadline) _ = stream.SetDeadline(deadline)
// Write message length (2 bytes) followed by message // Write message length (2 bytes) followed by message