go.uber.org/yarpc@v1.72.1/transport/http/transport.go (about)

     1  // Copyright (c) 2022 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package http
    22  
    23  import (
    24  	"context"
    25  	"math/rand"
    26  	"net"
    27  	"net/http"
    28  	"sync"
    29  	"time"
    30  
    31  	"github.com/opentracing/opentracing-go"
    32  	"go.uber.org/net/metrics"
    33  	backoffapi "go.uber.org/yarpc/api/backoff"
    34  	"go.uber.org/yarpc/api/peer"
    35  	"go.uber.org/yarpc/api/transport"
    36  	yarpctls "go.uber.org/yarpc/api/transport/tls"
    37  	"go.uber.org/yarpc/internal/backoff"
    38  	"go.uber.org/yarpc/pkg/lifecycle"
    39  	"go.uber.org/zap"
    40  )
    41  
    42  type transportOptions struct {
    43  	keepAlive                 time.Duration
    44  	maxIdleConns              int
    45  	maxIdleConnsPerHost       int
    46  	idleConnTimeout           time.Duration
    47  	disableKeepAlives         bool
    48  	disableCompression        bool
    49  	responseHeaderTimeout     time.Duration
    50  	connTimeout               time.Duration
    51  	connBackoffStrategy       backoffapi.Strategy
    52  	innocenceWindow           time.Duration
    53  	dialContext               func(ctx context.Context, network, addr string) (net.Conn, error)
    54  	jitter                    func(int64) int64
    55  	tracer                    opentracing.Tracer
    56  	buildClient               func(*transportOptions) *http.Client
    57  	logger                    *zap.Logger
    58  	meter                     *metrics.Scope
    59  	serviceName               string
    60  	outboundTLSConfigProvider yarpctls.OutboundTLSConfigProvider
    61  }
    62  
    63  var defaultTransportOptions = transportOptions{
    64  	keepAlive:           30 * time.Second,
    65  	maxIdleConnsPerHost: 2,
    66  	connTimeout:         defaultConnTimeout,
    67  	connBackoffStrategy: backoff.DefaultExponential,
    68  	buildClient:         buildHTTPClient,
    69  	innocenceWindow:     defaultInnocenceWindow,
    70  	idleConnTimeout:     defaultIdleConnTimeout,
    71  	jitter:              rand.Int63n,
    72  }
    73  
    74  func newTransportOptions() transportOptions {
    75  	options := defaultTransportOptions
    76  	options.tracer = opentracing.GlobalTracer()
    77  	return options
    78  }
    79  
    80  // TransportOption customizes the behavior of an HTTP transport.
    81  type TransportOption func(*transportOptions)
    82  
    83  func (TransportOption) httpOption() {}
    84  
    85  // KeepAlive specifies the keep-alive period for the network connection. If
    86  // zero, keep-alives are disabled.
    87  //
    88  // Defaults to 30 seconds.
    89  func KeepAlive(t time.Duration) TransportOption {
    90  	return func(options *transportOptions) {
    91  		options.keepAlive = t
    92  	}
    93  }
    94  
    95  // MaxIdleConns controls the maximum number of idle (keep-alive) connections
    96  // across all hosts. Zero means no limit.
    97  func MaxIdleConns(i int) TransportOption {
    98  	return func(options *transportOptions) {
    99  		options.maxIdleConns = i
   100  	}
   101  }
   102  
   103  // MaxIdleConnsPerHost specifies the number of idle (keep-alive) HTTP
   104  // connections that will be maintained per host.
   105  // Existing idle connections will be used instead of creating new HTTP
   106  // connections.
   107  //
   108  // Defaults to 2 connections.
   109  func MaxIdleConnsPerHost(i int) TransportOption {
   110  	return func(options *transportOptions) {
   111  		options.maxIdleConnsPerHost = i
   112  	}
   113  }
   114  
   115  // IdleConnTimeout is the maximum amount of time an idle (keep-alive)
   116  // connection will remain idle before closing itself.
   117  // Zero means no limit.
   118  //
   119  // Defaults to 15 minutes.
   120  func IdleConnTimeout(t time.Duration) TransportOption {
   121  	return func(options *transportOptions) {
   122  		options.idleConnTimeout = t
   123  	}
   124  }
   125  
   126  // DisableKeepAlives prevents re-use of TCP connections between different HTTP
   127  // requests.
   128  func DisableKeepAlives() TransportOption {
   129  	return func(options *transportOptions) {
   130  		options.disableKeepAlives = true
   131  	}
   132  }
   133  
   134  // DisableCompression if true prevents the Transport from requesting
   135  // compression with an "Accept-Encoding: gzip" request header when the Request
   136  // contains no existing Accept-Encoding value. If the Transport requests gzip
   137  // on its own and gets a gzipped response, it's transparently decoded in the
   138  // Response.Body. However, if the user explicitly requested gzip it is not
   139  // automatically uncompressed.
   140  func DisableCompression() TransportOption {
   141  	return func(options *transportOptions) {
   142  		options.disableCompression = true
   143  	}
   144  }
   145  
   146  // ResponseHeaderTimeout if non-zero specifies the amount of time to wait for
   147  // a server's response headers after fully writing the request (including its
   148  // body, if any).  This time does not include the time to read the response
   149  // body.
   150  func ResponseHeaderTimeout(t time.Duration) TransportOption {
   151  	return func(options *transportOptions) {
   152  		options.responseHeaderTimeout = t
   153  	}
   154  }
   155  
   156  // ConnTimeout is the time that the transport will wait for a connection attempt.
   157  // If a peer has been retained by a peer list, connection attempts are
   158  // performed in a goroutine off the request path.
   159  //
   160  // The default is half a second.
   161  func ConnTimeout(d time.Duration) TransportOption {
   162  	return func(options *transportOptions) {
   163  		options.connTimeout = d
   164  	}
   165  }
   166  
   167  // ConnBackoff specifies the connection backoff strategy for delays between
   168  // connection attempts for each peer.
   169  //
   170  // The default is exponential backoff starting with 10ms fully jittered,
   171  // doubling each attempt, with a maximum interval of 30s.
   172  func ConnBackoff(s backoffapi.Strategy) TransportOption {
   173  	return func(options *transportOptions) {
   174  		options.connBackoffStrategy = s
   175  	}
   176  }
   177  
   178  // InnocenceWindow is the duration after the peer connection management loop
   179  // will suspend suspicion for a peer after successfully checking whether the
   180  // peer is live with a fresh TCP connection.
   181  //
   182  // The default innocence window is 5 seconds.
   183  //
   184  // A timeout does not necessarily indicate that a peer is unavailable,
   185  // but it could indicate that the connection is half-open, that the peer died
   186  // without sending a TCP FIN packet.
   187  // In this case, the peer connection management loop attempts to open a TCP
   188  // connection in the background, once per innocence window, while suspicious of
   189  // the connection, leaving the peer available until it fails.
   190  func InnocenceWindow(d time.Duration) TransportOption {
   191  	return func(options *transportOptions) {
   192  		options.innocenceWindow = d
   193  	}
   194  }
   195  
   196  // DialContext specifies the dial function for creating TCP connections on the
   197  // outbound. This will override the default dial context, which has a 30 second
   198  // timeout and respects the KeepAlive option.
   199  //
   200  // See https://golang.org/pkg/net/http/#Transport.DialContext for details.
   201  func DialContext(f func(ctx context.Context, network, addr string) (net.Conn, error)) TransportOption {
   202  	return func(options *transportOptions) {
   203  		options.dialContext = f
   204  	}
   205  }
   206  
   207  // Tracer configures a tracer for the transport and all its inbounds and
   208  // outbounds.
   209  func Tracer(tracer opentracing.Tracer) TransportOption {
   210  	return func(options *transportOptions) {
   211  		options.tracer = tracer
   212  	}
   213  }
   214  
   215  // Logger sets a logger to use for internal logging.
   216  //
   217  // The default is to not write any logs.
   218  func Logger(logger *zap.Logger) TransportOption {
   219  	return func(options *transportOptions) {
   220  		options.logger = logger
   221  	}
   222  }
   223  
   224  // Meter sets a meter to use for internal transport metrics.
   225  //
   226  // The default is to not emit any metrics.
   227  func Meter(meter *metrics.Scope) TransportOption {
   228  	return func(options *transportOptions) {
   229  		options.meter = meter
   230  	}
   231  }
   232  
   233  // ServiceName sets the name of the service used in transport logging
   234  // and metrics.
   235  func ServiceName(name string) TransportOption {
   236  	return func(options *transportOptions) {
   237  		options.serviceName = name
   238  	}
   239  }
   240  
   241  // OutboundTLSConfigProvider returns an TransportOption that provides the
   242  // outbound TLS config provider.
   243  func OutboundTLSConfigProvider(provider yarpctls.OutboundTLSConfigProvider) TransportOption {
   244  	return func(options *transportOptions) {
   245  		options.outboundTLSConfigProvider = provider
   246  	}
   247  }
   248  
   249  // Hidden option to override the buildHTTPClient function. This is used only
   250  // for testing.
   251  func buildClient(f func(*transportOptions) *http.Client) TransportOption {
   252  	return func(options *transportOptions) {
   253  		options.buildClient = f
   254  	}
   255  }
   256  
   257  // NewTransport creates a new HTTP transport for managing peers and sending requests
   258  func NewTransport(opts ...TransportOption) *Transport {
   259  	options := newTransportOptions()
   260  	for _, opt := range opts {
   261  		opt(&options)
   262  	}
   263  	return options.newTransport()
   264  }
   265  
   266  func (o *transportOptions) newTransport() *Transport {
   267  	logger := o.logger
   268  	if logger == nil {
   269  		logger = zap.NewNop()
   270  	}
   271  	return &Transport{
   272  		once:                     lifecycle.NewOnce(),
   273  		client:                   o.buildClient(o),
   274  		connTimeout:              o.connTimeout,
   275  		connBackoffStrategy:      o.connBackoffStrategy,
   276  		innocenceWindow:          o.innocenceWindow,
   277  		jitter:                   o.jitter,
   278  		peers:                    make(map[string]*httpPeer),
   279  		tracer:                   o.tracer,
   280  		logger:                   logger,
   281  		meter:                    o.meter,
   282  		serviceName:              o.serviceName,
   283  		ouboundTLSConfigProvider: o.outboundTLSConfigProvider,
   284  	}
   285  }
   286  
   287  func buildHTTPClient(options *transportOptions) *http.Client {
   288  	dialContext := options.dialContext
   289  	if dialContext == nil {
   290  		dialContext = (&net.Dialer{
   291  			Timeout:   30 * time.Second,
   292  			KeepAlive: options.keepAlive,
   293  		}).DialContext
   294  	}
   295  
   296  	return &http.Client{
   297  		Transport: &http.Transport{
   298  			// options lifted from https://golang.org/src/net/http/transport.go
   299  			Proxy:                 http.ProxyFromEnvironment,
   300  			DialContext:           dialContext,
   301  			TLSHandshakeTimeout:   10 * time.Second,
   302  			ExpectContinueTimeout: 1 * time.Second,
   303  			MaxIdleConns:          options.maxIdleConns,
   304  			MaxIdleConnsPerHost:   options.maxIdleConnsPerHost,
   305  			IdleConnTimeout:       options.idleConnTimeout,
   306  			DisableKeepAlives:     options.disableKeepAlives,
   307  			DisableCompression:    options.disableCompression,
   308  			ResponseHeaderTimeout: options.responseHeaderTimeout,
   309  		},
   310  	}
   311  }
   312  
   313  // Transport keeps track of HTTP peers and the associated HTTP client. It
   314  // allows using a single HTTP client to make requests to multiple YARPC
   315  // services and pooling the resources needed therein.
   316  type Transport struct {
   317  	lock sync.Mutex
   318  	once *lifecycle.Once
   319  
   320  	client *http.Client
   321  	peers  map[string]*httpPeer
   322  
   323  	connTimeout         time.Duration
   324  	connBackoffStrategy backoffapi.Strategy
   325  	connectorsGroup     sync.WaitGroup
   326  	innocenceWindow     time.Duration
   327  	jitter              func(int64) int64
   328  
   329  	tracer                   opentracing.Tracer
   330  	logger                   *zap.Logger
   331  	meter                    *metrics.Scope
   332  	serviceName              string
   333  	ouboundTLSConfigProvider yarpctls.OutboundTLSConfigProvider
   334  }
   335  
   336  var _ transport.Transport = (*Transport)(nil)
   337  
   338  // Start starts the HTTP transport.
   339  func (a *Transport) Start() error {
   340  	return a.once.Start(func() error {
   341  		return nil // Nothing to do
   342  	})
   343  }
   344  
   345  // Stop stops the HTTP transport.
   346  func (a *Transport) Stop() error {
   347  	return a.once.Stop(func() error {
   348  		closeIdleConnections(a.client)
   349  		a.connectorsGroup.Wait()
   350  		return nil
   351  	})
   352  }
   353  
   354  // IsRunning returns whether the HTTP transport is running.
   355  func (a *Transport) IsRunning() bool {
   356  	return a.once.IsRunning()
   357  }
   358  
   359  // RetainPeer gets or creates a Peer for the specified peer.Subscriber (usually a peer.Chooser)
   360  func (a *Transport) RetainPeer(pid peer.Identifier, sub peer.Subscriber) (peer.Peer, error) {
   361  	a.lock.Lock()
   362  	defer a.lock.Unlock()
   363  
   364  	p := a.getOrCreatePeer(pid)
   365  	p.Subscribe(sub)
   366  	return p, nil
   367  }
   368  
   369  // **NOTE** should only be called while the lock write mutex is acquired
   370  func (a *Transport) getOrCreatePeer(pid peer.Identifier) *httpPeer {
   371  	addr := pid.Identifier()
   372  	if p, ok := a.peers[addr]; ok {
   373  		return p
   374  	}
   375  	p := newPeer(addr, a)
   376  	a.peers[addr] = p
   377  	a.connectorsGroup.Add(1)
   378  	go p.MaintainConn()
   379  
   380  	return p
   381  }
   382  
   383  // ReleasePeer releases a peer from the peer.Subscriber and removes that peer from the Transport if nothing is listening to it
   384  func (a *Transport) ReleasePeer(pid peer.Identifier, sub peer.Subscriber) error {
   385  	a.lock.Lock()
   386  	defer a.lock.Unlock()
   387  
   388  	p, ok := a.peers[pid.Identifier()]
   389  	if !ok {
   390  		return peer.ErrTransportHasNoReferenceToPeer{
   391  			TransportName:  "http.Transport",
   392  			PeerIdentifier: pid.Identifier(),
   393  		}
   394  	}
   395  
   396  	if err := p.Unsubscribe(sub); err != nil {
   397  		return err
   398  	}
   399  
   400  	if p.NumSubscribers() == 0 {
   401  		delete(a.peers, pid.Identifier())
   402  		p.Release()
   403  	}
   404  
   405  	return nil
   406  }