diff --git a/docs/config.html b/docs/config.html index 6b7269172..6323d1c94 100644 --- a/docs/config.html +++ b/docs/config.html @@ -963,6 +963,19 @@

Defines the number of remote executors to use simultaneously.

+
  • +
    +

    + CASConnections (int) +

    + +

    + Number of connections to open to the CAS server. More connections can + improve throughput for large transfers, at the cost of more load on the + server. Defaults to 4. +

    +
    +
  • Name

    diff --git a/src/core/config.go b/src/core/config.go index f293c867a..80e28854d 100644 --- a/src/core/config.go +++ b/src/core/config.go @@ -407,6 +407,7 @@ func DefaultConfiguration() *Configuration { config.Display.MaxWorkers = 40 config.Display.ColourScheme = "dark" config.Remote.NumExecutors = 20 // kind of arbitrary + config.Remote.CASConnections = 4 config.Remote.Secure = true config.Remote.VerifyOutputs = true config.Remote.UploadDirs = true @@ -566,6 +567,7 @@ type Configuration struct { CASURL string `help:"URL for the CAS service, if it is different to the main one."` AssetURL string `help:"URL for the remote asset server, if it is different to the main one."` NumExecutors int `help:"Maximum number of remote executors to use simultaneously."` + CASConnections int `help:"Number of connections to open to the CAS server. More connections can improve throughput for large transfers, at the cost of more load on the server."` Instance string `help:"Remote instance name to request; depending on the server this may be required."` Name string `help:"A name for this worker instance. This is attached to artifacts uploaded to remote storage." example:"agent-001"` DisplayURL string `help:"A URL to browse the remote server with (e.g. using buildbarn-browser). Only used when printing hashes."` diff --git a/src/remote/BUILD b/src/remote/BUILD index 3c5343b2b..4695fbb3e 100644 --- a/src/remote/BUILD +++ b/src/remote/BUILD @@ -10,6 +10,7 @@ go_library( "///third_party/go/cloud.google.com_go_longrunning//autogen/longrunningpb", "///third_party/go/github.com_alessio_shellescape//:shellescape", "///third_party/go/github.com_bazelbuild_remote-apis-sdks//go/api/command", + "///third_party/go/github.com_bazelbuild_remote-apis-sdks//go/pkg/balancer", "///third_party/go/github.com_bazelbuild_remote-apis-sdks//go/pkg/client", "///third_party/go/github.com_bazelbuild_remote-apis-sdks//go/pkg/command", "///third_party/go/github.com_bazelbuild_remote-apis-sdks//go/pkg/digest", diff --git a/src/remote/remote.go b/src/remote/remote.go index b2ef4fad0..51d085fd4 100644 --- a/src/remote/remote.go +++ b/src/remote/remote.go @@ -15,6 +15,7 @@ import ( "time" "cloud.google.com/go/longrunning/autogen/longrunningpb" + "github.com/bazelbuild/remote-apis-sdks/go/pkg/balancer" "github.com/bazelbuild/remote-apis-sdks/go/pkg/client" "github.com/bazelbuild/remote-apis-sdks/go/pkg/digest" "github.com/bazelbuild/remote-apis-sdks/go/pkg/filemetadata" @@ -207,13 +208,11 @@ func (c *Client) initExec() error { if err != nil { return err } - client, err := client.NewClient(context.Background(), c.instance, client.DialParams{ - Service: c.state.Config.Remote.URL, - CASService: c.state.Config.Remote.CASURL, - NoSecurity: !c.state.Config.Remote.Secure, - TransportCredsOnly: c.state.Config.Remote.Secure, - DialOpts: dialOpts, - }, client.UseBatchOps(true), &client.TreeSymlinkOpts{Preserved: true}, client.RetryTransient(), client.RPCTimeouts(map[string]time.Duration{ + execConn, casConn, err := c.dialConnections(dialOpts) + if err != nil { + return err + } + client, err := client.NewClientFromConnection(context.Background(), c.instance, execConn, casConn, client.UseBatchOps(true), &client.TreeSymlinkOpts{Preserved: true}, client.RetryTransient(), client.RPCTimeouts(map[string]time.Duration{ "default": time.Duration(c.state.Config.Remote.Timeout), "GetCapabilities": 5 * time.Second, "BatchUpdateBlobs": time.Minute, @@ -284,6 +283,52 @@ func (c *Client) initExec() error { return nil } +// dialConnections opens the connections to the execution and CAS servers. +// The two have quite different traffic so we size them separately; execution is a relatively +// small number of long-lived but mostly idle streams, whereas CAS has many short-lived +// RPCs which can benefit from multiple connections. +func (c *Client) dialConnections(dialOpts []grpc.DialOption) (execConn, casConn client.GrpcClientConn, err error) { + ctx := context.Background() + opts, _, err := client.OptsFromParams(ctx, client.DialParams{ + NoSecurity: !c.state.Config.Remote.Secure, + TransportCredsOnly: c.state.Config.Remote.Secure, + DialOpts: dialOpts, + }) + if err != nil { + return nil, nil, fmt.Errorf("failed to prepare gRPC dial options: %w", err) + } + dial := func(url string, n int) (client.GrpcClientConn, error) { + pool, err := balancer.NewRRConnPool(ctx, n, func(ctx context.Context) (*grpc.ClientConn, error) { + return grpc.NewClient(url, opts...) + }) + if err != nil { + return nil, fmt.Errorf("failed to connect to %s: %w", url, err) + } + return pool, nil + } + // gRPC won't open more than one HTTP/2 connection per ClientConn, so if the server (or some + // proxy in front of it) limits concurrent streams per connection, further Execute calls block + // until one finishes. Common limits are 100-128; 64 per connection gives some headroom since + // round-robin doesn't balance long-lived streams perfectly. + numExecConns := max((c.state.Config.NumRemoteExecutors()+63)/64, 1) + numCASConns := max(c.state.Config.Remote.CASConnections, 1) + url := c.state.Config.Remote.URL + casURL := c.state.Config.Remote.CASURL + if casURL == "" || casURL == url { + // Both kinds of traffic share these connections, so we need the capacity of both. + conn, err := dial(url, numExecConns+numCASConns) + return conn, conn, err + } + if execConn, err = dial(url, numExecConns); err != nil { + return nil, nil, err + } + if casConn, err = dial(casURL, numCASConns); err != nil { + execConn.Close() + return nil, nil, err + } + return execConn, casConn, nil +} + // initFetch initialises the remote fetch server. func (c *Client) initFetch() error { dialOpts, err := c.dialOpts()