github.com/ryanslade/nomad@v0.2.4-0.20160128061903-fc95782f2089/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_UpdateJob(t *testing.T) { 115 t.Parallel() 116 p, _ := testPeriodicDispatcher() 117 job := mock.PeriodicJob() 118 if err := p.Add(job); err != nil { 119 t.Fatalf("Add failed %v", err) 120 } 121 122 tracked := p.Tracked() 123 if len(tracked) != 1 { 124 t.Fatalf("Add didn't track the job: %v", tracked) 125 } 126 127 // Update the job and add it again. 128 job.Periodic.Spec = "foo" 129 if err := p.Add(job); err != nil { 130 t.Fatalf("Add failed %v", err) 131 } 132 133 tracked = p.Tracked() 134 if len(tracked) != 1 { 135 t.Fatalf("Add didn't update: %v", tracked) 136 } 137 138 if !reflect.DeepEqual(job, tracked[0]) { 139 t.Fatalf("Add didn't properly update: got %v; want %v", tracked[0], job) 140 } 141 } 142 143 func TestPeriodicDispatch_Add_RemoveJob(t *testing.T) { 144 t.Parallel() 145 p, _ := testPeriodicDispatcher() 146 job := mock.PeriodicJob() 147 if err := p.Add(job); err != nil { 148 t.Fatalf("Add failed %v", err) 149 } 150 151 tracked := p.Tracked() 152 if len(tracked) != 1 { 153 t.Fatalf("Add didn't track the job: %v", tracked) 154 } 155 156 // Update the job to be non-periodic and add it again. 157 job.Periodic = nil 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) != 0 { 164 t.Fatalf("Add didn't remove: %v", tracked) 165 } 166 } 167 168 func TestPeriodicDispatch_Add_TriggersUpdate(t *testing.T) { 169 t.Parallel() 170 p, m := testPeriodicDispatcher() 171 172 // Create a job that won't be evalauted for a while. 173 job := testPeriodicJob(time.Now().Add(10 * time.Second)) 174 175 // Add it. 176 if err := p.Add(job); err != nil { 177 t.Fatalf("Add failed %v", err) 178 } 179 180 // Update it to be sooner and re-add. 181 expected := time.Now().Round(1 * time.Second).Add(1 * time.Second) 182 job.Periodic.Spec = fmt.Sprintf("%d", expected.Unix()) 183 if err := p.Add(job); err != nil { 184 t.Fatalf("Add failed %v", err) 185 } 186 187 // Check that nothing is created. 188 if _, ok := m.Jobs[job.ID]; ok { 189 t.Fatalf("periodic dispatcher created eval at the wrong time") 190 } 191 192 time.Sleep(2 * time.Second) 193 194 // Check that job was launched correctly. 195 times, err := m.LaunchTimes(p, job.ID) 196 if err != nil { 197 t.Fatalf("failed to get launch times for job %q", job.ID) 198 } 199 if len(times) != 1 { 200 t.Fatalf("incorrect number of launch times for job %q", job.ID) 201 } 202 if times[0] != expected { 203 t.Fatalf("periodic dispatcher created eval for time %v; want %v", times[0], expected) 204 } 205 } 206 207 func TestPeriodicDispatch_Remove_Untracked(t *testing.T) { 208 t.Parallel() 209 p, _ := testPeriodicDispatcher() 210 if err := p.Remove("foo"); err != nil { 211 t.Fatalf("Remove failed %v; expected a no-op", err) 212 } 213 } 214 215 func TestPeriodicDispatch_Remove_Tracked(t *testing.T) { 216 t.Parallel() 217 p, _ := testPeriodicDispatcher() 218 219 job := mock.PeriodicJob() 220 if err := p.Add(job); err != nil { 221 t.Fatalf("Add failed %v", err) 222 } 223 224 tracked := p.Tracked() 225 if len(tracked) != 1 { 226 t.Fatalf("Add didn't track the job: %v", tracked) 227 } 228 229 if err := p.Remove(job.ID); err != nil { 230 t.Fatalf("Remove failed %v", err) 231 } 232 233 tracked = p.Tracked() 234 if len(tracked) != 0 { 235 t.Fatalf("Remove didn't untrack the job: %v", tracked) 236 } 237 } 238 239 func TestPeriodicDispatch_Remove_TriggersUpdate(t *testing.T) { 240 t.Parallel() 241 p, _ := testPeriodicDispatcher() 242 243 // Create a job that will be evaluated soon. 244 job := testPeriodicJob(time.Now().Add(1 * time.Second)) 245 246 // Add it. 247 if err := p.Add(job); err != nil { 248 t.Fatalf("Add failed %v", err) 249 } 250 251 // Remove the job. 252 if err := p.Remove(job.ID); err != nil { 253 t.Fatalf("Add failed %v", err) 254 } 255 256 time.Sleep(2 * time.Second) 257 258 // Check that an eval wasn't created. 259 d := p.dispatcher.(*MockJobEvalDispatcher) 260 if _, ok := d.Jobs[job.ID]; ok { 261 t.Fatalf("Remove didn't cancel creation of an eval") 262 } 263 } 264 265 func TestPeriodicDispatch_ForceRun_Untracked(t *testing.T) { 266 t.Parallel() 267 p, _ := testPeriodicDispatcher() 268 269 if _, err := p.ForceRun("foo"); err == nil { 270 t.Fatal("ForceRun of untracked job should fail") 271 } 272 } 273 274 func TestPeriodicDispatch_ForceRun_Tracked(t *testing.T) { 275 t.Parallel() 276 p, m := testPeriodicDispatcher() 277 278 // Create a job that won't be evalauted for a while. 279 job := testPeriodicJob(time.Now().Add(10 * time.Second)) 280 281 // Add it. 282 if err := p.Add(job); err != nil { 283 t.Fatalf("Add failed %v", err) 284 } 285 286 // ForceRun the job 287 if _, err := p.ForceRun(job.ID); err != nil { 288 t.Fatalf("ForceRun failed %v", err) 289 } 290 291 // Check that job was launched correctly. 292 launches, err := m.LaunchTimes(p, job.ID) 293 if err != nil { 294 t.Fatalf("failed to get launch times for job %q: %v", job.ID, err) 295 } 296 l := len(launches) 297 if l != 1 { 298 t.Fatalf("restorePeriodicDispatcher() created an unexpected"+ 299 " number of evals; got %d; want 1", l) 300 } 301 } 302 303 func TestPeriodicDispatch_Run_DisallowOverlaps(t *testing.T) { 304 t.Parallel() 305 p, m := testPeriodicDispatcher() 306 307 // Create a job that will trigger two launches but disallows overlapping. 308 launch1 := time.Now().Round(1 * time.Second).Add(1 * time.Second) 309 launch2 := time.Now().Round(1 * time.Second).Add(2 * time.Second) 310 job := testPeriodicJob(launch1, launch2) 311 job.Periodic.ProhibitOverlap = true 312 313 // Add it. 314 if err := p.Add(job); err != nil { 315 t.Fatalf("Add failed %v", err) 316 } 317 318 time.Sleep(3 * time.Second) 319 320 // Check that only one job was launched. 321 times, err := m.LaunchTimes(p, job.ID) 322 if err != nil { 323 t.Fatalf("failed to get launch times for job %q", job.ID) 324 } 325 if len(times) != 1 { 326 t.Fatalf("incorrect number of launch times for job %q; got %v", job.ID, times) 327 } 328 if times[0] != launch1 { 329 t.Fatalf("periodic dispatcher created eval for time %v; want %v", times[0], launch1) 330 } 331 } 332 333 func TestPeriodicDispatch_Run_Multiple(t *testing.T) { 334 t.Parallel() 335 p, m := testPeriodicDispatcher() 336 337 // Create a job that will be launched twice. 338 launch1 := time.Now().Round(1 * time.Second).Add(1 * time.Second) 339 launch2 := time.Now().Round(1 * time.Second).Add(2 * time.Second) 340 job := testPeriodicJob(launch1, launch2) 341 342 // Add it. 343 if err := p.Add(job); err != nil { 344 t.Fatalf("Add failed %v", err) 345 } 346 347 time.Sleep(3 * time.Second) 348 349 // Check that job was launched correctly. 350 times, err := m.LaunchTimes(p, job.ID) 351 if err != nil { 352 t.Fatalf("failed to get launch times for job %q", job.ID) 353 } 354 if len(times) != 2 { 355 t.Fatalf("incorrect number of launch times for job %q", job.ID) 356 } 357 if times[0] != launch1 { 358 t.Fatalf("periodic dispatcher created eval for time %v; want %v", times[0], launch1) 359 } 360 if times[1] != launch2 { 361 t.Fatalf("periodic dispatcher created eval for time %v; want %v", times[1], launch2) 362 } 363 } 364 365 func TestPeriodicDispatch_Run_SameTime(t *testing.T) { 366 t.Parallel() 367 p, m := testPeriodicDispatcher() 368 369 // Create two job that will be launched at the same time. 370 launch := time.Now().Round(1 * time.Second).Add(1 * time.Second) 371 job := testPeriodicJob(launch) 372 job2 := testPeriodicJob(launch) 373 374 // Add them. 375 if err := p.Add(job); err != nil { 376 t.Fatalf("Add failed %v", err) 377 } 378 if err := p.Add(job2); err != nil { 379 t.Fatalf("Add failed %v", err) 380 } 381 382 time.Sleep(2 * time.Second) 383 384 // Check that the jobs were launched correctly. 385 for _, job := range []*structs.Job{job, job2} { 386 times, err := m.LaunchTimes(p, job.ID) 387 if err != nil { 388 t.Fatalf("failed to get launch times for job %q", job.ID) 389 } 390 if len(times) != 1 { 391 t.Fatalf("incorrect number of launch times for job %q; got %d; want 1", job.ID, len(times)) 392 } 393 if times[0] != launch { 394 t.Fatalf("periodic dispatcher created eval for time %v; want %v", times[0], launch) 395 } 396 } 397 } 398 399 // This test adds and removes a bunch of jobs, some launching at the same time, 400 // some after each other and some invalid times, and ensures the correct 401 // behavior. 402 func TestPeriodicDispatch_Complex(t *testing.T) { 403 t.Parallel() 404 p, m := testPeriodicDispatcher() 405 406 // Create some jobs launching at different times. 407 now := time.Now().Round(1 * time.Second) 408 same := now.Add(1 * time.Second) 409 launch1 := same.Add(1 * time.Second) 410 launch2 := same.Add(2 * time.Second) 411 launch3 := same.Add(3 * time.Second) 412 invalid := now.Add(-200 * time.Second) 413 414 // Create two jobs launching at the same time. 415 job1 := testPeriodicJob(same) 416 job2 := testPeriodicJob(same) 417 418 // Create a job that will never launch. 419 job3 := testPeriodicJob(invalid) 420 421 // Create a job that launches twice. 422 job4 := testPeriodicJob(launch1, launch3) 423 424 // Create a job that launches once. 425 job5 := testPeriodicJob(launch2) 426 427 // Create 3 jobs we will delete. 428 job6 := testPeriodicJob(same) 429 job7 := testPeriodicJob(launch1, launch3) 430 job8 := testPeriodicJob(launch2) 431 432 // Create a map of expected eval job ids. 433 expected := map[string][]time.Time{ 434 job1.ID: []time.Time{same}, 435 job2.ID: []time.Time{same}, 436 job3.ID: nil, 437 job4.ID: []time.Time{launch1, launch3}, 438 job5.ID: []time.Time{launch2}, 439 job6.ID: nil, 440 job7.ID: nil, 441 job8.ID: nil, 442 } 443 444 // Shuffle the jobs so they can be added randomly 445 jobs := []*structs.Job{job1, job2, job3, job4, job5, job6, job7, job8} 446 toDelete := []*structs.Job{job6, job7, job8} 447 shuffle(jobs) 448 shuffle(toDelete) 449 450 for _, job := range jobs { 451 if err := p.Add(job); err != nil { 452 t.Fatalf("Add failed %v", err) 453 } 454 } 455 456 for _, job := range toDelete { 457 if err := p.Remove(job.ID); err != nil { 458 t.Fatalf("Remove failed %v", err) 459 } 460 } 461 462 time.Sleep(5 * time.Second) 463 actual := make(map[string][]time.Time, len(expected)) 464 for _, job := range jobs { 465 launches, err := m.LaunchTimes(p, job.ID) 466 if err != nil { 467 t.Fatalf("LaunchTimes(%v) failed %v", job.ID, err) 468 } 469 470 actual[job.ID] = launches 471 } 472 473 if !reflect.DeepEqual(actual, expected) { 474 t.Fatalf("Unexpected launches; got %#v; want %#v", actual, expected) 475 } 476 } 477 478 func shuffle(jobs []*structs.Job) { 479 rand.Seed(time.Now().Unix()) 480 for i := range jobs { 481 j := rand.Intn(len(jobs)) 482 jobs[i], jobs[j] = jobs[j], jobs[i] 483 } 484 } 485 486 func TestPeriodicHeap_Order(t *testing.T) { 487 t.Parallel() 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 }