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 }