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  }