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