From 24d1c42b4d7109ae7a722f37a6c8aa19736d7fb1 Mon Sep 17 00:00:00 2001 From: David Bell Date: Tue, 17 Jan 2023 21:01:48 -0800 Subject: [PATCH] http2: add IdleConnTimeout to http2.Transport Exposes an IdleConnTimeout on http2.Transport directly, rather than rely on configuring it through the underlying http1 transport. For golang/go#57893 --- http2/transport.go | 14 ++++++++++ http2/transport_test.go | 62 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/http2/transport.go b/http2/transport.go index ac90a2631c..bb1cbda994 100644 --- a/http2/transport.go +++ b/http2/transport.go @@ -146,6 +146,12 @@ type Transport struct { // waiting for their turn. StrictMaxConcurrentStreams bool + // IdleConnTimeout is the maximum amount of time an idle + // (keep-alive) connection will remain idle before closing + // itself. + // Zero means no limit. + IdleConnTimeout time.Duration + // ReadIdleTimeout is the timeout after which a health check using ping // frame will be carried out if no frame is received on the connection. // Note that a ping response will is considered a received frame, so if @@ -3109,9 +3115,17 @@ func (rt noDialH2RoundTripper) RoundTrip(req *http.Request) (*http.Response, err } func (t *Transport) idleConnTimeout() time.Duration { + // to keep things backwards compatible, we use non-zero values of + // IdleConnTimeout, followed by using the IdleConnTimeout on the underlying + // http1 transport, followed by 0 + if t.IdleConnTimeout != 0 { + return t.IdleConnTimeout + } + if t.t1 != nil { return t.t1.IdleConnTimeout } + return 0 } diff --git a/http2/transport_test.go b/http2/transport_test.go index 54d4551484..ec0ea750c9 100644 --- a/http2/transport_test.go +++ b/http2/transport_test.go @@ -95,6 +95,68 @@ func startH2cServer(t *testing.T) net.Listener { return l } +func TestIdleConnTimeout(t *testing.T) { + for _, test := range []struct { + idleConnTimeout time.Duration + wait time.Duration + baseTransport *http.Transport + wantConns int32 + }{{ + idleConnTimeout: 2 * time.Second, + wait: 1 * time.Second, + baseTransport: nil, + wantConns: 1, + }, { + idleConnTimeout: 1 * time.Second, + wait: 2 * time.Second, + baseTransport: nil, + wantConns: 5, + }, { + idleConnTimeout: 0 * time.Second, + wait: 1 * time.Second, + baseTransport: &http.Transport{ + IdleConnTimeout: 2 * time.Second, + }, + wantConns: 1, + }} { + var gotConns int32 + + st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, r.RemoteAddr) + }, optOnlyServer) + defer st.Close() + + tr := &Transport{ + IdleConnTimeout: test.idleConnTimeout, + TLSClientConfig: tlsConfigInsecure, + } + defer tr.CloseIdleConnections() + + for i := 0; i < 5; i++ { + req, _ := http.NewRequest("GET", st.ts.URL, http.NoBody) + trace := &httptrace.ClientTrace{ + GotConn: func(connInfo httptrace.GotConnInfo) { + if !connInfo.Reused { + atomic.AddInt32(&gotConns, 1) + } + }, + } + req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace)) + + _, err := tr.RoundTrip(req) + if err != nil { + t.Fatalf("%v", err) + } + + <-time.After(test.wait) + } + + if gotConns != test.wantConns { + t.Errorf("incorrect gotConns: %d != %d", gotConns, test.wantConns) + } + } +} + func TestTransportH2c(t *testing.T) { l := startH2cServer(t) defer l.Close()