Skip to content

Commit a881869

Browse files
net/http: add MaxConnLifespan to Transport
Existing implementation appeared to have no way to set a connection max lifetime. A max lifetime allows for refreshing connection lifecycle concerns such as dns resolutions for those that need it. When initialized to a non-zero value the connection will be closed after the duration has passed. This change is backwards compatible. Fixes golang#23427
1 parent 7cf3268 commit a881869

File tree

4 files changed

+163
-10
lines changed

4 files changed

+163
-10
lines changed

api/next.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pkg net/http, type Transport struct, MaxConnLifespan time.Duration

src/net/http/h2_bundle.go

Lines changed: 44 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/net/http/transport.go

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,10 @@ type Transport struct {
189189
// uncompressed.
190190
DisableCompression bool
191191

192+
// MaxConnLifespan controls how long a connection is allowed
193+
// to be reused before it must be closed. Zero means no limit.
194+
MaxConnLifespan time.Duration
195+
192196
// MaxIdleConns controls the maximum number of idle (keep-alive)
193197
// connections across all hosts. Zero means no limit.
194198
MaxIdleConns int
@@ -316,6 +320,7 @@ func (t *Transport) Clone() *Transport {
316320
TLSHandshakeTimeout: t.TLSHandshakeTimeout,
317321
DisableKeepAlives: t.DisableKeepAlives,
318322
DisableCompression: t.DisableCompression,
323+
MaxConnLifespan: t.MaxConnLifespan,
319324
MaxIdleConns: t.MaxIdleConns,
320325
MaxIdleConnsPerHost: t.MaxIdleConnsPerHost,
321326
MaxConnsPerHost: t.MaxConnsPerHost,
@@ -987,14 +992,22 @@ func (t *Transport) tryPutIdleConn(pconn *persistConn) error {
987992
t.removeIdleConnLocked(oldest)
988993
}
989994

995+
ttl, hasTtl := pconn.timeToLive()
996+
990997
// Set idle timer, but only for HTTP/1 (pconn.alt == nil).
991998
// The HTTP/2 implementation manages the idle timer itself
992999
// (see idleConnTimeout in h2_bundle.go).
993-
if t.IdleConnTimeout > 0 && pconn.alt == nil {
1000+
if (hasTtl || t.IdleConnTimeout > 0) && pconn.alt == nil {
1001+
1002+
timeout := t.IdleConnTimeout
1003+
if hasTtl && (timeout <= 0 || ttl < timeout) {
1004+
timeout = ttl
1005+
}
1006+
9941007
if pconn.idleTimer != nil {
995-
pconn.idleTimer.Reset(t.IdleConnTimeout)
1008+
pconn.idleTimer.Reset(timeout)
9961009
} else {
997-
pconn.idleTimer = time.AfterFunc(t.IdleConnTimeout, pconn.closeConnIfStillIdle)
1010+
pconn.idleTimer = time.AfterFunc(timeout, pconn.closeConnIfStillIdle)
9981011
}
9991012
}
10001013
pconn.idleAt = time.Now()
@@ -1024,9 +1037,10 @@ func (t *Transport) queueForIdleConn(w *wantConn) (delivered bool) {
10241037
// If IdleConnTimeout is set, calculate the oldest
10251038
// persistConn.idleAt time we're willing to use a cached idle
10261039
// conn.
1040+
now := time.Now()
10271041
var oldTime time.Time
10281042
if t.IdleConnTimeout > 0 {
1029-
oldTime = time.Now().Add(-t.IdleConnTimeout)
1043+
oldTime = now.Add(-t.IdleConnTimeout)
10301044
}
10311045

10321046
// Look for most recently-used idle connection.
@@ -1039,7 +1053,8 @@ func (t *Transport) queueForIdleConn(w *wantConn) (delivered bool) {
10391053
// See whether this connection has been idle too long, considering
10401054
// only the wall time (the Round(0)), in case this is a laptop or VM
10411055
// coming out of suspend with previously cached idle connections.
1042-
tooOld := !oldTime.IsZero() && pconn.idleAt.Round(0).Before(oldTime)
1056+
tooOld := (!oldTime.IsZero() && pconn.idleAt.Round(0).Before(oldTime)) || (!pconn.reuseDeadline.IsZero() && pconn.reuseDeadline.Round(0).Before(now))
1057+
10431058
if tooOld {
10441059
// Async cleanup. Launch in its own goroutine (as if a
10451060
// time.AfterFunc called it); it acquires idleMu, which we're
@@ -1620,6 +1635,11 @@ func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (pconn *pers
16201635
}
16211636
}
16221637

1638+
var reuseDeadline time.Time
1639+
if t.MaxConnLifespan > 0 {
1640+
reuseDeadline = time.Now().Add(t.MaxConnLifespan)
1641+
}
1642+
16231643
// Proxy setup.
16241644
switch {
16251645
case cm.proxyURL == nil:
@@ -1740,10 +1760,11 @@ func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (pconn *pers
17401760
// pconn.conn was closed by next (http2configureTransports.upgradeFn).
17411761
return nil, e.RoundTripErr()
17421762
}
1743-
return &persistConn{t: t, cacheKey: pconn.cacheKey, alt: alt}, nil
1763+
return &persistConn{t: t, cacheKey: pconn.cacheKey, alt: alt, reuseDeadline: reuseDeadline}, nil
17441764
}
17451765
}
17461766

1767+
pconn.reuseDeadline = reuseDeadline
17471768
pconn.br = bufio.NewReaderSize(pconn, t.readBufferSize())
17481769
pconn.bw = bufio.NewWriterSize(persistConnWriter{pconn}, t.writeBufferSize())
17491770

@@ -1895,6 +1916,8 @@ type persistConn struct {
18951916

18961917
writeLoopDone chan struct{} // closed when write loop ends
18971918

1919+
reuseDeadline time.Time // time when this connection can no longer be reused
1920+
18981921
// Both guarded by Transport.idleMu:
18991922
idleAt time.Time // time it last become idle
19001923
idleTimer *time.Timer // holding an AfterFunc to close it
@@ -1911,6 +1934,30 @@ type persistConn struct {
19111934
mutateHeaderFunc func(Header)
19121935
}
19131936

1937+
// timeToLive checks if a persistent connection has been initialized
1938+
// from a transport with MaxConnLifespan > 0 and returns the time
1939+
// remaining for this connection to be reusable. The second response
1940+
// would be true in this case.
1941+
//
1942+
// If the connection has a zero-value reuseDeadline set then
1943+
// it returns (0, false)
1944+
//
1945+
// The returned duration will never be less than zero and the connection's
1946+
// idle time is NOT taken into account.
1947+
func (pc *persistConn) timeToLive() (time.Duration, bool) {
1948+
1949+
if pc.reuseDeadline.IsZero() {
1950+
return 0, false
1951+
}
1952+
1953+
ttl := time.Until(pc.reuseDeadline)
1954+
if ttl < 0 {
1955+
return 0, true
1956+
}
1957+
1958+
return ttl, true
1959+
}
1960+
19141961
func (pc *persistConn) maxHeaderResponseSize() int64 {
19151962
if v := pc.t.MaxResponseHeaderBytes; v != 0 {
19161963
return v

src/net/http/transport_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4920,6 +4920,70 @@ func TestTransportMaxIdleConns(t *testing.T) {
49204920
}
49214921
}
49224922

4923+
func TestTransportMaxConnLifespan_h1(t *testing.T) { testTransportMaxConnLifespan(t, h1Mode) }
4924+
func TestTransportMaxConnLifespan_h2(t *testing.T) { testTransportMaxConnLifespan(t, h2Mode) }
4925+
func testTransportMaxConnLifespan(t *testing.T, h2 bool) {
4926+
if testing.Short() {
4927+
t.Skip("skipping in short mode")
4928+
}
4929+
defer afterTest(t)
4930+
4931+
const timeout = 1 * time.Second
4932+
4933+
cst := newClientServerTest(t, h2, HandlerFunc(func(w ResponseWriter, r *Request) {
4934+
// No body for convenience.
4935+
}))
4936+
defer cst.close()
4937+
tr := cst.tr
4938+
tr.MaxConnLifespan = timeout
4939+
tr.IdleConnTimeout = timeout * 3
4940+
defer tr.CloseIdleConnections()
4941+
c := &Client{Transport: tr}
4942+
4943+
idleConns := func() []string {
4944+
if h2 {
4945+
return tr.IdleConnStrsForTesting_h2()
4946+
} else {
4947+
return tr.IdleConnStrsForTesting()
4948+
}
4949+
}
4950+
4951+
var conn string
4952+
doReq := func(n int) {
4953+
req, _ := NewRequest("GET", cst.ts.URL, nil)
4954+
req = req.WithContext(httptrace.WithClientTrace(context.Background(), &httptrace.ClientTrace{
4955+
PutIdleConn: func(err error) {
4956+
if err != nil {
4957+
t.Errorf("failed to keep idle conn: %v", err)
4958+
}
4959+
},
4960+
}))
4961+
res, err := c.Do(req)
4962+
if err != nil {
4963+
t.Fatal(err)
4964+
}
4965+
res.Body.Close()
4966+
conns := idleConns()
4967+
if len(conns) != 1 {
4968+
t.Fatalf("req %v: unexpected number of idle conns: %q", n, conns)
4969+
}
4970+
if conn == "" {
4971+
conn = conns[0]
4972+
}
4973+
if conn != conns[0] {
4974+
t.Fatalf("req %v: cached connection changed; expected the same one throughout the test", n)
4975+
}
4976+
}
4977+
for i := 0; i < 3; i++ {
4978+
doReq(i)
4979+
time.Sleep(timeout / 4)
4980+
}
4981+
time.Sleep(timeout / 2)
4982+
if got := idleConns(); len(got) != 0 {
4983+
t.Errorf("idle conns = %q; want none", got)
4984+
}
4985+
}
4986+
49234987
func TestTransportIdleConnTimeout_h1(t *testing.T) { testTransportIdleConnTimeout(t, h1Mode) }
49244988
func TestTransportIdleConnTimeout_h2(t *testing.T) { testTransportIdleConnTimeout(t, h2Mode) }
49254989
func testTransportIdleConnTimeout(t *testing.T, h2 bool) {
@@ -5912,6 +5976,7 @@ func TestTransportClone(t *testing.T) {
59125976
TLSHandshakeTimeout: time.Second,
59135977
DisableKeepAlives: true,
59145978
DisableCompression: true,
5979+
MaxConnLifespan: time.Second,
59155980
MaxIdleConns: 1,
59165981
MaxIdleConnsPerHost: 1,
59175982
MaxConnsPerHost: 1,

0 commit comments

Comments
 (0)