go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/run/impl/handler/cl_update.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 handler
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"sort"
    21  	"time"
    22  
    23  	"go.chromium.org/luci/common/clock"
    24  	"go.chromium.org/luci/common/errors"
    25  	"go.chromium.org/luci/common/logging"
    26  
    27  	"go.chromium.org/luci/cv/internal/changelist"
    28  	"go.chromium.org/luci/cv/internal/common"
    29  	"go.chromium.org/luci/cv/internal/configs/prjcfg"
    30  	"go.chromium.org/luci/cv/internal/gerrit/trigger"
    31  	"go.chromium.org/luci/cv/internal/run"
    32  	"go.chromium.org/luci/cv/internal/run/impl/state"
    33  )
    34  
    35  // OnCLsUpdated implements Handler interface.
    36  func (impl *Impl) OnCLsUpdated(ctx context.Context, rs *state.RunState, clids common.CLIDs) (*Result, error) {
    37  	switch status := rs.Status; {
    38  	case status == run.Status_STATUS_UNSPECIFIED:
    39  		err := errors.Reason("CRITICAL: Received CLUpdated events but Run is in unspecified status").Err()
    40  		common.LogError(ctx, err)
    41  		panic(err)
    42  	case status == run.Status_SUBMITTING:
    43  		return &Result{State: rs, PreserveEvents: true}, nil
    44  	case isCurrentlyResettingTriggers(rs):
    45  		// It's likely CL is updated when resetting the triggers, defer the process
    46  		// of CLsUpdated event till resetting the trigger is done.
    47  		return &Result{State: rs, PreserveEvents: true}, nil
    48  	case run.IsEnded(status):
    49  		logging.Debugf(ctx, "skipping OnCLUpdated because Run is %s", status)
    50  		return &Result{State: rs}, nil
    51  	}
    52  	clids.Dedupe()
    53  
    54  	cg, runCLs, cls, err := loadCLsAndConfig(ctx, rs, clids)
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  
    59  	hasNilSnapshot := false
    60  	var earliestReconsiderAt time.Time
    61  	clsCausingCancellation := make(common.CLIDsSet, len(clids))
    62  	cancellationReasons := make([]string, 0, len(clids))
    63  	for i, clid := range clids {
    64  		cl, runCL := cls[i], runCLs[i]
    65  		if cl.Snapshot == nil {
    66  			// This doesn't necessarily assume that shouldCancel() would
    67  			// or would not return a cancelReason for nil Snapshot; hence,
    68  			// let it decide. hasNilSnapshot is to decide whether
    69  			// runs.CheckRunCreate() should be checked or not.
    70  			hasNilSnapshot = true
    71  		}
    72  		switch reconsiderAt, cancellationReason := shouldCancel(ctx, cl, runCL, cg); {
    73  		case !reconsiderAt.IsZero():
    74  			if earliestReconsiderAt.IsZero() || earliestReconsiderAt.After(reconsiderAt) {
    75  				earliestReconsiderAt = reconsiderAt
    76  			}
    77  		case cancellationReason != "":
    78  			clsCausingCancellation.Add(clid)
    79  			cancellationReasons = append(cancellationReasons, cancellationReason)
    80  		}
    81  	}
    82  
    83  	sort.Strings(cancellationReasons)
    84  	switch numCLsCausingCancellation := len(clsCausingCancellation); {
    85  	// If all CLs in this Run have been updated at the same time and cause
    86  	// Run cancellation, directly cancel the Run. Otherwise, if part of the
    87  	// CLs cause Run cancellation, LUCI CV would need to reset the trigger on
    88  	// the rest of the CLs in this Run so that the existing trigger won't
    89  	// end up with creating new Run that developers are not intended. For example
    90  	// imagine a stack of CL A, B, C where C depends on B depends on A and a
    91  	// Dry Run is currently running involving all CLs. If B gains a new patchset,
    92  	// CV needs to reset the trigger on A and C and then patiently wait for
    93  	// developers creating a new Run with all 3 CLs.
    94  	case numCLsCausingCancellation == len(rs.CLs):
    95  		return impl.Cancel(ctx, rs, cancellationReasons)
    96  	case rs.HasRootCL() && clsCausingCancellation.Has(rs.RootCL):
    97  		// if the root cl has caused the cancellation, there's no need to reset
    98  		// the trigger for all other CLs because they don't have trigger at all.
    99  		// Directly cancelling the run would be enough.
   100  		return impl.Cancel(ctx, rs, cancellationReasons)
   101  	case numCLsCausingCancellation > 0:
   102  		rs = rs.ShallowCopy()
   103  		meta := reviewInputMeta{
   104  			// Just pick the very first reason as the reason to reset the trigger.
   105  			// In typical cases, there will only be one cancellation reason anyway.
   106  			message: fmt.Sprintf("Reset the trigger of this CL because %s", cancellationReasons[0]),
   107  			notify:  rs.Mode.GerritNotifyTargets(),
   108  		}
   109  		metas := make(map[common.CLID]reviewInputMeta, len(rs.CLs)-numCLsCausingCancellation)
   110  		if rs.HasRootCL() {
   111  			metas[rs.RootCL] = meta
   112  		} else {
   113  			for _, clid := range rs.CLs {
   114  				if !clsCausingCancellation.Has(clid) {
   115  					metas[clid] = meta
   116  				}
   117  			}
   118  		}
   119  		scheduleTriggersReset(ctx, rs, metas, run.Status_CANCELLED)
   120  		// TODO(yiwzhang): It is unfortunate that CV has to set the cancellation
   121  		// reason before triggers are reset and status is turned to CANCELLED. Find
   122  		// a way to pass the cancellation reasons to `onCompletedResetTriggers`.
   123  		rs.CancellationReasons = append(rs.CancellationReasons, cancellationReasons...)
   124  		sort.Strings(rs.CancellationReasons)
   125  		return &Result{State: rs}, nil
   126  	case !earliestReconsiderAt.IsZero():
   127  		logging.Debugf(ctx, "Will reconsider OnCLUpdated event(s) after %s", earliestReconsiderAt.Sub(clock.Now(ctx)))
   128  		if err := impl.RM.Invoke(ctx, rs.ID, earliestReconsiderAt); err != nil {
   129  			return nil, err
   130  		}
   131  		return &Result{State: rs, PreserveEvents: true}, nil
   132  	}
   133  
   134  	// If any of the CLs has a nil snapshot, skip acls.CheckRunCreate().
   135  	// It needs snapshots for the entire CL set.
   136  	if hasNilSnapshot {
   137  		return &Result{State: rs}, nil
   138  	}
   139  
   140  	// Check the Run creation, in case the Run is no longer valid
   141  	// with the newly updated CL info.
   142  	rs = rs.ShallowCopy()
   143  	allRunCLs, allCLs := runCLs, cls
   144  	remainingCLIDs := rs.CLs.Set()
   145  	remainingCLIDs.DelAll(clids)
   146  	if len(remainingCLIDs) > 0 {
   147  		remainingRunCLs, remainingCLs, err := loadRunCLsAndCLs(ctx, rs.ID, remainingCLIDs.ToCLIDs())
   148  		if err != nil {
   149  			return nil, err
   150  		}
   151  		allRunCLs = append(allRunCLs, remainingRunCLs...)
   152  		allCLs = append(allCLs, remainingCLs...)
   153  	}
   154  	if _, err := checkRunCreate(ctx, rs, cg, allRunCLs, allCLs); err != nil {
   155  		return nil, err
   156  	}
   157  	return &Result{State: rs}, nil
   158  }
   159  
   160  func shouldCancel(ctx context.Context, cl *changelist.CL, rcl *run.RunCL, cg *prjcfg.ConfigGroup) (time.Time, string) {
   161  	project := cg.ProjectString()
   162  	clString := fmt.Sprintf("CL %d %s", cl.ID, cl.ExternalID)
   163  	switch kind, reason := cl.AccessKindWithReason(ctx, project); kind {
   164  	case changelist.AccessDenied:
   165  		logging.Warningf(ctx, "No longer have access to %s: %s", clString, reason)
   166  		return time.Time{}, fmt.Sprintf("no longer have access to %s: %s", cl.ExternalID.MustURL(), reason)
   167  	case changelist.AccessDeniedProbably:
   168  		logging.Warningf(ctx, "Probably no longer have access to %s (%s), not canceling yet", clString, reason)
   169  		// Keep the run Running for now. The access should become either
   170  		// AccessGranted or AccessDenied, eventually.
   171  		return cl.Access.GetByProject()[project].GetNoAccessTime().AsTime(), ""
   172  	case changelist.AccessUnknown:
   173  		logging.Errorf(ctx, "Unknown access to %s (%s), not canceling yet", clString, reason)
   174  		// Keep the run Running for now, it should become clear eventually.
   175  		return time.Time{}, ""
   176  	case changelist.AccessGranted:
   177  		// The expected and most likely case.
   178  	default:
   179  		panic(fmt.Errorf("unknown AccessKind %d in %s", kind, clString))
   180  	}
   181  
   182  	if o, c := rcl.Detail.GetPatchset(), cl.Snapshot.GetPatchset(); o != c {
   183  		logging.Infof(ctx, "%s has new patchset %d => %d", clString, o, c)
   184  		return time.Time{}, fmt.Sprintf("the patchset of %s has changed from %d to %d", cl.ExternalID.MustURL(), o, c)
   185  	}
   186  	if o, c := rcl.Detail.GetGerrit().GetInfo().GetRef(), cl.Snapshot.GetGerrit().GetInfo().GetRef(); o != c {
   187  		logging.Warningf(ctx, "%s has new ref %q => %q", clString, o, c)
   188  		return time.Time{}, fmt.Sprintf("the ref of %s has moved from %s to %s", cl.ExternalID.MustURL(), o, c)
   189  	}
   190  	if rcl.Trigger != nil {
   191  		o, c := rcl.Trigger, trigger.Find(&trigger.FindInput{
   192  			ChangeInfo:                   cl.Snapshot.GetGerrit().GetInfo(),
   193  			ConfigGroup:                  cg.Content,
   194  			TriggerNewPatchsetRunAfterPS: cl.TriggerNewPatchsetRunAfterPS,
   195  		})
   196  		if whatChanged := run.HasTriggerChanged(o, c, cl.ExternalID.MustURL()); whatChanged != "" {
   197  			logging.Infof(ctx, "%s has new trigger\nOLD: %s\nNEW: %s", clString, o, c)
   198  			return time.Time{}, whatChanged
   199  		}
   200  	}
   201  	return time.Time{}, ""
   202  }