go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/prjmanager/triager/deps.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 triager
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"time"
    22  
    23  	"go.chromium.org/luci/auth/identity"
    24  	"go.chromium.org/luci/common/logging"
    25  
    26  	cfgpb "go.chromium.org/luci/cv/api/config/v2"
    27  	"go.chromium.org/luci/cv/internal/changelist"
    28  	"go.chromium.org/luci/cv/internal/common"
    29  	"go.chromium.org/luci/cv/internal/prjmanager/prjpb"
    30  	"go.chromium.org/luci/cv/internal/run"
    31  )
    32  
    33  // maxAllowedDeps limits how many non-submitted deps a CL may have for CV to
    34  // consider it.
    35  //
    36  // This applies to both singular and combinable modes.
    37  // See also https://crbug.com/1217100.
    38  const maxAllowedDeps = 240
    39  
    40  // triagedDeps categorizes deps of a CL, referred to below as the "dependent"
    41  // CL.
    42  //
    43  // Categories are exclusive. Non-submitted OK deps are not recorded here to
    44  // avoid unnecessary allocations in the most common case, but they do affect
    45  // the lastTriggered time.
    46  type triagedDeps struct {
    47  	// lastCQVoteTriggered among *all* deps which are triggered. Can be Zero
    48  	//time if no dep is triggered.
    49  	lastCQVoteTriggered time.Time
    50  
    51  	// submitted are already submitted deps watched by this project, though not
    52  	// necessarily the same config group as the dependent CL. These deps are OK.
    53  	submitted []*changelist.Dep
    54  
    55  	// notYetLoaded means that more specific category isn't yet known.
    56  	notYetLoaded []*changelist.Dep
    57  
    58  	// needToTrigger is a list of the deps that should be triggered with CQ
    59  	// votes.
    60  	needToTrigger []*changelist.Dep
    61  
    62  	invalidDeps *changelist.CLError_InvalidDeps
    63  }
    64  
    65  // triageDeps triages deps of a PCL. See triagedDeps for documentation.
    66  func triageDeps(ctx context.Context, pcl *prjpb.PCL, cgIndex int32, pm pmState) *triagedDeps {
    67  	cg := pm.ConfigGroup(cgIndex).Content
    68  	res := &triagedDeps{}
    69  	for _, dep := range pcl.GetDeps() {
    70  		dPCL := pm.PCL(dep.GetClid())
    71  		res.categorize(ctx, pcl, cgIndex, cg, dPCL, dep)
    72  		cqTrigger := dPCL.GetTriggers().GetCqVoteTrigger()
    73  		if cqTrigger != nil {
    74  			if tPB := cqTrigger.GetTime(); tPB != nil {
    75  				if t := tPB.AsTime(); res.lastCQVoteTriggered.IsZero() || res.lastCQVoteTriggered.Before(t) {
    76  					res.lastCQVoteTriggered = t
    77  				}
    78  			}
    79  		}
    80  	}
    81  	if okDeps := len(pcl.GetDeps()) - len(res.submitted); okDeps > maxAllowedDeps {
    82  		// Only declare this invalid if every non-submitted DEP is OK.
    83  		if res.invalidDeps == nil && len(res.notYetLoaded) == 0 {
    84  			res.ensureInvalidDeps()
    85  			res.invalidDeps.TooMany = &changelist.CLError_InvalidDeps_TooMany{
    86  				Actual:     int32(okDeps),
    87  				MaxAllowed: maxAllowedDeps,
    88  			}
    89  		}
    90  	}
    91  	return res
    92  }
    93  
    94  // OK is true if triagedDeps doesn't have any not-OK deps.
    95  func (t *triagedDeps) OK() bool {
    96  	return t.invalidDeps == nil
    97  }
    98  
    99  func (t *triagedDeps) makePurgeReason() *changelist.CLError {
   100  	if t.OK() {
   101  		panic("makePurgeReason must be called only iff !OK")
   102  	}
   103  	return &changelist.CLError{Kind: &changelist.CLError_InvalidDeps_{InvalidDeps: t.invalidDeps}}
   104  }
   105  
   106  // categorize adds dep to the applicable slice (if any).
   107  //
   108  // pcl is dependent PCL, which must be triggered.
   109  // Its dep is represented by dPCL.
   110  func (t *triagedDeps) categorize(ctx context.Context, pcl *prjpb.PCL, cgIndex int32, cg *cfgpb.ConfigGroup, dPCL *prjpb.PCL, dep *changelist.Dep) {
   111  	if dPCL == nil {
   112  		t.notYetLoaded = append(t.notYetLoaded, dep)
   113  		return
   114  	}
   115  
   116  	switch s := dPCL.GetStatus(); s {
   117  	case prjpb.PCL_UNKNOWN:
   118  		t.notYetLoaded = append(t.notYetLoaded, dep)
   119  		return
   120  
   121  	case prjpb.PCL_UNWATCHED, prjpb.PCL_DELETED:
   122  		// PCL deleted from Datastore should not happen outside of project
   123  		// re-enablement, so it's OK to treat the same as PCL_UNWATCHED for
   124  		// simplicity.
   125  		t.ensureInvalidDeps()
   126  		t.invalidDeps.Unwatched = append(t.invalidDeps.Unwatched, dep)
   127  		return
   128  
   129  	case prjpb.PCL_OK:
   130  		// Happy path; continue after the switch.
   131  	default:
   132  		panic(fmt.Errorf("unrecognized CL %d dep %d status %s", pcl.GetClid(), dPCL.GetClid(), s))
   133  	}
   134  	// CL is watched by this LUCI project.
   135  
   136  	if dPCL.GetSubmitted() {
   137  		// Submitted CL may no longer be in the expected ConfigGroup,
   138  		// but since it's in the same project, it's OK to refer to it as it
   139  		// doesn't create an information leak.
   140  		t.submitted = append(t.submitted, dep)
   141  		return
   142  	}
   143  
   144  	switch cgIndexes := dPCL.GetConfigGroupIndexes(); len(cgIndexes) {
   145  	case 0:
   146  		panic(fmt.Errorf("at least one ConfigGroup index required for watched dep PCL %d", dPCL.GetClid()))
   147  	case 1:
   148  		if cgIndexes[0] != cgIndex {
   149  			t.ensureInvalidDeps()
   150  			t.invalidDeps.WrongConfigGroup = append(t.invalidDeps.WrongConfigGroup, dep)
   151  			return
   152  		}
   153  		// Happy path; continue after the switch.
   154  	default:
   155  		// Strictly speaking, it may be OK iff dependentCGIndex is matched among
   156  		// other config groups. However, there is no compelling use-case for
   157  		// depending on a CL which matches several config groups. So, for
   158  		// compatibility with CQDaemon, be strict.
   159  		t.ensureInvalidDeps()
   160  		t.invalidDeps.WrongConfigGroup = append(t.invalidDeps.WrongConfigGroup, dep)
   161  		return
   162  	}
   163  
   164  	tr := pcl.GetTriggers().GetCqVoteTrigger()
   165  	dtr := dPCL.GetTriggers().GetCqVoteTrigger()
   166  	if cg.GetCombineCls() == nil {
   167  		t.categorizeSingle(ctx, tr, dtr, dep, cg)
   168  	} else {
   169  		t.categorizeCombinable(tr, dtr, dep)
   170  	}
   171  }
   172  
   173  func (t *triagedDeps) categorizeCombinable(tr, dtr *run.Trigger, dep *changelist.Dep) {
   174  	// During the `combine_cls.stabilization_delay` since the last triggered CL
   175  	// in a group, a user can change their mind. Since the full group of CLs
   176  	// isn't known here, categorization decision may or may not be final.
   177  	switch {
   178  	case dtr.GetMode() == tr.GetMode():
   179  		// Happy path.
   180  		return
   181  	case dtr == nil:
   182  		t.ensureInvalidDeps()
   183  		t.invalidDeps.CombinableUntriggered = append(t.invalidDeps.CombinableUntriggered, dep)
   184  		return
   185  	default:
   186  		// TODO(tandrii): support dry run on dependent and full Run on dep.
   187  		// For example, on a CL stack:
   188  		//      CL  | Mode
   189  		//       D    CQ+1
   190  		//       C    CQ+1
   191  		//       B    CQ+2
   192  		//       A    CQ+2
   193  		//      (base)  -
   194  		// D+C+B+A are can be dry-run-ed and B+A can be CQ+2ed at the same time
   195  		t.ensureInvalidDeps()
   196  		t.invalidDeps.CombinableMismatchedMode = append(t.invalidDeps.CombinableMismatchedMode, dep)
   197  		return
   198  	}
   199  }
   200  
   201  func (t *triagedDeps) categorizeSingle(ctx context.Context, tr, dtr *run.Trigger, dep *changelist.Dep, cg *cfgpb.ConfigGroup) {
   202  	// TODO(crbug/1470341) once the dogfood process is done,
   203  	// enable chained cq votes by default, and remove all the unnecessary
   204  	// param "ctx".
   205  	var isMCEDogfooder bool
   206  	switch triggerer, err := identity.MakeIdentity(fmt.Sprintf("%s:%s", identity.User, tr.Email)); {
   207  	case err != nil:
   208  		// Log the error w/o handling it. Chained CQ votes will be turned on
   209  		// by default.
   210  		logging.Errorf(ctx, "categorizeSingle: MakeIdentity: %s", err)
   211  	default:
   212  		isMCEDogfooder = common.IsMCEDogfooder(ctx, triggerer)
   213  	}
   214  	// dependent is guaranteed non-nil.
   215  	switch mode := run.Mode(tr.GetMode()); {
   216  	case mode == run.FullRun && isMCEDogfooder && dep.GetKind() == changelist.DepKind_HARD:
   217  		// If a dep has no or different (prob CQ+1) CQ vote, then schedule
   218  		// a trigger for CQ+2 on the dep, and postpone a run creation for
   219  		// this CL.
   220  		if tr.GetMode() != dtr.GetMode() {
   221  			t.needToTrigger = append(t.needToTrigger, dep)
   222  			return
   223  		}
   224  	case mode == run.FullRun:
   225  		if cg.GetVerifiers().GetGerritCqAbility().GetAllowSubmitWithOpenDeps() && dep.GetKind() == changelist.DepKind_HARD {
   226  			// If configured, allow CV to submit the entire stack (HARD deps
   227  			// only) of changes.
   228  			return
   229  		}
   230  		t.ensureInvalidDeps()
   231  		t.invalidDeps.SingleFullDeps = append(t.invalidDeps.SingleFullDeps, dep)
   232  	}
   233  }
   234  
   235  // ensureInvalidDeps initializes if necessary and returns .invalidDeps.
   236  func (t *triagedDeps) ensureInvalidDeps() *changelist.CLError_InvalidDeps {
   237  	if t.invalidDeps == nil {
   238  		t.invalidDeps = &changelist.CLError_InvalidDeps{}
   239  	}
   240  	return t.invalidDeps
   241  }
   242  
   243  // iterateNotSubmitted calls clbk per each dep which isn't submitted.
   244  //
   245  // Must be called with the same PCL as was used to construct the triagedDeps.
   246  func (t *triagedDeps) iterateNotSubmitted(pcl *prjpb.PCL, clbk func(dep *changelist.Dep)) {
   247  	// Because construction of triagedDeps is in order of PCL's Deps, the
   248  	// submitted must be a sub-sequence of Deps and we can compare just Dep
   249  	// pointers.
   250  	all, subs := pcl.GetDeps(), t.submitted
   251  	for {
   252  		switch {
   253  		case len(subs) == 0:
   254  			for _, dep := range all {
   255  				clbk(dep)
   256  			}
   257  			return
   258  		case len(all) == 0:
   259  			panic(errors.New("must not happen because submitted must be a subset of all deps (wrong PCL?)"))
   260  		default:
   261  			if all[0] == subs[0] {
   262  				subs = subs[1:]
   263  			} else {
   264  				clbk(all[0])
   265  			}
   266  			all = all[1:]
   267  		}
   268  	}
   269  }