go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/prjmanager/state/runs.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  	"context"
    19  
    20  	"go.chromium.org/luci/common/errors"
    21  	"go.chromium.org/luci/common/logging"
    22  	"go.chromium.org/luci/common/retry/transient"
    23  	"go.chromium.org/luci/gae/service/datastore"
    24  
    25  	"go.chromium.org/luci/cv/internal/common"
    26  	"go.chromium.org/luci/cv/internal/prjmanager/prjpb"
    27  	"go.chromium.org/luci/cv/internal/run"
    28  )
    29  
    30  // addCreatedRuns adds previously unknown runs.
    31  func (s *State) addCreatedRuns(ctx context.Context, ids map[common.RunID]struct{}) error {
    32  	// Each newly created Run relates to the State in one of 4 ways:
    33  	//   (0) Run has already finished.
    34  	//
    35  	//   (1) Already tracked in State. This method assumes its caller,
    36  	//   OnRunsCreated, has already filtered these Runs out.
    37  	//
    38  	//   (2) Most likely, there is already a fitting component s.t. all Run's CLs
    39  	//   are in the component. Then, we just append Run to the component's Runs.
    40  	//
    41  	//   (3) But, it's also possible that component doesn't yet exist, e.g. if Run
    42  	//   was just created via API call. Then, we add their CLs to tracked CLs, but
    43  	//   store these Runs separately from components until components are
    44  	//   re-computed.
    45  
    46  	// Approach: load Runs and filter out already finished. Then, after loading CL
    47  	// IDs for each Run, loop over all existing components and add all Runs of
    48  	// type (2). All remaining Runs are thus of type (3).
    49  	runs, errs, err := loadRuns(ctx, ids)
    50  	if err != nil {
    51  		return err
    52  	}
    53  	// maps CL ID to index of Run(s) in runs slice.
    54  	// Allocate 2x runs, because most projects have 1..2 CLs per Run on average.
    55  	clToRunIndex := make(map[common.CLID][]int, 2*len(runs))
    56  	// processed keeps track of already processed Runs.
    57  	processed := make([]bool, len(runs))
    58  	for i, r := range runs {
    59  		switch err := errs[i]; {
    60  		case err == datastore.ErrNoSuchEntity:
    61  			logging.Errorf(ctx, "Newly created Run %s not found", r.ID)
    62  			processed[i] = true
    63  		case err != nil:
    64  			return errors.Annotate(err, "failed to load Run %s", r.ID).Tag(transient.Tag).Err()
    65  		case run.IsEnded(r.Status):
    66  			logging.Warningf(ctx, "Newly created Run %s is already finished %s", r.ID, r.Status)
    67  			processed[i] = true
    68  		default:
    69  			for _, clid := range r.CLs {
    70  				clToRunIndex[clid] = append(clToRunIndex[clid], i)
    71  			}
    72  		}
    73  	}
    74  
    75  	// Add Runs of type (2) to existing components.
    76  	var modified bool
    77  	s.PB.Components, modified = s.PB.COWComponents(func(c *prjpb.Component) *prjpb.Component {
    78  		// Count CLs in this component which match a Run's index in `runs`.
    79  		matchedRunIdx := make(map[int]int, len(c.GetClids()))
    80  		for _, clid := range c.GetClids() {
    81  			for _, idx := range clToRunIndex[common.CLID(clid)] {
    82  				matchedRunIdx[idx]++
    83  			}
    84  		}
    85  		// Add runs to the component iff run's all CLs were matched.
    86  		var toAdd []*prjpb.PRun
    87  		for idx, count := range matchedRunIdx {
    88  			if count == len(runs[idx].CLs) {
    89  				processed[idx] = true
    90  				toAdd = append(toAdd, prjpb.MakePRun(runs[idx]))
    91  			}
    92  		}
    93  		if len(toAdd) == 0 {
    94  			return c
    95  		}
    96  		if pruns, modified := c.COWPRuns(nil, toAdd); modified {
    97  			return &prjpb.Component{
    98  				Clids:          c.GetClids(),
    99  				DecisionTime:   c.GetDecisionTime(),
   100  				Pruns:          pruns,
   101  				TriageRequired: true,
   102  			}
   103  		}
   104  		return c
   105  	}, nil)
   106  	if modified {
   107  		s.PB.RepartitionRequired = true
   108  	}
   109  
   110  	// Add remaining Runs are of type (3) to CreatedPruns for later processing.
   111  	var toAdd []*prjpb.PRun
   112  	for i, r := range runs {
   113  		if !processed[i] {
   114  			toAdd = append(toAdd, prjpb.MakePRun(r))
   115  		}
   116  	}
   117  	s.PB.CreatedPruns, _ = s.PB.COWCreatedRuns(nil, toAdd)
   118  	return nil
   119  }
   120  
   121  // removeFinishedRuns removes known runs and returns number of the still tracked
   122  // runs.
   123  func (s *State) removeFinishedRuns(ids map[common.RunID]run.Status, removeCB func(r *prjpb.PRun)) int {
   124  	delIfFinished := func(r *prjpb.PRun) *prjpb.PRun {
   125  		id := common.RunID(r.GetId())
   126  		if _, ok := ids[id]; ok {
   127  			removeCB(r)
   128  			delete(ids, id)
   129  			return nil
   130  		}
   131  		return r
   132  	}
   133  
   134  	removeFromComponent := func(c *prjpb.Component) *prjpb.Component {
   135  		if len(ids) == 0 {
   136  			return c
   137  		}
   138  		if pruns, modified := c.COWPRuns(delIfFinished, nil); modified {
   139  			return &prjpb.Component{
   140  				Pruns:          pruns,
   141  				Clids:          c.GetClids(),
   142  				DecisionTime:   c.GetDecisionTime(),
   143  				TriageRequired: true,
   144  			}
   145  		}
   146  		return c
   147  	}
   148  
   149  	s.PB.CreatedPruns, _ = s.PB.COWCreatedRuns(delIfFinished, nil)
   150  	stillTrackedRuns := len(s.PB.GetCreatedPruns())
   151  	var modified bool
   152  	s.PB.Components, modified = s.PB.COWComponents(func(c *prjpb.Component) *prjpb.Component {
   153  		c = removeFromComponent(c)
   154  		stillTrackedRuns += len(c.GetPruns())
   155  		return c
   156  	}, nil)
   157  	if modified {
   158  		// Removing usually changes components and/or pruning of PCLs.
   159  		s.PB.RepartitionRequired = true
   160  	}
   161  	return stillTrackedRuns
   162  }
   163  
   164  func incompleteRuns(ctx context.Context, ids map[common.RunID]struct{}) (common.RunIDs, error) {
   165  	runs, errs, err := loadRuns(ctx, ids)
   166  	if err != nil {
   167  		return nil, err
   168  	}
   169  	var incomplete common.RunIDs
   170  	for i, r := range runs {
   171  		switch {
   172  		case errs[i] != nil:
   173  			return nil, errors.Annotate(errs[i], "failed to load Run %s", r.ID).Tag(transient.Tag).Err()
   174  		case !run.IsEnded(r.Status):
   175  			incomplete = append(incomplete, r.ID)
   176  		}
   177  	}
   178  	return incomplete, err
   179  }
   180  
   181  // loadRuns loads Runs from Datastore corresponding to given Run IDs.
   182  //
   183  // Returns slice of Runs, error.MultiError slice corresponding to
   184  // per-Run errors *(always exists and has same length as Runs)*, and a
   185  // top level error if it can't be attributed to any Run.
   186  //
   187  // *each error in per-Run errors* is not annotated and is nil if Run was loaded
   188  // successfully.
   189  func loadRuns(ctx context.Context, ids map[common.RunID]struct{}) ([]*run.Run, errors.MultiError, error) {
   190  	runs := make([]*run.Run, 0, len(ids))
   191  	for id := range ids {
   192  		runs = append(runs, &run.Run{ID: id})
   193  	}
   194  	err := datastore.Get(ctx, runs)
   195  	switch merr, ok := err.(errors.MultiError); {
   196  	case err == nil:
   197  		return runs, make(errors.MultiError, len(runs)), nil
   198  	case ok:
   199  		return runs, merr, nil
   200  	default:
   201  		return nil, nil, errors.Annotate(err, "failed to load %d Runs", len(runs)).Tag(transient.Tag).Err()
   202  	}
   203  }
   204  
   205  // maybeMCERun returns whether a given Run could be part of MCE Run.
   206  // That is, whether the Run was triggered by chained CQ votes.
   207  func maybeMCERun(ctx context.Context, s *State, r *prjpb.PRun) bool {
   208  	if run.Mode(r.GetMode()) != run.FullRun {
   209  		return false
   210  	}
   211  	pcl := s.PB.GetPCL(r.GetClids()[0])
   212  	if pcl == nil {
   213  		return false
   214  	}
   215  	switch idxs := pcl.GetConfigGroupIndexes(); {
   216  	case len(idxs) != 1:
   217  		// In case of misconfiguration, PCL may be involved with 0 or 2+
   218  		// applicable ConfigGroups, and evalcl uses an empty ConfigGroup
   219  		// to remove such CLs.
   220  		//
   221  		// Such PCL should not be a valid MCE run.
   222  		return false
   223  	case len(s.configGroups) == 0:
   224  		// This may be a bug in CV, but ok. It is not CombineCl.
   225  		return true
   226  	case s.configGroups[idxs[0]].Content.GetCombineCls() != nil:
   227  		return false
   228  	}
   229  	return true
   230  }