// Copyright 2015 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Transport code's client connection pooling. package http2 import ( "net/http" "sync" ) // ClientConnPool manages a pool of HTTP/2 client connections. type ClientConnPool interface { GetClientConn(req *http.Request, addr string) (*ClientConn, error) MarkDead(*ClientConn) } type clientConnPool struct { t *Transport mu sync.Mutex // TODO: maybe switch to RWMutex // TODO: add support for sharing conns based on cert names // (e.g. share conn for googleapis.com and appspot.com) conns map[string][]*ClientConn // key is host:port dialing map[string]*dialCall // currently in-flight dials keys map[*ClientConn][]string } func (p *clientConnPool) GetClientConn(req *http.Request, addr string) (*ClientConn, error) { return p.getClientConn(req, addr, true) } func (p *clientConnPool) getClientConn(req *http.Request, addr string, dialOnMiss bool) (*ClientConn, error) { p.mu.Lock() for _, cc := range p.conns[addr] { if cc.CanTakeNewRequest() { p.mu.Unlock() return cc, nil } } if !dialOnMiss { p.mu.Unlock() return nil, ErrNoCachedConn } call := p.getStartDialLocked(addr) p.mu.Unlock() <-call.done return call.res, call.err } // dialCall is an in-flight Transport dial call to a host. type dialCall struct { p *clientConnPool done chan struct{} // closed when done res *ClientConn // valid after done is closed err error // valid after done is closed } // requires p.mu is held. func (p *clientConnPool) getStartDialLocked(addr string) *dialCall { if call, ok := p.dialing[addr]; ok { // A dial is already in-flight. Don't start another. return call } call := &dialCall{p: p, done: make(chan struct{})} if p.dialing == nil { p.dialing = make(map[string]*dialCall) } p.dialing[addr] = call go call.dial(addr) return call } // run in its own goroutine. func (c *dialCall) dial(addr string) { c.res, c.err = c.p.t.dialClientConn(addr) close(c.done) c.p.mu.Lock() delete(c.p.dialing, addr) if c.err == nil { c.p.addConnLocked(addr, c.res) } c.p.mu.Unlock() } func (p *clientConnPool) addConn(key string, cc *ClientConn) { p.mu.Lock() p.addConnLocked(key, cc) p.mu.Unlock() } // p.mu must be held func (p *clientConnPool) addConnLocked(key string, cc *ClientConn) { for _, v := range p.conns[key] { if v == cc { return } } if p.conns == nil { p.conns = make(map[string][]*ClientConn) } if p.keys == nil { p.keys = make(map[*ClientConn][]string) } p.conns[key] = append(p.conns[key], cc) p.keys[cc] = append(p.keys[cc], key) } func (p *clientConnPool) MarkDead(cc *ClientConn) { p.mu.Lock() defer p.mu.Unlock() for _, key := range p.keys[cc] { vv, ok := p.conns[key] if !ok { continue } newList := filterOutClientConn(vv, cc) if len(newList) > 0 { p.conns[key] = newList } else { delete(p.conns, key) } } delete(p.keys, cc) } func (p *clientConnPool) closeIdleConnections() { p.mu.Lock() defer p.mu.Unlock() // TODO: don't close a cc if it was just added to the pool // milliseconds ago and has never been used. There's currently // a small race window with the HTTP/1 Transport's integration // where it can add an idle conn just before using it, and // somebody else can concurrently call CloseIdleConns and // break some caller's RoundTrip. for _, vv := range p.conns { for _, cc := range vv { cc.closeIfIdle() } } } func filterOutClientConn(in []*ClientConn, exclude *ClientConn) []*ClientConn { out := in[:0] for _, v := range in { if v != exclude { out = append(out, v) } } // If we filtered it out, zero out the last item to prevent // the GC from seeing it. if len(in) != len(out) { in[len(in)-1] = nil } return out }