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 }