mirror of
https://github.com/golang/go
synced 2024-11-12 04:50:21 -07:00
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/22670 Reviewed-by: Andrew Gerrand <adg@golang.org> Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org>
This commit is contained in:
parent
0ab78df9ea
commit
abc1472d78
@ -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) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Transport) IdleConnStrsForTesting() []string {
|
||||||
|
var ret []string
|
||||||
|
t.idleMu.Lock()
|
||||||
|
defer t.idleMu.Unlock()
|
||||||
|
for _, conns := range t.idleConn {
|
||||||
|
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 {
|
func (t *Transport) IdleConnCountForTesting(cacheKey string) int {
|
||||||
t.idleMu.Lock()
|
t.idleMu.Lock()
|
||||||
defer t.idleMu.Unlock()
|
defer t.idleMu.Unlock()
|
||||||
if t.idleConn == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
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{
|
|||||||
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
|
|||||||
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 {
|
|||||||
// 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() {
|
|||||||
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 (
|
|||||||
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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
|||||||
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
|
|||||||
// 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) {
|
|||||||
|
|
||||||
// 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) {
|
|||||||
// 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) {
|
|||||||
}
|
}
|
||||||
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
|
|||||||
// 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 {
|
|||||||
// 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() {
|
|||||||
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) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
Loading…
Reference in New Issue
Block a user