github.com/hernad/nomad@v1.6.112/nomad/periodic_test.go (about)

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