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.
1205 lines
32 KiB
1205 lines
32 KiB
package gocron |
|
|
|
import ( |
|
"context" |
|
"fmt" |
|
"reflect" |
|
"sort" |
|
"strings" |
|
"sync" |
|
"time" |
|
|
|
"github.com/robfig/cron/v3" |
|
"golang.org/x/sync/semaphore" |
|
) |
|
|
|
type limitMode int8 |
|
|
|
// Scheduler struct stores a list of Jobs and the location of time used by the Scheduler, |
|
// and implements the sort.Interface{} for sorting Jobs, by the time of nextRun |
|
type Scheduler struct { |
|
jobsMutex sync.RWMutex |
|
jobs []*Job |
|
|
|
locationMutex sync.RWMutex |
|
location *time.Location |
|
runningMutex sync.RWMutex |
|
running bool // represents if the scheduler is running at the moment or not |
|
|
|
time timeWrapper // wrapper around time.Time |
|
executor *executor // executes jobs passed via chan |
|
|
|
tags sync.Map // for storing tags when unique tags is set |
|
|
|
tagsUnique bool // defines whether tags should be unique |
|
updateJob bool // so the scheduler knows to create a new job or update the current |
|
waitForInterval bool // defaults jobs to waiting for first interval to start |
|
singletonMode bool // defaults all jobs to use SingletonMode() |
|
jobCreated bool // so the scheduler knows a job was created prior to calling Every or Cron |
|
} |
|
|
|
// days in a week |
|
const allWeekDays = 7 |
|
|
|
// NewScheduler creates a new Scheduler |
|
func NewScheduler(loc *time.Location) *Scheduler { |
|
executor := newExecutor() |
|
|
|
return &Scheduler{ |
|
jobs: make([]*Job, 0), |
|
location: loc, |
|
running: false, |
|
time: &trueTime{}, |
|
executor: &executor, |
|
tagsUnique: false, |
|
} |
|
} |
|
|
|
// SetMaxConcurrentJobs limits how many jobs can be running at the same time. |
|
// This is useful when running resource intensive jobs and a precise start time is not critical. |
|
func (s *Scheduler) SetMaxConcurrentJobs(n int, mode limitMode) { |
|
s.executor.maxRunningJobs = semaphore.NewWeighted(int64(n)) |
|
s.executor.limitMode = mode |
|
} |
|
|
|
// StartBlocking starts all jobs and blocks the current thread |
|
func (s *Scheduler) StartBlocking() { |
|
s.StartAsync() |
|
<-make(chan bool) |
|
} |
|
|
|
// StartAsync starts all jobs without blocking the current thread |
|
func (s *Scheduler) StartAsync() { |
|
if !s.IsRunning() { |
|
s.start() |
|
} |
|
} |
|
|
|
//start starts the scheduler, scheduling and running jobs |
|
func (s *Scheduler) start() { |
|
go s.executor.start() |
|
s.setRunning(true) |
|
s.runJobs(s.Jobs()) |
|
} |
|
|
|
func (s *Scheduler) runJobs(jobs []*Job) { |
|
for _, job := range jobs { |
|
s.scheduleNextRun(job) |
|
} |
|
} |
|
|
|
func (s *Scheduler) setRunning(b bool) { |
|
s.runningMutex.Lock() |
|
defer s.runningMutex.Unlock() |
|
s.running = b |
|
} |
|
|
|
// IsRunning returns true if the scheduler is running |
|
func (s *Scheduler) IsRunning() bool { |
|
s.runningMutex.RLock() |
|
defer s.runningMutex.RUnlock() |
|
return s.running |
|
} |
|
|
|
// Jobs returns the list of Jobs from the Scheduler |
|
func (s *Scheduler) Jobs() []*Job { |
|
s.jobsMutex.RLock() |
|
defer s.jobsMutex.RUnlock() |
|
return s.jobs |
|
} |
|
|
|
func (s *Scheduler) setJobs(jobs []*Job) { |
|
s.jobsMutex.Lock() |
|
defer s.jobsMutex.Unlock() |
|
s.jobs = jobs |
|
} |
|
|
|
// Len returns the number of Jobs in the Scheduler - implemented for sort |
|
func (s *Scheduler) Len() int { |
|
s.jobsMutex.RLock() |
|
defer s.jobsMutex.RUnlock() |
|
return len(s.jobs) |
|
} |
|
|
|
// Swap places each job into the other job's position given |
|
// the provided job indexes. |
|
func (s *Scheduler) Swap(i, j int) { |
|
s.jobsMutex.Lock() |
|
defer s.jobsMutex.Unlock() |
|
s.jobs[i], s.jobs[j] = s.jobs[j], s.jobs[i] |
|
} |
|
|
|
// Less compares the next run of jobs based on their index. |
|
// Returns true if the second job is after the first. |
|
func (s *Scheduler) Less(first, second int) bool { |
|
return s.Jobs()[second].NextRun().Unix() >= s.Jobs()[first].NextRun().Unix() |
|
} |
|
|
|
// ChangeLocation changes the default time location |
|
func (s *Scheduler) ChangeLocation(newLocation *time.Location) { |
|
s.locationMutex.Lock() |
|
defer s.locationMutex.Unlock() |
|
s.location = newLocation |
|
} |
|
|
|
// Location provides the current location set on the scheduler |
|
func (s *Scheduler) Location() *time.Location { |
|
s.locationMutex.RLock() |
|
defer s.locationMutex.RUnlock() |
|
return s.location |
|
} |
|
|
|
type nextRun struct { |
|
duration time.Duration |
|
dateTime time.Time |
|
} |
|
|
|
// scheduleNextRun Compute the instant when this Job should run next |
|
func (s *Scheduler) scheduleNextRun(job *Job) { |
|
now := s.now() |
|
lastRun := job.LastRun() |
|
if !s.jobPresent(job) { |
|
return |
|
} |
|
|
|
if job.getStartsImmediately() { |
|
s.run(job) |
|
lastRun = now |
|
job.setStartsImmediately(false) |
|
} |
|
|
|
if job.neverRan() { |
|
// Increment startAtTime to the future |
|
if !job.startAtTime.IsZero() && job.startAtTime.Before(now) { |
|
duration := s.durationToNextRun(job.startAtTime, job).duration |
|
job.startAtTime = job.startAtTime.Add(duration) |
|
if job.startAtTime.Before(now) { |
|
diff := now.Sub(job.startAtTime) |
|
duration := s.durationToNextRun(job.startAtTime, job).duration |
|
count := diff / duration |
|
if diff%duration != 0 { |
|
count++ |
|
} |
|
job.startAtTime = job.startAtTime.Add(duration * count) |
|
} |
|
} |
|
lastRun = now |
|
} |
|
|
|
if !job.shouldRun() { |
|
s.RemoveByReference(job) |
|
return |
|
} |
|
|
|
next := s.durationToNextRun(lastRun, job) |
|
|
|
if next.dateTime.IsZero() { |
|
job.setNextRun(lastRun.Add(next.duration)) |
|
} else { |
|
job.setNextRun(next.dateTime) |
|
} |
|
job.setTimer(time.AfterFunc(next.duration, func() { |
|
if !next.dateTime.IsZero() { |
|
for { |
|
if time.Now().Unix() >= next.dateTime.Unix() { |
|
break |
|
} |
|
} |
|
} |
|
s.run(job) |
|
s.scheduleNextRun(job) |
|
})) |
|
} |
|
|
|
// durationToNextRun calculate how much time to the next run, depending on unit |
|
func (s *Scheduler) durationToNextRun(lastRun time.Time, job *Job) nextRun { |
|
// job can be scheduled with .StartAt() |
|
if job.getStartAtTime().After(lastRun) { |
|
return nextRun{duration: job.getStartAtTime().Sub(s.now()), dateTime: job.getStartAtTime()} |
|
} |
|
|
|
var next nextRun |
|
switch job.getUnit() { |
|
case milliseconds, seconds, minutes, hours: |
|
next.duration = s.calculateDuration(job) |
|
case days: |
|
next = s.calculateDays(job, lastRun) |
|
case weeks: |
|
if len(job.scheduledWeekdays) != 0 { // weekday selected, Every().Monday(), for example |
|
next = s.calculateWeekday(job, lastRun) |
|
} else { |
|
next = s.calculateWeeks(job, lastRun) |
|
} |
|
case months: |
|
next = s.calculateMonths(job, lastRun) |
|
case duration: |
|
next.duration = job.getDuration() |
|
case crontab: |
|
next.dateTime = job.cronSchedule.Next(lastRun) |
|
next.duration = next.dateTime.Sub(lastRun) |
|
} |
|
return next |
|
} |
|
|
|
func (s *Scheduler) calculateMonths(job *Job, lastRun time.Time) nextRun { |
|
lastRunRoundedMidnight := s.roundToMidnight(lastRun) |
|
|
|
// Special case: the last day of the month |
|
if len(job.daysOfTheMonth) == 1 && job.daysOfTheMonth[0] == -1 { |
|
return calculateNextRunForLastDayOfMonth(s, job, lastRun) |
|
} |
|
|
|
if len(job.daysOfTheMonth) != 0 { // calculate days to job.daysOfTheMonth |
|
|
|
nextRunDateMap := make(map[int]nextRun) |
|
for _, day := range job.daysOfTheMonth { |
|
nextRunDateMap[day] = calculateNextRunForMonth(s, job, lastRun, day) |
|
} |
|
|
|
nextRunResult := nextRun{} |
|
for _, val := range nextRunDateMap { |
|
if nextRunResult.dateTime.IsZero() { |
|
nextRunResult = val |
|
} else if nextRunResult.dateTime.Sub(val.dateTime).Milliseconds() > 0 { |
|
nextRunResult = val |
|
} |
|
} |
|
|
|
return nextRunResult |
|
} |
|
next := lastRunRoundedMidnight.Add(job.getFirstAtTime()).AddDate(0, job.interval, 0) |
|
return nextRun{duration: until(lastRun, next), dateTime: next} |
|
} |
|
|
|
func calculateNextRunForLastDayOfMonth(s *Scheduler, job *Job, lastRun time.Time) nextRun { |
|
// Calculate the last day of the next month, by adding job.interval+1 months (i.e. the |
|
// first day of the month after the next month), and subtracting one day, unless the |
|
// last run occurred before the end of the month. |
|
addMonth := job.interval |
|
atTime := job.getAtTime(lastRun) |
|
if testDate := lastRun.AddDate(0, 0, 1); testDate.Month() != lastRun.Month() && |
|
!s.roundToMidnight(lastRun).Add(atTime).After(lastRun) { |
|
// Our last run was on the last day of this month. |
|
addMonth++ |
|
atTime = job.getFirstAtTime() |
|
} |
|
|
|
next := time.Date(lastRun.Year(), lastRun.Month(), 1, 0, 0, 0, 0, s.Location()). |
|
Add(atTime). |
|
AddDate(0, addMonth, 0). |
|
AddDate(0, 0, -1) |
|
return nextRun{duration: until(lastRun, next), dateTime: next} |
|
} |
|
|
|
func calculateNextRunForMonth(s *Scheduler, job *Job, lastRun time.Time, dayOfMonth int) nextRun { |
|
atTime := job.getAtTime(lastRun) |
|
natTime := atTime |
|
jobDay := time.Date(lastRun.Year(), lastRun.Month(), dayOfMonth, 0, 0, 0, 0, s.Location()).Add(atTime) |
|
difference := absDuration(lastRun.Sub(jobDay)) |
|
next := lastRun |
|
if jobDay.Before(lastRun) { // shouldn't run this month; schedule for next interval minus day difference |
|
next = next.AddDate(0, job.interval, -0) |
|
next = next.Add(-difference) |
|
natTime = job.getFirstAtTime() |
|
} else { |
|
if job.interval == 1 && !jobDay.Equal(lastRun) { // every month counts current month |
|
next = next.AddDate(0, job.interval-1, 0) |
|
} else { // should run next month interval |
|
next = next.AddDate(0, job.interval, 0) |
|
natTime = job.getFirstAtTime() |
|
} |
|
next = next.Add(difference) |
|
} |
|
if atTime != natTime { |
|
next = next.Add(-atTime).Add(natTime) |
|
} |
|
return nextRun{duration: until(lastRun, next), dateTime: next} |
|
} |
|
|
|
func (s *Scheduler) calculateWeekday(job *Job, lastRun time.Time) nextRun { |
|
daysToWeekday := s.remainingDaysToWeekday(lastRun, job) |
|
totalDaysDifference := s.calculateTotalDaysDifference(lastRun, daysToWeekday, job) |
|
acTime := job.getAtTime(lastRun) |
|
if totalDaysDifference > 0 { |
|
acTime = job.getFirstAtTime() |
|
} |
|
next := s.roundToMidnight(lastRun).Add(acTime).AddDate(0, 0, totalDaysDifference) |
|
return nextRun{duration: until(lastRun, next), dateTime: next} |
|
} |
|
|
|
func (s *Scheduler) calculateWeeks(job *Job, lastRun time.Time) nextRun { |
|
totalDaysDifference := int(job.interval) * 7 |
|
next := s.roundToMidnight(lastRun).Add(job.getFirstAtTime()).AddDate(0, 0, totalDaysDifference) |
|
return nextRun{duration: until(lastRun, next), dateTime: next} |
|
} |
|
|
|
func (s *Scheduler) calculateTotalDaysDifference(lastRun time.Time, daysToWeekday int, job *Job) int { |
|
if job.interval > 1 && job.RunCount() < len(job.Weekdays()) { // just count weeks after the first jobs were done |
|
return daysToWeekday |
|
} |
|
if job.interval > 1 && job.RunCount() >= len(job.Weekdays()) { |
|
if daysToWeekday > 0 { |
|
return int(job.interval)*7 - (allWeekDays - daysToWeekday) |
|
} |
|
|
|
return int(job.interval) * 7 |
|
} |
|
|
|
if daysToWeekday == 0 { // today, at future time or already passed |
|
lastRunAtTime := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, s.Location()).Add(job.getAtTime(lastRun)) |
|
if lastRun.Before(lastRunAtTime) { |
|
return 0 |
|
} |
|
return 7 |
|
} |
|
return daysToWeekday |
|
} |
|
|
|
func (s *Scheduler) calculateDays(job *Job, lastRun time.Time) nextRun { |
|
|
|
if job.interval == 1 { |
|
lastRunDayPlusJobAtTime := s.roundToMidnight(lastRun).Add(job.getAtTime(lastRun)) |
|
|
|
// handle occasional occurrence of job running to quickly / too early such that last run was within a second of now |
|
lastRunUnix, nowUnix := job.LastRun().Unix(), s.now().Unix() |
|
if lastRunUnix == nowUnix || lastRunUnix == nowUnix-1 || lastRunUnix == nowUnix+1 { |
|
lastRun = lastRunDayPlusJobAtTime |
|
} |
|
|
|
if shouldRunToday(lastRun, lastRunDayPlusJobAtTime) { |
|
return nextRun{duration: until(lastRun, lastRunDayPlusJobAtTime), dateTime: lastRunDayPlusJobAtTime} |
|
} |
|
} |
|
|
|
nextRunAtTime := s.roundToMidnight(lastRun).Add(job.getFirstAtTime()).AddDate(0, 0, job.interval).In(s.Location()) |
|
return nextRun{duration: until(lastRun, nextRunAtTime), dateTime: nextRunAtTime} |
|
} |
|
|
|
func until(from time.Time, until time.Time) time.Duration { |
|
return until.Sub(from) |
|
} |
|
|
|
func shouldRunToday(lastRun time.Time, atTime time.Time) bool { |
|
return lastRun.Before(atTime) |
|
} |
|
|
|
func in(scheduleWeekdays []time.Weekday, weekday time.Weekday) bool { |
|
in := false |
|
|
|
for _, weekdayInSchedule := range scheduleWeekdays { |
|
if int(weekdayInSchedule) == int(weekday) { |
|
in = true |
|
break |
|
} |
|
} |
|
return in |
|
} |
|
|
|
func (s *Scheduler) calculateDuration(job *Job) time.Duration { |
|
if job.neverRan() && shouldRunAtSpecificTime(job) { // ugly. in order to avoid this we could prohibit setting .At() and allowing only .StartAt() when dealing with Duration types |
|
now := s.time.Now(s.location) |
|
next := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, s.Location()).Add(job.getFirstAtTime()) |
|
if now.Before(next) || now.Equal(next) { |
|
return next.Sub(now) |
|
} |
|
} |
|
|
|
interval := job.interval |
|
switch job.getUnit() { |
|
case milliseconds: |
|
return time.Duration(interval) * time.Millisecond |
|
case seconds: |
|
return time.Duration(interval) * time.Second |
|
case minutes: |
|
return time.Duration(interval) * time.Minute |
|
default: |
|
return time.Duration(interval) * time.Hour |
|
} |
|
} |
|
|
|
func shouldRunAtSpecificTime(job *Job) bool { |
|
return job.getAtTime(job.lastRun) != 0 |
|
} |
|
|
|
func (s *Scheduler) remainingDaysToWeekday(lastRun time.Time, job *Job) int { |
|
weekDays := job.Weekdays() |
|
sort.Slice(weekDays, func(i, j int) bool { |
|
return weekDays[i] < weekDays[j] |
|
}) |
|
|
|
equals := false |
|
lastRunWeekday := lastRun.Weekday() |
|
index := sort.Search(len(weekDays), func(i int) bool { |
|
b := weekDays[i] >= lastRunWeekday |
|
if b { |
|
equals = weekDays[i] == lastRunWeekday |
|
} |
|
return b |
|
}) |
|
// check atTime |
|
if equals { |
|
if s.roundToMidnight(lastRun).Add(job.getAtTime(lastRun)).After(lastRun) { |
|
return 0 |
|
} |
|
index++ |
|
} |
|
|
|
if index < len(weekDays) { |
|
return int(weekDays[index] - lastRunWeekday) |
|
} |
|
|
|
return int(weekDays[0]) + allWeekDays - int(lastRunWeekday) |
|
} |
|
|
|
// absDuration returns the abs time difference |
|
func absDuration(a time.Duration) time.Duration { |
|
if a >= 0 { |
|
return a |
|
} |
|
return -a |
|
} |
|
|
|
// roundToMidnight truncates time to midnight |
|
func (s *Scheduler) roundToMidnight(t time.Time) time.Time { |
|
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, s.Location()) |
|
} |
|
|
|
// NextRun datetime when the next Job should run. |
|
func (s *Scheduler) NextRun() (*Job, time.Time) { |
|
if len(s.Jobs()) <= 0 { |
|
return nil, s.now() |
|
} |
|
|
|
sort.Sort(s) |
|
|
|
return s.Jobs()[0], s.Jobs()[0].NextRun() |
|
} |
|
|
|
// Every schedules a new periodic Job with an interval. |
|
// Interval can be an int, time.Duration or a string that |
|
// parses with time.ParseDuration(). |
|
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". |
|
func (s *Scheduler) Every(interval interface{}) *Scheduler { |
|
job := &Job{} |
|
if s.updateJob || s.jobCreated { |
|
job = s.getCurrentJob() |
|
} |
|
|
|
switch interval := interval.(type) { |
|
case int: |
|
if !(s.updateJob || s.jobCreated) { |
|
job = s.newJob(interval) |
|
} else { |
|
job = s.newJob(interval) |
|
} |
|
if interval <= 0 { |
|
job.error = wrapOrError(job.error, ErrInvalidInterval) |
|
} |
|
case time.Duration: |
|
if !(s.updateJob || s.jobCreated) { |
|
job = s.newJob(0) |
|
} else { |
|
job.interval = 0 |
|
} |
|
job.setDuration(interval) |
|
job.setUnit(duration) |
|
case string: |
|
if !(s.updateJob || s.jobCreated) { |
|
job = s.newJob(0) |
|
} else { |
|
job.interval = 0 |
|
} |
|
d, err := time.ParseDuration(interval) |
|
if err != nil { |
|
job.error = wrapOrError(job.error, err) |
|
} |
|
job.setDuration(d) |
|
job.setUnit(duration) |
|
default: |
|
if !(s.updateJob || s.jobCreated) { |
|
job = s.newJob(0) |
|
} else { |
|
job.interval = 0 |
|
} |
|
job.error = wrapOrError(job.error, ErrInvalidIntervalType) |
|
} |
|
|
|
if s.updateJob || s.jobCreated { |
|
s.setJobs(append(s.Jobs()[:len(s.Jobs())-1], job)) |
|
if s.jobCreated { |
|
s.jobCreated = false |
|
} |
|
} else { |
|
s.setJobs(append(s.Jobs(), job)) |
|
} |
|
|
|
return s |
|
} |
|
|
|
func (s *Scheduler) run(job *Job) { |
|
if !s.IsRunning() { |
|
return |
|
} |
|
|
|
job.mu.Lock() |
|
defer job.mu.Unlock() |
|
job.setLastRun(s.now()) |
|
job.runCount++ |
|
s.executor.jobFunctions <- job.jobFunction |
|
} |
|
|
|
// RunAll run all Jobs regardless if they are scheduled to run or not |
|
func (s *Scheduler) RunAll() { |
|
s.RunAllWithDelay(0) |
|
} |
|
|
|
// RunAllWithDelay runs all jobs with the provided delay in between each job |
|
func (s *Scheduler) RunAllWithDelay(d time.Duration) { |
|
for _, job := range s.Jobs() { |
|
s.run(job) |
|
s.time.Sleep(d) |
|
} |
|
} |
|
|
|
// RunByTag runs all the jobs containing a specific tag |
|
// regardless of whether they are scheduled to run or not |
|
func (s *Scheduler) RunByTag(tag string) error { |
|
return s.RunByTagWithDelay(tag, 0) |
|
} |
|
|
|
// RunByTagWithDelay is same as RunByTag but introduces a delay between |
|
// each job execution |
|
func (s *Scheduler) RunByTagWithDelay(tag string, d time.Duration) error { |
|
jobs, err := s.FindJobsByTag(tag) |
|
if err != nil { |
|
return err |
|
} |
|
for _, job := range jobs { |
|
s.run(job) |
|
s.time.Sleep(d) |
|
} |
|
return nil |
|
} |
|
|
|
// Remove specific Job by function |
|
// |
|
// Removing a job stops that job's timer. However, if a job has already |
|
// been started by by the job's timer before being removed, there is no way to stop |
|
// it through gocron as https://pkg.go.dev/time#Timer.Stop explains. |
|
// The job function would need to have implemented a means of |
|
// stopping, e.g. using a context.WithCancel(). |
|
func (s *Scheduler) Remove(job interface{}) { |
|
fName := getFunctionName(job) |
|
j := s.findJobByTaskName(fName) |
|
s.removeJobsUniqueTags(j) |
|
s.removeByCondition(func(someJob *Job) bool { |
|
return someJob.name == fName |
|
}) |
|
} |
|
|
|
// RemoveByReference removes specific Job by reference |
|
func (s *Scheduler) RemoveByReference(job *Job) { |
|
s.removeJobsUniqueTags(job) |
|
s.removeByCondition(func(someJob *Job) bool { |
|
job.mu.RLock() |
|
defer job.mu.RUnlock() |
|
return someJob == job |
|
}) |
|
} |
|
|
|
func (s *Scheduler) findJobByTaskName(name string) *Job { |
|
for _, job := range s.Jobs() { |
|
if job.name == name { |
|
return job |
|
} |
|
} |
|
return nil |
|
} |
|
|
|
func (s *Scheduler) removeJobsUniqueTags(job *Job) { |
|
if job == nil { |
|
return |
|
} |
|
if s.tagsUnique && len(job.tags) > 0 { |
|
for _, tag := range job.tags { |
|
s.tags.Delete(tag) |
|
} |
|
} |
|
} |
|
|
|
func (s *Scheduler) removeByCondition(shouldRemove func(*Job) bool) { |
|
retainedJobs := make([]*Job, 0) |
|
for _, job := range s.Jobs() { |
|
if !shouldRemove(job) { |
|
retainedJobs = append(retainedJobs, job) |
|
} else { |
|
job.stop() |
|
} |
|
} |
|
s.setJobs(retainedJobs) |
|
} |
|
|
|
// RemoveByTag will remove Jobs that match the given tag. |
|
func (s *Scheduler) RemoveByTag(tag string) error { |
|
return s.RemoveByTags(tag) |
|
} |
|
|
|
// RemoveByTags will remove Jobs that match all given tags. |
|
func (s *Scheduler) RemoveByTags(tags ...string) error { |
|
jobs, err := s.FindJobsByTag(tags...) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
for _, job := range jobs { |
|
s.RemoveByReference(job) |
|
} |
|
return nil |
|
} |
|
|
|
// RemoveByTagsAny will remove Jobs that match any one of the given tags. |
|
func (s *Scheduler) RemoveByTagsAny(tags ...string) error { |
|
var errs error |
|
mJob := make(map[*Job]struct{}) |
|
for _, tag := range tags { |
|
jobs, err := s.FindJobsByTag(tag) |
|
if err != nil { |
|
errs = wrapOrError(errs, fmt.Errorf("%s: %s", err.Error(), tag)) |
|
} |
|
for _, job := range jobs { |
|
mJob[job] = struct{}{} |
|
} |
|
} |
|
|
|
for job := range mJob { |
|
s.RemoveByReference(job) |
|
} |
|
|
|
return errs |
|
} |
|
|
|
// FindJobsByTag will return a slice of Jobs that match all given tags |
|
func (s *Scheduler) FindJobsByTag(tags ...string) ([]*Job, error) { |
|
var jobs []*Job |
|
|
|
Jobs: |
|
for _, job := range s.Jobs() { |
|
if job.hasTags(tags...) { |
|
jobs = append(jobs, job) |
|
continue Jobs |
|
} |
|
} |
|
|
|
if len(jobs) > 0 { |
|
return jobs, nil |
|
} |
|
return nil, ErrJobNotFoundWithTag |
|
} |
|
|
|
// LimitRunsTo limits the number of executions of this job to n. |
|
// Upon reaching the limit, the job is removed from the scheduler. |
|
func (s *Scheduler) LimitRunsTo(i int) *Scheduler { |
|
job := s.getCurrentJob() |
|
job.LimitRunsTo(i) |
|
return s |
|
} |
|
|
|
// SingletonMode prevents a new job from starting if the prior job has not yet |
|
// completed its run |
|
func (s *Scheduler) SingletonMode() *Scheduler { |
|
job := s.getCurrentJob() |
|
job.SingletonMode() |
|
return s |
|
} |
|
|
|
// SingletonModeAll prevents new jobs from starting if the prior instance of the |
|
// particular job has not yet completed its run |
|
func (s *Scheduler) SingletonModeAll() { |
|
s.singletonMode = true |
|
} |
|
|
|
// TaskPresent checks if specific job's function was added to the scheduler. |
|
func (s *Scheduler) TaskPresent(j interface{}) bool { |
|
for _, job := range s.Jobs() { |
|
if job.name == getFunctionName(j) { |
|
return true |
|
} |
|
} |
|
return false |
|
} |
|
|
|
// To avoid the recursive read lock on s.Jobs() and this function, |
|
// creating this new function and distributing the lock between jobPresent, _jobPresent |
|
func (s *Scheduler) _jobPresent(j *Job, jobs []*Job) bool { |
|
s.jobsMutex.RLock() |
|
defer s.jobsMutex.RUnlock() |
|
for _, job := range jobs { |
|
if job == j { |
|
return true |
|
} |
|
} |
|
return false |
|
} |
|
|
|
func (s *Scheduler) jobPresent(j *Job) bool { |
|
return s._jobPresent(j, s.Jobs()) |
|
} |
|
|
|
// Clear clears all Jobs from this scheduler |
|
func (s *Scheduler) Clear() { |
|
for _, job := range s.Jobs() { |
|
job.stop() |
|
} |
|
s.setJobs(make([]*Job, 0)) |
|
// If unique tags was enabled, delete all the tags loaded in the tags sync.Map |
|
if s.tagsUnique { |
|
s.tags.Range(func(key interface{}, value interface{}) bool { |
|
s.tags.Delete(key) |
|
return true |
|
}) |
|
} |
|
} |
|
|
|
// Stop stops the scheduler. This is a no-op if the scheduler is already stopped. |
|
// It waits for all running jobs to finish before returning, so it is safe to assume that running jobs will finish when calling this. |
|
func (s *Scheduler) Stop() { |
|
if s.IsRunning() { |
|
s.stop() |
|
} |
|
} |
|
|
|
func (s *Scheduler) stop() { |
|
s.setRunning(false) |
|
s.executor.stop() |
|
} |
|
|
|
// Do specifies the jobFunc that should be called every time the Job runs |
|
func (s *Scheduler) Do(jobFun interface{}, params ...interface{}) (*Job, error) { |
|
job := s.getCurrentJob() |
|
|
|
jobUnit := job.getUnit() |
|
if job.getAtTime(job.lastRun) != 0 && (jobUnit <= hours || jobUnit >= duration) { |
|
job.error = wrapOrError(job.error, ErrAtTimeNotSupported) |
|
} |
|
|
|
if len(job.scheduledWeekdays) != 0 && jobUnit != weeks { |
|
job.error = wrapOrError(job.error, ErrWeekdayNotSupported) |
|
} |
|
|
|
if job.unit != crontab && job.interval == 0 { |
|
if job.unit != duration { |
|
job.error = wrapOrError(job.error, ErrInvalidInterval) |
|
} |
|
} |
|
|
|
if job.error != nil { |
|
// delete the job from the scheduler as this job |
|
// cannot be executed |
|
s.RemoveByReference(job) |
|
return nil, job.error |
|
} |
|
|
|
typ := reflect.TypeOf(jobFun) |
|
if typ.Kind() != reflect.Func { |
|
// delete the job for the same reason as above |
|
s.RemoveByReference(job) |
|
return nil, ErrNotAFunction |
|
} |
|
|
|
f := reflect.ValueOf(jobFun) |
|
if len(params) != f.Type().NumIn() { |
|
s.RemoveByReference(job) |
|
job.error = wrapOrError(job.error, ErrWrongParams) |
|
return nil, job.error |
|
} |
|
|
|
fname := getFunctionName(jobFun) |
|
if job.name != fname { |
|
job.function = jobFun |
|
job.parameters = params |
|
job.name = fname |
|
} |
|
|
|
// we should not schedule if not running since we can't foresee how long it will take for the scheduler to start |
|
if s.IsRunning() { |
|
s.scheduleNextRun(job) |
|
} |
|
|
|
return job, nil |
|
} |
|
|
|
// At schedules the Job at a specific time of day in the form "HH:MM:SS" or "HH:MM" |
|
// or time.Time (note that only the hours, minutes, seconds and nanos are used). |
|
func (s *Scheduler) At(i interface{}) *Scheduler { |
|
job := s.getCurrentJob() |
|
|
|
switch t := i.(type) { |
|
case string: |
|
for _, tt := range strings.Split(t, ";") { |
|
hour, min, sec, err := parseTime(tt) |
|
if err != nil { |
|
job.error = wrapOrError(job.error, err) |
|
return s |
|
} |
|
// save atTime start as duration from midnight |
|
job.addAtTime(time.Duration(hour)*time.Hour + time.Duration(min)*time.Minute + time.Duration(sec)*time.Second) |
|
} |
|
case time.Time: |
|
job.addAtTime(time.Duration(t.Hour())*time.Hour + time.Duration(t.Minute())*time.Minute + time.Duration(t.Second())*time.Second + time.Duration(t.Nanosecond())*time.Nanosecond) |
|
default: |
|
job.error = wrapOrError(job.error, ErrUnsupportedTimeFormat) |
|
} |
|
job.startsImmediately = false |
|
return s |
|
} |
|
|
|
// Tag will add a tag when creating a job. |
|
func (s *Scheduler) Tag(t ...string) *Scheduler { |
|
job := s.getCurrentJob() |
|
|
|
if s.tagsUnique { |
|
for _, tag := range t { |
|
if _, ok := s.tags.Load(tag); ok { |
|
job.error = wrapOrError(job.error, ErrTagsUnique(tag)) |
|
return s |
|
} |
|
s.tags.Store(tag, struct{}{}) |
|
} |
|
} |
|
|
|
job.tags = append(job.tags, t...) |
|
return s |
|
} |
|
|
|
// StartAt schedules the next run of the Job. If this time is in the past, the configured interval will be used |
|
// to calculate the next future time |
|
func (s *Scheduler) StartAt(t time.Time) *Scheduler { |
|
job := s.getCurrentJob() |
|
job.setStartAtTime(t) |
|
job.startsImmediately = false |
|
return s |
|
} |
|
|
|
// setUnit sets the unit type |
|
func (s *Scheduler) setUnit(unit schedulingUnit) { |
|
job := s.getCurrentJob() |
|
currentUnit := job.getUnit() |
|
if currentUnit == duration || currentUnit == crontab { |
|
job.error = wrapOrError(job.error, ErrInvalidIntervalUnitsSelection) |
|
return |
|
} |
|
job.setUnit(unit) |
|
} |
|
|
|
// Millisecond sets the unit with seconds |
|
func (s *Scheduler) Millisecond() *Scheduler { |
|
return s.Milliseconds() |
|
} |
|
|
|
// Milliseconds sets the unit with seconds |
|
func (s *Scheduler) Milliseconds() *Scheduler { |
|
s.setUnit(milliseconds) |
|
return s |
|
} |
|
|
|
// Second sets the unit with seconds |
|
func (s *Scheduler) Second() *Scheduler { |
|
return s.Seconds() |
|
} |
|
|
|
// Seconds sets the unit with seconds |
|
func (s *Scheduler) Seconds() *Scheduler { |
|
s.setUnit(seconds) |
|
return s |
|
} |
|
|
|
// Minute sets the unit with minutes |
|
func (s *Scheduler) Minute() *Scheduler { |
|
return s.Minutes() |
|
} |
|
|
|
// Minutes sets the unit with minutes |
|
func (s *Scheduler) Minutes() *Scheduler { |
|
s.setUnit(minutes) |
|
return s |
|
} |
|
|
|
// Hour sets the unit with hours |
|
func (s *Scheduler) Hour() *Scheduler { |
|
return s.Hours() |
|
} |
|
|
|
// Hours sets the unit with hours |
|
func (s *Scheduler) Hours() *Scheduler { |
|
s.setUnit(hours) |
|
return s |
|
} |
|
|
|
// Day sets the unit with days |
|
func (s *Scheduler) Day() *Scheduler { |
|
s.setUnit(days) |
|
return s |
|
} |
|
|
|
// Days set the unit with days |
|
func (s *Scheduler) Days() *Scheduler { |
|
s.setUnit(days) |
|
return s |
|
} |
|
|
|
// Week sets the unit with weeks |
|
func (s *Scheduler) Week() *Scheduler { |
|
s.setUnit(weeks) |
|
return s |
|
} |
|
|
|
// Weeks sets the unit with weeks |
|
func (s *Scheduler) Weeks() *Scheduler { |
|
s.setUnit(weeks) |
|
return s |
|
} |
|
|
|
// Month sets the unit with months |
|
func (s *Scheduler) Month(daysOfMonth ...int) *Scheduler { |
|
return s.Months(daysOfMonth...) |
|
} |
|
|
|
// MonthLastDay sets the unit with months at every last day of the month |
|
func (s *Scheduler) MonthLastDay() *Scheduler { |
|
return s.Months(-1) |
|
} |
|
|
|
// Months sets the unit with months |
|
// Note: Only days 1 through 28 are allowed for monthly schedules |
|
// Note: Multiple add same days of month cannot be allowed |
|
// Note: -1 is a special value and can only occur as single argument |
|
func (s *Scheduler) Months(daysOfTheMonth ...int) *Scheduler { |
|
job := s.getCurrentJob() |
|
|
|
if len(daysOfTheMonth) == 0 { |
|
job.error = wrapOrError(job.error, ErrInvalidDayOfMonthEntry) |
|
} else if len(daysOfTheMonth) == 1 { |
|
dayOfMonth := daysOfTheMonth[0] |
|
if dayOfMonth != -1 && (dayOfMonth < 1 || dayOfMonth > 28) { |
|
job.error = wrapOrError(job.error, ErrInvalidDayOfMonthEntry) |
|
} |
|
} else { |
|
|
|
repeatMap := make(map[int]int) |
|
for _, dayOfMonth := range daysOfTheMonth { |
|
|
|
if dayOfMonth < 1 || dayOfMonth > 28 { |
|
job.error = wrapOrError(job.error, ErrInvalidDayOfMonthEntry) |
|
break |
|
} |
|
|
|
for _, dayOfMonthInJob := range job.daysOfTheMonth { |
|
if dayOfMonthInJob == dayOfMonth { |
|
job.error = wrapOrError(job.error, ErrInvalidDaysOfMonthDuplicateValue) |
|
break |
|
} |
|
} |
|
|
|
if _, ok := repeatMap[dayOfMonth]; ok { |
|
job.error = wrapOrError(job.error, ErrInvalidDaysOfMonthDuplicateValue) |
|
break |
|
} else { |
|
repeatMap[dayOfMonth]++ |
|
} |
|
} |
|
} |
|
if job.daysOfTheMonth == nil { |
|
job.daysOfTheMonth = make([]int, 0) |
|
} |
|
job.daysOfTheMonth = append(job.daysOfTheMonth, daysOfTheMonth...) |
|
job.startsImmediately = false |
|
s.setUnit(months) |
|
return s |
|
} |
|
|
|
// NOTE: If the dayOfTheMonth for the above two functions is |
|
// more than the number of days in that month, the extra day(s) |
|
// spill over to the next month. Similarly, if it's less than 0, |
|
// it will go back to the month before |
|
|
|
// Weekday sets the scheduledWeekdays with a specifics weekdays |
|
func (s *Scheduler) Weekday(weekDay time.Weekday) *Scheduler { |
|
job := s.getCurrentJob() |
|
|
|
if in := in(job.scheduledWeekdays, weekDay); !in { |
|
job.scheduledWeekdays = append(job.scheduledWeekdays, weekDay) |
|
} |
|
|
|
job.startsImmediately = false |
|
s.setUnit(weeks) |
|
return s |
|
} |
|
|
|
func (s *Scheduler) Midday() *Scheduler { |
|
return s.At("12:00") |
|
} |
|
|
|
// Monday sets the start day as Monday |
|
func (s *Scheduler) Monday() *Scheduler { |
|
return s.Weekday(time.Monday) |
|
} |
|
|
|
// Tuesday sets the start day as Tuesday |
|
func (s *Scheduler) Tuesday() *Scheduler { |
|
return s.Weekday(time.Tuesday) |
|
} |
|
|
|
// Wednesday sets the start day as Wednesday |
|
func (s *Scheduler) Wednesday() *Scheduler { |
|
return s.Weekday(time.Wednesday) |
|
} |
|
|
|
// Thursday sets the start day as Thursday |
|
func (s *Scheduler) Thursday() *Scheduler { |
|
return s.Weekday(time.Thursday) |
|
} |
|
|
|
// Friday sets the start day as Friday |
|
func (s *Scheduler) Friday() *Scheduler { |
|
return s.Weekday(time.Friday) |
|
} |
|
|
|
// Saturday sets the start day as Saturday |
|
func (s *Scheduler) Saturday() *Scheduler { |
|
return s.Weekday(time.Saturday) |
|
} |
|
|
|
// Sunday sets the start day as Sunday |
|
func (s *Scheduler) Sunday() *Scheduler { |
|
return s.Weekday(time.Sunday) |
|
} |
|
|
|
func (s *Scheduler) getCurrentJob() *Job { |
|
if len(s.Jobs()) == 0 { |
|
s.setJobs([]*Job{{}}) |
|
s.jobCreated = true |
|
} |
|
return s.Jobs()[len(s.Jobs())-1] |
|
} |
|
|
|
func (s *Scheduler) now() time.Time { |
|
return s.time.Now(s.Location()) |
|
} |
|
|
|
// TagsUnique forces job tags to be unique across the scheduler |
|
// when adding tags with (s *Scheduler) Tag(). |
|
// This does not enforce uniqueness on tags added via |
|
// (j *Job) Tag() |
|
func (s *Scheduler) TagsUnique() { |
|
s.tagsUnique = true |
|
} |
|
|
|
// Job puts the provided job in focus for the purpose |
|
// of making changes to the job with the scheduler chain |
|
// and finalized by calling Update() |
|
func (s *Scheduler) Job(j *Job) *Scheduler { |
|
jobs := s.Jobs() |
|
for index, job := range jobs { |
|
if job == j { |
|
// the current job is always last, so put this job there |
|
s.Swap(len(jobs)-1, index) |
|
} |
|
} |
|
s.updateJob = true |
|
return s |
|
} |
|
|
|
// Update stops the job (if running) and starts it with any updates |
|
// that were made to the job in the scheduler chain. Job() must be |
|
// called first to put the given job in focus. |
|
func (s *Scheduler) Update() (*Job, error) { |
|
job := s.getCurrentJob() |
|
|
|
if !s.updateJob { |
|
return job, wrapOrError(job.error, ErrUpdateCalledWithoutJob) |
|
} |
|
s.updateJob = false |
|
job.stop() |
|
job.ctx, job.cancel = context.WithCancel(context.Background()) |
|
return s.Do(job.function, job.parameters...) |
|
} |
|
|
|
func (s *Scheduler) Cron(cronExpression string) *Scheduler { |
|
return s.cron(cronExpression, false) |
|
} |
|
|
|
func (s *Scheduler) CronWithSeconds(cronExpression string) *Scheduler { |
|
return s.cron(cronExpression, true) |
|
} |
|
|
|
func (s *Scheduler) cron(cronExpression string, withSeconds bool) *Scheduler { |
|
job := s.newJob(0) |
|
if s.updateJob || s.jobCreated { |
|
job = s.getCurrentJob() |
|
} |
|
|
|
withLocation := fmt.Sprintf("CRON_TZ=%s %s", s.location.String(), cronExpression) |
|
|
|
var ( |
|
cronSchedule cron.Schedule |
|
err error |
|
) |
|
|
|
if withSeconds { |
|
p := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor) |
|
cronSchedule, err = p.Parse(withLocation) |
|
} else { |
|
cronSchedule, err = cron.ParseStandard(withLocation) |
|
} |
|
|
|
if err != nil { |
|
job.error = wrapOrError(err, ErrCronParseFailure) |
|
} |
|
|
|
job.cronSchedule = cronSchedule |
|
job.setUnit(crontab) |
|
job.startsImmediately = false |
|
|
|
if s.updateJob || s.jobCreated { |
|
s.setJobs(append(s.Jobs()[:len(s.Jobs())-1], job)) |
|
s.jobCreated = false |
|
} else { |
|
s.setJobs(append(s.Jobs(), job)) |
|
} |
|
return s |
|
} |
|
|
|
func (s *Scheduler) newJob(interval int) *Job { |
|
return newJob(interval, !s.waitForInterval, s.singletonMode) |
|
} |
|
|
|
// WaitForScheduleAll defaults the scheduler to create all |
|
// new jobs with the WaitForSchedule option as true. |
|
// The jobs will not start immediately but rather will |
|
// wait until their first scheduled interval. |
|
func (s *Scheduler) WaitForScheduleAll() { |
|
s.waitForInterval = true |
|
} |
|
|
|
// WaitForSchedule sets the job to not start immediately |
|
// but rather wait until the first scheduled interval. |
|
func (s *Scheduler) WaitForSchedule() *Scheduler { |
|
job := s.getCurrentJob() |
|
job.startsImmediately = false |
|
return s |
|
} |
|
|
|
// StartImmediately sets the job to run immediately upon |
|
// starting the scheduler or adding the job to a running |
|
// scheduler. This overrides the jobs start status of any |
|
// previously called methods in the chain. |
|
// |
|
// Note: This is the default behavior of the scheduler |
|
// for most jobs, but is useful for overriding the default |
|
// behavior of Cron scheduled jobs which default to |
|
// WaitForSchedule. |
|
func (s *Scheduler) StartImmediately() *Scheduler { |
|
job := s.getCurrentJob() |
|
job.startsImmediately = true |
|
return s |
|
}
|
|
|