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

     1  package scheduler_test
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  
     8  	"code.cloudfoundry.org/lager/lagertest"
     9  	"github.com/pf-qiu/concourse/v6/atc"
    10  	"github.com/pf-qiu/concourse/v6/atc/db"
    11  	"github.com/pf-qiu/concourse/v6/atc/db/dbfakes"
    12  	. "github.com/pf-qiu/concourse/v6/atc/scheduler"
    13  	"github.com/pf-qiu/concourse/v6/atc/scheduler/schedulerfakes"
    14  	"github.com/pf-qiu/concourse/v6/tracing"
    15  	. "github.com/onsi/ginkgo"
    16  	. "github.com/onsi/gomega"
    17  	"go.opentelemetry.io/otel/api/trace/tracetest"
    18  )
    19  
    20  var _ = Describe("Scheduler", func() {
    21  	var (
    22  		fakeAlgorithm    *schedulerfakes.FakeAlgorithm
    23  		fakeBuildStarter *schedulerfakes.FakeBuildStarter
    24  
    25  		scheduler *Scheduler
    26  
    27  		disaster error
    28  		ctx      context.Context
    29  	)
    30  
    31  	BeforeEach(func() {
    32  		fakeAlgorithm = new(schedulerfakes.FakeAlgorithm)
    33  		fakeBuildStarter = new(schedulerfakes.FakeBuildStarter)
    34  
    35  		scheduler = &Scheduler{
    36  			Algorithm:    fakeAlgorithm,
    37  			BuildStarter: fakeBuildStarter,
    38  		}
    39  
    40  		disaster = errors.New("bad thing")
    41  	})
    42  
    43  	Describe("Schedule", func() {
    44  		var (
    45  			fakePipeline *dbfakes.FakePipeline
    46  			fakeJob      *dbfakes.FakeJob
    47  			scheduleErr  error
    48  		)
    49  
    50  		BeforeEach(func() {
    51  			fakeJob = new(dbfakes.FakeJob)
    52  			fakePipeline = new(dbfakes.FakePipeline)
    53  			fakePipeline.NameReturns("fake-pipeline")
    54  			ctx = context.Background()
    55  		})
    56  
    57  		JustBeforeEach(func() {
    58  			var waiter interface{ Wait() }
    59  
    60  			_, scheduleErr = scheduler.Schedule(
    61  				ctx,
    62  				lagertest.NewTestLogger("test"),
    63  				db.SchedulerJob{
    64  					Job: fakeJob,
    65  					Resources: db.SchedulerResources{
    66  						{
    67  							Name: "some-resource",
    68  						},
    69  					},
    70  				},
    71  			)
    72  			if waiter != nil {
    73  				waiter.Wait()
    74  			}
    75  		})
    76  
    77  		Context("when the job has no inputs", func() {
    78  			BeforeEach(func() {
    79  				fakeJob.NameReturns("some-job-1")
    80  
    81  				fakeJob.AlgorithmInputsReturns(nil, nil)
    82  			})
    83  
    84  			Context("when computing the inputs fails", func() {
    85  				BeforeEach(func() {
    86  					fakeAlgorithm.ComputeReturns(nil, false, false, disaster)
    87  				})
    88  
    89  				It("returns the error", func() {
    90  					Expect(scheduleErr).To(Equal(fmt.Errorf("compute inputs: %w", disaster)))
    91  				})
    92  			})
    93  
    94  			Context("when computing the inputs succeeds", func() {
    95  				var expectedInputMapping db.InputMapping
    96  
    97  				BeforeEach(func() {
    98  					expectedInputMapping = map[string]db.InputResult{
    99  						"input-1": db.InputResult{
   100  							Input: &db.AlgorithmInput{
   101  								AlgorithmVersion: db.AlgorithmVersion{
   102  									ResourceID: 1,
   103  									Version:    db.ResourceVersion("1"),
   104  								},
   105  								FirstOccurrence: true,
   106  							},
   107  						},
   108  					}
   109  
   110  					fakeAlgorithm.ComputeReturns(expectedInputMapping, true, false, nil)
   111  				})
   112  
   113  				It("computed the inputs", func() {
   114  					Expect(fakeAlgorithm.ComputeCallCount()).To(Equal(1))
   115  					_, actualJob, actualInputs := fakeAlgorithm.ComputeArgsForCall(0)
   116  					Expect(actualJob.Name()).To(Equal(fakeJob.Name()))
   117  					Expect(actualInputs).To(BeNil())
   118  				})
   119  
   120  				Context("when the algorithm can run again", func() {
   121  					BeforeEach(func() {
   122  						fakeAlgorithm.ComputeReturns(expectedInputMapping, true, true, nil)
   123  					})
   124  
   125  					It("requests schedule on the pipeline", func() {
   126  						Expect(fakeJob.RequestScheduleCallCount()).To(Equal(1))
   127  					})
   128  				})
   129  
   130  				Context("when the algorithm can not compute a next set of inputs", func() {
   131  					BeforeEach(func() {
   132  						fakeAlgorithm.ComputeReturns(expectedInputMapping, true, false, nil)
   133  					})
   134  
   135  					It("does not request schedule on the pipeline", func() {
   136  						Expect(fakeJob.RequestScheduleCallCount()).To(Equal(0))
   137  					})
   138  				})
   139  
   140  				Context("when saving the next input mapping fails", func() {
   141  					BeforeEach(func() {
   142  						fakeJob.SaveNextInputMappingReturns(disaster)
   143  					})
   144  
   145  					It("returns the error", func() {
   146  						Expect(scheduleErr).To(Equal(fmt.Errorf("save next input mapping: %w", disaster)))
   147  					})
   148  				})
   149  
   150  				Context("when saving the next input mapping succeeds", func() {
   151  					BeforeEach(func() {
   152  						fakeJob.SaveNextInputMappingReturns(nil)
   153  					})
   154  
   155  					It("saved the next input mapping", func() {
   156  						Expect(fakeJob.SaveNextInputMappingCallCount()).To(Equal(1))
   157  						actualInputMapping, resolved := fakeJob.SaveNextInputMappingArgsForCall(0)
   158  						Expect(actualInputMapping).To(Equal(expectedInputMapping))
   159  						Expect(resolved).To(BeTrue())
   160  					})
   161  
   162  					Context("when getting the full next build inputs fails", func() {
   163  						BeforeEach(func() {
   164  							fakeJob.GetFullNextBuildInputsReturns(nil, false, disaster)
   165  						})
   166  
   167  						It("returns the error", func() {
   168  							Expect(scheduleErr).To(Equal(fmt.Errorf("get next build inputs: %w", disaster)))
   169  						})
   170  					})
   171  
   172  					Context("when getting the full next build inputs succeeds", func() {
   173  						BeforeEach(func() {
   174  							fakeJob.GetFullNextBuildInputsReturns([]db.BuildInput{}, true, nil)
   175  						})
   176  
   177  						Context("when starting pending builds for job fails", func() {
   178  							BeforeEach(func() {
   179  								fakeBuildStarter.TryStartPendingBuildsForJobReturns(false, disaster)
   180  							})
   181  
   182  							It("returns the error", func() {
   183  								Expect(scheduleErr).To(Equal(disaster))
   184  							})
   185  
   186  							It("started all pending builds", func() {
   187  								Expect(fakeBuildStarter.TryStartPendingBuildsForJobCallCount()).To(Equal(1))
   188  								_, actualJob, actualInputs := fakeBuildStarter.TryStartPendingBuildsForJobArgsForCall(0)
   189  								Expect(actualJob.Name()).To(Equal(fakeJob.Name()))
   190  								Expect(len(actualJob.Resources)).To(Equal(1))
   191  								Expect(actualJob.Resources[0].Name).To(Equal("some-resource"))
   192  								Expect(actualInputs).To(BeNil())
   193  							})
   194  						})
   195  
   196  						Context("when starting all pending builds succeeds", func() {
   197  							BeforeEach(func() {
   198  								fakeBuildStarter.TryStartPendingBuildsForJobReturns(false, nil)
   199  							})
   200  
   201  							It("returns no error", func() {
   202  								Expect(scheduleErr).NotTo(HaveOccurred())
   203  							})
   204  
   205  							It("didn't create a pending build", func() {
   206  								//TODO: create a positive test case for this
   207  								Expect(fakeJob.EnsurePendingBuildExistsCallCount()).To(BeZero())
   208  							})
   209  						})
   210  					})
   211  				})
   212  
   213  				It("didn't mark the job as having new inputs", func() {
   214  					Expect(fakeJob.SetHasNewInputsCallCount()).To(BeZero())
   215  				})
   216  			})
   217  		})
   218  
   219  		Context("when the job has one trigger: true input", func() {
   220  			BeforeEach(func() {
   221  				fakeJob.NameReturns("some-job")
   222  				fakeJob.AlgorithmInputsReturns(db.InputConfigs{
   223  					{Name: "a", Trigger: true},
   224  					{Name: "b", Trigger: false},
   225  				}, nil)
   226  
   227  				fakeBuildStarter.TryStartPendingBuildsForJobReturns(false, nil)
   228  				fakeJob.SaveNextInputMappingReturns(nil)
   229  			})
   230  
   231  			It("started the builds with the correct arguments", func() {
   232  				Expect(fakeBuildStarter.TryStartPendingBuildsForJobCallCount()).To(Equal(1))
   233  				_, actualJob, actualInputs := fakeBuildStarter.TryStartPendingBuildsForJobArgsForCall(0)
   234  				Expect(actualJob.Name()).To(Equal(fakeJob.Name()))
   235  				Expect(len(actualJob.Resources)).To(Equal(1))
   236  				Expect(actualJob.Resources[0].Name).To(Equal("some-resource"))
   237  				Expect(actualInputs).To(Equal(db.InputConfigs{
   238  					{Name: "a", Trigger: true},
   239  					{Name: "b", Trigger: false},
   240  				}))
   241  			})
   242  
   243  			Context("when no input mapping is found", func() {
   244  				BeforeEach(func() {
   245  					fakeAlgorithm.ComputeReturns(db.InputMapping{}, false, false, nil)
   246  				})
   247  
   248  				It("starts all pending builds and returns no error", func() {
   249  					Expect(fakeBuildStarter.TryStartPendingBuildsForJobCallCount()).To(Equal(1))
   250  					Expect(scheduleErr).NotTo(HaveOccurred())
   251  				})
   252  
   253  				It("didn't create a pending build", func() {
   254  					Expect(fakeJob.EnsurePendingBuildExistsCallCount()).To(BeZero())
   255  				})
   256  
   257  				It("didn't mark the job as having new inputs", func() {
   258  					Expect(fakeJob.SetHasNewInputsCallCount()).To(BeZero())
   259  				})
   260  			})
   261  
   262  			Context("when no first occurrence input has trigger: true", func() {
   263  				BeforeEach(func() {
   264  					fakeJob.GetFullNextBuildInputsReturns([]db.BuildInput{
   265  						{
   266  							Name:            "a",
   267  							Version:         atc.Version{"ref": "v1"},
   268  							ResourceID:      11,
   269  							FirstOccurrence: false,
   270  						},
   271  						{
   272  							Name:            "b",
   273  							Version:         atc.Version{"ref": "v2"},
   274  							ResourceID:      12,
   275  							FirstOccurrence: true,
   276  						},
   277  					}, true, nil)
   278  				})
   279  
   280  				It("starts all pending builds and returns no error", func() {
   281  					Expect(fakeBuildStarter.TryStartPendingBuildsForJobCallCount()).To(Equal(1))
   282  					Expect(scheduleErr).NotTo(HaveOccurred())
   283  				})
   284  
   285  				It("didn't create a pending build", func() {
   286  					Expect(fakeJob.EnsurePendingBuildExistsCallCount()).To(BeZero())
   287  				})
   288  
   289  				Context("when the job does not have new inputs since before", func() {
   290  					BeforeEach(func() {
   291  						fakeJob.HasNewInputsReturns(false)
   292  					})
   293  
   294  					Context("when marking job as having new input fails", func() {
   295  						BeforeEach(func() {
   296  							fakeJob.SetHasNewInputsReturns(disaster)
   297  						})
   298  
   299  						It("returns the error", func() {
   300  							Expect(scheduleErr).To(Equal(fmt.Errorf("set has new inputs: %w", disaster)))
   301  						})
   302  					})
   303  
   304  					Context("when marking job as having new input succeeds", func() {
   305  						BeforeEach(func() {
   306  							fakeJob.SetHasNewInputsReturns(nil)
   307  						})
   308  
   309  						It("did the needful", func() {
   310  							Expect(fakeJob.SetHasNewInputsCallCount()).To(Equal(1))
   311  							Expect(fakeJob.SetHasNewInputsArgsForCall(0)).To(Equal(true))
   312  						})
   313  					})
   314  				})
   315  
   316  				Context("when the job has new inputs since before", func() {
   317  					BeforeEach(func() {
   318  						fakeJob.HasNewInputsReturns(true)
   319  					})
   320  
   321  					It("doesn't mark the job as having new inputs", func() {
   322  						Expect(fakeJob.SetHasNewInputsCallCount()).To(BeZero())
   323  					})
   324  				})
   325  			})
   326  
   327  			Context("when a first occurrence input has trigger: true", func() {
   328  				BeforeEach(func() {
   329  					fakeJob.GetFullNextBuildInputsReturns([]db.BuildInput{
   330  						{
   331  							Name:            "a",
   332  							Version:         atc.Version{"ref": "v1"},
   333  							ResourceID:      11,
   334  							FirstOccurrence: true,
   335  						},
   336  						{
   337  							Name:            "b",
   338  							Version:         atc.Version{"ref": "v2"},
   339  							ResourceID:      12,
   340  							FirstOccurrence: false,
   341  						},
   342  					}, true, nil)
   343  				})
   344  
   345  				Context("when creating a pending build fails", func() {
   346  					BeforeEach(func() {
   347  						fakeJob.EnsurePendingBuildExistsReturns(disaster)
   348  					})
   349  
   350  					It("returns the error", func() {
   351  						Expect(scheduleErr).To(Equal(fmt.Errorf("ensure pending build exists: %w", disaster)))
   352  					})
   353  
   354  					It("created a pending build for the right job", func() {
   355  						Expect(fakeJob.EnsurePendingBuildExistsCallCount()).To(Equal(1))
   356  					})
   357  				})
   358  
   359  				Context("when creating a pending build succeeds", func() {
   360  					BeforeEach(func() {
   361  						fakeJob.EnsurePendingBuildExistsReturns(nil)
   362  					})
   363  
   364  					It("starts all pending builds and returns no error", func() {
   365  						Expect(fakeBuildStarter.TryStartPendingBuildsForJobCallCount()).To(Equal(1))
   366  						Expect(scheduleErr).NotTo(HaveOccurred())
   367  					})
   368  				})
   369  			})
   370  
   371  			Context("when no first occurrence", func() {
   372  				BeforeEach(func() {
   373  					fakeJob.GetFullNextBuildInputsReturns([]db.BuildInput{
   374  						{
   375  							Name:            "a",
   376  							Version:         atc.Version{"ref": "v1"},
   377  							ResourceID:      11,
   378  							FirstOccurrence: false,
   379  						},
   380  						{
   381  							Name:            "b",
   382  							Version:         atc.Version{"ref": "v2"},
   383  							ResourceID:      12,
   384  							FirstOccurrence: false,
   385  						},
   386  					}, true, nil)
   387  				})
   388  
   389  				Context("when job had new inputs", func() {
   390  					BeforeEach(func() {
   391  						fakeJob.HasNewInputsReturns(true)
   392  					})
   393  
   394  					It("marks the job as not having new inputs", func() {
   395  						Expect(fakeJob.SetHasNewInputsCallCount()).To(Equal(1))
   396  						Expect(fakeJob.SetHasNewInputsArgsForCall(0)).To(Equal(false))
   397  					})
   398  				})
   399  
   400  				Context("when job did not have new inputs", func() {
   401  					BeforeEach(func() {
   402  						fakeJob.HasNewInputsReturns(false)
   403  					})
   404  
   405  					It("doesn't mark the the job as not having new inputs again", func() {
   406  						Expect(fakeJob.SetHasNewInputsCallCount()).To(Equal(0))
   407  					})
   408  				})
   409  			})
   410  		})
   411  
   412  		Context("when multiple first occurrence inputs have trigger: true and tracing is configured", func() {
   413  			var inputCtx1, inputCtx2 context.Context
   414  
   415  			BeforeEach(func() {
   416  				fakeJob.NameReturns("some-job")
   417  				fakeJob.AlgorithmInputsReturns(db.InputConfigs{
   418  					{Name: "a", Trigger: true},
   419  					{Name: "b", Trigger: false},
   420  					{Name: "c", Trigger: true},
   421  				}, nil)
   422  				fakeBuildStarter.TryStartPendingBuildsForJobReturns(false, nil)
   423  				fakeJob.SaveNextInputMappingReturns(nil)
   424  
   425  				tracing.ConfigureTraceProvider(tracetest.NewProvider())
   426  
   427  				ctx, _ = tracing.StartSpan(context.Background(), "scheduler.Run", nil)
   428  				inputCtx1, _ = tracing.StartSpan(context.Background(), "checker.Run", nil)
   429  				inputCtx2, _ = tracing.StartSpan(context.Background(), "checker.Run", nil)
   430  				fakeJob.GetFullNextBuildInputsReturns([]db.BuildInput{
   431  					{
   432  						Name:            "a",
   433  						Version:         atc.Version{"ref": "v1"},
   434  						ResourceID:      11,
   435  						FirstOccurrence: true,
   436  						Context:         db.NewSpanContext(inputCtx1),
   437  					},
   438  					{
   439  						Name:            "b",
   440  						Version:         atc.Version{"ref": "v2"},
   441  						ResourceID:      12,
   442  						FirstOccurrence: false,
   443  					},
   444  					{
   445  						Name:            "c",
   446  						Version:         atc.Version{"ref": "v3"},
   447  						ResourceID:      13,
   448  						FirstOccurrence: true,
   449  						Context:         db.NewSpanContext(inputCtx2),
   450  					},
   451  				}, true, nil)
   452  			})
   453  
   454  			AfterEach(func() {
   455  				tracing.Configured = false
   456  			})
   457  
   458  			It("starts a linked span", func() {
   459  				pendingBuildCtx := fakeJob.EnsurePendingBuildExistsArgsForCall(0)
   460  				span := tracing.FromContext(pendingBuildCtx).(*tracetest.Span)
   461  				Expect(span.Links()).To(HaveLen(1))
   462  				Expect(span.Links()).To(HaveKey(tracing.FromContext(ctx).SpanContext()))
   463  				Expect(span.ParentSpanID()).To(Equal(tracing.FromContext(inputCtx1).SpanContext().SpanID))
   464  			})
   465  		})
   466  
   467  		Context("when the job inputs fail to fetch", func() {
   468  			BeforeEach(func() {
   469  				fakeJob.AlgorithmInputsReturns(nil, disaster)
   470  			})
   471  
   472  			It("returns the error", func() {
   473  				Expect(scheduleErr).To(Equal(fmt.Errorf("inputs: %w", disaster)))
   474  			})
   475  		})
   476  	})
   477  })