github.com/jrxfive/nomad@v0.6.1-0.20170802162750-1fef470e89bf/nomad/periodic_test.go (about) 1 package nomad 2 3 import ( 4 "fmt" 5 "log" 6 "math/rand" 7 "os" 8 "reflect" 9 "sort" 10 "strconv" 11 "strings" 12 "sync" 13 "testing" 14 "time" 15 16 "github.com/hashicorp/nomad/nomad/mock" 17 "github.com/hashicorp/nomad/nomad/structs" 18 "github.com/hashicorp/nomad/testutil" 19 ) 20 21 type MockJobEvalDispatcher struct { 22 Jobs map[string]*structs.Job 23 lock sync.Mutex 24 } 25 26 func NewMockJobEvalDispatcher() *MockJobEvalDispatcher { 27 return &MockJobEvalDispatcher{Jobs: make(map[string]*structs.Job)} 28 } 29 30 func (m *MockJobEvalDispatcher) DispatchJob(job *structs.Job) (*structs.Evaluation, error) { 31 m.lock.Lock() 32 defer m.lock.Unlock() 33 m.Jobs[job.ID] = job 34 return nil, nil 35 } 36 37 func (m *MockJobEvalDispatcher) RunningChildren(parent *structs.Job) (bool, error) { 38 m.lock.Lock() 39 defer m.lock.Unlock() 40 for _, job := range m.Jobs { 41 if job.ParentID == parent.ID { 42 return true, nil 43 } 44 } 45 return false, nil 46 } 47 48 // LaunchTimes returns the launch times of child jobs in sorted order. 49 func (m *MockJobEvalDispatcher) LaunchTimes(p *PeriodicDispatch, parentID string) ([]time.Time, error) { 50 m.lock.Lock() 51 defer m.lock.Unlock() 52 var launches []time.Time 53 for _, job := range m.Jobs { 54 if job.ParentID != parentID { 55 continue 56 } 57 58 t, err := p.LaunchTime(job.ID) 59 if err != nil { 60 return nil, err 61 } 62 launches = append(launches, t) 63 } 64 sort.Sort(times(launches)) 65 return launches, nil 66 } 67 68 type times []time.Time 69 70 func (t times) Len() int { return len(t) } 71 func (t times) Swap(i, j int) { t[i], t[j] = t[j], t[i] } 72 func (t times) Less(i, j int) bool { return t[i].Before(t[j]) } 73 74 // testPeriodicDispatcher returns an enabled PeriodicDispatcher which uses the 75 // MockJobEvalDispatcher. 76 func testPeriodicDispatcher() (*PeriodicDispatch, *MockJobEvalDispatcher) { 77 logger := log.New(os.Stderr, "", log.LstdFlags) 78 m := NewMockJobEvalDispatcher() 79 d := NewPeriodicDispatch(logger, m) 80 d.SetEnabled(true) 81 d.Start() 82 return d, m 83 } 84 85 // testPeriodicJob is a helper that creates a periodic job that launches at the 86 // passed times. 87 func testPeriodicJob(times ...time.Time) *structs.Job { 88 job := mock.PeriodicJob() 89 job.Periodic.SpecType = structs.PeriodicSpecTest 90 91 l := make([]string, len(times)) 92 for i, t := range times { 93 l[i] = strconv.Itoa(int(t.Round(1 * time.Second).Unix())) 94 } 95 96 job.Periodic.Spec = strings.Join(l, ",") 97 return job 98 } 99 100 func TestPeriodicDispatch_Add_NonPeriodic(t *testing.T) { 101 t.Parallel() 102 p, _ := testPeriodicDispatcher() 103 job := mock.Job() 104 if err := p.Add(job); err != nil { 105 t.Fatalf("Add of non-periodic job failed: %v; expect no-op", err) 106 } 107 108 tracked := p.Tracked() 109 if len(tracked) != 0 { 110 t.Fatalf("Add of non-periodic job should be no-op: %v", tracked) 111 } 112 } 113 114 func TestPeriodicDispatch_Add_Periodic_Parameterized(t *testing.T) { 115 t.Parallel() 116 p, _ := testPeriodicDispatcher() 117 job := mock.PeriodicJob() 118 job.ParameterizedJob = &structs.ParameterizedJobConfig{} 119 if err := p.Add(job); err != nil { 120 t.Fatalf("Add of periodic parameterized job failed: %v; expect no-op", err) 121 } 122 123 tracked := p.Tracked() 124 if len(tracked) != 0 { 125 t.Fatalf("Add of periodic parameterized job should be no-op: %v", tracked) 126 } 127 } 128 129 func TestPeriodicDispatch_Add_UpdateJob(t *testing.T) { 130 t.Parallel() 131 p, _ := testPeriodicDispatcher() 132 job := mock.PeriodicJob() 133 if err := p.Add(job); err != nil { 134 t.Fatalf("Add failed %v", err) 135 } 136 137 tracked := p.Tracked() 138 if len(tracked) != 1 { 139 t.Fatalf("Add didn't track the job: %v", tracked) 140 } 141 142 // Update the job and add it again. 143 job.Periodic.Spec = "foo" 144 if err := p.Add(job); err != nil { 145 t.Fatalf("Add failed %v", err) 146 } 147 148 tracked = p.Tracked() 149 if len(tracked) != 1 { 150 t.Fatalf("Add didn't update: %v", tracked) 151 } 152 153 if !reflect.DeepEqual(job, tracked[0]) { 154 t.Fatalf("Add didn't properly update: got %v; want %v", tracked[0], job) 155 } 156 } 157 158 func TestPeriodicDispatch_Add_RemoveJob(t *testing.T) { 159 t.Parallel() 160 p, _ := testPeriodicDispatcher() 161 job := mock.PeriodicJob() 162 if err := p.Add(job); err != nil { 163 t.Fatalf("Add failed %v", err) 164 } 165 166 tracked := p.Tracked() 167 if len(tracked) != 1 { 168 t.Fatalf("Add didn't track the job: %v", tracked) 169 } 170 171 // Update the job to be non-periodic and add it again. 172 job.Periodic = nil 173 if err := p.Add(job); err != nil { 174 t.Fatalf("Add failed %v", err) 175 } 176 177 tracked = p.Tracked() 178 if len(tracked) != 0 { 179 t.Fatalf("Add didn't remove: %v", tracked) 180 } 181 } 182 183 func TestPeriodicDispatch_Add_TriggersUpdate(t *testing.T) { 184 t.Parallel() 185 p, m := testPeriodicDispatcher() 186 187 // Create a job that won't be evalauted for a while. 188 job := testPeriodicJob(time.Now().Add(10 * time.Second)) 189 190 // Add it. 191 if err := p.Add(job); err != nil { 192 t.Fatalf("Add failed %v", err) 193 } 194 195 // Update it to be sooner and re-add. 196 expected := time.Now().Round(1 * time.Second).Add(1 * time.Second) 197 job.Periodic.Spec = fmt.Sprintf("%d", expected.Unix()) 198 if err := p.Add(job); err != nil { 199 t.Fatalf("Add failed %v", err) 200 } 201 202 // Check that nothing is created. 203 if _, ok := m.Jobs[job.ID]; ok { 204 t.Fatalf("periodic dispatcher created eval at the wrong time") 205 } 206 207 time.Sleep(2 * time.Second) 208 209 // Check that job was launched correctly. 210 times, err := m.LaunchTimes(p, job.ID) 211 if err != nil { 212 t.Fatalf("failed to get launch times for job %q", job.ID) 213 } 214 if len(times) != 1 { 215 t.Fatalf("incorrect number of launch times for job %q", job.ID) 216 } 217 if times[0] != expected { 218 t.Fatalf("periodic dispatcher created eval for time %v; want %v", times[0], expected) 219 } 220 } 221 222 func TestPeriodicDispatch_Remove_Untracked(t *testing.T) { 223 t.Parallel() 224 p, _ := testPeriodicDispatcher() 225 if err := p.Remove("foo"); err != nil { 226 t.Fatalf("Remove failed %v; expected a no-op", err) 227 } 228 } 229 230 func TestPeriodicDispatch_Remove_Tracked(t *testing.T) { 231 t.Parallel() 232 p, _ := testPeriodicDispatcher() 233 234 job := mock.PeriodicJob() 235 if err := p.Add(job); err != nil { 236 t.Fatalf("Add failed %v", err) 237 } 238 239 tracked := p.Tracked() 240 if len(tracked) != 1 { 241 t.Fatalf("Add didn't track the job: %v", tracked) 242 } 243 244 if err := p.Remove(job.ID); err != nil { 245 t.Fatalf("Remove failed %v", err) 246 } 247 248 tracked = p.Tracked() 249 if len(tracked) != 0 { 250 t.Fatalf("Remove didn't untrack the job: %v", tracked) 251 } 252 } 253 254 func TestPeriodicDispatch_Remove_TriggersUpdate(t *testing.T) { 255 t.Parallel() 256 p, _ := testPeriodicDispatcher() 257 258 // Create a job that will be evaluated soon. 259 job := testPeriodicJob(time.Now().Add(1 * time.Second)) 260 261 // Add it. 262 if err := p.Add(job); err != nil { 263 t.Fatalf("Add failed %v", err) 264 } 265 266 // Remove the job. 267 if err := p.Remove(job.ID); err != nil { 268 t.Fatalf("Add failed %v", err) 269 } 270 271 time.Sleep(2 * time.Second) 272 273 // Check that an eval wasn't created. 274 d := p.dispatcher.(*MockJobEvalDispatcher) 275 if _, ok := d.Jobs[job.ID]; ok { 276 t.Fatalf("Remove didn't cancel creation of an eval") 277 } 278 } 279 280 func TestPeriodicDispatch_ForceRun_Untracked(t *testing.T) { 281 t.Parallel() 282 p, _ := testPeriodicDispatcher() 283 284 if _, err := p.ForceRun("foo"); err == nil { 285 t.Fatal("ForceRun of untracked job should fail") 286 } 287 } 288 289 func TestPeriodicDispatch_ForceRun_Tracked(t *testing.T) { 290 t.Parallel() 291 p, m := testPeriodicDispatcher() 292 293 // Create a job that won't be evalauted for a while. 294 job := testPeriodicJob(time.Now().Add(10 * time.Second)) 295 296 // Add it. 297 if err := p.Add(job); err != nil { 298 t.Fatalf("Add failed %v", err) 299 } 300 301 // ForceRun the job 302 if _, err := p.ForceRun(job.ID); err != nil { 303 t.Fatalf("ForceRun failed %v", err) 304 } 305 306 // Check that job was launched correctly. 307 launches, err := m.LaunchTimes(p, job.ID) 308 if err != nil { 309 t.Fatalf("failed to get launch times for job %q: %v", job.ID, err) 310 } 311 l := len(launches) 312 if l != 1 { 313 t.Fatalf("restorePeriodicDispatcher() created an unexpected"+ 314 " number of evals; got %d; want 1", l) 315 } 316 } 317 318 func TestPeriodicDispatch_Run_DisallowOverlaps(t *testing.T) { 319 t.Parallel() 320 p, m := testPeriodicDispatcher() 321 322 // Create a job that will trigger two launches but disallows overlapping. 323 launch1 := time.Now().Round(1 * time.Second).Add(1 * time.Second) 324 launch2 := time.Now().Round(1 * time.Second).Add(2 * time.Second) 325 job := testPeriodicJob(launch1, launch2) 326 job.Periodic.ProhibitOverlap = true 327 328 // Add it. 329 if err := p.Add(job); err != nil { 330 t.Fatalf("Add failed %v", err) 331 } 332 333 time.Sleep(3 * time.Second) 334 335 // Check that only one job was launched. 336 times, err := m.LaunchTimes(p, job.ID) 337 if err != nil { 338 t.Fatalf("failed to get launch times for job %q", job.ID) 339 } 340 if len(times) != 1 { 341 t.Fatalf("incorrect number of launch times for job %q; got %v", job.ID, times) 342 } 343 if times[0] != launch1 { 344 t.Fatalf("periodic dispatcher created eval for time %v; want %v", times[0], launch1) 345 } 346 } 347 348 func TestPeriodicDispatch_Run_Multiple(t *testing.T) { 349 t.Parallel() 350 p, m := testPeriodicDispatcher() 351 352 // Create a job that will be launched twice. 353 launch1 := time.Now().Round(1 * time.Second).Add(1 * time.Second) 354 launch2 := time.Now().Round(1 * time.Second).Add(2 * time.Second) 355 job := testPeriodicJob(launch1, launch2) 356 357 // Add it. 358 if err := p.Add(job); err != nil { 359 t.Fatalf("Add failed %v", err) 360 } 361 362 time.Sleep(3 * time.Second) 363 364 // Check that job was launched correctly. 365 times, err := m.LaunchTimes(p, job.ID) 366 if err != nil { 367 t.Fatalf("failed to get launch times for job %q", job.ID) 368 } 369 if len(times) != 2 { 370 t.Fatalf("incorrect number of launch times for job %q", job.ID) 371 } 372 if times[0] != launch1 { 373 t.Fatalf("periodic dispatcher created eval for time %v; want %v", times[0], launch1) 374 } 375 if times[1] != launch2 { 376 t.Fatalf("periodic dispatcher created eval for time %v; want %v", times[1], launch2) 377 } 378 } 379 380 func TestPeriodicDispatch_Run_SameTime(t *testing.T) { 381 t.Parallel() 382 p, m := testPeriodicDispatcher() 383 384 // Create two job that will be launched at the same time. 385 launch := time.Now().Round(1 * time.Second).Add(1 * time.Second) 386 job := testPeriodicJob(launch) 387 job2 := testPeriodicJob(launch) 388 389 // Add them. 390 if err := p.Add(job); err != nil { 391 t.Fatalf("Add failed %v", err) 392 } 393 if err := p.Add(job2); err != nil { 394 t.Fatalf("Add failed %v", err) 395 } 396 397 time.Sleep(2 * time.Second) 398 399 // Check that the jobs were launched correctly. 400 for _, job := range []*structs.Job{job, job2} { 401 times, err := m.LaunchTimes(p, job.ID) 402 if err != nil { 403 t.Fatalf("failed to get launch times for job %q", job.ID) 404 } 405 if len(times) != 1 { 406 t.Fatalf("incorrect number of launch times for job %q; got %d; want 1", job.ID, len(times)) 407 } 408 if times[0] != launch { 409 t.Fatalf("periodic dispatcher created eval for time %v; want %v", times[0], launch) 410 } 411 } 412 } 413 414 // This test adds and removes a bunch of jobs, some launching at the same time, 415 // some after each other and some invalid times, and ensures the correct 416 // behavior. 417 func TestPeriodicDispatch_Complex(t *testing.T) { 418 t.Parallel() 419 p, m := testPeriodicDispatcher() 420 421 // Create some jobs launching at different times. 422 now := time.Now().Round(1 * time.Second) 423 same := now.Add(1 * time.Second) 424 launch1 := same.Add(1 * time.Second) 425 launch2 := same.Add(2 * time.Second) 426 launch3 := same.Add(3 * time.Second) 427 invalid := now.Add(-200 * time.Second) 428 429 // Create two jobs launching at the same time. 430 job1 := testPeriodicJob(same) 431 job2 := testPeriodicJob(same) 432 433 // Create a job that will never launch. 434 job3 := testPeriodicJob(invalid) 435 436 // Create a job that launches twice. 437 job4 := testPeriodicJob(launch1, launch3) 438 439 // Create a job that launches once. 440 job5 := testPeriodicJob(launch2) 441 442 // Create 3 jobs we will delete. 443 job6 := testPeriodicJob(same) 444 job7 := testPeriodicJob(launch1, launch3) 445 job8 := testPeriodicJob(launch2) 446 447 // Create a map of expected eval job ids. 448 expected := map[string][]time.Time{ 449 job1.ID: []time.Time{same}, 450 job2.ID: []time.Time{same}, 451 job3.ID: nil, 452 job4.ID: []time.Time{launch1, launch3}, 453 job5.ID: []time.Time{launch2}, 454 job6.ID: nil, 455 job7.ID: nil, 456 job8.ID: nil, 457 } 458 459 // Shuffle the jobs so they can be added randomly 460 jobs := []*structs.Job{job1, job2, job3, job4, job5, job6, job7, job8} 461 toDelete := []*structs.Job{job6, job7, job8} 462 shuffle(jobs) 463 shuffle(toDelete) 464 465 for _, job := range jobs { 466 if err := p.Add(job); err != nil { 467 t.Fatalf("Add failed %v", err) 468 } 469 } 470 471 for _, job := range toDelete { 472 if err := p.Remove(job.ID); err != nil { 473 t.Fatalf("Remove failed %v", err) 474 } 475 } 476 477 time.Sleep(5 * time.Second) 478 actual := make(map[string][]time.Time, len(expected)) 479 for _, job := range jobs { 480 launches, err := m.LaunchTimes(p, job.ID) 481 if err != nil { 482 t.Fatalf("LaunchTimes(%v) failed %v", job.ID, err) 483 } 484 485 actual[job.ID] = launches 486 } 487 488 if !reflect.DeepEqual(actual, expected) { 489 t.Fatalf("Unexpected launches; got %#v; want %#v", actual, expected) 490 } 491 } 492 493 func shuffle(jobs []*structs.Job) { 494 rand.Seed(time.Now().Unix()) 495 for i := range jobs { 496 j := rand.Intn(len(jobs)) 497 jobs[i], jobs[j] = jobs[j], jobs[i] 498 } 499 } 500 501 func TestPeriodicHeap_Order(t *testing.T) { 502 t.Parallel() 503 h := NewPeriodicHeap() 504 j1 := mock.PeriodicJob() 505 j2 := mock.PeriodicJob() 506 j3 := mock.PeriodicJob() 507 508 lookup := map[*structs.Job]string{ 509 j1: "j1", 510 j2: "j2", 511 j3: "j3", 512 } 513 514 h.Push(j1, time.Time{}) 515 h.Push(j2, time.Unix(10, 0)) 516 h.Push(j3, time.Unix(11, 0)) 517 518 exp := []string{"j2", "j3", "j1"} 519 var act []string 520 for i := 0; i < 3; i++ { 521 pJob := h.Pop() 522 act = append(act, lookup[pJob.job]) 523 } 524 525 if !reflect.DeepEqual(act, exp) { 526 t.Fatalf("Wrong ordering; got %v; want %v", act, exp) 527 } 528 } 529 530 // deriveChildJob takes a parent periodic job and returns a job with fields set 531 // such that it appears spawned from the parent. 532 func deriveChildJob(parent *structs.Job) *structs.Job { 533 childjob := mock.Job() 534 childjob.ParentID = parent.ID 535 childjob.ID = fmt.Sprintf("%s%s%v", parent.ID, structs.PeriodicLaunchSuffix, time.Now().Unix()) 536 return childjob 537 } 538 539 func TestPeriodicDispatch_RunningChildren_NoEvals(t *testing.T) { 540 t.Parallel() 541 s1 := testServer(t, nil) 542 defer s1.Shutdown() 543 testutil.WaitForLeader(t, s1.RPC) 544 545 // Insert job. 546 state := s1.fsm.State() 547 job := mock.PeriodicJob() 548 if err := state.UpsertJob(1000, job); err != nil { 549 t.Fatalf("UpsertJob failed: %v", err) 550 } 551 552 running, err := s1.RunningChildren(job) 553 if err != nil { 554 t.Fatalf("RunningChildren failed: %v", err) 555 } 556 557 if running { 558 t.Fatalf("RunningChildren should return false") 559 } 560 } 561 562 func TestPeriodicDispatch_RunningChildren_ActiveEvals(t *testing.T) { 563 t.Parallel() 564 s1 := testServer(t, nil) 565 defer s1.Shutdown() 566 testutil.WaitForLeader(t, s1.RPC) 567 568 // Insert periodic job and child. 569 state := s1.fsm.State() 570 job := mock.PeriodicJob() 571 if err := state.UpsertJob(1000, job); err != nil { 572 t.Fatalf("UpsertJob failed: %v", err) 573 } 574 575 childjob := deriveChildJob(job) 576 if err := state.UpsertJob(1001, childjob); err != nil { 577 t.Fatalf("UpsertJob failed: %v", err) 578 } 579 580 // Insert non-terminal eval 581 eval := mock.Eval() 582 eval.JobID = childjob.ID 583 eval.Status = structs.EvalStatusPending 584 if err := state.UpsertEvals(1002, []*structs.Evaluation{eval}); err != nil { 585 t.Fatalf("UpsertEvals failed: %v", err) 586 } 587 588 running, err := s1.RunningChildren(job) 589 if err != nil { 590 t.Fatalf("RunningChildren failed: %v", err) 591 } 592 593 if !running { 594 t.Fatalf("RunningChildren should return true") 595 } 596 } 597 598 func TestPeriodicDispatch_RunningChildren_ActiveAllocs(t *testing.T) { 599 t.Parallel() 600 s1 := testServer(t, nil) 601 defer s1.Shutdown() 602 testutil.WaitForLeader(t, s1.RPC) 603 604 // Insert periodic job and child. 605 state := s1.fsm.State() 606 job := mock.PeriodicJob() 607 if err := state.UpsertJob(1000, job); err != nil { 608 t.Fatalf("UpsertJob failed: %v", err) 609 } 610 611 childjob := deriveChildJob(job) 612 if err := state.UpsertJob(1001, childjob); err != nil { 613 t.Fatalf("UpsertJob failed: %v", err) 614 } 615 616 // Insert terminal eval 617 eval := mock.Eval() 618 eval.JobID = childjob.ID 619 eval.Status = structs.EvalStatusPending 620 if err := state.UpsertEvals(1002, []*structs.Evaluation{eval}); err != nil { 621 t.Fatalf("UpsertEvals failed: %v", err) 622 } 623 624 // Insert active alloc 625 alloc := mock.Alloc() 626 alloc.JobID = childjob.ID 627 alloc.EvalID = eval.ID 628 alloc.DesiredStatus = structs.AllocDesiredStatusRun 629 if err := state.UpsertAllocs(1003, []*structs.Allocation{alloc}); err != nil { 630 t.Fatalf("UpsertAllocs failed: %v", err) 631 } 632 633 running, err := s1.RunningChildren(job) 634 if err != nil { 635 t.Fatalf("RunningChildren failed: %v", err) 636 } 637 638 if !running { 639 t.Fatalf("RunningChildren should return true") 640 } 641 }