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 }