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 <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Damien Neil <dneil@google.com>
Reviewed-by: Nicholas Husin <nsh@golang.org>
Reviewed-by: Nicholas Husin <husin@google.com>
This commit is contained in:
Damien Neil 2024-09-25 11:10:07 -07:00 committed by Gopher Robot
parent 16be34df02
commit 3a5df9d2b2
4 changed files with 57 additions and 2 deletions

1
api/next/67813.txt Normal file
View file

@ -0,0 +1 @@
pkg net/http, type HTTP2Config struct, StrictMaxConcurrentRequests bool #67813

View file

@ -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.

View file

@ -235,10 +235,23 @@ type Pusher interface {
// both [Transport] and [Server]. // both [Transport] and [Server].
type HTTP2Config struct { type HTTP2Config struct {
// MaxConcurrentStreams optionally specifies the number of // 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. // If zero, MaxConcurrentStreams defaults to at least 100.
//
// This parameter only applies to Servers.
MaxConcurrentStreams int 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 // MaxDecoderHeaderTableSize optionally specifies an upper limit for the
// size of the header compression table used for decoding headers sent // size of the header compression table used for decoding headers sent
// by the peer. // by the peer.

View file

@ -59,7 +59,7 @@ func TestTransportPoolConnCannotReuseConnectionInUse(t *testing.T) {
// When an HTTP/2 connection is at its stream limit // When an HTTP/2 connection is at its stream limit
// a new request is made on a new connection. // 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) { synctest.Test(t, func(t *testing.T) {
dt := newTransportDialTester(t, http2Mode, func(srv *http.Server) { dt := newTransportDialTester(t, http2Mode, func(srv *http.Server) {
srv.HTTP2 = &http.HTTP2Config{ 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. // A new request made while an HTTP/2 dial is in progress will start a second dial.
func TestTransportPoolConnHTTP2Startup(t *testing.T) { func TestTransportPoolConnHTTP2Startup(t *testing.T) {
synctest.Test(t, func(t *testing.T) { synctest.Test(t, func(t *testing.T) {