diff --git a/pkg/appview/storage/proxy_blob_store.go b/pkg/appview/storage/proxy_blob_store.go index bb03ad6..1210c71 100644 --- a/pkg/appview/storage/proxy_blob_store.go +++ b/pkg/appview/storage/proxy_blob_store.go @@ -32,6 +32,36 @@ var ( globalUploadsMu sync.RWMutex ) +// The transport and client below are package-level on purpose. RoutingRepository +// (and therefore ProxyBlobStore) is built fresh on every registry request, so a +// per-instance transport was thrown away after a single request: nothing was ever +// reused, every XRPC call to the hold and every presigned S3 request paid for a +// fresh TCP + TLS handshake, the MaxIdleConns settings below were dead config, and +// each discarded transport still sat on its idle sockets for the full +// IdleConnTimeout. Sharing one transport process-wide is what makes the idle pool +// and keep-alives mean anything. +// +// ForceAttemptHTTP2 is explicit to document intent; no custom DialContext or +// TLSClientConfig is set, so Go's automatic HTTP/2 negotiation over ALPN stays on +// (see 416ba4a, where the load balancer started speaking HTTP/2 on the frontend). +// The LB talks HTTP/1.1 to the hold backend, so multiplexing stops at the LB. The +// win here is not multiplexing: it is removing the handshake and socket churn. +var ( + sharedTransport = &http.Transport{ + DisableKeepAlives: false, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 100, + MaxConnsPerHost: 0, // unlimited + IdleConnTimeout: 90 * time.Second, + ForceAttemptHTTP2: true, + } + + sharedHTTPClient = &http.Client{ + Timeout: 5 * time.Minute, // Timeout for presigned URL requests and uploads + Transport: sharedTransport, + } +) + // ProxyBlobStore proxies blob requests to an external storage service type ProxyBlobStore struct { ctx *RegistryContext // All context and services @@ -49,16 +79,9 @@ func NewProxyBlobStore(ctx *RegistryContext) *ProxyBlobStore { return &ProxyBlobStore{ ctx: ctx, holdURL: holdURL, - httpClient: &http.Client{ - Timeout: 5 * time.Minute, // Timeout for presigned URL requests and uploads - Transport: &http.Transport{ - DisableKeepAlives: false, // Re-enable keep-alive - MaxIdleConns: 100, - MaxIdleConnsPerHost: 100, - MaxConnsPerHost: 0, // unlimited - IdleConnTimeout: 90 * time.Second, - }, - }, + // Field stays per-instance so tests can substitute a client; the default + // points at the process-wide client so connections are actually reused. + httpClient: sharedHTTPClient, } }