Skip to content

Commit

Permalink
add EveryRandom for random interval (#339)
Browse files Browse the repository at this point in the history
* add EveryRandom for random interval

* linting
  • Loading branch information
JohnRoesler committed Jun 14, 2022
1 parent 9203b30 commit f8144d2
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 36 deletions.
12 changes: 12 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
35 changes: 34 additions & 1 deletion job.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package gocron
import (
"context"
"fmt"
"math/rand"
"sort"
"sync"
"sync/atomic"
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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()
}
Expand Down
76 changes: 41 additions & 35 deletions scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,15 +249,15 @@ 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}
}

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
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) {
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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().
Expand All @@ -469,40 +493,22 @@ 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)
}
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)
}

Expand Down Expand Up @@ -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)
}
Expand Down
25 changes: 25 additions & 0 deletions scheduler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit f8144d2

Please sign in to comment.