github.com/blend/go-sdk@v1.20220411.3/cron/job_manager_test.go (about)

     1  /*
     2  
     3  Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file.
     5  
     6  */
     7  
     8  package cron
     9  
    10  import (
    11  	"context"
    12  	"fmt"
    13  	"io"
    14  	"sync"
    15  	"testing"
    16  	"time"
    17  
    18  	"github.com/blend/go-sdk/assert"
    19  	"github.com/blend/go-sdk/graceful"
    20  	"github.com/blend/go-sdk/logger"
    21  	"github.com/blend/go-sdk/uuid"
    22  )
    23  
    24  // assert the job manager is graceful
    25  var (
    26  	_ graceful.Graceful = (*JobManager)(nil)
    27  )
    28  
    29  func Test_JobManager_New(t *testing.T) {
    30  	its := assert.New(t)
    31  
    32  	jm := New(
    33  		OptLog(logger.None()),
    34  	)
    35  	its.NotNil(jm.Latch)
    36  	its.NotNil(jm.Jobs)
    37  	its.NotNil(jm.Log)
    38  }
    39  
    40  func Test_JobManager_Start(t *testing.T) {
    41  	its := assert.New(t)
    42  
    43  	jm := New(
    44  		OptLog(logger.None()),
    45  	)
    46  	defer jm.Stop()
    47  
    48  	// start blocks, use a channel and goroutine
    49  	errors := make(chan error, 1)
    50  	go func() {
    51  		if err := jm.Start(); err != nil {
    52  			errors <- err
    53  		}
    54  	}()
    55  
    56  	<-jm.Latch.NotifyStarted()
    57  	its.Empty(errors)
    58  	err := jm.Start()
    59  	its.NotNil(err)
    60  }
    61  
    62  func Test_JobManager_DisableJobs(t *testing.T) {
    63  	its := assert.New(t)
    64  
    65  	jm := New()
    66  	its.Nil(jm.LoadJobs(&runAtJob{RunAt: time.Now().UTC().Add(100 * time.Millisecond), RunDelegate: func(ctx context.Context) error {
    67  		return nil
    68  	}}))
    69  	its.Nil(jm.DisableJobs(runAtJobName))
    70  	its.True(jm.IsJobDisabled(runAtJobName))
    71  }
    72  
    73  func Test_JobManager_handleJobPanics(t *testing.T) {
    74  	its := assert.New(t)
    75  
    76  	manager := New()
    77  	waitGroup := sync.WaitGroup{}
    78  	waitGroup.Add(1)
    79  
    80  	action := func(ctx context.Context) error {
    81  		defer waitGroup.Done()
    82  		panic("this is only a test")
    83  	}
    84  	its.Nil(manager.LoadJobs(NewJob(OptJobName("panic-test"), OptJobAction(action))))
    85  	_, _, err := manager.RunJob("panic-test")
    86  	its.Nil(err)
    87  	waitGroup.Wait()
    88  	its.True(true, "should complete")
    89  }
    90  
    91  func Test_JobManager_jobConfigProvider_disabled(t *testing.T) {
    92  	its := assert.New(t)
    93  
    94  	manager := New()
    95  	job := &testWithDisabled{
    96  		disabled: false,
    97  	}
    98  
    99  	jobName := "testWithEnabled"
   100  
   101  	its.Nil(manager.LoadJobs(job))
   102  
   103  	// test provider
   104  	its.False(manager.IsJobDisabled(jobName))
   105  	job.disabled = true
   106  	its.True(manager.IsJobDisabled(jobName))
   107  
   108  	// test explicit
   109  	its.Nil(manager.DisableJobs(jobName))
   110  	its.True(manager.IsJobDisabled(jobName))
   111  	its.Nil(manager.EnableJobs(jobName))
   112  	its.False(manager.IsJobDisabled(jobName))
   113  }
   114  
   115  func Test_JobManager_onError(t *testing.T) {
   116  	its := assert.New(t)
   117  
   118  	agent := logger.All(logger.OptOutput(io.Discard))
   119  	defer agent.Close()
   120  
   121  	manager := New(
   122  		OptLog(agent),
   123  	)
   124  	defer func() { _ = manager.Stop() }()
   125  
   126  	var errorDidFire bool
   127  	var errorMatched bool
   128  	wg := sync.WaitGroup{}
   129  	wg.Add(2)
   130  
   131  	agent.Listen(logger.Error, uuid.V4().String(), func(_ context.Context, e logger.Event) {
   132  		defer wg.Done()
   133  		errorDidFire = true
   134  		if typed, isTyped := e.(logger.ErrorEvent); isTyped {
   135  			if typed.Err != nil {
   136  				errorMatched = typed.Err.Error() == "this is only a test"
   137  			}
   138  		}
   139  	})
   140  	job := NewJob(
   141  		OptJobName("error_test"),
   142  		OptJobAction(func(ctx context.Context) error {
   143  			defer wg.Done()
   144  			return fmt.Errorf("this is only a test")
   145  		}),
   146  	)
   147  	its.Nil(manager.LoadJobs(job))
   148  	_, done, err := manager.RunJob(job.Name())
   149  	its.Nil(err)
   150  	wg.Wait()
   151  
   152  	its.True(errorDidFire)
   153  	its.True(errorMatched)
   154  	<-done
   155  }
   156  
   157  func Test_JobManager_Tracer(t *testing.T) {
   158  	its := assert.New(t)
   159  
   160  	wg := sync.WaitGroup{}
   161  	wg.Add(2)
   162  	var didCallStart, didCallFinish bool
   163  	var errorUnset bool
   164  	var foundJobName string
   165  	manager := New(OptTracer(&mockTracer{
   166  		OnStart: func(ctx context.Context, jobName string) {
   167  			defer wg.Done()
   168  			didCallStart = true
   169  			foundJobName = jobName
   170  		},
   171  		OnFinish: func(ctx context.Context, err error) {
   172  			defer wg.Done()
   173  			didCallFinish = true
   174  			errorUnset = err == nil
   175  		},
   176  	}))
   177  
   178  	its.Nil(manager.LoadJobs(NewJob(OptJobName("tracer-test"))))
   179  	_, _, err := manager.RunJob("tracer-test")
   180  	its.Nil(err)
   181  	wg.Wait()
   182  	its.True(didCallStart)
   183  	its.True(didCallFinish)
   184  	its.True(errorUnset)
   185  	its.Equal("tracer-test", foundJobName)
   186  }
   187  
   188  func Test_JobManager_JobLifecycle(t *testing.T) {
   189  	its := assert.New(t)
   190  
   191  	jm := New()
   192  	its.Nil(jm.StartAsync())
   193  	defer func() { _ = jm.Stop() }()
   194  
   195  	var shouldFail bool
   196  	j := newLifecycleTest(func(ctx context.Context) error {
   197  		defer func() {
   198  			shouldFail = !shouldFail
   199  		}()
   200  		if shouldFail {
   201  			return fmt.Errorf("only a test")
   202  		}
   203  		return nil
   204  	})
   205  	its.Nil(jm.LoadJobs(j))
   206  
   207  	successSignal := j.SuccessSignal
   208  	_, done, err := jm.RunJob(j.Name())
   209  	its.Nil(err)
   210  	<-done
   211  	<-successSignal
   212  
   213  	brokenSignal := j.BrokenSignal
   214  	_, done, err = jm.RunJob(j.Name())
   215  	its.Nil(err)
   216  	<-done
   217  	<-brokenSignal
   218  
   219  	fixedSignal := j.FixedSignal
   220  	_, done, err = jm.RunJob(j.Name())
   221  	its.Nil(err)
   222  	<-done
   223  	<-fixedSignal
   224  
   225  	its.Equal(3, j.Starts)
   226  	its.Equal(3, j.Completes)
   227  	its.Equal(1, j.Failures)
   228  	its.Equal(2, j.Successes)
   229  }
   230  
   231  func Test_JobManager_Job(t *testing.T) {
   232  	its := assert.New(t)
   233  
   234  	jm := New()
   235  	j := newLifecycleTest(func(_ context.Context) error {
   236  		return nil
   237  	})
   238  	its.Nil(jm.LoadJobs(j))
   239  
   240  	meta, err := jm.Job(j.Name())
   241  	its.Nil(err)
   242  	its.NotNil(meta)
   243  
   244  	meta, err = jm.Job(uuid.V4().String())
   245  	its.NotNil(err)
   246  	its.Nil(meta)
   247  }
   248  
   249  func Test_JobManager_LoadJobs(t *testing.T) {
   250  	its := assert.New(t)
   251  
   252  	jm := New()
   253  	its.Nil(jm.LoadJobs(&loadJobTestMinimum{}))
   254  
   255  	its.True(jm.HasJob("load-job-test-minimum"))
   256  
   257  	jobScheduler, err := jm.Job("load-job-test-minimum")
   258  	its.Nil(err)
   259  	its.NotNil(jobScheduler)
   260  
   261  	its.Equal("load-job-test-minimum", jobScheduler.Name())
   262  	its.NotNil(jobScheduler.Job)
   263  
   264  	its.Equal(DefaultDisabled, jobScheduler.Disabled())
   265  	its.Zero(jobScheduler.Config().TimeoutOrDefault())
   266  
   267  	its.Nil(jm.LoadJobs(&testJobWithTimeout{TimeoutDuration: time.Second}))
   268  
   269  	jobScheduler, err = jm.Job("testJobWithTimeout")
   270  	its.Nil(err)
   271  	its.NotNil(jobScheduler)
   272  	its.Equal(time.Second, jobScheduler.Config().TimeoutOrDefault())
   273  }
   274  
   275  func Test_JobManager_IsRunning(t *testing.T) {
   276  	its := assert.New(t)
   277  
   278  	jm := New()
   279  
   280  	checked := make(chan struct{})
   281  	proceed := make(chan struct{})
   282  	its.Nil(jm.LoadJobs(NewJob(OptJobName("is-running-test"), OptJobAction(func(_ context.Context) error {
   283  		close(proceed)
   284  		<-checked
   285  		return nil
   286  	})))) // hadoooooken
   287  
   288  	_, _, err := jm.RunJob("is-running-test")
   289  	its.Nil(err)
   290  	<-proceed
   291  	its.True(jm.IsJobRunning("is-running-test"))
   292  	close(checked)
   293  	its.False(jm.IsJobRunning(uuid.V4().String()))
   294  }
   295  
   296  func Test_JobManager_CancelJob(t *testing.T) {
   297  	its := assert.New(t)
   298  
   299  	started := make(chan struct{})
   300  	canceling := make(chan struct{})
   301  	canceled := make(chan struct{})
   302  
   303  	jm := New()
   304  	job := NewJob(OptJobName("is-running-test"), OptJobAction(func(ctx context.Context) error {
   305  		close(started)
   306  		<-canceling
   307  		time.Sleep(time.Millisecond) // this is a pad to make the test more reliable.
   308  		return nil
   309  	}), OptJobOnCancellation(func(_ context.Context) {
   310  		close(canceled) // but signal on the lifecycle event
   311  	}))
   312  	its.Nil(jm.LoadJobs(job))
   313  
   314  	_, done, err := jm.RunJob(job.Name())
   315  	its.Nil(err)
   316  	<-started
   317  	close(canceling)
   318  	its.Nil(jm.CancelJob(job.Name()))
   319  	<-canceled
   320  	its.False(jm.IsJobRunning(job.Name()))
   321  	<-done
   322  }
   323  
   324  func Test_JobManager_EnableDisableJob(t *testing.T) {
   325  	its := assert.New(t)
   326  
   327  	name := "enable-disable-test"
   328  	jm := New()
   329  	its.Nil(jm.LoadJobs(NewJob(OptJobName(name))))
   330  
   331  	j, err := jm.Job(name)
   332  	its.Nil(err)
   333  	its.False(j.Disabled())
   334  
   335  	its.Nil(jm.DisableJobs(name))
   336  	j, err = jm.Job(name)
   337  	its.Nil(err)
   338  	its.True(j.Disabled())
   339  
   340  	its.Nil(jm.EnableJobs(name))
   341  	j, err = jm.Job(name)
   342  	its.Nil(err)
   343  	its.False(j.Disabled())
   344  }
   345  
   346  func Test_JobManager_LoadJobs_lifecycle(t *testing.T) {
   347  	its := assert.New(t)
   348  
   349  	baseContext := context.WithValue(context.Background(), testContextKey{}, "load-jobs-lifecycle")
   350  	jm := New(
   351  		OptBaseContext(baseContext),
   352  	)
   353  
   354  	onLoadContexts := make(chan context.Context, 1)
   355  	job := NewJob(
   356  		OptJobName("load-test"),
   357  		OptJobOnLoad(func(ctx context.Context) error {
   358  			onLoadContexts <- ctx
   359  			return nil
   360  		}),
   361  	)
   362  
   363  	err := jm.LoadJobs(job)
   364  	its.Nil(err)
   365  
   366  	gotContext := <-onLoadContexts
   367  	its.Equal("load-jobs-lifecycle", gotContext.Value(testContextKey{}))
   368  	js := GetJobScheduler(gotContext)
   369  	its.NotNil(js)
   370  	its.Equal("load-test", js.Job.Name())
   371  }
   372  
   373  func Test_JobManager_UnloadJobs_lifecycle(t *testing.T) {
   374  	its := assert.New(t)
   375  
   376  	baseContext := context.WithValue(context.Background(), testContextKey{}, "load-jobs-lifecycle")
   377  	jm := New(
   378  		OptBaseContext(baseContext),
   379  	)
   380  
   381  	onUnloadContexts := make(chan context.Context, 1)
   382  	job0 := NewJob(
   383  		OptJobName("load-test"),
   384  		OptJobOnUnload(func(ctx context.Context) error {
   385  			onUnloadContexts <- ctx
   386  			return nil
   387  		}),
   388  	)
   389  	job1 := NewJob(
   390  		OptJobName("load-test-1"),
   391  		OptJobOnUnload(func(ctx context.Context) error {
   392  			onUnloadContexts <- ctx
   393  			return nil
   394  		}),
   395  	)
   396  
   397  	err := jm.LoadJobs(job0, job1)
   398  	its.Nil(err)
   399  
   400  	its.True(jm.HasJob("load-test"))
   401  	its.True(jm.HasJob("load-test-1"))
   402  
   403  	err = jm.UnloadJobs("load-test")
   404  	its.Nil(err)
   405  
   406  	its.False(jm.HasJob("load-test"))
   407  	its.True(jm.HasJob("load-test-1"))
   408  
   409  	gotContext := <-onUnloadContexts
   410  	its.Equal("load-jobs-lifecycle", gotContext.Value(testContextKey{}))
   411  	js := GetJobScheduler(gotContext)
   412  	its.NotNil(js)
   413  	its.Equal("load-test", js.Job.Name())
   414  
   415  	err = jm.UnloadJobs(uuid.V4().String())
   416  	its.NotNil(err)
   417  }
   418  
   419  func Test_JobManager_Background(t *testing.T) {
   420  	its := assert.New(t)
   421  
   422  	jm := New()
   423  	its.Equal(jm.Background(), context.Background())
   424  
   425  	type contextKey struct{}
   426  	jm = New(
   427  		OptBaseContext(context.WithValue(context.Background(), contextKey{}, "test-value")),
   428  	)
   429  
   430  	ctx := jm.Background()
   431  	its.Equal("test-value", ctx.Value(contextKey{}))
   432  }