You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
382 lines
9.3 KiB
382 lines
9.3 KiB
3 years ago
|
package gocron
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"fmt"
|
||
|
"sort"
|
||
|
"sync"
|
||
|
"sync/atomic"
|
||
|
"time"
|
||
|
|
||
|
"github.com/robfig/cron/v3"
|
||
|
"golang.org/x/sync/singleflight"
|
||
|
)
|
||
|
|
||
|
// Job struct stores the information necessary to run a Job
|
||
|
type Job struct {
|
||
|
mu sync.RWMutex
|
||
|
jobFunction
|
||
|
interval int // pause interval * unit between runs
|
||
|
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
|
||
|
lastRun time.Time // datetime of last run
|
||
|
nextRun time.Time // datetime of next run
|
||
|
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
|
||
|
runCount int // number of times the job ran
|
||
|
timer *time.Timer // handles running tasks at specific time
|
||
|
cronSchedule cron.Schedule // stores the schedule when a task uses cron
|
||
|
}
|
||
|
|
||
|
type jobFunction struct {
|
||
|
function interface{} // task's function
|
||
|
parameters []interface{} // task's function parameters
|
||
|
name string //nolint the function name to run
|
||
|
runConfig runConfig // configuration for how many times to run the job
|
||
|
limiter *singleflight.Group // limits inflight runs of job to one
|
||
|
ctx context.Context // for cancellation
|
||
|
cancel context.CancelFunc // for cancellation
|
||
|
runState *int64 // will be non-zero when jobs are running
|
||
|
}
|
||
|
|
||
|
func (jf *jobFunction) incrementRunState() {
|
||
|
if jf.runState != nil {
|
||
|
atomic.AddInt64(jf.runState, 1)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (jf *jobFunction) decrementRunState() {
|
||
|
if jf.runState != nil {
|
||
|
atomic.AddInt64(jf.runState, -1)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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())
|
||
|
var zero int64
|
||
|
job := &Job{
|
||
|
interval: interval,
|
||
|
unit: seconds,
|
||
|
lastRun: time.Time{},
|
||
|
nextRun: time.Time{},
|
||
|
jobFunction: jobFunction{
|
||
|
ctx: ctx,
|
||
|
cancel: cancel,
|
||
|
runState: &zero,
|
||
|
},
|
||
|
tags: []string{},
|
||
|
startsImmediately: startImmediately,
|
||
|
}
|
||
|
if singletonMode {
|
||
|
job.SingletonMode()
|
||
|
}
|
||
|
return job
|
||
|
}
|
||
|
|
||
|
func (j *Job) neverRan() bool {
|
||
|
return j.lastRun.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 {
|
||
|
var r time.Duration
|
||
|
if len(j.atTimes) == 0 {
|
||
|
return r
|
||
|
}
|
||
|
|
||
|
if len(j.atTimes) == 1 {
|
||
|
return j.atTimes[0]
|
||
|
}
|
||
|
|
||
|
if lastRun.IsZero() {
|
||
|
r = j.atTimes[0]
|
||
|
} else {
|
||
|
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 {
|
||
|
return j.startAtTime
|
||
|
}
|
||
|
|
||
|
func (j *Job) setStartAtTime(t time.Time) {
|
||
|
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
|
||
|
}
|
||
|
|
||
|
// 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
|
||
|
}
|
||
|
|
||
|
// 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
|
||
|
}
|
||
|
|
||
|
// 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
|
||
|
}
|
||
|
|
||
|
// 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 "0:0"
|
||
|
}
|
||
|
|
||
|
return fmt.Sprintf("%d:%d", 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("%d:%d", 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}
|
||
|
}
|
||
|
|
||
|
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.limiter = &singleflight.Group{}
|
||
|
|
||
|
}
|
||
|
|
||
|
// 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.runCount < j.runConfig.maxRuns
|
||
|
}
|
||
|
|
||
|
// LastRun returns the time the job was run last
|
||
|
func (j *Job) LastRun() time.Time {
|
||
|
return j.lastRun
|
||
|
}
|
||
|
|
||
|
func (j *Job) setLastRun(t time.Time) {
|
||
|
j.lastRun = t
|
||
|
}
|
||
|
|
||
|
// NextRun returns the time the job will run next
|
||
|
func (j *Job) NextRun() time.Time {
|
||
|
j.mu.RLock()
|
||
|
defer j.mu.RUnlock()
|
||
|
return j.nextRun
|
||
|
}
|
||
|
|
||
|
func (j *Job) setNextRun(t time.Time) {
|
||
|
j.mu.Lock()
|
||
|
defer j.mu.Unlock()
|
||
|
j.nextRun = t
|
||
|
}
|
||
|
|
||
|
// RunCount returns the number of time the job ran so far
|
||
|
func (j *Job) RunCount() int {
|
||
|
return j.runCount
|
||
|
}
|
||
|
|
||
|
func (j *Job) stop() {
|
||
|
j.mu.Lock()
|
||
|
defer j.mu.Unlock()
|
||
|
if j.timer != nil {
|
||
|
j.timer.Stop()
|
||
|
}
|
||
|
if j.cancel != nil {
|
||
|
j.cancel()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// IsRunning reports whether any instances of the job function are currently running
|
||
|
func (j *Job) IsRunning() bool {
|
||
|
return atomic.LoadInt64(j.runState) != 0
|
||
|
}
|