blob: c936843eafa151d3164dcc2481d66a42cface736 [file] [log] [blame]
kesavand2cde6582020-06-22 04:56:23 -04001// Copyright 2015 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// Transport code's client connection pooling.
6
7package http2
8
9import (
kesavandc71914f2022-03-25 11:19:03 +053010 "context"
kesavand2cde6582020-06-22 04:56:23 -040011 "crypto/tls"
kesavandc71914f2022-03-25 11:19:03 +053012 "errors"
kesavand2cde6582020-06-22 04:56:23 -040013 "net/http"
14 "sync"
15)
16
17// ClientConnPool manages a pool of HTTP/2 client connections.
18type ClientConnPool interface {
kesavandc71914f2022-03-25 11:19:03 +053019 // GetClientConn returns a specific HTTP/2 connection (usually
20 // a TLS-TCP connection) to an HTTP/2 server. On success, the
21 // returned ClientConn accounts for the upcoming RoundTrip
22 // call, so the caller should not omit it. If the caller needs
23 // to, ClientConn.RoundTrip can be called with a bogus
24 // new(http.Request) to release the stream reservation.
kesavand2cde6582020-06-22 04:56:23 -040025 GetClientConn(req *http.Request, addr string) (*ClientConn, error)
26 MarkDead(*ClientConn)
27}
28
29// clientConnPoolIdleCloser is the interface implemented by ClientConnPool
30// implementations which can close their idle connections.
31type clientConnPoolIdleCloser interface {
32 ClientConnPool
33 closeIdleConnections()
34}
35
36var (
37 _ clientConnPoolIdleCloser = (*clientConnPool)(nil)
38 _ clientConnPoolIdleCloser = noDialClientConnPool{}
39)
40
41// TODO: use singleflight for dialing and addConnCalls?
42type clientConnPool struct {
43 t *Transport
44
45 mu sync.Mutex // TODO: maybe switch to RWMutex
46 // TODO: add support for sharing conns based on cert names
47 // (e.g. share conn for googleapis.com and appspot.com)
48 conns map[string][]*ClientConn // key is host:port
49 dialing map[string]*dialCall // currently in-flight dials
50 keys map[*ClientConn][]string
kesavandc71914f2022-03-25 11:19:03 +053051 addConnCalls map[string]*addConnCall // in-flight addConnIfNeeded calls
kesavand2cde6582020-06-22 04:56:23 -040052}
53
54func (p *clientConnPool) GetClientConn(req *http.Request, addr string) (*ClientConn, error) {
55 return p.getClientConn(req, addr, dialOnMiss)
56}
57
58const (
59 dialOnMiss = true
60 noDialOnMiss = false
61)
62
kesavand2cde6582020-06-22 04:56:23 -040063func (p *clientConnPool) getClientConn(req *http.Request, addr string, dialOnMiss bool) (*ClientConn, error) {
kesavandc71914f2022-03-25 11:19:03 +053064 // TODO(dneil): Dial a new connection when t.DisableKeepAlives is set?
kesavand2cde6582020-06-22 04:56:23 -040065 if isConnectionCloseRequest(req) && dialOnMiss {
66 // It gets its own connection.
67 traceGetConn(req, addr)
68 const singleUse = true
kesavandc71914f2022-03-25 11:19:03 +053069 cc, err := p.t.dialClientConn(req.Context(), addr, singleUse)
kesavand2cde6582020-06-22 04:56:23 -040070 if err != nil {
71 return nil, err
72 }
73 return cc, nil
74 }
kesavandc71914f2022-03-25 11:19:03 +053075 for {
76 p.mu.Lock()
77 for _, cc := range p.conns[addr] {
78 if cc.ReserveNewRequest() {
79 // When a connection is presented to us by the net/http package,
80 // the GetConn hook has already been called.
81 // Don't call it a second time here.
82 if !cc.getConnCalled {
83 traceGetConn(req, addr)
84 }
85 cc.getConnCalled = false
86 p.mu.Unlock()
87 return cc, nil
kesavand2cde6582020-06-22 04:56:23 -040088 }
kesavandc71914f2022-03-25 11:19:03 +053089 }
90 if !dialOnMiss {
kesavand2cde6582020-06-22 04:56:23 -040091 p.mu.Unlock()
kesavandc71914f2022-03-25 11:19:03 +053092 return nil, ErrNoCachedConn
93 }
94 traceGetConn(req, addr)
95 call := p.getStartDialLocked(req.Context(), addr)
96 p.mu.Unlock()
97 <-call.done
98 if shouldRetryDial(call, req) {
99 continue
100 }
101 cc, err := call.res, call.err
102 if err != nil {
103 return nil, err
104 }
105 if cc.ReserveNewRequest() {
kesavand2cde6582020-06-22 04:56:23 -0400106 return cc, nil
107 }
108 }
kesavand2cde6582020-06-22 04:56:23 -0400109}
110
111// dialCall is an in-flight Transport dial call to a host.
112type dialCall struct {
kesavandc71914f2022-03-25 11:19:03 +0530113 _ incomparable
114 p *clientConnPool
115 // the context associated with the request
116 // that created this dialCall
117 ctx context.Context
kesavand2cde6582020-06-22 04:56:23 -0400118 done chan struct{} // closed when done
119 res *ClientConn // valid after done is closed
120 err error // valid after done is closed
121}
122
123// requires p.mu is held.
kesavandc71914f2022-03-25 11:19:03 +0530124func (p *clientConnPool) getStartDialLocked(ctx context.Context, addr string) *dialCall {
kesavand2cde6582020-06-22 04:56:23 -0400125 if call, ok := p.dialing[addr]; ok {
126 // A dial is already in-flight. Don't start another.
127 return call
128 }
kesavandc71914f2022-03-25 11:19:03 +0530129 call := &dialCall{p: p, done: make(chan struct{}), ctx: ctx}
kesavand2cde6582020-06-22 04:56:23 -0400130 if p.dialing == nil {
131 p.dialing = make(map[string]*dialCall)
132 }
133 p.dialing[addr] = call
kesavandc71914f2022-03-25 11:19:03 +0530134 go call.dial(call.ctx, addr)
kesavand2cde6582020-06-22 04:56:23 -0400135 return call
136}
137
138// run in its own goroutine.
kesavandc71914f2022-03-25 11:19:03 +0530139func (c *dialCall) dial(ctx context.Context, addr string) {
kesavand2cde6582020-06-22 04:56:23 -0400140 const singleUse = false // shared conn
kesavandc71914f2022-03-25 11:19:03 +0530141 c.res, c.err = c.p.t.dialClientConn(ctx, addr, singleUse)
kesavand2cde6582020-06-22 04:56:23 -0400142 close(c.done)
143
144 c.p.mu.Lock()
145 delete(c.p.dialing, addr)
146 if c.err == nil {
147 c.p.addConnLocked(addr, c.res)
148 }
149 c.p.mu.Unlock()
150}
151
152// addConnIfNeeded makes a NewClientConn out of c if a connection for key doesn't
153// already exist. It coalesces concurrent calls with the same key.
154// This is used by the http1 Transport code when it creates a new connection. Because
155// the http1 Transport doesn't de-dup TCP dials to outbound hosts (because it doesn't know
156// the protocol), it can get into a situation where it has multiple TLS connections.
157// This code decides which ones live or die.
158// The return value used is whether c was used.
159// c is never closed.
160func (p *clientConnPool) addConnIfNeeded(key string, t *Transport, c *tls.Conn) (used bool, err error) {
161 p.mu.Lock()
162 for _, cc := range p.conns[key] {
163 if cc.CanTakeNewRequest() {
164 p.mu.Unlock()
165 return false, nil
166 }
167 }
168 call, dup := p.addConnCalls[key]
169 if !dup {
170 if p.addConnCalls == nil {
171 p.addConnCalls = make(map[string]*addConnCall)
172 }
173 call = &addConnCall{
174 p: p,
175 done: make(chan struct{}),
176 }
177 p.addConnCalls[key] = call
178 go call.run(t, key, c)
179 }
180 p.mu.Unlock()
181
182 <-call.done
183 if call.err != nil {
184 return false, call.err
185 }
186 return !dup, nil
187}
188
189type addConnCall struct {
Andrea Campanella764f1ed2022-03-24 11:46:38 +0100190 _ incomparable
kesavand2cde6582020-06-22 04:56:23 -0400191 p *clientConnPool
192 done chan struct{} // closed when done
193 err error
194}
195
196func (c *addConnCall) run(t *Transport, key string, tc *tls.Conn) {
197 cc, err := t.NewClientConn(tc)
198
199 p := c.p
200 p.mu.Lock()
201 if err != nil {
202 c.err = err
203 } else {
kesavandc71914f2022-03-25 11:19:03 +0530204 cc.getConnCalled = true // already called by the net/http package
kesavand2cde6582020-06-22 04:56:23 -0400205 p.addConnLocked(key, cc)
206 }
207 delete(p.addConnCalls, key)
208 p.mu.Unlock()
209 close(c.done)
210}
211
kesavand2cde6582020-06-22 04:56:23 -0400212// p.mu must be held
213func (p *clientConnPool) addConnLocked(key string, cc *ClientConn) {
214 for _, v := range p.conns[key] {
215 if v == cc {
216 return
217 }
218 }
219 if p.conns == nil {
220 p.conns = make(map[string][]*ClientConn)
221 }
222 if p.keys == nil {
223 p.keys = make(map[*ClientConn][]string)
224 }
225 p.conns[key] = append(p.conns[key], cc)
226 p.keys[cc] = append(p.keys[cc], key)
227}
228
229func (p *clientConnPool) MarkDead(cc *ClientConn) {
230 p.mu.Lock()
231 defer p.mu.Unlock()
232 for _, key := range p.keys[cc] {
233 vv, ok := p.conns[key]
234 if !ok {
235 continue
236 }
237 newList := filterOutClientConn(vv, cc)
238 if len(newList) > 0 {
239 p.conns[key] = newList
240 } else {
241 delete(p.conns, key)
242 }
243 }
244 delete(p.keys, cc)
245}
246
247func (p *clientConnPool) closeIdleConnections() {
248 p.mu.Lock()
249 defer p.mu.Unlock()
250 // TODO: don't close a cc if it was just added to the pool
251 // milliseconds ago and has never been used. There's currently
252 // a small race window with the HTTP/1 Transport's integration
253 // where it can add an idle conn just before using it, and
254 // somebody else can concurrently call CloseIdleConns and
255 // break some caller's RoundTrip.
256 for _, vv := range p.conns {
257 for _, cc := range vv {
258 cc.closeIfIdle()
259 }
260 }
261}
262
263func filterOutClientConn(in []*ClientConn, exclude *ClientConn) []*ClientConn {
264 out := in[:0]
265 for _, v := range in {
266 if v != exclude {
267 out = append(out, v)
268 }
269 }
270 // If we filtered it out, zero out the last item to prevent
271 // the GC from seeing it.
272 if len(in) != len(out) {
273 in[len(in)-1] = nil
274 }
275 return out
276}
277
278// noDialClientConnPool is an implementation of http2.ClientConnPool
279// which never dials. We let the HTTP/1.1 client dial and use its TLS
280// connection instead.
281type noDialClientConnPool struct{ *clientConnPool }
282
283func (p noDialClientConnPool) GetClientConn(req *http.Request, addr string) (*ClientConn, error) {
284 return p.getClientConn(req, addr, noDialOnMiss)
285}
kesavandc71914f2022-03-25 11:19:03 +0530286
287// shouldRetryDial reports whether the current request should
288// retry dialing after the call finished unsuccessfully, for example
289// if the dial was canceled because of a context cancellation or
290// deadline expiry.
291func shouldRetryDial(call *dialCall, req *http.Request) bool {
292 if call.err == nil {
293 // No error, no need to retry
294 return false
295 }
296 if call.ctx == req.Context() {
297 // If the call has the same context as the request, the dial
298 // should not be retried, since any cancellation will have come
299 // from this request.
300 return false
301 }
302 if !errors.Is(call.err, context.Canceled) && !errors.Is(call.err, context.DeadlineExceeded) {
303 // If the call error is not because of a context cancellation or a deadline expiry,
304 // the dial should not be retried.
305 return false
306 }
307 // Only retry if the error is a context cancellation error or deadline expiry
308 // and the context associated with the call was canceled or expired.
309 return call.ctx.Err() != nil
310}