From f8144d2de1a3edc191cca7c302ce9461dc7d1ec5 Mon Sep 17 00:00:00 2001 From: John Roesler Date: Tue, 14 Jun 2022 05:46:53 -0500 Subject: [PATCH] add EveryRandom for random interval (#339) * add EveryRandom for random interval * linting --- example_test.go | 12 ++++++++ job.go | 35 +++++++++++++++++++++- scheduler.go | 76 +++++++++++++++++++++++++---------------------- scheduler_test.go | 25 ++++++++++++++++ 4 files changed, 112 insertions(+), 36 deletions(-) diff --git a/example_test.go b/example_test.go index 113509b1..1441ae1e 100644 --- a/example_test.go +++ b/example_test.go @@ -291,6 +291,18 @@ func ExampleScheduler_Every() { s.StartAsync() } +func ExampleScheduler_EveryRandom() { + s := gocron.NewScheduler(time.UTC) + + // every 1 - 5 seconds randomly + _, _ = s.EveryRandom(1, 5).Seconds().Do(task) + + // every 5 - 10 hours randomly + _, _ = s.EveryRandom(5, 10).Hours().Do(task) + + s.StartAsync() +} + func ExampleScheduler_Friday() { s := gocron.NewScheduler(time.UTC) j, _ := s.Every(1).Day().Friday().Do(task) diff --git a/job.go b/job.go index cd9f21f2..1b3d7a5f 100644 --- a/job.go +++ b/job.go @@ -3,6 +3,7 @@ package gocron import ( "context" "fmt" + "math/rand" "sort" "sync" "sync/atomic" @@ -16,7 +17,8 @@ import ( type Job struct { mu *jobMutex jobFunction - interval int // pause interval * unit between runs + 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 @@ -34,6 +36,12 @@ type Job struct { runWithDetails bool // when true the job is passed as the last arg of the jobFunc } +type random struct { + rand *rand.Rand + randomizeInterval bool // whether the interval is random + randomIntervalRange [2]int // random interval range +} + type jobFunction struct { eventListeners // additional functions to allow run 'em during job performing function interface{} // task's function @@ -126,6 +134,31 @@ func newJob(interval int, startImmediately bool, singletonMode bool) *Job { return job } +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 { return j.lastRun.IsZero() } diff --git a/scheduler.go b/scheduler.go index a76ac522..bf190f92 100644 --- a/scheduler.go +++ b/scheduler.go @@ -249,7 +249,7 @@ func (s *Scheduler) calculateMonths(job *Job, lastRun time.Time) nextRun { return nextRunResult } - next := lastRunRoundedMidnight.Add(job.getFirstAtTime()).AddDate(0, job.interval, 0) + next := lastRunRoundedMidnight.Add(job.getFirstAtTime()).AddDate(0, job.getInterval(), 0) return nextRun{duration: until(lastRun, next), dateTime: next} } @@ -257,7 +257,7 @@ func calculateNextRunForLastDayOfMonth(s *Scheduler, job *Job, lastRun time.Time // 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 + addMonth := job.getInterval() atTime := job.getAtTime(lastRun) if testDate := lastRun.AddDate(0, 0, 1); testDate.Month() != lastRun.Month() && !s.roundToMidnight(lastRun).Add(atTime).After(lastRun) { @@ -280,14 +280,14 @@ func calculateNextRunForMonth(s *Scheduler, job *Job, lastRun time.Time, dayOfMo 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.AddDate(0, job.getInterval(), -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) + if job.getInterval() == 1 && !jobDay.Equal(lastRun) { // every month counts current month + next = next.AddDate(0, job.getInterval()-1, 0) } else { // should run next month interval - next = next.AddDate(0, job.interval, 0) + next = next.AddDate(0, job.getInterval(), 0) natTime = job.getFirstAtTime() } next = next.Add(difference) @@ -310,21 +310,21 @@ func (s *Scheduler) calculateWeekday(job *Job, lastRun time.Time) nextRun { } func (s *Scheduler) calculateWeeks(job *Job, lastRun time.Time) nextRun { - totalDaysDifference := int(job.interval) * 7 + totalDaysDifference := int(job.getInterval()) * 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 + if job.getInterval() > 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 job.getInterval() > 1 && job.RunCount() >= len(job.Weekdays()) { if daysToWeekday > 0 { - return int(job.interval)*7 - (allWeekDays - daysToWeekday) + return int(job.getInterval())*7 - (allWeekDays - daysToWeekday) } - return int(job.interval) * 7 + return int(job.getInterval()) * 7 } if daysToWeekday == 0 { // today, at future time or already passed @@ -339,7 +339,7 @@ func (s *Scheduler) calculateTotalDaysDifference(lastRun time.Time, daysToWeekda func (s *Scheduler) calculateDays(job *Job, lastRun time.Time) nextRun { - if job.interval == 1 { + if job.getInterval() == 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 @@ -353,7 +353,7 @@ func (s *Scheduler) calculateDays(job *Job, lastRun time.Time) nextRun { } } - nextRunAtTime := s.roundToMidnight(lastRun).Add(job.getFirstAtTime()).AddDate(0, 0, job.interval).In(s.Location()) + nextRunAtTime := s.roundToMidnight(lastRun).Add(job.getFirstAtTime()).AddDate(0, 0, job.getInterval()).In(s.Location()) return nextRun{duration: until(lastRun, nextRunAtTime), dateTime: nextRunAtTime} } @@ -386,7 +386,7 @@ func (s *Scheduler) calculateDuration(job *Job) time.Duration { } } - interval := job.interval + interval := job.getInterval() switch job.getUnit() { case milliseconds: return time.Duration(interval) * time.Millisecond @@ -457,6 +457,30 @@ func (s *Scheduler) NextRun() (*Job, time.Time) { return s.Jobs()[0], s.Jobs()[0].NextRun() } +// EveryRandom schedules a new period Job that runs at random intervals +// between the provided lower (inclusive) and upper (inclusive) bounds. +// The default unit is Seconds(). Call a different unit in the chain +// if you would like to change that. For example, Minutes(), Hours(), etc. +func (s *Scheduler) EveryRandom(lower, upper int) *Scheduler { + job := s.newJob(0) + if s.updateJob || s.jobCreated { + job = s.getCurrentJob() + } + + job.setRandomInterval(lower, upper) + + 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 +} + // Every schedules a new periodic Job with an interval. // Interval can be an int, time.Duration or a string that // parses with time.ParseDuration(). @@ -469,28 +493,15 @@ func (s *Scheduler) Every(interval interface{}) *Scheduler { switch interval := interval.(type) { case int: - if !(s.updateJob || s.jobCreated) { - job = s.newJob(interval) - } else { - job.interval = interval - } + job.interval = 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.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) @@ -498,11 +509,6 @@ func (s *Scheduler) Every(interval interface{}) *Scheduler { 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) } @@ -815,7 +821,7 @@ func (s *Scheduler) doCommon(jobFun interface{}, params ...interface{}) (*Job, e job.error = wrapOrError(job.error, ErrWeekdayNotSupported) } - if job.unit != crontab && job.interval == 0 { + if job.unit != crontab && job.getInterval() == 0 { if job.unit != duration { job.error = wrapOrError(job.error, ErrInvalidInterval) } diff --git a/scheduler_test.go b/scheduler_test.go index fed11c40..643d1261 100644 --- a/scheduler_test.go +++ b/scheduler_test.go @@ -78,6 +78,31 @@ func TestScheduler_Every_InvalidInterval(t *testing.T) { } +func TestScheduler_EveryRandom(t *testing.T) { + s := NewScheduler(time.UTC) + semaphore := make(chan bool) + + j, err := s.EveryRandom(1, 2).Seconds().Do(func() { + semaphore <- true + }) + require.NoError(t, err) + assert.True(t, j.randomizeInterval) + + s.StartAsync() + + var counter int + + now := time.Now() + for time.Now().Before(now.Add(2 * time.Second)) { + if <-semaphore { + counter++ + } + } + s.Stop() + assert.LessOrEqual(t, counter, 3) + assert.GreaterOrEqual(t, counter, 1) +} + func TestScheduler_Every(t *testing.T) { t.Run("time.Duration", func(t *testing.T) { s := NewScheduler(time.UTC)