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