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  }