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.