Skip to content

Commit a8e212f

Browse files
committed
http2: make Transport close unneeded connections after h1->h2 upgrade
If a user starts two HTTP requests when no http2 connection is available, both end up creating new TCP connections, since the server's protocol (h1 or h2) isn't yet known. Once it turns out that the server supports h2, one of the connections is useless. Previously we kept upgrading both TLS connections to h2 (SETTINGS frame exchange, etc). Now the unnecessary connections are closed instead, before the h2 preface/SETTINGS. Tests in the standard library (where it's easier to test), in the commit which updates h2_bundle.go with this change. Updates golang/go#13957 Change-Id: I5af177e6ea755d572b551cc0b0de9da865ef4ae7 Reviewed-on: https://go-review.googlesource.com/18675 Reviewed-by: Andrew Gerrand <[email protected]> Run-TryBot: Brad Fitzpatrick <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
1 parent 99ca920 commit a8e212f

File tree

2 files changed

+83
-12
lines changed

2 files changed

+83
-12
lines changed

http2/client_conn_pool.go

+73-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
package http2
88

99
import (
10+
"crypto/tls"
1011
"net/http"
1112
"sync"
1213
)
@@ -17,21 +18,29 @@ type ClientConnPool interface {
1718
MarkDead(*ClientConn)
1819
}
1920

21+
// TODO: use singleflight for dialing and addConnCalls?
2022
type clientConnPool struct {
21-
t *Transport
23+
t *Transport
24+
2225
mu sync.Mutex // TODO: maybe switch to RWMutex
2326
// TODO: add support for sharing conns based on cert names
2427
// (e.g. share conn for googleapis.com and appspot.com)
25-
conns map[string][]*ClientConn // key is host:port
26-
dialing map[string]*dialCall // currently in-flight dials
27-
keys map[*ClientConn][]string
28+
conns map[string][]*ClientConn // key is host:port
29+
dialing map[string]*dialCall // currently in-flight dials
30+
keys map[*ClientConn][]string
31+
addConnCalls map[string]*addConnCall // in-flight addConnIfNeede calls
2832
}
2933

3034
func (p *clientConnPool) GetClientConn(req *http.Request, addr string) (*ClientConn, error) {
31-
return p.getClientConn(req, addr, true)
35+
return p.getClientConn(req, addr, dialOnMiss)
3236
}
3337

34-
func (p *clientConnPool) getClientConn(req *http.Request, addr string, dialOnMiss bool) (*ClientConn, error) {
38+
const (
39+
dialOnMiss = true
40+
noDialOnMiss = false
41+
)
42+
43+
func (p *clientConnPool) getClientConn(_ *http.Request, addr string, dialOnMiss bool) (*ClientConn, error) {
3544
p.mu.Lock()
3645
for _, cc := range p.conns[addr] {
3746
if cc.CanTakeNewRequest() {
@@ -85,6 +94,64 @@ func (c *dialCall) dial(addr string) {
8594
c.p.mu.Unlock()
8695
}
8796

97+
// addConnIfNeeded makes a NewClientConn out of c if a connection for key doesn't
98+
// already exist. It coalesces concurrent calls with the same key.
99+
// This is used by the http1 Transport code when it creates a new connection. Because
100+
// the http1 Transport doesn't de-dup TCP dials to outbound hosts (because it doesn't know
101+
// the protocol), it can get into a situation where it has multiple TLS connections.
102+
// This code decides which ones live or die.
103+
// The return value used is whether c was used.
104+
// c is never closed.
105+
func (p *clientConnPool) addConnIfNeeded(key string, t *Transport, c *tls.Conn) (used bool, err error) {
106+
p.mu.Lock()
107+
for _, cc := range p.conns[key] {
108+
if cc.CanTakeNewRequest() {
109+
p.mu.Unlock()
110+
return false, nil
111+
}
112+
}
113+
call, dup := p.addConnCalls[key]
114+
if !dup {
115+
if p.addConnCalls == nil {
116+
p.addConnCalls = make(map[string]*addConnCall)
117+
}
118+
call = &addConnCall{
119+
p: p,
120+
done: make(chan struct{}),
121+
}
122+
p.addConnCalls[key] = call
123+
go call.run(t, key, c)
124+
}
125+
p.mu.Unlock()
126+
127+
<-call.done
128+
if call.err != nil {
129+
return false, call.err
130+
}
131+
return !dup, nil
132+
}
133+
134+
type addConnCall struct {
135+
p *clientConnPool
136+
done chan struct{} // closed when done
137+
err error
138+
}
139+
140+
func (c *addConnCall) run(t *Transport, key string, tc *tls.Conn) {
141+
cc, err := t.NewClientConn(tc)
142+
143+
p := c.p
144+
p.mu.Lock()
145+
if err != nil {
146+
c.err = err
147+
} else {
148+
p.addConnLocked(key, cc)
149+
}
150+
delete(p.addConnCalls, key)
151+
p.mu.Unlock()
152+
close(c.done)
153+
}
154+
88155
func (p *clientConnPool) addConn(key string, cc *ClientConn) {
89156
p.mu.Lock()
90157
p.addConnLocked(key, cc)

http2/configure_transport.go

+10-6
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,17 @@ func configureTransport(t1 *http.Transport) error {
2828
t1.TLSClientConfig.NextProtos = append(t1.TLSClientConfig.NextProtos, "http/1.1")
2929
}
3030
upgradeFn := func(authority string, c *tls.Conn) http.RoundTripper {
31-
cc, err := t2.NewClientConn(c)
32-
if err != nil {
33-
c.Close()
31+
addr := authorityAddr(authority)
32+
if used, err := connPool.addConnIfNeeded(addr, t2, c); err != nil {
33+
go c.Close()
3434
return erringRoundTripper{err}
35+
} else if !used {
36+
// Turns out we don't need this c.
37+
// For example, two goroutines made requests to the same host
38+
// at the same time, both kicking off TCP dials. (since protocol
39+
// was unknown)
40+
go c.Close()
3541
}
36-
connPool.addConn(authorityAddr(authority), cc)
3742
return t2
3843
}
3944
if m := t1.TLSNextProto; len(m) == 0 {
@@ -64,8 +69,7 @@ func registerHTTPSProtocol(t *http.Transport, rt http.RoundTripper) (err error)
6469
type noDialClientConnPool struct{ *clientConnPool }
6570

6671
func (p noDialClientConnPool) GetClientConn(req *http.Request, addr string) (*ClientConn, error) {
67-
const doDial = false
68-
return p.getClientConn(req, addr, doDial)
72+
return p.getClientConn(req, addr, noDialOnMiss)
6973
}
7074

7175
// noDialH2RoundTripper is a RoundTripper which only tries to complete the request

0 commit comments

Comments
 (0)