go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/run/impl/state/state.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 state defines the model for a Run state.
    16  package state
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"time"
    22  
    23  	"google.golang.org/protobuf/proto"
    24  	"google.golang.org/protobuf/types/known/timestamppb"
    25  
    26  	"go.chromium.org/luci/common/clock"
    27  	"go.chromium.org/luci/common/errors"
    28  
    29  	"go.chromium.org/luci/cv/internal/common"
    30  	"go.chromium.org/luci/cv/internal/common/tree"
    31  	"go.chromium.org/luci/cv/internal/configs/prjcfg"
    32  	"go.chromium.org/luci/cv/internal/run"
    33  )
    34  
    35  // RunState represents the current state of a Run.
    36  type RunState struct {
    37  	run.Run
    38  	LogEntries []*run.LogEntry
    39  
    40  	// Helper fields used during state mutations.
    41  
    42  	// NewLongOpIDs which should be scheduled transactionally with the state
    43  	// transition.
    44  	NewLongOpIDs []string
    45  	// SubmissionScheduled is true if a submission will be attempted after state
    46  	// transition completes.
    47  	SubmissionScheduled bool
    48  }
    49  
    50  // ShallowCopy returns a shallow copy of this RunState.
    51  func (rs *RunState) ShallowCopy() *RunState {
    52  	if rs == nil {
    53  		return nil
    54  	}
    55  	ret := *rs
    56  	// Intentionally use nil check instead of checking len(slice), because
    57  	// otherwise, the copy will always have nil slice even if the `rs` has
    58  	// zero-length slice which will fail the equality check in test.
    59  	if rs.CLs != nil {
    60  		ret.CLs = append(common.CLIDs(nil), rs.CLs...)
    61  	}
    62  	if rs.CancellationReasons != nil {
    63  		rs.CancellationReasons = append([]string(nil), rs.CancellationReasons...)
    64  	}
    65  	if rs.LogEntries != nil {
    66  		ret.LogEntries = append([]*run.LogEntry(nil), rs.LogEntries...)
    67  	}
    68  	if rs.NewLongOpIDs != nil {
    69  		ret.NewLongOpIDs = append([]string(nil), rs.NewLongOpIDs...)
    70  	}
    71  	if rs.DepRuns != nil {
    72  		ret.DepRuns = append(common.RunIDs(nil), rs.DepRuns...)
    73  	}
    74  	return &ret
    75  }
    76  
    77  // DeepCopy returns a deep copy of this RunState.
    78  //
    79  // This is an expensive operation. It should only be called in tests.
    80  func (rs *RunState) DeepCopy() *RunState {
    81  	if rs == nil {
    82  		return nil
    83  	}
    84  	// Explicitly copy by hand instead of creating a shallow copy first like
    85  	// `ShallowCopy` to ensure all newly added fields will be *deep* copied.
    86  	// TODO(yiwzhang): Make a generic recursive deep copy (similar to
    87  	// cvtesting.SafeShouldResemble) which recognizes proto and uses `Clone`
    88  	// to DeepCopy instead.
    89  	ret := &RunState{
    90  		Run: run.Run{
    91  			ID:                                rs.ID,
    92  			CreationOperationID:               rs.CreationOperationID,
    93  			Mode:                              rs.Mode,
    94  			Status:                            rs.Status,
    95  			EVersion:                          rs.EVersion,
    96  			CreateTime:                        rs.CreateTime,
    97  			StartTime:                         rs.StartTime,
    98  			UpdateTime:                        rs.UpdateTime,
    99  			EndTime:                           rs.EndTime,
   100  			Owner:                             rs.Owner,
   101  			CreatedBy:                         rs.CreatedBy,
   102  			BilledTo:                          rs.BilledTo,
   103  			ConfigGroupID:                     rs.ConfigGroupID,
   104  			RootCL:                            rs.RootCL,
   105  			Options:                           proto.Clone(rs.Options).(*run.Options),
   106  			Submission:                        proto.Clone(rs.Submission).(*run.Submission),
   107  			Tryjobs:                           proto.Clone(rs.Tryjobs).(*run.Tryjobs),
   108  			OngoingLongOps:                    proto.Clone(rs.OngoingLongOps).(*run.OngoingLongOps),
   109  			LatestCLsRefresh:                  rs.LatestCLsRefresh,
   110  			LatestTryjobsRefresh:              rs.LatestTryjobsRefresh,
   111  			QuotaExhaustionMsgLongOpRequested: rs.QuotaExhaustionMsgLongOpRequested,
   112  		},
   113  		SubmissionScheduled: rs.SubmissionScheduled,
   114  	}
   115  	// Intentionally use nil check instead of checking len(slice), because
   116  	// otherwise, the copy will always have nil slice even if the `rs` has
   117  	// zero-length slice which will fail the equality check in test.
   118  	if rs.CLs != nil {
   119  		ret.CLs = append(common.CLIDs(nil), rs.CLs...)
   120  	}
   121  	if rs.CancellationReasons != nil {
   122  		rs.CancellationReasons = append([]string(nil), rs.CancellationReasons...)
   123  	}
   124  	if rs.LogEntries != nil {
   125  		ret.LogEntries = make([]*run.LogEntry, len(rs.LogEntries))
   126  		for i, entry := range rs.LogEntries {
   127  			ret.LogEntries[i] = proto.Clone(entry).(*run.LogEntry)
   128  		}
   129  	}
   130  	if rs.NewLongOpIDs != nil {
   131  		ret.NewLongOpIDs = append([]string(nil), rs.NewLongOpIDs...)
   132  	}
   133  	if rs.DepRuns != nil {
   134  		ret.DepRuns = append(common.RunIDs(nil), rs.DepRuns...)
   135  	}
   136  	return ret
   137  }
   138  
   139  // CloneSubmission clones the `Submission` property.
   140  //
   141  // Initializes the property if this is nil.
   142  func (rs *RunState) CloneSubmission() {
   143  	if rs.Submission == nil {
   144  		rs.Submission = &run.Submission{}
   145  	} else {
   146  		rs.Submission = proto.Clone(rs.Submission).(*run.Submission)
   147  	}
   148  }
   149  
   150  // CheckTree returns whether Tree is open for this Run.
   151  //
   152  // Returns true if no Tree or Options.SkipTreeChecks is configured for this Run.
   153  // Updates the latest result to `rs.Submission`.
   154  // In case fetching the status results in error for the first time, it records
   155  // the time in the appropriate field in Submission.
   156  // Records a new LogEntry.
   157  func (rs *RunState) CheckTree(ctx context.Context, tc tree.Client) (bool, error) {
   158  	treeOpen := true
   159  	if !rs.Options.GetSkipTreeChecks() {
   160  		cg, err := prjcfg.GetConfigGroup(ctx, rs.ID.LUCIProject(), rs.ConfigGroupID)
   161  		if err != nil {
   162  			return false, err
   163  		}
   164  		if treeURL := cg.Content.GetVerifiers().GetTreeStatus().GetUrl(); treeURL != "" {
   165  			status, err := tc.FetchLatest(ctx, treeURL)
   166  			switch {
   167  			case err != nil && rs.Submission.TreeErrorSince == nil:
   168  				rs.Submission.TreeErrorSince = timestamppb.New(clock.Now(ctx))
   169  				fallthrough
   170  			case err != nil:
   171  				rs.Submission.LastTreeCheckTime = timestamppb.New(clock.Now(ctx))
   172  				return false, err
   173  			}
   174  			rs.Submission.TreeErrorSince = nil
   175  			treeOpen = status.State == tree.Open || status.State == tree.Throttled
   176  			rs.LogEntries = append(rs.LogEntries, &run.LogEntry{
   177  				Time: timestamppb.New(clock.Now(ctx)),
   178  				Kind: &run.LogEntry_TreeChecked_{
   179  					TreeChecked: &run.LogEntry_TreeChecked{
   180  						Open: treeOpen,
   181  					},
   182  				},
   183  			})
   184  		}
   185  	}
   186  	rs.Submission.TreeOpen = treeOpen
   187  	rs.Submission.LastTreeCheckTime = timestamppb.New(clock.Now(ctx).UTC())
   188  	return treeOpen, nil
   189  }
   190  
   191  // EnqueueLongOp adds a new long op to the Run state and returns its ID.
   192  //
   193  // The actual long operation will be scheduled transactioncally with the Run
   194  // mutation.
   195  func (rs *RunState) EnqueueLongOp(op *run.OngoingLongOps_Op) string {
   196  	if err := validateLongOp(op); err != nil {
   197  		panic(errors.Annotate(err, "validateLongOp").Err())
   198  	}
   199  	// Find an ID which wasn't used yet.
   200  	// Use future EVersion as a prefix to ensure resulting ID is unique over Run's
   201  	// lifetime.
   202  	id := ""
   203  	prefix := rs.EVersion + 1
   204  	suffix := len(rs.NewLongOpIDs) + 1
   205  	for {
   206  		id = fmt.Sprintf("%d-%d", prefix, suffix)
   207  		if _, dup := rs.OngoingLongOps.GetOps()[id]; !dup {
   208  			break
   209  		}
   210  		suffix++
   211  	}
   212  
   213  	if rs.OngoingLongOps == nil {
   214  		rs.OngoingLongOps = &run.OngoingLongOps{}
   215  	} else {
   216  		rs.OngoingLongOps = proto.Clone(rs.OngoingLongOps).(*run.OngoingLongOps)
   217  	}
   218  	if rs.OngoingLongOps.Ops == nil {
   219  		rs.OngoingLongOps.Ops = make(map[string]*run.OngoingLongOps_Op, 1)
   220  	}
   221  	rs.OngoingLongOps.Ops[id] = op
   222  	rs.NewLongOpIDs = append(rs.NewLongOpIDs, id)
   223  	return id
   224  }
   225  
   226  func validateLongOp(op *run.OngoingLongOps_Op) error {
   227  	switch {
   228  	case op.GetDeadline() == nil:
   229  		return errors.New("deadline is required")
   230  	case op.GetWork() == nil:
   231  		return errors.New("work is required")
   232  	case op.GetResetTriggers() != nil:
   233  		if st := op.GetResetTriggers().GetRunStatusIfSucceeded(); !run.IsEnded(st) {
   234  			return errors.Reason("expect terminal run status; got %s", st).Err()
   235  		}
   236  	}
   237  	return nil
   238  }
   239  
   240  // RequestLongOpCancellation records soft request to cancel a long running op.
   241  //
   242  // This request is asynchronous but it's stored in the Run state.
   243  func (rs *RunState) RequestLongOpCancellation(opID string) {
   244  	if _, exists := rs.OngoingLongOps.GetOps()[opID]; !exists {
   245  		panic(fmt.Errorf("long Operation %q doesn't exist", opID))
   246  	}
   247  	rs.OngoingLongOps = proto.Clone(rs.OngoingLongOps).(*run.OngoingLongOps)
   248  	rs.OngoingLongOps.GetOps()[opID].CancelRequested = true
   249  }
   250  
   251  // RemoveCompletedLongOp removes long op from the ongoing ones.
   252  func (rs *RunState) RemoveCompletedLongOp(opID string) {
   253  	if _, exists := rs.OngoingLongOps.GetOps()[opID]; !exists {
   254  		panic(fmt.Errorf("long Operation %q doesn't exist", opID))
   255  	}
   256  	if len(rs.OngoingLongOps.GetOps()) == 1 {
   257  		rs.OngoingLongOps = nil
   258  		return
   259  	}
   260  	// At least 1 other long op will remain.
   261  	rs.OngoingLongOps = proto.Clone(rs.OngoingLongOps).(*run.OngoingLongOps)
   262  	delete(rs.OngoingLongOps.Ops, opID)
   263  }
   264  
   265  // LogInfo adds a generic LogEntry visible in debug UI as is.
   266  //
   267  // Don't use for adding a complicated info formatted into the message string,
   268  // use a specialized LogEntry type instead.
   269  func (rs *RunState) LogInfo(ctx context.Context, label, message string) {
   270  	rs.LogInfoAt(clock.Now(ctx), label, message)
   271  }
   272  
   273  // LogInfof is like LogInfo but formats the message according to format
   274  // specifier.
   275  func (rs *RunState) LogInfof(ctx context.Context, label, format string, args ...any) {
   276  	rs.LogInfo(ctx, label, fmt.Sprintf(format, args...))
   277  }
   278  
   279  // LogInfoAt is LogInfo with a custom timestamp.
   280  func (rs *RunState) LogInfoAt(at time.Time, label, message string) {
   281  	rs.LogEntries = append(rs.LogEntries, &run.LogEntry{
   282  		Time: timestamppb.New(at),
   283  		Kind: &run.LogEntry_Info_{
   284  			Info: &run.LogEntry_Info{
   285  				Label:   label,
   286  				Message: message,
   287  			},
   288  		},
   289  	})
   290  }
   291  
   292  // LogInfofAt is like LogInfoAt but formats the message according to format
   293  // specifier.
   294  func (rs *RunState) LogInfofAt(at time.Time, label, format string, args ...any) {
   295  	rs.LogInfoAt(at, label, fmt.Sprintf(format, args...))
   296  }