http2: fix a few issues revealed by the net/http unified pool TestAPITransportCoalescedDial attempts to be agnostic on the legacy behavior (in which a connection is needlessly discarded) and the desired behavior (in which it is used). Add a missing read of the client preface in the case where we use the conn. TestAPITransportIdleConnTimeout exercises the IdleConnTimeout field, but uses an http.ClientConn. It doesn't make sense for ClientConn to apply idle timeouts (that's the responsibility of the pool using the conn). Change the test to use an http.Transport instead. The existing net/http HTTP/2 ClientConn has a bug in reporting in-flight requests, where it does not report requests waiting for a concurrency slot as active. Fixing this bug breaks an x/net/http2 test, but the real issue is that the http2.ClientConnState includes information that http.ClientConn does not provide. Add a way for net/http to provide that information to x/net/http2. The net/http change which fixes the in-flight request reporting will include the other half of this change, keeping x/net tests passing before and after. Fixes golang/go#83005 Change-Id: I599c4f4381c4f029c2e0138e2016e53f6a6a6964 Reviewed-on: https://go-review.googlesource.com/c/net/+/860083 Reviewed-by: Nicholas Husin <nsh@golang.org> Reviewed-by: Nicholas Husin <husin@google.com> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/http2/transport_api_test.go b/http2/transport_api_test.go index 7521678..fb2d85c 100644 --- a/http2/transport_api_test.go +++ b/http2/transport_api_test.go
@@ -582,6 +582,8 @@ dial3 := tt.wantDial("dummy.tld:80") dial3.finish(nil) tc2 = tt.getConn() + } else { + tc2.readClientPreface() } // One request on tc1... @@ -739,10 +741,30 @@ func TestAPITransportIdleConnTimeout(t *testing.T) { synctestTestRoundTrip(t, func(t *testing.T, mode roundTripTestMode) { const idleConnTimeout = 3 * time.Second - tc := newTestClientConn(t, mode, func(tr2 *http2.Transport) { + tt := newTestTransport(t, mode, func(tr2 *http2.Transport) { tr2.IdleConnTimeout = idleConnTimeout }) - tc.greet() + + req, _ := http.NewRequest("GET", "http://dummy.tld/", nil) + rt := tt.roundTrip(req) + + tc := tt.getConn() + tc.wantFrameType(http2.FrameSettings) + tc.wantFrameType(http2.FrameWindowUpdate) + tc.wantFrameType(http2.FrameHeaders) + + tc.writeSettings() + tc.wantFrameType(http2.FrameSettings) // ACK + + tc.writeHeaders(http2.HeadersFrameParam{ + StreamID: 1, + EndHeaders: true, + EndStream: true, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", "200", + ), + }) + rt.wantStatus(200) tc.wantIdle() closeDelay := tc.connReader.waitForData(t)
diff --git a/http2/transport_wrap.go b/http2/transport_wrap.go index eeaa95f..002cb50 100644 --- a/http2/transport_wrap.go +++ b/http2/transport_wrap.go
@@ -58,6 +58,12 @@ t.t.lazyt1 = t1 } +// GetClientConnStateFunc is called to provide us with a function +// which may be used to fetch additional state from a ClientConn. +func (t transportConfig) GetClientConnStateFunc(f func(*http.ClientConn) any) { + t.t.getCCState = f +} + func (t transportConfig) DisableCompression() bool { return t.t.DisableCompression } @@ -129,8 +135,9 @@ } type transportInternal struct { - initOnce sync.Once - lazyt1 *http.Transport + initOnce sync.Once + lazyt1 *http.Transport + getCCState func(*http.ClientConn) any dialMu sync.Mutex dials map[string]*transportDialState @@ -525,10 +532,32 @@ cc.mu.Lock() defer cc.mu.Unlock() cc.updateStateLocked() + var gotStats bool + var streamsActive int + if cc.tr.getCCState != nil { + stats, ok := cc.tr.getCCState(cc.cc).(interface { + StreamsActive() int + MaxConcurrentStreams() int + }) + if ok { + gotStats = true + streamsActive = stats.StreamsActive() + cc.maxConcurrent = stats.MaxConcurrentStreams() + } + } + if !gotStats { + streamsActive = cc.cc.InFlight() - cc.reserved + if !cc.closed { + // This is slightly racy (a request could start or finish in between + // the Available and InFlight calls), but the best we can do given that + // the net/http ClientConn API doesn't expose the conn's max concurrency. + cc.maxConcurrent = cc.cc.Available() + cc.cc.InFlight() + } + } return ClientConnState{ Closed: cc.closed, Closing: cc.closing, - StreamsActive: cc.cc.InFlight() - cc.reserved, + StreamsActive: streamsActive, StreamsReserved: cc.reserved, StreamsPending: cc.pending, MaxConcurrentStreams: uint32(min(int64(cc.maxConcurrent), math.MaxUint32)), @@ -556,12 +585,6 @@ if cc.cc.InFlight() == 0 && cc.roundTrips > 0 && cc.starting == 0 { cc.lastIdle = time.Now() } - if !cc.closed { - // This is slightly racy (a request could start or finish in between - // the Available and InFlight calls), but the best we can do given that - // the net/http ClientConn API doesn't expose the conn's max concurrency. - cc.maxConcurrent = cc.cc.Available() + cc.cc.InFlight() - } if cc.shutdownc != nil && cc.cc.InFlight()+cc.starting == 0 { close(cc.shutdownc) cc.shutdownc = nil