service/vendor/github.com/go-co-op/gocron/job.go

632 lines
18 KiB
Go

package gocron
import (
"context"
"fmt"
"math/rand"
"sort"
"sync"
"time"
"github.com/google/uuid"
"github.com/robfig/cron/v3"
"go.uber.org/atomic"
)
// Job struct stores the information necessary to run a Job
type Job struct {
mu *jobMutex
jobFunction
interval int // interval * unit between runs
random // details for randomness
duration time.Duration // time duration between runs
unit schedulingUnit // time units, e.g. 'minutes', 'hours'...
startsImmediately bool // if the Job should run upon scheduler start
atTimes []time.Duration // optional time(s) at which this Job runs when interval is day
startAtTime time.Time // optional time at which the Job starts
error error // error related to Job
scheduledWeekdays []time.Weekday // Specific days of the week to start on
daysOfTheMonth []int // Specific days of the month to run the job
tags []string // allow the user to tag jobs with certain labels
timer *time.Timer // handles running tasks at specific time
cronSchedule cron.Schedule // stores the schedule when a task uses cron
runWithDetails bool // when true the job is passed as the last arg of the jobFunc
}
type jobRunTimes struct {
jobRunTimesMu *sync.Mutex
previousRun time.Time // datetime of the run before last run
lastRun time.Time // datetime of last run
nextRun time.Time // datetime of next run
}
type random struct {
rand *rand.Rand
randomizeInterval bool // whether the interval is random
randomIntervalRange [2]int // random interval range
}
type jobFunction struct {
id uuid.UUID // unique identifier for the job
*jobRunTimes // tracking all the markers for job run times
eventListeners // additional functions to allow run 'em during job performing
function interface{} // task's function
parameters []interface{} // task's function parameters
parametersLen int // length of the passed parameters
jobName string // key of the distributed lock
funcName string // the name of the function - e.g. main.func1
runConfig runConfig // configuration for how many times to run the job
singletonQueueMu *sync.Mutex // mutex for singletonQueue
singletonQueue chan struct{} // queues jobs for the singleton runner to handle
singletonRunnerOn *atomic.Bool // whether the runner function for singleton is running
ctx context.Context // for cancellation
cancel context.CancelFunc // for cancellation
isRunning *atomic.Bool // whether the job func is currently being run
runStartCount *atomic.Int64 // number of times the job was started
runFinishCount *atomic.Int64 // number of times the job was finished
singletonWg *sync.WaitGroup // used by singleton runner
singletonWgMu *sync.Mutex // use to protect the singletonWg
stopped *atomic.Bool // tracks whether the job is currently stopped
jobFuncNextRun time.Time // the next time the job is scheduled to run
}
type eventListeners struct {
onAfterJobExecution interface{} // deprecated
onBeforeJobExecution interface{} // deprecated
beforeJobRuns func(jobName string) // called before the job executes
afterJobRuns func(jobName string) // called after the job executes
onError func(jobName string, err error) // called when the job returns an error
noError func(jobName string) // called when no error is returned
}
type jobMutex struct {
sync.RWMutex
}
func (jf *jobFunction) copy() jobFunction {
cp := jobFunction{
id: jf.id,
jobRunTimes: jf.jobRunTimes,
eventListeners: jf.eventListeners,
function: jf.function,
parameters: nil,
parametersLen: jf.parametersLen,
funcName: jf.funcName,
jobName: jf.jobName,
runConfig: jf.runConfig,
singletonQueue: jf.singletonQueue,
singletonQueueMu: jf.singletonQueueMu,
ctx: jf.ctx,
cancel: jf.cancel,
isRunning: jf.isRunning,
runStartCount: jf.runStartCount,
runFinishCount: jf.runFinishCount,
singletonWg: jf.singletonWg,
singletonWgMu: jf.singletonWgMu,
singletonRunnerOn: jf.singletonRunnerOn,
stopped: jf.stopped,
jobFuncNextRun: jf.jobFuncNextRun,
}
cp.parameters = append(cp.parameters, jf.parameters...)
return cp
}
func (jf *jobFunction) getName() string {
if jf.jobName != "" {
return jf.jobName
}
return jf.funcName
}
type runConfig struct {
finiteRuns bool
maxRuns int
mode mode
}
// mode is the Job's running mode
type mode int8
const (
// defaultMode disable any mode
defaultMode mode = iota
// singletonMode switch to single job mode
singletonMode
)
// newJob creates a new Job with the provided interval
func newJob(interval int, startImmediately bool, singletonMode bool) *Job {
ctx, cancel := context.WithCancel(context.Background())
job := &Job{
mu: &jobMutex{},
interval: interval,
unit: seconds,
jobFunction: jobFunction{
id: uuid.New(),
jobRunTimes: &jobRunTimes{
jobRunTimesMu: &sync.Mutex{},
lastRun: time.Time{},
nextRun: time.Time{},
},
ctx: ctx,
cancel: cancel,
isRunning: atomic.NewBool(false),
runStartCount: atomic.NewInt64(0),
runFinishCount: atomic.NewInt64(0),
singletonRunnerOn: atomic.NewBool(false),
stopped: atomic.NewBool(false),
},
tags: []string{},
startsImmediately: startImmediately,
}
if singletonMode {
job.SingletonMode()
}
return job
}
// Name sets the name of the current job.
//
// If the scheduler is running using WithDistributedLocker(),
// the job name is used as the distributed lock key.
func (j *Job) Name(name string) {
j.mu.Lock()
defer j.mu.Unlock()
j.jobName = name
}
// GetName returns the name of the current job.
// The name is either the name set using Job.Name() / Scheduler.Name() or
// the name of the funcion as Go sees it, for example `main.func1`
func (j *Job) GetName() string {
j.mu.Lock()
defer j.mu.Unlock()
return j.jobFunction.getName()
}
func (j *Job) setRandomInterval(a, b int) {
j.random.rand = rand.New(rand.NewSource(time.Now().UnixNano())) // nolint
j.random.randomizeInterval = true
if a < b {
j.random.randomIntervalRange[0] = a
j.random.randomIntervalRange[1] = b + 1
} else {
j.random.randomIntervalRange[0] = b
j.random.randomIntervalRange[1] = a + 1
}
}
func (j *Job) getRandomInterval() int {
randNum := j.rand.Intn(j.randomIntervalRange[1] - j.randomIntervalRange[0])
return j.randomIntervalRange[0] + randNum
}
func (j *Job) getInterval() int {
if j.randomizeInterval {
return j.getRandomInterval()
}
return j.interval
}
func (j *Job) neverRan() bool {
jobLastRun := j.LastRun()
return jobLastRun.IsZero()
}
func (j *Job) getStartsImmediately() bool {
return j.startsImmediately
}
func (j *Job) setStartsImmediately(b bool) {
j.startsImmediately = b
}
func (j *Job) setTimer(t *time.Timer) {
j.mu.Lock()
defer j.mu.Unlock()
j.timer = t
}
func (j *Job) getFirstAtTime() time.Duration {
var t time.Duration
if len(j.atTimes) > 0 {
t = j.atTimes[0]
}
return t
}
func (j *Job) getAtTime(lastRun time.Time) time.Duration {
if len(j.atTimes) == 0 {
return 0
}
r := j.atTimes[0]
if len(j.atTimes) == 1 || lastRun.IsZero() {
return r
}
for _, d := range j.atTimes {
nt := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, lastRun.Location()).Add(d)
if nt.After(lastRun) {
r = d
break
}
}
return r
}
func (j *Job) addAtTime(t time.Duration) {
if len(j.atTimes) == 0 {
j.atTimes = append(j.atTimes, t)
return
}
exist := false
index := sort.Search(len(j.atTimes), func(i int) bool {
atTime := j.atTimes[i]
b := atTime >= t
if b {
exist = atTime == t
}
return b
})
// ignore if present
if exist {
return
}
j.atTimes = append(j.atTimes, time.Duration(0))
copy(j.atTimes[index+1:], j.atTimes[index:])
j.atTimes[index] = t
}
func (j *Job) getStartAtTime() time.Time {
j.mu.RLock()
defer j.mu.RUnlock()
return j.startAtTime
}
func (j *Job) setStartAtTime(t time.Time) {
j.mu.Lock()
defer j.mu.Unlock()
j.startAtTime = t
}
func (j *Job) getUnit() schedulingUnit {
j.mu.RLock()
defer j.mu.RUnlock()
return j.unit
}
func (j *Job) setUnit(t schedulingUnit) {
j.mu.Lock()
defer j.mu.Unlock()
j.unit = t
}
func (j *Job) getDuration() time.Duration {
j.mu.RLock()
defer j.mu.RUnlock()
return j.duration
}
func (j *Job) setDuration(t time.Duration) {
j.mu.Lock()
defer j.mu.Unlock()
j.duration = t
}
func (j *Job) setInterval(i int) {
j.mu.Lock()
defer j.mu.Unlock()
j.interval = i
}
// hasTags returns true if all tags are matched on this Job
func (j *Job) hasTags(tags ...string) bool {
// Build map of all Job tags for easy comparison
jobTags := map[string]int{}
for _, tag := range j.tags {
jobTags[tag] = 0
}
// Loop through required tags and if one doesn't exist, return false
for _, tag := range tags {
_, ok := jobTags[tag]
if !ok {
return false
}
}
return true
}
// Error returns an error if one occurred while creating the Job.
// If multiple errors occurred, they will be wrapped and can be
// checked using the standard unwrap options.
func (j *Job) Error() error {
return j.error
}
// Context returns the job's context. The context controls cancellation.
func (j *Job) Context() context.Context {
return j.ctx
}
// Tag allows you to add arbitrary labels to a Job that do not
// impact the functionality of the Job
func (j *Job) Tag(tags ...string) {
j.tags = append(j.tags, tags...)
}
// Untag removes a tag from a Job
func (j *Job) Untag(t string) {
var newTags []string
for _, tag := range j.tags {
if t != tag {
newTags = append(newTags, tag)
}
}
j.tags = newTags
}
// Tags returns the tags attached to the Job
func (j *Job) Tags() []string {
return j.tags
}
// EventListener functions utilize the job's name and are triggered
// by or in the condition that the name suggests
type EventListener func(j *Job)
// BeforeJobRuns is called before the job is run
func BeforeJobRuns(eventListenerFunc func(jobName string)) EventListener {
return func(j *Job) {
j.mu.Lock()
defer j.mu.Unlock()
j.eventListeners.beforeJobRuns = eventListenerFunc
}
}
// AfterJobRuns is called after the job is run
// This is called even when an error is returned
func AfterJobRuns(eventListenerFunc func(jobName string)) EventListener {
return func(j *Job) {
j.mu.Lock()
defer j.mu.Unlock()
j.eventListeners.afterJobRuns = eventListenerFunc
}
}
// WhenJobReturnsError is called when the job returns an error
func WhenJobReturnsError(eventListenerFunc func(jobName string, err error)) EventListener {
return func(j *Job) {
j.mu.Lock()
defer j.mu.Unlock()
j.eventListeners.onError = eventListenerFunc
}
}
// WhenJobReturnsNoError is called when the job does not return an error
// the function must accept a single parameter, which is an error
func WhenJobReturnsNoError(eventListenerFunc func(jobName string)) EventListener {
return func(j *Job) {
j.mu.Lock()
defer j.mu.Unlock()
j.eventListeners.noError = eventListenerFunc
}
}
// RegisterEventListeners accepts EventListeners and registers them for the job
// The event listeners are then called at the times described by each listener.
func (j *Job) RegisterEventListeners(eventListeners ...EventListener) {
for _, el := range eventListeners {
el(j)
}
}
// Deprecated: SetEventListeners accepts two functions that will be called, one before and one after the job is run
func (j *Job) SetEventListeners(onBeforeJobExecution interface{}, onAfterJobExecution interface{}) {
j.eventListeners = eventListeners{
onBeforeJobExecution: onBeforeJobExecution,
onAfterJobExecution: onAfterJobExecution,
}
}
// ScheduledTime returns the time of the Job's next scheduled run
func (j *Job) ScheduledTime() time.Time {
j.mu.RLock()
defer j.mu.RUnlock()
return j.nextRun
}
// ScheduledUnit returns the scheduled unit of the Job.
func (j *Job) ScheduledUnit() string {
return j.unit.String()
}
// Interval returns the scheduled interval of the Job.
func (j *Job) ScheduledInterval() int {
return j.interval
}
// ScheduledAtTime returns the specific time of day the Job will run at.
// If multiple times are set, the earliest time will be returned.
func (j *Job) ScheduledAtTime() string {
if len(j.atTimes) == 0 {
return "00:00"
}
return fmt.Sprintf("%02d:%02d", j.getFirstAtTime()/time.Hour, (j.getFirstAtTime()%time.Hour)/time.Minute)
}
// ScheduledAtTimes returns the specific times of day the Job will run at
func (j *Job) ScheduledAtTimes() []string {
r := make([]string, len(j.atTimes))
for i, t := range j.atTimes {
r[i] = fmt.Sprintf("%02d:%02d", t/time.Hour, (t%time.Hour)/time.Minute)
}
return r
}
// Weekday returns which day of the week the Job will run on and
// will return an error if the Job is not scheduled weekly
func (j *Job) Weekday() (time.Weekday, error) {
if len(j.scheduledWeekdays) == 0 {
return time.Sunday, ErrNotScheduledWeekday
}
return j.scheduledWeekdays[0], nil
}
// Weekdays returns a slice of time.Weekday that the Job will run in a week and
// will return an error if the Job is not scheduled weekly
func (j *Job) Weekdays() []time.Weekday {
// appending on j.scheduledWeekdays may cause a side effect
if len(j.scheduledWeekdays) == 0 {
return []time.Weekday{time.Sunday}
}
sort.Slice(j.scheduledWeekdays, func(i, k int) bool {
return j.scheduledWeekdays[i] < j.scheduledWeekdays[k]
})
return j.scheduledWeekdays
}
// LimitRunsTo limits the number of executions of this job to n.
// Upon reaching the limit, the job is removed from the scheduler.
//
// Note: If a job is added to a running scheduler and this method is then used
// you may see the job run more than the set limit as job is scheduled immediately
// by default upon being added to the scheduler. It is recommended to use the
// LimitRunsTo() func on the scheduler chain when scheduling the job.
// For example: scheduler.LimitRunsTo(1).Do()
func (j *Job) LimitRunsTo(n int) {
j.mu.Lock()
defer j.mu.Unlock()
j.runConfig.finiteRuns = true
j.runConfig.maxRuns = n
}
// SingletonMode prevents a new job from starting if the prior job has not yet
// completed it's run
// Note: If a job is added to a running scheduler and this method is then used
// you may see the job run overrun itself as job is scheduled immediately
// by default upon being added to the scheduler. It is recommended to use the
// SingletonMode() func on the scheduler chain when scheduling the job.
func (j *Job) SingletonMode() {
j.mu.Lock()
defer j.mu.Unlock()
j.runConfig.mode = singletonMode
j.jobFunction.singletonWgMu = &sync.Mutex{}
j.jobFunction.singletonWgMu.Lock()
j.jobFunction.singletonWg = &sync.WaitGroup{}
j.jobFunction.singletonWgMu.Unlock()
j.jobFunction.singletonQueueMu = &sync.Mutex{}
j.jobFunction.singletonQueueMu.Lock()
j.jobFunction.singletonQueue = make(chan struct{}, 100)
j.jobFunction.singletonQueueMu.Unlock()
}
// shouldRun evaluates if this job should run again
// based on the runConfig
func (j *Job) shouldRun() bool {
j.mu.RLock()
defer j.mu.RUnlock()
return !j.runConfig.finiteRuns || j.runStartCount.Load() < int64(j.runConfig.maxRuns)
}
// LastRun returns the time the job was run last
func (j *Job) LastRun() time.Time {
j.jobRunTimesMu.Lock()
defer j.jobRunTimesMu.Unlock()
return j.lastRun
}
func (j *Job) setLastRun(t time.Time) {
j.previousRun = j.lastRun
j.lastRun = t
}
// NextRun returns the time the job will run next
func (j *Job) NextRun() time.Time {
j.jobRunTimesMu.Lock()
defer j.jobRunTimesMu.Unlock()
return j.nextRun
}
func (j *Job) setNextRun(t time.Time) {
j.jobRunTimesMu.Lock()
defer j.jobRunTimesMu.Unlock()
j.nextRun = t
j.jobFunction.jobFuncNextRun = t
}
// PreviousRun returns the job run time previous to LastRun
func (j *Job) PreviousRun() time.Time {
j.jobRunTimesMu.Lock()
defer j.jobRunTimesMu.Unlock()
return j.previousRun
}
// RunCount returns the number of times the job has been started
func (j *Job) RunCount() int {
j.mu.Lock()
defer j.mu.Unlock()
return int(j.runStartCount.Load())
}
// FinishedRunCount returns the number of times the job has finished running
func (j *Job) FinishedRunCount() int {
j.mu.Lock()
defer j.mu.Unlock()
return int(j.runFinishCount.Load())
}
func (j *Job) stop() {
j.mu.Lock()
defer j.mu.Unlock()
if j.timer != nil {
j.timer.Stop()
}
if j.cancel != nil {
j.cancel()
j.ctx, j.cancel = context.WithCancel(context.Background())
}
j.stopped.Store(true)
}
// IsRunning reports whether any instances of the job function are currently running
func (j *Job) IsRunning() bool {
return j.isRunning.Load()
}
// you must Lock the job before calling copy
func (j *Job) copy() Job {
return Job{
mu: &jobMutex{},
jobFunction: j.jobFunction,
interval: j.interval,
duration: j.duration,
unit: j.unit,
startsImmediately: j.startsImmediately,
atTimes: j.atTimes,
startAtTime: j.startAtTime,
error: j.error,
scheduledWeekdays: j.scheduledWeekdays,
daysOfTheMonth: j.daysOfTheMonth,
tags: j.tags,
timer: j.timer,
cronSchedule: j.cronSchedule,
runWithDetails: j.runWithDetails,
}
}