blob: 56ea0663d4fc72f6a9f29bff609f068da486b05b [file]
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"context"
"fmt"
"log"
"net"
"net/http"
"os"
"sync"
"time"
"cloud.google.com/go/storage"
"golang.org/x/net/http2"
"google.golang.org/api/option"
htransport "google.golang.org/api/transport/http"
"google.golang.org/grpc"
)
// clientPool functions much like a sync Pool (https://pkg.go.dev/sync#Pool),
// except it does not automatically remove items stored in the clientPool.
// Re-using the clients rather than creating a new one each time reduces overhead
// (such as re-creating the underlying HTTP client and opening credential files),
// and is the intended way to use Storage clients.
//
// There is no limit to how many clients will be created, but it should be around
// the order of 5 * min(workers, max_samples).
type clientPool struct {
New func() *storage.Client
clients []*storage.Client
}
func (p *clientPool) Get() *storage.Client {
// Create the slice if not already created
if p.clients == nil {
p.clients = make([]*storage.Client, 0)
}
// If there is an unused client, return it
if len(p.clients) > 0 {
c := p.clients[0]
p.clients = p.clients[1:]
return c
}
// Otherwise, create a new client and return it
return p.New()
}
func (p *clientPool) Put(c *storage.Client) {
p.clients = append(p.clients, c)
}
// we can share clients as long as the app buffer sizes are constant
var httpClients, gRPCClients *clientPool
var nonBenchmarkingClients = clientPool{
New: func() *storage.Client {
// For debuggability's sake, these are HTTP
clientMu.Lock()
client, err := storage.NewClient(context.Background())
clientMu.Unlock()
if err != nil {
log.Fatalf("storage.NewClient: %v", err)
}
return client
},
}
func initializeClientPools(opts *benchmarkOptions) func() {
httpClients = &clientPool{
New: func() *storage.Client {
client, err := initializeHTTPClient(context.Background(), opts.minWriteSize, opts.maxReadSize, opts.useDefaults)
if err != nil {
log.Fatalf("initializeHTTPClient: %v", err)
}
return client
},
}
gRPCClients = &clientPool{
New: func() *storage.Client {
client, err := initializeGRPCClient(context.Background(), opts.minWriteSize, opts.maxReadSize, opts.connPoolSize, opts.useDefaults)
if err != nil {
log.Fatalf("initializeGRPCClient: %v", err)
}
return client
},
}
return func() {
for _, c := range httpClients.clients {
c.Close()
}
for _, c := range gRPCClients.clients {
c.Close()
}
}
}
// We can't pool storage clients if we need to change parameters at the HTTP or GRPC client level,
// since we can't access those after creation as it is set up now.
// If we are using defaults (ie. not creating an underlying HTTP client ourselves), or if
// we are only interested in one app buffer size at a time, we don't need to change anything on the underlying
// client and can re-use it (and therefore the storage client) for other benchmark runs.
func canUseClientPool(opts *benchmarkOptions) bool {
return opts.useDefaults || (opts.maxReadSize == opts.minReadSize && opts.maxWriteSize == opts.minWriteSize)
}
func getClient(ctx context.Context, opts *benchmarkOptions, br benchmarkResult) (*storage.Client, func() error, error) {
noOp := func() error { return nil }
grpc := br.params.api == grpcAPI || br.params.api == directPath
if canUseClientPool(opts) {
if grpc {
c := gRPCClients.Get()
return c, func() error { gRPCClients.Put(c); return nil }, nil
}
c := httpClients.Get()
return c, func() error { httpClients.Put(c); return nil }, nil
}
// if necessary, create a client
if grpc {
c, err := initializeGRPCClient(ctx, br.params.appBufferSize, br.params.appBufferSize, opts.connPoolSize, false)
if err != nil {
return nil, noOp, fmt.Errorf("initializeGRPCClient: %w", err)
}
return c, c.Close, nil
}
c, err := initializeHTTPClient(ctx, br.params.appBufferSize, br.params.appBufferSize, false)
if err != nil {
return nil, noOp, fmt.Errorf("initializeHTTPClient: %w", err)
}
return c, c.Close, nil
}
// mutex on starting a client so that we can set an env variable for GRPC clients
var clientMu sync.Mutex
func initializeHTTPClient(ctx context.Context, writeBufferSize, readBufferSize int, useDefaults bool) (*storage.Client, error) {
if useDefaults {
clientMu.Lock()
c, err := storage.NewClient(ctx, option.WithCredentialsFile(credentialsFile))
clientMu.Unlock()
return c, err
}
dialer := &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}
// These are the default parameters with write and read buffer sizes modified
base := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: dialer.DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
WriteBufferSize: writeBufferSize,
ReadBufferSize: readBufferSize,
}
http2Trans, err := http2.ConfigureTransports(base)
if err == nil {
http2Trans.ReadIdleTimeout = time.Second * 31
}
trans, err := htransport.NewTransport(ctx, base,
option.WithScopes("https://www.googleapis.com/auth/devstorage.full_control"),
option.WithCredentialsFile(credentialsFile))
if err != nil {
return nil, err
}
clientMu.Lock()
client, err := storage.NewClient(ctx, option.WithHTTPClient(&http.Client{Transport: trans}))
clientMu.Unlock()
return client, err
}
func initializeGRPCClient(ctx context.Context, writeBufferSize, readBufferSize int, connPoolSize int, useDefaults bool) (*storage.Client, error) {
if useDefaults {
clientMu.Lock()
os.Setenv("STORAGE_USE_GRPC", "true")
c, err := storage.NewClient(ctx, option.WithCredentialsFile(credentialsFile))
os.Unsetenv("STORAGE_USE_GRPC")
clientMu.Unlock()
return c, err
}
clientMu.Lock()
os.Setenv("STORAGE_USE_GRPC", "true")
client, err := storage.NewClient(ctx, option.WithCredentialsFile(credentialsFile),
option.WithGRPCDialOption(grpc.WithReadBufferSize(readBufferSize)),
option.WithGRPCDialOption(grpc.WithWriteBufferSize(writeBufferSize)),
option.WithGRPCConnectionPool(connPoolSize))
os.Unsetenv("STORAGE_USE_GRPC")
clientMu.Unlock()
return client, err
}