go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/run/impl/util/gerrit.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 util
    16  
    17  import (
    18  	"context"
    19  	"time"
    20  
    21  	"google.golang.org/grpc"
    22  	"google.golang.org/grpc/codes"
    23  
    24  	"go.chromium.org/luci/common/clock"
    25  	"go.chromium.org/luci/common/errors"
    26  	gerritpb "go.chromium.org/luci/common/proto/gerrit"
    27  	"go.chromium.org/luci/common/retry/transient"
    28  	"go.chromium.org/luci/gae/service/datastore"
    29  	"go.chromium.org/luci/grpc/grpcutil"
    30  
    31  	"go.chromium.org/luci/cv/internal/changelist"
    32  	"go.chromium.org/luci/cv/internal/common"
    33  	"go.chromium.org/luci/cv/internal/common/lease"
    34  	"go.chromium.org/luci/cv/internal/gerrit"
    35  	"go.chromium.org/luci/cv/internal/run"
    36  )
    37  
    38  // StaleCLAgeThreshold is the window that CL entity in datastore should be
    39  // considered latest if refreshed within the threshold.
    40  //
    41  // Large values increase the chance of returning false result on stale Change
    42  // info while low values increase load on Gerrit.
    43  const StaleCLAgeThreshold = 10 * time.Second
    44  
    45  // ActionTakeEvalFn is the function signature to evaluate whether certain
    46  // action has already been taken on the given Gerrit Change.
    47  //
    48  // Returns the time when action is taken. Otherwise, returns zero time.
    49  type ActionTakeEvalFn func(rcl *run.RunCL, ci *gerritpb.ChangeInfo) time.Time
    50  
    51  // IsActionTakenOnGerritCL checks whether an action specified by `evalFn` has
    52  // been take on a Gerrit CL.
    53  //
    54  // Checks against CV's own cache (CL entity in Datastore) first. If the action
    55  // is not taken and cache is too old (before `now-StaleCLAgeThreshold“),
    56  // then fetch the latest change info from Gerrit and check.
    57  func IsActionTakenOnGerritCL(ctx context.Context, gf gerrit.Factory, rcl *run.RunCL, gerritQueryOpts []gerritpb.QueryOption, evalFn ActionTakeEvalFn) (time.Time, error) {
    58  	cl := changelist.CL{ID: rcl.ID}
    59  	switch err := datastore.Get(ctx, &cl); {
    60  	case err == datastore.ErrNoSuchEntity:
    61  		return time.Time{}, errors.Annotate(err, "CL no longer exists").Err()
    62  	case err != nil:
    63  		return time.Time{}, errors.Annotate(err, "failed to load CL").Tag(transient.Tag).Err()
    64  	}
    65  
    66  	switch actionTime := evalFn(rcl, cl.Snapshot.GetGerrit().GetInfo()); {
    67  	case !actionTime.IsZero():
    68  		return actionTime, nil
    69  	case clock.Since(ctx, cl.Snapshot.GetExternalUpdateTime().AsTime()) < StaleCLAgeThreshold:
    70  		// Accept possibility of duplicate messages within the staleCLAgeThreshold.
    71  		return time.Time{}, nil
    72  	}
    73  
    74  	// Fetch the latest CL details from Gerrit.
    75  	luciProject := common.RunID(rcl.Run.StringID()).LUCIProject()
    76  	gc, err := gf.MakeClient(ctx, rcl.Detail.GetGerrit().GetHost(), luciProject)
    77  	if err != nil {
    78  		return time.Time{}, err
    79  	}
    80  
    81  	req := &gerritpb.GetChangeRequest{
    82  		Project: rcl.Detail.GetGerrit().GetInfo().GetProject(),
    83  		Number:  rcl.Detail.GetGerrit().GetInfo().GetNumber(),
    84  		Options: gerritQueryOpts,
    85  	}
    86  	var ci *gerritpb.ChangeInfo
    87  	outerErr := gf.MakeMirrorIterator(ctx).RetryIfStale(func(opt grpc.CallOption) error {
    88  		ci, err = gc.GetChange(ctx, req, opt)
    89  		switch grpcutil.Code(err) {
    90  		case codes.OK:
    91  			return nil
    92  		case codes.PermissionDenied:
    93  			// This is permanent error which shouldn't be retried.
    94  			return err
    95  		case codes.NotFound:
    96  			return gerrit.ErrStaleData
    97  		default:
    98  			err = gerrit.UnhandledError(ctx, err, "Gerrit.GetChange")
    99  			return err
   100  		}
   101  	})
   102  	switch {
   103  	case err != nil:
   104  		return time.Time{}, errors.Annotate(err, "failed to get the latest Gerrit ChangeInfo").Err()
   105  	case outerErr != nil:
   106  		// Shouldn't happen, unless Mirror iterate itself errors out for some
   107  		// reason.
   108  		return time.Time{}, outerErr
   109  	default:
   110  		return evalFn(rcl, ci), nil
   111  	}
   112  }
   113  
   114  // MutateGerritCL calls SetReview on the given Gerrit CL.
   115  //
   116  // Uses mirror iterator and leases the CL before making the Gerrit call.
   117  func MutateGerritCL(ctx context.Context, gf gerrit.Factory, rcl *run.RunCL, req *gerritpb.SetReviewRequest, leaseDuration time.Duration, motivation string) error {
   118  	luciProject := common.RunID(rcl.Run.StringID()).LUCIProject()
   119  	gc, err := gf.MakeClient(ctx, rcl.Detail.GetGerrit().GetHost(), luciProject)
   120  	if err != nil {
   121  		return err
   122  	}
   123  
   124  	ctx, cancelLease, err := lease.ApplyOnCL(ctx, rcl.ID, leaseDuration, motivation)
   125  	if err != nil {
   126  		return err
   127  	}
   128  	defer cancelLease()
   129  
   130  	outerErr := gf.MakeMirrorIterator(ctx).RetryIfStale(func(opt grpc.CallOption) error {
   131  		_, err = gc.SetReview(ctx, req, opt)
   132  		switch grpcutil.Code(err) {
   133  		case codes.OK:
   134  			return nil
   135  		case codes.PermissionDenied:
   136  			// This is a permanent error which shouldn't be retried.
   137  			return err
   138  		case codes.NotFound:
   139  			// This is known to happen on new CLs or on recently created revisions.
   140  			return gerrit.ErrStaleData
   141  		case codes.FailedPrecondition:
   142  			// SetReview() returns FailedPrecondition, if the CL is abandoned.
   143  			return err
   144  		default:
   145  			err = gerrit.UnhandledError(ctx, err, "Gerrit.SetReview")
   146  			return err
   147  		}
   148  	})
   149  	switch {
   150  	case err != nil:
   151  		return errors.Annotate(err, "failed to call Gerrit.SetReview").Err()
   152  	case outerErr != nil:
   153  		// Shouldn't happen, unless MirrorIterator itself errors out for some
   154  		// reason.
   155  		return outerErr
   156  	default:
   157  		return nil
   158  	}
   159  }