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 }