blob: 1e654fc34c5c95f5ae2851aefdecff7631c5ba87 [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 handler
import (
"context"
"fmt"
"time"
"go.chromium.org/luci/common/clock"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/cv/internal/changelist"
"go.chromium.org/luci/cv/internal/common"
"go.chromium.org/luci/cv/internal/configs/prjcfg"
"go.chromium.org/luci/cv/internal/gerrit/trigger"
"go.chromium.org/luci/cv/internal/run"
"go.chromium.org/luci/cv/internal/run/impl/state"
)
// OnCLsUpdated implements Handler interface.
func (impl *Impl) OnCLsUpdated(ctx context.Context, rs *state.RunState, clids common.CLIDs) (*Result, error) {
switch status := rs.Status; {
case status == run.Status_STATUS_UNSPECIFIED:
err := errors.Reason("CRITICAL: Received CLUpdated events but Run is in unspecified status").Err()
common.LogError(ctx, err)
panic(err)
case status == run.Status_SUBMITTING:
return &Result{State: rs, PreserveEvents: true}, nil
case isTriggersCancellationOngoing(rs):
// It's likely CL is updated due to trigger cancellation, defer the process
// of CLsUpdated event till triggers cancellation is done.
return &Result{State: rs, PreserveEvents: true}, nil
case run.IsEnded(status):
logging.Debugf(ctx, "skipping OnCLUpdated because Run is %s", status)
return &Result{State: rs}, nil
}
clids.Dedupe()
cg, runCLs, cls, err := loadCLsAndConfig(ctx, rs, clids)
if err != nil {
return nil, err
}
hasNilSnapshot := false
var earliestReconsiderAt time.Time
for i := range clids {
if cls[i].Snapshot == nil {
// This doesn't necessarily assume that shouldCancel() would
// or would not return a cancelReason for nil Snapshot; hence,
// let it decide. hasNilSnapshot is to decide whether
// runs.CheckRunCreate() should be checked or not.
hasNilSnapshot = true
}
switch reconsiderAt, cancellationReason := shouldCancel(ctx, cls[i], runCLs[i], cg); {
case !reconsiderAt.IsZero():
if earliestReconsiderAt.IsZero() || earliestReconsiderAt.After(reconsiderAt) {
earliestReconsiderAt = reconsiderAt
}
case cancellationReason != "":
return impl.Cancel(ctx, rs, []string{cancellationReason})
}
}
if !earliestReconsiderAt.IsZero() {
logging.Debugf(ctx, "Will reconsider OnCLUpdated event(s) after %s", earliestReconsiderAt.Sub(clock.Now(ctx)))
if err := impl.RM.Invoke(ctx, rs.ID, earliestReconsiderAt); err != nil {
return nil, err
}
return &Result{State: rs, PreserveEvents: true}, nil
}
// If any of the CLs has a nil snapshot, skip acls.CheckRunCreate().
// It needs snapshots for the entire CL set.
if hasNilSnapshot {
return &Result{State: rs}, nil
}
// Check the Run creation, in case the Run is no longer valid
// with the newly updated CL info.
rs = rs.ShallowCopy()
allRunCLs, allCLs := runCLs, cls
remainingCLIDs := rs.CLs.Set()
remainingCLIDs.DelAll(clids)
if len(remainingCLIDs) > 0 {
remainingRunCLs, remainingCLs, err := loadRunCLsAndCLs(ctx, rs.ID, remainingCLIDs.ToCLIDs())
if err != nil {
return nil, err
}
allRunCLs = append(allRunCLs, remainingRunCLs...)
allCLs = append(allCLs, remainingCLs...)
}
if _, err := checkRunCreate(ctx, rs, cg, allRunCLs, allCLs); err != nil {
return nil, err
}
return &Result{State: rs}, nil
}
func shouldCancel(ctx context.Context, cl *changelist.CL, rcl *run.RunCL, cg *prjcfg.ConfigGroup) (time.Time, string) {
project := cg.ProjectString()
clString := fmt.Sprintf("CL %d %s", cl.ID, cl.ExternalID)
switch kind, reason := cl.AccessKindWithReason(ctx, project); kind {
case changelist.AccessDenied:
logging.Warningf(ctx, "No longer have access to %s: %s", clString, reason)
return time.Time{}, fmt.Sprintf("no longer have access to %s: %s", cl.ExternalID.MustURL(), reason)
case changelist.AccessDeniedProbably:
logging.Warningf(ctx, "Probably no longer have access to %s (%s), not canceling yet", clString, reason)
// Keep the run Running for now. The access should become either
// AccessGranted or AccessDenied, eventually.
return cl.Access.GetByProject()[project].GetNoAccessTime().AsTime(), ""
case changelist.AccessUnknown:
logging.Errorf(ctx, "Unknown access to %s (%s), not canceling yet", clString, reason)
// Keep the run Running for now, it should become clear eventually.
return time.Time{}, ""
case changelist.AccessGranted:
// The expected and most likely case.
default:
panic(fmt.Errorf("unknown AccessKind %d in %s", kind, clString))
}
if o, c := rcl.Detail.GetPatchset(), cl.Snapshot.GetPatchset(); o != c {
logging.Infof(ctx, "%s has new patchset %d => %d", clString, o, c)
return time.Time{}, fmt.Sprintf("the patchset of %s has changed from %d to %d", cl.ExternalID.MustURL(), o, c)
}
if o, c := rcl.Detail.GetGerrit().GetInfo().GetRef(), cl.Snapshot.GetGerrit().GetInfo().GetRef(); o != c {
logging.Warningf(ctx, "%s has new ref %q => %q", clString, o, c)
return time.Time{}, fmt.Sprintf("the ref of %s has moved from %s to %s", cl.ExternalID.MustURL(), o, c)
}
o, c := rcl.Trigger, trigger.Find(&trigger.FindInput{
ChangeInfo: cl.Snapshot.GetGerrit().GetInfo(),
ConfigGroup: cg.Content,
TriggerNewPatchsetRunAfterPS: cl.TriggerNewPatchsetRunAfterPS,
})
if whatChanged := run.HasTriggerChanged(o, c, cl.ExternalID.MustURL()); whatChanged != "" {
logging.Infof(ctx, "%s has new trigger\nOLD: %s\nNEW: %s", clString, o, c)
return time.Time{}, whatChanged
}
return time.Time{}, ""
}
func isTriggersCancellationOngoing(rs *state.RunState) bool {
for _, op := range rs.OngoingLongOps.GetOps() {
if op.GetCancelTriggers() != nil {
return true
}
}
return false
}