go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/prjmanager/triager/trigger.go (about) 1 // Copyright 2023 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 "fmt" 20 "sort" 21 22 "go.chromium.org/luci/common/logging" 23 "go.chromium.org/luci/cv/internal/prjmanager/prjpb" 24 "go.chromium.org/luci/cv/internal/run" 25 ) 26 27 // stageTriggerCLDeps creates TriggeringCLsTasks(s) for CLs that shoul propagate 28 // its CQ votes to its deps. 29 func stageTriggerCLDeps(ctx context.Context, cls map[int64]*clInfo, pm pmState) []*prjpb.TriggeringCLDeps { 30 if len(cls) == 0 { 31 return nil 32 } 33 clsToTriggerDeps := make(map[int64]*clInfo, len(cls)) 34 clsShouldNotTriggerDeps := make(map[int64]struct{}, len(cls)) 35 for _, clid := range computeSortedCLIDs(cls) { 36 info := cls[clid] 37 38 switch { 39 case info.deps == nil: 40 case len(info.deps.needToTrigger) == 0: 41 case canScheduleTriggerCLDeps(ctx, clid, cls): 42 clsToTriggerDeps[clid] = info 43 default: 44 // If a clinfo reaches here, 45 // - the CL has at least one dep to trigger 46 // - however, canScheduleTriggerCLDeps() says don't schedule 47 // a new one. e.g., it already has TriggeringCLDeps op. 48 // 49 // If a CL shouldn't schedule a new one, none of its deps should 50 // either. 51 for _, dep := range info.pcl.GetDeps() { 52 clsShouldNotTriggerDeps[dep.GetClid()] = struct{}{} 53 } 54 } 55 } 56 57 // schedule new tasks for the top most CLs only. 58 for _, ci := range clsToTriggerDeps { 59 if _, exist := clsShouldNotTriggerDeps[ci.pcl.GetClid()]; exist { 60 delete(clsToTriggerDeps, ci.pcl.GetClid()) 61 } 62 for _, dep := range ci.pcl.GetDeps() { 63 switch _, ok := clsToTriggerDeps[dep.GetClid()]; { 64 case !ok: 65 continue 66 default: 67 delete(clsToTriggerDeps, dep.GetClid()) 68 } 69 } 70 } 71 var ret []*prjpb.TriggeringCLDeps 72 for clid, ci := range clsToTriggerDeps { 73 t := &prjpb.TriggeringCLDeps{ 74 OriginClid: clid, 75 DepClids: make([]int64, len(ci.deps.needToTrigger)), 76 Trigger: ci.pcl.GetTriggers().GetCqVoteTrigger(), 77 ConfigGroupName: pm.ConfigGroup(ci.pcl.GetConfigGroupIndexes()[0]).ID.Name(), 78 } 79 for i, dep := range ci.deps.needToTrigger { 80 t.DepClids[i] = dep.GetClid() 81 } 82 logging.Infof(ctx, "Scheduling a TriggeringCLDeps for clid %d with deps %v", 83 t.GetOriginClid(), t.GetDepClids()) 84 ret = append(ret, t) 85 } 86 return ret 87 } 88 89 func computeSortedCLIDs(clinfos map[int64]*clInfo) []int64 { 90 // sort cls by clid in descending order to produce a consistent decision 91 // for OriginClid. 92 if len(clinfos) == 0 { 93 return nil 94 } 95 clids := make([]int64, 0, len(clinfos)) 96 for clid := range clinfos { 97 clids = append(clids, clid) 98 } 99 sort.Slice(clids, func(i, j int) bool { 100 return clids[i] > clids[j] 101 }) 102 return clids 103 } 104 105 // canScheduleTriggerCLDeps returns whether triager can schedule 106 // a new TriggeringCLDeps for a given CL. 107 // 108 // Panic if the ci has no needToTrigger 109 func canScheduleTriggerCLDeps(ctx context.Context, clid int64, cls map[int64]*clInfo) bool { 110 ci := cls[clid] 111 if ci == nil || ci.deps == nil || len(ci.deps.needToTrigger) == 0 { 112 panic(fmt.Errorf("canScheduleTriggerCLDeps called with 0 needToTrigger")) 113 } 114 cqMode := run.Mode(ci.pcl.GetTriggers().GetCqVoteTrigger().GetMode()) 115 if cqMode == "" { 116 return false 117 } 118 switch { 119 case ci.pcl.GetOutdated() != nil: 120 return false 121 case len(ci.deps.notYetLoaded) > 0: 122 return false 123 case ci.purgingCL != nil || len(ci.purgeReasons) > 0: 124 return false 125 case ci.triggeringCLDeps != nil: 126 return false 127 case ci.hasIncompleteRun(cqMode): 128 return false 129 } 130 // If the dep is currently being purged or triggered, don't schedule 131 // a new task, until they are done. For example, given CL{1-5} with CL1 132 // being the bottommost CL, let's say that 133 // - the CL author triggers CQ+2 on CL3. 134 // - while the TriggeringCLDeps{} for CL{1,2,3} is in progress, 135 // the CL author triggers CQ+2 on CL5. 136 // 137 // If so, wait until the TriggeringCLDeps{} for CL{1,2,3} is done 138 // to remove unusual corner cases. 139 for _, dep := range ci.pcl.GetDeps() { 140 switch dci, ok := cls[dep.GetClid()]; { 141 case !ok: 142 continue 143 case dci.pcl.GetOutdated() != nil: 144 return false 145 case dci.purgingCL != nil, len(dci.purgeReasons) > 0: 146 return false 147 case dci.triggeringCLDeps != nil: 148 return false 149 case dci.hasIncompleteRun(cqMode) && (dci.deps != nil && len(dci.deps.needToTrigger) > 0): 150 // This is the case where 151 // - a dep CL has an ongoing Run, 152 // - the dep CL has dependenies, and 153 // - at least one of the deps of the dep CL have no CQ+2 votes. 154 // 155 // Let's say that there are CL{1-3}, with CL1 being the bottommost 156 // CL and the following conditions. 157 // - CL1 with CQ(0). 158 // - CL2 with CQ(+2) and an ongoing run. 159 // - CL3 with CQ(+2) and no run. 160 // 161 // This can happen only if the Run for CL1 failed, but CL2 hasn't 162 // ended yet. PM should NOT schedule a new TriggeringCLDeps for CL3, 163 // until CL2 ends. 164 return false 165 } 166 } 167 return true 168 }