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