github.com/useflyent/fhttp@v0.0.0-20211004035111-333f430cfbbf/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 "crypto/tls" 11 "sync" 12 13 http "github.com/useflyent/fhttp" 14 ) 15 16 // ClientConnPool manages a pool of HTTP/2 client connections. 17 type ClientConnPool interface { 18 GetClientConn(req *http.Request, addr string) (*ClientConn, error) 19 MarkDead(*ClientConn) 20 } 21 22 // clientConnPoolIdleCloser is the interface implemented by ClientConnPool 23 // implementations which can close their idle connections. 24 type clientConnPoolIdleCloser interface { 25 ClientConnPool 26 closeIdleConnections() 27 } 28 29 var ( 30 _ clientConnPoolIdleCloser = (*clientConnPool)(nil) 31 _ clientConnPoolIdleCloser = noDialClientConnPool{} 32 ) 33 34 // TODO: use singleflight for dialing and addConnCalls? 35 type clientConnPool struct { 36 t *Transport 37 38 mu sync.Mutex // TODO: maybe switch to RWMutex 39 // TODO: add support for sharing conns based on cert names 40 // (e.g. share conn for googleapis.com and appspot.com) 41 conns map[string][]*ClientConn // key is host:port 42 dialing map[string]*dialCall // currently in-flight dials 43 keys map[*ClientConn][]string 44 addConnCalls map[string]*addConnCall // in-flight addConnIfNeede calls 45 } 46 47 func (p *clientConnPool) GetClientConn(req *http.Request, addr string) (*ClientConn, error) { 48 return p.getClientConn(req, addr, dialOnMiss) 49 } 50 51 const ( 52 dialOnMiss = true 53 noDialOnMiss = false 54 ) 55 56 // shouldTraceGetConn reports whether getClientConn should call any 57 // ClientTrace.GetConn hook associated with the http.Request. 58 // 59 // This complexity is needed to avoid double calls of the GetConn hook 60 // during the back-and-forth between net/http and x/net/http2 (when the 61 // net/http.Transport is upgraded to also speak http2), as well as support 62 // the case where x/net/http2 is being used directly. 63 func (p *clientConnPool) shouldTraceGetConn(st clientConnIdleState) bool { 64 // If our Transport wasn't made via ConfigureTransport, always 65 // trace the GetConn hook if provided, because that means the 66 // http2 package is being used directly and it's the one 67 // dialing, as opposed to net/http. 68 if _, ok := p.t.ConnPool.(noDialClientConnPool); !ok { 69 return true 70 } 71 // Otherwise, only use the GetConn hook if this connection has 72 // been used previously for other requests. For fresh 73 // connections, the net/http package does the dialing. 74 return !st.freshConn 75 } 76 77 func (p *clientConnPool) getClientConn(req *http.Request, addr string, dialOnMiss bool) (*ClientConn, error) { 78 if isConnectionCloseRequest(req) && dialOnMiss { 79 // It gets its own connection. 80 traceGetConn(req, addr) 81 const singleUse = true 82 cc, err := p.t.dialClientConn(addr, singleUse) 83 if err != nil { 84 return nil, err 85 } 86 return cc, nil 87 } 88 p.mu.Lock() 89 for _, cc := range p.conns[addr] { 90 if st := cc.idleState(); st.canTakeNewRequest { 91 if p.shouldTraceGetConn(st) { 92 traceGetConn(req, addr) 93 } 94 p.mu.Unlock() 95 return cc, nil 96 } 97 } 98 if !dialOnMiss { 99 p.mu.Unlock() 100 return nil, ErrNoCachedConn 101 } 102 traceGetConn(req, addr) 103 call := p.getStartDialLocked(addr) 104 p.mu.Unlock() 105 <-call.done 106 return call.res, call.err 107 } 108 109 // dialCall is an in-flight Transport dial call to a host. 110 type dialCall struct { 111 _ incomparable 112 p *clientConnPool 113 done chan struct{} // closed when done 114 res *ClientConn // valid after done is closed 115 err error // valid after done is closed 116 } 117 118 // requires p.mu is held. 119 func (p *clientConnPool) getStartDialLocked(addr string) *dialCall { 120 if call, ok := p.dialing[addr]; ok { 121 // A dial is already in-flight. Don't start another. 122 return call 123 } 124 call := &dialCall{p: p, done: make(chan struct{})} 125 if p.dialing == nil { 126 p.dialing = make(map[string]*dialCall) 127 } 128 p.dialing[addr] = call 129 go call.dial(addr) 130 return call 131 } 132 133 // run in its own goroutine. 134 func (c *dialCall) dial(addr string) { 135 const singleUse = false // shared conn 136 c.res, c.err = c.p.t.dialClientConn(addr, singleUse) 137 close(c.done) 138 139 c.p.mu.Lock() 140 delete(c.p.dialing, addr) 141 if c.err == nil { 142 c.p.addConnLocked(addr, c.res) 143 } 144 c.p.mu.Unlock() 145 } 146 147 // addConnIfNeeded makes a NewClientConn out of c if a connection for key doesn't 148 // already exist. It coalesces concurrent calls with the same key. 149 // This is used by the http1 Transport code when it creates a new connection. Because 150 // the http1 Transport doesn't de-dup TCP dials to outbound hosts (because it doesn't know 151 // the protocol), it can get into a situation where it has multiple TLS connections. 152 // This code decides which ones live or die. 153 // The return value used is whether c was used. 154 // c is never closed. 155 func (p *clientConnPool) addConnIfNeeded(key string, t *Transport, c *tls.Conn) (used bool, err error) { 156 p.mu.Lock() 157 for _, cc := range p.conns[key] { 158 if cc.CanTakeNewRequest() { 159 p.mu.Unlock() 160 return false, nil 161 } 162 } 163 call, dup := p.addConnCalls[key] 164 if !dup { 165 if p.addConnCalls == nil { 166 p.addConnCalls = make(map[string]*addConnCall) 167 } 168 call = &addConnCall{ 169 p: p, 170 done: make(chan struct{}), 171 } 172 p.addConnCalls[key] = call 173 go call.run(t, key, c) 174 } 175 p.mu.Unlock() 176 177 <-call.done 178 if call.err != nil { 179 return false, call.err 180 } 181 return !dup, nil 182 } 183 184 type addConnCall struct { 185 _ incomparable 186 p *clientConnPool 187 done chan struct{} // closed when done 188 err error 189 } 190 191 func (c *addConnCall) run(t *Transport, key string, tc *tls.Conn) { 192 cc, err := t.NewClientConn(tc) 193 194 p := c.p 195 p.mu.Lock() 196 if err != nil { 197 c.err = err 198 } else { 199 p.addConnLocked(key, cc) 200 } 201 delete(p.addConnCalls, key) 202 p.mu.Unlock() 203 close(c.done) 204 } 205 206 // p.mu must be held 207 func (p *clientConnPool) addConnLocked(key string, cc *ClientConn) { 208 for _, v := range p.conns[key] { 209 if v == cc { 210 return 211 } 212 } 213 if p.conns == nil { 214 p.conns = make(map[string][]*ClientConn) 215 } 216 if p.keys == nil { 217 p.keys = make(map[*ClientConn][]string) 218 } 219 p.conns[key] = append(p.conns[key], cc) 220 p.keys[cc] = append(p.keys[cc], key) 221 } 222 223 func (p *clientConnPool) MarkDead(cc *ClientConn) { 224 p.mu.Lock() 225 defer p.mu.Unlock() 226 for _, key := range p.keys[cc] { 227 vv, ok := p.conns[key] 228 if !ok { 229 continue 230 } 231 newList := filterOutClientConn(vv, cc) 232 if len(newList) > 0 { 233 p.conns[key] = newList 234 } else { 235 delete(p.conns, key) 236 } 237 } 238 delete(p.keys, cc) 239 } 240 241 func (p *clientConnPool) closeIdleConnections() { 242 p.mu.Lock() 243 defer p.mu.Unlock() 244 // TODO: don't close a cc if it was just added to the pool 245 // milliseconds ago and has never been used. There's currently 246 // a small race window with the HTTP/1 Transport's integration 247 // where it can add an idle conn just before using it, and 248 // somebody else can concurrently call CloseIdleConns and 249 // break some caller's RoundTrip. 250 for _, vv := range p.conns { 251 for _, cc := range vv { 252 cc.closeIfIdle() 253 } 254 } 255 } 256 257 func filterOutClientConn(in []*ClientConn, exclude *ClientConn) []*ClientConn { 258 out := in[:0] 259 for _, v := range in { 260 if v != exclude { 261 out = append(out, v) 262 } 263 } 264 // If we filtered it out, zero out the last item to prevent 265 // the GC from seeing it. 266 if len(in) != len(out) { 267 in[len(in)-1] = nil 268 } 269 return out 270 } 271 272 // noDialClientConnPool is an implementation of http2.ClientConnPool 273 // which never dials. We let the HTTP/1.1 client dial and use its TLS 274 // connection instead. 275 type noDialClientConnPool struct{ *clientConnPool } 276 277 func (p noDialClientConnPool) GetClientConn(req *http.Request, addr string) (*ClientConn, error) { 278 return p.getClientConn(req, addr, noDialOnMiss) 279 }