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  })