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

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
}