blob: ce4c5435223c128081c2fcd7cb5fb32fb690e255 [file]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package reapi
import (
"context"
"fmt"
"io"
"net"
"net/url"
"os"
"sync"
"time"
bspb "google.golang.org/genproto/googleapis/bytestream"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"go.chromium.org/build/hashigo/digest"
rpb "go.chromium.org/build/remote-apis/build/bazel/remote/execution/v2"
"go.chromium.org/build/siso/o11y/clog"
)
// Proxy is RE API proxy.
type Proxy struct {
client *Client
addr string
}
// NewProxy creates new Proxy using client at addr.
func NewProxy(client *Client, addr string) *Proxy {
return &Proxy{
client: client,
addr: addr,
}
}
// Serve serves RE API requests and proxies to the client.
func (p *Proxy) Serve(ctx context.Context) error {
loc, err := url.Parse(p.addr)
if err != nil {
return fmt.Errorf("invalid addr %q: %w", p.addr, err)
}
err = os.Remove(loc.Path)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove %s: %w", loc.Path, err)
}
lis, err := net.Listen(loc.Scheme, loc.Path)
if err != nil {
return fmt.Errorf("failed to listen %s %s: %w", loc.Scheme, loc.Path, err)
}
// TODO: set recv msg size based on server capabilities?
server := grpc.NewServer(grpc.MaxRecvMsgSize(20 * 1024 * 1024))
cp := &capabilitiesProxy{
client: rpb.NewCapabilitiesClient(p.client.conn),
resps: map[string]*rpb.ServerCapabilities{
p.client.opt.Instance: p.client.capabilities,
},
}
rpb.RegisterCapabilitiesServer(server, cp)
ap := &actionCacheProxy{client: rpb.NewActionCacheClient(p.client.casConn)}
rpb.RegisterActionCacheServer(server, ap)
casp := &contentAddressableStorageProxy{
client: rpb.NewContentAddressableStorageClient(p.client.casConn),
emptyDigest: p.client.digestFn.Empty(),
writableInstances: make(map[string]bool),
}
casp.checkWritable(ctx, p.client)
rpb.RegisterContentAddressableStorageServer(server, casp)
ep := &executionProxy{client: rpb.NewExecutionClient(p.client.conn)}
rpb.RegisterExecutionServer(server, ep)
bsp := &byteStreamProxy{client: bspb.NewByteStreamClient(p.client.casConn)}
bspb.RegisterByteStreamServer(server, bsp)
errCh := make(chan error, 1)
go func() {
errCh <- server.Serve(lis)
}()
select {
case <-ctx.Done():
stopped := make(chan struct{})
go func() {
server.GracefulStop()
close(stopped)
}()
// Give active requests 30 seconds to finish, then force close
shutdownTimer := time.NewTimer(30 * time.Second)
select {
case <-stopped:
shutdownTimer.Stop()
fmt.Printf("gracefully shut down proxy\n")
case <-shutdownTimer.C:
// Forcefully stop the server if it takes too long
server.Stop()
fmt.Fprintf(os.Stderr, "forced shutdown of proxy\n")
}
return <-errCh
case err := <-errCh:
return err
}
}
type capabilitiesProxy struct {
rpb.UnimplementedCapabilitiesServer
client rpb.CapabilitiesClient
mu sync.Mutex
resps map[string]*rpb.ServerCapabilities
}
func (cp *capabilitiesProxy) GetCapabilities(ctx context.Context, req *rpb.GetCapabilitiesRequest) (*rpb.ServerCapabilities, error) {
cp.mu.Lock()
resp, ok := cp.resps[req.InstanceName]
cp.mu.Unlock()
if ok {
clog.Infof(ctx, "cached capabilities for %q: %v", req.InstanceName, resp)
return resp, nil
}
resp, err := cp.client.GetCapabilities(ctx, req)
if err != nil {
return resp, err
}
cp.mu.Lock()
cp.resps[req.InstanceName] = resp
cp.mu.Unlock()
return resp, nil
}
type actionCacheProxy struct {
rpb.UnimplementedActionCacheServer
client rpb.ActionCacheClient
}
func (ap *actionCacheProxy) GetActionResult(ctx context.Context, req *rpb.GetActionResultRequest) (*rpb.ActionResult, error) {
return ap.client.GetActionResult(ctx, req)
}
func (ap *actionCacheProxy) UpdateActionResult(ctx context.Context, req *rpb.UpdateActionResultRequest) (*rpb.ActionResult, error) {
return ap.client.UpdateActionResult(ctx, req)
}
func (ap *actionCacheProxy) AddActionLookup(ctx context.Context, req *rpb.AddActionLookupRequest) (*rpb.AddActionLookupResponse, error) {
return ap.client.AddActionLookup(ctx, req)
}
func (ap *actionCacheProxy) ListActions(ctx context.Context, req *rpb.ListActionsRequest) (*rpb.ListActionsResponse, error) {
return ap.client.ListActions(ctx, req)
}
type contentAddressableStorageProxy struct {
rpb.UnimplementedContentAddressableStorageServer
client rpb.ContentAddressableStorageClient
// emptyDigest is the digest of the empty blob under the backend's digest
// function, used to recognize writability probes.
emptyDigest digest.Digest
mu sync.Mutex
writableInstances map[string]bool
}
func (cp *contentAddressableStorageProxy) checkWritable(ctx context.Context, client *Client) {
err := client.CheckWritable(ctx)
if err != nil {
clog.Warningf(ctx, "check writable: %v", err)
return
}
cp.mu.Lock()
defer cp.mu.Unlock()
cp.writableInstances[client.opt.Instance] = true
clog.Infof(ctx, "check writable %q: true", client.opt.Instance)
}
func (cp *contentAddressableStorageProxy) FindMissingBlobs(ctx context.Context, req *rpb.FindMissingBlobsRequest) (*rpb.FindMissingBlobsResponse, error) {
return cp.client.FindMissingBlobs(ctx, req)
}
func (cp *contentAddressableStorageProxy) BatchUpdateBlobs(ctx context.Context, req *rpb.BatchUpdateBlobsRequest) (*rpb.BatchUpdateBlobsResponse, error) {
if len(req.Requests) == 1 && digest.FromProto(req.Requests[0].GetDigest()) == cp.emptyDigest && len(req.Requests[0].GetData()) == 0 {
cp.mu.Lock()
ok := cp.writableInstances[req.InstanceName]
cp.mu.Unlock()
if ok {
clog.Infof(ctx, "cached check writable %q: true", req.InstanceName)
return &rpb.BatchUpdateBlobsResponse{
Responses: []*rpb.BatchUpdateBlobsResponse_Response{
{
Digest: req.Requests[0].GetDigest(),
Status: status.New(codes.OK, "").Proto(),
},
},
}, nil
}
}
return cp.client.BatchUpdateBlobs(ctx, req)
}
func (cp *contentAddressableStorageProxy) BatchReadBlobs(ctx context.Context, req *rpb.BatchReadBlobsRequest) (*rpb.BatchReadBlobsResponse, error) {
return cp.client.BatchReadBlobs(ctx, req)
}
func (cp *contentAddressableStorageProxy) GetTree(req *rpb.GetTreeRequest, serv rpb.ContentAddressableStorage_GetTreeServer) error {
gtc, err := cp.client.GetTree(serv.Context(), req)
if err != nil {
return err
}
for {
resp, err := gtc.Recv()
if err != nil {
return err
}
err = serv.Send(resp)
if err != nil {
return err
}
if resp.GetNextPageToken() != "" {
continue
}
return nil
}
}
type executionProxy struct {
rpb.UnimplementedExecutionServer
client rpb.ExecutionClient
}
func (ep *executionProxy) Execute(req *rpb.ExecuteRequest, serv rpb.Execution_ExecuteServer) error {
exc, err := ep.client.Execute(serv.Context(), req)
if err != nil {
return err
}
for {
op, err := exc.Recv()
if err != nil {
return err
}
err = serv.Send(op)
if err != nil {
return err
}
if !op.GetDone() {
continue
}
return nil
}
}
func (ep *executionProxy) WaitExecution(req *rpb.WaitExecutionRequest, serv rpb.Execution_WaitExecutionServer) error {
exc, err := ep.client.WaitExecution(serv.Context(), req)
if err != nil {
return err
}
for {
op, err := exc.Recv()
if err != nil {
return err
}
err = serv.Send(op)
if err != nil {
return err
}
if !op.GetDone() {
continue
}
return nil
}
}
type byteStreamProxy struct {
bspb.UnimplementedByteStreamServer
client bspb.ByteStreamClient
}
func (bp *byteStreamProxy) Read(req *bspb.ReadRequest, serv bspb.ByteStream_ReadServer) error {
bsc, err := bp.client.Read(serv.Context(), req)
if err != nil {
return err
}
for {
resp, err := bsc.Recv()
if err == io.EOF { //nolint:errorlint
return nil
}
if err != nil {
return err
}
err = serv.Send(resp)
if err != nil {
return err
}
}
}
func (bp *byteStreamProxy) Write(serv bspb.ByteStream_WriteServer) error {
bsc, err := bp.client.Write(serv.Context())
if err != nil {
return err
}
for {
wreq, err := serv.Recv()
if err == io.EOF { //nolint:errorlint
resp, err := bsc.CloseAndRecv()
if err != nil {
return err
}
return serv.SendAndClose(resp)
}
if err != nil {
return err
}
err = bsc.Send(wreq)
if err != nil {
return err
}
}
}