From c165311f4270e8a2d75d9a610abdfb54d72ae4e5 Mon Sep 17 00:00:00 2001 From: ale <ale@incal.net> Date: Mon, 6 Jan 2020 11:09:19 +0000 Subject: [PATCH] Let the configuration override connection and max request timeouts Add the 'connect_timeout' and 'request_max_timeout' configuration fields, to control respectively the initial connection timeout, and the maximum time for each individual request. This allows fine-tuning of the expected performance of specific backends, for example to let optional backends fail fast. --- clientutil/backend.go | 7 ++++++ clientutil/balancer.go | 43 ++++++++++++++++++++++++++++--------- clientutil/dialer.go | 20 +++++++++++++++++ clientutil/dialer_legacy.go | 23 ++++++++++++++++++++ clientutil/transport.go | 37 ++++++++++++++++--------------- 5 files changed, 101 insertions(+), 29 deletions(-) create mode 100644 clientutil/dialer.go create mode 100644 clientutil/dialer_legacy.go diff --git a/clientutil/backend.go b/clientutil/backend.go index 9e08fa0..cdde33b 100644 --- a/clientutil/backend.go +++ b/clientutil/backend.go @@ -16,6 +16,13 @@ type BackendConfig struct { TLSConfig *TLSClientConfig `yaml:"tls"` Sharded bool `yaml:"sharded"` Debug bool `yaml:"debug"` + + // Connection timeout (if unset, use default value). + ConnectTimeout string `yaml:"connect_timeout"` + + // Maximum timeout for each individual request to this backend + // (if unset, use the Context timeout). + RequestMaxTimeout string `yaml:"request_max_timeout"` } // Backend is a runtime class that provides http Clients for use with diff --git a/clientutil/balancer.go b/clientutil/balancer.go index d2ca827..8f6e99f 100644 --- a/clientutil/balancer.go +++ b/clientutil/balancer.go @@ -60,10 +60,11 @@ func newExponentialBackOff() *backoff.ExponentialBackOff { type balancedBackend struct { *backendTracker *transportCache - baseURI *url.URL - sharded bool - resolver resolver - log logger + baseURI *url.URL + sharded bool + resolver resolver + log logger + requestMaxTimeout time.Duration } func newBalancedBackend(config *BackendConfig, resolver resolver) (*balancedBackend, error) { @@ -80,17 +81,36 @@ func newBalancedBackend(config *BackendConfig, resolver resolver) (*balancedBack } } + var connectTimeout time.Duration + if config.ConnectTimeout != "" { + t, err := time.ParseDuration(config.ConnectTimeout) + if err != nil { + return nil, fmt.Errorf("error in connect_timeout: %v", err) + } + connectTimeout = t + } + + var reqTimeout time.Duration + if config.RequestMaxTimeout != "" { + t, err := time.ParseDuration(config.RequestMaxTimeout) + if err != nil { + return nil, fmt.Errorf("error in request_max_timeout: %v", err) + } + reqTimeout = t + } + var logger logger = &nilLogger{} if config.Debug { logger = log.New(os.Stderr, fmt.Sprintf("backend %s: ", u.Host), 0) } return &balancedBackend{ - backendTracker: newBackendTracker(u.Host, resolver, logger), - transportCache: newTransportCache(tlsConfig), - sharded: config.Sharded, - baseURI: u, - resolver: resolver, - log: logger, + backendTracker: newBackendTracker(u.Host, resolver, logger), + transportCache: newTransportCache(tlsConfig, connectTimeout), + requestMaxTimeout: reqTimeout, + sharded: config.Sharded, + baseURI: u, + resolver: resolver, + log: logger, }, nil } @@ -115,6 +135,9 @@ func (b *balancedBackend) Call(ctx context.Context, shard, path string, req, res if deadline, ok := ctx.Deadline(); ok { innerTimeout = time.Until(deadline) / time.Duration(seq.Len()) } + if b.requestMaxTimeout > 0 && innerTimeout > b.requestMaxTimeout { + innerTimeout = b.requestMaxTimeout + } // Call the backends in the sequence until one succeeds, with an // exponential backoff policy controlled by the outer Context. diff --git a/clientutil/dialer.go b/clientutil/dialer.go new file mode 100644 index 0000000..c8e7390 --- /dev/null +++ b/clientutil/dialer.go @@ -0,0 +1,20 @@ +// +build go1.9 + +package clientutil + +import ( + "context" + "net" + "time" +) + +func netDialContext(addr string, connectTimeout time.Duration) func(context.Context, string, string) (net.Conn, error) { + dialer := &net.Dialer{ + Timeout: connectTimeout, + KeepAlive: 30 * time.Second, + DualStack: true, + } + return func(ctx context.Context, net string, _ string) (net.Conn, error) { + return dialer.DialContext(ctx, net, addr) + } +} diff --git a/clientutil/dialer_legacy.go b/clientutil/dialer_legacy.go new file mode 100644 index 0000000..f257990 --- /dev/null +++ b/clientutil/dialer_legacy.go @@ -0,0 +1,23 @@ +// +build !go1.9 + +package clientutil + +import ( + "context" + "net" + "time" +) + +// Go < 1.9 does not have net.DialContext, reimplement it in terms of +// net.DialTimeout. +func netDialContext(addr string, connectTimeout time.Duration) func(context.Context, string, string) (net.Conn, error) { + return func(ctx context.Context, net string, _ string) (net.Conn, error) { + if deadline, ok := ctx.Deadline(); ok { + ctxTimeout := time.Until(deadline) + if ctxTimeout < connectTimeout { + connectTimeout = ctxTimeout + } + } + return net.DialTimeout(network, addr, connectTimeout) + } +} diff --git a/clientutil/transport.go b/clientutil/transport.go index 1cc6c6d..39b1b3d 100644 --- a/clientutil/transport.go +++ b/clientutil/transport.go @@ -1,9 +1,7 @@ package clientutil import ( - "context" "crypto/tls" - "net" "net/http" "sync" "time" @@ -11,31 +9,42 @@ import ( "git.autistici.org/ai3/go-common/tracing" ) +var defaultConnectTimeout = 30 * time.Second + // The transportCache is just a cache of http transports, each // connecting to a specific address. // // We use this to control the HTTP Host header and the TLS ServerName // independently of the target address. type transportCache struct { - tlsConfig *tls.Config + tlsConfig *tls.Config + connectTimeout time.Duration mx sync.RWMutex transports map[string]http.RoundTripper } -func newTransportCache(tlsConfig *tls.Config) *transportCache { +func newTransportCache(tlsConfig *tls.Config, connectTimeout time.Duration) *transportCache { + if connectTimeout == 0 { + connectTimeout = defaultConnectTimeout + } return &transportCache{ - tlsConfig: tlsConfig, - transports: make(map[string]http.RoundTripper), + tlsConfig: tlsConfig, + connectTimeout: connectTimeout, + transports: make(map[string]http.RoundTripper), } } func (m *transportCache) newTransport(addr string) http.RoundTripper { return tracing.WrapTransport(&http.Transport{ TLSClientConfig: m.tlsConfig, - DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) { - return netDialContext(ctx, network, addr) - }, + DialContext: netDialContext(addr, m.connectTimeout), + + // Parameters match those of net/http.DefaultTransport. + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, }) } @@ -55,13 +64,3 @@ func (m *transportCache) getTransport(addr string) http.RoundTripper { return t } - -// Go < 1.9 does not have net.DialContext, reimplement it in terms of -// net.DialTimeout. -func netDialContext(ctx context.Context, network, addr string) (net.Conn, error) { - timeout := 60 * time.Second // some arbitrary max timeout - if deadline, ok := ctx.Deadline(); ok { - timeout = time.Until(deadline) - } - return net.DialTimeout(network, addr, timeout) -} -- GitLab