From 3a5df9d2b20c0f059c463b5d51e89be17a2c685b Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Wed, 25 Sep 2024 11:10:07 -0700 Subject: [PATCH] net/http: add HTTP2Config.StrictMaxConcurrentRequests Add a field to HTTP2Config controlling how we behave when an HTTP/2 connection reaches its concurrency limit. This field will have no effect until golang.org/x/net/http2 is updated to make use of it, and h2_bundle.go is updated with the new http2 package. For #67813 Change-Id: Ic72a0986528abb21649f28e9fe7cf6e1236b388d Reviewed-on: https://go-review.googlesource.com/c/go/+/615875 LUCI-TryBot-Result: Go LUCI Auto-Submit: Damien Neil Reviewed-by: Nicholas Husin Reviewed-by: Nicholas Husin --- api/next/67813.txt | 1 + doc/next/6-stdlib/99-minor/net/http/67813.md | 4 ++ src/net/http/http.go | 15 +++++++- src/net/http/transport_dial_test.go | 39 +++++++++++++++++++- 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 api/next/67813.txt create mode 100644 doc/next/6-stdlib/99-minor/net/http/67813.md diff --git a/api/next/67813.txt b/api/next/67813.txt new file mode 100644 index 0000000000..b420a141e1 --- /dev/null +++ b/api/next/67813.txt @@ -0,0 +1 @@ +pkg net/http, type HTTP2Config struct, StrictMaxConcurrentRequests bool #67813 diff --git a/doc/next/6-stdlib/99-minor/net/http/67813.md b/doc/next/6-stdlib/99-minor/net/http/67813.md new file mode 100644 index 0000000000..74b8c7644f --- /dev/null +++ b/doc/next/6-stdlib/99-minor/net/http/67813.md @@ -0,0 +1,4 @@ +The new +[HTTP2Config.StrictMaxConcurrentRequests](/pkg/net/http#HTTP2Config.StrictMaxConcurrentRequests) +field controls whether a new connection should be opened +if an existing HTTP/2 connection has exceeded its stream limit. diff --git a/src/net/http/http.go b/src/net/http/http.go index 0f9165bf03..e7959fa3b6 100644 --- a/src/net/http/http.go +++ b/src/net/http/http.go @@ -235,10 +235,23 @@ type Pusher interface { // both [Transport] and [Server]. type HTTP2Config struct { // MaxConcurrentStreams optionally specifies the number of - // concurrent streams that a peer may have open at a time. + // concurrent streams that a client may have open at a time. // If zero, MaxConcurrentStreams defaults to at least 100. + // + // This parameter only applies to Servers. MaxConcurrentStreams int + // StrictMaxConcurrentRequests controls whether an HTTP/2 server's + // concurrency limit should be respected across all connections + // to that server. + // If true, new requests sent when a connection's concurrency limit + // has been exceeded will block until an existing request completes. + // If false, an additional connection will be opened if all + // existing connections are at their limit. + // + // This parameter only applies to Transports. + StrictMaxConcurrentRequests bool + // MaxDecoderHeaderTableSize optionally specifies an upper limit for the // size of the header compression table used for decoding headers sent // by the peer. diff --git a/src/net/http/transport_dial_test.go b/src/net/http/transport_dial_test.go index d7f7a3d539..086039ece9 100644 --- a/src/net/http/transport_dial_test.go +++ b/src/net/http/transport_dial_test.go @@ -59,7 +59,7 @@ func TestTransportPoolConnCannotReuseConnectionInUse(t *testing.T) { // When an HTTP/2 connection is at its stream limit // a new request is made on a new connection. -func TestTransportPoolConnHTTP2OverStreamLimit(t *testing.T) { +func testTransportPoolConnHTTP2NoStrictMaxConcurrentRequests(t *testing.T) { synctest.Test(t, func(t *testing.T) { dt := newTransportDialTester(t, http2Mode, func(srv *http.Server) { srv.HTTP2 = &http.HTTP2Config{ @@ -100,6 +100,43 @@ func TestTransportPoolConnHTTP2OverStreamLimit(t *testing.T) { }) } +// When an HTTP/2 connection is at its stream limit +// and StrictMaxConcurrentRequests = true, +// a new request waits for a slot on the existing connection. +func TestTransportPoolConnHTTP2StrictMaxConcurrentRequests(t *testing.T) { + t.Skip("skipped until h2_bundle.go includes support for StrictMaxConcurrentRequests") + + synctest.Test(t, func(t *testing.T) { + dt := newTransportDialTester(t, http2Mode, func(srv *http.Server) { + srv.HTTP2.MaxConcurrentStreams = 2 + }, func(tr *http.Transport) { + tr.HTTP2 = &http.HTTP2Config{ + StrictMaxConcurrentRequests: true, + } + }) + + // First request dials an HTTP/2 connection. + rt1 := dt.roundTrip() + c1 := dt.wantDial() + c1.finish(nil) + rt1.wantDone(c1, "HTTP/2.0") + + // Second request uses the existing connection. + rt2 := dt.roundTrip() + rt2.wantDone(c1, "HTTP/2.0") + + // Third request blocks waiting for a slot on the existing connection. + rt3 := dt.roundTrip() + + // First request finishing unblocks the thirrd. + rt1.finish() + rt3.wantDone(c1, "HTTP/2.0") + + rt2.finish() + rt3.finish() + }) +} + // A new request made while an HTTP/2 dial is in progress will start a second dial. func TestTransportPoolConnHTTP2Startup(t *testing.T) { synctest.Test(t, func(t *testing.T) { -- 2.52.0