git » go-net » commit 3b99394

http2: when using Go 1.7, make Transport use httptrace hooks

author Brad Fitzpatrick
2016-05-18 04:27:24 UTC
committer Brad Fitzpatrick
2016-05-18 15:10:36 UTC
parent 76e74a336561c500a376b406eb649727eeb3477f

http2: when using Go 1.7, make Transport use httptrace hooks

Updates golang/go#12580

Change-Id: I95d7206a8fde494f88288b381814a5d72d42df5e
Reviewed-on: https://go-review.googlesource.com/23205
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>

http2/go17.go +44 -0
http2/not_go17.go +8 -0
http2/transport.go +19 -0

diff --git a/http2/go17.go b/http2/go17.go
index 5f6238c..2e2eabd 100644
--- a/http2/go17.go
+++ b/http2/go17.go
@@ -9,8 +9,52 @@ package http2
 import (
 	"context"
 	"net/http"
+	"net/http/httptrace"
+	"time"
 )
 
+type clientTrace httptrace.ClientTrace
+
 func reqContext(r *http.Request) context.Context { return r.Context() }
 
 func setResponseUncompressed(res *http.Response) { res.Uncompressed = true }
+
+func traceGotConn(req *http.Request, cc *ClientConn) {
+	trace := httptrace.ContextClientTrace(req.Context())
+	if trace == nil || trace.GotConn == nil {
+		return
+	}
+	ci := httptrace.GotConnInfo{Conn: cc.tconn}
+	cc.mu.Lock()
+	ci.Reused = cc.nextStreamID > 1
+	ci.WasIdle = len(cc.streams) == 0
+	if ci.WasIdle {
+		ci.IdleTime = time.Now().Sub(cc.lastActive)
+	}
+	cc.mu.Unlock()
+
+	trace.GotConn(ci)
+}
+
+func traceWroteHeaders(trace *clientTrace) {
+	if trace != nil && trace.WroteHeaders != nil {
+		trace.WroteHeaders()
+	}
+}
+
+func traceWroteRequest(trace *clientTrace, err error) {
+	if trace != nil && trace.WroteRequest != nil {
+		trace.WroteRequest(httptrace.WroteRequestInfo{Err: err})
+	}
+}
+
+func traceFirstResponseByte(trace *clientTrace) {
+	if trace != nil && trace.GotFirstResponseByte != nil {
+		trace.GotFirstResponseByte()
+	}
+}
+
+func requestTrace(req *http.Request) *clientTrace {
+	trace := httptrace.ContextClientTrace(req.Context())
+	return (*clientTrace)(trace)
+}
diff --git a/http2/not_go17.go b/http2/not_go17.go
index c7a4d30..deffe68 100644
--- a/http2/not_go17.go
+++ b/http2/not_go17.go
@@ -20,3 +20,11 @@ func reqContext(r *http.Request) fakeContext {
 func setResponseUncompressed(res *http.Response) {
 	// Nothing.
 }
+
+type clientTrace struct{}
+
+func requestTrace(*http.Request) *clientTrace { return nil }
+func traceGotConn(*http.Request, *ClientConn) {}
+func traceFirstResponseByte(*clientTrace)     {}
+func traceWroteHeaders(*clientTrace)          {}
+func traceWroteRequest(*clientTrace, error)   {}
diff --git a/http2/transport.go b/http2/transport.go
index a7ada8f..4aa3b7b 100644
--- a/http2/transport.go
+++ b/http2/transport.go
@@ -153,6 +153,8 @@ type ClientConn struct {
 	bw           *bufio.Writer
 	br           *bufio.Reader
 	fr           *Framer
+	lastActive   time.Time
+
 	// Settings from peer:
 	maxFrameSize         uint32
 	maxConcurrentStreams uint32
@@ -170,6 +172,7 @@ type ClientConn struct {
 type clientStream struct {
 	cc            *ClientConn
 	req           *http.Request
+	trace         *clientTrace // or nil
 	ID            uint32
 	resc          chan resAndError
 	bufPipe       pipe // buffered pipe with the flow-controlled response payload
@@ -288,6 +291,7 @@ func (t *Transport) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Res
 			t.vlogf("http2: Transport failed to get client conn for %s: %v", addr, err)
 			return nil, err
 		}
+		traceGotConn(req, cc)
 		res, err := cc.RoundTrip(req)
 		if shouldRetryRequest(req, err) {
 			continue
@@ -622,6 +626,7 @@ func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) {
 	}
 
 	cc.mu.Lock()
+	cc.lastActive = time.Now()
 	if cc.closed || !cc.canTakeNewRequestLocked() {
 		cc.mu.Unlock()
 		return nil, errClientConnUnusable
@@ -629,6 +634,7 @@ func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) {
 
 	cs := cc.newStream()
 	cs.req = req
+	cs.trace = requestTrace(req)
 	hasBody := body != nil
 
 	// TODO(bradfitz): this is a copy of the logic in net/http. Unify somewhere?
@@ -659,6 +665,7 @@ func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) {
 	endStream := !hasBody && !hasTrailers
 	werr := cc.writeHeaders(cs.ID, endStream, hdrs)
 	cc.wmu.Unlock()
+	traceWroteHeaders(cs.trace)
 	cc.mu.Unlock()
 
 	if werr != nil {
@@ -668,6 +675,7 @@ func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) {
 		cc.forgetStreamID(cs.ID)
 		// Don't bother sending a RST_STREAM (our write already failed;
 		// no need to keep writing)
+		traceWroteRequest(cs.trace, werr)
 		return nil, werr
 	}
 
@@ -679,6 +687,7 @@ func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) {
 			bodyCopyErrc <- cs.writeRequestBody(body, req.Body)
 		}()
 	} else {
+		traceWroteRequest(cs.trace, nil)
 		if d := cc.responseHeaderTimeout(); d != 0 {
 			timer := time.NewTimer(d)
 			defer timer.Stop()
@@ -743,6 +752,7 @@ func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) {
 			// forgetStreamID.
 			return nil, cs.resetErr
 		case err := <-bodyCopyErrc:
+			traceWroteRequest(cs.trace, err)
 			if err != nil {
 				return nil, err
 			}
@@ -1068,6 +1078,7 @@ func (cc *ClientConn) streamByID(id uint32, andRemove bool) *clientStream {
 	defer cc.mu.Unlock()
 	cs := cc.streams[id]
 	if andRemove && cs != nil && !cc.closed {
+		cc.lastActive = time.Now()
 		delete(cc.streams, id)
 		close(cs.done)
 	}
@@ -1196,6 +1207,13 @@ func (rl *clientConnReadLoop) processHeaders(f *MetaHeadersFrame) error {
 	} else {
 		return rl.processTrailers(cs, f)
 	}
+	if cs.trace != nil {
+		// TODO(bradfitz): move first response byte earlier,
+		// when we first read the 9 byte header, not waiting
+		// until all the HEADERS+CONTINUATION frames have been
+		// merged. This works for now.
+		traceFirstResponseByte(cs.trace)
+	}
 
 	res, err := rl.handleResponse(cs, f)
 	if err != nil {
@@ -1243,6 +1261,7 @@ func (rl *clientConnReadLoop) handleResponse(cs *clientStream, f *MetaHeadersFra
 	if statusCode == 100 {
 		// Just skip 100-continue response headers for now.
 		// TODO: golang.org/issue/13851 for doing it properly.
+		// TODO: also call the httptrace.ClientTrace hooks
 		cs.pastHeaders = false // do it all again
 		return nil, nil
 	}