newFrontedClient builds a single *http.Client that dials googleIP and presents sniHost in the TLS handshake.
(googleIP, sniHost string, pollTimeout time.Duration, sessionCache tls.ClientSessionCache)
| 347 | // newFrontedClient builds a single *http.Client that dials googleIP and |
| 348 | // presents sniHost in the TLS handshake. |
| 349 | func newFrontedClient(googleIP, sniHost string, pollTimeout time.Duration, sessionCache tls.ClientSessionCache) *http.Client { |
| 350 | dialer := &net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second} |
| 351 | |
| 352 | transport := &http.Transport{ |
| 353 | DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { |
| 354 | if googleIP != "" { |
| 355 | return dialer.DialContext(ctx, "tcp", googleIP) |
| 356 | } |
| 357 | return dialer.DialContext(ctx, network, addr) |
| 358 | }, |
| 359 | TLSClientConfig: &tls.Config{ |
| 360 | ServerName: sniHost, |
| 361 | // Pin TLS 1.3 floor to prevent downgrade; Go's default minimum is 1.2. |
| 362 | MinVersion: tls.VersionTLS13, |
| 363 | // Enable TLS session resumption tickets so reconnects after |
| 364 | // idle timeout (and the prewarm dial in NewFrontedClients) can |
| 365 | // skip a full handshake round-trip. |
| 366 | ClientSessionCache: sessionCache, |
| 367 | // Pin ALPN so the resumed session matches the prewarm dial. |
| 368 | // (The TLS 1.3 resumption ticket is bound to ALPN; mismatched |
| 369 | // NextProtos causes the server to fall back to a full handshake.) |
| 370 | NextProtos: []string{"h2", "http/1.1"}, |
| 371 | }, |
| 372 | ForceAttemptHTTP2: true, |
| 373 | MaxIdleConns: 16, |
| 374 | // Default MaxIdleConnsPerHost is 2, which forces idle h1 conns to be |
| 375 | // recycled between poll workers when ALPN downgrades or the server |
| 376 | // closes h2 streams. Pin it to roughly the worker count per endpoint |
| 377 | // so each worker can keep its own warm conn. |
| 378 | MaxIdleConnsPerHost: workersPerEndpoint * 2, |
| 379 | // Larger HTTP read/write buffers cut syscall count on bulk batch |
| 380 | // bodies (server can return up to ~12 MB per poll under busy |
| 381 | // fan-out: 144 frames × 256 KB max payload, base64-expanded). |
| 382 | WriteBufferSize: 64 * 1024, |
| 383 | ReadBufferSize: 64 * 1024, |
| 384 | IdleConnTimeout: 90 * time.Second, |
| 385 | TLSHandshakeTimeout: 15 * time.Second, |
| 386 | ExpectContinueTimeout: 1 * time.Second, |
| 387 | } |
| 388 | |
| 389 | // Configure HTTP/2 so the long-lived h2 connection sends pings and detects |
| 390 | // black-holed peers quickly. Without ReadIdleTimeout, a dead h2 conn can |
| 391 | // linger until the kernel's TCP keepalive fires (~2 hours by default), |
| 392 | // leaking poll worker time as in-flight requests stall. |
| 393 | if h2t, err := http2.ConfigureTransports(transport); err == nil && h2t != nil { |
| 394 | h2t.ReadIdleTimeout = 30 * time.Second |
| 395 | h2t.PingTimeout = 15 * time.Second |
| 396 | // Raise the max DATA frame size we are willing to receive from 16 KiB |
| 397 | // (spec default) to 1 MiB. Each DATA frame carries a 9-byte header, |
| 398 | // so on a long bulk download (Apps Script gateway streaming a video |
| 399 | // chunk back) the framing overhead drops by ~64× and the receiver |
| 400 | // makes ~64× fewer Read syscalls per MiB. Stream/conn flow control |
| 401 | // windows in golang.org/x/net/http2 already default to 4 MiB / 1 GiB, |
| 402 | // so the actual throughput cap is RTT-bound, not window-bound. |
| 403 | h2t.MaxReadFrameSize = 1 << 20 |
| 404 | } |
| 405 | |
| 406 | return &http.Client{Transport: transport, Timeout: pollTimeout} |