KeepAlive: 30 * time.Second,
},
MaxIdleConns: 100,
+ IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
type Transport struct {
idleMu sync.Mutex
wantIdle bool // user has requested to close all idle conns
- idleCount int
idleConn map[connectMethodKey][]*persistConn
idleConnCh map[connectMethodKey]chan *persistConn
idleLRU connLRU
// DefaultMaxIdleConnsPerHost is used.
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
// time to wait for a server's response headers after fully
// writing the request (including its body, if any). This
t.idleConn = nil
t.idleConnCh = nil
t.wantIdle = true
- t.idleCount = 0
t.idleLRU = connLRU{}
t.idleMu.Unlock()
for _, conns := range m {
errCloseIdleConns = errors.New("http: CloseIdleConnections called")
errReadLoopExiting = errors.New("http: persistConn.readLoop exiting")
errServerClosedIdle = errors.New("http: server closed idle conn")
+ errIdleConnTimeout = errors.New("http: idle connection timeout")
)
func (t *Transport) putOrCloseIdleConn(pconn *persistConn) {
}
}
t.idleConn[key] = append(idles, pconn)
- t.idleCount++
t.idleLRU.add(pconn)
if t.MaxIdleConns != 0 && t.idleLRU.len() > t.MaxIdleConns {
oldest := t.idleLRU.removeOldest()
oldest.close(errTooManyIdle)
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()
return nil
}
pconn = pconns[len(pconns)-1]
t.idleConn[key] = pconns[:len(pconns)-1]
}
- t.idleCount--
t.idleLRU.remove(pconn)
if pconn.isBroken() {
// There is a tiny window where this is
// carry on.
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
}
}
// t.idleMu must be held.
func (t *Transport) removeIdleConnLocked(pconn *persistConn) {
+ if pconn.idleTimer != nil {
+ pconn.idleTimer.Stop()
+ }
t.idleLRU.remove(pconn)
key := pconn.cacheKey
pconns, _ := t.idleConn[key]
// Nothing
case 1:
if pconns[0] == pconn {
- t.idleCount--
delete(t.idleConn, key)
}
default:
}
pconns[i] = pconns[len(pconns)-1]
t.idleConn[key] = pconns[:len(pconns)-1]
- t.idleCount--
break
}
}
// But our dial is still going, so give it away
// when it finishes:
handlePendingDial()
- if trace != nil {
+ if trace != nil && trace.GotConn != nil {
trace.GotConn(httptrace.GotConnInfo{Conn: pc.conn, Reused: pc.isReused()})
}
return pc, nil
// whether or not a connection can be reused. Issue 7569.
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
numExpectedResponses int
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() {
closeErr := errReadLoopExiting // default value, if not changed below
defer func() {
}
}
+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")
type funcRoundTripper func()