go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/run/impl/handler/cl_update.go (about) 1 // Copyright 2021 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 handler 16 17 import ( 18 "context" 19 "fmt" 20 "sort" 21 "time" 22 23 "go.chromium.org/luci/common/clock" 24 "go.chromium.org/luci/common/errors" 25 "go.chromium.org/luci/common/logging" 26 27 "go.chromium.org/luci/cv/internal/changelist" 28 "go.chromium.org/luci/cv/internal/common" 29 "go.chromium.org/luci/cv/internal/configs/prjcfg" 30 "go.chromium.org/luci/cv/internal/gerrit/trigger" 31 "go.chromium.org/luci/cv/internal/run" 32 "go.chromium.org/luci/cv/internal/run/impl/state" 33 ) 34 35 // OnCLsUpdated implements Handler interface. 36 func (impl *Impl) OnCLsUpdated(ctx context.Context, rs *state.RunState, clids common.CLIDs) (*Result, error) { 37 switch status := rs.Status; { 38 case status == run.Status_STATUS_UNSPECIFIED: 39 err := errors.Reason("CRITICAL: Received CLUpdated events but Run is in unspecified status").Err() 40 common.LogError(ctx, err) 41 panic(err) 42 case status == run.Status_SUBMITTING: 43 return &Result{State: rs, PreserveEvents: true}, nil 44 case isCurrentlyResettingTriggers(rs): 45 // It's likely CL is updated when resetting the triggers, defer the process 46 // of CLsUpdated event till resetting the trigger is done. 47 return &Result{State: rs, PreserveEvents: true}, nil 48 case run.IsEnded(status): 49 logging.Debugf(ctx, "skipping OnCLUpdated because Run is %s", status) 50 return &Result{State: rs}, nil 51 } 52 clids.Dedupe() 53 54 cg, runCLs, cls, err := loadCLsAndConfig(ctx, rs, clids) 55 if err != nil { 56 return nil, err 57 } 58 59 hasNilSnapshot := false 60 var earliestReconsiderAt time.Time 61 clsCausingCancellation := make(common.CLIDsSet, len(clids)) 62 cancellationReasons := make([]string, 0, len(clids)) 63 for i, clid := range clids { 64 cl, runCL := cls[i], runCLs[i] 65 if cl.Snapshot == nil { 66 // This doesn't necessarily assume that shouldCancel() would 67 // or would not return a cancelReason for nil Snapshot; hence, 68 // let it decide. hasNilSnapshot is to decide whether 69 // runs.CheckRunCreate() should be checked or not. 70 hasNilSnapshot = true 71 } 72 switch reconsiderAt, cancellationReason := shouldCancel(ctx, cl, runCL, cg); { 73 case !reconsiderAt.IsZero(): 74 if earliestReconsiderAt.IsZero() || earliestReconsiderAt.After(reconsiderAt) { 75 earliestReconsiderAt = reconsiderAt 76 } 77 case cancellationReason != "": 78 clsCausingCancellation.Add(clid) 79 cancellationReasons = append(cancellationReasons, cancellationReason) 80 } 81 } 82 83 sort.Strings(cancellationReasons) 84 switch numCLsCausingCancellation := len(clsCausingCancellation); { 85 // If all CLs in this Run have been updated at the same time and cause 86 // Run cancellation, directly cancel the Run. Otherwise, if part of the 87 // CLs cause Run cancellation, LUCI CV would need to reset the trigger on 88 // the rest of the CLs in this Run so that the existing trigger won't 89 // end up with creating new Run that developers are not intended. For example 90 // imagine a stack of CL A, B, C where C depends on B depends on A and a 91 // Dry Run is currently running involving all CLs. If B gains a new patchset, 92 // CV needs to reset the trigger on A and C and then patiently wait for 93 // developers creating a new Run with all 3 CLs. 94 case numCLsCausingCancellation == len(rs.CLs): 95 return impl.Cancel(ctx, rs, cancellationReasons) 96 case rs.HasRootCL() && clsCausingCancellation.Has(rs.RootCL): 97 // if the root cl has caused the cancellation, there's no need to reset 98 // the trigger for all other CLs because they don't have trigger at all. 99 // Directly cancelling the run would be enough. 100 return impl.Cancel(ctx, rs, cancellationReasons) 101 case numCLsCausingCancellation > 0: 102 rs = rs.ShallowCopy() 103 meta := reviewInputMeta{ 104 // Just pick the very first reason as the reason to reset the trigger. 105 // In typical cases, there will only be one cancellation reason anyway. 106 message: fmt.Sprintf("Reset the trigger of this CL because %s", cancellationReasons[0]), 107 notify: rs.Mode.GerritNotifyTargets(), 108 } 109 metas := make(map[common.CLID]reviewInputMeta, len(rs.CLs)-numCLsCausingCancellation) 110 if rs.HasRootCL() { 111 metas[rs.RootCL] = meta 112 } else { 113 for _, clid := range rs.CLs { 114 if !clsCausingCancellation.Has(clid) { 115 metas[clid] = meta 116 } 117 } 118 } 119 scheduleTriggersReset(ctx, rs, metas, run.Status_CANCELLED) 120 // TODO(yiwzhang): It is unfortunate that CV has to set the cancellation 121 // reason before triggers are reset and status is turned to CANCELLED. Find 122 // a way to pass the cancellation reasons to `onCompletedResetTriggers`. 123 rs.CancellationReasons = append(rs.CancellationReasons, cancellationReasons...) 124 sort.Strings(rs.CancellationReasons) 125 return &Result{State: rs}, nil 126 case !earliestReconsiderAt.IsZero(): 127 logging.Debugf(ctx, "Will reconsider OnCLUpdated event(s) after %s", earliestReconsiderAt.Sub(clock.Now(ctx))) 128 if err := impl.RM.Invoke(ctx, rs.ID, earliestReconsiderAt); err != nil { 129 return nil, err 130 } 131 return &Result{State: rs, PreserveEvents: true}, nil 132 } 133 134 // If any of the CLs has a nil snapshot, skip acls.CheckRunCreate(). 135 // It needs snapshots for the entire CL set. 136 if hasNilSnapshot { 137 return &Result{State: rs}, nil 138 } 139 140 // Check the Run creation, in case the Run is no longer valid 141 // with the newly updated CL info. 142 rs = rs.ShallowCopy() 143 allRunCLs, allCLs := runCLs, cls 144 remainingCLIDs := rs.CLs.Set() 145 remainingCLIDs.DelAll(clids) 146 if len(remainingCLIDs) > 0 { 147 remainingRunCLs, remainingCLs, err := loadRunCLsAndCLs(ctx, rs.ID, remainingCLIDs.ToCLIDs()) 148 if err != nil { 149 return nil, err 150 } 151 allRunCLs = append(allRunCLs, remainingRunCLs...) 152 allCLs = append(allCLs, remainingCLs...) 153 } 154 if _, err := checkRunCreate(ctx, rs, cg, allRunCLs, allCLs); err != nil { 155 return nil, err 156 } 157 return &Result{State: rs}, nil 158 } 159 160 func shouldCancel(ctx context.Context, cl *changelist.CL, rcl *run.RunCL, cg *prjcfg.ConfigGroup) (time.Time, string) { 161 project := cg.ProjectString() 162 clString := fmt.Sprintf("CL %d %s", cl.ID, cl.ExternalID) 163 switch kind, reason := cl.AccessKindWithReason(ctx, project); kind { 164 case changelist.AccessDenied: 165 logging.Warningf(ctx, "No longer have access to %s: %s", clString, reason) 166 return time.Time{}, fmt.Sprintf("no longer have access to %s: %s", cl.ExternalID.MustURL(), reason) 167 case changelist.AccessDeniedProbably: 168 logging.Warningf(ctx, "Probably no longer have access to %s (%s), not canceling yet", clString, reason) 169 // Keep the run Running for now. The access should become either 170 // AccessGranted or AccessDenied, eventually. 171 return cl.Access.GetByProject()[project].GetNoAccessTime().AsTime(), "" 172 case changelist.AccessUnknown: 173 logging.Errorf(ctx, "Unknown access to %s (%s), not canceling yet", clString, reason) 174 // Keep the run Running for now, it should become clear eventually. 175 return time.Time{}, "" 176 case changelist.AccessGranted: 177 // The expected and most likely case. 178 default: 179 panic(fmt.Errorf("unknown AccessKind %d in %s", kind, clString)) 180 } 181 182 if o, c := rcl.Detail.GetPatchset(), cl.Snapshot.GetPatchset(); o != c { 183 logging.Infof(ctx, "%s has new patchset %d => %d", clString, o, c) 184 return time.Time{}, fmt.Sprintf("the patchset of %s has changed from %d to %d", cl.ExternalID.MustURL(), o, c) 185 } 186 if o, c := rcl.Detail.GetGerrit().GetInfo().GetRef(), cl.Snapshot.GetGerrit().GetInfo().GetRef(); o != c { 187 logging.Warningf(ctx, "%s has new ref %q => %q", clString, o, c) 188 return time.Time{}, fmt.Sprintf("the ref of %s has moved from %s to %s", cl.ExternalID.MustURL(), o, c) 189 } 190 if rcl.Trigger != nil { 191 o, c := rcl.Trigger, trigger.Find(&trigger.FindInput{ 192 ChangeInfo: cl.Snapshot.GetGerrit().GetInfo(), 193 ConfigGroup: cg.Content, 194 TriggerNewPatchsetRunAfterPS: cl.TriggerNewPatchsetRunAfterPS, 195 }) 196 if whatChanged := run.HasTriggerChanged(o, c, cl.ExternalID.MustURL()); whatChanged != "" { 197 logging.Infof(ctx, "%s has new trigger\nOLD: %s\nNEW: %s", clString, o, c) 198 return time.Time{}, whatChanged 199 } 200 } 201 return time.Time{}, "" 202 }