github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/atc/scheduler/runner_test.go (about) 1 package scheduler_test 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "sync" 8 "time" 9 10 "code.cloudfoundry.org/lager" 11 "code.cloudfoundry.org/lager/lagertest" 12 "github.com/pf-qiu/concourse/v6/atc" 13 "github.com/pf-qiu/concourse/v6/atc/component" 14 "github.com/pf-qiu/concourse/v6/atc/db/lock/lockfakes" 15 . "github.com/pf-qiu/concourse/v6/atc/scheduler" 16 "github.com/pf-qiu/concourse/v6/atc/scheduler/schedulerfakes" 17 18 "github.com/pf-qiu/concourse/v6/atc/db" 19 "github.com/pf-qiu/concourse/v6/atc/db/dbfakes" 20 . "github.com/onsi/ginkgo" 21 . "github.com/onsi/gomega" 22 ) 23 24 var _ = Describe("Runner", func() { 25 var ( 26 fakePipeline *dbfakes.FakePipeline 27 fakeScheduler *schedulerfakes.FakeBuildScheduler 28 maxInFlight uint64 29 30 lock *lockfakes.FakeLock 31 32 fakeJobFactory *dbfakes.FakeJobFactory 33 fakeJob1 *dbfakes.FakeJob 34 fakeJob2 *dbfakes.FakeJob 35 fakeJob3 *dbfakes.FakeJob 36 37 job1RequestedTime time.Time 38 job2RequestedTime time.Time 39 job3RequestedTime time.Time 40 41 schedulerRunner component.Runnable 42 schedulerErr error 43 ) 44 45 BeforeEach(func() { 46 fakeScheduler = new(schedulerfakes.FakeBuildScheduler) 47 fakeJobFactory = new(dbfakes.FakeJobFactory) 48 maxInFlight = 1 49 50 lock = new(lockfakes.FakeLock) 51 }) 52 53 JustBeforeEach(func() { 54 schedulerRunner = NewRunner( 55 lagertest.NewTestLogger("test"), 56 fakeJobFactory, 57 fakeScheduler, 58 maxInFlight, 59 ) 60 61 schedulerErr = schedulerRunner.Run(context.TODO()) 62 }) 63 64 It("loads up all the jobs to schedule", func() { 65 Expect(fakeJobFactory.JobsToScheduleCallCount()).To(Equal(1)) 66 }) 67 68 Context("when there is one pipeline and two jobs that need to be scheduled", func() { 69 BeforeEach(func() { 70 fakePipeline = new(dbfakes.FakePipeline) 71 fakePipeline.IDReturns(1) 72 fakePipeline.NameReturns("fake-pipeline") 73 fakePipeline.ReloadReturns(true, nil) 74 75 job1RequestedTime = time.Now() 76 job2RequestedTime = time.Now().Add(time.Minute) 77 78 fakeJob1 = new(dbfakes.FakeJob) 79 fakeJob1.IDReturns(1) 80 fakeJob1.NameReturns("some-job") 81 fakeJob1.ReloadReturns(true, nil) 82 fakeJob1.PipelineIDReturns(1) 83 fakeJob1.ScheduleRequestedTimeReturns(job1RequestedTime) 84 fakeJob2 = new(dbfakes.FakeJob) 85 fakeJob2.IDReturns(2) 86 fakeJob2.NameReturns("some-other-job") 87 fakeJob2.ReloadReturns(true, nil) 88 fakeJob2.PipelineIDReturns(1) 89 fakeJob2.ScheduleRequestedTimeReturns(job2RequestedTime) 90 91 fakeJobFactory.JobsToScheduleReturns([]db.SchedulerJob{ 92 { 93 Job: fakeJob1, 94 Resources: db.SchedulerResources{ 95 { 96 Name: "some-resource", 97 Type: "git", 98 Source: atc.Source{"uri": "git://some-resource"}, 99 }, 100 { 101 Name: "some-dependent-resource", 102 Type: "git", 103 Source: atc.Source{"uri": "git://some-dependent-resource"}, 104 }, 105 }, 106 }, 107 { 108 Job: fakeJob2, 109 Resources: db.SchedulerResources{ 110 { 111 Name: "some-resource", 112 Type: "git", 113 Source: atc.Source{"uri": "git://some-resource"}, 114 }, 115 { 116 Name: "some-dependent-resource", 117 Type: "git", 118 Source: atc.Source{"uri": "git://some-dependent-resource"}, 119 }, 120 }, 121 }, 122 }, nil) 123 }) 124 125 It("tries to acquire the scheduling lock for each job", func() { 126 Eventually(fakeJob1.AcquireSchedulingLockCallCount).Should(Equal(1)) 127 Eventually(fakeJob2.AcquireSchedulingLockCallCount).Should(Equal(1)) 128 }) 129 130 Context("when it can't get the lock", func() { 131 BeforeEach(func() { 132 fakeJob1.AcquireSchedulingLockReturns(nil, false, nil) 133 }) 134 135 It("does not do any scheduling", func() { 136 Expect(schedulerErr).ToNot(HaveOccurred()) 137 Eventually(fakeJob1.AcquireSchedulingLockCallCount).Should(Equal(1)) 138 Eventually(fakeJob1.UpdateLastScheduledCallCount).Should(Equal(0)) 139 Eventually(fakeScheduler.ScheduleCallCount).Should(BeZero()) 140 }) 141 }) 142 143 Context("when getting the lock blows up", func() { 144 BeforeEach(func() { 145 fakeJob1.AcquireSchedulingLockReturns(nil, false, errors.New(":3")) 146 }) 147 148 It("does not do any scheduling", func() { 149 Expect(schedulerErr).ToNot(HaveOccurred()) 150 Eventually(fakeJob1.AcquireSchedulingLockCallCount).Should(Equal(1)) 151 Eventually(fakeJob1.UpdateLastScheduledCallCount).Should(Equal(0)) 152 Eventually(fakeScheduler.ScheduleCallCount).Should(BeZero()) 153 }) 154 }) 155 156 Context("when getting both locks succeeds", func() { 157 BeforeEach(func() { 158 fakeJob1.AcquireSchedulingLockReturns(lock, true, nil) 159 fakeJob2.AcquireSchedulingLockReturns(lock, true, nil) 160 }) 161 162 It("reloads the job", func() { 163 Eventually(fakeJob1.ReloadCallCount).Should(Equal(1)) 164 Eventually(fakeJob2.ReloadCallCount).Should(Equal(1)) 165 }) 166 167 Context("when reloading the job succeeds", func() { 168 BeforeEach(func() { 169 fakeJob1.ReloadReturns(true, nil) 170 fakeJob2.ReloadReturns(true, nil) 171 }) 172 173 It("schedules pending builds", func() { 174 Eventually(fakeScheduler.ScheduleCallCount).Should(Equal(2)) 175 176 jobs := []string{} 177 _, _, job := fakeScheduler.ScheduleArgsForCall(0) 178 Expect(job.Resources).To(Equal(db.SchedulerResources{ 179 { 180 Name: "some-resource", 181 Type: "git", 182 Source: atc.Source{"uri": "git://some-resource"}, 183 }, 184 { 185 Name: "some-dependent-resource", 186 Type: "git", 187 Source: atc.Source{"uri": "git://some-dependent-resource"}, 188 }, 189 })) 190 jobs = append(jobs, job.Name()) 191 192 _, _, job = fakeScheduler.ScheduleArgsForCall(1) 193 Expect(job.Resources).To(Equal(db.SchedulerResources{ 194 { 195 Name: "some-resource", 196 Type: "git", 197 Source: atc.Source{"uri": "git://some-resource"}, 198 }, 199 { 200 Name: "some-dependent-resource", 201 Type: "git", 202 Source: atc.Source{"uri": "git://some-dependent-resource"}, 203 }, 204 })) 205 jobs = append(jobs, job.Name()) 206 207 Expect(jobs).To(ConsistOf([]string{"some-job", "some-other-job"})) 208 }) 209 210 Context("when all jobs scheduling succeeds", func() { 211 BeforeEach(func() { 212 fakeScheduler.ScheduleReturns(false, nil) 213 }) 214 215 It("updates last schedule", func() { 216 Expect(schedulerErr).ToNot(HaveOccurred()) 217 218 Eventually(fakeJob1.UpdateLastScheduledCallCount).Should(Equal(1)) 219 Eventually(fakeJob2.UpdateLastScheduledCallCount).Should(Equal(1)) 220 Expect(fakeJob1.UpdateLastScheduledArgsForCall(0)).To(Equal(job1RequestedTime)) 221 Expect(fakeJob2.UpdateLastScheduledArgsForCall(0)).To(Equal(job2RequestedTime)) 222 }) 223 }) 224 225 Context("when the same job is already being scheduled", func() { 226 var scheduleWg *sync.WaitGroup 227 228 BeforeEach(func() { 229 maxInFlight = 2 230 231 fakeJobFactory.JobsToScheduleReturns([]db.SchedulerJob{ 232 { 233 Job: fakeJob1, 234 Resources: db.SchedulerResources{ 235 { 236 Name: "some-resource", 237 Type: "git", 238 Source: atc.Source{"uri": "git://some-resource"}, 239 }, 240 }, 241 }, 242 { 243 Job: fakeJob1, 244 Resources: db.SchedulerResources{ 245 { 246 Name: "some-resource", 247 Type: "git", 248 Source: atc.Source{"uri": "git://some-resource"}, 249 }, 250 }, 251 }, 252 }, nil) 253 254 wg := new(sync.WaitGroup) 255 wg.Add(2) 256 257 scheduleWg = wg 258 259 fakeScheduler.ScheduleStub = func( 260 context.Context, 261 lager.Logger, 262 db.SchedulerJob, 263 ) (bool, error) { 264 wg.Done() 265 wg.Wait() 266 return false, nil 267 } 268 }) 269 270 AfterEach(func() { 271 // release the waiting schedule call 272 scheduleWg.Done() 273 }) 274 275 It("only schedules the job once", func() { 276 Eventually(fakeScheduler.ScheduleCallCount).ShouldNot(BeZero()) 277 Consistently(fakeScheduler.ScheduleCallCount).Should(Equal(1)) 278 }) 279 }) 280 281 Context("when job scheduling fails", func() { 282 BeforeEach(func() { 283 fakeScheduler.ScheduleReturnsOnCall(0, false, errors.New("error")) 284 fakeScheduler.ScheduleReturnsOnCall(1, false, nil) 285 }) 286 287 It("does not update last scheduled", func() { 288 Expect(schedulerErr).ToNot(HaveOccurred()) 289 Eventually(fakeJob2.UpdateLastScheduledCallCount).Should(Equal(1)) 290 Consistently(fakeJob1.UpdateLastScheduledCallCount).Should(Equal(0)) 291 }) 292 }) 293 294 Context("when job scheduling panic", func() { 295 BeforeEach(func() { 296 fakeScheduler.ScheduleStub = func(_ context.Context, _ lager.Logger, job db.SchedulerJob) (bool, error) { 297 if job.Name() == "some-job" { 298 panic("something went wrong") 299 } 300 return false, nil 301 } 302 }) 303 304 It("does not update last scheduled", func() { 305 Expect(schedulerErr).ToNot(HaveOccurred()) 306 Eventually(fakeJob2.UpdateLastScheduledCallCount).Should(Equal(1)) 307 Consistently(fakeJob1.UpdateLastScheduledCallCount).Should(Equal(0)) 308 }) 309 }) 310 311 Context("when there is no error but needs retry", func() { 312 BeforeEach(func() { 313 fakeScheduler.ScheduleReturnsOnCall(0, true, nil) 314 fakeScheduler.ScheduleReturnsOnCall(1, false, nil) 315 }) 316 317 It("does not update last scheduled for the job that needs retry", func() { 318 Expect(schedulerErr).ToNot(HaveOccurred()) 319 Eventually(fakeJob1.UpdateLastScheduledCallCount).Should(Equal(0)) 320 Eventually(fakeJob2.UpdateLastScheduledCallCount).Should(Equal(1)) 321 }) 322 }) 323 }) 324 325 Context("when reloading the job fails", func() { 326 BeforeEach(func() { 327 fakeJob1.ReloadReturns(false, errors.New("disappointment")) 328 }) 329 330 It("does not update last schedule", func() { 331 Expect(schedulerErr).ToNot(HaveOccurred()) 332 Eventually(fakeJob2.UpdateLastScheduledCallCount).Should(Equal(1)) 333 Consistently(fakeJob1.UpdateLastScheduledCallCount).Should(Equal(0)) 334 }) 335 }) 336 337 Context("when the job to reload is not found", func() { 338 BeforeEach(func() { 339 fakeJob1.ReloadReturns(false, nil) 340 }) 341 342 It("does not update last schedule", func() { 343 Expect(schedulerErr).ToNot(HaveOccurred()) 344 Eventually(fakeJob2.UpdateLastScheduledCallCount).Should(Equal(1)) 345 Consistently(fakeJob1.UpdateLastScheduledCallCount).Should(Equal(0)) 346 }) 347 }) 348 }) 349 350 Context("when acquiring one job lock succeeds", func() { 351 BeforeEach(func() { 352 fakeJob1.AcquireSchedulingLockReturns(nil, false, nil) 353 fakeJob2.AcquireSchedulingLockReturns(lock, true, nil) 354 }) 355 356 It("schedules pending builds for one job", func() { 357 Expect(schedulerErr).ToNot(HaveOccurred()) 358 Eventually(fakeScheduler.ScheduleCallCount).Should(Equal(1)) 359 360 _, _, job := fakeScheduler.ScheduleArgsForCall(0) 361 Expect(job).To(Equal(db.SchedulerJob{ 362 Job: fakeJob2, 363 Resources: db.SchedulerResources{ 364 { 365 Name: "some-resource", 366 Type: "git", 367 Source: atc.Source{"uri": "git://some-resource"}, 368 }, 369 { 370 Name: "some-dependent-resource", 371 Type: "git", 372 Source: atc.Source{"uri": "git://some-dependent-resource"}, 373 }, 374 }, 375 })) 376 }) 377 }) 378 }) 379 380 Context("when there are multiple jobs and pipelines", func() { 381 var fakePipeline2 *dbfakes.FakePipeline 382 383 BeforeEach(func() { 384 fakePipeline = new(dbfakes.FakePipeline) 385 fakePipeline.NameReturns("fake-pipeline") 386 fakePipeline.IDReturns(1) 387 fakePipeline2 = new(dbfakes.FakePipeline) 388 fakePipeline2.NameReturns("another-fake-pipeline") 389 fakePipeline2.IDReturns(2) 390 391 job1RequestedTime = time.Now() 392 job2RequestedTime = time.Now().Add(time.Minute) 393 job3RequestedTime = time.Now().Add(2 * time.Minute) 394 395 fakeJob1 = new(dbfakes.FakeJob) 396 fakeJob1.IDReturns(1) 397 fakeJob1.NameReturns("some-job") 398 fakeJob1.ReloadReturns(true, nil) 399 fakeJob1.PipelineIDReturns(1) 400 fakeJob1.PipelineReturns(fakePipeline, true, nil) 401 fakeJob1.ScheduleRequestedTimeReturns(job1RequestedTime) 402 fakeJob2 = new(dbfakes.FakeJob) 403 fakeJob2.IDReturns(2) 404 fakeJob2.NameReturns("some-other-job") 405 fakeJob2.ReloadReturns(true, nil) 406 fakeJob2.PipelineIDReturns(2) 407 fakeJob2.PipelineReturns(fakePipeline2, true, nil) 408 fakeJob2.ScheduleRequestedTimeReturns(job2RequestedTime) 409 fakeJob3 = new(dbfakes.FakeJob) 410 fakeJob3.IDReturns(3) 411 fakeJob3.NameReturns("another-other-job") 412 fakeJob3.ReloadReturns(true, nil) 413 fakeJob3.PipelineIDReturns(2) 414 fakeJob3.PipelineReturns(fakePipeline2, true, nil) 415 fakeJob3.ScheduleRequestedTimeReturns(job3RequestedTime) 416 417 fakeScheduler.ScheduleReturns(false, nil) 418 }) 419 420 Context("when both pipelines successfully schedule", func() { 421 BeforeEach(func() { 422 fakeJob4 := new(dbfakes.FakeJob) 423 fakeJob4.IDReturns(1) 424 fakeJob4.NameReturns("unscheduled-job") 425 426 fakeJob1.AcquireSchedulingLockReturns(lock, true, nil) 427 fakeJob2.AcquireSchedulingLockReturns(lock, true, nil) 428 fakeJob3.AcquireSchedulingLockReturns(lock, true, nil) 429 430 fakeJobFactory.JobsToScheduleReturns([]db.SchedulerJob{ 431 { 432 Job: fakeJob1, 433 Resources: db.SchedulerResources{ 434 { 435 Name: "some-resource", 436 Type: "git", 437 Source: atc.Source{"uri": "git://some-resource"}, 438 }, 439 }, 440 }, 441 { 442 Job: fakeJob2, 443 Resources: db.SchedulerResources{ 444 { 445 Name: "some-dependent-resource", 446 Type: "git", 447 Source: atc.Source{"uri": "git://some-dependent-resource"}, 448 }, 449 }, 450 }, 451 { 452 Job: fakeJob3, 453 Resources: db.SchedulerResources{ 454 { 455 Name: "some-dependent-resource", 456 Type: "git", 457 Source: atc.Source{"uri": "git://some-dependent-resource"}, 458 }, 459 }, 460 }, 461 { 462 Job: fakeJob4, 463 Resources: db.SchedulerResources{ 464 { 465 Name: "some-resource", 466 Type: "git", 467 Source: atc.Source{"uri": "git://some-resource"}, 468 }, 469 }, 470 }, 471 }, nil) 472 }) 473 474 It("all three jobs update the last scheduled", func() { 475 Expect(schedulerErr).ToNot(HaveOccurred()) 476 Eventually(fakeScheduler.ScheduleCallCount).Should(Equal(3)) 477 478 Eventually(fakeJob1.UpdateLastScheduledCallCount).Should(Equal(1)) 479 Eventually(fakeJob2.UpdateLastScheduledCallCount).Should(Equal(1)) 480 Eventually(fakeJob3.UpdateLastScheduledCallCount).Should(Equal(1)) 481 482 Eventually(fakeJob1.UpdateLastScheduledArgsForCall(0)).Should(Equal(job1RequestedTime)) 483 Eventually(fakeJob2.UpdateLastScheduledArgsForCall(0)).Should(Equal(job2RequestedTime)) 484 Eventually(fakeJob3.UpdateLastScheduledArgsForCall(0)).Should(Equal(job3RequestedTime)) 485 }) 486 }) 487 488 Context("when the two jobs fail to schedule", func() { 489 BeforeEach(func() { 490 fakePipeline.JobsReturns([]db.Job{fakeJob1}, nil) 491 fakeJob1.AcquireSchedulingLockReturns(lock, true, nil) 492 fakeJob1.ReloadReturns(false, errors.New("error-1")) 493 494 fakePipeline2.JobsReturns([]db.Job{fakeJob2, fakeJob3}, nil) 495 fakeJob2.AcquireSchedulingLockReturns(lock, true, nil) 496 fakeJob3.AcquireSchedulingLockReturns(lock, true, nil) 497 fakeJob3.ReloadReturns(false, errors.New("error-3")) 498 499 fakeJobFactory.JobsToScheduleReturns([]db.SchedulerJob{ 500 { 501 Job: fakeJob1, 502 Resources: db.SchedulerResources{ 503 { 504 Name: "some-resource", 505 Type: "git", 506 Source: atc.Source{"uri": "git://some-resource"}, 507 }, 508 }, 509 }, 510 { 511 Job: fakeJob2, 512 Resources: db.SchedulerResources{ 513 { 514 Name: "some-dependent-resource", 515 Type: "git", 516 Source: atc.Source{"uri": "git://some-dependent-resource"}, 517 }, 518 }, 519 }, 520 { 521 Job: fakeJob3, 522 Resources: db.SchedulerResources{ 523 { 524 Name: "some-dependent-resource", 525 Type: "git", 526 Source: atc.Source{"uri": "git://some-dependent-resource"}, 527 }, 528 }, 529 }, 530 }, nil) 531 }) 532 533 It("schedules the remaining job", func() { 534 Expect(schedulerErr).ToNot(HaveOccurred()) 535 Eventually(fakeScheduler.ScheduleCallCount).Should(Equal(1)) 536 Eventually(fakeJob1.UpdateLastScheduledCallCount).Should(Equal(0)) 537 Eventually(fakeJob2.UpdateLastScheduledCallCount).Should(Equal(1)) 538 Eventually(fakeJob3.UpdateLastScheduledCallCount).Should(Equal(0)) 539 }) 540 }) 541 }) 542 543 Context("when finding jobs to schedule fails", func() { 544 BeforeEach(func() { 545 fakeJobFactory.JobsToScheduleReturns(nil, errors.New("disaster")) 546 }) 547 548 It("returns an error", func() { 549 Expect(schedulerErr).To(Equal(fmt.Errorf("find jobs to schedule: %w", errors.New("disaster")))) 550 }) 551 }) 552 })