go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/cron/job_manager.go (about)

     1  /*
     2  
     3  Copyright (c) 2023 - Present. Will Charczuk. All rights reserved.
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository.
     5  
     6  */
     7  
     8  package cron
     9  
    10  import (
    11  	"context"
    12  	"sync"
    13  
    14  	"go.charczuk.com/sdk/async"
    15  	"go.charczuk.com/sdk/logutil"
    16  )
    17  
    18  // New returns a new job manager.
    19  func New(opts ...Option) *JobManager {
    20  	jm := JobManager{
    21  		latch: async.NewLatch(),
    22  		jobs:  make(map[string]*JobScheduler),
    23  	}
    24  	for _, opt := range opts {
    25  		opt(&jm)
    26  	}
    27  	return &jm
    28  }
    29  
    30  // Option mutates a job manager.
    31  type Option func(*JobManager)
    32  
    33  func OptLog(log Logger) Option {
    34  	return func(jm *JobManager) {
    35  		jm.onJobBegin = append(jm.onJobBegin, func(e JobSchedulerEvent) {
    36  			log.Output(logutil.SDKStackDepth(), "CRON: "+e.String())
    37  			if e.Err != nil {
    38  				log.Output(logutil.SDKStackDepth(), "ERROR: "+e.Err.Error())
    39  			}
    40  		})
    41  	}
    42  }
    43  
    44  // JobManager is the main orchestration and job management object.
    45  type JobManager struct {
    46  	mu    sync.Mutex
    47  	latch *async.Latch
    48  	jobs  map[string]*JobScheduler
    49  
    50  	onJobBegin    []func(JobSchedulerEvent)
    51  	onJobComplete []func(JobSchedulerEvent)
    52  }
    53  
    54  //
    55  // Life Cycle
    56  //
    57  
    58  // Start starts the job manager and blocks.
    59  func (jm *JobManager) Start(ctx context.Context) error {
    60  	if err := jm.StartAsync(ctx); err != nil {
    61  		return err
    62  	}
    63  	<-jm.latch.NotifyStopped()
    64  	return nil
    65  }
    66  
    67  // StartAsync starts the job manager and the loaded jobs.
    68  // It does not block.
    69  func (jm *JobManager) StartAsync(ctx context.Context) error {
    70  	if !jm.latch.CanStart() {
    71  		return ErrCannotStart
    72  	}
    73  	jm.latch.Starting()
    74  	for _, jobScheduler := range jm.jobs {
    75  		go jobScheduler.Start(ctx)
    76  		<-jobScheduler.NotifyStarted()
    77  	}
    78  	jm.latch.Started()
    79  	return nil
    80  }
    81  
    82  // Restart doesn't do anything right now.
    83  func (jm *JobManager) Restart(ctx context.Context) error {
    84  	return nil
    85  }
    86  
    87  // Stop stops the schedule runner for a JobManager.
    88  func (jm *JobManager) Stop(ctx context.Context) error {
    89  	if !jm.latch.CanStop() {
    90  		return ErrCannotStop
    91  	}
    92  	jm.latch.Stopping()
    93  	defer func() {
    94  		jm.latch.Stopped()
    95  		jm.latch.Reset()
    96  	}()
    97  	for _, jobScheduler := range jm.jobs {
    98  		_ = jobScheduler.onRemove(ctx)
    99  		_ = jobScheduler.Stop(ctx)
   100  	}
   101  	return nil
   102  }
   103  
   104  //
   105  // job management
   106  //
   107  
   108  // Register adds list of jobs to the job manager and calls their
   109  // "OnRegister" lifecycle handler(s).
   110  func (jm *JobManager) Register(ctx context.Context, jobs ...Job) error {
   111  	jm.mu.Lock()
   112  	defer jm.mu.Unlock()
   113  
   114  	for _, job := range jobs {
   115  		jobName := job.Name()
   116  		if _, hasJob := jm.jobs[jobName]; hasJob {
   117  			return ErrJobAlreadyLoaded
   118  		}
   119  		jobScheduler := NewJobScheduler(jm, job)
   120  		if err := jobScheduler.onRegister(ctx); err != nil {
   121  			return err
   122  		}
   123  		jm.jobs[jobName] = jobScheduler
   124  	}
   125  	return nil
   126  }
   127  
   128  // Remove removes jobs from the manager and stops them.
   129  func (jm *JobManager) Remove(ctx context.Context, jobNames ...string) (err error) {
   130  	jm.mu.Lock()
   131  	defer jm.mu.Unlock()
   132  
   133  	for _, jobName := range jobNames {
   134  		if jobScheduler, ok := jm.jobs[jobName]; ok {
   135  			err = jobScheduler.onRemove(ctx)
   136  			if err != nil {
   137  				return
   138  			}
   139  			err = jobScheduler.Stop(ctx)
   140  			if err != nil && err != ErrCannotStop {
   141  				return
   142  			}
   143  			delete(jm.jobs, jobName)
   144  		} else {
   145  			return ErrJobNotLoaded
   146  		}
   147  	}
   148  	return nil
   149  }
   150  
   151  // Disable disables a variadic list of job names.
   152  func (jm *JobManager) Disable(ctx context.Context, jobNames ...string) error {
   153  	jm.mu.Lock()
   154  	defer jm.mu.Unlock()
   155  
   156  	for _, jobName := range jobNames {
   157  		if job, ok := jm.jobs[jobName]; ok {
   158  			job.Disable(ctx)
   159  		} else {
   160  			return ErrJobNotLoaded
   161  		}
   162  	}
   163  	return nil
   164  }
   165  
   166  // Enable enables a variadic list of job names.
   167  func (jm *JobManager) Enable(ctx context.Context, jobNames ...string) error {
   168  	jm.mu.Lock()
   169  	defer jm.mu.Unlock()
   170  
   171  	for _, jobName := range jobNames {
   172  		if job, ok := jm.jobs[jobName]; ok {
   173  			job.Enable(ctx)
   174  		} else {
   175  			return ErrJobNotLoaded
   176  		}
   177  	}
   178  	return nil
   179  }
   180  
   181  // Has returns if a jobName is loaded or not.
   182  func (jm *JobManager) Has(jobName string) (hasJob bool) {
   183  	jm.mu.Lock()
   184  	defer jm.mu.Unlock()
   185  	_, hasJob = jm.jobs[jobName]
   186  	return
   187  }
   188  
   189  // Job returns a job metadata by name.
   190  func (jm *JobManager) Job(jobName string) (job *JobScheduler, ok bool) {
   191  	jm.mu.Lock()
   192  	job, ok = jm.jobs[jobName]
   193  	jm.mu.Unlock()
   194  	return
   195  }
   196  
   197  // IsJobDisabled returns if a job is disabled.
   198  func (jm *JobManager) IsJobDisabled(jobName string) (value bool) {
   199  	jm.mu.Lock()
   200  	jobScheduler, hasJob := jm.jobs[jobName]
   201  	jm.mu.Unlock()
   202  	if hasJob {
   203  		value = jobScheduler.Config().Disabled
   204  	}
   205  	return
   206  }
   207  
   208  // IsJobRunning returns if a job is currently running.
   209  func (jm *JobManager) IsJobRunning(jobName string) (isRunning bool) {
   210  	jm.mu.Lock()
   211  	jobScheduler, ok := jm.jobs[jobName]
   212  	jm.mu.Unlock()
   213  	if ok {
   214  		isRunning = !jobScheduler.IsIdle()
   215  	}
   216  	return
   217  }
   218  
   219  // RunJob runs a job by jobName on demand with a given context.
   220  func (jm *JobManager) RunJob(ctx context.Context, jobName string) (*JobInvocation, error) {
   221  	jm.mu.Lock()
   222  	jobScheduler, ok := jm.jobs[jobName]
   223  	jm.mu.Unlock()
   224  	if !ok {
   225  		return nil, ErrJobNotLoaded
   226  	}
   227  	return jobScheduler.RunAsync(ctx)
   228  }
   229  
   230  // CancelJob cancels (sends the cancellation signal) to a running job.
   231  func (jm *JobManager) CancelJob(ctx context.Context, jobName string) (err error) {
   232  	jm.mu.Lock()
   233  	jobScheduler, ok := jm.jobs[jobName]
   234  	jm.mu.Unlock()
   235  	if !ok {
   236  		err = ErrJobNotLoaded
   237  		return
   238  	}
   239  	err = jobScheduler.Cancel(ctx)
   240  	return
   241  }
   242  
   243  //
   244  // status and state
   245  //
   246  
   247  // State returns the job manager state.
   248  func (jm *JobManager) State() JobManagerState {
   249  	if jm.latch.IsStarted() {
   250  		return JobManagerStateRunning
   251  	} else if jm.latch.IsStopped() {
   252  		return JobManagerStateStopped
   253  	}
   254  	return JobManagerStateUnknown
   255  }