go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/prjmanager/state/categorize_cls.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 "fmt" 20 "time" 21 22 "go.chromium.org/luci/common/clock" 23 24 "go.chromium.org/luci/cv/internal/changelist" 25 "go.chromium.org/luci/cv/internal/common" 26 "go.chromium.org/luci/cv/internal/prjmanager/prjpb" 27 ) 28 29 type categorizedCLs struct { 30 // active CLs must remain in PCLs. 31 // 32 // A CL is active iff either: 33 // * it has non-nil .Trigger within `common.MaxTriggerAge` and is watched by 34 // this LUCI project (see `isActiveStandalonePCL`); 35 // * OR it belongs to an incomplete Run. 36 // NOTE: In this case, CL may be no longer watched by this project or even 37 // be with status=DELETED. Such state may temporary arise due to changes 38 // in project's config. Eventually Run Manager will cancel the Run, 39 // resulting in removal of the Run from the PM's State and hence removal 40 // of the CL from the active set. 41 active common.CLIDsSet 42 // deps CLs are non-active CLs which should be tracked in PCLs because they 43 // are deps of active CLs. 44 // 45 // Similar to active CLs of incomplete Run, these CLs may not even be watched 46 // by this project or be with status=DELETED, but such state should be 47 // temporary. 48 deps common.CLIDsSet 49 // unused CLs are CLs in PCLs that are neither active nor deps and should be 50 // deleted from PCLs. 51 unused common.CLIDsSet 52 // unloaded are CLs that are either active or deps but not present in PCLs. 53 // 54 // NOTE: if this is not empty, it means `deps` and `unused` aren't yet final 55 // and may be changed after the `unloaded` CLs are loaded. 56 unloaded common.CLIDsSet 57 } 58 59 // isActiveStandalonePCL returns true if PCL is active on its own. 60 // 61 // See categorizedCLs.active spec. 62 func isActiveStandalonePCL(pcl *prjpb.PCL, now time.Time) bool { 63 if pcl.GetStatus() != prjpb.PCL_OK { 64 return false 65 } 66 67 cutoff := now.Add(-common.MaxTriggerAge) 68 switch { 69 case pcl.GetTriggers().GetCqVoteTrigger().GetTime().AsTime().After(cutoff): 70 case pcl.GetTriggers().GetNewPatchsetRunTrigger().GetTime().AsTime().After(cutoff): 71 default: 72 return false 73 } 74 return true 75 } 76 77 // categorizeCLs computes categorizedCLs based on current State. 78 // 79 // The resulting categorizeCLs spans not just PCLs, but also CreatedRuns, since 80 // newly created Runs may reference CLs not yet tracked in PCLs. 81 func (s *State) categorizeCLs(ctx context.Context) *categorizedCLs { 82 s.ensurePCLIndex() 83 now := clock.Now(ctx) 84 85 // reduce typing and redundant !=nil check in GetPcls(). 86 pcls := s.PB.GetPcls() 87 88 res := &categorizedCLs{ 89 // Allocate the maps guessing initial size: 90 // * most PCLs must be active, with very few being pure deps or unused, 91 // * unloaded come from CreatedRuns, assume 2 CL per Run. 92 active: make(common.CLIDsSet, len(pcls)), 93 deps: make(common.CLIDsSet, 16), 94 unused: make(common.CLIDsSet, 16), 95 unloaded: make(common.CLIDsSet, len(s.PB.GetCreatedPruns())*2), 96 } 97 98 // First, compute all active CLs and if any of them are unloaded. 99 for _, r := range s.PB.GetCreatedPruns() { 100 for _, id := range r.GetClids() { 101 res.active.AddI64(id) 102 if !s.pclIndex.hasI64(id) { 103 res.unloaded.AddI64(id) 104 } 105 } 106 } 107 for _, c := range s.PB.GetComponents() { 108 for _, r := range c.GetPruns() { 109 for _, id := range r.GetClids() { 110 res.active.AddI64(id) 111 } 112 } 113 } 114 for _, pcl := range pcls { 115 id := pcl.GetClid() 116 if isActiveStandalonePCL(pcl, now) { 117 res.active.AddI64(id) 118 } 119 } 120 121 // Second, compute `deps` and if any of them are unloaded. 122 for _, pcl := range pcls { 123 if res.active.HasI64(pcl.GetClid()) { 124 for _, dep := range pcl.GetDeps() { 125 id := dep.GetClid() 126 if !res.active.HasI64(id) { 127 res.deps.AddI64(id) 128 if !s.pclIndex.hasI64(id) { 129 res.unloaded.AddI64(id) 130 } 131 } 132 } 133 } 134 } 135 // Third, compute `unused` as all unreferenced CLs in PCLs. 136 for _, pcl := range s.PB.GetPcls() { 137 id := pcl.GetClid() 138 if !res.active.HasI64(id) && !res.deps.HasI64(id) { 139 res.unused.AddI64(id) 140 } 141 } 142 return res 143 } 144 145 // loadActiveIntoPCLs ensures PCLs contain all active CLs and modifies 146 // categorizedCLs accordingly. 147 // 148 // Doesn't guarantee that all their deps are loaded. 149 func (s *State) loadActiveIntoPCLs(ctx context.Context, cat *categorizedCLs) error { 150 // Consider a long *chain* of dependent CLs each with CQ+1 vote: 151 // A <- B (B depends on A) 152 // B <- C 153 // ... 154 // Y <- Z 155 // Suppose CV first notices just Z, s.t. CL updater notifies PM with 156 // OnCLUpdated event of just Z. Upon receiving which, PM will add Z(deps: Y) 157 // into PCL, and then calls this function. 158 // Even if at this point Datastore contains all {Y..A} CLs, it's unreasonable 159 // to load them all in this function because it'd require O(len(chain)) of 160 // RPCs to Datastore, whereby each (% last one) detects yet another dep. 161 // Thus, no guarantee to load deps is provided. 162 // 163 // Normally, the next PM invocation to receive remaining {Y..A} OnCLUpdated 164 // events, which would allow to load them all in 1 Datastore GetMulti. 165 // 166 // Also, such CL chains are rare; most frequently CV deals with long CL stacks, 167 // where the latest CL depends on most others, e.g. Z{deps: A..Y}. So, loading 168 // all already known deps is beneficial. 169 // Furthermore, we must load not yet known CLs which are referenced in 170 // CreatedRuns. The categorizeCLs() bundles both missing active and deps into 171 // unloaded CLs. 172 for len(cat.unloaded) == 0 { 173 return nil 174 } 175 if err := s.loadUnloadedCLsOnce(ctx, cat); err != nil { 176 return err 177 } 178 for u := range cat.unloaded { 179 if cat.active.Has(u) { 180 panic(fmt.Errorf("%d CL is not loaded but active", u)) 181 } 182 } 183 return nil 184 } 185 186 // loadUnloadedCLsOnce loads `unloaded` CLs from Datastore, updates PCLs and 187 // categorizedCLs. 188 // 189 // If any previously `unloaded` CLs were or turned out to be active, 190 // then their deps may end up in `unloaded`. 191 func (s *State) loadUnloadedCLsOnce(ctx context.Context, cat *categorizedCLs) error { 192 cls := make([]*changelist.CL, 0, len(cat.unloaded)) 193 for clid := range cat.unloaded { 194 cls = append(cls, &changelist.CL{ID: clid}) 195 } 196 if err := s.evalCLsFromDS(ctx, cls); err != nil { 197 return err 198 } 199 now := clock.Now(ctx) 200 // This is inefficient, since this could have been done only for loaded CLs. 201 // Consider adding callback to the evalCLsFromDS. 202 for _, pcl := range s.PB.GetPcls() { 203 id := pcl.GetClid() 204 if !cat.unloaded.HasI64(id) { 205 continue 206 } 207 cat.unloaded.DelI64(id) 208 if !cat.active.HasI64(id) { 209 // CL was a mere dep before, but its details weren't known. 210 if !isActiveStandalonePCL(pcl, now) { 211 continue // pcl was and remains just a dep. 212 } else { 213 // Promote CL to active. 214 cat.deps.DelI64(id) 215 cat.active.AddI64(id) 216 } 217 } 218 // Recurse into deps of a just loaded active CLs. 219 for _, dep := range pcl.GetDeps() { 220 id := dep.GetClid() 221 if !cat.active.HasI64(id) && !cat.deps.HasI64(id) { 222 cat.deps.AddI64(id) 223 if cat.unused.HasI64(id) { 224 cat.unused.DelI64(id) 225 } else { 226 cat.unloaded.AddI64(id) 227 } 228 } 229 } 230 } 231 return nil 232 }