github.com/blend/go-sdk@v1.20220411.3/cron/job_manager.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  // NOTE: ALL TIMES ARE IN UTC. JUST USE UTC.
    11  
    12  import (
    13  	"context"
    14  	"sync"
    15  	"time"
    16  
    17  	"github.com/blend/go-sdk/async"
    18  	"github.com/blend/go-sdk/ex"
    19  	"github.com/blend/go-sdk/logger"
    20  )
    21  
    22  // New returns a new job manager.
    23  func New(options ...JobManagerOption) *JobManager {
    24  	jm := JobManager{
    25  		Latch:       async.NewLatch(),
    26  		BaseContext: context.Background(),
    27  		Jobs:        make(map[string]*JobScheduler),
    28  	}
    29  	for _, option := range options {
    30  		option(&jm)
    31  	}
    32  	return &jm
    33  }
    34  
    35  // JobManager is the main orchestration and job management object.
    36  type JobManager struct {
    37  	sync.Mutex
    38  	Latch       *async.Latch
    39  	BaseContext context.Context
    40  	Tracer      Tracer
    41  	Log         logger.Log
    42  	Started     time.Time
    43  	Stopped     time.Time
    44  	Jobs        map[string]*JobScheduler
    45  }
    46  
    47  // Background returns the BaseContext or context.Background().
    48  func (jm *JobManager) Background() context.Context {
    49  	if jm.BaseContext != nil {
    50  		return jm.BaseContext
    51  	}
    52  	return context.Background()
    53  }
    54  
    55  //
    56  // Life Cycle
    57  //
    58  
    59  // Start starts the job manager and blocks.
    60  func (jm *JobManager) Start() error {
    61  	if err := jm.StartAsync(); err != nil {
    62  		return err
    63  	}
    64  	<-jm.Latch.NotifyStopped()
    65  	return nil
    66  }
    67  
    68  // StartAsync starts the job manager and the loaded jobs.
    69  // It does not block.
    70  func (jm *JobManager) StartAsync() error {
    71  	if !jm.Latch.CanStart() {
    72  		return async.ErrCannotStart
    73  	}
    74  	jm.Latch.Starting()
    75  	jm.info("job manager starting")
    76  	for _, jobScheduler := range jm.Jobs {
    77  		errors := make(chan error)
    78  		go func() {
    79  			errors <- jobScheduler.Start()
    80  		}()
    81  		jm.debugf("job manager starting job %s", jobScheduler.Name())
    82  		select {
    83  		case err := <-errors:
    84  			jm.error(err)
    85  		case <-jobScheduler.NotifyStarted():
    86  			jm.debugf("job manager starting job %s complete", jobScheduler.Name())
    87  			continue
    88  		}
    89  	}
    90  
    91  	jm.Latch.Started()
    92  	jm.Started = time.Now().UTC()
    93  	jm.info("job manager started")
    94  	return nil
    95  }
    96  
    97  // Stop stops the schedule runner for a JobManager.
    98  func (jm *JobManager) Stop() error {
    99  	if !jm.Latch.CanStop() {
   100  		return async.ErrCannotStop
   101  	}
   102  	jm.Latch.Stopping()
   103  	jm.info("job manager stopping")
   104  	defer func() {
   105  		jm.Stopped = time.Now().UTC()
   106  		jm.Latch.Stopped()
   107  		jm.Latch.Reset()
   108  		jm.info("job manager stopping complete")
   109  	}()
   110  	for _, jobScheduler := range jm.Jobs {
   111  		if err := jobScheduler.OnUnload(jobScheduler.Background()); err != nil {
   112  			jm.error(err)
   113  		}
   114  		if err := jobScheduler.Stop(); err != nil {
   115  			jm.error(err)
   116  		}
   117  	}
   118  	return nil
   119  }
   120  
   121  //
   122  // job management
   123  //
   124  
   125  // LoadJobs loads a variadic list of jobs.
   126  func (jm *JobManager) LoadJobs(jobs ...Job) error {
   127  	jm.Lock()
   128  	defer jm.Unlock()
   129  
   130  	for _, job := range jobs {
   131  		jobName := job.Name()
   132  		if _, hasJob := jm.Jobs[jobName]; hasJob {
   133  			return ex.New(ErrJobAlreadyLoaded, ex.OptMessagef("job: %s", job.Name()))
   134  		}
   135  
   136  		jobScheduler := NewJobScheduler(
   137  			job,
   138  			OptJobSchedulerLog(jm.Log),
   139  			OptJobSchedulerTracer(jm.Tracer),
   140  			OptJobSchedulerBaseContext(jm.Background()),
   141  		)
   142  		if err := jobScheduler.OnLoad(jobScheduler.Background()); err != nil {
   143  			return err
   144  		}
   145  		jm.Jobs[jobName] = jobScheduler
   146  	}
   147  	return nil
   148  }
   149  
   150  // UnloadJobs removes jobs from the manager and stops them.
   151  func (jm *JobManager) UnloadJobs(jobNames ...string) error {
   152  	jm.Lock()
   153  	defer jm.Unlock()
   154  
   155  	for _, jobName := range jobNames {
   156  		if jobScheduler, ok := jm.Jobs[jobName]; ok {
   157  			if err := jobScheduler.OnUnload(jobScheduler.Background()); err != nil {
   158  				return err
   159  			}
   160  			if err := jobScheduler.Stop(); err != nil {
   161  				jm.error(err)
   162  			}
   163  			delete(jm.Jobs, jobName)
   164  		} else {
   165  			return ex.New(ErrJobNotFound, ex.OptMessagef("job: %s", jobName))
   166  		}
   167  	}
   168  	return nil
   169  }
   170  
   171  // DisableJobs disables a variadic list of job names.
   172  func (jm *JobManager) DisableJobs(jobNames ...string) error {
   173  	jm.Lock()
   174  	defer jm.Unlock()
   175  
   176  	for _, jobName := range jobNames {
   177  		if job, ok := jm.Jobs[jobName]; ok {
   178  			job.Disable()
   179  		} else {
   180  			return ex.New(ErrJobNotFound, ex.OptMessagef("job: %s", jobName))
   181  		}
   182  	}
   183  	return nil
   184  }
   185  
   186  // EnableJobs enables a variadic list of job names.
   187  func (jm *JobManager) EnableJobs(jobNames ...string) error {
   188  	jm.Lock()
   189  	defer jm.Unlock()
   190  
   191  	for _, jobName := range jobNames {
   192  		if job, ok := jm.Jobs[jobName]; ok {
   193  			job.Enable()
   194  		} else {
   195  			return ex.New(ErrJobNotFound, ex.OptMessagef("job: %s", jobName))
   196  		}
   197  	}
   198  	return nil
   199  }
   200  
   201  // HasJob returns if a jobName is loaded or not.
   202  func (jm *JobManager) HasJob(jobName string) (hasJob bool) {
   203  	jm.Lock()
   204  	defer jm.Unlock()
   205  	_, hasJob = jm.Jobs[jobName]
   206  	return
   207  }
   208  
   209  // Job returns a job metadata by name.
   210  func (jm *JobManager) Job(jobName string) (job *JobScheduler, err error) {
   211  	jm.Lock()
   212  	jobScheduler, hasJob := jm.Jobs[jobName]
   213  	jm.Unlock()
   214  	if hasJob {
   215  		job = jobScheduler
   216  	} else {
   217  		err = ex.New(ErrJobNotLoaded, ex.OptMessagef("job: %s", jobName))
   218  	}
   219  	return
   220  }
   221  
   222  // IsJobDisabled returns if a job is disabled.
   223  func (jm *JobManager) IsJobDisabled(jobName string) (value bool) {
   224  	jm.Lock()
   225  	jobScheduler, hasJob := jm.Jobs[jobName]
   226  	jm.Unlock()
   227  	if hasJob {
   228  		value = jobScheduler.Disabled()
   229  	}
   230  	return
   231  }
   232  
   233  // IsJobRunning returns if a job is currently running.
   234  func (jm *JobManager) IsJobRunning(jobName string) (isRunning bool) {
   235  	jm.Lock()
   236  	jobScheduler, ok := jm.Jobs[jobName]
   237  	jm.Unlock()
   238  	if ok {
   239  		isRunning = !jobScheduler.IsIdle()
   240  	}
   241  	return
   242  }
   243  
   244  // RunJob runs a job by jobName on demand.
   245  func (jm *JobManager) RunJob(jobName string) (*JobInvocation, <-chan struct{}, error) {
   246  	jm.Lock()
   247  	jobScheduler, ok := jm.Jobs[jobName]
   248  	jm.Unlock()
   249  	if !ok {
   250  		return nil, nil, ex.New(ErrJobNotLoaded, ex.OptMessagef("job: %s", jobName))
   251  	}
   252  	return jobScheduler.RunAsync()
   253  }
   254  
   255  // RunJobContext runs a job by jobName on demand with a given context.
   256  func (jm *JobManager) RunJobContext(ctx context.Context, jobName string) (*JobInvocation, <-chan struct{}, error) {
   257  	jm.Lock()
   258  	jobScheduler, ok := jm.Jobs[jobName]
   259  	jm.Unlock()
   260  	if !ok {
   261  		return nil, nil, ex.New(ErrJobNotLoaded, ex.OptMessagef("job: %s", jobName))
   262  	}
   263  	return jobScheduler.RunAsyncContext(ctx)
   264  }
   265  
   266  // CancelJob cancels (sends the cancellation signal) to a running job.
   267  func (jm *JobManager) CancelJob(jobName string) (err error) {
   268  	jm.Lock()
   269  	jobScheduler, ok := jm.Jobs[jobName]
   270  	jm.Unlock()
   271  	if !ok {
   272  		err = ex.New(ErrJobNotFound, ex.OptMessagef("job: %s", jobName))
   273  		return
   274  	}
   275  	err = jobScheduler.Cancel()
   276  	return
   277  }
   278  
   279  //
   280  // status and state
   281  //
   282  
   283  // State returns the job manager state.
   284  func (jm *JobManager) State() JobManagerState {
   285  	if jm.Latch.IsStarted() {
   286  		return JobManagerStateRunning
   287  	} else if jm.Latch.IsStopped() {
   288  		return JobManagerStateStopped
   289  	}
   290  	return JobManagerStateUnknown
   291  }
   292  
   293  func (jm *JobManager) info(message string) {
   294  	logger.MaybeInfoContext(jm.Background(), jm.Log, message)
   295  }
   296  
   297  func (jm *JobManager) infof(format string, args ...interface{}) {
   298  	logger.MaybeInfofContext(jm.Background(), jm.Log, format, args...)
   299  }
   300  
   301  func (jm *JobManager) debugf(format string, args ...interface{}) {
   302  	logger.MaybeDebugfContext(jm.Background(), jm.Log, format, args...)
   303  }
   304  
   305  func (jm *JobManager) warning(err error) {
   306  	logger.MaybeWarningContext(jm.Background(), jm.Log, err)
   307  }
   308  
   309  func (jm *JobManager) error(err error) {
   310  	logger.MaybeErrorContext(jm.Background(), jm.Log, err)
   311  }