go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/prjmanager/state/evalcl.go (about) 1 // Copyright 2020 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 state 16 17 import ( 18 "context" 19 "fmt" 20 "strings" 21 22 "go.chromium.org/luci/common/data/stringset" 23 "go.chromium.org/luci/common/errors" 24 "go.chromium.org/luci/common/logging" 25 gerritpb "go.chromium.org/luci/common/proto/gerrit" 26 "go.chromium.org/luci/common/retry/transient" 27 "go.chromium.org/luci/gae/service/datastore" 28 "google.golang.org/protobuf/proto" 29 30 cfgpb "go.chromium.org/luci/cv/api/config/v2" 31 "go.chromium.org/luci/cv/internal/changelist" 32 "go.chromium.org/luci/cv/internal/common" 33 "go.chromium.org/luci/cv/internal/configs/prjcfg" 34 "go.chromium.org/luci/cv/internal/gerrit/cfgmatcher" 35 "go.chromium.org/luci/cv/internal/gerrit/trigger" 36 "go.chromium.org/luci/cv/internal/prjmanager/prjpb" 37 "go.chromium.org/luci/cv/internal/run" 38 ) 39 40 // reevalPCLs re-evaluates PCLs after a project config change. 41 // 42 // If any are changed, marks PB.RepartitionRequired. 43 func (s *State) reevalPCLs(ctx context.Context) error { 44 cls, errs, err := s.loadCLsForPCLs(ctx) 45 if err != nil { 46 return err 47 } 48 newPCLs := make([]*prjpb.PCL, len(cls)) 49 mutated := false 50 for i, cl := range cls { 51 old := s.PB.GetPcls()[i] 52 switch pcl, err := s.makePCLFromDS(ctx, cl, errs[i], old); { 53 case err != nil: 54 return err 55 case pcl == nil: 56 panic("makePCLFromDS is wrong") 57 case pcl != old: 58 mutated = true 59 fallthrough 60 default: 61 newPCLs[i] = pcl 62 } 63 } 64 if mutated { 65 s.PB.Pcls = newPCLs 66 s.PB.RepartitionRequired = true 67 s.pclIndex = nil 68 } 69 return nil 70 } 71 72 // evalUpdatedCLs updates/inserts PCLs, if the PCL doesn't exist or with 73 // an older eversion than the given eversion. 74 func (s *State) evalUpdatedCLs(ctx context.Context, clEVersions map[int64]int64) error { 75 cls := make([]*changelist.CL, 0, len(clEVersions)) 76 // Avoid doing anything in cases where all CL updates sent due to recent full 77 // poll iff we already know about each CL based on its EVersion. 78 for clid, ev := range clEVersions { 79 switch pcl := s.PB.GetPCL(clid); { 80 case pcl != nil && ev <= pcl.GetEversion(): 81 default: 82 cls = append(cls, &changelist.CL{ID: common.CLID(clid)}) 83 } 84 } 85 if len(cls) == 0 { 86 return nil 87 } 88 return s.evalCLsFromDS(ctx, cls) 89 } 90 91 func (s *State) evalCLs(ctx context.Context, clids []int64) error { 92 cls := make([]*changelist.CL, len(clids)) 93 for i, clid := range clids { 94 cls[i] = &changelist.CL{ID: common.CLID(clid)} 95 } 96 return s.evalCLsFromDS(ctx, cls) 97 } 98 99 // evalCLsFromDS adds, updates, and marks for deletion PCLs based on CLs in 100 // Datastore. 101 // 102 // Sorts passed cls slice and updates it with loaded from DS info. 103 func (s *State) evalCLsFromDS(ctx context.Context, cls []*changelist.CL) error { 104 if s.cfgMatcher == nil { 105 meta, err := prjcfg.GetHashMeta(ctx, s.PB.GetLuciProject(), s.PB.GetConfigHash()) 106 if err != nil { 107 return err 108 } 109 if s.configGroups, err = meta.GetConfigGroups(ctx); err != nil { 110 return err 111 } 112 s.cfgMatcher = cfgmatcher.LoadMatcherFromConfigGroups(ctx, s.configGroups, &meta) 113 } 114 115 // Sort new/updated CLs in the way as PCLs already are, namely by CL ID. Do it 116 // before loading from Datastore because `errs` must correspond to `cls`. 117 changelist.Sort(cls) 118 cls, errs, err := loadCLs(ctx, cls) 119 if err != nil { 120 return err 121 } 122 123 // Now, while copying old PCLs slice into new PCLs slice, 124 // insert new PCL objects for new CLs and replace PCL objects for updated CLs. 125 // This preserves the property of PCLs being sorted. 126 // Since we have to re-create PCLs slice anyways, it's fastest to merge two 127 // sorted in the same way PCLs and CLs slices in O(len(PCLs) + len(cls)). 128 oldPCLs := s.PB.GetPcls() 129 newPCLs := make([]*prjpb.PCL, 0, len(oldPCLs)+len(cls)) 130 changed := common.CLIDsSet{} 131 for i, cl := range cls { 132 // Copy all old PCLs before this CL. 133 for len(oldPCLs) > 0 && common.CLID(oldPCLs[0].GetClid()) < cl.ID { 134 newPCLs = append(newPCLs, oldPCLs[0]) 135 oldPCLs = oldPCLs[1:] 136 } 137 // If CL is updated, pop old. 138 var old *prjpb.PCL 139 if len(oldPCLs) > 0 && common.CLID(oldPCLs[0].GetClid()) == cl.ID { 140 old = oldPCLs[0] 141 oldPCLs = oldPCLs[1:] 142 } 143 // Compute new PCL. 144 switch pcl, err := s.makePCLFromDS(ctx, cl, errs[i], old); { 145 case err != nil: 146 return err 147 case pcl == nil && old != nil: 148 panic(fmt.Errorf("makePCLFromDS is wrong")) 149 case pcl == nil: 150 // New CL, but not in datastore. Don't add anything to newPCLs. 151 // This weird case was logged by makePCLFromDS already. 152 case pcl != old: 153 changed.Add(cl.ID) 154 fallthrough 155 default: 156 newPCLs = append(newPCLs, pcl) 157 } 158 } 159 if len(changed) == 0 { 160 return nil 161 } 162 // Copy remaining oldPCLs. 163 for len(oldPCLs) > 0 { 164 newPCLs = append(newPCLs, oldPCLs[0]) 165 oldPCLs = oldPCLs[1:] 166 } 167 s.PB.Pcls = newPCLs 168 s.PB.RepartitionRequired = true 169 s.PB.Components = markForTriageOnChangedPCLs(s.PB.GetComponents(), s.PB.GetPcls(), changed) 170 s.pclIndex = nil 171 return nil 172 } 173 174 func (s *State) makePCLFromDS(ctx context.Context, cl *changelist.CL, err error, old *prjpb.PCL) (*prjpb.PCL, error) { 175 switch { 176 case err == datastore.ErrNoSuchEntity: 177 oldEversion := int64(0) 178 if old == nil { 179 logging.Errorf(ctx, "New CL %d not in Datastore", cl.ID) 180 } else { 181 // Must not happen outside of extremely rare un-deletion of a project 182 // whose PM state references long ago wiped out CLs. 183 logging.Errorf(ctx, "Old CL %d no longer in Datastore", cl.ID) 184 oldEversion = old.GetEversion() 185 } 186 return &prjpb.PCL{ 187 Clid: int64(cl.ID), 188 Eversion: oldEversion, 189 Status: prjpb.PCL_DELETED, 190 }, nil 191 case err != nil: 192 return nil, errors.Annotate(err, "failed to load CL %d", cl.ID).Tag(transient.Tag).Err() 193 default: 194 pcl := s.makePCL(ctx, cl) 195 if proto.Equal(pcl, old) { 196 return old, nil 197 } 198 return pcl, nil 199 } 200 } 201 202 // makePCL creates new PCL based on Datastore CL entity and current config. 203 func (s *State) makePCL(ctx context.Context, cl *changelist.CL) *prjpb.PCL { 204 if s.cfgMatcher == nil { 205 panic("cfgMather must be initialized") 206 } 207 pcl := &prjpb.PCL{ 208 Clid: int64(cl.ID), 209 Eversion: int64(cl.EVersion), 210 Status: prjpb.PCL_UNKNOWN, 211 } 212 213 var ap *changelist.ApplicableConfig_Project 214 switch kind, reason := cl.AccessKindWithReason(ctx, s.PB.GetLuciProject()); kind { 215 case changelist.AccessUnknown: 216 // Need more time to fetch this. 217 logging.Debugf(ctx, "CL %d %s %s", cl.ID, cl.ExternalID, reason) 218 return pcl 219 case changelist.AccessDeniedProbably: 220 // PM should not create a new Run in such cases, but PM won't terminate 221 // existing Run when Run Manager can and should do it on its own. 222 fallthrough 223 case changelist.AccessDenied: 224 logging.Infof(ctx, "This project has no access to CL(%d %s): %s", cl.ID, cl.ExternalID, reason) 225 var watchedByMultiple bool 226 ap, watchedByMultiple = cl.IsWatchedByThisAndOtherProjects(s.PB.GetLuciProject()) 227 if !watchedByMultiple { 228 pcl.Status = prjpb.PCL_UNWATCHED 229 return pcl 230 } 231 // Special case if the CL is watched by more than one project. 232 err := newMultiProjectWatchError(cl) 233 pcl.PurgeReasons = append(pcl.PurgeReasons, 234 &prjpb.PurgeReason{ 235 ClError: err, 236 ApplyTo: &prjpb.PurgeReason_AllActiveTriggers{AllActiveTriggers: true}, 237 }) 238 pcl.Errors = append(pcl.Errors, err) 239 pcl.Status = prjpb.PCL_OK 240 case changelist.AccessGranted: 241 switch { 242 case cl.Snapshot.GetOutdated() != nil: 243 // Need more time to fetch this. 244 logging.Debugf(ctx, "CL %d %s Snapshot is outdated", cl.ID, cl.ExternalID) 245 pcl.Outdated = cl.Snapshot.GetOutdated() 246 return pcl 247 case cl.Snapshot.GetGerrit() == nil: 248 panic(fmt.Errorf("only Gerrit CLs supported for now")) 249 case len(cl.ApplicableConfig.GetProjects()) != 1: 250 panic(fmt.Errorf("AccessGranted but %d projects in ApplicableConfig", len(cl.ApplicableConfig.GetProjects()))) 251 } 252 ap = cl.ApplicableConfig.GetProjects()[0] 253 pcl.Status = prjpb.PCL_OK 254 default: 255 panic(fmt.Errorf("unknown access kind %d", kind)) 256 } 257 258 s.setApplicableConfigGroups(ap, cl.Snapshot, pcl) 259 if errs := cl.Snapshot.GetErrors(); len(errs) > 0 { 260 for _, err := range errs { 261 pcl.PurgeReasons = append(pcl.PurgeReasons, &prjpb.PurgeReason{ 262 ClError: err, 263 ApplyTo: &prjpb.PurgeReason_AllActiveTriggers{AllActiveTriggers: true}, 264 }) 265 pcl.Errors = append(pcl.Errors, err) 266 } 267 } 268 269 pcl.Outdated = cl.Snapshot.GetOutdated() 270 pcl.Deps = cl.Snapshot.GetDeps() 271 for _, d := range pcl.GetDeps() { 272 if d.GetClid() == pcl.GetClid() { 273 if d.GetKind() != changelist.DepKind_SOFT { 274 logging.Errorf(ctx, "BUG: self-referential %s dep: CL %d with Snapshot\n%s", d.GetKind(), cl.ID, cl.Snapshot) 275 // If this actually happens, better to proceed with bad error message to 276 // the user than crash later while processing the CL. 277 } 278 // TODO(robertocn): allow new patchset runs to continue even in the 279 // case of this error, since new patchset runs should not be subject 280 // to dependencies. 281 err := &changelist.CLError{ 282 Kind: &changelist.CLError_SelfCqDepend{SelfCqDepend: true}, 283 } 284 pcl.PurgeReasons = append(pcl.PurgeReasons, &prjpb.PurgeReason{ 285 ClError: err, 286 ApplyTo: &prjpb.PurgeReason_AllActiveTriggers{AllActiveTriggers: true}, 287 }) 288 pcl.Errors = append(pcl.Errors, err) 289 } 290 } 291 292 ci := cl.Snapshot.GetGerrit().GetInfo() 293 if ci.GetStatus() == gerritpb.ChangeStatus_MERGED { 294 pcl.Submitted = true 295 return pcl 296 } 297 298 if ci.GetOwner().GetEmail() == "" { 299 err := &changelist.CLError{ 300 Kind: &changelist.CLError_OwnerLacksEmail{OwnerLacksEmail: true}, 301 } 302 pcl.PurgeReasons = append(pcl.PurgeReasons, &prjpb.PurgeReason{ 303 ClError: err, 304 ApplyTo: &prjpb.PurgeReason_AllActiveTriggers{AllActiveTriggers: true}, 305 }) 306 pcl.Errors = append(pcl.Errors, err) 307 } 308 309 s.setTriggers(ci, pcl, cl.TriggerNewPatchsetRunAfterPS) 310 311 // Check for "Commit: false" footer after setting Trigger, because this should 312 // only have an effect in the case of an attempted full run. 313 if hasCommitFalseFlag(cl.Snapshot.GetMetadata()) && pcl.GetTriggers().GetCqVoteTrigger().GetMode() == string(run.FullRun) { 314 err := &changelist.CLError{ 315 Kind: &changelist.CLError_CommitBlocked{CommitBlocked: true}, 316 } 317 pcl.PurgeReasons = append(pcl.PurgeReasons, &prjpb.PurgeReason{ 318 ClError: err, 319 ApplyTo: &prjpb.PurgeReason_Triggers{ 320 Triggers: &run.Triggers{ 321 CqVoteTrigger: pcl.GetTriggers().GetCqVoteTrigger(), 322 }, 323 }, 324 }) 325 pcl.Errors = append(pcl.Errors, err) 326 } 327 return pcl 328 } 329 330 // setApplicableConfigGroups sets ConfigGroup indexes of PCL. 331 // 332 // If provided ApplicableConfig_Project is up to date, uses config groups from 333 // it. Otherwise, matches against project config directly using s.cfgMatcher. 334 // 335 // Expects s.cfgMatcher to be set. 336 // 337 // Modifies the passed PCL. 338 func (s *State) setApplicableConfigGroups(ap *changelist.ApplicableConfig_Project, snapshot *changelist.Snapshot, pcl *prjpb.PCL) { 339 // Most likely, ApplicableConfig stored in a CL entity is still up-to-date. 340 if upToDate := s.tryUsingApplicableConfigGroups(ap, pcl); upToDate { 341 return 342 } 343 // Project's config has been updated after CL snapshot was made. 344 g := snapshot.GetGerrit() 345 ci := g.GetInfo() 346 for _, id := range s.cfgMatcher.Match(g.GetHost(), ci.GetProject(), ci.GetRef()) { 347 index := s.indexOfConfigGroup(prjcfg.ConfigGroupID(id)) 348 pcl.ConfigGroupIndexes = append(pcl.ConfigGroupIndexes, index) 349 } 350 } 351 352 // tryUsingApplicableConfigGroups sets ConfigGroup indexes of a PCL based on 353 // ApplicableConfig_Project if ApplicableConfig_Project references the State's 354 // config hash. 355 // 356 // Modifies the passed PCL. 357 // Returns whether config hash matched. 358 func (s *State) tryUsingApplicableConfigGroups(ap *changelist.ApplicableConfig_Project, pcl *prjpb.PCL) bool { 359 expectedConfigHash := s.PB.GetConfigHash() 360 // At least 1 ID is guaranteed in ApplicableConfig_Project by gerrit.gobmap. 361 for _, id := range ap.GetConfigGroupIds() { 362 if prjcfg.ConfigGroupID(id).Hash() != expectedConfigHash { 363 return false 364 } 365 } 366 for _, id := range ap.GetConfigGroupIds() { 367 index := s.indexOfConfigGroup(prjcfg.ConfigGroupID(id)) 368 pcl.ConfigGroupIndexes = append(pcl.ConfigGroupIndexes, index) 369 } 370 return true 371 } 372 373 // setTriggers populates a PCL's .Triggers field with the triggers present in 374 // the given ChangeInfo. 375 // 376 // It also validates that the trigger mode is allowed, and strips the 377 // information from the triggerer. 378 func (s *State) setTriggers(ci *gerritpb.ChangeInfo, pcl *prjpb.PCL, latestPSRun int32) { 379 // Triggers are a function of a CL and applicable ConfigGroup, which may 380 // define additional modes. 381 // In case of misconfiguration, there may be 0 or 2+ applicable 382 // ConfigGroups, in which case we use empty ConfigGroup{}. 383 // This doesn't matter much, since such CLs will be soon purged. 384 // In the very worst case, CL purger will remove just the CQ vote and not 385 // the additional label's vote defined in actually intended ConfigGroup, 386 // which isn't a big deal. 387 var cg *cfgpb.ConfigGroup 388 if idxs := pcl.GetConfigGroupIndexes(); len(idxs) == 1 { 389 cg = s.configGroups[idxs[0]].Content 390 } else { 391 cg = &cfgpb.ConfigGroup{} 392 } 393 ts := trigger.Find(&trigger.FindInput{ChangeInfo: ci, ConfigGroup: cg, TriggerNewPatchsetRunAfterPS: latestPSRun}) 394 if ts == nil { 395 return 396 } 397 398 allowedRunModes := stringset.NewFromSlice( 399 "", 400 string(run.DryRun), 401 string(run.FullRun), 402 string(run.NewPatchsetRun), 403 ) 404 for _, am := range cg.GetAdditionalModes() { 405 allowedRunModes.Add(am.GetName()) 406 } 407 408 for _, modeString := range []string{ts.GetCqVoteTrigger().GetMode(), ts.GetNewPatchsetRunTrigger().GetMode()} { 409 if !allowedRunModes.Has(modeString) { 410 err := &changelist.CLError{ 411 Kind: &changelist.CLError_UnsupportedMode{UnsupportedMode: string(modeString)}, 412 } 413 pcl.PurgeReasons = append(pcl.PurgeReasons, &prjpb.PurgeReason{ 414 ClError: err, 415 ApplyTo: &prjpb.PurgeReason_Triggers{ 416 Triggers: ts, 417 }, 418 }) 419 pcl.Errors = append(pcl.Errors, err) 420 return 421 } 422 } 423 424 pcl.Triggers = ts 425 } 426 427 // loadCLsForPCLs loads CLs from Datastore corresponding to PCLs. 428 // 429 // Returns slice of CLs, error.MultiError slice corresponding to 430 // per-CL errors *(always exists and has same length as CLs)*, and a 431 // top level error if it can't be attributed to any CL. 432 // 433 // *each error in per-CL errors* is not annotated and is nil if CL was loaded 434 // successfully. 435 func (s *State) loadCLsForPCLs(ctx context.Context) ([]*changelist.CL, errors.MultiError, error) { 436 cls := make([]*changelist.CL, len(s.PB.GetPcls())) 437 for i, pcl := range s.PB.GetPcls() { 438 cls[i] = &changelist.CL{ID: common.CLID(pcl.GetClid())} 439 } 440 return loadCLs(ctx, cls) 441 } 442 443 func loadCLs(ctx context.Context, cls []*changelist.CL) ([]*changelist.CL, errors.MultiError, error) { 444 // At 0.007 KiB (serialized) per CL as of Jan 2021, this should scale 2000 CLs 445 // with reasonable RAM footprint and well within 10s because 446 // datastore.GetMulti splits it into concurrently queried batches. 447 // To support more, CLs would need to be loaded and processed in batches, 448 // or CL snapshot size reduced. 449 err := datastore.Get(ctx, cls) 450 switch merr, ok := err.(errors.MultiError); { 451 case err == nil: 452 return cls, make(errors.MultiError, len(cls)), nil 453 case ok: 454 return cls, merr, nil 455 default: 456 return nil, nil, errors.Annotate(err, "failed to load %d CLs", len(cls)).Tag(transient.Tag).Err() 457 } 458 } 459 460 func newMultiProjectWatchError(cl *changelist.CL) *changelist.CLError { 461 projects := make([]string, len(cl.ApplicableConfig.GetProjects())) 462 for i, p := range cl.ApplicableConfig.GetProjects() { 463 projects[i] = p.GetName() 464 } 465 return &changelist.CLError{ 466 Kind: &changelist.CLError_WatchedByManyProjects_{ 467 WatchedByManyProjects: &changelist.CLError_WatchedByManyProjects{ 468 Projects: projects, 469 }, 470 }, 471 } 472 } 473 474 func hasCommitFalseFlag(metadata []*changelist.StringPair) bool { 475 for _, p := range metadata { 476 // The values stored in the CL Snapshot Metadata are not necessarily normalized, 477 // and could have come from "Commit: false", "COMMIT=FALSE" or some other style. 478 // Other possible values like "Commit: no" are not recognized. 479 if strings.ToLower(p.Key) == "commit" && strings.ToLower(p.Value) == "false" { 480 return true 481 } 482 } 483 return false 484 }