github.com/zoomfoo/nomad@v0.8.5-0.20180907175415-f28fd3a1a056/nomad/periodic_test.go (about)

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