Commit abc1472d authored by Brad Fitzpatrick's avatar Brad Fitzpatrick

net/http: add Transport.IdleConnTimeout

Don't keep idle HTTP client connections open forever. Add a new knob,
Transport.IdleConnTimeout, and make the default be 90 seconds. I
figure 90 seconds is more than a minute, and less than infinite, and I
figure enough code has things waking up once a minute polling APIs.

This also removes the Transport's idleCount field which was unused and
redundant with the size of the idleLRU map (which was actually used).

Change-Id: Ibb698a9a9a26f28e00a20fe7ed23f4afb20c2322
Reviewed-on: https://go-review.googlesource.com/22670Reviewed-by: 's avatarAndrew Gerrand <adg@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
parent 0ab78df9
...@@ -81,9 +81,6 @@ func (t *Transport) IdleConnKeysForTesting() (keys []string) { ...@@ -81,9 +81,6 @@ func (t *Transport) IdleConnKeysForTesting() (keys []string) {
keys = make([]string, 0) keys = make([]string, 0)
t.idleMu.Lock() t.idleMu.Lock()
defer t.idleMu.Unlock() defer t.idleMu.Unlock()
if t.idleConn == nil {
return
}
for key := range t.idleConn { for key := range t.idleConn {
keys = append(keys, key.String()) keys = append(keys, key.String())
} }
...@@ -91,12 +88,22 @@ func (t *Transport) IdleConnKeysForTesting() (keys []string) { ...@@ -91,12 +88,22 @@ func (t *Transport) IdleConnKeysForTesting() (keys []string) {
return return
} }
func (t *Transport) IdleConnCountForTesting(cacheKey string) int { func (t *Transport) IdleConnStrsForTesting() []string {
var ret []string
t.idleMu.Lock() t.idleMu.Lock()
defer t.idleMu.Unlock() defer t.idleMu.Unlock()
if t.idleConn == nil { for _, conns := range t.idleConn {
return 0 for _, pc := range conns {
ret = append(ret, pc.conn.LocalAddr().String()+"/"+pc.conn.RemoteAddr().String())
}
} }
sort.Strings(ret)
return ret
}
func (t *Transport) IdleConnCountForTesting(cacheKey string) int {
t.idleMu.Lock()
defer t.idleMu.Unlock()
for k, conns := range t.idleConn { for k, conns := range t.idleConn {
if k.String() == cacheKey { if k.String() == cacheKey {
return len(conns) return len(conns)
......
...@@ -40,6 +40,7 @@ var DefaultTransport RoundTripper = &Transport{ ...@@ -40,6 +40,7 @@ var DefaultTransport RoundTripper = &Transport{
KeepAlive: 30 * time.Second, KeepAlive: 30 * time.Second,
}, },
MaxIdleConns: 100, MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second, TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second, ExpectContinueTimeout: 1 * time.Second,
} }
...@@ -68,7 +69,6 @@ const DefaultMaxIdleConnsPerHost = 2 ...@@ -68,7 +69,6 @@ const DefaultMaxIdleConnsPerHost = 2
type Transport struct { type Transport struct {
idleMu sync.Mutex idleMu sync.Mutex
wantIdle bool // user has requested to close all idle conns wantIdle bool // user has requested to close all idle conns
idleCount int
idleConn map[connectMethodKey][]*persistConn idleConn map[connectMethodKey][]*persistConn
idleConnCh map[connectMethodKey]chan *persistConn idleConnCh map[connectMethodKey]chan *persistConn
idleLRU connLRU idleLRU connLRU
...@@ -139,6 +139,12 @@ type Transport struct { ...@@ -139,6 +139,12 @@ type Transport struct {
// DefaultMaxIdleConnsPerHost is used. // DefaultMaxIdleConnsPerHost is used.
MaxIdleConnsPerHost int MaxIdleConnsPerHost int
// IdleConnTimeout is the maximum amount of time an idle
// (keep-alive) connection will remain idle before closing
// itself.
// Zero means no limit.
IdleConnTimeout time.Duration
// ResponseHeaderTimeout, if non-zero, specifies the amount of // ResponseHeaderTimeout, if non-zero, specifies the amount of
// time to wait for a server's response headers after fully // time to wait for a server's response headers after fully
// writing the request (including its body, if any). This // writing the request (including its body, if any). This
...@@ -462,7 +468,6 @@ func (t *Transport) CloseIdleConnections() { ...@@ -462,7 +468,6 @@ func (t *Transport) CloseIdleConnections() {
t.idleConn = nil t.idleConn = nil
t.idleConnCh = nil t.idleConnCh = nil
t.wantIdle = true t.wantIdle = true
t.idleCount = 0
t.idleLRU = connLRU{} t.idleLRU = connLRU{}
t.idleMu.Unlock() t.idleMu.Unlock()
for _, conns := range m { for _, conns := range m {
...@@ -568,6 +573,7 @@ var ( ...@@ -568,6 +573,7 @@ var (
errCloseIdleConns = errors.New("http: CloseIdleConnections called") errCloseIdleConns = errors.New("http: CloseIdleConnections called")
errReadLoopExiting = errors.New("http: persistConn.readLoop exiting") errReadLoopExiting = errors.New("http: persistConn.readLoop exiting")
errServerClosedIdle = errors.New("http: server closed idle conn") errServerClosedIdle = errors.New("http: server closed idle conn")
errIdleConnTimeout = errors.New("http: idle connection timeout")
) )
func (t *Transport) putOrCloseIdleConn(pconn *persistConn) { func (t *Transport) putOrCloseIdleConn(pconn *persistConn) {
...@@ -633,13 +639,19 @@ func (t *Transport) tryPutIdleConn(pconn *persistConn) error { ...@@ -633,13 +639,19 @@ func (t *Transport) tryPutIdleConn(pconn *persistConn) error {
} }
} }
t.idleConn[key] = append(idles, pconn) t.idleConn[key] = append(idles, pconn)
t.idleCount++
t.idleLRU.add(pconn) t.idleLRU.add(pconn)
if t.MaxIdleConns != 0 && t.idleLRU.len() > t.MaxIdleConns { if t.MaxIdleConns != 0 && t.idleLRU.len() > t.MaxIdleConns {
oldest := t.idleLRU.removeOldest() oldest := t.idleLRU.removeOldest()
oldest.close(errTooManyIdle) oldest.close(errTooManyIdle)
t.removeIdleConnLocked(oldest) t.removeIdleConnLocked(oldest)
} }
if t.IdleConnTimeout > 0 {
if pconn.idleTimer != nil {
pconn.idleTimer.Reset(t.IdleConnTimeout)
} else {
pconn.idleTimer = time.AfterFunc(t.IdleConnTimeout, pconn.closeConnIfStillIdle)
}
}
pconn.idleAt = time.Now() pconn.idleAt = time.Now()
return nil return nil
} }
...@@ -684,7 +696,6 @@ func (t *Transport) getIdleConn(cm connectMethod) (pconn *persistConn, idleSince ...@@ -684,7 +696,6 @@ func (t *Transport) getIdleConn(cm connectMethod) (pconn *persistConn, idleSince
pconn = pconns[len(pconns)-1] pconn = pconns[len(pconns)-1]
t.idleConn[key] = pconns[:len(pconns)-1] t.idleConn[key] = pconns[:len(pconns)-1]
} }
t.idleCount--
t.idleLRU.remove(pconn) t.idleLRU.remove(pconn)
if pconn.isBroken() { if pconn.isBroken() {
// There is a tiny window where this is // There is a tiny window where this is
...@@ -694,6 +705,12 @@ func (t *Transport) getIdleConn(cm connectMethod) (pconn *persistConn, idleSince ...@@ -694,6 +705,12 @@ func (t *Transport) getIdleConn(cm connectMethod) (pconn *persistConn, idleSince
// carry on. // carry on.
continue continue
} }
if pconn.idleTimer != nil && !pconn.idleTimer.Stop() {
// We picked this conn at the ~same time it
// was expiring and it's trying to close
// itself in another goroutine. Don't use it.
continue
}
return pconn, pconn.idleAt return pconn, pconn.idleAt
} }
} }
...@@ -707,6 +724,9 @@ func (t *Transport) removeIdleConn(pconn *persistConn) { ...@@ -707,6 +724,9 @@ func (t *Transport) removeIdleConn(pconn *persistConn) {
// t.idleMu must be held. // t.idleMu must be held.
func (t *Transport) removeIdleConnLocked(pconn *persistConn) { func (t *Transport) removeIdleConnLocked(pconn *persistConn) {
if pconn.idleTimer != nil {
pconn.idleTimer.Stop()
}
t.idleLRU.remove(pconn) t.idleLRU.remove(pconn)
key := pconn.cacheKey key := pconn.cacheKey
pconns, _ := t.idleConn[key] pconns, _ := t.idleConn[key]
...@@ -715,7 +735,6 @@ func (t *Transport) removeIdleConnLocked(pconn *persistConn) { ...@@ -715,7 +735,6 @@ func (t *Transport) removeIdleConnLocked(pconn *persistConn) {
// Nothing // Nothing
case 1: case 1:
if pconns[0] == pconn { if pconns[0] == pconn {
t.idleCount--
delete(t.idleConn, key) delete(t.idleConn, key)
} }
default: default:
...@@ -725,7 +744,6 @@ func (t *Transport) removeIdleConnLocked(pconn *persistConn) { ...@@ -725,7 +744,6 @@ func (t *Transport) removeIdleConnLocked(pconn *persistConn) {
} }
pconns[i] = pconns[len(pconns)-1] pconns[i] = pconns[len(pconns)-1]
t.idleConn[key] = pconns[:len(pconns)-1] t.idleConn[key] = pconns[:len(pconns)-1]
t.idleCount--
break break
} }
} }
...@@ -845,7 +863,7 @@ func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (*persistC ...@@ -845,7 +863,7 @@ func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (*persistC
// But our dial is still going, so give it away // But our dial is still going, so give it away
// when it finishes: // when it finishes:
handlePendingDial() handlePendingDial()
if trace != nil { if trace != nil && trace.GotConn != nil {
trace.GotConn(httptrace.GotConnInfo{Conn: pc.conn, Reused: pc.isReused()}) trace.GotConn(httptrace.GotConnInfo{Conn: pc.conn, Reused: pc.isReused()})
} }
return pc, nil return pc, nil
...@@ -1136,7 +1154,9 @@ type persistConn struct { ...@@ -1136,7 +1154,9 @@ type persistConn struct {
// whether or not a connection can be reused. Issue 7569. // whether or not a connection can be reused. Issue 7569.
writeErrCh chan error writeErrCh chan error
idleAt time.Time // time it last become idle; guarded by Transport.idleMu // Both guarded by Transport.idleMu:
idleAt time.Time // time it last become idle
idleTimer *time.Timer // holding an AfterFunc to close it
mu sync.Mutex // guards following fields mu sync.Mutex // guards following fields
numExpectedResponses int numExpectedResponses int
...@@ -1212,6 +1232,21 @@ func (pc *persistConn) cancelRequest() { ...@@ -1212,6 +1232,21 @@ func (pc *persistConn) cancelRequest() {
pc.closeLocked(errRequestCanceled) pc.closeLocked(errRequestCanceled)
} }
// closeConnIfStillIdle closes the connection if it's still sitting idle.
// This is what's called by the persistConn's idleTimer, and is run in its
// own goroutine.
func (pc *persistConn) closeConnIfStillIdle() {
t := pc.t
t.idleMu.Lock()
defer t.idleMu.Unlock()
if _, ok := t.idleLRU.m[pc]; !ok {
// Not idle.
return
}
t.removeIdleConnLocked(pc)
pc.close(errIdleConnTimeout)
}
func (pc *persistConn) readLoop() { func (pc *persistConn) readLoop() {
closeErr := errReadLoopExiting // default value, if not changed below closeErr := errReadLoopExiting // default value, if not changed below
defer func() { defer func() {
......
...@@ -3359,6 +3359,60 @@ func TestTransportMaxIdleConns(t *testing.T) { ...@@ -3359,6 +3359,60 @@ func TestTransportMaxIdleConns(t *testing.T) {
} }
} }
func TestTransportIdleConnTimeout(t *testing.T) {
if testing.Short() {
t.Skip("skipping in short mode")
}
defer afterTest(t)
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
// No body for convenience.
}))
defer ts.Close()
const timeout = 1 * time.Second
tr := &Transport{
IdleConnTimeout: timeout,
}
defer tr.CloseIdleConnections()
c := &Client{Transport: tr}
var conn string
doReq := func(n int) {
req, _ := NewRequest("GET", ts.URL, nil)
req = req.WithContext(httptrace.WithClientTrace(context.Background(), &httptrace.ClientTrace{
PutIdleConn: func(err error) {
if err != nil {
t.Errorf("failed to keep idle conn: %v", err)
}
},
}))
res, err := c.Do(req)
if err != nil {
t.Fatal(err)
}
res.Body.Close()
conns := tr.IdleConnStrsForTesting()
if len(conns) != 1 {
t.Fatalf("req %v: unexpected number of idle conns: %q", n, conns)
}
if conn == "" {
conn = conns[0]
}
if conn != conns[0] {
t.Fatalf("req %v: cached connection changed; expected the same one throughout the test", n)
}
}
for i := 0; i < 3; i++ {
doReq(i)
time.Sleep(timeout / 2)
}
time.Sleep(timeout * 3 / 2)
if got := tr.IdleConnStrsForTesting(); len(got) != 0 {
t.Errorf("idle conns = %q; want none", got)
}
}
var errFakeRoundTrip = errors.New("fake roundtrip") var errFakeRoundTrip = errors.New("fake roundtrip")
type funcRoundTripper func() type funcRoundTripper func()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment