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  }