go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/cmd/bbagent/build_client.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 main 16 17 import ( 18 "context" 19 "time" 20 21 "golang.org/x/time/rate" 22 "google.golang.org/genproto/protobuf/field_mask" 23 "google.golang.org/grpc" 24 "google.golang.org/grpc/codes" 25 "google.golang.org/grpc/metadata" 26 "google.golang.org/protobuf/types/known/fieldmaskpb" 27 28 "go.chromium.org/luci/auth" 29 "go.chromium.org/luci/common/errors" 30 "go.chromium.org/luci/common/lhttp" 31 "go.chromium.org/luci/common/logging" 32 "go.chromium.org/luci/common/proto/reflectutil" 33 "go.chromium.org/luci/common/retry" 34 "go.chromium.org/luci/common/retry/transient" 35 "go.chromium.org/luci/common/sync/dispatcher" 36 "go.chromium.org/luci/common/sync/dispatcher/buffer" 37 "go.chromium.org/luci/grpc/grpcutil" 38 "go.chromium.org/luci/grpc/prpc" 39 "go.chromium.org/luci/lucictx" 40 41 "go.chromium.org/luci/buildbucket" 42 bbpb "go.chromium.org/luci/buildbucket/proto" 43 ) 44 45 // BuildsClient is a trimmed version of `bbpb.BuildsClient` which only 46 // contains the required RPC methods for bbagent. 47 // 48 // The live implementation automatically binds the "x-buildbucket-token" key with 49 // a token where necessary. 50 // 51 // Note: The dummy implementation will always return an EMPTY Build message; 52 // Make sure any code using BuildsClient can handle this scenario. 53 type BuildsClient interface { 54 UpdateBuild(ctx context.Context, in *bbpb.UpdateBuildRequest, opts ...grpc.CallOption) (*bbpb.Build, error) 55 StartBuild(ctx context.Context, in *bbpb.StartBuildRequest, opts ...grpc.CallOption) (*bbpb.StartBuildResponse, error) 56 } 57 58 var _ BuildsClient = dummyBBClient{} 59 60 var readMask = &bbpb.BuildMask{ 61 Fields: &fieldmaskpb.FieldMask{ 62 Paths: []string{ 63 "id", 64 "status", 65 "cancel_time", 66 "start_time", 67 "update_time", 68 }, 69 }, 70 } 71 72 type dummyBBClient struct{} 73 74 func (dummyBBClient) UpdateBuild(ctx context.Context, in *bbpb.UpdateBuildRequest, opts ...grpc.CallOption) (*bbpb.Build, error) { 75 return &bbpb.Build{}, nil 76 } 77 78 func (dummyBBClient) StartBuild(ctx context.Context, in *bbpb.StartBuildRequest, opts ...grpc.CallOption) (*bbpb.StartBuildResponse, error) { 79 return &bbpb.StartBuildResponse{}, nil 80 } 81 82 type liveBBClient struct { 83 // A BUILD token for agent to call UpdateBuild. 84 buildToken string 85 // A START_BUILD token for agent to call StartBuild. 86 startBuildToken string 87 c bbpb.BuildsClient 88 retryF retry.Factory 89 } 90 91 func retryRPC(ctx context.Context, retryF retry.Factory, funcName string, f func() error) error { 92 return retry.Retry(ctx, transient.Only(retryF), f, func(err error, sleepTime time.Duration) { 93 logging.Fields{ 94 logging.ErrorKey: err, 95 "sleepTime": sleepTime, 96 }.Warningf(ctx, "%s will retry in %s", funcName, sleepTime) 97 }) 98 } 99 100 func (bb *liveBBClient) UpdateBuild(ctx context.Context, in *bbpb.UpdateBuildRequest, opts ...grpc.CallOption) (build *bbpb.Build, err error) { 101 if bb.buildToken == "" { 102 return nil, errors.New("update build token not found.") 103 } 104 ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs(buildbucket.BuildbucketTokenHeader, bb.buildToken)) 105 err = retryRPC(ctx, bb.retryF, "UpdateBuild", func() (err error) { 106 build, err = bb.c.UpdateBuild(ctx, in) 107 return grpcutil.WrapIfTransientOr(err, codes.DeadlineExceeded, codes.NotFound) 108 }) 109 return 110 } 111 112 func (bb *liveBBClient) StartBuild(ctx context.Context, in *bbpb.StartBuildRequest, opts ...grpc.CallOption) (res *bbpb.StartBuildResponse, err error) { 113 if bb.startBuildToken == "" { 114 return nil, errors.New("start build token not found.") 115 } 116 ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs(buildbucket.BuildbucketTokenHeader, bb.startBuildToken)) 117 err = retryRPC(ctx, bb.retryF, "StartBuild", func() (err error) { 118 res, err = bb.c.StartBuild(ctx, in) 119 return grpcutil.WrapIfTransientOr(err, codes.DeadlineExceeded, codes.NotFound) 120 }) 121 if err == nil { 122 bb.buildToken = res.UpdateBuildToken 123 } 124 return 125 } 126 127 // Reads the build secrets from the environment and constructs a BuildsClient 128 // which can be used to update the build state. 129 // 130 // retryEnabled allows us to switch retries for this client on and off 131 func newBuildsClient(ctx context.Context, bbagentCtx *bbpb.BuildbucketAgentContext, hostname string, retryF retry.Factory) (BuildsClient, error) { 132 if hostname == "" { 133 logging.Infof(ctx, "No buildbucket hostname set; making dummy buildbucket client.") 134 return dummyBBClient{}, nil 135 } 136 137 bc, err := newBuildsClientWithSecrets(ctx, hostname, retryF, bbagentCtx.Secrets) 138 if err != nil { 139 return nil, err 140 } 141 return bc, nil 142 } 143 144 // Reads the provided build secrets and constructs a BuildsClient 145 // which can be used to update the build state. 146 // 147 // retryEnabled allows us to switch retries for this client on and off 148 func newBuildsClientWithSecrets(ctx context.Context, hostname string, retryF retry.Factory, secrets *bbpb.BuildSecrets) (BuildsClient, error) { 149 if hostname == "" { 150 logging.Infof(ctx, "No buildbucket hostname set; making dummy buildbucket client.") 151 return dummyBBClient{}, nil 152 } 153 154 prpcClient := &prpc.Client{ 155 Host: hostname, 156 Options: &prpc.Options{ 157 Insecure: lhttp.IsLocalHost(hostname), 158 PerRPCTimeout: 30 * time.Second, 159 Debug: true, 160 }, 161 } 162 163 // Use "system" account to call UpdateBuild RPCs. 164 sctx, err := lucictx.SwitchLocalAccount(ctx, "system") 165 if err != nil { 166 return nil, errors.Annotate(err, "could not switch to 'system' account in LUCI_CONTEXT").Err() 167 } 168 prpcClient.C, err = auth.NewAuthenticator(sctx, auth.SilentLogin, auth.Options{ 169 MonitorAs: "bbagent/buildbucket", 170 }).Client() 171 if err != nil { 172 return nil, err 173 } 174 175 startBuildToken := secrets.StartBuildToken 176 if startBuildToken == "" { 177 // TODO(crbug.com/1416971): remove this. 178 startBuildToken = secrets.BuildToken 179 } 180 181 return &liveBBClient{ 182 buildToken: secrets.BuildToken, 183 startBuildToken: startBuildToken, 184 c: bbpb.NewBuildsPRPCClient(prpcClient), 185 retryF: retryF, 186 }, nil 187 } 188 189 // options for the dispatcher.Channel 190 func channelOpts(ctx context.Context) (*dispatcher.Options, <-chan error) { 191 errorFn, errCh := dispatcher.ErrorFnReport(10, func(failedBatch *buffer.Batch, err error) bool { 192 return transient.Tag.In(err) 193 }) 194 opts := &dispatcher.Options{ 195 QPSLimit: rate.NewLimiter(rate.Every(3*time.Second), 1), 196 MinQPS: rate.Every(buildbucket.MinUpdateBuildInterval), 197 Buffer: buffer.Options{ 198 MaxLeases: 1, 199 BatchItemsMax: 1, 200 FullBehavior: &buffer.DropOldestBatch{MaxLiveItems: 1}, 201 Retry: func() retry.Iterator { 202 return &retry.ExponentialBackoff{ 203 Limited: retry.Limited{ 204 Delay: 200 * time.Millisecond, // initial delay 205 Retries: -1, 206 MaxTotal: 5 * time.Minute, 207 }, 208 Multiplier: 1.2, 209 MaxDelay: 30 * time.Second, 210 } 211 }, 212 }, 213 DropFn: dispatcher.DropFnSummarized(ctx, rate.NewLimiter(rate.Every(10*time.Second), 1)), 214 ErrorFn: errorFn, 215 } 216 return opts, errCh 217 } 218 219 func mkSendFn(ctx context.Context, client BuildsClient, bID int64, canceledBuildCh *closeOnceCh) dispatcher.SendFn { 220 return func(b *buffer.Batch) error { 221 var req *bbpb.UpdateBuildRequest 222 223 // Nil batch. Synthesize a UpdateBuild request. 224 if b == nil { 225 req = &bbpb.UpdateBuildRequest{ 226 Build: &bbpb.Build{ 227 Id: bID, 228 }, 229 Mask: readMask, 230 } 231 } else if b.Meta != nil { 232 req = b.Meta.(*bbpb.UpdateBuildRequest) 233 } else { 234 build := b.Data[0].Item.(*bbpb.Build) 235 adaptedTags := buildbucket.WithoutDisallowedTagKeys(build.Tags) 236 if len(build.Tags) != len(adaptedTags) { 237 build = reflectutil.ShallowCopy(build).(*bbpb.Build) 238 build.Tags = adaptedTags 239 } 240 req = &bbpb.UpdateBuildRequest{ 241 Build: build, 242 UpdateMask: &field_mask.FieldMask{ 243 Paths: []string{ 244 "build.steps", 245 "build.output", 246 "build.summary_markdown", 247 }, 248 }, 249 Mask: readMask, 250 } 251 if len(build.Tags) > 0 { 252 req.UpdateMask.Paths = append(req.UpdateMask.Paths, "build.tags") 253 } 254 b.Meta = req 255 b.Data[0].Item = nil 256 } 257 258 updatedBuild, err := client.UpdateBuild(ctx, req) 259 if err != nil { 260 return err 261 } 262 if updatedBuild.CancelTime != nil { 263 logging.Infof(ctx, "The build is in the cancel process, cancel time is %s.", updatedBuild.CancelTime.AsTime().String()) 264 canceledBuildCh.close() 265 } 266 return nil 267 } 268 } 269 270 // defaultRetryStrategy defines a default build client retry strategy in bbagent. 271 func defaultRetryStrategy() retry.Iterator { 272 return &retry.ExponentialBackoff{ 273 Limited: retry.Limited{ 274 Delay: 200 * time.Millisecond, 275 Retries: 10, 276 }, 277 MaxDelay: 80 * time.Second, 278 Multiplier: 2, 279 } 280 }