http2: add Transport support for IdleConnTimeout
Tests will be in the go repo's net/http package when this
package is re-bundled into std.
Updates golang/go#16808 (fixes after bundle into std)
Change-Id: Iad31dc120bc008b1e9679bf7b2b988aac9c893c8
Reviewed-on: https://go-review.googlesource.com/30075
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
diff --git a/http2/go17.go b/http2/go17.go
index e3fb412..47b7fae 100644
--- a/http2/go17.go
+++ b/http2/go17.go
@@ -39,6 +39,13 @@
func reqContext(r *http.Request) context.Context { return r.Context() }
+func (t *Transport) idleConnTimeout() time.Duration {
+ if t.t1 != nil {
+ return t.t1.IdleConnTimeout
+ }
+ return 0
+}
+
func setResponseUncompressed(res *http.Response) { res.Uncompressed = true }
func traceGotConn(req *http.Request, cc *ClientConn) {
diff --git a/http2/not_go17.go b/http2/not_go17.go
index 187f33c..140434a 100644
--- a/http2/not_go17.go
+++ b/http2/not_go17.go
@@ -10,6 +10,7 @@
"crypto/tls"
"net"
"net/http"
+ "time"
)
type contextContext interface {
@@ -82,3 +83,5 @@
func (cc *ClientConn) Ping(ctx contextContext) error {
return cc.ping(ctx)
}
+
+func (t *Transport) idleConnTimeout() time.Duration { return 0 }
diff --git a/http2/transport.go b/http2/transport.go
index 9651130..165d474 100644
--- a/http2/transport.go
+++ b/http2/transport.go
@@ -151,6 +151,9 @@
readerDone chan struct{} // closed on error
readerErr error // set before readerDone is closed
+ idleTimeout time.Duration // or 0 for never
+ idleTimer *time.Timer
+
mu sync.Mutex // guards following
cond *sync.Cond // hold mu; broadcast on flow/closed changes
flow flow // our conn-level flow control quota (cs.flow is per stream)
@@ -435,6 +438,10 @@
wantSettingsAck: true,
pings: make(map[[8]byte]chan struct{}),
}
+ if d := t.idleConnTimeout(); d != 0 {
+ cc.idleTimeout = d
+ cc.idleTimer = time.AfterFunc(d, cc.onIdleTimeout)
+ }
if VerboseLogs {
t.vlogf("http2: Transport creating client conn %p to %v", cc, c.RemoteAddr())
}
@@ -511,6 +518,16 @@
cc.nextStreamID < math.MaxInt32
}
+// onIdleTimeout is called from a time.AfterFunc goroutine. It will
+// only be called when we're idle, but because we're coming from a new
+// goroutine, there could be a new request coming in at the same time,
+// so this simply calls the synchronized closeIfIdle to shut down this
+// connection. The timer could just call closeIfIdle, but this is more
+// clear.
+func (cc *ClientConn) onIdleTimeout() {
+ cc.closeIfIdle()
+}
+
func (cc *ClientConn) closeIfIdle() {
cc.mu.Lock()
if len(cc.streams) > 0 {
@@ -652,6 +669,9 @@
if err := checkConnHeaders(req); err != nil {
return nil, err
}
+ if cc.idleTimer != nil {
+ cc.idleTimer.Stop()
+ }
trailers, err := commaSeparatedTrailers(req)
if err != nil {
@@ -1176,6 +1196,9 @@
if andRemove && cs != nil && !cc.closed {
cc.lastActive = time.Now()
delete(cc.streams, id)
+ if len(cc.streams) == 0 && cc.idleTimer != nil {
+ cc.idleTimer.Reset(cc.idleTimeout)
+ }
close(cs.done)
cc.cond.Broadcast() // wake up checkResetOrDone via clientStream.awaitFlowControl
}
@@ -1232,6 +1255,10 @@
defer cc.t.connPool().MarkDead(cc)
defer close(cc.readerDone)
+ if cc.idleTimer != nil {
+ cc.idleTimer.Stop()
+ }
+
// Close any response bodies if the server closes prematurely.
// TODO: also do this if we've written the headers but not
// gotten a response yet.