go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/prjmanager/cltriggerer/trigger_dep_op.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 cltriggerer
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"sync/atomic"
    21  	"time"
    22  
    23  	"google.golang.org/grpc"
    24  	"google.golang.org/grpc/codes"
    25  
    26  	"go.chromium.org/luci/common/errors"
    27  	"go.chromium.org/luci/common/logging"
    28  	gerritpb "go.chromium.org/luci/common/proto/gerrit"
    29  	"go.chromium.org/luci/common/retry/transient"
    30  	"go.chromium.org/luci/grpc/grpcutil"
    31  
    32  	"go.chromium.org/luci/cv/internal/changelist"
    33  	"go.chromium.org/luci/cv/internal/common"
    34  	"go.chromium.org/luci/cv/internal/common/lease"
    35  	"go.chromium.org/luci/cv/internal/gerrit"
    36  	"go.chromium.org/luci/cv/internal/gerrit/trigger"
    37  	"go.chromium.org/luci/cv/internal/prjmanager/prjpb"
    38  	"go.chromium.org/luci/cv/internal/run"
    39  )
    40  
    41  type triggerDepResult struct {
    42  	voteDone bool
    43  	lastErr  error
    44  }
    45  
    46  type triggerDepOp struct {
    47  	depCLID     int64
    48  	originCLURL string
    49  	trigger     *run.Trigger
    50  
    51  	result     triggerDepResult
    52  	isCanceled *atomic.Bool
    53  }
    54  
    55  func makeTriggerDepOps(originCLURL string, payload *prjpb.TriggeringCLDeps, isCanceled *atomic.Bool) []*triggerDepOp {
    56  	deps := payload.GetDepClids()
    57  	if len(deps) == 0 {
    58  		return nil
    59  	}
    60  	ret := make([]*triggerDepOp, len(deps))
    61  	for i, dep := range deps {
    62  		ret[i] = &triggerDepOp{
    63  			depCLID:     dep,
    64  			originCLURL: originCLURL,
    65  			trigger:     payload.GetTrigger(),
    66  			isCanceled:  isCanceled,
    67  		}
    68  	}
    69  	return ret
    70  }
    71  
    72  func (op *triggerDepOp) isSucceeded() bool {
    73  	return op != nil && op.result.voteDone
    74  }
    75  
    76  func (op *triggerDepOp) isPermanentlyFailed() bool {
    77  	if op.isSucceeded() {
    78  		return false
    79  	}
    80  	switch err := errors.Unwrap(op.result.lastErr); err {
    81  	case nil, context.Canceled, context.DeadlineExceeded:
    82  		return false
    83  	default:
    84  		return !transient.Tag.In(err)
    85  	}
    86  }
    87  
    88  // getCLError() returns changelist.CLError_TriggerDeps for the permanent error.
    89  //
    90  // Panic if the lastErr is not a permanent error.
    91  func (op *triggerDepOp) getCLError() *changelist.CLError_TriggerDeps {
    92  	if !op.isPermanentlyFailed() {
    93  		panic(fmt.Errorf("FIXME: triggerDepOp.getCLError() called for non-permanent error"))
    94  	}
    95  
    96  	failure := &changelist.CLError_TriggerDeps{}
    97  	switch grpcutil.Code(op.result.lastErr) {
    98  	case codes.OK:
    99  		panic(fmt.Errorf("FIXME: triggerDepOp.result.lastErr with codes.OK"))
   100  	case codes.PermissionDenied:
   101  		failure.PermissionDenied = append(failure.PermissionDenied,
   102  			&changelist.CLError_TriggerDeps_PermissionDenied{
   103  				Clid:  op.depCLID,
   104  				Email: op.trigger.GetEmail(),
   105  			},
   106  		)
   107  	case codes.NotFound:
   108  		failure.NotFound = append(failure.NotFound, op.depCLID)
   109  	default:
   110  		failure.InternalGerritError = append(failure.InternalGerritError, op.depCLID)
   111  	}
   112  	return failure
   113  }
   114  
   115  func isAlreadyVoted(ctx context.Context, depCL *changelist.CL) bool {
   116  	// Skip voting if the CL already have a CQ vote.
   117  	switch mode := findCQTriggerMode(depCL); mode {
   118  	case "":
   119  	case string(run.FullRun):
   120  		logging.Infof(ctx, "the CL is voted already; skip triggering")
   121  		return true
   122  	default:
   123  		logging.Infof(ctx, "the CL is voted for %q; overriding", mode)
   124  	}
   125  	return false
   126  }
   127  
   128  func (op *triggerDepOp) execute(ctx context.Context, gFactory gerrit.Factory, luciPrj string, clm *changelist.Mutator, clu clUpdater) error {
   129  	if op.isCanceled.Load() {
   130  		return nil
   131  	}
   132  	// Lease the CL to prevent other unexpected CL updates, during the vote
   133  	// process.
   134  	ctx, leaseClose, lErr := lease.ApplyOnCL(ctx, common.CLID(op.depCLID), 2*time.Minute, "triggerDepOp")
   135  	if lErr != nil {
   136  		return lErr
   137  	}
   138  	defer leaseClose()
   139  
   140  	depCL := &changelist.CL{ID: common.CLID(op.depCLID)}
   141  	if err := changelist.LoadCLs(ctx, []*changelist.CL{depCL}); err != nil {
   142  		return err
   143  	}
   144  	// if the snapshot has the vote and fresh already, skip other operations.
   145  	if isAlreadyVoted(ctx, depCL) && depCL.Snapshot.GetOutdated() == nil {
   146  		op.result.voteDone = true
   147  		return nil
   148  	}
   149  	if err := op.vote(ctx, gFactory, luciPrj, depCL); err != nil {
   150  		return errors.Annotate(err, "op.vote").Err()
   151  	}
   152  	return errors.Annotate(op.markOutdated(ctx, luciPrj, clm, clu, depCL), "triggerDepOp.markOutdated").Err()
   153  }
   154  
   155  func processGerritErr(ctx context.Context, err error) error {
   156  	switch grpcutil.Code(err) {
   157  	case codes.OK:
   158  		return nil
   159  	case codes.PermissionDenied:
   160  		return err
   161  	case codes.NotFound:
   162  		// This is known to happen on new CLs or on recently created
   163  		// revisions.
   164  		return grpcutil.NotFoundTag.Apply(gerrit.ErrStaleData)
   165  	default:
   166  		return gerrit.UnhandledError(ctx, err, "Gerrit.SetReview")
   167  	}
   168  }
   169  
   170  func (op *triggerDepOp) makeSetReviewRequest(depCL *changelist.CL) *gerritpb.SetReviewRequest {
   171  	mode := op.trigger.GetMode()
   172  	return &gerritpb.SetReviewRequest{
   173  		Project:    depCL.Snapshot.GetGerrit().GetInfo().GetProject(),
   174  		Number:     depCL.Snapshot.GetGerrit().GetInfo().GetNumber(),
   175  		RevisionId: "current",
   176  		Labels: map[string]int32{
   177  			trigger.CQLabelName: trigger.CQVoteByMode(run.Mode(op.trigger.GetMode())),
   178  		},
   179  		// The author will be notified by the run start message, anyways.
   180  		Notify:     gerritpb.Notify_NOTIFY_NONE,
   181  		OnBehalfOf: op.trigger.GetGerritAccountId(),
   182  		Message: fmt.Sprintf(
   183  			"Triggering %s, because %s is triggered on %s, which depends on this CL",
   184  			mode, mode, op.originCLURL),
   185  		Tag: gerrit.Tag("trigger-dep-cl", ""),
   186  	}
   187  }
   188  
   189  func (op *triggerDepOp) vote(ctx context.Context, gFactory gerrit.Factory, luciPrj string, depCL *changelist.CL) error {
   190  	if op.result.voteDone {
   191  		return nil
   192  	}
   193  	gc, err := gFactory.MakeClient(ctx, depCL.Snapshot.GetGerrit().GetHost(), luciPrj)
   194  	if err != nil {
   195  		return errors.Annotate(err, "gFactory.MakeClient").Err()
   196  	}
   197  	voteReq := op.makeSetReviewRequest(depCL)
   198  	vErr := gFactory.MakeMirrorIterator(ctx).RetryIfStale(func(opt grpc.CallOption) error {
   199  		if op.isCanceled.Load() {
   200  			return nil
   201  		}
   202  		_, err := gc.SetReview(ctx, voteReq, opt)
   203  		op.result.lastErr = processGerritErr(ctx, err)
   204  		if op.result.lastErr != nil {
   205  			return op.result.lastErr
   206  		}
   207  		return nil
   208  	})
   209  	op.result.voteDone = vErr == nil
   210  	return vErr
   211  }
   212  
   213  func (op *triggerDepOp) markOutdated(ctx context.Context, luciPrj string, clm *changelist.Mutator, clu clUpdater, depCL *changelist.CL) error {
   214  	var isOutdated bool
   215  	_, err := clm.Update(ctx, luciPrj, depCL.ID, func(cl *changelist.CL) error {
   216  		if cl.Snapshot == nil || cl.Snapshot.GetOutdated() != nil {
   217  			return changelist.ErrStopMutation // noop
   218  		}
   219  		isOutdated = true
   220  		cl.Snapshot.Outdated = &changelist.Snapshot_Outdated{}
   221  		return nil
   222  	})
   223  	switch {
   224  	case err != nil:
   225  		return errors.Annotate(err, "CLMutator.Update").Err()
   226  	case isOutdated:
   227  		// TODO(crbug.com/1284393): use Gerrit's consistency-on-demand.
   228  		return clu.Schedule(ctx, &changelist.UpdateCLTask{
   229  			LuciProject: luciPrj,
   230  			ExternalId:  string(depCL.ExternalID),
   231  			Id:          int64(depCL.ID),
   232  			Requester:   changelist.UpdateCLTask_DEP_CL_TRIGGERER,
   233  		})
   234  	default:
   235  		return nil
   236  	}
   237  }
   238  
   239  func findCQTriggerMode(cl *changelist.CL) string {
   240  	trs := trigger.Find(&trigger.FindInput{
   241  		ChangeInfo: cl.Snapshot.GetGerrit().GetInfo(),
   242  	})
   243  	if trs == nil {
   244  		return ""
   245  	}
   246  	return trs.CqVoteTrigger.GetMode()
   247  }