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 }