go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/prjmanager/triager/deps.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 triager 16 17 import ( 18 "context" 19 "errors" 20 "fmt" 21 "time" 22 23 "go.chromium.org/luci/auth/identity" 24 "go.chromium.org/luci/common/logging" 25 26 cfgpb "go.chromium.org/luci/cv/api/config/v2" 27 "go.chromium.org/luci/cv/internal/changelist" 28 "go.chromium.org/luci/cv/internal/common" 29 "go.chromium.org/luci/cv/internal/prjmanager/prjpb" 30 "go.chromium.org/luci/cv/internal/run" 31 ) 32 33 // maxAllowedDeps limits how many non-submitted deps a CL may have for CV to 34 // consider it. 35 // 36 // This applies to both singular and combinable modes. 37 // See also https://crbug.com/1217100. 38 const maxAllowedDeps = 240 39 40 // triagedDeps categorizes deps of a CL, referred to below as the "dependent" 41 // CL. 42 // 43 // Categories are exclusive. Non-submitted OK deps are not recorded here to 44 // avoid unnecessary allocations in the most common case, but they do affect 45 // the lastTriggered time. 46 type triagedDeps struct { 47 // lastCQVoteTriggered among *all* deps which are triggered. Can be Zero 48 //time if no dep is triggered. 49 lastCQVoteTriggered time.Time 50 51 // submitted are already submitted deps watched by this project, though not 52 // necessarily the same config group as the dependent CL. These deps are OK. 53 submitted []*changelist.Dep 54 55 // notYetLoaded means that more specific category isn't yet known. 56 notYetLoaded []*changelist.Dep 57 58 // needToTrigger is a list of the deps that should be triggered with CQ 59 // votes. 60 needToTrigger []*changelist.Dep 61 62 invalidDeps *changelist.CLError_InvalidDeps 63 } 64 65 // triageDeps triages deps of a PCL. See triagedDeps for documentation. 66 func triageDeps(ctx context.Context, pcl *prjpb.PCL, cgIndex int32, pm pmState) *triagedDeps { 67 cg := pm.ConfigGroup(cgIndex).Content 68 res := &triagedDeps{} 69 for _, dep := range pcl.GetDeps() { 70 dPCL := pm.PCL(dep.GetClid()) 71 res.categorize(ctx, pcl, cgIndex, cg, dPCL, dep) 72 cqTrigger := dPCL.GetTriggers().GetCqVoteTrigger() 73 if cqTrigger != nil { 74 if tPB := cqTrigger.GetTime(); tPB != nil { 75 if t := tPB.AsTime(); res.lastCQVoteTriggered.IsZero() || res.lastCQVoteTriggered.Before(t) { 76 res.lastCQVoteTriggered = t 77 } 78 } 79 } 80 } 81 if okDeps := len(pcl.GetDeps()) - len(res.submitted); okDeps > maxAllowedDeps { 82 // Only declare this invalid if every non-submitted DEP is OK. 83 if res.invalidDeps == nil && len(res.notYetLoaded) == 0 { 84 res.ensureInvalidDeps() 85 res.invalidDeps.TooMany = &changelist.CLError_InvalidDeps_TooMany{ 86 Actual: int32(okDeps), 87 MaxAllowed: maxAllowedDeps, 88 } 89 } 90 } 91 return res 92 } 93 94 // OK is true if triagedDeps doesn't have any not-OK deps. 95 func (t *triagedDeps) OK() bool { 96 return t.invalidDeps == nil 97 } 98 99 func (t *triagedDeps) makePurgeReason() *changelist.CLError { 100 if t.OK() { 101 panic("makePurgeReason must be called only iff !OK") 102 } 103 return &changelist.CLError{Kind: &changelist.CLError_InvalidDeps_{InvalidDeps: t.invalidDeps}} 104 } 105 106 // categorize adds dep to the applicable slice (if any). 107 // 108 // pcl is dependent PCL, which must be triggered. 109 // Its dep is represented by dPCL. 110 func (t *triagedDeps) categorize(ctx context.Context, pcl *prjpb.PCL, cgIndex int32, cg *cfgpb.ConfigGroup, dPCL *prjpb.PCL, dep *changelist.Dep) { 111 if dPCL == nil { 112 t.notYetLoaded = append(t.notYetLoaded, dep) 113 return 114 } 115 116 switch s := dPCL.GetStatus(); s { 117 case prjpb.PCL_UNKNOWN: 118 t.notYetLoaded = append(t.notYetLoaded, dep) 119 return 120 121 case prjpb.PCL_UNWATCHED, prjpb.PCL_DELETED: 122 // PCL deleted from Datastore should not happen outside of project 123 // re-enablement, so it's OK to treat the same as PCL_UNWATCHED for 124 // simplicity. 125 t.ensureInvalidDeps() 126 t.invalidDeps.Unwatched = append(t.invalidDeps.Unwatched, dep) 127 return 128 129 case prjpb.PCL_OK: 130 // Happy path; continue after the switch. 131 default: 132 panic(fmt.Errorf("unrecognized CL %d dep %d status %s", pcl.GetClid(), dPCL.GetClid(), s)) 133 } 134 // CL is watched by this LUCI project. 135 136 if dPCL.GetSubmitted() { 137 // Submitted CL may no longer be in the expected ConfigGroup, 138 // but since it's in the same project, it's OK to refer to it as it 139 // doesn't create an information leak. 140 t.submitted = append(t.submitted, dep) 141 return 142 } 143 144 switch cgIndexes := dPCL.GetConfigGroupIndexes(); len(cgIndexes) { 145 case 0: 146 panic(fmt.Errorf("at least one ConfigGroup index required for watched dep PCL %d", dPCL.GetClid())) 147 case 1: 148 if cgIndexes[0] != cgIndex { 149 t.ensureInvalidDeps() 150 t.invalidDeps.WrongConfigGroup = append(t.invalidDeps.WrongConfigGroup, dep) 151 return 152 } 153 // Happy path; continue after the switch. 154 default: 155 // Strictly speaking, it may be OK iff dependentCGIndex is matched among 156 // other config groups. However, there is no compelling use-case for 157 // depending on a CL which matches several config groups. So, for 158 // compatibility with CQDaemon, be strict. 159 t.ensureInvalidDeps() 160 t.invalidDeps.WrongConfigGroup = append(t.invalidDeps.WrongConfigGroup, dep) 161 return 162 } 163 164 tr := pcl.GetTriggers().GetCqVoteTrigger() 165 dtr := dPCL.GetTriggers().GetCqVoteTrigger() 166 if cg.GetCombineCls() == nil { 167 t.categorizeSingle(ctx, tr, dtr, dep, cg) 168 } else { 169 t.categorizeCombinable(tr, dtr, dep) 170 } 171 } 172 173 func (t *triagedDeps) categorizeCombinable(tr, dtr *run.Trigger, dep *changelist.Dep) { 174 // During the `combine_cls.stabilization_delay` since the last triggered CL 175 // in a group, a user can change their mind. Since the full group of CLs 176 // isn't known here, categorization decision may or may not be final. 177 switch { 178 case dtr.GetMode() == tr.GetMode(): 179 // Happy path. 180 return 181 case dtr == nil: 182 t.ensureInvalidDeps() 183 t.invalidDeps.CombinableUntriggered = append(t.invalidDeps.CombinableUntriggered, dep) 184 return 185 default: 186 // TODO(tandrii): support dry run on dependent and full Run on dep. 187 // For example, on a CL stack: 188 // CL | Mode 189 // D CQ+1 190 // C CQ+1 191 // B CQ+2 192 // A CQ+2 193 // (base) - 194 // D+C+B+A are can be dry-run-ed and B+A can be CQ+2ed at the same time 195 t.ensureInvalidDeps() 196 t.invalidDeps.CombinableMismatchedMode = append(t.invalidDeps.CombinableMismatchedMode, dep) 197 return 198 } 199 } 200 201 func (t *triagedDeps) categorizeSingle(ctx context.Context, tr, dtr *run.Trigger, dep *changelist.Dep, cg *cfgpb.ConfigGroup) { 202 // TODO(crbug/1470341) once the dogfood process is done, 203 // enable chained cq votes by default, and remove all the unnecessary 204 // param "ctx". 205 var isMCEDogfooder bool 206 switch triggerer, err := identity.MakeIdentity(fmt.Sprintf("%s:%s", identity.User, tr.Email)); { 207 case err != nil: 208 // Log the error w/o handling it. Chained CQ votes will be turned on 209 // by default. 210 logging.Errorf(ctx, "categorizeSingle: MakeIdentity: %s", err) 211 default: 212 isMCEDogfooder = common.IsMCEDogfooder(ctx, triggerer) 213 } 214 // dependent is guaranteed non-nil. 215 switch mode := run.Mode(tr.GetMode()); { 216 case mode == run.FullRun && isMCEDogfooder && dep.GetKind() == changelist.DepKind_HARD: 217 // If a dep has no or different (prob CQ+1) CQ vote, then schedule 218 // a trigger for CQ+2 on the dep, and postpone a run creation for 219 // this CL. 220 if tr.GetMode() != dtr.GetMode() { 221 t.needToTrigger = append(t.needToTrigger, dep) 222 return 223 } 224 case mode == run.FullRun: 225 if cg.GetVerifiers().GetGerritCqAbility().GetAllowSubmitWithOpenDeps() && dep.GetKind() == changelist.DepKind_HARD { 226 // If configured, allow CV to submit the entire stack (HARD deps 227 // only) of changes. 228 return 229 } 230 t.ensureInvalidDeps() 231 t.invalidDeps.SingleFullDeps = append(t.invalidDeps.SingleFullDeps, dep) 232 } 233 } 234 235 // ensureInvalidDeps initializes if necessary and returns .invalidDeps. 236 func (t *triagedDeps) ensureInvalidDeps() *changelist.CLError_InvalidDeps { 237 if t.invalidDeps == nil { 238 t.invalidDeps = &changelist.CLError_InvalidDeps{} 239 } 240 return t.invalidDeps 241 } 242 243 // iterateNotSubmitted calls clbk per each dep which isn't submitted. 244 // 245 // Must be called with the same PCL as was used to construct the triagedDeps. 246 func (t *triagedDeps) iterateNotSubmitted(pcl *prjpb.PCL, clbk func(dep *changelist.Dep)) { 247 // Because construction of triagedDeps is in order of PCL's Deps, the 248 // submitted must be a sub-sequence of Deps and we can compare just Dep 249 // pointers. 250 all, subs := pcl.GetDeps(), t.submitted 251 for { 252 switch { 253 case len(subs) == 0: 254 for _, dep := range all { 255 clbk(dep) 256 } 257 return 258 case len(all) == 0: 259 panic(errors.New("must not happen because submitted must be a subset of all deps (wrong PCL?)")) 260 default: 261 if all[0] == subs[0] { 262 subs = subs[1:] 263 } else { 264 clbk(all[0]) 265 } 266 all = all[1:] 267 } 268 } 269 }