Skip to content

Commit

Permalink
Every() accepts int, time.Duration, string to allow any custom durati…
Browse files Browse the repository at this point in the history
…on between runs (#119)

* add EveryDuration() for any desired duration between runs

* fix up test

* fix linting for test

* .Every() accepts int, time.Duration, string parsed to duration

* fix test so it passes go 1.14
  • Loading branch information
JohnRoesler committed Feb 6, 2021
1 parent 0fc078e commit fc07510
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 34 deletions.
8 changes: 8 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ func ExampleScheduler_StartAsync() {
s.StartAsync()
}

func ExampleScheduler_Every() {
s := gocron.NewScheduler(time.UTC)
_, _ = s.Every(1).Second().Do(task)
_, _ = s.Every(1 * time.Second).Do(task)
_, _ = s.Every("1s").Do(task)
s.StartAsync()
}

func ExampleScheduler_StartAt() {
s := gocron.NewScheduler(time.UTC)
specificTime := time.Date(2019, time.November, 10, 15, 0, 0, 0, time.UTC)
Expand Down
3 changes: 3 additions & 0 deletions gocron.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ var (
ErrJobNotFoundWithTag = errors.New("no jobs found with given tag")
ErrUnsupportedTimeFormat = errors.New("the given time format is not supported")
ErrInvalidInterval = errors.New(".Every() interval must be greater than 0")
ErrInvalidIntervalType = errors.New(".Every() interval must be int, time.Duration, or string")
ErrInvalidSelection = errors.New("an .Every() duration interval cannot be used with units (e.g. .Seconds())")
)

// regex patterns for supported time formats
Expand All @@ -45,6 +47,7 @@ const (
days
weeks
months
duration
)

func callJobFuncWithParams(jobFunc interface{}, params []interface{}) ([]reflect.Value, error) {
Expand Down
9 changes: 4 additions & 5 deletions job.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@ import (
"time"
)

type jobInterval uint64

// Job struct stores the information necessary to run a Job
type Job struct {
sync.RWMutex
interval jobInterval // pause interval * unit between runs
interval int // pause interval * unit between runs
duration time.Duration // time duration between runs
unit timeUnit // time units, ,e.g. 'minutes', 'hours'...
startsImmediately bool // if the Job should run upon scheduler start
jobFunc string // the Job jobFunc to run, func[jobFunc]
Expand All @@ -37,9 +36,9 @@ type runConfig struct {
}

// NewJob creates a new Job with the provided interval
func NewJob(interval uint64) *Job {
func NewJob(interval int) *Job {
return &Job{
interval: jobInterval(interval),
interval: interval,
lastRun: time.Time{},
nextRun: time.Time{},
funcs: make(map[string]interface{}),
Expand Down
54 changes: 40 additions & 14 deletions scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,22 +156,24 @@ func (s *Scheduler) durationToNextRun(lastRun time.Time, job *Job) time.Duration
return job.getStartAtTime().Sub(s.now())
}

var duration time.Duration
var d time.Duration
switch job.unit {
case seconds, minutes, hours:
duration = s.calculateDuration(job)
d = s.calculateDuration(job)
case days:
duration = s.calculateDays(job, lastRun)
d = s.calculateDays(job, lastRun)
case weeks:
if job.scheduledWeekday != nil { // weekday selected, Every().Monday(), for example
duration = s.calculateWeekday(job, lastRun)
d = s.calculateWeekday(job, lastRun)
} else {
duration = s.calculateWeeks(job, lastRun)
d = s.calculateWeeks(job, lastRun)
}
case months:
duration = s.calculateMonths(job, lastRun)
d = s.calculateMonths(job, lastRun)
case duration:
d = job.duration
}
return duration
return d
}

func (s *Scheduler) calculateMonths(job *Job, lastRun time.Time) time.Duration {
Expand Down Expand Up @@ -294,12 +296,33 @@ func (s *Scheduler) NextRun() (*Job, time.Time) {
}

// Every schedules a new periodic Job with interval
func (s *Scheduler) Every(interval uint64) *Scheduler {
job := NewJob(interval)
if interval == 0 {
job.err = ErrInvalidInterval
func (s *Scheduler) Every(interval interface{}) *Scheduler {
switch interval := interval.(type) {
case int:
job := NewJob(interval)
if interval <= 0 {
job.err = ErrInvalidInterval
}
s.setJobs(append(s.Jobs(), job))
case time.Duration:
job := NewJob(0)
job.duration = interval
job.unit = duration
s.setJobs(append(s.Jobs(), job))
case string:
job := NewJob(0)
d, err := time.ParseDuration(interval)
if err != nil {
job.err = err
}
job.duration = d
job.unit = duration
s.setJobs(append(s.Jobs(), job))
default:
job := NewJob(0)
job.err = ErrInvalidIntervalType
s.setJobs(append(s.Jobs(), job))
}
s.setJobs(append(s.Jobs(), job))
return s
}

Expand Down Expand Up @@ -484,8 +507,11 @@ func (s *Scheduler) StartAt(t time.Time) *Scheduler {

// setUnit sets the unit type
func (s *Scheduler) setUnit(unit timeUnit) {
currentJob := s.getCurrentJob()
currentJob.unit = unit
job := s.getCurrentJob()
if job.unit == duration {
job.err = ErrInvalidSelection
}
job.unit = unit
}

// Second sets the unit with seconds
Expand Down
108 changes: 98 additions & 10 deletions scheduler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package gocron

import (
"fmt"
"strings"
"sync"
"testing"
"time"
Expand All @@ -11,6 +12,8 @@ import (
"github.com/stretchr/testify/assert"
)

var _ timeWrapper = (*fakeTime)(nil)

type fakeTime struct {
onNow func(location *time.Location) time.Time
}
Expand All @@ -27,10 +30,6 @@ func (f fakeTime) Sleep(duration time.Duration) {
panic("implement me")
}

func (f fakeTime) NewTicker(duration time.Duration) *time.Ticker {
panic("implement me")
}

func task() {
fmt.Println("I am a running job.")
}
Expand All @@ -57,11 +56,99 @@ func TestImmediateExecution(t *testing.T) {
}

func TestInvalidEveryInterval(t *testing.T) {
testCases := []struct {
description string
interval interface{}
expectedError string
}{
{"zero", 0, ErrInvalidInterval.Error()},
{"negative", -1, ErrInvalidInterval.Error()},
{"invalid string duration", "bad", "time: invalid duration"},
}

s := NewScheduler(time.UTC)

_, err := s.Every(0).Do(func() {})
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
_, err := s.Every(tc.interval).Do(func() {})
require.Error(t, err)
// wonky way to assert on the error message, but between go 1.14
// and go 1.15 the error value was wrapped in quotes
assert.True(t, strings.HasPrefix(err.Error(), tc.expectedError))
})
}

assert.EqualError(t, err, ErrInvalidInterval.Error())
}

func TestScheduler_Every(t *testing.T) {
t.Run("time.Duration", func(t *testing.T) {
s := NewScheduler(time.UTC)
semaphore := make(chan bool)

_, err := s.Every(100 * time.Millisecond).Do(func() {
semaphore <- true
})
require.NoError(t, err)

s.StartAsync()

var counter int

now := time.Now()
for time.Now().Before(now.Add(500 * time.Millisecond)) {
if <-semaphore {
counter++
}
}
s.Stop()
assert.Equal(t, 6, counter)
})

t.Run("int", func(t *testing.T) {
s := NewScheduler(time.UTC)
semaphore := make(chan bool)

_, err := s.Every(1).Second().Do(func() {
semaphore <- true
})
require.NoError(t, err)

s.StartAsync()

var counter int

now := time.Now()
for time.Now().Before(now.Add(1 * time.Second)) {
if <-semaphore {
counter++
}
}
s.Stop()
assert.Equal(t, 2, counter)
})

t.Run("string duration", func(t *testing.T) {
s := NewScheduler(time.UTC)
semaphore := make(chan bool)

_, err := s.Every("1s").Do(func() {
semaphore <- true
})
require.NoError(t, err)

s.StartAsync()

var counter int

now := time.Now()
for time.Now().Before(now.Add(1 * time.Second)) {
if <-semaphore {
counter++
}
}
s.Stop()
assert.Equal(t, 2, counter)
})
}

func TestExecutionSeconds(t *testing.T) {
Expand All @@ -70,8 +157,8 @@ func TestExecutionSeconds(t *testing.T) {

var (
executions []int64
interval uint64 = 2
expectedExecutions = 4
interval = 2
expectedExecutions = 4
mu sync.RWMutex
)

Expand Down Expand Up @@ -158,7 +245,7 @@ func TestAt(t *testing.T) {
})
}

func schedulerForNextOrPreviousWeekdayEveryNTimes(weekday time.Weekday, next bool, n uint64, s *Scheduler) *Scheduler {
func schedulerForNextOrPreviousWeekdayEveryNTimes(weekday time.Weekday, next bool, n int, s *Scheduler) *Scheduler {
switch weekday {
case time.Monday:
if next {
Expand Down Expand Up @@ -211,7 +298,8 @@ func TestWeekdayBeforeToday(t *testing.T) {
s := NewScheduler(time.UTC)

s = schedulerForNextOrPreviousWeekdayEveryNTimes(now.Weekday(), false, 1, s)
weekJob, _ := s.Do(task)
weekJob, err := s.Do(task)
require.NoError(t, err)
s.scheduleNextRun(weekJob)
sixDaysFromNow := now.AddDate(0, 0, 6)

Expand Down
7 changes: 2 additions & 5 deletions timeHelper.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ package gocron

import "time"

var _ timeWrapper = (*trueTime)(nil)

type timeWrapper interface {
Now(*time.Location) time.Time
Unix(int64, int64) time.Time
Sleep(time.Duration)
NewTicker(time.Duration) *time.Ticker
}

type trueTime struct{}
Expand All @@ -22,7 +23,3 @@ func (t *trueTime) Unix(sec int64, nsec int64) time.Time {
func (t *trueTime) Sleep(d time.Duration) {
time.Sleep(d)
}

func (t *trueTime) NewTicker(d time.Duration) *time.Ticker {
return time.NewTicker(d)
}

0 comments on commit fc07510

Please sign in to comment.