diff --git a/gocron.go b/gocron.go index 65e140a..6571618 100644 --- a/gocron.go +++ b/gocron.go @@ -1,4 +1,4 @@ -// goCron : A Golang Job Scheduling Package. +// Package gocron : A Golang Job Scheduling Package. // // An in-process scheduler for periodic jobs that uses the builder pattern // for configuration. Schedule lets you run Golang functions periodically @@ -20,6 +20,7 @@ package gocron import ( "errors" + "fmt" "reflect" "runtime" "sort" @@ -31,50 +32,34 @@ import ( // Time location, default set by the time.Local (*time.Location) var loc = time.Local -// Change the time location +// ChangeLoc change default the time location func ChangeLoc(newLocation *time.Location) { loc = newLocation } -// Max number of jobs, hack it if you need. +// MAXJOBNUM max number of jobs, hack it if you need. const MAXJOBNUM = 10000 +// Job struct keeping information about job type Job struct { - - // pause interval * unit bettween runs - interval uint64 - - // the job jobFunc to run, func[jobFunc] - jobFunc string - // time units, ,e.g. 'minutes', 'hours'... - unit string - // optional time at which this job runs - atTime string - - // datetime of last run - lastRun time.Time - // datetime of next run - nextRun time.Time - // cache the period between last an next run - period time.Duration - - // Specific day of the week to start on - startDay time.Weekday - - // Map for the function task store - funcs map[string]interface{} - - // Map for function and params of function - fparams map[string]([]interface{}) -} - -// Create a new job with the time interval. -func NewJob(intervel uint64) *Job { + interval uint64 // pause interval * unit bettween runs + jobFunc string // the job jobFunc to run, func[jobFunc] + unit string // time units, ,e.g. 'minutes', 'hours'... + atTime time.Duration // optional time at which this job runs + lastRun time.Time // datetime of last run + nextRun time.Time // datetime of next run + startDay time.Weekday // Specific day of the week to start on + funcs map[string]interface{} // Map for the function task store + fparams map[string]([]interface{}) // Map for function and params of function +} + +// NewJob creates a new job with the time interval. +func NewJob(interval uint64) *Job { return &Job{ - intervel, - "", "", "", + interval, + "", "", 0, + time.Unix(0, 0), time.Unix(0, 0), - time.Unix(0, 0), 0, time.Sunday, make(map[string]interface{}), make(map[string]([]interface{})), @@ -91,7 +76,7 @@ func (j *Job) run() (result []reflect.Value, err error) { f := reflect.ValueOf(j.funcs[j.jobFunc]) params := j.fparams[j.jobFunc] if len(params) != f.Type().NumIn() { - err = errors.New("The number of param is not adapted.") + err = errors.New("the number of param is not adapted") return } in := make([]reflect.Value, len(params)) @@ -109,19 +94,16 @@ func getFunctionName(fn interface{}) string { return runtime.FuncForPC(reflect.ValueOf((fn)).Pointer()).Name() } -// Specifies the jobFunc that should be called every time the job runs -// +// Do specifies the jobFunc that should be called every time the job runs func (j *Job) Do(jobFun interface{}, params ...interface{}) { typ := reflect.TypeOf(jobFun) if typ.Kind() != reflect.Func { panic("only function can be schedule into the job queue.") } - fname := getFunctionName(jobFun) j.funcs[fname] = jobFun j.fparams[fname] = params j.jobFunc = fname - //schedule the next run j.scheduleNextRun() } @@ -133,12 +115,10 @@ func formatTime(t string) (hour, min int, err error) { return } - hour, err = strconv.Atoi(ts[0]) - if err != nil { + if hour, err = strconv.Atoi(ts[0]); err != nil { return } - min, err = strconv.Atoi(ts[1]) - if err != nil { + if min, err = strconv.Atoi(ts[1]); err != nil { return } @@ -149,6 +129,7 @@ func formatTime(t string) (hour, min int, err error) { return hour, min, nil } +// At schedules job at specific time of day // s.Every(1).Day().At("10:30").Do(task) // s.Every(1).Monday().At("10:30").Do(task) func (j *Job) At(t string) *Job { @@ -156,66 +137,59 @@ func (j *Job) At(t string) *Job { if err != nil { panic(err) } + // save atTime start as duration from midnight + j.atTime = time.Duration(hour)*time.Hour + time.Duration(min)*time.Minute + return j +} - // time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) - mock := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), int(hour), int(min), 0, 0, loc) - - if j.unit == "days" { - if time.Now().After(mock) { - j.lastRun = mock - } else { - j.lastRun = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day()-1, hour, min, 0, 0, loc) - } - } else if j.unit == "weeks" { - if time.Now().After(mock) { - i := mock.Weekday() - j.startDay - if i < 0 { - i = 7 + i - } - j.lastRun = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day()-int(i), hour, min, 0, 0, loc) - } else { - j.lastRun = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day()-7, hour, min, 0, 0, loc) - } +func (j *Job) periodDuration() time.Duration { + interval := time.Duration(j.interval) + switch j.unit { + case "seconds": + return time.Duration(interval * time.Second) + case "minutes": + return time.Duration(interval * time.Minute) + case "hours": + return time.Duration(interval * time.Hour) + case "days": + return time.Duration(interval * time.Hour * 24) + case "weeks": + return time.Duration(interval * time.Hour * 24 * 7) } - return j + panic("unspecified job period") // unspecified period +} + +// roundToMidnight truncate time to midnight +func (j *Job) roundToMidnight(t time.Time) time.Time { + return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc) } -//Compute the instant when this job should run next +// scheduleNextRun Compute the instant when this job should run next func (j *Job) scheduleNextRun() { + now := time.Now() if j.lastRun == time.Unix(0, 0) { - if j.unit == "weeks" { - i := time.Now().Weekday() - j.startDay - if i < 0 { - i = 7 + i - } - j.lastRun = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day()-int(i), 0, 0, 0, 0, loc) + j.lastRun = now + } - } else { - j.lastRun = time.Now() + switch j.unit { + case "days": + j.nextRun = j.roundToMidnight(j.lastRun) + j.nextRun = j.nextRun.Add(j.atTime) + case "weeks": + j.nextRun = j.roundToMidnight(j.lastRun) + dayDiff := int(j.startDay) + dayDiff -= int(j.nextRun.Weekday()) + if dayDiff != 0 { + j.nextRun = j.nextRun.Add(time.Duration(dayDiff) * 24 * time.Hour) } + j.nextRun = j.nextRun.Add(j.atTime) + default: + j.nextRun = j.lastRun } - if j.period != 0 { - // translate all the units to the Seconds - j.nextRun = j.lastRun.Add(j.period * time.Second) - } else { - switch j.unit { - case "minutes": - j.period = time.Duration(j.interval * 60) - break - case "hours": - j.period = time.Duration(j.interval * 60 * 60) - break - case "days": - j.period = time.Duration(j.interval * 60 * 60 * 24) - break - case "weeks": - j.period = time.Duration(j.interval * 60 * 60 * 24 * 7) - break - case "seconds": - j.period = time.Duration(j.interval) - } - j.nextRun = j.lastRun.Add(j.period * time.Second) + // advance to next possible schedule + for j.nextRun.Before(now) || j.nextRun.Before(j.lastRun) { + j.nextRun = j.nextRun.Add(j.periodDuration()) } } @@ -226,153 +200,116 @@ func (j *Job) NextScheduledTime() time.Time { // the follow functions set the job's unit with seconds,minutes,hours... -// Set the unit with second -func (j *Job) Second() (job *Job) { - if j.interval != 1 { - panic("") +func (j *Job) mustInterval(i uint64) { + if j.interval != i { + panic(fmt.Sprintf("interval maust be %d", i)) } - job = j.Seconds() - return } -// Set the unit with seconds -func (j *Job) Seconds() (job *Job) { - j.unit = "seconds" +// setUnit sets unit type +func (j *Job) setUnit(unit string) *Job { + j.unit = unit return j } -// Set the unit with minute, which interval is 1 -func (j *Job) Minute() (job *Job) { - if j.interval != 1 { - panic("") - } - job = j.Minutes() - return +// Seconds set the unit with seconds +func (j *Job) Seconds() *Job { + return j.setUnit("seconds") } -//set the unit with minute -func (j *Job) Minutes() (job *Job) { - j.unit = "minutes" - return j +// Minutes set the unit with minute +func (j *Job) Minutes() *Job { + return j.setUnit("minutes") } -//set the unit with hour, which interval is 1 -func (j *Job) Hour() (job *Job) { - if j.interval != 1 { - panic("") - } - job = j.Hours() - return +// Hours set the unit with hours +func (j *Job) Hours() *Job { + return j.setUnit("hours") } -// Set the unit with hours -func (j *Job) Hours() (job *Job) { - j.unit = "hours" - return j +// Days set the job's unit with days +func (j *Job) Days() *Job { + return j.setUnit("days") } -// Set the job's unit with day, which interval is 1 -func (j *Job) Day() (job *Job) { - if j.interval != 1 { - panic("") - } - job = j.Days() - return +//Weeks sets the units as weeks +func (j *Job) Weeks() *Job { + return j.setUnit("weeks") } -// Set the job's unit with days -func (j *Job) Days() *Job { - j.unit = "days" - return j +// Second set the unit with second +func (j *Job) Second() *Job { + j.mustInterval(1) + return j.Seconds() } -// s.Every(1).Monday().Do(task) -// Set the start day with Monday -func (j *Job) Monday() (job *Job) { - if j.interval != 1 { - panic("") - } - j.startDay = 1 - job = j.Weeks() - return +// Minute set the unit with minute, which interval is 1 +func (j *Job) Minute() *Job { + j.mustInterval(1) + return j.Minutes() } -// Set the start day with Tuesday -func (j *Job) Tuesday() (job *Job) { - if j.interval != 1 { - panic("") - } - j.startDay = 2 - job = j.Weeks() - return +// Hour set the unit with hour, which interval is 1 +func (j *Job) Hour() *Job { + j.mustInterval(1) + return j.Hours() } -// Set the start day woth Wednesday -func (j *Job) Wednesday() (job *Job) { - if j.interval != 1 { - panic("") - } - j.startDay = 3 - job = j.Weeks() - return +// Day set the job's unit with day, which interval is 1 +func (j *Job) Day() *Job { + j.mustInterval(1) + return j.Days() } -// Set the start day with thursday -func (j *Job) Thursday() (job *Job) { - if j.interval != 1 { - panic("") - } - j.startDay = 4 - job = j.Weeks() - return +// Weekday start job on specific Weekday +func (j *Job) Weekday(startDay time.Weekday) *Job { + j.mustInterval(1) + j.startDay = startDay + return j.Weeks() } -// Set the start day with friday -func (j *Job) Friday() (job *Job) { - if j.interval != 1 { - panic("") - } - j.startDay = 5 - job = j.Weeks() - return +// Monday set the start day with Monday +// - s.Every(1).Monday().Do(task) +func (j *Job) Monday() (job *Job) { + return j.Weekday(time.Monday) } -// Set the start day with saturday -func (j *Job) Saturday() (job *Job) { - if j.interval != 1 { - panic("") - } - j.startDay = 6 - job = j.Weeks() - return +// Tuesday sets the job start day Tuesday +func (j *Job) Tuesday() *Job { + return j.Weekday(time.Tuesday) } -// Set the start day with sunday -func (j *Job) Sunday() (job *Job) { - if j.interval != 1 { - panic("") - } - j.startDay = 0 - job = j.Weeks() - return +// Wednesday sets the job start day Wednesday +func (j *Job) Wednesday() *Job { + return j.Weekday(time.Wednesday) } -//Set the units as weeks -func (j *Job) Weeks() *Job { - j.unit = "weeks" - return j +// Thursday sets the job start day Thursday +func (j *Job) Thursday() *Job { + return j.Weekday(time.Thursday) } -// Class Scheduler, the only data member is the list of jobs. -type Scheduler struct { - // Array store jobs - jobs [MAXJOBNUM]*Job +// Friday sets the job start day Friday +func (j *Job) Friday() *Job { + return j.Weekday(time.Friday) +} + +// Saturday sets the job start day Saturday +func (j *Job) Saturday() *Job { + return j.Weekday(time.Saturday) +} - // Size of jobs which jobs holding. - size int +// Sunday sets the job start day Sunday +func (j *Job) Sunday() *Job { + return j.Weekday(time.Sunday) } -// Scheduler implements the sort.Interface{} for sorting jobs, by the time nextRun +// Scheduler struct, the only data member is the list of jobs. +// - implements the sort.Interface{} for sorting jobs, by the time nextRun +type Scheduler struct { + jobs [MAXJOBNUM]*Job // Array store jobs + size int // Size of jobs which jobs holding. +} func (s *Scheduler) Len() int { return s.size @@ -386,7 +323,7 @@ func (s *Scheduler) Less(i, j int) bool { return s.jobs[j].nextRun.After(s.jobs[i].nextRun) } -// Create a new scheduler +// NewScheduler creates a new scheduler func NewScheduler() *Scheduler { return &Scheduler{[MAXJOBNUM]*Job{}, 0} } @@ -409,7 +346,7 @@ func (s *Scheduler) getRunnableJobs() (running_jobs [MAXJOBNUM]*Job, n int) { return runnableJobs, n } -// Datetime when the next job should run. +// NextRun datetime when the next job should run. func (s *Scheduler) NextRun() (*Job, time.Time) { if s.size <= 0 { return nil, time.Now() @@ -418,7 +355,7 @@ func (s *Scheduler) NextRun() (*Job, time.Time) { return s.jobs[0], s.jobs[0].nextRun } -// Schedule a new periodic job +// Every schedule a new periodic job with interval func (s *Scheduler) Every(interval uint64) *Job { job := NewJob(interval) s.jobs[s.size] = job @@ -426,7 +363,7 @@ func (s *Scheduler) Every(interval uint64) *Job { return job } -// Run all the jobs that are scheduled to run. +// RunPending runs all the jobs that are scheduled to run. func (s *Scheduler) RunPending() { runnableJobs, n := s.getRunnableJobs() @@ -437,18 +374,18 @@ func (s *Scheduler) RunPending() { } } -// Run all jobs regardless if they are scheduled to run or not +// RunAll run all jobs regardless if they are scheduled to run or not func (s *Scheduler) RunAll() { - for i := 0; i < s.size; i++ { - s.jobs[i].run() - } + s.RunAllwithDelay(0) } -// Run all jobs with delay seconds +// RunAllwithDelay runs all jobs with delay seconds func (s *Scheduler) RunAllwithDelay(d int) { for i := 0; i < s.size; i++ { s.jobs[i].run() - time.Sleep(time.Duration(d)) + if 0 != d { + time.Sleep(time.Duration(d)) + } } } @@ -468,7 +405,7 @@ func (s *Scheduler) Remove(j interface{}) { s.size = s.size - 1 } -// Delete all scheduled jobs +// Clear delete all scheduled jobs func (s *Scheduler) Clear() { for i := 0; i < s.size; i++ { s.jobs[i] = nil @@ -488,6 +425,7 @@ func (s *Scheduler) Start() chan bool { case <-ticker.C: s.RunPending() case <-stopped: + ticker.Stop() return } } @@ -500,14 +438,13 @@ func (s *Scheduler) Start() chan bool { // create a Schduler instance var defaultScheduler = NewScheduler() -var jobs = defaultScheduler.jobs -// Schedule a new periodic job +// Every schedules a new periodic job running in specific interval func Every(interval uint64) *Job { return defaultScheduler.Every(interval) } -// Run all jobs that are scheduled to run +// RunPending run all jobs that are scheduled to run // // Please note that it is *intended behavior that run_pending() // does not run missed jobs*. For example, if you've registered a job @@ -518,12 +455,12 @@ func RunPending() { defaultScheduler.RunPending() } -// Run all jobs regardless if they are scheduled to run or not. +// RunAll run all jobs regardless if they are scheduled to run or not. func RunAll() { defaultScheduler.RunAll() } -// Run all the jobs with a delay in seconds +// RunAllwithDelay run all the jobs with a delay in seconds // // A delay of `delay` seconds is added between each job. This can help // to distribute the system load generated by the jobs more evenly over @@ -532,17 +469,17 @@ func RunAllwithDelay(d int) { defaultScheduler.RunAllwithDelay(d) } -// Run all jobs that are scheduled to run +// Start run all jobs that are scheduled to run func Start() chan bool { return defaultScheduler.Start() } -// Clear +// Clear all scheduled jobs func Clear() { defaultScheduler.Clear() } -// Remove +// Remove specific job func Remove(j interface{}) { defaultScheduler.Remove(j) } diff --git a/gocron_test.go b/gocron_test.go index 87a7497..fd405ce 100644 --- a/gocron_test.go +++ b/gocron_test.go @@ -6,8 +6,6 @@ import ( "time" ) -var err = 1 - func task() { fmt.Println("I am a running job.") } @@ -16,11 +14,19 @@ func taskWithParams(a int, b string) { fmt.Println(a, b) } +func assertEqualTime(t *testing.T, actual, expected time.Time) { + if actual != expected { + t.Errorf("actual different than expected want: %v -> got: %v", expected, actual) + } +} + func TestSecond(*testing.T) { defaultScheduler.Every(1).Second().Do(task) defaultScheduler.Every(1).Second().Do(taskWithParams, 1, "hello") - defaultScheduler.Start() - time.Sleep(10 * time.Second) + stop := defaultScheduler.Start() + time.Sleep(5 * time.Second) + close(stop) + defaultScheduler.Clear() } func Test_formatTime(t *testing.T) { @@ -90,3 +96,193 @@ func Test_formatTime(t *testing.T) { }) } } + +func TestTaskAt(t *testing.T) { + // Create new scheduler to have clean test env + s := NewScheduler() + + // Schedule to run in next minute + now := time.Now() + // Expected start time + startTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute()+1, 0, 0, loc) + // Expected next start time day after + startNext := startTime.AddDate(0, 0, 1) + + // Schedule every day At + startAt := fmt.Sprintf("%02d:%02d", now.Hour(), now.Minute()+1) + dayJob := s.Every(1).Day().At(startAt) + + dayJobDone := make(chan bool, 1) + // Job running 5 sec + dayJob.Do(func() { + t.Log(time.Now(), "job start") + time.Sleep(2 * time.Second) + dayJobDone <- true + t.Log(time.Now(), "job done") + }) + + // Check first run + nextRun := dayJob.NextScheduledTime() + assertEqualTime(t, nextRun, startTime) + + sStop := s.Start() // Start scheduler + <-dayJobDone // Wait job done + close(sStop) // Stop scheduler + time.Sleep(time.Second) // wait for scheduler to reschedule job + + // Check next run + nextRun = dayJob.NextScheduledTime() + assertEqualTime(t, nextRun, startNext) +} + +func TestDaily(t *testing.T) { + now := time.Now() + + // Create new scheduler to have clean test env + s := NewScheduler() + + // schedule next run 1 day + dayJob := s.Every(1).Day() + dayJob.scheduleNextRun() + exp := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, loc) + assertEqualTime(t, dayJob.nextRun, exp) + + // schedule next run 2 days + dayJob = s.Every(2).Days() + dayJob.scheduleNextRun() + exp = time.Date(now.Year(), now.Month(), now.Day()+2, 0, 0, 0, 0, loc) + assertEqualTime(t, dayJob.nextRun, exp) + + // Job running longer than next schedule 1day 2 hours + dayJob = s.Every(1).Day() + dayJob.lastRun = time.Date(now.Year(), now.Month(), now.Day(), now.Hour()+2, 0, 0, 0, loc) + dayJob.scheduleNextRun() + exp = time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, loc) + assertEqualTime(t, dayJob.nextRun, exp) + + // At() 2 hours before now + hour := now.Hour() - 2 + minute := now.Minute() + startAt := fmt.Sprintf("%02d:%02d", hour, minute) + dayJob = s.Every(1).Day().At(startAt) + dayJob.scheduleNextRun() + exp = time.Date(now.Year(), now.Month(), now.Day()+1, hour, minute, 0, 0, loc) + assertEqualTime(t, dayJob.nextRun, exp) +} + +func TestWeekdayAfterToday(t *testing.T) { + now := time.Now() + + // Create new scheduler to have clean test env + s := NewScheduler() + + // Schedule job at next week day + var weekJob *Job + switch now.Weekday() { + case time.Monday: + weekJob = s.Every(1).Tuesday() + case time.Tuesday: + weekJob = s.Every(1).Wednesday() + case time.Wednesday: + weekJob = s.Every(1).Thursday() + case time.Thursday: + weekJob = s.Every(1).Friday() + case time.Friday: + weekJob = s.Every(1).Saturday() + case time.Saturday: + weekJob = s.Every(1).Sunday() + case time.Sunday: + weekJob = s.Every(1).Monday() + } + + // First run + weekJob.scheduleNextRun() + exp := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, loc) + assertEqualTime(t, weekJob.nextRun, exp) + + // Simulate job run 7 days before + weekJob.lastRun = weekJob.nextRun.AddDate(0, 0, -7) + // Next run + weekJob.scheduleNextRun() + exp = time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, loc) + assertEqualTime(t, weekJob.nextRun, exp) +} + +func TestWeekdayBeforeToday(t *testing.T) { + now := time.Now() + + // Create new scheduler to have clean test env + s := NewScheduler() + + // Schedule job at day before + var weekJob *Job + switch now.Weekday() { + case time.Monday: + weekJob = s.Every(1).Sunday() + case time.Tuesday: + weekJob = s.Every(1).Monday() + case time.Wednesday: + weekJob = s.Every(1).Tuesday() + case time.Thursday: + weekJob = s.Every(1).Wednesday() + case time.Friday: + weekJob = s.Every(1).Thursday() + case time.Saturday: + weekJob = s.Every(1).Friday() + case time.Sunday: + weekJob = s.Every(1).Saturday() + } + + weekJob.scheduleNextRun() + exp := time.Date(now.Year(), now.Month(), now.Day()+6, 0, 0, 0, 0, loc) + assertEqualTime(t, weekJob.nextRun, exp) + + // Simulate job run 7 days before + weekJob.lastRun = weekJob.nextRun.AddDate(0, 0, -7) + // Next run + weekJob.scheduleNextRun() + exp = time.Date(now.Year(), now.Month(), now.Day()+6, 0, 0, 0, 0, loc) + assertEqualTime(t, weekJob.nextRun, exp) +} + +func TestWeekdayAt(t *testing.T) { + now := time.Now() + + hour := now.Hour() + minute := now.Minute() + startAt := fmt.Sprintf("%02d:%02d", hour, minute) + + // Create new scheduler to have clean test env + s := NewScheduler() + + // Schedule job at next week day + var weekJob *Job + switch now.Weekday() { + case time.Monday: + weekJob = s.Every(1).Tuesday().At(startAt) + case time.Tuesday: + weekJob = s.Every(1).Wednesday().At(startAt) + case time.Wednesday: + weekJob = s.Every(1).Thursday().At(startAt) + case time.Thursday: + weekJob = s.Every(1).Friday().At(startAt) + case time.Friday: + weekJob = s.Every(1).Saturday().At(startAt) + case time.Saturday: + weekJob = s.Every(1).Sunday().At(startAt) + case time.Sunday: + weekJob = s.Every(1).Monday().At(startAt) + } + + // First run + weekJob.scheduleNextRun() + exp := time.Date(now.Year(), now.Month(), now.Day()+1, hour, minute, 0, 0, loc) + assertEqualTime(t, weekJob.nextRun, exp) + + // Simulate job run 7 days before + weekJob.lastRun = weekJob.nextRun.AddDate(0, 0, -7) + // Next run + weekJob.scheduleNextRun() + exp = time.Date(now.Year(), now.Month(), now.Day()+1, hour, minute, 0, 0, loc) + assertEqualTime(t, weekJob.nextRun, exp) +}