go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/prjmanager/state/categorize_cls.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  	"fmt"
    20  	"time"
    21  
    22  	"go.chromium.org/luci/common/clock"
    23  
    24  	"go.chromium.org/luci/cv/internal/changelist"
    25  	"go.chromium.org/luci/cv/internal/common"
    26  	"go.chromium.org/luci/cv/internal/prjmanager/prjpb"
    27  )
    28  
    29  type categorizedCLs struct {
    30  	// active CLs must remain in PCLs.
    31  	//
    32  	// A CL is active iff either:
    33  	//   * it has non-nil .Trigger within `common.MaxTriggerAge` and is watched by
    34  	//     this LUCI project (see `isActiveStandalonePCL`);
    35  	//   * OR it belongs to an incomplete Run.
    36  	//     NOTE: In this case, CL may be no longer watched by this project or even
    37  	//     be with status=DELETED. Such state may temporary arise due to changes
    38  	//     in project's config.  Eventually Run Manager will cancel the Run,
    39  	//     resulting in removal of the Run from the PM's State and hence removal
    40  	//     of the CL from the active set.
    41  	active common.CLIDsSet
    42  	// deps CLs are non-active CLs which should be tracked in PCLs because they
    43  	// are deps of active CLs.
    44  	//
    45  	// Similar to active CLs of incomplete Run, these CLs may not even be watched
    46  	// by this project or be with status=DELETED, but such state should be
    47  	// temporary.
    48  	deps common.CLIDsSet
    49  	// unused CLs are CLs in PCLs that are neither active nor deps and should be
    50  	// deleted from PCLs.
    51  	unused common.CLIDsSet
    52  	// unloaded are CLs that are either active or deps but not present in PCLs.
    53  	//
    54  	// NOTE: if this is not empty, it means `deps` and `unused` aren't yet final
    55  	// and may be changed after the `unloaded` CLs are loaded.
    56  	unloaded common.CLIDsSet
    57  }
    58  
    59  // isActiveStandalonePCL returns true if PCL is active on its own.
    60  //
    61  // See categorizedCLs.active spec.
    62  func isActiveStandalonePCL(pcl *prjpb.PCL, now time.Time) bool {
    63  	if pcl.GetStatus() != prjpb.PCL_OK {
    64  		return false
    65  	}
    66  
    67  	cutoff := now.Add(-common.MaxTriggerAge)
    68  	switch {
    69  	case pcl.GetTriggers().GetCqVoteTrigger().GetTime().AsTime().After(cutoff):
    70  	case pcl.GetTriggers().GetNewPatchsetRunTrigger().GetTime().AsTime().After(cutoff):
    71  	default:
    72  		return false
    73  	}
    74  	return true
    75  }
    76  
    77  // categorizeCLs computes categorizedCLs based on current State.
    78  //
    79  // The resulting categorizeCLs spans not just PCLs, but also CreatedRuns, since
    80  // newly created Runs may reference CLs not yet tracked in PCLs.
    81  func (s *State) categorizeCLs(ctx context.Context) *categorizedCLs {
    82  	s.ensurePCLIndex()
    83  	now := clock.Now(ctx)
    84  
    85  	// reduce typing and redundant !=nil check in GetPcls().
    86  	pcls := s.PB.GetPcls()
    87  
    88  	res := &categorizedCLs{
    89  		// Allocate the maps guessing initial size:
    90  		//  * most PCLs must be active, with very few being pure deps or unused,
    91  		//  * unloaded come from CreatedRuns, assume 2 CL per Run.
    92  		active:   make(common.CLIDsSet, len(pcls)),
    93  		deps:     make(common.CLIDsSet, 16),
    94  		unused:   make(common.CLIDsSet, 16),
    95  		unloaded: make(common.CLIDsSet, len(s.PB.GetCreatedPruns())*2),
    96  	}
    97  
    98  	// First, compute all active CLs and if any of them are unloaded.
    99  	for _, r := range s.PB.GetCreatedPruns() {
   100  		for _, id := range r.GetClids() {
   101  			res.active.AddI64(id)
   102  			if !s.pclIndex.hasI64(id) {
   103  				res.unloaded.AddI64(id)
   104  			}
   105  		}
   106  	}
   107  	for _, c := range s.PB.GetComponents() {
   108  		for _, r := range c.GetPruns() {
   109  			for _, id := range r.GetClids() {
   110  				res.active.AddI64(id)
   111  			}
   112  		}
   113  	}
   114  	for _, pcl := range pcls {
   115  		id := pcl.GetClid()
   116  		if isActiveStandalonePCL(pcl, now) {
   117  			res.active.AddI64(id)
   118  		}
   119  	}
   120  
   121  	// Second, compute `deps` and if any of them are unloaded.
   122  	for _, pcl := range pcls {
   123  		if res.active.HasI64(pcl.GetClid()) {
   124  			for _, dep := range pcl.GetDeps() {
   125  				id := dep.GetClid()
   126  				if !res.active.HasI64(id) {
   127  					res.deps.AddI64(id)
   128  					if !s.pclIndex.hasI64(id) {
   129  						res.unloaded.AddI64(id)
   130  					}
   131  				}
   132  			}
   133  		}
   134  	}
   135  	// Third, compute `unused` as all unreferenced CLs in PCLs.
   136  	for _, pcl := range s.PB.GetPcls() {
   137  		id := pcl.GetClid()
   138  		if !res.active.HasI64(id) && !res.deps.HasI64(id) {
   139  			res.unused.AddI64(id)
   140  		}
   141  	}
   142  	return res
   143  }
   144  
   145  // loadActiveIntoPCLs ensures PCLs contain all active CLs and modifies
   146  // categorizedCLs accordingly.
   147  //
   148  // Doesn't guarantee that all their deps are loaded.
   149  func (s *State) loadActiveIntoPCLs(ctx context.Context, cat *categorizedCLs) error {
   150  	// Consider a long *chain* of dependent CLs each with CQ+1 vote:
   151  	//    A <- B  (B depends on A)
   152  	//    B <- C
   153  	//    ...
   154  	//    Y <- Z
   155  	// Suppose CV first notices just Z, s.t. CL updater notifies PM with
   156  	// OnCLUpdated event of just Z. Upon receiving which, PM will add Z(deps: Y)
   157  	// into PCL, and then calls this function.
   158  	// Even if at this point Datastore contains all {Y..A} CLs, it's unreasonable
   159  	// to load them all in this function because it'd require O(len(chain)) of
   160  	// RPCs to Datastore, whereby each (% last one) detects yet another dep.
   161  	// Thus, no guarantee to load deps is provided.
   162  	//
   163  	// Normally, the next PM invocation to receive remaining {Y..A} OnCLUpdated
   164  	// events, which would allow to load them all in 1 Datastore GetMulti.
   165  	//
   166  	// Also, such CL chains are rare; most frequently CV deals with long CL stacks,
   167  	// where the latest CL depends on most others, e.g. Z{deps: A..Y}. So, loading
   168  	// all already known deps is beneficial.
   169  	// Furthermore, we must load not yet known CLs which are referenced in
   170  	// CreatedRuns. The categorizeCLs() bundles both missing active and deps into
   171  	// unloaded CLs.
   172  	for len(cat.unloaded) == 0 {
   173  		return nil
   174  	}
   175  	if err := s.loadUnloadedCLsOnce(ctx, cat); err != nil {
   176  		return err
   177  	}
   178  	for u := range cat.unloaded {
   179  		if cat.active.Has(u) {
   180  			panic(fmt.Errorf("%d CL is not loaded but active", u))
   181  		}
   182  	}
   183  	return nil
   184  }
   185  
   186  // loadUnloadedCLsOnce loads `unloaded` CLs from Datastore, updates PCLs and
   187  // categorizedCLs.
   188  //
   189  // If any previously `unloaded` CLs were or turned out to be active,
   190  // then their deps may end up in `unloaded`.
   191  func (s *State) loadUnloadedCLsOnce(ctx context.Context, cat *categorizedCLs) error {
   192  	cls := make([]*changelist.CL, 0, len(cat.unloaded))
   193  	for clid := range cat.unloaded {
   194  		cls = append(cls, &changelist.CL{ID: clid})
   195  	}
   196  	if err := s.evalCLsFromDS(ctx, cls); err != nil {
   197  		return err
   198  	}
   199  	now := clock.Now(ctx)
   200  	// This is inefficient, since this could have been done only for loaded CLs.
   201  	// Consider adding callback to the evalCLsFromDS.
   202  	for _, pcl := range s.PB.GetPcls() {
   203  		id := pcl.GetClid()
   204  		if !cat.unloaded.HasI64(id) {
   205  			continue
   206  		}
   207  		cat.unloaded.DelI64(id)
   208  		if !cat.active.HasI64(id) {
   209  			// CL was a mere dep before, but its details weren't known.
   210  			if !isActiveStandalonePCL(pcl, now) {
   211  				continue // pcl was and remains just a dep.
   212  			} else {
   213  				// Promote CL to active.
   214  				cat.deps.DelI64(id)
   215  				cat.active.AddI64(id)
   216  			}
   217  		}
   218  		// Recurse into deps of a just loaded active CLs.
   219  		for _, dep := range pcl.GetDeps() {
   220  			id := dep.GetClid()
   221  			if !cat.active.HasI64(id) && !cat.deps.HasI64(id) {
   222  				cat.deps.AddI64(id)
   223  				if cat.unused.HasI64(id) {
   224  					cat.unused.DelI64(id)
   225  				} else {
   226  					cat.unloaded.AddI64(id)
   227  				}
   228  			}
   229  		}
   230  	}
   231  	return nil
   232  }