go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/prjmanager/cltriggerer/trigger_dep_op.go (about) 1 // Copyright 2023 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 cltriggerer 16 17 import ( 18 "context" 19 "fmt" 20 "sync/atomic" 21 "time" 22 23 "google.golang.org/grpc" 24 "google.golang.org/grpc/codes" 25 26 "go.chromium.org/luci/common/errors" 27 "go.chromium.org/luci/common/logging" 28 gerritpb "go.chromium.org/luci/common/proto/gerrit" 29 "go.chromium.org/luci/common/retry/transient" 30 "go.chromium.org/luci/grpc/grpcutil" 31 32 "go.chromium.org/luci/cv/internal/changelist" 33 "go.chromium.org/luci/cv/internal/common" 34 "go.chromium.org/luci/cv/internal/common/lease" 35 "go.chromium.org/luci/cv/internal/gerrit" 36 "go.chromium.org/luci/cv/internal/gerrit/trigger" 37 "go.chromium.org/luci/cv/internal/prjmanager/prjpb" 38 "go.chromium.org/luci/cv/internal/run" 39 ) 40 41 type triggerDepResult struct { 42 voteDone bool 43 lastErr error 44 } 45 46 type triggerDepOp struct { 47 depCLID int64 48 originCLURL string 49 trigger *run.Trigger 50 51 result triggerDepResult 52 isCanceled *atomic.Bool 53 } 54 55 func makeTriggerDepOps(originCLURL string, payload *prjpb.TriggeringCLDeps, isCanceled *atomic.Bool) []*triggerDepOp { 56 deps := payload.GetDepClids() 57 if len(deps) == 0 { 58 return nil 59 } 60 ret := make([]*triggerDepOp, len(deps)) 61 for i, dep := range deps { 62 ret[i] = &triggerDepOp{ 63 depCLID: dep, 64 originCLURL: originCLURL, 65 trigger: payload.GetTrigger(), 66 isCanceled: isCanceled, 67 } 68 } 69 return ret 70 } 71 72 func (op *triggerDepOp) isSucceeded() bool { 73 return op != nil && op.result.voteDone 74 } 75 76 func (op *triggerDepOp) isPermanentlyFailed() bool { 77 if op.isSucceeded() { 78 return false 79 } 80 switch err := errors.Unwrap(op.result.lastErr); err { 81 case nil, context.Canceled, context.DeadlineExceeded: 82 return false 83 default: 84 return !transient.Tag.In(err) 85 } 86 } 87 88 // getCLError() returns changelist.CLError_TriggerDeps for the permanent error. 89 // 90 // Panic if the lastErr is not a permanent error. 91 func (op *triggerDepOp) getCLError() *changelist.CLError_TriggerDeps { 92 if !op.isPermanentlyFailed() { 93 panic(fmt.Errorf("FIXME: triggerDepOp.getCLError() called for non-permanent error")) 94 } 95 96 failure := &changelist.CLError_TriggerDeps{} 97 switch grpcutil.Code(op.result.lastErr) { 98 case codes.OK: 99 panic(fmt.Errorf("FIXME: triggerDepOp.result.lastErr with codes.OK")) 100 case codes.PermissionDenied: 101 failure.PermissionDenied = append(failure.PermissionDenied, 102 &changelist.CLError_TriggerDeps_PermissionDenied{ 103 Clid: op.depCLID, 104 Email: op.trigger.GetEmail(), 105 }, 106 ) 107 case codes.NotFound: 108 failure.NotFound = append(failure.NotFound, op.depCLID) 109 default: 110 failure.InternalGerritError = append(failure.InternalGerritError, op.depCLID) 111 } 112 return failure 113 } 114 115 func isAlreadyVoted(ctx context.Context, depCL *changelist.CL) bool { 116 // Skip voting if the CL already have a CQ vote. 117 switch mode := findCQTriggerMode(depCL); mode { 118 case "": 119 case string(run.FullRun): 120 logging.Infof(ctx, "the CL is voted already; skip triggering") 121 return true 122 default: 123 logging.Infof(ctx, "the CL is voted for %q; overriding", mode) 124 } 125 return false 126 } 127 128 func (op *triggerDepOp) execute(ctx context.Context, gFactory gerrit.Factory, luciPrj string, clm *changelist.Mutator, clu clUpdater) error { 129 if op.isCanceled.Load() { 130 return nil 131 } 132 // Lease the CL to prevent other unexpected CL updates, during the vote 133 // process. 134 ctx, leaseClose, lErr := lease.ApplyOnCL(ctx, common.CLID(op.depCLID), 2*time.Minute, "triggerDepOp") 135 if lErr != nil { 136 return lErr 137 } 138 defer leaseClose() 139 140 depCL := &changelist.CL{ID: common.CLID(op.depCLID)} 141 if err := changelist.LoadCLs(ctx, []*changelist.CL{depCL}); err != nil { 142 return err 143 } 144 // if the snapshot has the vote and fresh already, skip other operations. 145 if isAlreadyVoted(ctx, depCL) && depCL.Snapshot.GetOutdated() == nil { 146 op.result.voteDone = true 147 return nil 148 } 149 if err := op.vote(ctx, gFactory, luciPrj, depCL); err != nil { 150 return errors.Annotate(err, "op.vote").Err() 151 } 152 return errors.Annotate(op.markOutdated(ctx, luciPrj, clm, clu, depCL), "triggerDepOp.markOutdated").Err() 153 } 154 155 func processGerritErr(ctx context.Context, err error) error { 156 switch grpcutil.Code(err) { 157 case codes.OK: 158 return nil 159 case codes.PermissionDenied: 160 return err 161 case codes.NotFound: 162 // This is known to happen on new CLs or on recently created 163 // revisions. 164 return grpcutil.NotFoundTag.Apply(gerrit.ErrStaleData) 165 default: 166 return gerrit.UnhandledError(ctx, err, "Gerrit.SetReview") 167 } 168 } 169 170 func (op *triggerDepOp) makeSetReviewRequest(depCL *changelist.CL) *gerritpb.SetReviewRequest { 171 mode := op.trigger.GetMode() 172 return &gerritpb.SetReviewRequest{ 173 Project: depCL.Snapshot.GetGerrit().GetInfo().GetProject(), 174 Number: depCL.Snapshot.GetGerrit().GetInfo().GetNumber(), 175 RevisionId: "current", 176 Labels: map[string]int32{ 177 trigger.CQLabelName: trigger.CQVoteByMode(run.Mode(op.trigger.GetMode())), 178 }, 179 // The author will be notified by the run start message, anyways. 180 Notify: gerritpb.Notify_NOTIFY_NONE, 181 OnBehalfOf: op.trigger.GetGerritAccountId(), 182 Message: fmt.Sprintf( 183 "Triggering %s, because %s is triggered on %s, which depends on this CL", 184 mode, mode, op.originCLURL), 185 Tag: gerrit.Tag("trigger-dep-cl", ""), 186 } 187 } 188 189 func (op *triggerDepOp) vote(ctx context.Context, gFactory gerrit.Factory, luciPrj string, depCL *changelist.CL) error { 190 if op.result.voteDone { 191 return nil 192 } 193 gc, err := gFactory.MakeClient(ctx, depCL.Snapshot.GetGerrit().GetHost(), luciPrj) 194 if err != nil { 195 return errors.Annotate(err, "gFactory.MakeClient").Err() 196 } 197 voteReq := op.makeSetReviewRequest(depCL) 198 vErr := gFactory.MakeMirrorIterator(ctx).RetryIfStale(func(opt grpc.CallOption) error { 199 if op.isCanceled.Load() { 200 return nil 201 } 202 _, err := gc.SetReview(ctx, voteReq, opt) 203 op.result.lastErr = processGerritErr(ctx, err) 204 if op.result.lastErr != nil { 205 return op.result.lastErr 206 } 207 return nil 208 }) 209 op.result.voteDone = vErr == nil 210 return vErr 211 } 212 213 func (op *triggerDepOp) markOutdated(ctx context.Context, luciPrj string, clm *changelist.Mutator, clu clUpdater, depCL *changelist.CL) error { 214 var isOutdated bool 215 _, err := clm.Update(ctx, luciPrj, depCL.ID, func(cl *changelist.CL) error { 216 if cl.Snapshot == nil || cl.Snapshot.GetOutdated() != nil { 217 return changelist.ErrStopMutation // noop 218 } 219 isOutdated = true 220 cl.Snapshot.Outdated = &changelist.Snapshot_Outdated{} 221 return nil 222 }) 223 switch { 224 case err != nil: 225 return errors.Annotate(err, "CLMutator.Update").Err() 226 case isOutdated: 227 // TODO(crbug.com/1284393): use Gerrit's consistency-on-demand. 228 return clu.Schedule(ctx, &changelist.UpdateCLTask{ 229 LuciProject: luciPrj, 230 ExternalId: string(depCL.ExternalID), 231 Id: int64(depCL.ID), 232 Requester: changelist.UpdateCLTask_DEP_CL_TRIGGERER, 233 }) 234 default: 235 return nil 236 } 237 } 238 239 func findCQTriggerMode(cl *changelist.CL) string { 240 trs := trigger.Find(&trigger.FindInput{ 241 ChangeInfo: cl.Snapshot.GetGerrit().GetInfo(), 242 }) 243 if trs == nil { 244 return "" 245 } 246 return trs.CqVoteTrigger.GetMode() 247 }