go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/prjmanager/state/runs.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 state 16 17 import ( 18 "context" 19 20 "go.chromium.org/luci/common/errors" 21 "go.chromium.org/luci/common/logging" 22 "go.chromium.org/luci/common/retry/transient" 23 "go.chromium.org/luci/gae/service/datastore" 24 25 "go.chromium.org/luci/cv/internal/common" 26 "go.chromium.org/luci/cv/internal/prjmanager/prjpb" 27 "go.chromium.org/luci/cv/internal/run" 28 ) 29 30 // addCreatedRuns adds previously unknown runs. 31 func (s *State) addCreatedRuns(ctx context.Context, ids map[common.RunID]struct{}) error { 32 // Each newly created Run relates to the State in one of 4 ways: 33 // (0) Run has already finished. 34 // 35 // (1) Already tracked in State. This method assumes its caller, 36 // OnRunsCreated, has already filtered these Runs out. 37 // 38 // (2) Most likely, there is already a fitting component s.t. all Run's CLs 39 // are in the component. Then, we just append Run to the component's Runs. 40 // 41 // (3) But, it's also possible that component doesn't yet exist, e.g. if Run 42 // was just created via API call. Then, we add their CLs to tracked CLs, but 43 // store these Runs separately from components until components are 44 // re-computed. 45 46 // Approach: load Runs and filter out already finished. Then, after loading CL 47 // IDs for each Run, loop over all existing components and add all Runs of 48 // type (2). All remaining Runs are thus of type (3). 49 runs, errs, err := loadRuns(ctx, ids) 50 if err != nil { 51 return err 52 } 53 // maps CL ID to index of Run(s) in runs slice. 54 // Allocate 2x runs, because most projects have 1..2 CLs per Run on average. 55 clToRunIndex := make(map[common.CLID][]int, 2*len(runs)) 56 // processed keeps track of already processed Runs. 57 processed := make([]bool, len(runs)) 58 for i, r := range runs { 59 switch err := errs[i]; { 60 case err == datastore.ErrNoSuchEntity: 61 logging.Errorf(ctx, "Newly created Run %s not found", r.ID) 62 processed[i] = true 63 case err != nil: 64 return errors.Annotate(err, "failed to load Run %s", r.ID).Tag(transient.Tag).Err() 65 case run.IsEnded(r.Status): 66 logging.Warningf(ctx, "Newly created Run %s is already finished %s", r.ID, r.Status) 67 processed[i] = true 68 default: 69 for _, clid := range r.CLs { 70 clToRunIndex[clid] = append(clToRunIndex[clid], i) 71 } 72 } 73 } 74 75 // Add Runs of type (2) to existing components. 76 var modified bool 77 s.PB.Components, modified = s.PB.COWComponents(func(c *prjpb.Component) *prjpb.Component { 78 // Count CLs in this component which match a Run's index in `runs`. 79 matchedRunIdx := make(map[int]int, len(c.GetClids())) 80 for _, clid := range c.GetClids() { 81 for _, idx := range clToRunIndex[common.CLID(clid)] { 82 matchedRunIdx[idx]++ 83 } 84 } 85 // Add runs to the component iff run's all CLs were matched. 86 var toAdd []*prjpb.PRun 87 for idx, count := range matchedRunIdx { 88 if count == len(runs[idx].CLs) { 89 processed[idx] = true 90 toAdd = append(toAdd, prjpb.MakePRun(runs[idx])) 91 } 92 } 93 if len(toAdd) == 0 { 94 return c 95 } 96 if pruns, modified := c.COWPRuns(nil, toAdd); modified { 97 return &prjpb.Component{ 98 Clids: c.GetClids(), 99 DecisionTime: c.GetDecisionTime(), 100 Pruns: pruns, 101 TriageRequired: true, 102 } 103 } 104 return c 105 }, nil) 106 if modified { 107 s.PB.RepartitionRequired = true 108 } 109 110 // Add remaining Runs are of type (3) to CreatedPruns for later processing. 111 var toAdd []*prjpb.PRun 112 for i, r := range runs { 113 if !processed[i] { 114 toAdd = append(toAdd, prjpb.MakePRun(r)) 115 } 116 } 117 s.PB.CreatedPruns, _ = s.PB.COWCreatedRuns(nil, toAdd) 118 return nil 119 } 120 121 // removeFinishedRuns removes known runs and returns number of the still tracked 122 // runs. 123 func (s *State) removeFinishedRuns(ids map[common.RunID]run.Status, removeCB func(r *prjpb.PRun)) int { 124 delIfFinished := func(r *prjpb.PRun) *prjpb.PRun { 125 id := common.RunID(r.GetId()) 126 if _, ok := ids[id]; ok { 127 removeCB(r) 128 delete(ids, id) 129 return nil 130 } 131 return r 132 } 133 134 removeFromComponent := func(c *prjpb.Component) *prjpb.Component { 135 if len(ids) == 0 { 136 return c 137 } 138 if pruns, modified := c.COWPRuns(delIfFinished, nil); modified { 139 return &prjpb.Component{ 140 Pruns: pruns, 141 Clids: c.GetClids(), 142 DecisionTime: c.GetDecisionTime(), 143 TriageRequired: true, 144 } 145 } 146 return c 147 } 148 149 s.PB.CreatedPruns, _ = s.PB.COWCreatedRuns(delIfFinished, nil) 150 stillTrackedRuns := len(s.PB.GetCreatedPruns()) 151 var modified bool 152 s.PB.Components, modified = s.PB.COWComponents(func(c *prjpb.Component) *prjpb.Component { 153 c = removeFromComponent(c) 154 stillTrackedRuns += len(c.GetPruns()) 155 return c 156 }, nil) 157 if modified { 158 // Removing usually changes components and/or pruning of PCLs. 159 s.PB.RepartitionRequired = true 160 } 161 return stillTrackedRuns 162 } 163 164 func incompleteRuns(ctx context.Context, ids map[common.RunID]struct{}) (common.RunIDs, error) { 165 runs, errs, err := loadRuns(ctx, ids) 166 if err != nil { 167 return nil, err 168 } 169 var incomplete common.RunIDs 170 for i, r := range runs { 171 switch { 172 case errs[i] != nil: 173 return nil, errors.Annotate(errs[i], "failed to load Run %s", r.ID).Tag(transient.Tag).Err() 174 case !run.IsEnded(r.Status): 175 incomplete = append(incomplete, r.ID) 176 } 177 } 178 return incomplete, err 179 } 180 181 // loadRuns loads Runs from Datastore corresponding to given Run IDs. 182 // 183 // Returns slice of Runs, error.MultiError slice corresponding to 184 // per-Run errors *(always exists and has same length as Runs)*, and a 185 // top level error if it can't be attributed to any Run. 186 // 187 // *each error in per-Run errors* is not annotated and is nil if Run was loaded 188 // successfully. 189 func loadRuns(ctx context.Context, ids map[common.RunID]struct{}) ([]*run.Run, errors.MultiError, error) { 190 runs := make([]*run.Run, 0, len(ids)) 191 for id := range ids { 192 runs = append(runs, &run.Run{ID: id}) 193 } 194 err := datastore.Get(ctx, runs) 195 switch merr, ok := err.(errors.MultiError); { 196 case err == nil: 197 return runs, make(errors.MultiError, len(runs)), nil 198 case ok: 199 return runs, merr, nil 200 default: 201 return nil, nil, errors.Annotate(err, "failed to load %d Runs", len(runs)).Tag(transient.Tag).Err() 202 } 203 } 204 205 // maybeMCERun returns whether a given Run could be part of MCE Run. 206 // That is, whether the Run was triggered by chained CQ votes. 207 func maybeMCERun(ctx context.Context, s *State, r *prjpb.PRun) bool { 208 if run.Mode(r.GetMode()) != run.FullRun { 209 return false 210 } 211 pcl := s.PB.GetPCL(r.GetClids()[0]) 212 if pcl == nil { 213 return false 214 } 215 switch idxs := pcl.GetConfigGroupIndexes(); { 216 case len(idxs) != 1: 217 // In case of misconfiguration, PCL may be involved with 0 or 2+ 218 // applicable ConfigGroups, and evalcl uses an empty ConfigGroup 219 // to remove such CLs. 220 // 221 // Such PCL should not be a valid MCE run. 222 return false 223 case len(s.configGroups) == 0: 224 // This may be a bug in CV, but ok. It is not CombineCl. 225 return true 226 case s.configGroups[idxs[0]].Content.GetCombineCls() != nil: 227 return false 228 } 229 return true 230 }