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  }