go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/rpc/admin/analyze_mutli_cls.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 admin 16 17 import ( 18 "context" 19 "fmt" 20 "sort" 21 "strings" 22 "time" 23 24 "go.chromium.org/luci/common/errors" 25 "go.chromium.org/luci/common/gcloud/gs" 26 "go.chromium.org/luci/common/retry/transient" 27 "go.chromium.org/luci/gae/service/datastore" 28 "go.chromium.org/luci/server/auth" 29 "go.chromium.org/luci/server/dsmapper" 30 "golang.org/x/sync/errgroup" 31 32 "go.chromium.org/luci/cv/internal/changelist" 33 "go.chromium.org/luci/cv/internal/common" 34 "go.chromium.org/luci/cv/internal/run" 35 ) 36 37 var multiCLAnalysisConfig = dsmapper.JobConfig{ 38 Mapper: "multiCLAnalysis", 39 Query: dsmapper.Query{ 40 Kind: "Run", 41 }, 42 PageSize: 32, 43 ShardCount: 4, 44 } 45 46 var multiCLAnalysisMapperFactory = func(_ context.Context, j *dsmapper.Job, _ int) (dsmapper.Mapper, error) { 47 analyzeMultiCLs := func(ctx context.Context, keys []*datastore.Key) error { 48 runs := make([]*run.Run, len(keys)) 49 for i, k := range keys { 50 runs[i] = &run.Run{ 51 ID: common.RunID(k.StringID()), 52 } 53 } 54 55 // Check before a transaction if an update is even necessary. 56 if err := datastore.Get(ctx, runs); err != nil { 57 return errors.Annotate(err, "failed to fetch RunCLs").Tag(transient.Tag).Err() 58 } 59 // Only interested in all multi CL runs created between 2022-08-01 to 60 // 2023-08-01 61 multiCLRuns := runs[:0] 62 for _, r := range runs { 63 switch { 64 case len(r.CLs) < 2: 65 case r.CreateTime.Before(time.Date(2022, 8, 1, 0, 0, 0, 0, time.UTC)): 66 case r.CreateTime.After(time.Date(2023, 8, 1, 0, 0, 0, 0, time.UTC)): 67 default: 68 multiCLRuns = append(multiCLRuns, r) 69 } 70 } 71 if len(multiCLRuns) == 0 { 72 return nil 73 } 74 75 transport, err := auth.GetRPCTransport(ctx, auth.AsSelf, auth.WithScopes(auth.CloudOAuthScopes...)) 76 if err != nil { 77 return errors.Annotate(err, "failed to create Google Storage RPC transport").Err() 78 } 79 gsClient, err := gs.NewProdClient(ctx, transport) 80 if err != nil { 81 return errors.Annotate(err, "Failed to create GS client.").Err() 82 } 83 defer func() { _ = gsClient.Close() }() 84 85 eg, ectx := errgroup.WithContext(ctx) 86 eg.SetLimit(8) 87 for _, r := range multiCLRuns { 88 r := r 89 eg.Go(func() error { 90 cls, err := run.LoadRunCLs(ectx, r.ID, r.CLs) 91 if err != nil { 92 return errors.Annotate(err, "failed to load RunCLs").Tag(transient.Tag).Err() 93 } 94 clGraph := makeCLGraph(cls) 95 if !clGraph.hasRootCL() { 96 writer, err := gsClient.NewWriter(gs.MakePath("temp-multi-cl-graph-store", fmt.Sprintf("%s-%d", j.Config.Mapper, j.ID), fmt.Sprintf("%s.txt", r.ID))) 97 if err != nil { 98 return errors.Annotate(err, "failed to create new gs writer").Err() 99 } 100 defer func() { _ = writer.Close() }() 101 if _, err := writer.Write([]byte(clGraph.computeDotGraph())); err != nil { 102 return errors.Annotate(err, "failed to write to gs object").Err() 103 } 104 } 105 return nil 106 }) 107 } 108 return eg.Wait() 109 } 110 111 return analyzeMultiCLs, nil 112 } 113 114 type clGraph struct { 115 deps map[common.CLID][]*changelist.Dep 116 clMap map[common.CLID]*run.RunCL 117 } 118 119 func makeCLGraph(cls []*run.RunCL) clGraph { 120 ret := clGraph{} 121 ret.clMap = make(map[common.CLID]*run.RunCL, len(cls)) 122 externalIDToCL := make(map[changelist.ExternalID]*run.RunCL, len(cls)) 123 for _, cl := range cls { 124 ret.clMap[cl.ID] = cl 125 externalIDToCL[cl.ExternalID] = cl 126 } 127 ret.deps = make(map[common.CLID][]*changelist.Dep, len(cls)) 128 for _, cl := range cls { 129 for _, hardDep := range cl.Detail.GetGerrit().GetGitDeps() { 130 if !hardDep.GetImmediate() { 131 continue 132 } 133 depExternalID := changelist.MustGobID(cl.Detail.GetGerrit().GetHost(), hardDep.GetChange()) 134 // A CL may depend on an already submitted CL that are not part of the 135 // Run. Ignore them. 136 if depCL, ok := externalIDToCL[depExternalID]; ok { 137 ret.deps[cl.ID] = append(ret.deps[cl.ID], &changelist.Dep{ 138 Clid: int64(depCL.ID), 139 Kind: changelist.DepKind_HARD, 140 }) 141 } 142 143 } 144 145 for _, softDep := range cl.Detail.GetGerrit().GetSoftDeps() { 146 depExternalID := changelist.MustGobID(softDep.GetHost(), softDep.GetChange()) 147 // A CL may depend on an already submitted CL that are not part of the 148 // Run. Ignore them. 149 if depCL, ok := externalIDToCL[depExternalID]; ok { 150 ret.deps[cl.ID] = append(ret.deps[cl.ID], &changelist.Dep{ 151 Clid: int64(depCL.ID), 152 Kind: changelist.DepKind_SOFT, 153 }) 154 } 155 } 156 } 157 return ret 158 } 159 160 // hasRootCL checks whether a multi CL Run has any root CLs. 161 // 162 // A root CL means if we start from this CL following its dependencies (i.e. 163 // hard dep == git parent-child relationship, soft dep == `cq-depend` footer), 164 // it can reach all other CLs involved in the Run. 165 func (g clGraph) hasRootCL() bool { 166 for cl := range g.clMap { 167 if g.canTraverseAllCLs(cl) { 168 return true 169 } 170 } 171 return false 172 } 173 174 func (g clGraph) canTraverseAllCLs(root common.CLID) bool { 175 visited := common.CLIDsSet{} 176 g.dfs(root, visited) 177 return len(visited) == g.numCls() 178 } 179 180 func (g clGraph) dfs(cl common.CLID, visited common.CLIDsSet) { 181 visited.Add(cl) 182 for _, depCL := range g.depCLIDs(cl) { 183 if !visited.Has(depCL) { 184 g.dfs(common.CLID(depCL), visited) 185 } 186 } 187 } 188 189 func (g clGraph) numCls() int { 190 return len(g.clMap) 191 } 192 193 func (g clGraph) depCLIDs(cl common.CLID) common.CLIDs { 194 var ret common.CLIDs 195 for _, dep := range g.deps[cl] { 196 ret = append(ret, common.CLID(dep.Clid)) 197 } 198 return ret 199 } 200 201 func (g clGraph) computeDotGraph() string { 202 var nodeWriter strings.Builder 203 var depsWriter strings.Builder 204 205 for _, clid := range g.sortedCLs() { 206 cl := g.clMap[clid] 207 fmt.Fprintf(&nodeWriter, ` "%s" [href="%s", target="_parent"]`, cl.ExternalID, cl.ExternalID.MustURL()) 208 nodeWriter.WriteString("\n") 209 210 for _, dep := range g.deps[clid] { 211 depCL := g.clMap[common.CLID(dep.Clid)] 212 fmt.Fprintf(&depsWriter, ` "%s" -> "%s"`, cl.ExternalID, depCL.ExternalID) 213 if dep.Kind == changelist.DepKind_SOFT { 214 depsWriter.WriteString(`[style="dotted"]`) 215 } 216 depsWriter.WriteString("\n") 217 } 218 } 219 220 return fmt.Sprintf("digraph {\n%s\n%s}", nodeWriter.String(), depsWriter.String()) 221 } 222 223 func (g clGraph) sortedCLs() common.CLIDs { 224 ret := make(common.CLIDs, 0, len(g.clMap)) 225 for cl := range g.clMap { 226 ret = append(ret, cl) 227 } 228 sort.Slice(ret, func(i, j int) bool { 229 return ret[i] < ret[j] 230 }) 231 return ret 232 }