go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/tryjob/execute/execute_test.go (about) 1 // Copyright 2022 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 execute 16 17 import ( 18 "context" 19 "fmt" 20 "math" 21 "testing" 22 "time" 23 24 "google.golang.org/protobuf/proto" 25 "google.golang.org/protobuf/types/known/timestamppb" 26 27 bbpb "go.chromium.org/luci/buildbucket/proto" 28 "go.chromium.org/luci/common/clock" 29 gerritpb "go.chromium.org/luci/common/proto/gerrit" 30 "go.chromium.org/luci/gae/service/datastore" 31 32 cfgpb "go.chromium.org/luci/cv/api/config/v2" 33 "go.chromium.org/luci/cv/api/recipe/v1" 34 bbfacade "go.chromium.org/luci/cv/internal/buildbucket/facade" 35 "go.chromium.org/luci/cv/internal/changelist" 36 "go.chromium.org/luci/cv/internal/common" 37 "go.chromium.org/luci/cv/internal/configs/prjcfg" 38 "go.chromium.org/luci/cv/internal/cvtesting" 39 "go.chromium.org/luci/cv/internal/metrics" 40 "go.chromium.org/luci/cv/internal/run" 41 "go.chromium.org/luci/cv/internal/tryjob" 42 43 . "github.com/smartystreets/goconvey/convey" 44 . "go.chromium.org/luci/common/testing/assertions" 45 ) 46 47 func TestPrepExecutionPlan(t *testing.T) { 48 t.Parallel() 49 Convey("PrepExecutionPlan", t, func() { 50 ct := cvtesting.Test{} 51 ctx, cancel := ct.SetUp(t) 52 defer cancel() 53 54 executor := &Executor{ 55 Env: ct.Env, 56 } 57 58 Convey("Tryjobs Updated", func() { 59 const builderFoo = "foo" 60 r := &run.Run{ 61 Mode: run.FullRun, 62 } 63 prepPlan := func(execState *tryjob.ExecutionState, updatedTryjobs ...int64) *plan { 64 _, p, err := executor.prepExecutionPlan(ctx, execState, r, updatedTryjobs, false) 65 So(err, ShouldBeNil) 66 return p 67 } 68 Convey("Updates", func() { 69 const prevTryjobID = 101 70 const curTryjobID = 102 71 builderFooDef := makeDefinition(builderFoo, true) 72 execState := newExecStateBuilder(). 73 appendDefinition(builderFooDef). 74 appendAttempt(builderFoo, makeAttempt(prevTryjobID, tryjob.Status_TRIGGERED, tryjob.Result_UNKNOWN)). 75 appendAttempt(builderFoo, makeAttempt(curTryjobID, tryjob.Status_TRIGGERED, tryjob.Result_UNKNOWN)). 76 build() 77 Convey("Updates previous attempts", func() { 78 ensureTryjob(ctx, prevTryjobID, builderFooDef, tryjob.Status_ENDED, tryjob.Result_FAILED_TRANSIENTLY) 79 plan := prepPlan(execState, prevTryjobID) 80 So(plan.isEmpty(), ShouldBeTrue) 81 So(execState, ShouldResembleProto, newExecStateBuilder(). 82 appendDefinition(builderFooDef). 83 appendAttempt(builderFoo, makeAttempt(prevTryjobID, tryjob.Status_ENDED, tryjob.Result_FAILED_TRANSIENTLY)). 84 appendAttempt(builderFoo, makeAttempt(curTryjobID, tryjob.Status_TRIGGERED, tryjob.Result_UNKNOWN)). 85 build()) 86 }) 87 Convey("Ignore For not relevant Tryjob", func() { 88 original := proto.Clone(execState).(*tryjob.ExecutionState) 89 const randomTryjob int64 = 567 90 ensureTryjob(ctx, randomTryjob, builderFooDef, tryjob.Status_ENDED, tryjob.Result_FAILED_TRANSIENTLY) 91 plan := prepPlan(execState, randomTryjob) 92 So(plan.isEmpty(), ShouldBeTrue) 93 So(execState, ShouldResembleProto, original) 94 }) 95 So(executor.logEntries, ShouldBeEmpty) 96 }) 97 98 Convey("Single Tryjob", func() { 99 const tjID = 101 100 def := makeDefinition(builderFoo, true) 101 execState := newExecStateBuilder(). 102 appendDefinition(def). 103 appendAttempt(builderFoo, makeAttempt(tjID, tryjob.Status_PENDING, tryjob.Result_UNKNOWN)). 104 withRetryConfig(&cfgpb.Verifiers_Tryjob_RetryConfig{ 105 SingleQuota: 2, 106 GlobalQuota: 10, 107 FailureWeight: 5, 108 TransientFailureWeight: 1, 109 }). 110 build() 111 112 Convey("Succeeded", func() { 113 tj := ensureTryjob(ctx, tjID, def, tryjob.Status_ENDED, tryjob.Result_SUCCEEDED) 114 plan := prepPlan(execState, tjID) 115 So(plan.isEmpty(), ShouldBeTrue) 116 So(execState.Executions[0].Attempts, ShouldResembleProto, []*tryjob.ExecutionState_Execution_Attempt{ 117 makeAttempt(tjID, tryjob.Status_ENDED, tryjob.Result_SUCCEEDED), 118 }) 119 So(execState.Status, ShouldEqual, tryjob.ExecutionState_SUCCEEDED) 120 So(executor.logEntries, ShouldResembleProto, []*tryjob.ExecutionLogEntry{ 121 { 122 Time: timestamppb.New(ct.Clock.Now().UTC()), 123 Kind: &tryjob.ExecutionLogEntry_TryjobsEnded_{ 124 TryjobsEnded: &tryjob.ExecutionLogEntry_TryjobsEnded{ 125 Tryjobs: []*tryjob.ExecutionLogEntry_TryjobSnapshot{ 126 makeLogTryjobSnapshot(def, tj, false), 127 }, 128 }, 129 }, 130 }, 131 }) 132 }) 133 134 Convey("Succeeded but not reusable", func() { 135 ro := &recipe.Output{ 136 Reusability: &recipe.Output_Reusability{ 137 ModeAllowlist: []string{string(run.DryRun)}, 138 }, 139 } 140 So(isModeAllowed(r.Mode, ro.GetReusability().GetModeAllowlist()), ShouldBeFalse) 141 tj := ensureTryjob(ctx, tjID, def, tryjob.Status_ENDED, tryjob.Result_SUCCEEDED) 142 tj.Result.Output = ro 143 So(datastore.Put(ctx, tj), ShouldBeNil) 144 tryjob.LatestAttempt(execState.GetExecutions()[0]).Reused = true 145 plan := prepPlan(execState, tjID) 146 So(plan.isEmpty(), ShouldBeFalse) 147 So(plan.triggerNewAttempt, ShouldHaveLength, 1) 148 So(plan.triggerNewAttempt[0].definition, ShouldResembleProto, def) 149 attempt := makeAttempt(tjID, tryjob.Status_ENDED, tryjob.Result_SUCCEEDED) 150 attempt.Reused = true 151 attempt.Result.Output = ro 152 So(execState.Executions[0].Attempts, ShouldResembleProto, []*tryjob.ExecutionState_Execution_Attempt{attempt}) 153 So(execState.Status, ShouldEqual, tryjob.ExecutionState_RUNNING) 154 So(executor.logEntries, ShouldResembleProto, []*tryjob.ExecutionLogEntry{ 155 { 156 Time: timestamppb.New(ct.Clock.Now().UTC()), 157 Kind: &tryjob.ExecutionLogEntry_TryjobsEnded_{ 158 TryjobsEnded: &tryjob.ExecutionLogEntry_TryjobsEnded{ 159 Tryjobs: []*tryjob.ExecutionLogEntry_TryjobSnapshot{ 160 makeLogTryjobSnapshot(def, tj, true), 161 }, 162 }, 163 }, 164 }, 165 }) 166 }) 167 168 Convey("Still Running", func() { 169 tj := ensureTryjob(ctx, tjID, def, tryjob.Status_TRIGGERED, tryjob.Result_UNKNOWN) 170 Convey("Tryjob is critical", func() { 171 plan := prepPlan(execState, tjID) 172 So(plan.isEmpty(), ShouldBeTrue) 173 So(execState.Status, ShouldEqual, tryjob.ExecutionState_RUNNING) 174 So(execState.Executions[0].Attempts, ShouldResembleProto, []*tryjob.ExecutionState_Execution_Attempt{ 175 makeAttempt(tjID, tryjob.Status_TRIGGERED, tryjob.Result_UNKNOWN), 176 }) 177 Convey("but no longer reusable", func() { 178 ro := &recipe.Output{ 179 Reusability: &recipe.Output_Reusability{ 180 ModeAllowlist: []string{string(run.DryRun)}, 181 }, 182 } 183 So(isModeAllowed(r.Mode, ro.GetReusability().GetModeAllowlist()), ShouldBeFalse) 184 tj.Result.Output = ro 185 So(datastore.Put(ctx, tj), ShouldBeNil) 186 tryjob.LatestAttempt(execState.GetExecutions()[0]).Reused = true 187 plan := prepPlan(execState, tjID) 188 So(plan.isEmpty(), ShouldBeFalse) 189 So(plan.triggerNewAttempt, ShouldHaveLength, 1) 190 So(plan.triggerNewAttempt[0].definition, ShouldResembleProto, def) 191 attempt := makeAttempt(tjID, tryjob.Status_TRIGGERED, tryjob.Result_UNKNOWN) 192 attempt.Reused = true 193 attempt.Result.Output = ro 194 So(execState.Executions[0].Attempts, ShouldResembleProto, []*tryjob.ExecutionState_Execution_Attempt{attempt}) 195 So(execState.Status, ShouldEqual, tryjob.ExecutionState_RUNNING) 196 }) 197 }) 198 Convey("Tryjob is not critical", func() { 199 execState.GetRequirement().GetDefinitions()[0].Critical = false 200 plan := prepPlan(execState, tjID) 201 So(plan.isEmpty(), ShouldBeTrue) 202 So(execState.Status, ShouldEqual, tryjob.ExecutionState_SUCCEEDED) 203 So(execState.Executions[0].Attempts, ShouldResembleProto, []*tryjob.ExecutionState_Execution_Attempt{ 204 makeAttempt(tjID, tryjob.Status_TRIGGERED, tryjob.Result_UNKNOWN), 205 }) 206 }) 207 So(executor.logEntries, ShouldBeEmpty) 208 }) 209 210 Convey("Failed", func() { 211 var tj *tryjob.Tryjob 212 Convey("Critical and can retry", func() { 213 // Quota allows retrying transient failure. 214 tj = ensureTryjob(ctx, tjID, def, tryjob.Status_ENDED, tryjob.Result_FAILED_TRANSIENTLY) 215 plan := prepPlan(execState, tjID) 216 So(plan.isEmpty(), ShouldBeFalse) 217 So(plan.triggerNewAttempt, ShouldHaveLength, 1) 218 So(plan.triggerNewAttempt[0].definition, ShouldResembleProto, execState.GetRequirement().GetDefinitions()[0]) 219 So(execState.Executions[0].Attempts, ShouldResembleProto, []*tryjob.ExecutionState_Execution_Attempt{ 220 makeAttempt(tjID, tryjob.Status_ENDED, tryjob.Result_FAILED_TRANSIENTLY), 221 }) 222 So(execState.Status, ShouldEqual, tryjob.ExecutionState_RUNNING) 223 So(executor.logEntries, ShouldResembleProto, []*tryjob.ExecutionLogEntry{ 224 { 225 Time: timestamppb.New(ct.Clock.Now().UTC()), 226 Kind: &tryjob.ExecutionLogEntry_TryjobsEnded_{ 227 TryjobsEnded: &tryjob.ExecutionLogEntry_TryjobsEnded{ 228 Tryjobs: []*tryjob.ExecutionLogEntry_TryjobSnapshot{ 229 makeLogTryjobSnapshot(def, tj, false), 230 }, 231 }, 232 }, 233 }, 234 }) 235 }) 236 Convey("Critical and can NOT retry", func() { 237 // Quota doesn't allow retrying permanent failure. 238 tj = ensureTryjob(ctx, tjID, def, tryjob.Status_ENDED, tryjob.Result_FAILED_PERMANENTLY) 239 plan := prepPlan(execState, tjID) 240 So(plan.isEmpty(), ShouldBeTrue) 241 So(execState.Executions[0].Attempts, ShouldResembleProto, []*tryjob.ExecutionState_Execution_Attempt{ 242 makeAttempt(tjID, tryjob.Status_ENDED, tryjob.Result_FAILED_PERMANENTLY), 243 }) 244 So(execState.Status, ShouldEqual, tryjob.ExecutionState_FAILED) 245 So(execState.Failures, ShouldResembleProto, &tryjob.ExecutionState_Failures{ 246 UnsuccessfulResults: []*tryjob.ExecutionState_Failures_UnsuccessfulResult{ 247 {TryjobId: tjID}, 248 }, 249 }) 250 So(executor.logEntries, ShouldResembleProto, []*tryjob.ExecutionLogEntry{ 251 { 252 Time: timestamppb.New(ct.Clock.Now().UTC()), 253 Kind: &tryjob.ExecutionLogEntry_TryjobsEnded_{ 254 TryjobsEnded: &tryjob.ExecutionLogEntry_TryjobsEnded{ 255 Tryjobs: []*tryjob.ExecutionLogEntry_TryjobSnapshot{ 256 makeLogTryjobSnapshot(def, tj, false), 257 }, 258 }, 259 }, 260 }, 261 { 262 Time: timestamppb.New(ct.Clock.Now().UTC()), 263 Kind: &tryjob.ExecutionLogEntry_RetryDenied_{ 264 RetryDenied: &tryjob.ExecutionLogEntry_RetryDenied{ 265 Tryjobs: []*tryjob.ExecutionLogEntry_TryjobSnapshot{ 266 makeLogTryjobSnapshot(def, tj, false), 267 }, 268 Reason: "insufficient quota", 269 }, 270 }, 271 }, 272 }) 273 }) 274 Convey("Tryjob is not critical", func() { 275 tj = ensureTryjob(ctx, tjID, def, tryjob.Status_ENDED, tryjob.Result_FAILED_PERMANENTLY) 276 execState.GetRequirement().GetDefinitions()[0].Critical = false 277 plan := prepPlan(execState, tjID) 278 So(plan.isEmpty(), ShouldBeTrue) 279 So(execState.Executions[0].Attempts, ShouldResembleProto, []*tryjob.ExecutionState_Execution_Attempt{ 280 makeAttempt(tjID, tryjob.Status_ENDED, tryjob.Result_FAILED_PERMANENTLY), 281 }) 282 // Still consider execution as succeeded even though non-critical 283 // tryjob has failed. 284 So(execState.Status, ShouldEqual, tryjob.ExecutionState_SUCCEEDED) 285 So(executor.logEntries, ShouldResembleProto, []*tryjob.ExecutionLogEntry{ 286 { 287 Time: timestamppb.New(ct.Clock.Now().UTC()), 288 Kind: &tryjob.ExecutionLogEntry_TryjobsEnded_{ 289 TryjobsEnded: &tryjob.ExecutionLogEntry_TryjobsEnded{ 290 Tryjobs: []*tryjob.ExecutionLogEntry_TryjobSnapshot{ 291 makeLogTryjobSnapshot(def, tj, false), 292 }, 293 }, 294 }, 295 }, 296 }) 297 }) 298 }) 299 300 Convey("Untriggered", func() { 301 ensureTryjob(ctx, tjID, def, tryjob.Status_UNTRIGGERED, tryjob.Result_UNKNOWN) 302 Convey("Reused", func() { 303 tryjob.LatestAttempt(execState.GetExecutions()[0]).Reused = true 304 plan := prepPlan(execState, tjID) 305 So(plan.isEmpty(), ShouldBeFalse) 306 So(plan.triggerNewAttempt, ShouldHaveLength, 1) 307 So(plan.triggerNewAttempt[0].definition, ShouldResembleProto, execState.GetRequirement().GetDefinitions()[0]) 308 attempt := makeAttempt(tjID, tryjob.Status_UNTRIGGERED, tryjob.Result_UNKNOWN) 309 attempt.Reused = true 310 So(execState.Executions[0].Attempts, ShouldResembleProto, []*tryjob.ExecutionState_Execution_Attempt{attempt}) 311 So(execState.Status, ShouldEqual, tryjob.ExecutionState_RUNNING) 312 }) 313 Convey("Not critical", func() { 314 execState.GetRequirement().GetDefinitions()[0].Critical = false 315 plan := prepPlan(execState, tjID) 316 So(plan.isEmpty(), ShouldBeTrue) 317 }) 318 So(executor.logEntries, ShouldBeEmpty) 319 }) 320 }) 321 322 Convey("Multiple Tryjobs", func() { 323 builder1Def := makeDefinition("builder1", true) 324 builder2Def := makeDefinition("builder2", false) 325 builder3Def := makeDefinition("builder3", true) 326 execState := newExecStateBuilder(). 327 appendDefinition(builder1Def). 328 appendAttempt("builder1", makeAttempt(101, tryjob.Status_PENDING, tryjob.Result_UNKNOWN)). 329 appendDefinition(builder2Def). 330 appendAttempt("builder2", makeAttempt(201, tryjob.Status_PENDING, tryjob.Result_UNKNOWN)). 331 appendDefinition(builder3Def). 332 appendAttempt("builder3", makeAttempt(301, tryjob.Status_PENDING, tryjob.Result_UNKNOWN)). 333 withRetryConfig(&cfgpb.Verifiers_Tryjob_RetryConfig{ 334 SingleQuota: 2, 335 GlobalQuota: 10, 336 FailureWeight: 5, 337 TransientFailureWeight: 1, 338 }). 339 build() 340 341 Convey("Has non-ended critical Tryjobs", func() { 342 ensureTryjob(ctx, 101, builder1Def, tryjob.Status_ENDED, tryjob.Result_SUCCEEDED) 343 ensureTryjob(ctx, 201, builder2Def, tryjob.Status_ENDED, tryjob.Result_SUCCEEDED) 344 // 301 has not completed. 345 plan := prepPlan(execState, 101, 201) 346 So(plan.isEmpty(), ShouldBeTrue) 347 So(execState.Status, ShouldEqual, tryjob.ExecutionState_RUNNING) 348 }) 349 Convey("Execution ends when all critical tryjob ended", func() { 350 ensureTryjob(ctx, 101, builder1Def, tryjob.Status_ENDED, tryjob.Result_SUCCEEDED) 351 // 201 (non-critical) has not completed. 352 ensureTryjob(ctx, 301, builder3Def, tryjob.Status_ENDED, tryjob.Result_SUCCEEDED) 353 plan := prepPlan(execState, 101, 301) 354 So(plan.isEmpty(), ShouldBeTrue) 355 So(execState.Status, ShouldEqual, tryjob.ExecutionState_SUCCEEDED) 356 }) 357 Convey("Failed critical Tryjob fails the execution", func() { 358 ensureTryjob(ctx, 101, builder1Def, tryjob.Status_ENDED, tryjob.Result_FAILED_PERMANENTLY) 359 // One critical tryjob is still running. 360 ensureTryjob(ctx, 301, builder3Def, tryjob.Status_TRIGGERED, tryjob.Result_UNKNOWN) 361 plan := prepPlan(execState, 101, 301) 362 So(plan.isEmpty(), ShouldBeTrue) 363 So(execState.Status, ShouldEqual, tryjob.ExecutionState_FAILED) 364 So(execState.Failures, ShouldResembleProto, &tryjob.ExecutionState_Failures{ 365 UnsuccessfulResults: []*tryjob.ExecutionState_Failures_UnsuccessfulResult{ 366 {TryjobId: 101}, 367 }, 368 }) 369 }) 370 Convey("Can retry multiple", func() { 371 ensureTryjob(ctx, 101, builder1Def, tryjob.Status_ENDED, tryjob.Result_FAILED_TRANSIENTLY) 372 // 201 has not completed. 373 ensureTryjob(ctx, 301, builder3Def, tryjob.Status_ENDED, tryjob.Result_FAILED_TRANSIENTLY) 374 plan := prepPlan(execState, 101, 301) 375 So(plan.isEmpty(), ShouldBeFalse) 376 So(plan.triggerNewAttempt, ShouldHaveLength, 2) 377 So(execState.Status, ShouldEqual, tryjob.ExecutionState_RUNNING) 378 }) 379 }) 380 }) 381 382 Convey("Requirement Changed", func() { 383 var builderFooDef = makeDefinition("builderFoo", true) 384 latestReqmt := &tryjob.Requirement{ 385 Definitions: []*tryjob.Definition{ 386 builderFooDef, 387 }, 388 } 389 r := &run.Run{ 390 Tryjobs: &run.Tryjobs{ 391 Requirement: latestReqmt, 392 RequirementVersion: 2, 393 RequirementComputedAt: timestamppb.New(ct.Clock.Now()), 394 }, 395 } 396 397 Convey("No-op if current version newer than target version", func() { 398 execState := newExecStateBuilder(). 399 withRequirementVersion(int(r.Tryjobs.GetRequirementVersion()) + 1). 400 build() 401 _, plan, err := executor.prepExecutionPlan(ctx, execState, r, nil, true) 402 So(err, ShouldBeNil) 403 So(plan.isEmpty(), ShouldBeTrue) 404 So(executor.logEntries, ShouldBeEmpty) 405 }) 406 407 Convey("New Requirement", func() { 408 execState := newExecStateBuilder().build() 409 execState, plan, err := executor.prepExecutionPlan(ctx, execState, r, nil, true) 410 So(err, ShouldBeNil) 411 So(execState.Requirement, ShouldResembleProto, latestReqmt) 412 So(plan.triggerNewAttempt, ShouldHaveLength, 1) 413 So(plan.triggerNewAttempt[0].definition, ShouldResembleProto, builderFooDef) 414 So(executor.logEntries, ShouldBeEmpty) 415 }) 416 417 Convey("Removed Tryjob", func() { 418 removedDef := makeDefinition("removed", true) 419 execState := newExecStateBuilder(). 420 appendDefinition(removedDef). 421 appendDefinition(builderFooDef). 422 build() 423 execState, plan, err := executor.prepExecutionPlan(ctx, execState, r, nil, true) 424 So(err, ShouldBeNil) 425 So(execState.Requirement, ShouldResembleProto, latestReqmt) 426 So(plan.triggerNewAttempt, ShouldBeEmpty) 427 So(plan.discard, ShouldHaveLength, 1) 428 So(plan.discard[0].definition, ShouldResembleProto, removedDef) 429 So(execState.Executions, ShouldHaveLength, 1) 430 So(plan.discard[0].discardReason, ShouldEqual, noLongerRequiredInConfig) 431 So(executor.logEntries, ShouldResembleProto, []*tryjob.ExecutionLogEntry{ 432 { 433 Time: timestamppb.New(ct.Clock.Now().UTC()), 434 Kind: &tryjob.ExecutionLogEntry_RequirementChanged_{ 435 RequirementChanged: &tryjob.ExecutionLogEntry_RequirementChanged{}, 436 }, 437 }, 438 }) 439 }) 440 441 Convey("Changed Tryjob", func() { 442 builderFooOriginal := proto.Clone(builderFooDef).(*tryjob.Definition) 443 builderFooOriginal.DisableReuse = !builderFooDef.DisableReuse 444 execState := newExecStateBuilder(). 445 appendDefinition(builderFooOriginal). 446 build() 447 execState, plan, err := executor.prepExecutionPlan(ctx, execState, r, nil, true) 448 So(err, ShouldBeNil) 449 So(execState.Requirement, ShouldResembleProto, latestReqmt) 450 So(plan.isEmpty(), ShouldBeTrue) 451 So(executor.logEntries, ShouldResembleProto, []*tryjob.ExecutionLogEntry{ 452 { 453 Time: timestamppb.New(ct.Clock.Now().UTC()), 454 Kind: &tryjob.ExecutionLogEntry_RequirementChanged_{ 455 RequirementChanged: &tryjob.ExecutionLogEntry_RequirementChanged{}, 456 }, 457 }, 458 }) 459 }) 460 461 Convey("Change in retry config", func() { 462 execState := newExecStateBuilder(). 463 appendDefinition(builderFooDef). 464 withRetryConfig(&cfgpb.Verifiers_Tryjob_RetryConfig{ 465 SingleQuota: 2, 466 GlobalQuota: 10, 467 }). 468 build() 469 execState, plan, err := executor.prepExecutionPlan(ctx, execState, r, nil, true) 470 So(err, ShouldBeNil) 471 So(execState.Requirement, ShouldResembleProto, latestReqmt) 472 So(plan.isEmpty(), ShouldBeTrue) 473 So(executor.logEntries, ShouldResembleProto, []*tryjob.ExecutionLogEntry{ 474 { 475 Time: timestamppb.New(ct.Clock.Now().UTC()), 476 Kind: &tryjob.ExecutionLogEntry_RequirementChanged_{ 477 RequirementChanged: &tryjob.ExecutionLogEntry_RequirementChanged{}, 478 }, 479 }, 480 }) 481 }) 482 483 Convey("Empty definitions", func() { 484 latestReqmt.Definitions = nil 485 execState := newExecStateBuilder(). 486 appendDefinition(builderFooDef). 487 build() 488 execState, plan, err := executor.prepExecutionPlan(ctx, execState, r, nil, true) 489 So(err, ShouldBeNil) 490 So(execState.Requirement, ShouldResembleProto, latestReqmt) 491 So(plan.discard, ShouldHaveLength, 1) 492 So(execState.Status, ShouldEqual, tryjob.ExecutionState_SUCCEEDED) 493 }) 494 }) 495 }) 496 } 497 498 func TestExecutePlan(t *testing.T) { 499 t.Parallel() 500 Convey("ExecutePlan", t, func() { 501 ct := cvtesting.Test{} 502 ctx, cancel := ct.SetUp(t) 503 defer cancel() 504 505 Convey("Discard tryjobs", func() { 506 executor := &Executor{ 507 Env: ct.Env, 508 } 509 const bbHost = "buildbucket.example.com" 510 def := &tryjob.Definition{ 511 Backend: &tryjob.Definition_Buildbucket_{ 512 Buildbucket: &tryjob.Definition_Buildbucket{ 513 Host: bbHost, 514 Builder: &bbpb.BuilderID{ 515 Project: "some_proj", 516 Bucket: "some_bucket", 517 Builder: "some_builder", 518 }, 519 }, 520 }, 521 } 522 _, err := executor.executePlan(ctx, &plan{ 523 discard: []planItem{ 524 { 525 definition: def, 526 execution: &tryjob.ExecutionState_Execution{ 527 Attempts: []*tryjob.ExecutionState_Execution_Attempt{ 528 { 529 TryjobId: 67, 530 ExternalId: string(tryjob.MustBuildbucketID(bbHost, 6767)), 531 }, 532 { 533 TryjobId: 58, 534 ExternalId: string(tryjob.MustBuildbucketID(bbHost, 5858)), 535 }, 536 }, 537 }, 538 discardReason: "no longer needed", 539 }, 540 }, 541 }, nil, nil) 542 So(err, ShouldBeNil) 543 So(executor.logEntries, ShouldResembleProto, []*tryjob.ExecutionLogEntry{ 544 { 545 Time: timestamppb.New(ct.Clock.Now().UTC()), 546 Kind: &tryjob.ExecutionLogEntry_TryjobDiscarded_{ 547 TryjobDiscarded: &tryjob.ExecutionLogEntry_TryjobDiscarded{ 548 Snapshot: &tryjob.ExecutionLogEntry_TryjobSnapshot{ 549 Definition: def, 550 Id: 58, 551 ExternalId: string(tryjob.MustBuildbucketID(bbHost, 5858)), 552 }, 553 Reason: "no longer needed", 554 }, 555 }, 556 }, 557 }) 558 }) 559 560 Convey("Trigger new attempt", func() { 561 const ( 562 lProject = "test_proj" 563 configGroupName = "test_config_group" 564 bbHost = "buildbucket.example.com" 565 buildID = 9524107902457 566 clid = 34586452134 567 gHost = "example-review.com" 568 gRepo = "repo/a" 569 gChange = 123 570 gPatchset = 5 571 gMinPatchset = 4 572 ) 573 now := ct.Clock.Now().UTC() 574 var runID = common.MakeRunID(lProject, now.Add(-1*time.Hour), 1, []byte("abcd")) 575 r := &run.Run{ 576 ID: runID, 577 ConfigGroupID: prjcfg.MakeConfigGroupID("deedbeef", configGroupName), 578 Mode: run.DryRun, 579 CLs: common.CLIDs{clid}, 580 Status: run.Status_RUNNING, 581 } 582 runCL := &run.RunCL{ 583 ID: clid, 584 Run: datastore.NewKey(ctx, common.RunKind, string(runID), 0, nil), 585 Detail: &changelist.Snapshot{ 586 Patchset: gPatchset, 587 MinEquivalentPatchset: gMinPatchset, 588 Kind: &changelist.Snapshot_Gerrit{ 589 Gerrit: &changelist.Gerrit{ 590 Host: gHost, 591 Info: &gerritpb.ChangeInfo{ 592 Project: gRepo, 593 Number: gChange, 594 Owner: &gerritpb.AccountInfo{ 595 Email: "owner@example.com", 596 }, 597 }, 598 }, 599 }, 600 }, 601 Trigger: &run.Trigger{ 602 Mode: string(run.DryRun), 603 Email: "triggerer@example.com", 604 }, 605 } 606 So(datastore.Put(ctx, runCL), ShouldBeNil) 607 builder := &bbpb.BuilderID{ 608 Project: lProject, 609 Bucket: "some_bucket", 610 Builder: "some_builder", 611 } 612 def := &tryjob.Definition{ 613 Backend: &tryjob.Definition_Buildbucket_{ 614 Buildbucket: &tryjob.Definition_Buildbucket{ 615 Host: bbHost, 616 Builder: builder, 617 }, 618 }, 619 Critical: true, 620 } 621 ct.BuildbucketFake.AddBuilder(bbHost, builder, nil) 622 executor := &Executor{ 623 Env: ct.Env, 624 Backend: &bbfacade.Facade{ 625 ClientFactory: ct.BuildbucketFake.NewClientFactory(), 626 }, 627 RM: run.NewNotifier(ct.TQDispatcher), 628 } 629 execution := &tryjob.ExecutionState_Execution{} 630 execState := &tryjob.ExecutionState{ 631 Executions: []*tryjob.ExecutionState_Execution{execution}, 632 Requirement: &tryjob.Requirement{ 633 Definitions: []*tryjob.Definition{def}, 634 }, 635 Status: tryjob.ExecutionState_RUNNING, 636 } 637 638 Convey("New Tryjob is not launched when tryjob can be reused", func() { 639 result := &tryjob.Result{ 640 CreateTime: timestamppb.New(now.Add(-staleTryjobAge / 2)), 641 Backend: &tryjob.Result_Buildbucket_{ 642 Buildbucket: &tryjob.Result_Buildbucket{ 643 Id: buildID, 644 Builder: builder, 645 }, 646 }, 647 Status: tryjob.Result_SUCCEEDED, 648 } 649 reuseTryjob := &tryjob.Tryjob{ 650 ExternalID: tryjob.MustBuildbucketID(bbHost, buildID), 651 EVersion: 1, 652 EntityCreateTime: now.Add(-staleTryjobAge / 2), 653 EntityUpdateTime: now.Add(-1 * time.Minute), 654 ReuseKey: computeReuseKey([]*run.RunCL{runCL}), 655 CLPatchsets: tryjob.CLPatchsets{tryjob.MakeCLPatchset(runCL.ID, gPatchset)}, 656 Definition: def, 657 Status: tryjob.Status_ENDED, 658 LaunchedBy: common.MakeRunID(lProject, now.Add(-2*time.Hour), 1, []byte("efgh")), 659 Result: result, 660 } 661 So(datastore.RunInTransaction(ctx, func(ctx context.Context) error { 662 return tryjob.SaveTryjobs(ctx, []*tryjob.Tryjob{reuseTryjob}, nil) 663 }, nil), ShouldBeNil) 664 execState, err := executor.executePlan(ctx, &plan{ 665 triggerNewAttempt: []planItem{ 666 { 667 definition: def, 668 execution: execution, 669 }, 670 }, 671 }, r, execState) 672 So(err, ShouldBeNil) 673 So(execState.GetStatus(), ShouldEqual, tryjob.ExecutionState_RUNNING) 674 So(execState.GetExecutions()[0].GetAttempts(), ShouldResembleProto, 675 []*tryjob.ExecutionState_Execution_Attempt{ 676 { 677 TryjobId: int64(reuseTryjob.ID), 678 ExternalId: string(tryjob.MustBuildbucketID(bbHost, buildID)), 679 Status: tryjob.Status_ENDED, 680 Result: result, 681 Reused: true, 682 }, 683 }) 684 So(executor.stagedMetricReportFns, ShouldBeEmpty) 685 }) 686 687 Convey("When Tryjob can't be reused, thus launch a new Tryjob", func() { 688 execState, err := executor.executePlan(ctx, &plan{ 689 triggerNewAttempt: []planItem{ 690 { 691 definition: def, 692 execution: execution, 693 }, 694 }, 695 }, r, execState) 696 So(err, ShouldBeNil) 697 So(execState.GetStatus(), ShouldEqual, tryjob.ExecutionState_RUNNING) 698 So(execState.GetExecutions()[0].GetAttempts(), ShouldHaveLength, 1) 699 attempt := execState.GetExecutions()[0].GetAttempts()[0] 700 So(attempt.GetTryjobId(), ShouldNotEqual, 0) 701 So(attempt.GetExternalId(), ShouldNotBeEmpty) 702 So(attempt.GetStatus(), ShouldEqual, tryjob.Status_TRIGGERED) 703 So(attempt.GetReused(), ShouldBeFalse) 704 So(executor.stagedMetricReportFns, ShouldHaveLength, 1) 705 executor.stagedMetricReportFns[0](ctx) 706 tryjob.RunWithBuilderMetricsTarget(ctx, ct.Env, def, func(ctx context.Context) { 707 So(ct.TSMonSentValue(ctx, metrics.Public.TryjobLaunched, lProject, configGroupName, true, false), ShouldEqual, 1) 708 }) 709 }) 710 711 Convey("Fail the execution if encounter launch failure", func() { 712 def.GetBuildbucket().GetBuilder().Project = "another_proj" 713 execState, err := executor.executePlan(ctx, &plan{ 714 triggerNewAttempt: []planItem{ 715 { 716 definition: def, 717 execution: execution, 718 }, 719 }, 720 }, r, execState) 721 So(err, ShouldBeNil) 722 So(execState.GetExecutions()[0].GetAttempts(), ShouldHaveLength, 1) 723 attempt := execState.GetExecutions()[0].GetAttempts()[0] 724 So(attempt.GetTryjobId(), ShouldNotEqual, 0) 725 So(attempt.GetExternalId(), ShouldBeEmpty) 726 So(attempt.GetStatus(), ShouldEqual, tryjob.Status_UNTRIGGERED) 727 So(execState.Status, ShouldEqual, tryjob.ExecutionState_FAILED) 728 So(execState.Failures, ShouldResembleProto, &tryjob.ExecutionState_Failures{ 729 LaunchFailures: []*tryjob.ExecutionState_Failures_LaunchFailure{ 730 { 731 Definition: def, 732 Reason: "received NotFound from buildbucket. message: builder another_proj/some_bucket/some_builder not found", 733 }, 734 }, 735 }) 736 So(executor.stagedMetricReportFns, ShouldBeEmpty) 737 }) 738 }) 739 }) 740 } 741 742 type execStateBuilder struct { 743 state *tryjob.ExecutionState 744 } 745 746 func newExecStateBuilder(original ...*tryjob.ExecutionState) *execStateBuilder { 747 switch len(original) { 748 case 0: 749 return &execStateBuilder{ 750 state: initExecutionState(), 751 } 752 case 1: 753 return &execStateBuilder{ 754 state: original[0], 755 } 756 default: 757 panic(fmt.Errorf("more than one original execState provided")) 758 } 759 } 760 761 func (esb *execStateBuilder) withStatus(status tryjob.ExecutionState_Status) *execStateBuilder { 762 esb.state.Status = status 763 return esb 764 } 765 766 func (esb *execStateBuilder) appendDefinition(def *tryjob.Definition) *execStateBuilder { 767 esb.ensureRequirement() 768 esb.state.Requirement.Definitions = append(esb.state.Requirement.Definitions, def) 769 esb.state.Executions = append(esb.state.Executions, &tryjob.ExecutionState_Execution{}) 770 return esb 771 } 772 773 func (esb *execStateBuilder) withRetryConfig(retryConfig *cfgpb.Verifiers_Tryjob_RetryConfig) *execStateBuilder { 774 esb.ensureRequirement() 775 esb.state.Requirement.RetryConfig = proto.Clone(retryConfig).(*cfgpb.Verifiers_Tryjob_RetryConfig) 776 return esb 777 } 778 779 func (esb *execStateBuilder) withRequirementVersion(ver int) *execStateBuilder { 780 esb.state.RequirementVersion = int32(ver) 781 return esb 782 } 783 784 func (esb *execStateBuilder) appendAttempt(builderName string, attempt *tryjob.ExecutionState_Execution_Attempt) *execStateBuilder { 785 esb.ensureRequirement() 786 for i, def := range esb.state.Requirement.GetDefinitions() { 787 if builderName == def.GetBuildbucket().GetBuilder().GetBuilder() { 788 esb.state.Executions[i].Attempts = append(esb.state.Executions[i].Attempts, attempt) 789 return esb 790 } 791 } 792 panic(fmt.Errorf("can't find builder %s", builderName)) 793 } 794 795 func (esb *execStateBuilder) ensureRequirement() { 796 if esb.state.Requirement == nil { 797 esb.state.Requirement = &tryjob.Requirement{} 798 } 799 } 800 801 func (esb *execStateBuilder) build() *tryjob.ExecutionState { 802 return esb.state 803 } 804 805 func makeDefinition(builder string, critical bool) *tryjob.Definition { 806 return &tryjob.Definition{ 807 Backend: &tryjob.Definition_Buildbucket_{ 808 Buildbucket: &tryjob.Definition_Buildbucket{ 809 Host: "buildbucket.example.com", 810 Builder: &bbpb.BuilderID{ 811 Project: "test", 812 Bucket: "bucket", 813 Builder: builder, 814 }, 815 }, 816 }, 817 Critical: critical, 818 } 819 } 820 821 func makeAttempt(tjID int64, status tryjob.Status, resultStatus tryjob.Result_Status) *tryjob.ExecutionState_Execution_Attempt { 822 return &tryjob.ExecutionState_Execution_Attempt{ 823 TryjobId: int64(tjID), 824 ExternalId: string(tryjob.MustBuildbucketID("buildbucket.example.com", math.MaxInt64-tjID)), 825 Status: status, 826 Result: &tryjob.Result{ 827 Status: resultStatus, 828 }, 829 } 830 } 831 832 func ensureTryjob(ctx context.Context, id int64, def *tryjob.Definition, status tryjob.Status, resultStatus tryjob.Result_Status) *tryjob.Tryjob { 833 now := datastore.RoundTime(clock.Now(ctx).UTC()) 834 tj := &tryjob.Tryjob{ 835 ID: common.TryjobID(id), 836 ExternalID: tryjob.MustBuildbucketID("buildbucket.example.com", math.MaxInt64-id), 837 Definition: def, 838 Status: status, 839 EntityCreateTime: now, 840 EntityUpdateTime: now, 841 Result: &tryjob.Result{ 842 Status: resultStatus, 843 }, 844 } 845 So(datastore.Put(ctx, tj), ShouldBeNil) 846 return tj 847 }