go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/cli/batch.go (about) 1 // Copyright 2019 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 cli 16 17 import ( 18 "bytes" 19 "context" 20 "fmt" 21 "io" 22 "os" 23 "reflect" 24 "time" 25 26 "github.com/golang/protobuf/jsonpb" 27 "github.com/maruel/subcommands" 28 "google.golang.org/grpc/codes" 29 "google.golang.org/grpc/metadata" 30 "google.golang.org/grpc/status" 31 32 "go.chromium.org/luci/common/cli" 33 "go.chromium.org/luci/common/errors" 34 "go.chromium.org/luci/common/logging" 35 "go.chromium.org/luci/common/proto" 36 "go.chromium.org/luci/common/retry" 37 "go.chromium.org/luci/common/retry/transient" 38 "go.chromium.org/luci/grpc/grpcutil" 39 40 "go.chromium.org/luci/buildbucket" 41 pb "go.chromium.org/luci/buildbucket/proto" 42 ) 43 44 func cmdBatch(p Params) *subcommands.Command { 45 return &subcommands.Command{ 46 UsageLine: `batch [flags]`, 47 ShortDesc: "calls buildbucket.v2.Builds.Batch", 48 LongDesc: doc(` 49 Calls buildbucket.v2.Builds.Batch. 50 51 Stdin must be buildbucket.v2.BatchRequest in JSON format. 52 Stdout will be buildbucket.v2.BatchResponse in JSON format. 53 Exits with code 1 if at least one sub-request fails. 54 `), 55 CommandRun: func() subcommands.CommandRun { 56 r := &batchRun{} 57 r.RegisterDefaultFlags(p) 58 return r 59 }, 60 } 61 } 62 63 type batchRun struct { 64 baseCommandRun 65 pb.BatchRequest 66 } 67 68 func (r *batchRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { 69 ctx := cli.GetContext(a, r, env) 70 if err := r.initClients(ctx, nil); err != nil { 71 return r.done(ctx, err) 72 } 73 74 if len(args) != 0 { 75 return r.done(ctx, fmt.Errorf("unexpected argument")) 76 } 77 78 requestBytes, err := io.ReadAll(os.Stdin) 79 if err != nil { 80 return r.done(ctx, errors.Annotate(err, "failed to read stdin").Err()) 81 } 82 requestBytes, err = proto.FixFieldMasksBeforeUnmarshal(requestBytes, reflect.TypeOf(pb.BatchRequest{})) 83 if err != nil { 84 return r.done(ctx, errors.Annotate(err, "failed to parse BatchRequest from stdin").Err()) 85 } 86 req := &pb.BatchRequest{} 87 if err := jsonpb.Unmarshal(bytes.NewReader(requestBytes), req); err != nil { 88 return r.done(ctx, errors.Annotate(err, "failed to parse BatchRequest from stdin").Err()) 89 } 90 91 // Do not attach the buildbucket token if it's empty or the build is a led build. 92 // Because led builds are not real Buildbucket builds and they don't have 93 // real buildbucket tokens, so we cannot make them any builds's parent, 94 // even for the builds they scheduled. 95 if r.scheduleBuildToken != "" && r.scheduleBuildToken != buildbucket.DummyBuildbucketToken { 96 ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs(buildbucket.BuildbucketTokenHeader, r.scheduleBuildToken)) 97 } 98 99 // For led build, also clear out the canOutliveParent fields. 100 updateRequest(ctx, req, r.scheduleBuildToken) 101 102 res, err := sendBatchReq(ctx, req, r.buildsClient) 103 if err != nil { 104 return r.done(ctx, err) 105 } 106 107 m := &jsonpb.Marshaler{} 108 if err := m.Marshal(os.Stdout, res); err != nil { 109 return r.done(ctx, err) 110 } 111 112 for _, r := range res.Responses { 113 if _, ok := r.Response.(*pb.BatchResponse_Response_Error); ok { 114 return 1 115 } 116 } 117 return 0 118 } 119 120 // sendBatchReq sends the Batch request to Buildbucket and handles retries. 121 func sendBatchReq(ctx context.Context, req *pb.BatchRequest, buildsClient pb.BuildsClient) (*pb.BatchResponse, error) { 122 res := &pb.BatchResponse{ 123 Responses: make([]*pb.BatchResponse_Response, len(req.Requests)), 124 } 125 idxMap := make([]int, len(req.Requests)) // req.Requests index -> res.Responses index 126 for i := 0; i < len(req.Requests); i++ { 127 idxMap[i] = i 128 } 129 var globalErr error 130 _ = retry.Retry(ctx, transient.Only(retry.Default), func() error { 131 toRetry := make([]*pb.BatchRequest_Request, 0, len(req.Requests)) 132 idxMapForRetry := make([]int, 0, len(req.Requests)) 133 results, err := buildsClient.Batch(ctx, req) 134 if err != nil { 135 globalErr = err 136 return nil // buildsClient has already handled top-level retryable errors. 137 } 138 for i, result := range results.Responses { 139 if sts := result.GetError(); sts != nil { 140 code := status.FromProto(sts).Code() 141 // Should also retry if some sub-requests timed out. 142 if grpcutil.IsTransientCode(code) || code == codes.DeadlineExceeded { 143 idxMapForRetry = append(idxMapForRetry, idxMap[i]) 144 toRetry = append(toRetry, req.Requests[i]) 145 } 146 } 147 res.Responses[idxMap[i]] = result 148 } 149 if len(toRetry) > 0 { 150 idxMap = idxMapForRetry 151 req = &pb.BatchRequest{ 152 Requests: toRetry, 153 } 154 return errors.Reason("%d/%d batch subrequests failed", len(toRetry), len(res.Responses)).Tag(transient.Tag).Err() 155 } 156 return nil 157 }, func(err error, d time.Duration) { 158 logging.WithError(err).Debugf(ctx, "retrying them in %s...", d) 159 }) 160 161 return res, globalErr 162 } 163 164 // updateRequest makes changes to the batch request. 165 // Currently it unsets each sub ScheduleBuild request's CanOutliveParent 166 // if the token is a dummy token (meaning the build is a led build). 167 func updateRequest(ctx context.Context, req *pb.BatchRequest, tok string) { 168 updated := false 169 if tok == buildbucket.DummyBuildbucketToken { 170 for _, r := range req.Requests { 171 switch r.Request.(type) { 172 case *pb.BatchRequest_Request_ScheduleBuild: 173 r.GetScheduleBuild().CanOutliveParent = pb.Trinary_UNSET 174 updated = true 175 default: 176 continue 177 } 178 } 179 } 180 if updated { 181 logging.Infof(ctx, "ScheduleBuildRequest.CanOutliveParent is unset for led build") 182 } 183 }