github.com/hernad/nomad@v1.6.112/nomad/periodic_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package nomad 5 6 import ( 7 "fmt" 8 "math/rand" 9 "reflect" 10 "sort" 11 "strconv" 12 "strings" 13 "sync" 14 "testing" 15 "time" 16 17 "github.com/hernad/nomad/ci" 18 "github.com/hernad/nomad/helper/testlog" 19 "github.com/hernad/nomad/nomad/mock" 20 "github.com/hernad/nomad/nomad/structs" 21 "github.com/hernad/nomad/testutil" 22 "github.com/stretchr/testify/assert" 23 "github.com/stretchr/testify/require" 24 ) 25 26 type MockJobEvalDispatcher struct { 27 Jobs map[structs.NamespacedID]*structs.Job 28 lock sync.Mutex 29 } 30 31 func NewMockJobEvalDispatcher() *MockJobEvalDispatcher { 32 return &MockJobEvalDispatcher{Jobs: make(map[structs.NamespacedID]*structs.Job)} 33 } 34 35 func (m *MockJobEvalDispatcher) DispatchJob(job *structs.Job) (*structs.Evaluation, error) { 36 m.lock.Lock() 37 defer m.lock.Unlock() 38 tuple := structs.NamespacedID{ 39 ID: job.ID, 40 Namespace: job.Namespace, 41 } 42 m.Jobs[tuple] = job 43 return nil, nil 44 } 45 46 func (m *MockJobEvalDispatcher) RunningChildren(parent *structs.Job) (bool, error) { 47 m.lock.Lock() 48 defer m.lock.Unlock() 49 for _, job := range m.Jobs { 50 if job.ParentID == parent.ID && job.Namespace == parent.Namespace { 51 return true, nil 52 } 53 } 54 return false, nil 55 } 56 57 // LaunchTimes returns the launch times of child jobs in sorted order. 58 func (m *MockJobEvalDispatcher) LaunchTimes(p *PeriodicDispatch, namespace, parentID string) ([]time.Time, error) { 59 m.lock.Lock() 60 defer m.lock.Unlock() 61 var launches []time.Time 62 for _, job := range m.Jobs { 63 if job.ParentID != parentID || job.Namespace != namespace { 64 continue 65 } 66 67 t, err := p.LaunchTime(job.ID) 68 if err != nil { 69 return nil, err 70 } 71 launches = append(launches, t) 72 } 73 sort.Sort(times(launches)) 74 return launches, nil 75 } 76 77 func (m *MockJobEvalDispatcher) dispatchedJobs(parent *structs.Job) []*structs.Job { 78 m.lock.Lock() 79 defer m.lock.Unlock() 80 81 jobs := []*structs.Job{} 82 for _, job := range m.Jobs { 83 if job.ParentID == parent.ID && job.Namespace == parent.Namespace { 84 jobs = append(jobs, job) 85 } 86 } 87 88 return jobs 89 } 90 91 type times []time.Time 92 93 func (t times) Len() int { return len(t) } 94 func (t times) Swap(i, j int) { t[i], t[j] = t[j], t[i] } 95 func (t times) Less(i, j int) bool { return t[i].Before(t[j]) } 96 97 // testPeriodicDispatcher returns an enabled PeriodicDispatcher which uses the 98 // MockJobEvalDispatcher. 99 func testPeriodicDispatcher(t *testing.T) (*PeriodicDispatch, *MockJobEvalDispatcher) { 100 logger := testlog.HCLogger(t) 101 m := NewMockJobEvalDispatcher() 102 d := NewPeriodicDispatch(logger, m) 103 t.Cleanup(func() { d.SetEnabled(false) }) 104 d.SetEnabled(true) 105 return d, m 106 } 107 108 // testPeriodicJob is a helper that creates a periodic job that launches at the 109 // passed times. 110 func testPeriodicJob(times ...time.Time) *structs.Job { 111 job := mock.PeriodicJob() 112 job.Periodic.SpecType = structs.PeriodicSpecTest 113 114 l := make([]string, len(times)) 115 for i, t := range times { 116 l[i] = strconv.Itoa(int(t.Round(1 * time.Second).Unix())) 117 } 118 119 job.Periodic.Spec = strings.Join(l, ",") 120 return job 121 } 122 123 // TestPeriodicDispatch_SetEnabled test that setting enabled twice is a no-op. 124 // This tests the reported issue: https://github.com/hernad/nomad/issues/2829 125 func TestPeriodicDispatch_SetEnabled(t *testing.T) { 126 ci.Parallel(t) 127 p, _ := testPeriodicDispatcher(t) 128 129 // SetEnabled has been called once but do it again. 130 p.SetEnabled(true) 131 132 // Now disable and make sure everything is fine. 133 p.SetEnabled(false) 134 135 // Enable and track something 136 p.SetEnabled(true) 137 job := mock.PeriodicJob() 138 if err := p.Add(job); err != nil { 139 t.Fatalf("Add failed %v", err) 140 } 141 142 tracked := p.Tracked() 143 if len(tracked) != 1 { 144 t.Fatalf("Add didn't track the job: %v", tracked) 145 } 146 } 147 148 func TestPeriodicDispatch_Add_NonPeriodic(t *testing.T) { 149 ci.Parallel(t) 150 p, _ := testPeriodicDispatcher(t) 151 job := mock.Job() 152 if err := p.Add(job); err != nil { 153 t.Fatalf("Add of non-periodic job failed: %v; expect no-op", err) 154 } 155 156 tracked := p.Tracked() 157 if len(tracked) != 0 { 158 t.Fatalf("Add of non-periodic job should be no-op: %v", tracked) 159 } 160 } 161 162 func TestPeriodicDispatch_Add_Periodic_Parameterized(t *testing.T) { 163 ci.Parallel(t) 164 p, _ := testPeriodicDispatcher(t) 165 job := mock.PeriodicJob() 166 job.ParameterizedJob = &structs.ParameterizedJobConfig{} 167 if err := p.Add(job); err != nil { 168 t.Fatalf("Add of periodic parameterized job failed: %v", err) 169 } 170 171 tracked := p.Tracked() 172 if len(tracked) != 0 { 173 t.Fatalf("Add of periodic parameterized job should be no-op: %v", tracked) 174 } 175 } 176 177 func TestPeriodicDispatch_Add_Periodic_Stopped(t *testing.T) { 178 ci.Parallel(t) 179 p, _ := testPeriodicDispatcher(t) 180 job := mock.PeriodicJob() 181 job.Stop = true 182 if err := p.Add(job); err != nil { 183 t.Fatalf("Add of stopped periodic job failed: %v", err) 184 } 185 186 tracked := p.Tracked() 187 if len(tracked) != 0 { 188 t.Fatalf("Add of periodic parameterized job should be no-op: %v", tracked) 189 } 190 } 191 192 func TestPeriodicDispatch_Add_UpdateJob(t *testing.T) { 193 ci.Parallel(t) 194 p, _ := testPeriodicDispatcher(t) 195 job := mock.PeriodicJob() 196 err := p.Add(job) 197 require.NoError(t, err) 198 199 tracked := p.Tracked() 200 require.Lenf(t, tracked, 1, "did not track the job") 201 202 // Update the job and add it again. 203 job.Periodic.Spec = "foo" 204 err = p.Add(job) 205 require.Error(t, err) 206 require.Contains(t, err.Error(), "failed parsing cron expression") 207 208 tracked = p.Tracked() 209 require.Lenf(t, tracked, 1, "did not update") 210 211 require.Equalf(t, job, tracked[0], "add did not properly update") 212 } 213 214 func TestPeriodicDispatch_Add_Remove_Namespaced(t *testing.T) { 215 ci.Parallel(t) 216 assert := assert.New(t) 217 p, _ := testPeriodicDispatcher(t) 218 job := mock.PeriodicJob() 219 job2 := mock.PeriodicJob() 220 job2.Namespace = "test" 221 assert.Nil(p.Add(job)) 222 223 assert.Nil(p.Add(job2)) 224 225 assert.Len(p.Tracked(), 2) 226 227 assert.Nil(p.Remove(job2.Namespace, job2.ID)) 228 assert.Len(p.Tracked(), 1) 229 assert.Equal(p.Tracked()[0], job) 230 } 231 232 func TestPeriodicDispatch_Add_RemoveJob(t *testing.T) { 233 ci.Parallel(t) 234 p, _ := testPeriodicDispatcher(t) 235 job := mock.PeriodicJob() 236 if err := p.Add(job); err != nil { 237 t.Fatalf("Add failed %v", err) 238 } 239 240 tracked := p.Tracked() 241 if len(tracked) != 1 { 242 t.Fatalf("Add didn't track the job: %v", tracked) 243 } 244 245 // Update the job to be non-periodic and add it again. 246 job.Periodic = nil 247 if err := p.Add(job); err != nil { 248 t.Fatalf("Add failed %v", err) 249 } 250 251 tracked = p.Tracked() 252 if len(tracked) != 0 { 253 t.Fatalf("Add didn't remove: %v", tracked) 254 } 255 } 256 257 func TestPeriodicDispatch_Add_TriggersUpdate(t *testing.T) { 258 ci.Parallel(t) 259 p, m := testPeriodicDispatcher(t) 260 261 // Create a job that won't be evaluated for a while. 262 job := testPeriodicJob(time.Now().Add(10 * time.Second)) 263 264 // Add it. 265 if err := p.Add(job); err != nil { 266 t.Fatalf("Add failed %v", err) 267 } 268 269 // Update it to be sooner and re-add. 270 expected := time.Now().Round(1 * time.Second).Add(1 * time.Second) 271 job.Periodic.Spec = fmt.Sprintf("%d", expected.Unix()) 272 if err := p.Add(job); err != nil { 273 t.Fatalf("Add failed %v", err) 274 } 275 276 // Check that nothing is created. 277 tuple := structs.NamespacedID{ 278 ID: job.ID, 279 Namespace: job.Namespace, 280 } 281 if _, ok := m.Jobs[tuple]; ok { 282 t.Fatalf("periodic dispatcher created eval at the wrong time") 283 } 284 285 time.Sleep(2 * time.Second) 286 287 // Check that job was launched correctly. 288 times, err := m.LaunchTimes(p, job.Namespace, job.ID) 289 if err != nil { 290 t.Fatalf("failed to get launch times for job %q", job.ID) 291 } 292 if len(times) != 1 { 293 t.Fatalf("incorrect number of launch times for job %q", job.ID) 294 } 295 if times[0] != expected { 296 t.Fatalf("periodic dispatcher created eval for time %v; want %v", times[0], expected) 297 } 298 } 299 300 func TestPeriodicDispatch_Remove_Untracked(t *testing.T) { 301 ci.Parallel(t) 302 p, _ := testPeriodicDispatcher(t) 303 if err := p.Remove("ns", "foo"); err != nil { 304 t.Fatalf("Remove failed %v; expected a no-op", err) 305 } 306 } 307 308 func TestPeriodicDispatch_Remove_Tracked(t *testing.T) { 309 ci.Parallel(t) 310 p, _ := testPeriodicDispatcher(t) 311 312 job := mock.PeriodicJob() 313 if err := p.Add(job); err != nil { 314 t.Fatalf("Add failed %v", err) 315 } 316 317 tracked := p.Tracked() 318 if len(tracked) != 1 { 319 t.Fatalf("Add didn't track the job: %v", tracked) 320 } 321 322 if err := p.Remove(job.Namespace, job.ID); err != nil { 323 t.Fatalf("Remove failed %v", err) 324 } 325 326 tracked = p.Tracked() 327 if len(tracked) != 0 { 328 t.Fatalf("Remove didn't untrack the job: %v", tracked) 329 } 330 } 331 332 func TestPeriodicDispatch_Remove_TriggersUpdate(t *testing.T) { 333 ci.Parallel(t) 334 p, _ := testPeriodicDispatcher(t) 335 336 // Create a job that will be evaluated soon. 337 job := testPeriodicJob(time.Now().Add(1 * time.Second)) 338 339 // Add it. 340 if err := p.Add(job); err != nil { 341 t.Fatalf("Add failed %v", err) 342 } 343 344 // Remove the job. 345 if err := p.Remove(job.Namespace, job.ID); err != nil { 346 t.Fatalf("Remove failed %v", err) 347 } 348 349 time.Sleep(2 * time.Second) 350 351 // Check that an eval wasn't created. 352 d := p.dispatcher.(*MockJobEvalDispatcher) 353 tuple := structs.NamespacedID{ 354 ID: job.ID, 355 Namespace: job.Namespace, 356 } 357 if _, ok := d.Jobs[tuple]; ok { 358 t.Fatalf("Remove didn't cancel creation of an eval") 359 } 360 } 361 362 func TestPeriodicDispatch_ForceEval_Untracked(t *testing.T) { 363 ci.Parallel(t) 364 p, _ := testPeriodicDispatcher(t) 365 366 if _, err := p.ForceEval("ns", "foo"); err == nil { 367 t.Fatal("ForceEval of untracked job should fail") 368 } 369 } 370 371 func TestPeriodicDispatch_ForceEval_Tracked(t *testing.T) { 372 ci.Parallel(t) 373 p, m := testPeriodicDispatcher(t) 374 375 // Create a job that won't be evaluated for a while. 376 job := testPeriodicJob(time.Now().Add(10 * time.Second)) 377 378 // Add it. 379 if err := p.Add(job); err != nil { 380 t.Fatalf("Add failed %v", err) 381 } 382 383 // ForceEval the job 384 if _, err := p.ForceEval(job.Namespace, job.ID); err != nil { 385 t.Fatalf("ForceEval failed %v", err) 386 } 387 388 // Check that job was launched correctly. 389 launches, err := m.LaunchTimes(p, job.Namespace, job.ID) 390 if err != nil { 391 t.Fatalf("failed to get launch times for job %q: %v", job.ID, err) 392 } 393 l := len(launches) 394 if l != 1 { 395 t.Fatalf("restorePeriodicDispatcher() created an unexpected"+ 396 " number of evals; got %d; want 1", l) 397 } 398 } 399 400 func TestPeriodicDispatch_Run_DisallowOverlaps(t *testing.T) { 401 ci.Parallel(t) 402 p, m := testPeriodicDispatcher(t) 403 404 // Create a job that will trigger two launches but disallows overlapping. 405 launch1 := time.Now().Round(1 * time.Second).Add(1 * time.Second) 406 launch2 := time.Now().Round(1 * time.Second).Add(2 * time.Second) 407 job := testPeriodicJob(launch1, launch2) 408 job.Periodic.ProhibitOverlap = true 409 410 // Add it. 411 if err := p.Add(job); err != nil { 412 t.Fatalf("Add failed %v", err) 413 } 414 415 time.Sleep(3 * time.Second) 416 417 // Check that only one job was launched. 418 times, err := m.LaunchTimes(p, job.Namespace, job.ID) 419 if err != nil { 420 t.Fatalf("failed to get launch times for job %q", job.ID) 421 } 422 if len(times) != 1 { 423 t.Fatalf("incorrect number of launch times for job %q; got %v", job.ID, times) 424 } 425 if times[0] != launch1 { 426 t.Fatalf("periodic dispatcher created eval for time %v; want %v", times[0], launch1) 427 } 428 } 429 430 func TestPeriodicDispatch_Run_Multiple(t *testing.T) { 431 ci.Parallel(t) 432 p, m := testPeriodicDispatcher(t) 433 434 // Create a job that will be launched twice. 435 launch1 := time.Now().Round(1 * time.Second).Add(1 * time.Second) 436 launch2 := time.Now().Round(1 * time.Second).Add(2 * time.Second) 437 job := testPeriodicJob(launch1, launch2) 438 439 // Add it. 440 if err := p.Add(job); err != nil { 441 t.Fatalf("Add failed %v", err) 442 } 443 444 time.Sleep(3 * time.Second) 445 446 // Check that job was launched correctly. 447 times, err := m.LaunchTimes(p, job.Namespace, job.ID) 448 if err != nil { 449 t.Fatalf("failed to get launch times for job %q", job.ID) 450 } 451 if len(times) != 2 { 452 t.Fatalf("incorrect number of launch times for job %q", job.ID) 453 } 454 if times[0] != launch1 { 455 t.Fatalf("periodic dispatcher created eval for time %v; want %v", times[0], launch1) 456 } 457 if times[1] != launch2 { 458 t.Fatalf("periodic dispatcher created eval for time %v; want %v", times[1], launch2) 459 } 460 } 461 462 func TestPeriodicDispatch_Run_SameTime(t *testing.T) { 463 ci.Parallel(t) 464 p, m := testPeriodicDispatcher(t) 465 466 // Create two job that will be launched at the same time. 467 launch := time.Now().Round(1 * time.Second).Add(1 * time.Second) 468 job := testPeriodicJob(launch) 469 job2 := testPeriodicJob(launch) 470 471 // Add them. 472 if err := p.Add(job); err != nil { 473 t.Fatalf("Add failed %v", err) 474 } 475 if err := p.Add(job2); err != nil { 476 t.Fatalf("Add failed %v", err) 477 } 478 479 if l := len(p.Tracked()); l != 2 { 480 t.Fatalf("got %d tracked; want 2", l) 481 } 482 483 time.Sleep(2 * time.Second) 484 485 // Check that the jobs were launched correctly. 486 for _, job := range []*structs.Job{job, job2} { 487 times, err := m.LaunchTimes(p, job.Namespace, job.ID) 488 if err != nil { 489 t.Fatalf("failed to get launch times for job %q", job.ID) 490 } 491 if len(times) != 1 { 492 t.Fatalf("incorrect number of launch times for job %q; got %d; want 1", job.ID, len(times)) 493 } 494 if times[0] != launch { 495 t.Fatalf("periodic dispatcher created eval for time %v; want %v", times[0], launch) 496 } 497 } 498 } 499 500 func TestPeriodicDispatch_Run_SameID_Different_Namespace(t *testing.T) { 501 ci.Parallel(t) 502 p, m := testPeriodicDispatcher(t) 503 504 // Create two job that will be launched at the same time. 505 launch := time.Now().Round(1 * time.Second).Add(1 * time.Second) 506 job := testPeriodicJob(launch) 507 job2 := testPeriodicJob(launch) 508 job2.ID = job.ID 509 job2.Namespace = "test" 510 511 // Add them. 512 if err := p.Add(job); err != nil { 513 t.Fatalf("Add failed %v", err) 514 } 515 if err := p.Add(job2); err != nil { 516 t.Fatalf("Add failed %v", err) 517 } 518 519 if l := len(p.Tracked()); l != 2 { 520 t.Fatalf("got %d tracked; want 2", l) 521 } 522 523 if l := len(p.Tracked()); l != 2 { 524 t.Fatalf("got %d tracked; want 2", l) 525 } 526 527 time.Sleep(2 * time.Second) 528 529 // Check that the jobs were launched correctly. 530 for _, job := range []*structs.Job{job, job2} { 531 times, err := m.LaunchTimes(p, job.Namespace, job.ID) 532 if err != nil { 533 t.Fatalf("failed to get launch times for job %q", job.ID) 534 } 535 if len(times) != 1 { 536 t.Fatalf("incorrect number of launch times for job %q; got %d; want 1", job.ID, len(times)) 537 } 538 if times[0] != launch { 539 t.Fatalf("periodic dispatcher created eval for time %v; want %v", times[0], launch) 540 } 541 } 542 } 543 544 // This test adds and removes a bunch of jobs, some launching at the same time, 545 // some after each other and some invalid times, and ensures the correct 546 // behavior. 547 func TestPeriodicDispatch_Complex(t *testing.T) { 548 ci.Parallel(t) 549 p, m := testPeriodicDispatcher(t) 550 551 // Create some jobs launching at different times. 552 now := time.Now().Round(1 * time.Second) 553 same := now.Add(1 * time.Second) 554 launch1 := same.Add(1 * time.Second) 555 launch2 := same.Add(2 * time.Second) 556 launch3 := same.Add(3 * time.Second) 557 invalid := now.Add(-200 * time.Second) 558 559 // Create two jobs launching at the same time. 560 job1 := testPeriodicJob(same) 561 job2 := testPeriodicJob(same) 562 563 // Create a job that will never launch. 564 job3 := testPeriodicJob(invalid) 565 566 // Create a job that launches twice. 567 job4 := testPeriodicJob(launch1, launch3) 568 569 // Create a job that launches once. 570 job5 := testPeriodicJob(launch2) 571 572 // Create 3 jobs we will delete. 573 job6 := testPeriodicJob(same) 574 job7 := testPeriodicJob(launch1, launch3) 575 job8 := testPeriodicJob(launch2) 576 577 // Create a map of expected eval job ids. 578 expected := map[string][]time.Time{ 579 job1.ID: {same}, 580 job2.ID: {same}, 581 job3.ID: nil, 582 job4.ID: {launch1, launch3}, 583 job5.ID: {launch2}, 584 job6.ID: nil, 585 job7.ID: nil, 586 job8.ID: nil, 587 } 588 589 // Shuffle the jobs so they can be added randomly 590 jobs := []*structs.Job{job1, job2, job3, job4, job5, job6, job7, job8} 591 toDelete := []*structs.Job{job6, job7, job8} 592 shuffle(jobs) 593 shuffle(toDelete) 594 595 for _, job := range jobs { 596 if err := p.Add(job); err != nil { 597 t.Fatalf("Add failed %v", err) 598 } 599 } 600 601 for _, job := range toDelete { 602 if err := p.Remove(job.Namespace, job.ID); err != nil { 603 t.Fatalf("Remove failed %v", err) 604 } 605 } 606 607 time.Sleep(5 * time.Second) 608 actual := make(map[string][]time.Time, len(expected)) 609 for _, job := range jobs { 610 launches, err := m.LaunchTimes(p, job.Namespace, job.ID) 611 if err != nil { 612 t.Fatalf("LaunchTimes(%v, %v) failed %v", job.Namespace, job.ID, err) 613 } 614 615 actual[job.ID] = launches 616 } 617 618 if !reflect.DeepEqual(actual, expected) { 619 t.Fatalf("Unexpected launches; got %#v; want %#v", actual, expected) 620 } 621 } 622 623 func shuffle(jobs []*structs.Job) { 624 for i := range jobs { 625 j := rand.Intn(len(jobs)) 626 jobs[i], jobs[j] = jobs[j], jobs[i] 627 } 628 } 629 630 func TestPeriodicHeap_Order(t *testing.T) { 631 ci.Parallel(t) 632 h := NewPeriodicHeap() 633 j1 := mock.PeriodicJob() 634 j2 := mock.PeriodicJob() 635 j3 := mock.PeriodicJob() 636 637 lookup := map[*structs.Job]string{ 638 j1: "j1", 639 j2: "j2", 640 j3: "j3", 641 } 642 643 h.Push(j1, time.Time{}) 644 h.Push(j2, time.Unix(10, 0)) 645 h.Push(j3, time.Unix(11, 0)) 646 647 exp := []string{"j2", "j3", "j1"} 648 var act []string 649 for i := 0; i < 3; i++ { 650 pJob := h.Pop() 651 act = append(act, lookup[pJob.job]) 652 } 653 654 if !reflect.DeepEqual(act, exp) { 655 t.Fatalf("Wrong ordering; got %v; want %v", act, exp) 656 } 657 } 658 659 // deriveChildJob takes a parent periodic job and returns a job with fields set 660 // such that it appears spawned from the parent. 661 func deriveChildJob(parent *structs.Job) *structs.Job { 662 childjob := mock.Job() 663 childjob.ParentID = parent.ID 664 childjob.ID = fmt.Sprintf("%s%s%v", parent.ID, structs.PeriodicLaunchSuffix, time.Now().Unix()) 665 return childjob 666 } 667 668 func TestPeriodicDispatch_RunningChildren_NoEvals(t *testing.T) { 669 ci.Parallel(t) 670 671 s1, cleanupS1 := TestServer(t, nil) 672 defer cleanupS1() 673 testutil.WaitForLeader(t, s1.RPC) 674 675 // Insert job. 676 state := s1.fsm.State() 677 job := mock.PeriodicJob() 678 if err := state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, job); err != nil { 679 t.Fatalf("UpsertJob failed: %v", err) 680 } 681 682 running, err := s1.RunningChildren(job) 683 if err != nil { 684 t.Fatalf("RunningChildren failed: %v", err) 685 } 686 687 if running { 688 t.Fatalf("RunningChildren should return false") 689 } 690 } 691 692 func TestPeriodicDispatch_RunningChildren_ActiveEvals(t *testing.T) { 693 ci.Parallel(t) 694 695 s1, cleanupS1 := TestServer(t, nil) 696 defer cleanupS1() 697 testutil.WaitForLeader(t, s1.RPC) 698 699 // Insert periodic job and child. 700 state := s1.fsm.State() 701 job := mock.PeriodicJob() 702 if err := state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, job); err != nil { 703 t.Fatalf("UpsertJob failed: %v", err) 704 } 705 706 childjob := deriveChildJob(job) 707 if err := state.UpsertJob(structs.MsgTypeTestSetup, 1001, nil, childjob); err != nil { 708 t.Fatalf("UpsertJob failed: %v", err) 709 } 710 711 // Insert non-terminal eval 712 eval := mock.Eval() 713 eval.JobID = childjob.ID 714 eval.Status = structs.EvalStatusPending 715 if err := state.UpsertEvals(structs.MsgTypeTestSetup, 1002, []*structs.Evaluation{eval}); err != nil { 716 t.Fatalf("UpsertEvals failed: %v", err) 717 } 718 719 running, err := s1.RunningChildren(job) 720 if err != nil { 721 t.Fatalf("RunningChildren failed: %v", err) 722 } 723 724 if !running { 725 t.Fatalf("RunningChildren should return true") 726 } 727 } 728 729 func TestPeriodicDispatch_RunningChildren_ActiveAllocs(t *testing.T) { 730 ci.Parallel(t) 731 732 s1, cleanupS1 := TestServer(t, nil) 733 defer cleanupS1() 734 testutil.WaitForLeader(t, s1.RPC) 735 736 // Insert periodic job and child. 737 state := s1.fsm.State() 738 job := mock.PeriodicJob() 739 if err := state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, job); err != nil { 740 t.Fatalf("UpsertJob failed: %v", err) 741 } 742 743 childjob := deriveChildJob(job) 744 if err := state.UpsertJob(structs.MsgTypeTestSetup, 1001, nil, childjob); err != nil { 745 t.Fatalf("UpsertJob failed: %v", err) 746 } 747 748 // Insert terminal eval 749 eval := mock.Eval() 750 eval.JobID = childjob.ID 751 eval.Status = structs.EvalStatusPending 752 if err := state.UpsertEvals(structs.MsgTypeTestSetup, 1002, []*structs.Evaluation{eval}); err != nil { 753 t.Fatalf("UpsertEvals failed: %v", err) 754 } 755 756 // Insert active alloc 757 alloc := mock.Alloc() 758 alloc.JobID = childjob.ID 759 alloc.EvalID = eval.ID 760 alloc.DesiredStatus = structs.AllocDesiredStatusRun 761 if err := state.UpsertAllocs(structs.MsgTypeTestSetup, 1003, []*structs.Allocation{alloc}); err != nil { 762 t.Fatalf("UpsertAllocs failed: %v", err) 763 } 764 765 running, err := s1.RunningChildren(job) 766 if err != nil { 767 t.Fatalf("RunningChildren failed: %v", err) 768 } 769 770 if !running { 771 t.Fatalf("RunningChildren should return true") 772 } 773 } 774 775 // TestPeriodicDispatch_JobEmptyStatus asserts that dispatched 776 // job will always has an empty status 777 func TestPeriodicDispatch_JobEmptyStatus(t *testing.T) { 778 ci.Parallel(t) 779 p, m := testPeriodicDispatcher(t) 780 781 job := testPeriodicJob(time.Now().Add(1 * time.Second)) 782 job.Status = structs.JobStatusRunning 783 784 err := p.Add(job) 785 require.NoError(t, err) 786 787 time.Sleep(2 * time.Second) 788 789 dispatched := m.dispatchedJobs(job) 790 require.NotEmpty(t, dispatched) 791 require.Empty(t, dispatched[0].Status) 792 }