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