github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/atc/scheduler/buildstarter_test.go (about)

     1  package scheduler_test
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  
     7  	"code.cloudfoundry.org/lager/lagertest"
     8  	"github.com/pf-qiu/concourse/v6/atc"
     9  	"github.com/pf-qiu/concourse/v6/atc/db"
    10  	"github.com/pf-qiu/concourse/v6/atc/db/dbfakes"
    11  	"github.com/pf-qiu/concourse/v6/atc/scheduler"
    12  	"github.com/pf-qiu/concourse/v6/atc/scheduler/schedulerfakes"
    13  
    14  	. "github.com/onsi/ginkgo"
    15  	. "github.com/onsi/gomega"
    16  )
    17  
    18  var _ = Describe("BuildStarter", func() {
    19  	var (
    20  		fakePipeline  *dbfakes.FakePipeline
    21  		fakePlanner   *schedulerfakes.FakeBuildPlanner
    22  		pendingBuilds []db.Build
    23  		fakeAlgorithm *schedulerfakes.FakeAlgorithm
    24  
    25  		buildStarter scheduler.BuildStarter
    26  
    27  		jobInputs db.InputConfigs
    28  
    29  		disaster error
    30  	)
    31  
    32  	BeforeEach(func() {
    33  		fakePipeline = new(dbfakes.FakePipeline)
    34  		fakePlanner = new(schedulerfakes.FakeBuildPlanner)
    35  		fakeAlgorithm = new(schedulerfakes.FakeAlgorithm)
    36  
    37  		buildStarter = scheduler.NewBuildStarter(fakePlanner, fakeAlgorithm)
    38  
    39  		disaster = errors.New("bad thing")
    40  	})
    41  
    42  	Describe("TryStartPendingBuildsForJob", func() {
    43  		var tryStartErr error
    44  		var needsReschedule bool
    45  		var createdBuild *dbfakes.FakeBuild
    46  		var job *dbfakes.FakeJob
    47  		var resources db.SchedulerResources
    48  		var versionedResourceTypes atc.VersionedResourceTypes
    49  
    50  		BeforeEach(func() {
    51  			versionedResourceTypes = atc.VersionedResourceTypes{
    52  				{
    53  					ResourceType: atc.ResourceType{Name: "some-resource-type"},
    54  					Version:      atc.Version{"some": "version"},
    55  				},
    56  			}
    57  
    58  			resources = db.SchedulerResources{
    59  				{
    60  					Name: "some-resource",
    61  				},
    62  			}
    63  		})
    64  
    65  		Context("when pending builds are successfully fetched", func() {
    66  			BeforeEach(func() {
    67  				createdBuild = new(dbfakes.FakeBuild)
    68  				createdBuild.IDReturns(66)
    69  				createdBuild.NameReturns("some-build")
    70  
    71  				pendingBuilds = []db.Build{createdBuild}
    72  
    73  				job = new(dbfakes.FakeJob)
    74  				job.GetPendingBuildsReturns(pendingBuilds, nil)
    75  				job.NameReturns("some-job")
    76  				job.IDReturns(1)
    77  				job.ConfigReturns(atc.JobConfig{
    78  					PlanSequence: []atc.Step{
    79  						{
    80  							Config: &atc.GetStep{
    81  								Name:     "input-1",
    82  								Resource: "some-resource",
    83  							},
    84  						}, {
    85  							Config: &atc.GetStep{
    86  								Name:     "input-2",
    87  								Resource: "some-resource",
    88  							},
    89  						},
    90  					},
    91  				}, nil)
    92  
    93  				jobInputs = db.InputConfigs{
    94  					{
    95  						Name:       "input-1",
    96  						ResourceID: 1,
    97  					},
    98  					{
    99  						Name:       "input-2",
   100  						ResourceID: 1,
   101  					},
   102  				}
   103  			})
   104  
   105  			Context("when one pending build is aborted before start", func() {
   106  				var abortedBuild *dbfakes.FakeBuild
   107  
   108  				BeforeEach(func() {
   109  					abortedBuild = new(dbfakes.FakeBuild)
   110  					abortedBuild.IDReturns(42)
   111  					abortedBuild.IsAbortedReturns(true)
   112  					abortedBuild.FinishReturns(nil)
   113  				})
   114  
   115  				JustBeforeEach(func() {
   116  					needsReschedule, tryStartErr = buildStarter.TryStartPendingBuildsForJob(
   117  						lagertest.NewTestLogger("test"),
   118  						db.SchedulerJob{
   119  							Job:           job,
   120  							Resources:     resources,
   121  							ResourceTypes: versionedResourceTypes,
   122  						},
   123  						jobInputs,
   124  					)
   125  				})
   126  
   127  				Context("when there is one aborted build", func() {
   128  					BeforeEach(func() {
   129  						pendingBuilds = []db.Build{abortedBuild}
   130  						job.GetPendingBuildsReturns(pendingBuilds, nil)
   131  					})
   132  
   133  					It("won't try to start the aborted pending build", func() {
   134  						Expect(abortedBuild.FinishCallCount()).To(Equal(1))
   135  					})
   136  
   137  					It("returns without error", func() {
   138  						Expect(tryStartErr).NotTo(HaveOccurred())
   139  						Expect(needsReschedule).To(BeFalse())
   140  					})
   141  
   142  					Context("when finishing the aborted build fails", func() {
   143  						BeforeEach(func() {
   144  							abortedBuild.FinishReturns(disaster)
   145  						})
   146  
   147  						It("returns an error", func() {
   148  							Expect(tryStartErr).To(Equal(fmt.Errorf("finish aborted build: %w", disaster)))
   149  							Expect(needsReschedule).To(BeFalse())
   150  						})
   151  					})
   152  				})
   153  
   154  				Context("when there is multiple pending builds after the aborted build", func() {
   155  					BeforeEach(func() {
   156  						// make sure pending build can be started after another pending build is aborted
   157  						pendingBuilds = append([]db.Build{abortedBuild}, pendingBuilds...)
   158  						job.GetPendingBuildsReturns(pendingBuilds, nil)
   159  					})
   160  
   161  					It("will try to start the next non aborted pending build", func() {
   162  						Expect(job.ScheduleBuildCallCount()).To(Equal(1))
   163  						actualBuild := job.ScheduleBuildArgsForCall(0)
   164  						Expect(actualBuild.Name()).To(Equal(createdBuild.Name()))
   165  					})
   166  				})
   167  			})
   168  
   169  			Context("when manually triggered", func() {
   170  				BeforeEach(func() {
   171  					createdBuild.IsManuallyTriggeredReturns(true)
   172  
   173  					resources = db.SchedulerResources{
   174  						{
   175  							Name: "some-resource",
   176  						},
   177  					}
   178  				})
   179  
   180  				JustBeforeEach(func() {
   181  					needsReschedule, tryStartErr = buildStarter.TryStartPendingBuildsForJob(
   182  						lagertest.NewTestLogger("test"),
   183  						db.SchedulerJob{
   184  							Job:       job,
   185  							Resources: resources,
   186  						},
   187  						jobInputs,
   188  					)
   189  				})
   190  
   191  				It("tries to schedule the build", func() {
   192  					Expect(job.ScheduleBuildCallCount()).To(Equal(1))
   193  					actualBuild := job.ScheduleBuildArgsForCall(0)
   194  					Expect(actualBuild.Name()).To(Equal(createdBuild.Name()))
   195  				})
   196  
   197  				Context("when the build not scheduled", func() {
   198  					BeforeEach(func() {
   199  						job.ScheduleBuildReturns(false, nil)
   200  					})
   201  
   202  					It("does not start the build and needs to be rescheduled", func() {
   203  						Expect(createdBuild.StartCallCount()).To(BeZero())
   204  						Expect(tryStartErr).ToNot(HaveOccurred())
   205  						Expect(needsReschedule).To(BeTrue())
   206  					})
   207  				})
   208  
   209  				Context("when scheduling the build fails", func() {
   210  					BeforeEach(func() {
   211  						job.ScheduleBuildReturns(false, disaster)
   212  					})
   213  
   214  					It("returns the error", func() {
   215  						Expect(tryStartErr).To(Equal(fmt.Errorf("schedule build: %w", disaster)))
   216  						Expect(needsReschedule).To(BeFalse())
   217  					})
   218  				})
   219  
   220  				Context("when the build is successfully scheduled", func() {
   221  					BeforeEach(func() {
   222  						job.ScheduleBuildReturns(true, nil)
   223  					})
   224  
   225  					Context("when checking if resources have been checked fails", func() {
   226  						BeforeEach(func() {
   227  							createdBuild.ResourcesCheckedReturns(false, disaster)
   228  						})
   229  
   230  						It("returns the error", func() {
   231  							Expect(tryStartErr).To(Equal(fmt.Errorf("ready to determine inputs: %w", disaster)))
   232  							Expect(needsReschedule).To(BeFalse())
   233  						})
   234  					})
   235  
   236  					Context("when some of the resources are checked before build create time", func() {
   237  						BeforeEach(func() {
   238  							createdBuild.ResourcesCheckedReturns(false, nil)
   239  						})
   240  
   241  						It("does not save the next input mapping", func() {
   242  							Expect(fakeAlgorithm.ComputeCallCount()).To(BeZero())
   243  						})
   244  
   245  						It("does not start the build", func() {
   246  							Expect(createdBuild.StartCallCount()).To(BeZero())
   247  						})
   248  
   249  						It("returns without error", func() {
   250  							Expect(tryStartErr).NotTo(HaveOccurred())
   251  						})
   252  
   253  						It("retries to schedule", func() {
   254  							Expect(needsReschedule).To(BeTrue())
   255  						})
   256  					})
   257  
   258  					Context("when all resources are checked after build create time or pinned", func() {
   259  						BeforeEach(func() {
   260  							createdBuild.ResourcesCheckedReturns(true, nil)
   261  						})
   262  
   263  						It("computes a new set of versions for inputs to the build", func() {
   264  							Expect(fakeAlgorithm.ComputeCallCount()).To(Equal(1))
   265  						})
   266  
   267  						Context("when computing the next inputs fails", func() {
   268  							BeforeEach(func() {
   269  								fakeAlgorithm.ComputeReturns(nil, false, false, disaster)
   270  							})
   271  
   272  							It("computes the next inputs for the right job and versions", func() {
   273  								Expect(fakeAlgorithm.ComputeCallCount()).To(Equal(1))
   274  								_, actualJob, actualInputs := fakeAlgorithm.ComputeArgsForCall(0)
   275  								Expect(actualJob).To(Equal(
   276  									db.SchedulerJob{
   277  										Job:       job,
   278  										Resources: resources,
   279  									}))
   280  								Expect(actualInputs).To(Equal(jobInputs))
   281  							})
   282  
   283  							It("returns the error and retries to schedule", func() {
   284  								Expect(tryStartErr).To(Equal(fmt.Errorf("get build inputs: %w", fmt.Errorf("compute inputs: %w", disaster))))
   285  								Expect(needsReschedule).To(BeFalse())
   286  							})
   287  						})
   288  
   289  						Context("when computing the next inputs succeeds", func() {
   290  							var expectedInputMapping db.InputMapping
   291  
   292  							BeforeEach(func() {
   293  								expectedInputMapping = map[string]db.InputResult{
   294  									"input-1": db.InputResult{
   295  										Input: &db.AlgorithmInput{
   296  											AlgorithmVersion: db.AlgorithmVersion{
   297  												ResourceID: 1,
   298  												Version:    db.ResourceVersion("1"),
   299  											},
   300  											FirstOccurrence: true,
   301  										},
   302  									},
   303  								}
   304  
   305  								fakeAlgorithm.ComputeReturns(expectedInputMapping, true, false, nil)
   306  							})
   307  
   308  							Context("when the algorithm can run again", func() {
   309  								BeforeEach(func() {
   310  									fakeAlgorithm.ComputeReturns(expectedInputMapping, true, true, nil)
   311  								})
   312  
   313  								It("requests schedule on the job", func() {
   314  									Expect(job.RequestScheduleCallCount()).To(Equal(1))
   315  								})
   316  
   317  								Context("when requesting schedule fails", func() {
   318  									BeforeEach(func() {
   319  										job.RequestScheduleReturns(disaster)
   320  									})
   321  
   322  									It("returns the error and retries to schedule", func() {
   323  										Expect(tryStartErr).To(Equal(fmt.Errorf("get build inputs: %w", fmt.Errorf("request schedule: %w", disaster))))
   324  										Expect(needsReschedule).To(BeFalse())
   325  									})
   326  								})
   327  							})
   328  
   329  							Context("when the algorithm can not run again", func() {
   330  								BeforeEach(func() {
   331  									fakeAlgorithm.ComputeReturns(expectedInputMapping, true, false, nil)
   332  								})
   333  
   334  								It("does not requests schedule on the job", func() {
   335  									Expect(job.RequestScheduleCallCount()).To(Equal(0))
   336  								})
   337  							})
   338  
   339  							It("saves the next input mapping", func() {
   340  								Expect(job.SaveNextInputMappingCallCount()).To(Equal(1))
   341  							})
   342  
   343  							Context("when saving the next input mapping fails", func() {
   344  								BeforeEach(func() {
   345  									job.SaveNextInputMappingReturns(disaster)
   346  								})
   347  
   348  								It("saves the next input mapping with the right inputs", func() {
   349  									actualInputMapping, resolved := job.SaveNextInputMappingArgsForCall(0)
   350  									Expect(actualInputMapping).To(Equal(expectedInputMapping))
   351  									Expect(resolved).To(BeTrue())
   352  								})
   353  
   354  								It("returns the error and retries to schedule", func() {
   355  									Expect(tryStartErr).To(Equal(fmt.Errorf("get build inputs: %w", fmt.Errorf("save next input mapping: %w", disaster))))
   356  									Expect(needsReschedule).To(BeFalse())
   357  								})
   358  							})
   359  
   360  							Context("when saving the next input mapping succeeds", func() {
   361  								BeforeEach(func() {
   362  									job.SaveNextInputMappingReturns(nil)
   363  								})
   364  
   365  								It("saved the next input mapping and adopts the inputs and pipes", func() {
   366  									Expect(createdBuild.AdoptInputsAndPipesCallCount()).To(Equal(1))
   367  									Expect(tryStartErr).NotTo(HaveOccurred())
   368  								})
   369  							})
   370  
   371  							Context("when adopting inputs and pipes succeeds", func() {
   372  								BeforeEach(func() {
   373  									createdBuild.AdoptInputsAndPipesReturns([]db.BuildInput{}, true, nil)
   374  								})
   375  
   376  								It("tries to fetch the job config", func() {
   377  									Expect(job.ConfigCallCount()).To(Equal(1))
   378  								})
   379  							})
   380  
   381  							Context("when adopting inputs and pipes fails", func() {
   382  								BeforeEach(func() {
   383  									createdBuild.AdoptInputsAndPipesReturns(nil, false, errors.New("error"))
   384  								})
   385  
   386  								It("returns an error and retries to schedule", func() {
   387  									Expect(tryStartErr).To(HaveOccurred())
   388  									Expect(needsReschedule).To(BeFalse())
   389  								})
   390  							})
   391  
   392  							Context("when adopting inputs and pipes has no satisfiable inputs", func() {
   393  								BeforeEach(func() {
   394  									createdBuild.AdoptInputsAndPipesReturns(nil, false, nil)
   395  								})
   396  
   397  								It("does not return an error and does not try to reschedule", func() {
   398  									Expect(tryStartErr).ToNot(HaveOccurred())
   399  									Expect(needsReschedule).To(BeFalse())
   400  								})
   401  							})
   402  						})
   403  					})
   404  				})
   405  			})
   406  
   407  			Context("when not manually triggered", func() {
   408  				var pendingBuild1 *dbfakes.FakeBuild
   409  				var pendingBuild2 *dbfakes.FakeBuild
   410  				var rerunBuild *dbfakes.FakeBuild
   411  
   412  				var jobConfig = atc.JobConfig{
   413  					Name: "some-job",
   414  					PlanSequence: []atc.Step{
   415  						{
   416  							Config: &atc.GetStep{
   417  								Name: "some-input",
   418  							},
   419  						},
   420  					},
   421  				}
   422  
   423  				var plannedPlan = atc.Plan{
   424  					Get: &atc.GetPlan{
   425  						Name:     "some-input",
   426  						Resource: "some-input",
   427  					},
   428  				}
   429  
   430  				BeforeEach(func() {
   431  					job.NameReturns("some-job")
   432  					job.IDReturns(1)
   433  					job.ConfigReturns(jobConfig, nil)
   434  					createdBuild.IsManuallyTriggeredReturns(false)
   435  
   436  					jobInputs = db.InputConfigs{}
   437  				})
   438  
   439  				JustBeforeEach(func() {
   440  					needsReschedule, tryStartErr = buildStarter.TryStartPendingBuildsForJob(
   441  						lagertest.NewTestLogger("test"),
   442  						db.SchedulerJob{
   443  							Job:       job,
   444  							Resources: resources,
   445  							ResourceTypes: atc.VersionedResourceTypes{
   446  								{
   447  									ResourceType: atc.ResourceType{
   448  										Name: "some-resource-type",
   449  									},
   450  									Version: atc.Version{"some": "version"},
   451  								},
   452  							},
   453  						},
   454  						jobInputs,
   455  					)
   456  				})
   457  
   458  				It("doesn't compute the algorithm", func() {
   459  					Expect(fakeAlgorithm.ComputeCallCount()).To(Equal(0))
   460  				})
   461  
   462  				itScheduledAllBuilds := func() {
   463  					It("scheduled all the pending builds", func() {
   464  						Expect(job.ScheduleBuildCallCount()).To(Equal(3))
   465  						actualBuild := job.ScheduleBuildArgsForCall(0)
   466  						Expect(actualBuild.ID()).To(Equal(pendingBuild1.ID()))
   467  
   468  						actualBuild = job.ScheduleBuildArgsForCall(1)
   469  						Expect(actualBuild.ID()).To(Equal(rerunBuild.ID()))
   470  
   471  						actualBuild = job.ScheduleBuildArgsForCall(2)
   472  						Expect(actualBuild.ID()).To(Equal(pendingBuild2.ID()))
   473  					})
   474  				}
   475  
   476  				Context("when the stars align", func() {
   477  					BeforeEach(func() {
   478  						job.PausedReturns(false)
   479  						job.ScheduleBuildReturns(true, nil)
   480  						fakePipeline.PausedReturns(false)
   481  					})
   482  
   483  					Context("when adopting inputs and pipes for a rerun build fails", func() {
   484  						BeforeEach(func() {
   485  							pendingBuild1 = new(dbfakes.FakeBuild)
   486  							pendingBuild1.IDReturns(99)
   487  							pendingBuild1.RerunOfReturns(1)
   488  							pendingBuild1.AdoptRerunInputsAndPipesReturns([]db.BuildInput{{Name: "some-input"}}, false, disaster)
   489  							job.GetPendingBuildsReturns([]db.Build{pendingBuild1}, nil)
   490  						})
   491  
   492  						It("returns the error and retries to schedule", func() {
   493  							Expect(tryStartErr).To(Equal(fmt.Errorf("get build inputs: %w", fmt.Errorf("adopt rerun inputs and pipes: %w", disaster))))
   494  							Expect(needsReschedule).To(BeFalse())
   495  						})
   496  					})
   497  
   498  					Context("when adopting inputs and pipes for a rerun build has no satisfiable inputs", func() {
   499  						BeforeEach(func() {
   500  							pendingBuild1 = new(dbfakes.FakeBuild)
   501  							pendingBuild1.IDReturns(99)
   502  							pendingBuild1.RerunOfReturns(1)
   503  							pendingBuild1.AdoptRerunInputsAndPipesReturns([]db.BuildInput{{Name: "some-input"}}, false, nil)
   504  							job.GetPendingBuildsReturns([]db.Build{pendingBuild1}, nil)
   505  						})
   506  
   507  						It("returns the error and does not retry to schedule", func() {
   508  							Expect(tryStartErr).ToNot(HaveOccurred())
   509  							Expect(needsReschedule).To(BeFalse())
   510  						})
   511  					})
   512  
   513  					Context("when adopting inputs and pipes for a normal scheduler build fails", func() {
   514  						BeforeEach(func() {
   515  							pendingBuild1 = new(dbfakes.FakeBuild)
   516  							pendingBuild1.IDReturns(99)
   517  							pendingBuild1.AdoptInputsAndPipesReturns([]db.BuildInput{{Name: "some-input"}}, false, disaster)
   518  							job.GetPendingBuildsReturns([]db.Build{pendingBuild1}, nil)
   519  						})
   520  
   521  						It("returns the error and retries to schedule", func() {
   522  							Expect(tryStartErr).To(Equal(fmt.Errorf("get build inputs: %w", fmt.Errorf("adopt inputs and pipes: %w", disaster))))
   523  							Expect(needsReschedule).To(BeFalse())
   524  						})
   525  					})
   526  
   527  					Context("when adopting inputs and pipes for a normal scheduler build has no satisfiable inputs", func() {
   528  						BeforeEach(func() {
   529  							pendingBuild1 = new(dbfakes.FakeBuild)
   530  							pendingBuild1.IDReturns(99)
   531  							pendingBuild1.AdoptInputsAndPipesReturns([]db.BuildInput{{Name: "some-input"}}, false, nil)
   532  							job.GetPendingBuildsReturns([]db.Build{pendingBuild1}, nil)
   533  						})
   534  
   535  						It("returns the error and does not retry to schedule", func() {
   536  							Expect(tryStartErr).ToNot(HaveOccurred())
   537  							Expect(needsReschedule).To(BeFalse())
   538  						})
   539  					})
   540  
   541  					Context("when there are several pending builds consisting of both retrigger and normal scheduler builds", func() {
   542  						BeforeEach(func() {
   543  							pendingBuild1 = new(dbfakes.FakeBuild)
   544  							pendingBuild1.IDReturns(99)
   545  							pendingBuild1.AdoptInputsAndPipesReturns([]db.BuildInput{{Name: "some-input"}}, true, nil)
   546  							job.ScheduleBuildReturnsOnCall(0, true, nil)
   547  							pendingBuild2 = new(dbfakes.FakeBuild)
   548  							pendingBuild2.IDReturns(999)
   549  							pendingBuild2.AdoptInputsAndPipesReturns([]db.BuildInput{{Name: "some-input"}}, true, nil)
   550  							job.ScheduleBuildReturnsOnCall(1, true, nil)
   551  							rerunBuild = new(dbfakes.FakeBuild)
   552  							rerunBuild.IDReturns(555)
   553  							rerunBuild.RerunOfReturns(pendingBuild1.ID())
   554  							rerunBuild.AdoptRerunInputsAndPipesReturns([]db.BuildInput{{Name: "some-input"}}, true, nil)
   555  							job.ScheduleBuildReturnsOnCall(2, true, nil)
   556  							pendingBuilds = []db.Build{pendingBuild1, rerunBuild, pendingBuild2}
   557  							job.GetPendingBuildsReturns(pendingBuilds, nil)
   558  						})
   559  
   560  						Context("when marking the build as scheduled fails", func() {
   561  							BeforeEach(func() {
   562  								job.ScheduleBuildReturnsOnCall(0, false, disaster)
   563  							})
   564  
   565  							It("returns the error", func() {
   566  								Expect(tryStartErr).To(Equal(fmt.Errorf("schedule build: %w", disaster)))
   567  							})
   568  
   569  							It("only tried to schedule one pending build", func() {
   570  								Expect(job.ScheduleBuildCallCount()).To(Equal(1))
   571  							})
   572  						})
   573  
   574  						Context("when the build was not able to be scheduled", func() {
   575  							BeforeEach(func() {
   576  								job.ScheduleBuildReturnsOnCall(0, false, nil)
   577  							})
   578  
   579  							It("doesn't return an error", func() {
   580  								Expect(tryStartErr).NotTo(HaveOccurred())
   581  							})
   582  
   583  							It("doesn't try adopt build inputs and pipes for that pending build and doesn't try scheduling the next ones", func() {
   584  								Expect(pendingBuild1.AdoptInputsAndPipesCallCount()).To(BeZero())
   585  								Expect(pendingBuild2.AdoptInputsAndPipesCallCount()).To(BeZero())
   586  								Expect(rerunBuild.AdoptRerunInputsAndPipesCallCount()).To(BeZero())
   587  							})
   588  						})
   589  
   590  						Context("when the build was scheduled successfully", func() {
   591  							Context("when the resource types are successfully fetched", func() {
   592  								Context("when creating the build plan fails for the rerun build and the scheduler builds", func() {
   593  									BeforeEach(func() {
   594  										fakePlanner.CreateReturns(atc.Plan{}, disaster)
   595  									})
   596  
   597  									It("keeps going after failing to create", func() {
   598  										Expect(fakePlanner.CreateCallCount()).To(Equal(3))
   599  
   600  										Expect(rerunBuild.FinishCallCount()).To(Equal(1))
   601  										Expect(pendingBuild1.FinishCallCount()).To(Equal(1))
   602  										Expect(pendingBuild2.FinishCallCount()).To(Equal(1))
   603  									})
   604  
   605  									Context("when marking the build as errored fails", func() {
   606  										BeforeEach(func() {
   607  											pendingBuild1.FinishReturns(disaster)
   608  										})
   609  
   610  										It("returns an error", func() {
   611  											Expect(tryStartErr).To(Equal(fmt.Errorf("finish build: %w", disaster)))
   612  											Expect(needsReschedule).To(BeFalse())
   613  										})
   614  
   615  										It("does not start the other pending build", func() {
   616  											Expect(pendingBuild2.StartCallCount()).To(Equal(0))
   617  										})
   618  
   619  										It("marked the right build as errored", func() {
   620  											Expect(pendingBuild1.FinishCallCount()).To(Equal(1))
   621  											actualStatus := pendingBuild1.FinishArgsForCall(0)
   622  											Expect(actualStatus).To(Equal(db.BuildStatusErrored))
   623  										})
   624  									})
   625  
   626  									Context("when marking the build as errored succeeds", func() {
   627  										BeforeEach(func() {
   628  											pendingBuild1.FinishReturns(nil)
   629  										})
   630  
   631  										It("does not start the other builds", func() {
   632  											Expect(pendingBuild2.StartCallCount()).To(Equal(0))
   633  										})
   634  
   635  										It("doesn't return an error", func() {
   636  											Expect(tryStartErr).NotTo(HaveOccurred())
   637  											Expect(needsReschedule).To(BeFalse())
   638  										})
   639  									})
   640  								})
   641  
   642  								Context("when creating the build plan succeeds", func() {
   643  									BeforeEach(func() {
   644  										fakePlanner.CreateReturns(plannedPlan, nil)
   645  										pendingBuild1.StartReturns(true, nil)
   646  										pendingBuild2.StartReturns(true, nil)
   647  										rerunBuild.StartReturns(true, nil)
   648  									})
   649  
   650  									It("adopts the build inputs and pipes", func() {
   651  										Expect(pendingBuild1.AdoptInputsAndPipesCallCount()).To(Equal(1))
   652  										Expect(pendingBuild1.AdoptRerunInputsAndPipesCallCount()).To(BeZero())
   653  
   654  										Expect(pendingBuild2.AdoptInputsAndPipesCallCount()).To(Equal(1))
   655  										Expect(pendingBuild2.AdoptRerunInputsAndPipesCallCount()).To(BeZero())
   656  
   657  										Expect(rerunBuild.AdoptInputsAndPipesCallCount()).To(BeZero())
   658  										Expect(rerunBuild.AdoptRerunInputsAndPipesCallCount()).To(Equal(1))
   659  									})
   660  
   661  									It("creates build plans for all builds", func() {
   662  										Expect(fakePlanner.CreateCallCount()).To(Equal(3))
   663  
   664  										actualPlanConfig, actualResourceConfigs, actualResourceTypes, actualBuildInputs := fakePlanner.CreateArgsForCall(0)
   665  										Expect(actualPlanConfig).To(Equal(&atc.DoStep{Steps: jobConfig.PlanSequence}))
   666  										Expect(actualResourceConfigs).To(Equal(db.SchedulerResources{{Name: "some-resource"}}))
   667  										Expect(actualResourceTypes).To(Equal(versionedResourceTypes))
   668  										Expect(actualBuildInputs).To(Equal([]db.BuildInput{{Name: "some-input"}}))
   669  
   670  										actualPlanConfig, actualResourceConfigs, actualResourceTypes, actualBuildInputs = fakePlanner.CreateArgsForCall(1)
   671  										Expect(actualPlanConfig).To(Equal(&atc.DoStep{Steps: jobConfig.PlanSequence}))
   672  										Expect(actualResourceConfigs).To(Equal(db.SchedulerResources{{Name: "some-resource"}}))
   673  										Expect(actualResourceTypes).To(Equal(versionedResourceTypes))
   674  										Expect(actualBuildInputs).To(Equal([]db.BuildInput{{Name: "some-input"}}))
   675  
   676  										actualPlanConfig, actualResourceConfigs, actualResourceTypes, actualBuildInputs = fakePlanner.CreateArgsForCall(2)
   677  										Expect(actualPlanConfig).To(Equal(&atc.DoStep{Steps: jobConfig.PlanSequence}))
   678  										Expect(actualResourceConfigs).To(Equal(db.SchedulerResources{{Name: "some-resource"}}))
   679  										Expect(actualResourceTypes).To(Equal(versionedResourceTypes))
   680  										Expect(actualBuildInputs).To(Equal([]db.BuildInput{{Name: "some-input"}}))
   681  									})
   682  
   683  									Context("when starting the build fails", func() {
   684  										BeforeEach(func() {
   685  											pendingBuild1.StartReturns(false, disaster)
   686  										})
   687  
   688  										It("returns the error", func() {
   689  											Expect(tryStartErr).To(Equal(fmt.Errorf("start build: %w", disaster)))
   690  											Expect(needsReschedule).To(BeFalse())
   691  										})
   692  
   693  										It("does not start the other builds", func() {
   694  											Expect(pendingBuild2.StartCallCount()).To(Equal(0))
   695  										})
   696  									})
   697  
   698  									Context("when starting the build returns false", func() {
   699  										BeforeEach(func() {
   700  											pendingBuild1.StartReturns(false, nil)
   701  										})
   702  
   703  										It("doesn't return an error", func() {
   704  											Expect(tryStartErr).NotTo(HaveOccurred())
   705  											Expect(needsReschedule).To(BeFalse())
   706  										})
   707  
   708  										It("starts the other builds", func() {
   709  											Expect(pendingBuild2.StartCallCount()).To(Equal(1))
   710  										})
   711  
   712  										It("finishes the build with aborted status", func() {
   713  											Expect(pendingBuild1.FinishCallCount()).To(Equal(1))
   714  											Expect(pendingBuild1.FinishArgsForCall(0)).To(Equal(db.BuildStatusAborted))
   715  										})
   716  
   717  										Context("when marking the build as errored fails", func() {
   718  											BeforeEach(func() {
   719  												pendingBuild1.FinishReturns(disaster)
   720  											})
   721  
   722  											It("returns an error", func() {
   723  												Expect(tryStartErr).To(Equal(fmt.Errorf("finish build: %w", disaster)))
   724  												Expect(needsReschedule).To(BeFalse())
   725  											})
   726  
   727  											It("does not start the other builds", func() {
   728  												Expect(pendingBuild2.StartCallCount()).To(Equal(0))
   729  											})
   730  
   731  											It("marked the right build as errored", func() {
   732  												Expect(pendingBuild1.FinishCallCount()).To(Equal(1))
   733  												actualStatus := pendingBuild1.FinishArgsForCall(0)
   734  												Expect(actualStatus).To(Equal(db.BuildStatusAborted))
   735  											})
   736  										})
   737  
   738  										Context("when marking the build as errored succeeds", func() {
   739  											BeforeEach(func() {
   740  												pendingBuild1.FinishReturns(nil)
   741  											})
   742  
   743  											It("doesn't return an error", func() {
   744  												Expect(tryStartErr).NotTo(HaveOccurred())
   745  												Expect(needsReschedule).To(BeFalse())
   746  											})
   747  
   748  											It("starts the other builds", func() {
   749  												Expect(pendingBuild2.StartCallCount()).To(Equal(1))
   750  											})
   751  										})
   752  									})
   753  
   754  									Context("when starting the builds returns true", func() {
   755  										BeforeEach(func() {
   756  											pendingBuild1.StartReturns(true, nil)
   757  											pendingBuild2.StartReturns(true, nil)
   758  											rerunBuild.StartReturns(true, nil)
   759  										})
   760  
   761  										It("doesn't return an error", func() {
   762  											Expect(tryStartErr).NotTo(HaveOccurred())
   763  											Expect(needsReschedule).To(BeFalse())
   764  										})
   765  
   766  										itScheduledAllBuilds()
   767  
   768  										It("starts the build with the right plan", func() {
   769  											Expect(pendingBuild1.StartCallCount()).To(Equal(1))
   770  											Expect(pendingBuild1.StartArgsForCall(0)).To(Equal(plannedPlan))
   771  
   772  											Expect(pendingBuild2.StartCallCount()).To(Equal(1))
   773  											Expect(pendingBuild2.StartArgsForCall(0)).To(Equal(plannedPlan))
   774  
   775  											Expect(rerunBuild.StartCallCount()).To(Equal(1))
   776  											Expect(rerunBuild.StartArgsForCall(0)).To(Equal(plannedPlan))
   777  										})
   778  									})
   779  								})
   780  							})
   781  						})
   782  
   783  						Context("when adopting the inputs and pipes fails", func() {
   784  							BeforeEach(func() {
   785  								pendingBuild1.AdoptInputsAndPipesReturns(nil, false, disaster)
   786  							})
   787  
   788  							It("returns the error", func() {
   789  								Expect(tryStartErr).To(Equal(fmt.Errorf("get build inputs: %w", fmt.Errorf("adopt inputs and pipes: %w", disaster))))
   790  								Expect(needsReschedule).To(BeFalse())
   791  							})
   792  						})
   793  
   794  						Context("when there are no next build inputs", func() {
   795  							BeforeEach(func() {
   796  								pendingBuild1.AdoptInputsAndPipesReturns(nil, false, nil)
   797  							})
   798  
   799  							It("doesn't return an error", func() {
   800  								Expect(tryStartErr).NotTo(HaveOccurred())
   801  								Expect(needsReschedule).To(BeFalse())
   802  							})
   803  
   804  							It("does not start the build", func() {
   805  								Expect(createdBuild.StartCallCount()).To(BeZero())
   806  							})
   807  						})
   808  
   809  						Context("when fetching pending builds fail", func() {
   810  							BeforeEach(func() {
   811  								job.GetPendingBuildsReturns(nil, disaster)
   812  							})
   813  
   814  							It("returns the error", func() {
   815  								Expect(tryStartErr).To(Equal(fmt.Errorf("get pending builds: %w", disaster)))
   816  							})
   817  
   818  							It("does not need to be rescheduled", func() {
   819  								Expect(needsReschedule).To(BeFalse())
   820  							})
   821  						})
   822  					})
   823  
   824  					Context("when there are several pending builds with one failing to start rerun build", func() {
   825  						BeforeEach(func() {
   826  							pendingBuild1 = new(dbfakes.FakeBuild)
   827  							pendingBuild1.IDReturns(99)
   828  							pendingBuild1.AdoptInputsAndPipesReturns([]db.BuildInput{{Name: "some-input"}}, true, nil)
   829  							pendingBuild1.StartReturns(true, nil)
   830  							job.ScheduleBuildReturnsOnCall(0, true, nil)
   831  							pendingBuild2 = new(dbfakes.FakeBuild)
   832  							pendingBuild2.IDReturns(999)
   833  							pendingBuild2.AdoptInputsAndPipesReturns([]db.BuildInput{{Name: "some-input"}}, true, nil)
   834  							pendingBuild2.StartReturns(true, nil)
   835  							job.ScheduleBuildReturnsOnCall(2, true, nil)
   836  						})
   837  
   838  						Context("when the rerun build is failing to adopt inputs and outputs", func() {
   839  							BeforeEach(func() {
   840  								rerunBuild = new(dbfakes.FakeBuild)
   841  								rerunBuild.IDReturns(555)
   842  								rerunBuild.RerunOfReturns(pendingBuild1.ID())
   843  								rerunBuild.AdoptRerunInputsAndPipesReturns(nil, false, errors.New("error"))
   844  								job.ScheduleBuildReturnsOnCall(1, true, nil)
   845  								pendingBuilds = []db.Build{pendingBuild1, rerunBuild, pendingBuild2}
   846  								job.GetPendingBuildsReturns(pendingBuilds, nil)
   847  							})
   848  
   849  							It("does not schedule the next build", func() {
   850  								Expect(tryStartErr).To(HaveOccurred())
   851  								Expect(pendingBuild1.StartCallCount()).To(Equal(1))
   852  								Expect(pendingBuild2.StartCallCount()).To(Equal(0))
   853  							})
   854  						})
   855  
   856  						Context("when the rerun build is not started because it has no inputs or versions", func() {
   857  							BeforeEach(func() {
   858  								rerunBuild = new(dbfakes.FakeBuild)
   859  								rerunBuild.IDReturns(555)
   860  								rerunBuild.RerunOfReturns(pendingBuild1.ID())
   861  								rerunBuild.AdoptRerunInputsAndPipesReturns(nil, false, nil)
   862  								job.ScheduleBuildReturnsOnCall(1, true, nil)
   863  								pendingBuilds = []db.Build{pendingBuild1, rerunBuild, pendingBuild2}
   864  								job.GetPendingBuildsReturns(pendingBuilds, nil)
   865  							})
   866  
   867  							It("tries to schedule the 2 other pending builds", func() {
   868  								Expect(tryStartErr).ToNot(HaveOccurred())
   869  								Expect(needsReschedule).To(BeFalse())
   870  								Expect(pendingBuild1.StartCallCount()).To(Equal(1))
   871  								Expect(pendingBuild2.StartCallCount()).To(Equal(1))
   872  							})
   873  						})
   874  
   875  						Context("when the rerun build needs to retry a new scheduler tick", func() {
   876  							BeforeEach(func() {
   877  								rerunBuild = new(dbfakes.FakeBuild)
   878  								rerunBuild.IDReturns(555)
   879  								rerunBuild.RerunOfReturns(pendingBuild1.ID())
   880  								job.ScheduleBuildReturnsOnCall(1, false, nil)
   881  								pendingBuilds = []db.Build{pendingBuild1, rerunBuild, pendingBuild2}
   882  								job.GetPendingBuildsReturns(pendingBuilds, nil)
   883  							})
   884  
   885  							It("does not try to schedule the other pending build", func() {
   886  								Expect(tryStartErr).ToNot(HaveOccurred())
   887  								Expect(needsReschedule).To(BeTrue())
   888  								Expect(pendingBuild1.StartCallCount()).To(Equal(1))
   889  								Expect(pendingBuild2.StartCallCount()).To(Equal(0))
   890  							})
   891  						})
   892  					})
   893  				})
   894  			})
   895  		})
   896  	})
   897  })