blob: f990dd54481a2607cfe7f6fb37cef5c105246fde [file] [log] [blame]
// Copyright 2021 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package util
import (
"context"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"go.chromium.org/luci/common/clock"
"go.chromium.org/luci/common/errors"
gerritpb "go.chromium.org/luci/common/proto/gerrit"
"go.chromium.org/luci/common/retry/transient"
"go.chromium.org/luci/gae/service/datastore"
"go.chromium.org/luci/grpc/grpcutil"
"go.chromium.org/luci/cv/internal/changelist"
"go.chromium.org/luci/cv/internal/common"
"go.chromium.org/luci/cv/internal/common/lease"
"go.chromium.org/luci/cv/internal/gerrit"
"go.chromium.org/luci/cv/internal/run"
)
// StaleCLAgeThreshold is the window that CL entity in datastore should be
// considered latest if refreshed within the threshold.
//
// Large values increase the chance of returning false result on stale Change
// info while low values increase load on Gerrit.
const StaleCLAgeThreshold = 10 * time.Second
// ActionTakeEvalFn is the function signature to evaluate whether certain
// action has already been taken on the given Gerrit Change.
//
// Returns the time when action is taken. Otherwise, returns zero time.
type ActionTakeEvalFn func(rcl *run.RunCL, ci *gerritpb.ChangeInfo) time.Time
// IsActionTakenOnGerritCL checks whether an action specified by `evalFn` has
// been take on a Gerrit CL.
//
// Checks against CV's own cache (CL entity in Datastore) first. If the action
// is not taken and cache is too old (before `now-StaleCLAgeThreshold``),
// then fetch the latest change info from Gerrit and check.
func IsActionTakenOnGerritCL(ctx context.Context, gf gerrit.Factory, rcl *run.RunCL, gerritQueryOpts []gerritpb.QueryOption, evalFn ActionTakeEvalFn) (time.Time, error) {
cl := changelist.CL{ID: rcl.ID}
switch err := datastore.Get(ctx, &cl); {
case err == datastore.ErrNoSuchEntity:
return time.Time{}, errors.Annotate(err, "CL no longer exists").Err()
case err != nil:
return time.Time{}, errors.Annotate(err, "failed to load CL").Tag(transient.Tag).Err()
}
switch actionTime := evalFn(rcl, cl.Snapshot.GetGerrit().GetInfo()); {
case !actionTime.IsZero():
return actionTime, nil
case clock.Since(ctx, cl.Snapshot.GetExternalUpdateTime().AsTime()) < StaleCLAgeThreshold:
// Accept possibility of duplicate messages within the staleCLAgeThreshold.
return time.Time{}, nil
}
// Fetch the latest CL details from Gerrit.
luciProject := common.RunID(rcl.Run.StringID()).LUCIProject()
gc, err := gf.MakeClient(ctx, rcl.Detail.GetGerrit().GetHost(), luciProject)
if err != nil {
return time.Time{}, err
}
req := &gerritpb.GetChangeRequest{
Project: rcl.Detail.GetGerrit().GetInfo().GetProject(),
Number: rcl.Detail.GetGerrit().GetInfo().GetNumber(),
Options: gerritQueryOpts,
}
var ci *gerritpb.ChangeInfo
outerErr := gf.MakeMirrorIterator(ctx).RetryIfStale(func(opt grpc.CallOption) error {
ci, err = gc.GetChange(ctx, req, opt)
switch grpcutil.Code(err) {
case codes.OK:
return nil
case codes.PermissionDenied:
// This is permanent error which shouldn't be retried.
return err
case codes.NotFound:
return gerrit.ErrStaleData
default:
err = gerrit.UnhandledError(ctx, err, "Gerrit.GetChange")
return err
}
})
switch {
case err != nil:
return time.Time{}, errors.Annotate(err, "failed to get the latest Gerrit ChangeInfo").Err()
case outerErr != nil:
// Shouldn't happen, unless Mirror iterate itself errors out for some
// reason.
return time.Time{}, outerErr
default:
return evalFn(rcl, ci), nil
}
}
// MutateGerritCL calls SetReview on the given Gerrit CL.
//
// Uses mirror iterator and leases the CL before making the Gerrit call.
func MutateGerritCL(ctx context.Context, gf gerrit.Factory, rcl *run.RunCL, req *gerritpb.SetReviewRequest, leaseDuration time.Duration, motivation string) error {
luciProject := common.RunID(rcl.Run.StringID()).LUCIProject()
gc, err := gf.MakeClient(ctx, rcl.Detail.GetGerrit().GetHost(), luciProject)
if err != nil {
return err
}
ctx, cancelLease, err := lease.ApplyOnCL(ctx, rcl.ID, leaseDuration, motivation)
if err != nil {
return err
}
defer cancelLease()
outerErr := gf.MakeMirrorIterator(ctx).RetryIfStale(func(opt grpc.CallOption) error {
_, err = gc.SetReview(ctx, req, opt)
switch grpcutil.Code(err) {
case codes.OK:
return nil
case codes.PermissionDenied:
// This is a permanent error which shouldn't be retried.
return err
case codes.NotFound:
// This is known to happen on new CLs or on recently created revisions.
return gerrit.ErrStaleData
default:
err = gerrit.UnhandledError(ctx, err, "Gerrit.SetReview")
return err
}
})
switch {
case err != nil:
return errors.Annotate(err, "failed to call Gerrit.SetReview").Err()
case outerErr != nil:
// Shouldn't happen, unless MirrorIterator itself errors out for some
// reason.
return outerErr
default:
return nil
}
}