Skip to content

Commit 7cc745d

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 0d8a4bf commit 7cc745d

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
@@ -106,3 +106,4 @@ pkg syscall (windows-386), func WSASendtoInet4(Handle, *WSABuf, uint32, *uint32,
106106
pkg syscall (windows-386), func WSASendtoInet6(Handle, *WSABuf, uint32, *uint32, uint32, SockaddrInet6, *Overlapped, *uint8) error
107107
pkg syscall (windows-amd64), func WSASendtoInet4(Handle, *WSABuf, uint32, *uint32, uint32, SockaddrInet4, *Overlapped, *uint8) error
108108
pkg syscall (windows-amd64), func WSASendtoInet6(Handle, *WSABuf, uint32, *uint32, uint32, SockaddrInet6, *Overlapped, *uint8) error
109+
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,
@@ -984,14 +989,22 @@ func (t *Transport) tryPutIdleConn(pconn *persistConn) error {
984989
t.removeIdleConnLocked(oldest)
985990
}
986991

992+
ttl, hasTtl := pconn.timeToLive()
993+
987994
// Set idle timer, but only for HTTP/1 (pconn.alt == nil).
988995
// The HTTP/2 implementation manages the idle timer itself
989996
// (see idleConnTimeout in h2_bundle.go).
990-
if t.IdleConnTimeout > 0 && pconn.alt == nil {
997+
if (hasTtl || t.IdleConnTimeout > 0) && pconn.alt == nil {
998+
999+
timeout := t.IdleConnTimeout
1000+
if hasTtl && (timeout <= 0 || ttl < timeout) {
1001+
timeout = ttl
1002+
}
1003+
9911004
if pconn.idleTimer != nil {
992-
pconn.idleTimer.Reset(t.IdleConnTimeout)
1005+
pconn.idleTimer.Reset(timeout)
9931006
} else {
994-
pconn.idleTimer = time.AfterFunc(t.IdleConnTimeout, pconn.closeConnIfStillIdle)
1007+
pconn.idleTimer = time.AfterFunc(timeout, pconn.closeConnIfStillIdle)
9951008
}
9961009
}
9971010
pconn.idleAt = time.Now()
@@ -1021,9 +1034,10 @@ func (t *Transport) queueForIdleConn(w *wantConn) (delivered bool) {
10211034
// If IdleConnTimeout is set, calculate the oldest
10221035
// persistConn.idleAt time we're willing to use a cached idle
10231036
// conn.
1037+
now := time.Now()
10241038
var oldTime time.Time
10251039
if t.IdleConnTimeout > 0 {
1026-
oldTime = time.Now().Add(-t.IdleConnTimeout)
1040+
oldTime = now.Add(-t.IdleConnTimeout)
10271041
}
10281042

10291043
// Look for most recently-used idle connection.
@@ -1036,7 +1050,8 @@ func (t *Transport) queueForIdleConn(w *wantConn) (delivered bool) {
10361050
// See whether this connection has been idle too long, considering
10371051
// only the wall time (the Round(0)), in case this is a laptop or VM
10381052
// coming out of suspend with previously cached idle connections.
1039-
tooOld := !oldTime.IsZero() && pconn.idleAt.Round(0).Before(oldTime)
1053+
tooOld := (!oldTime.IsZero() && pconn.idleAt.Round(0).Before(oldTime)) || (!pconn.reuseDeadline.IsZero() && pconn.reuseDeadline.Round(0).Before(now))
1054+
10401055
if tooOld {
10411056
// Async cleanup. Launch in its own goroutine (as if a
10421057
// time.AfterFunc called it); it acquires idleMu, which we're
@@ -1617,6 +1632,11 @@ func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (pconn *pers
16171632
}
16181633
}
16191634

1635+
var reuseDeadline time.Time
1636+
if t.MaxConnLifespan > 0 {
1637+
reuseDeadline = time.Now().Add(t.MaxConnLifespan)
1638+
}
1639+
16201640
// Proxy setup.
16211641
switch {
16221642
case cm.proxyURL == nil:
@@ -1737,10 +1757,11 @@ func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (pconn *pers
17371757
// pconn.conn was closed by next (http2configureTransports.upgradeFn).
17381758
return nil, e.RoundTripErr()
17391759
}
1740-
return &persistConn{t: t, cacheKey: pconn.cacheKey, alt: alt}, nil
1760+
return &persistConn{t: t, cacheKey: pconn.cacheKey, alt: alt, reuseDeadline: reuseDeadline}, nil
17411761
}
17421762
}
17431763

1764+
pconn.reuseDeadline = reuseDeadline
17441765
pconn.br = bufio.NewReaderSize(pconn, t.readBufferSize())
17451766
pconn.bw = bufio.NewWriterSize(persistConnWriter{pconn}, t.writeBufferSize())
17461767

@@ -1893,6 +1914,8 @@ type persistConn struct {
18931914

18941915
writeLoopDone chan struct{} // closed when write loop ends
18951916

1917+
reuseDeadline time.Time // time when this connection can no longer be reused
1918+
18961919
// Both guarded by Transport.idleMu:
18971920
idleAt time.Time // time it last become idle
18981921
idleTimer *time.Timer // holding an AfterFunc to close it
@@ -1909,6 +1932,30 @@ type persistConn struct {
19091932
mutateHeaderFunc func(Header)
19101933
}
19111934

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

src/net/http/transport_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4895,6 +4895,70 @@ func TestTransportMaxIdleConns(t *testing.T) {
48954895
}
48964896
}
48974897

4898+
func TestTransportMaxConnLifespan_h1(t *testing.T) { testTransportMaxConnLifespan(t, h1Mode) }
4899+
func TestTransportMaxConnLifespan_h2(t *testing.T) { testTransportMaxConnLifespan(t, h2Mode) }
4900+
func testTransportMaxConnLifespan(t *testing.T, h2 bool) {
4901+
if testing.Short() {
4902+
t.Skip("skipping in short mode")
4903+
}
4904+
defer afterTest(t)
4905+
4906+
const timeout = 1 * time.Second
4907+
4908+
cst := newClientServerTest(t, h2, HandlerFunc(func(w ResponseWriter, r *Request) {
4909+
// No body for convenience.
4910+
}))
4911+
defer cst.close()
4912+
tr := cst.tr
4913+
tr.MaxConnLifespan = timeout
4914+
tr.IdleConnTimeout = timeout * 3
4915+
defer tr.CloseIdleConnections()
4916+
c := &Client{Transport: tr}
4917+
4918+
idleConns := func() []string {
4919+
if h2 {
4920+
return tr.IdleConnStrsForTesting_h2()
4921+
} else {
4922+
return tr.IdleConnStrsForTesting()
4923+
}
4924+
}
4925+
4926+
var conn string
4927+
doReq := func(n int) {
4928+
req, _ := NewRequest("GET", cst.ts.URL, nil)
4929+
req = req.WithContext(httptrace.WithClientTrace(context.Background(), &httptrace.ClientTrace{
4930+
PutIdleConn: func(err error) {
4931+
if err != nil {
4932+
t.Errorf("failed to keep idle conn: %v", err)
4933+
}
4934+
},
4935+
}))
4936+
res, err := c.Do(req)
4937+
if err != nil {
4938+
t.Fatal(err)
4939+
}
4940+
res.Body.Close()
4941+
conns := idleConns()
4942+
if len(conns) != 1 {
4943+
t.Fatalf("req %v: unexpected number of idle conns: %q", n, conns)
4944+
}
4945+
if conn == "" {
4946+
conn = conns[0]
4947+
}
4948+
if conn != conns[0] {
4949+
t.Fatalf("req %v: cached connection changed; expected the same one throughout the test", n)
4950+
}
4951+
}
4952+
for i := 0; i < 3; i++ {
4953+
doReq(i)
4954+
time.Sleep(timeout / 4)
4955+
}
4956+
time.Sleep(timeout / 2)
4957+
if got := idleConns(); len(got) != 0 {
4958+
t.Errorf("idle conns = %q; want none", got)
4959+
}
4960+
}
4961+
48984962
func TestTransportIdleConnTimeout_h1(t *testing.T) { testTransportIdleConnTimeout(t, h1Mode) }
48994963
func TestTransportIdleConnTimeout_h2(t *testing.T) { testTransportIdleConnTimeout(t, h2Mode) }
49004964
func testTransportIdleConnTimeout(t *testing.T, h2 bool) {
@@ -5887,6 +5951,7 @@ func TestTransportClone(t *testing.T) {
58875951
TLSHandshakeTimeout: time.Second,
58885952
DisableKeepAlives: true,
58895953
DisableCompression: true,
5954+
MaxConnLifespan: time.Second,
58905955
MaxIdleConns: 1,
58915956
MaxIdleConnsPerHost: 1,
58925957
MaxConnsPerHost: 1,

0 commit comments

Comments
 (0)