transport/http: update default transport settings (#496)

* transport/http: update default transport settings

Updates the default HTTP transport to use a larger value for
MaxIdleConnsPerHost. This improves performance under high load
for the GCS client.
diff --git a/transport/http/default_transport_go113.go b/transport/http/default_transport_go113.go
new file mode 100644
index 0000000..924f270
--- /dev/null
+++ b/transport/http/default_transport_go113.go
@@ -0,0 +1,20 @@
+// Copyright 2020 Google LLC.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build go1.13
+
+package http
+
+import "net/http"
+
+// clonedTransport returns the given RoundTripper as a cloned *http.Transport.
+// It returns nil if the RoundTripper can't be cloned or coerced to
+// *http.Transport.
+func clonedTransport(rt http.RoundTripper) *http.Transport {
+	t, ok := rt.(*http.Transport)
+	if !ok {
+		return nil
+	}
+	return t.Clone()
+}
diff --git a/transport/http/default_transport_not_go113.go b/transport/http/default_transport_not_go113.go
new file mode 100644
index 0000000..3cb16c6
--- /dev/null
+++ b/transport/http/default_transport_not_go113.go
@@ -0,0 +1,15 @@
+// Copyright 2020 Google LLC.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build !go1.13
+
+package http
+
+import "net/http"
+
+// clonedTransport returns the given RoundTripper as a cloned *http.Transport.
+// For versions of Go <1.13, this is not supported, so return nil.
+func clonedTransport(rt http.RoundTripper) *http.Transport {
+	return nil
+}
diff --git a/transport/http/dial.go b/transport/http/dial.go
index c8d79b8..96cf2e1 100644
--- a/transport/http/dial.go
+++ b/transport/http/dial.go
@@ -11,10 +11,13 @@
 	"context"
 	"crypto/tls"
 	"errors"
+	"net"
 	"net/http"
 	"net/url"
 	"os"
 	"strings"
+	"sync"
+	"time"
 
 	"go.opencensus.io/plugin/ochttp"
 	"golang.org/x/oauth2"
@@ -162,23 +165,57 @@
 
 // defaultBaseTransport returns the base HTTP transport.
 // On App Engine, this is urlfetch.Transport.
-// If TLSCertificate is available, return a custom Transport with TLSClientConfig.
-// Otherwise, return http.DefaultTransport.
+// Otherwise, use a default transport, taking most defaults from
+// http.DefaultTransport.
+// If TLSCertificate is available, set TLSClientConfig as well.
 func defaultBaseTransport(ctx context.Context, clientCertSource cert.Source) http.RoundTripper {
 	if appengineUrlfetchHook != nil {
 		return appengineUrlfetchHook(ctx)
 	}
 
+	// Copy http.DefaultTransport except for MaxIdleConnsPerHost setting,
+	// which is increased due to reported performance issues under load in the GCS
+	// client. Transport.Clone is only available in Go 1.13 and up.
+	trans := clonedTransport(http.DefaultTransport)
+	if trans == nil {
+		trans = fallbackBaseTransport()
+	}
+	trans.MaxIdleConnsPerHost = 100
+
 	if clientCertSource != nil {
-		// TODO (cbro): copy default transport settings from http.DefaultTransport
-		return &http.Transport{
-			TLSClientConfig: &tls.Config{
-				GetClientCertificate: clientCertSource,
-			},
+		trans.TLSClientConfig = &tls.Config{
+			GetClientCertificate: clientCertSource,
 		}
 	}
 
-	return http.DefaultTransport
+	return trans
+}
+
+var fallback struct {
+	*http.Transport
+	sync.Once
+}
+
+// fallbackBaseTransport is used in <go1.13 as well as in the rare case if
+// http.DefaultTransport has been reassigned something that's not a
+// *http.Transport.
+func fallbackBaseTransport() *http.Transport {
+	fallback.Do(func() {
+		fallback.Transport = &http.Transport{
+			Proxy: http.ProxyFromEnvironment,
+			DialContext: (&net.Dialer{
+				Timeout:   30 * time.Second,
+				KeepAlive: 30 * time.Second,
+				DualStack: true,
+			}).DialContext,
+			MaxIdleConns:          100,
+			MaxIdleConnsPerHost:   100,
+			IdleConnTimeout:       90 * time.Second,
+			TLSHandshakeTimeout:   10 * time.Second,
+			ExpectContinueTimeout: 1 * time.Second,
+		}
+	})
+	return fallback.Transport
 }
 
 func addOCTransport(trans http.RoundTripper, settings *internal.DialSettings) http.RoundTripper {