go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/prjmanager/state/partition.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 "fmt" 19 20 "go.chromium.org/luci/common/data/disjointset" 21 "go.chromium.org/luci/cv/internal/common" 22 "go.chromium.org/luci/cv/internal/prjmanager/prjpb" 23 ) 24 25 // repartition updates components. 26 // 27 // On success, guarantees the following: 28 // - .Pcls contains all referenced by components CLs. 29 // However, some deps of those CLs may be missing. 30 // - .Pcls doesn't contain any CLs not in components. 31 // - .CreatedRuns is nil, 32 // - .RepartitionRequired is false. 33 // - pclIndex is up-to-date. 34 func (s *State) repartition(cat *categorizedCLs) { 35 s.ensurePCLIndex() 36 plan := s.planPartition(cat) 37 s.PB.Components = s.execPartition(cat, plan) 38 39 // Remove unused PCLs while updating pclIndex. This can be done only after 40 // components are updated to ensure components don't reference any unused CLs. 41 pcls := make([]*prjpb.PCL, 0, len(s.PB.GetPcls())-len(cat.unused)) 42 for _, pcl := range s.PB.GetPcls() { 43 id := common.CLID(pcl.GetClid()) 44 if cat.unused.Has(id) { 45 delete(s.pclIndex, id) 46 } else { 47 s.pclIndex[id] = len(pcls) 48 pcls = append(pcls, pcl) 49 } 50 } 51 s.PB.Pcls = pcls 52 s.PB.CreatedPruns = nil 53 s.PB.RepartitionRequired = false 54 } 55 56 // planPartition returns a DisjointSet representing a partition of PCLs. 57 func (s *State) planPartition(cat *categorizedCLs) disjointset.DisjointSet { 58 s.ensurePCLIndex() 59 d := disjointset.New(len(s.PB.GetPcls())) // operates on indexes of PCLs 60 61 // CLs of a Run must be in the same set. 62 s.PB.IterIncompleteRuns(func(r *prjpb.PRun, _ *prjpb.Component) (stop bool) { 63 clids := r.GetClids() 64 first := s.pclIndex[common.CLID(clids[0])] 65 for _, id := range clids[1:] { 66 d.Merge(first, s.pclIndex[common.CLID(id)]) 67 } 68 return 69 }) 70 // Active deps of an active CL must be in the same set as the CL. 71 for i, pcl := range s.PB.GetPcls() { 72 if cat.active.HasI64(pcl.GetClid()) { 73 for _, dep := range pcl.GetDeps() { 74 id := dep.GetClid() 75 if j, exists := s.pclIndex[common.CLID(id)]; exists && cat.active.HasI64(id) { 76 d.Merge(i, j) 77 } 78 } 79 } 80 } 81 return d 82 } 83 84 // execPartition returns components according to partition plan. 85 // 86 // Expects pclIndex to be same used by planPartition. 87 func (s *State) execPartition(cat *categorizedCLs, d disjointset.DisjointSet) []*prjpb.Component { 88 89 canReuse := func(c *prjpb.Component) (root int, can bool) { 90 // Old component can be re-used iff all of: 91 // (1) it has exactly the same set in the new partition. 92 // (2) it does not contain unused CLs. 93 // (3) it contains at least 1 active CL. 94 clids := c.GetClids() 95 96 // Check (1). 97 root = d.RootOf(s.pclIndex[common.CLID(clids[0])]) 98 if d.SizeOf(root) != len(clids) { 99 return -1, false 100 } 101 for _, id := range clids[1:] { 102 if root != d.RootOf(s.pclIndex[common.CLID(id)]) { 103 return -1, false 104 } 105 } 106 107 // Check (2) and (3). 108 hasActive := false 109 for _, clid := range clids { 110 if cat.unused.HasI64(clid) { 111 if len(clids) != 1 { 112 // Note that 2+ CL component which satisfies (1) can't have an unused 113 // CL. Unused CL don't have relation to other CLs, and so wouldn't be 114 // grouped into the same set in a new partition. 115 panic(fmt.Errorf("component with %d CLs %v has unused CL %d", len(clids), clids, clid)) 116 } 117 return -1, false 118 } 119 if cat.active.HasI64(clid) { 120 hasActive = true 121 } 122 } 123 if !hasActive { 124 return -1, false 125 } 126 127 return root, true 128 } 129 // First, try to re-use existing components whenever possible. 130 // Typically, this should cover most components. 131 reused := make(map[int]*prjpb.Component, d.Count()) 132 var deleted []*prjpb.Component 133 for _, c := range s.PB.GetComponents() { 134 if root, yes := canReuse(c); yes { 135 reused[root] = c 136 continue 137 } 138 deleted = append(deleted, c) 139 } 140 141 // Now create new components. 142 created := make(map[int]*prjpb.Component, d.Count()-len(reused)) 143 for index, pcl := range s.PB.GetPcls() { 144 id := pcl.GetClid() 145 if !cat.active.HasI64(id) { 146 if size := d.SizeOf(index); size != 1 { 147 panic(fmt.Errorf("inactive CLs must end up in a disjoint set of their own: %d => %d", id, size)) 148 } 149 continue // no component for inactive CLs. 150 } 151 root := d.RootOf(index) 152 if _, ok := reused[root]; ok { 153 continue 154 } 155 c, exists := created[root] 156 if !exists { 157 size := d.SizeOf(root) 158 c = &prjpb.Component{ 159 Clids: make([]int64, 0, size), 160 TriageRequired: true, 161 } 162 created[root] = c 163 } 164 c.Clids = append(c.GetClids(), id) 165 } 166 167 // And fill them with Incomplete runs. 168 addPRuns := func(pruns []*prjpb.PRun) { 169 for _, r := range pruns { 170 index := s.pclIndex[common.CLID(r.GetClids()[0])] 171 c := created[d.RootOf(index)] 172 c.Pruns = append(c.GetPruns(), r) // not yet sorted order. 173 } 174 } 175 addPRuns(s.PB.GetCreatedPruns()) 176 for _, c := range deleted { 177 addPRuns(c.GetPruns()) 178 } 179 180 out := make([]*prjpb.Component, 0, len(reused)+len(created)) 181 for _, c := range reused { 182 out = append(out, c) 183 } 184 for _, c := range created { 185 prjpb.SortPRuns(c.Pruns) 186 out = append(out, c) 187 } 188 return out 189 }