Skip to content

Commit 43b9fcf

Browse files
fraenkelbradfitz
authored andcommitted
net/http: make Transport.MaxConnsPerHost work for HTTP/2
Treat HTTP/2 connections as an ongoing persistent connection. When we are told there is no cached connections, cleanup the associated connection and host connection count. Fixes #27753 Change-Id: I6b7bd915fc7819617cb5d3b35e46e225c75eda29 Reviewed-on: https://go-review.googlesource.com/c/go/+/140357 Reviewed-by: Brad Fitzpatrick <[email protected]>
1 parent c706d42 commit 43b9fcf

File tree

2 files changed

+112
-8
lines changed

2 files changed

+112
-8
lines changed

src/net/http/transport.go

+11-8
Original file line numberDiff line numberDiff line change
@@ -506,16 +506,19 @@ func (t *Transport) roundTrip(req *Request) (*Response, error) {
506506
var resp *Response
507507
if pconn.alt != nil {
508508
// HTTP/2 path.
509-
t.decHostConnCount(cm.key()) // don't count cached http2 conns toward conns per host
510-
t.setReqCanceler(req, nil) // not cancelable with CancelRequest
509+
t.putOrCloseIdleConn(pconn)
510+
t.setReqCanceler(req, nil) // not cancelable with CancelRequest
511511
resp, err = pconn.alt.RoundTrip(req)
512512
} else {
513513
resp, err = pconn.roundTrip(treq)
514514
}
515515
if err == nil {
516516
return resp, nil
517517
}
518-
if !pconn.shouldRetryRequest(req, err) {
518+
if http2isNoCachedConnError(err) {
519+
t.removeIdleConn(pconn)
520+
t.decHostConnCount(cm.key()) // clean up the persistent connection
521+
} else if !pconn.shouldRetryRequest(req, err) {
519522
// Issue 16465: return underlying net.Conn.Read error from peek,
520523
// as we've historically done.
521524
if e, ok := err.(transportReadFromServerError); ok {
@@ -778,9 +781,6 @@ func (t *Transport) tryPutIdleConn(pconn *persistConn) error {
778781
if pconn.isBroken() {
779782
return errConnBroken
780783
}
781-
if pconn.alt != nil {
782-
return errNotCachingH2Conn
783-
}
784784
pconn.markReused()
785785
key := pconn.cacheKey
786786

@@ -829,7 +829,10 @@ func (t *Transport) tryPutIdleConn(pconn *persistConn) error {
829829
if pconn.idleTimer != nil {
830830
pconn.idleTimer.Reset(t.IdleConnTimeout)
831831
} else {
832-
pconn.idleTimer = time.AfterFunc(t.IdleConnTimeout, pconn.closeConnIfStillIdle)
832+
// idleTimer does not apply to HTTP/2
833+
if pconn.alt == nil {
834+
pconn.idleTimer = time.AfterFunc(t.IdleConnTimeout, pconn.closeConnIfStillIdle)
835+
}
833836
}
834837
}
835838
pconn.idleAt = time.Now()
@@ -1377,7 +1380,7 @@ func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (*persistCon
13771380

13781381
if s := pconn.tlsState; s != nil && s.NegotiatedProtocolIsMutual && s.NegotiatedProtocol != "" {
13791382
if next, ok := t.TLSNextProto[s.NegotiatedProtocol]; ok {
1380-
return &persistConn{alt: next(cm.targetAddr, pconn.conn.(*tls.Conn))}, nil
1383+
return &persistConn{cacheKey: pconn.cacheKey, alt: next(cm.targetAddr, pconn.conn.(*tls.Conn))}, nil
13811384
}
13821385
}
13831386

src/net/http/transport_test.go

+101
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,107 @@ func TestTransportMaxConnsPerHostIncludeDialInProgress(t *testing.T) {
588588
<-reqComplete
589589
}
590590

591+
func TestTransportMaxConnsPerHost(t *testing.T) {
592+
defer afterTest(t)
593+
if runtime.GOOS == "js" {
594+
t.Skipf("skipping test on js/wasm")
595+
}
596+
h := HandlerFunc(func(w ResponseWriter, r *Request) {
597+
_, err := w.Write([]byte("foo"))
598+
if err != nil {
599+
t.Fatalf("Write: %v", err)
600+
}
601+
})
602+
603+
testMaxConns := func(scheme string, ts *httptest.Server) {
604+
defer ts.Close()
605+
606+
c := ts.Client()
607+
tr := c.Transport.(*Transport)
608+
tr.MaxConnsPerHost = 1
609+
if err := ExportHttp2ConfigureTransport(tr); err != nil {
610+
t.Fatalf("ExportHttp2ConfigureTransport: %v", err)
611+
}
612+
613+
connCh := make(chan net.Conn, 1)
614+
var dialCnt, gotConnCnt, tlsHandshakeCnt int32
615+
tr.Dial = func(network, addr string) (net.Conn, error) {
616+
atomic.AddInt32(&dialCnt, 1)
617+
c, err := net.Dial(network, addr)
618+
connCh <- c
619+
return c, err
620+
}
621+
622+
doReq := func() {
623+
trace := &httptrace.ClientTrace{
624+
GotConn: func(connInfo httptrace.GotConnInfo) {
625+
if !connInfo.Reused {
626+
atomic.AddInt32(&gotConnCnt, 1)
627+
}
628+
},
629+
TLSHandshakeStart: func() {
630+
atomic.AddInt32(&tlsHandshakeCnt, 1)
631+
},
632+
}
633+
req, _ := NewRequest("GET", ts.URL, nil)
634+
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
635+
636+
resp, err := c.Do(req)
637+
if err != nil {
638+
t.Fatalf("request failed: %v", err)
639+
}
640+
defer resp.Body.Close()
641+
_, err = ioutil.ReadAll(resp.Body)
642+
if err != nil {
643+
t.Fatalf("read body failed: %v", err)
644+
}
645+
}
646+
647+
wg := sync.WaitGroup{}
648+
for i := 0; i < 10; i++ {
649+
wg.Add(1)
650+
go func() {
651+
defer wg.Done()
652+
doReq()
653+
}()
654+
}
655+
wg.Wait()
656+
657+
expected := int32(tr.MaxConnsPerHost)
658+
if dialCnt != expected {
659+
t.Errorf("Too many dials (%s): %d", scheme, dialCnt)
660+
}
661+
if gotConnCnt != expected {
662+
t.Errorf("Too many get connections (%s): %d", scheme, gotConnCnt)
663+
}
664+
if ts.TLS != nil && tlsHandshakeCnt != expected {
665+
t.Errorf("Too many tls handshakes (%s): %d", scheme, tlsHandshakeCnt)
666+
}
667+
668+
(<-connCh).Close()
669+
670+
doReq()
671+
expected++
672+
if dialCnt != expected {
673+
t.Errorf("Too many dials (%s): %d", scheme, dialCnt)
674+
}
675+
if gotConnCnt != expected {
676+
t.Errorf("Too many get connections (%s): %d", scheme, gotConnCnt)
677+
}
678+
if ts.TLS != nil && tlsHandshakeCnt != expected {
679+
t.Errorf("Too many tls handshakes (%s): %d", scheme, tlsHandshakeCnt)
680+
}
681+
}
682+
683+
testMaxConns("http", httptest.NewServer(h))
684+
testMaxConns("https", httptest.NewTLSServer(h))
685+
686+
ts := httptest.NewUnstartedServer(h)
687+
ts.TLS = &tls.Config{NextProtos: []string{"h2"}}
688+
ts.StartTLS()
689+
testMaxConns("http2", ts)
690+
}
691+
591692
func TestTransportRemovesDeadIdleConnections(t *testing.T) {
592693
setParallel(t)
593694
defer afterTest(t)

0 commit comments

Comments
 (0)