go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/rpc/batch.go (about)

     1  // Copyright 2020 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package rpc
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  
    21  	"go.opentelemetry.io/otel"
    22  	otelcodes "go.opentelemetry.io/otel/codes"
    23  	"go.opentelemetry.io/otel/trace"
    24  	"google.golang.org/grpc/codes"
    25  	grpcStatus "google.golang.org/grpc/status"
    26  
    27  	"go.chromium.org/luci/common/errors"
    28  	"go.chromium.org/luci/common/logging"
    29  	"go.chromium.org/luci/common/sync/parallel"
    30  	"go.chromium.org/luci/grpc/appstatus"
    31  
    32  	"go.chromium.org/luci/buildbucket/appengine/internal/config"
    33  	pb "go.chromium.org/luci/buildbucket/proto"
    34  )
    35  
    36  const (
    37  	readReqsSizeLimit  = 1000
    38  	writeReqsSizeLimit = 200
    39  )
    40  
    41  var tracer = otel.Tracer("go.chromium.org/luci/buildbucket")
    42  
    43  // Batch handles a batch request. Implements pb.BuildsServer.
    44  func (b *Builds) Batch(ctx context.Context, req *pb.BatchRequest) (*pb.BatchResponse, error) {
    45  	globalCfg, err := config.GetSettingsCfg(ctx)
    46  	if err != nil {
    47  		return nil, errors.Annotate(err, "error fetching service config").Err()
    48  	}
    49  
    50  	res := &pb.BatchResponse{}
    51  	if len(req.GetRequests()) == 0 {
    52  		return res, nil
    53  	}
    54  	res.Responses = make([]*pb.BatchResponse_Response, len(req.Requests))
    55  
    56  	var goBatchReq []*pb.BatchRequest_Request
    57  	var schBatchReq []*pb.ScheduleBuildRequest
    58  	readReqs := 0
    59  	writeReqs := 0
    60  
    61  	// Record the mapping of indices in to indices in original req.
    62  	goIndices := make([]int, 0, len(req.Requests))
    63  	schIndices := make([]int, 0, len(req.Requests))
    64  	for i, r := range req.Requests {
    65  		switch r.Request.(type) {
    66  		case *pb.BatchRequest_Request_ScheduleBuild:
    67  			schIndices = append(schIndices, i)
    68  			schBatchReq = append(schBatchReq, r.GetScheduleBuild())
    69  			writeReqs++
    70  		case *pb.BatchRequest_Request_CancelBuild:
    71  			goIndices = append(goIndices, i)
    72  			goBatchReq = append(goBatchReq, r)
    73  			writeReqs++
    74  		case *pb.BatchRequest_Request_GetBuild, *pb.BatchRequest_Request_SearchBuilds, *pb.BatchRequest_Request_GetBuildStatus:
    75  			goIndices = append(goIndices, i)
    76  			goBatchReq = append(goBatchReq, r)
    77  			readReqs++
    78  		default:
    79  			return nil, appstatus.BadRequest(errors.New("request includes an unsupported type"))
    80  		}
    81  	}
    82  
    83  	if readReqs > readReqsSizeLimit {
    84  		return nil, appstatus.BadRequest(errors.Reason("the maximum allowed read request count in Batch is %d.", readReqsSizeLimit).Err())
    85  	}
    86  	if writeReqs > writeReqsSizeLimit {
    87  		return nil, appstatus.BadRequest(errors.Reason("the maximum allowed write request count in Batch is %d.", writeReqsSizeLimit).Err())
    88  	}
    89  
    90  	// ID used to log this Batch operation in the pRPC request log (see common.go).
    91  	// Used as the parent request log ID when logging individual operations here.
    92  	parent := trace.SpanContextFromContext(ctx).TraceID().String()
    93  	err = parallel.WorkPool(64, func(c chan<- func() error) {
    94  		c <- func() (err error) {
    95  			ctx, span := tracer.Start(ctx, "Batch.ScheduleBuild")
    96  			// Batch schedule requests. It allows partial success.
    97  			ret, merr := b.scheduleBuilds(ctx, globalCfg, schBatchReq)
    98  			defer func() { endSpan(span, err) }()
    99  			for i, e := range merr {
   100  				if e != nil {
   101  					res.Responses[schIndices[i]] = &pb.BatchResponse_Response{
   102  						Response: toBatchResponseError(ctx, e),
   103  					}
   104  					logToBQ(ctx, fmt.Sprintf("%s;%d", parent, schIndices[i]), parent, "ScheduleBuild")
   105  				}
   106  			}
   107  			for i, r := range ret {
   108  				if r != nil {
   109  					res.Responses[schIndices[i]] = &pb.BatchResponse_Response{
   110  						Response: &pb.BatchResponse_Response_ScheduleBuild{
   111  							ScheduleBuild: r,
   112  						},
   113  					}
   114  					logToBQ(ctx, fmt.Sprintf("%s;%d", parent, schIndices[i]), parent, "ScheduleBuild")
   115  				}
   116  			}
   117  			return nil
   118  		}
   119  		for i, r := range goBatchReq {
   120  			i, r := i, r
   121  			c <- func() (err error) {
   122  				ctx := ctx
   123  				method := ""
   124  				response := &pb.BatchResponse_Response{}
   125  
   126  				var span trace.Span // opened below
   127  				defer func() { endSpan(span, err) }()
   128  
   129  				switch r.Request.(type) {
   130  				case *pb.BatchRequest_Request_GetBuild:
   131  					ctx, span = tracer.Start(ctx, "Batch.GetBuild")
   132  					ret, e := b.GetBuild(ctx, r.GetGetBuild())
   133  					response.Response = &pb.BatchResponse_Response_GetBuild{GetBuild: ret}
   134  					err = e
   135  					method = "GetBuild"
   136  				case *pb.BatchRequest_Request_SearchBuilds:
   137  					ctx, span = tracer.Start(ctx, "Batch.SearchBuilds")
   138  					ret, e := b.SearchBuilds(ctx, r.GetSearchBuilds())
   139  					response.Response = &pb.BatchResponse_Response_SearchBuilds{SearchBuilds: ret}
   140  					err = e
   141  					method = "SearchBuilds"
   142  				case *pb.BatchRequest_Request_CancelBuild:
   143  					ctx, span = tracer.Start(ctx, "Batch.CancelBuild")
   144  					ret, e := b.CancelBuild(ctx, r.GetCancelBuild())
   145  					response.Response = &pb.BatchResponse_Response_CancelBuild{CancelBuild: ret}
   146  					err = e
   147  					method = "CancelBuild"
   148  				case *pb.BatchRequest_Request_GetBuildStatus:
   149  					ctx, span = tracer.Start(ctx, "Batch.GetBuildStatus")
   150  					ret, e := b.GetBuildStatus(ctx, r.GetGetBuildStatus())
   151  					response.Response = &pb.BatchResponse_Response_GetBuildStatus{GetBuildStatus: ret}
   152  					err = e
   153  					method = "GetBuildStatus"
   154  				default:
   155  					panic(fmt.Sprintf("attempted to handle unexpected request type %T", r.Request))
   156  				}
   157  				logToBQ(ctx, fmt.Sprintf("%s;%d", parent, goIndices[i]), parent, method)
   158  				if err != nil {
   159  					response.Response = toBatchResponseError(ctx, err)
   160  				}
   161  				res.Responses[goIndices[i]] = response
   162  				return nil
   163  			}
   164  		}
   165  	})
   166  	if err != nil {
   167  		return nil, err
   168  	}
   169  	return res, nil
   170  }
   171  
   172  // endSpan closes a tracing span.
   173  func endSpan(span trace.Span, err error) {
   174  	if span == nil {
   175  		return
   176  	}
   177  	if err != nil {
   178  		span.RecordError(err)
   179  		span.SetStatus(otelcodes.Error, err.Error())
   180  	}
   181  	span.End()
   182  }
   183  
   184  // toBatchResponseError converts an error to BatchResponse_Response_Error type.
   185  func toBatchResponseError(ctx context.Context, err error) *pb.BatchResponse_Response_Error {
   186  	if errors.Contains(err, context.DeadlineExceeded) {
   187  		return &pb.BatchResponse_Response_Error{Error: grpcStatus.New(codes.DeadlineExceeded, "deadline exceeded").Proto()}
   188  	}
   189  	st, ok := appstatus.Get(err)
   190  	if !ok {
   191  		if gStatus, ok := grpcStatus.FromError(err); ok {
   192  			return &pb.BatchResponse_Response_Error{Error: gStatus.Proto()}
   193  		}
   194  		logging.Errorf(ctx, "Non-appstatus and non-grpc error in a batch response: %s", err)
   195  		return &pb.BatchResponse_Response_Error{Error: grpcStatus.New(codes.Internal, "Internal server error").Proto()}
   196  	}
   197  	return &pb.BatchResponse_Response_Error{Error: st.Proto()}
   198  }