diff --git a/.github/workflows/go_test.yml b/.github/workflows/go_test.yml index 15ed5250..377f98ea 100644 --- a/.github/workflows/go_test.yml +++ b/.github/workflows/go_test.yml @@ -23,12 +23,12 @@ jobs: - name: Checkout code uses: actions/checkout@v1 - name: fmt - run: gofmt -w . + run: make check-fmt - name: lint run: | go get golang.org/x/lint/golint $(go list -f {{.Target}} golang.org/x/lint/golint) -set_exit_status ./... - name: vet - run: go vet ./... + run: make vet - name: test - run: go test ./... + run: make test diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..da2b262d --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +.PHONY: fmt check-fmt lint vet test + +GO_PKGS := $(shell go list -f {{.Dir}} ./...) + +fmt: + @go list -f {{.Dir}} ./... | xargs -I{} gofmt -w -s {} + +check-fmt: + @echo "Checking formatting..." + @FMT="0"; \ + for pkg in $(GO_PKGS); do \ + OUTPUT=`gofmt -l $$pkg/*.go`; \ + if [ -n "$$OUTPUT" ]; then \ + echo "$$OUTPUT"; \ + FMT="1"; \ + fi; \ + done ; \ + if [ "$$FMT" -eq "1" ]; then \ + echo "Problem with formatting in files above."; \ + exit 1; \ + else \ + echo "Success - way to run gofmt!"; \ + fi + +lint: +# Add -set_exit_status=true when/if we want to enforce the linter rules + @golint -min_confidence 0.8 -set_exit_status $(GO_PKGS) + +vet: + @go vet $(GO_FLAGS) $(GO_PKGS) + +test: + @go test -race $(GO_FLAGS) -count=1 $(GO_PKGS) diff --git a/README.md b/README.md index eed7c1c7..24332297 100644 --- a/README.md +++ b/README.md @@ -99,10 +99,6 @@ func main() { // Delay start of job s2.Every(1).Hour().StartAt(time.Now().Add(time.Duration(1 * time.Hour)).Do(task) - // Deprecated: Jobs start immediately by default - // use StartImmediately() to run job upon scheduler start - s2.Every(1).Hour().StartImmediately().Do(task) - // NextRun gets the next running time _, time := s2.NextRun() fmt.Println(time) diff --git a/example_test.go b/example_test.go index f984467e..ec634ae2 100644 --- a/example_test.go +++ b/example_test.go @@ -23,13 +23,6 @@ func ExampleScheduler_StartAsync() { s.StartAsync() } -// Deprecated: All jobs start immediately by default unless set to a specific date or time -func ExampleScheduler_StartImmediately() { - s := gocron.NewScheduler(time.UTC) - _, _ = s.Every(1).Hour().StartImmediately().Do(task) - s.StartBlocking() -} - func ExampleScheduler_StartAt() { s := gocron.NewScheduler(time.UTC) specificTime := time.Date(2019, time.November, 10, 15, 0, 0, 0, time.UTC) diff --git a/job.go b/job.go index 1775a431..23c13653 100644 --- a/job.go +++ b/job.go @@ -29,9 +29,9 @@ type Job struct { } type runConfig struct { - finiteRuns bool - maxRuns int - removeAfterLastRun bool + finiteRuns bool + maxRuns int + removeAfterLastRun bool } // NewJob creates a new Job with the provided interval @@ -56,17 +56,47 @@ func (j *Job) run() { } func (j *Job) neverRan() bool { + j.RLock() + defer j.RUnlock() return j.lastRun.IsZero() } +func (j *Job) getStartsImmediately() bool { + j.RLock() + defer j.RUnlock() + return j.startsImmediately +} + +func (j *Job) setStartsImmediately(b bool) { + j.Lock() + defer j.Unlock() + j.startsImmediately = b +} + +func (j *Job) getAtTime() time.Duration { + j.RLock() + defer j.RUnlock() + return j.atTime +} + +func (j *Job) setAtTime(t time.Duration) { + j.Lock() + defer j.Unlock() + j.atTime = t +} + // Err returns an error if one occurred while creating the Job func (j *Job) Err() error { + j.RLock() + defer j.RUnlock() return j.err } // Tag allows you to add arbitrary labels to a Job that do not // impact the functionality of the Job func (j *Job) Tag(t string, others ...string) { + j.Lock() + defer j.Unlock() j.tags = append(j.tags, t) for _, tag := range others { j.tags = append(j.tags, tag) @@ -75,6 +105,8 @@ func (j *Job) Tag(t string, others ...string) { // Untag removes a tag from a Job func (j *Job) Untag(t string) { + j.Lock() + defer j.Unlock() var newTags []string for _, tag := range j.tags { if t != tag { @@ -87,22 +119,30 @@ func (j *Job) Untag(t string) { // Tags returns the tags attached to the Job func (j *Job) Tags() []string { + j.RLock() + defer j.RUnlock() return j.tags } // ScheduledTime returns the time of the Job's next scheduled run func (j *Job) ScheduledTime() time.Time { + j.RLock() + defer j.RUnlock() return j.nextRun } // ScheduledAtTime returns the specific time of day the Job will run at func (j *Job) ScheduledAtTime() string { + j.RLock() + defer j.RUnlock() return fmt.Sprintf("%d:%d", j.atTime/time.Hour, (j.atTime%time.Hour)/time.Minute) } // Weekday returns which day of the week the Job will run on and // will return an error if the Job is not scheduled weekly func (j *Job) Weekday() (time.Weekday, error) { + j.RLock() + defer j.RUnlock() if j.scheduledWeekday == nil { return time.Sunday, ErrNotScheduledWeekday } @@ -113,6 +153,8 @@ func (j *Job) Weekday() (time.Weekday, error) { // job to n. However, the job will still remain in the // scheduler func (j *Job) LimitRunsTo(n int) { + j.Lock() + defer j.Unlock() j.runConfig = runConfig{ finiteRuns: true, maxRuns: n, @@ -122,32 +164,50 @@ func (j *Job) LimitRunsTo(n int) { // shouldRun evaluates if this job should run again // based on the runConfig func (j *Job) shouldRun() bool { + j.RLock() + defer j.RUnlock() return !j.runConfig.finiteRuns || j.runCount < j.runConfig.maxRuns } // LastRun returns the time the job was run last func (j *Job) LastRun() time.Time { + j.RLock() + defer j.RUnlock() + return j.lastRun +} + +func (j *Job) setLastRun(t time.Time) { j.Lock() defer j.Unlock() - lastRun := j.lastRun - return lastRun + j.lastRun = t } // NextRun returns the time the job will run next func (j *Job) NextRun() time.Time { + j.RLock() + defer j.RUnlock() + return j.nextRun +} + +func (j *Job) setNextRun(t time.Time) { j.Lock() defer j.Unlock() - nextRun := j.nextRun - return nextRun + j.nextRun = t } // RunCount returns the number of time the job ran so far func (j *Job) RunCount() int { + j.RLock() + defer j.RUnlock() + return j.runCount +} + +func (j *Job) setRunCount(i int) { j.Lock() defer j.Unlock() - runCount := j.runCount - return runCount + j.runCount = i } + // RemoveAfterLastRun update the job in order to remove the job after its last exec func (j *Job) RemoveAfterLastRun() *Job { j.Lock() @@ -155,3 +215,21 @@ func (j *Job) RemoveAfterLastRun() *Job { j.runConfig.removeAfterLastRun = true return j } + +func (j *Job) getFiniteRuns() bool { + j.RLock() + defer j.RUnlock() + return j.runConfig.finiteRuns +} + +func (j *Job) getMaxRuns() int { + j.RLock() + defer j.RUnlock() + return j.runConfig.maxRuns +} + +func (j *Job) getRemoveAfterLastRun() bool { + j.RLock() + defer j.RUnlock() + return j.runConfig.removeAfterLastRun +} diff --git a/scheduler.go b/scheduler.go index aefede3c..e23541e6 100644 --- a/scheduler.go +++ b/scheduler.go @@ -5,17 +5,21 @@ import ( "reflect" "sort" "strings" + "sync" "time" ) // Scheduler struct stores a list of Jobs and the location of time Scheduler // Scheduler implements the sort.Interface{} for sorting Jobs, by the time of nextRun type Scheduler struct { - jobs []*Job - loc *time.Location + jobsMutex sync.RWMutex + jobs []*Job - running bool // represents if the scheduler is running at the moment or not - stopChan chan struct{} // signal to stop scheduling + loc *time.Location + + runningMutex sync.RWMutex + running bool // represents if the scheduler is running at the moment or not + stopChan chan struct{} // signal to stop scheduling time timeWrapper // wrapper around time.Time } @@ -38,10 +42,10 @@ func (s *Scheduler) StartBlocking() { // StartAsync starts a goroutine that runs all the pending using a second-long ticker func (s *Scheduler) StartAsync() chan struct{} { - if s.running { + if s.IsRunning() { return s.stopChan } - s.running = true + s.setRunning(true) s.scheduleAllJobs() ticker := s.time.NewTicker(1 * time.Second) @@ -52,7 +56,7 @@ func (s *Scheduler) StartAsync() chan struct{} { s.RunPending() case <-s.stopChan: ticker.Stop() - s.running = false + s.setRunning(false) return } } @@ -61,23 +65,48 @@ func (s *Scheduler) StartAsync() chan struct{} { return s.stopChan } +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 } -// Len returns the number of Jobs in the Scheduler +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 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] } func (s *Scheduler) Less(i, j int) bool { - return s.jobs[j].nextRun.Unix() >= s.jobs[i].nextRun.Unix() + return s.Jobs()[j].NextRun().Unix() >= s.Jobs()[i].NextRun().Unix() } // ChangeLocation changes the default time location @@ -87,29 +116,27 @@ func (s *Scheduler) ChangeLocation(newLocation *time.Location) { // scheduleNextRun Compute the instant when this Job should run next func (s *Scheduler) scheduleNextRun(job *Job) { - job.Lock() - defer job.Unlock() now := s.time.Now(s.loc) if job.neverRan() { - if !job.nextRun.IsZero() { + if !job.NextRun().IsZero() { return // scheduled for future run and should skip scheduling } // default is for jobs to start immediately unless scheduled at a specific time or day - if job.startsImmediately { - job.nextRun = now + if job.getStartsImmediately() { + job.setNextRun(now) return } } - job.lastRun = now + job.setLastRun(now) durationToNextRun := s.durationToNextRun(job) - job.nextRun = job.lastRun.Add(durationToNextRun) + job.setNextRun(job.LastRun().Add(durationToNextRun)) } func (s *Scheduler) durationToNextRun(job *Job) time.Duration { - lastRun := job.lastRun + lastRun := job.LastRun() var duration time.Duration switch job.unit { case seconds, minutes, hours: @@ -128,20 +155,20 @@ func (s *Scheduler) durationToNextRun(job *Job) time.Duration { return duration } -func (s Scheduler) getJobLastRun(job *Job) time.Time { +func (s *Scheduler) getJobLastRun(job *Job) time.Time { if job.neverRan() { return s.time.Now(s.loc) } - return job.lastRun + return job.LastRun() } func (s *Scheduler) calculateMonths(job *Job, lastRun time.Time) time.Duration { lastRunRoundedMidnight := s.roundToMidnight(lastRun) if job.dayOfTheMonth > 0 { // calculate days to j.dayOfTheMonth - jobDay := time.Date(lastRun.Year(), lastRun.Month(), job.dayOfTheMonth, 0, 0, 0, 0, s.loc).Add(job.atTime) + jobDay := time.Date(lastRun.Year(), lastRun.Month(), job.dayOfTheMonth, 0, 0, 0, 0, s.loc).Add(job.getAtTime()) daysDifference := int(math.Abs(lastRun.Sub(jobDay).Hours()) / 24) - nextRun := s.roundToMidnight(lastRun).Add(job.atTime) + nextRun := s.roundToMidnight(lastRun).Add(job.getAtTime()) if jobDay.Before(lastRun) { // shouldn't run this month; schedule for next interval minus day difference nextRun = nextRun.AddDate(0, int(job.interval), -daysDifference) } else { @@ -153,20 +180,20 @@ func (s *Scheduler) calculateMonths(job *Job, lastRun time.Time) time.Duration { } return s.until(lastRunRoundedMidnight, nextRun) } - nextRun := lastRunRoundedMidnight.Add(job.atTime).AddDate(0, int(job.interval), 0) + nextRun := lastRunRoundedMidnight.Add(job.getAtTime()).AddDate(0, int(job.interval), 0) return s.until(lastRunRoundedMidnight, nextRun) } func (s *Scheduler) calculateWeekday(job *Job, lastRun time.Time) time.Duration { daysToWeekday := remainingDaysToWeekday(lastRun.Weekday(), *job.scheduledWeekday) totalDaysDifference := s.calculateTotalDaysDifference(lastRun, daysToWeekday, job) - nextRun := s.roundToMidnight(lastRun).Add(job.atTime).AddDate(0, 0, totalDaysDifference) + nextRun := s.roundToMidnight(lastRun).Add(job.getAtTime()).AddDate(0, 0, totalDaysDifference) return s.until(lastRun, nextRun) } func (s *Scheduler) calculateWeeks(job *Job, lastRun time.Time) time.Duration { totalDaysDifference := int(job.interval) * 7 - nextRun := s.roundToMidnight(lastRun).Add(job.atTime).AddDate(0, 0, totalDaysDifference) + nextRun := s.roundToMidnight(lastRun).Add(job.getAtTime()).AddDate(0, 0, totalDaysDifference) return s.until(lastRun, nextRun) } @@ -176,7 +203,7 @@ func (s *Scheduler) calculateTotalDaysDifference(lastRun time.Time, daysToWeekda } if daysToWeekday == 0 { // today, at future time or already passed - lastRunAtTime := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, s.loc).Add(job.atTime) + lastRunAtTime := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, s.loc).Add(job.getAtTime()) if lastRun.Before(lastRunAtTime) || lastRun.Equal(lastRunAtTime) { return 0 } @@ -188,17 +215,17 @@ func (s *Scheduler) calculateTotalDaysDifference(lastRun time.Time, daysToWeekda func (s *Scheduler) calculateDays(job *Job, lastRun time.Time) time.Duration { if job.interval == 1 { - lastRunDayPlusJobAtTime := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, s.loc).Add(job.atTime) + lastRunDayPlusJobAtTime := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, s.loc).Add(job.getAtTime()) if shouldRunToday(lastRun, lastRunDayPlusJobAtTime) { - return s.until(lastRun, s.roundToMidnight(lastRun).Add(job.atTime)) + return s.until(lastRun, s.roundToMidnight(lastRun).Add(job.getAtTime())) } } - nextRunAtTime := s.roundToMidnight(lastRun).Add(job.atTime).AddDate(0, 0, int(job.interval)).In(s.loc) + nextRunAtTime := s.roundToMidnight(lastRun).Add(job.getAtTime()).AddDate(0, 0, int(job.interval)).In(s.loc) return s.until(lastRun, nextRunAtTime) } -func (s Scheduler) until(from time.Time, until time.Time) time.Duration { +func (s *Scheduler) until(from time.Time, until time.Time) time.Duration { return until.Sub(from) } @@ -207,11 +234,11 @@ func shouldRunToday(lastRun time.Time, atTime time.Time) bool { } func (s *Scheduler) calculateDuration(job *Job) time.Duration { - lastRun := job.lastRun + lastRun := job.LastRun() 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 - atTime := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, s.loc).Add(job.atTime) + atTime := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, s.loc).Add(job.getAtTime()) if lastRun.Before(atTime) || lastRun.Equal(atTime) { - return time.Until(s.roundToMidnight(lastRun).Add(job.atTime)) + return time.Until(s.roundToMidnight(lastRun).Add(job.getAtTime())) } } @@ -227,7 +254,7 @@ func (s *Scheduler) calculateDuration(job *Job) time.Duration { } func shouldRunAtSpecificTime(job *Job) bool { - return job.atTime != 0 + return job.getAtTime() != 0 } func remainingDaysToWeekday(from time.Weekday, to time.Weekday) int { @@ -247,7 +274,7 @@ func (s *Scheduler) roundToMidnight(t time.Time) time.Time { func (s *Scheduler) runnableJobs() []*Job { var runnableJobs []*Job sort.Sort(s) - for _, job := range s.jobs { + for _, job := range s.Jobs() { if s.shouldRun(job) { runnableJobs = append(runnableJobs, job) } @@ -257,17 +284,19 @@ func (s *Scheduler) runnableJobs() []*Job { // NextRun datetime when the next Job should run. func (s *Scheduler) NextRun() (*Job, time.Time) { - if len(s.jobs) <= 0 { + if len(s.Jobs()) <= 0 { return nil, s.time.Now(s.loc) } + sort.Sort(s) - return s.jobs[0], s.jobs[0].nextRun + + return s.Jobs()[0], s.Jobs()[0].NextRun() } // Every schedules a new periodic Job with interval func (s *Scheduler) Every(interval uint64) *Scheduler { job := NewJob(interval) - s.jobs = append(s.jobs, job) + s.setJobs(append(s.Jobs(), job)) return s } @@ -287,7 +316,7 @@ func (s *Scheduler) runAndReschedule(job *Job) error { } func (s *Scheduler) run(job *Job) error { - job.lastRun = s.time.Now(s.loc) + job.setLastRun(s.time.Now(s.loc)) go job.run() return nil } @@ -299,7 +328,7 @@ func (s *Scheduler) RunAll() { // RunAllWithDelay runs all Jobs with delay seconds func (s *Scheduler) RunAllWithDelay(d int) { - for _, job := range s.jobs { + for _, job := range s.Jobs() { err := s.run(job) if err != nil { continue @@ -324,12 +353,12 @@ func (s *Scheduler) RemoveByReference(j *Job) { func (s *Scheduler) removeByCondition(shouldRemove func(*Job) bool) { retainedJobs := make([]*Job, 0) - for _, job := range s.jobs { + for _, job := range s.Jobs() { if !shouldRemove(job) { retainedJobs = append(retainedJobs, job) } } - s.jobs = retainedJobs + s.setJobs(retainedJobs) } // RemoveJobByTag will Remove Jobs by Tag @@ -339,13 +368,13 @@ func (s *Scheduler) RemoveJobByTag(tag string) error { return err } // Remove job if jobindex is valid - s.jobs = removeAtIndex(s.jobs, jobindex) + s.setJobs(removeAtIndex(s.jobs, jobindex)) return nil } // Find first job index by given string func (s *Scheduler) findJobsIndexByTag(tag string) (int, error) { - for i, job := range s.jobs { + for i, job := range s.Jobs() { if strings.Contains(strings.Join(job.Tags(), " "), tag) { return i, nil } @@ -363,7 +392,7 @@ func removeAtIndex(jobs []*Job, i int) []*Job { // Scheduled checks if specific Job j was already added func (s *Scheduler) Scheduled(j interface{}) bool { - for _, job := range s.jobs { + for _, job := range s.Jobs() { if job.jobFunc == getFunctionName(j) { return true } @@ -373,12 +402,12 @@ func (s *Scheduler) Scheduled(j interface{}) bool { // Clear clear all Jobs from this scheduler func (s *Scheduler) Clear() { - s.jobs = make([]*Job, 0) + s.setJobs(make([]*Job, 0)) } // Stop stops the scheduler. This is a no-op if the scheduler is already stopped . func (s *Scheduler) Stop() { - if s.running { + if s.IsRunning() { s.stopScheduler() } } @@ -410,7 +439,7 @@ func (s *Scheduler) Do(jobFun interface{}, params ...interface{}) (*Job, error) j.jobFunc = fname // we should not schedule if not running since we cant foresee how long it will take for the scheduler to start - if s.running { + if s.IsRunning() { s.scheduleNextRun(j) } @@ -426,7 +455,7 @@ func (s *Scheduler) At(t string) *Scheduler { return s } // save atTime start as duration from midnight - j.atTime = time.Duration(hour)*time.Hour + time.Duration(min)*time.Minute + time.Duration(sec)*time.Second + j.setAtTime(time.Duration(hour)*time.Hour + time.Duration(min)*time.Minute + time.Duration(sec)*time.Second) j.startsImmediately = false return s } @@ -441,28 +470,20 @@ func (s *Scheduler) SetTag(t []string) *Scheduler { // StartAt schedules the next run of the Job func (s *Scheduler) StartAt(t time.Time) *Scheduler { job := s.getCurrentJob() - job.nextRun = t + job.setNextRun(t) job.startsImmediately = false return s } -// StartImmediately sets the Jobs next run as soon as the scheduler starts -// Deprecated: Jobs start immediately by default unless a specific start day or time is set -func (s *Scheduler) StartImmediately() *Scheduler { - job := s.getCurrentJob() - job.startsImmediately = true - return s -} - // shouldRun returns true if the Job should be run now func (s *Scheduler) shouldRun(j *Job) bool { // option remove the job's in the scheduler after its last execution - if j.runConfig.removeAfterLastRun && (j.runConfig.maxRuns - j.runCount) == 1 { + if j.getRemoveAfterLastRun() && (j.getMaxRuns()-j.RunCount()) == 1 { s.RemoveByReference(j) } - return j.shouldRun() && s.time.Now(s.loc).Unix() >= j.nextRun.Unix() + return j.shouldRun() && s.time.Now(s.loc).Unix() >= j.NextRun().Unix() } // setUnit sets the unit type @@ -592,11 +613,11 @@ func (s *Scheduler) Sunday() *Scheduler { } func (s *Scheduler) getCurrentJob() *Job { - return s.jobs[len(s.jobs)-1] + return s.Jobs()[len(s.jobs)-1] } func (s *Scheduler) scheduleAllJobs() { - for _, j := range s.jobs { + for _, j := range s.Jobs() { s.scheduleNextRun(j) } } diff --git a/scheduler_test.go b/scheduler_test.go index 3c330d8c..2333fcc2 100644 --- a/scheduler_test.go +++ b/scheduler_test.go @@ -2,6 +2,7 @@ package gocron import ( "fmt" + "sync" "testing" "time" @@ -41,10 +42,16 @@ func taskWithParams(a int, b string) { func TestExecutionSecond(t *testing.T) { sched := NewScheduler(time.UTC) success := false - sched.Every(1).Second().Do(func(mutableValue *bool) { + mu := sync.Mutex{} + sched.Every(1).Second().Do(func(mutableValue *bool, mu *sync.Mutex) { + mu.Lock() + defer mu.Unlock() *mutableValue = !*mutableValue - }, &success) + }, &success, &mu) sched.RunAllWithDelay(1) + + mu.Lock() + defer mu.Unlock() assert.Equal(t, true, success, "Task did not get called") } @@ -97,20 +104,6 @@ func TestScheduledWithTag(t *testing.T) { } } -func TestStartImmediately(t *testing.T) { - sched := NewScheduler(time.UTC) - now := time.Now().UTC() - - job, _ := sched.Every(1).Hour().StartImmediately().Do(task) - sched.scheduleAllJobs() - next := job.ScheduledTime() - - nextRounded := time.Date(next.Year(), next.Month(), next.Day(), next.Hour(), next.Minute(), next.Second(), 0, time.UTC) - expected := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second(), 0, time.UTC) - - assert.Exactly(t, expected, nextRounded) -} - func TestAtFuture(t *testing.T) { s := NewScheduler(time.UTC) now := time.Now().UTC() @@ -380,15 +373,15 @@ func TestSetUnit(t *testing.T) { func TestScheduler_Stop(t *testing.T) { t.Run("stops a running scheduler", func(t *testing.T) { - sched := NewScheduler(time.UTC) - sched.StartAsync() - sched.Stop() - assert.False(t, sched.running) + s := NewScheduler(time.UTC) + s.StartAsync() + s.Stop() + assert.False(t, s.IsRunning()) }) t.Run("noop on stopped scheduler", func(t *testing.T) { - sched := NewScheduler(time.UTC) - sched.Stop() - assert.False(t, sched.running) + s := NewScheduler(time.UTC) + s.Stop() + assert.False(t, s.IsRunning()) }) } @@ -813,7 +806,7 @@ func _getMinutes(i int) time.Duration { func TestScheduler_Do(t *testing.T) { t.Run("adding a new job before scheduler starts does not schedule job", func(t *testing.T) { s := NewScheduler(time.UTC) - s.running = false + s.setRunning(false) job, err := s.Every(1).Second().Do(func() {}) assert.Equal(t, nil, err) assert.True(t, job.nextRun.IsZero()) @@ -821,7 +814,7 @@ func TestScheduler_Do(t *testing.T) { t.Run("adding a new job when scheduler is running schedules job", func(t *testing.T) { s := NewScheduler(time.UTC) - s.running = true + s.setRunning(true) job, err := s.Every(1).Second().Do(func() {}) assert.Equal(t, nil, err) assert.False(t, job.nextRun.IsZero()) @@ -829,28 +822,35 @@ func TestScheduler_Do(t *testing.T) { } func TestRunJobsWithLimit(t *testing.T) { - f := func(in *int) { + f := func(in *int, mu *sync.RWMutex) { + mu.Lock() + defer mu.Unlock() *in = *in + 1 } s := NewScheduler(time.UTC) s.StartAsync() - var j1Counter int - j1, err := s.Every(1).StartAt(time.Now().UTC().Add(1*time.Second)).Do(f, &j1Counter) + var j1Counter, j2Counter int + var j1Mutex, j2Mutex sync.RWMutex + j1, err := s.Every(1).StartAt(time.Now().UTC().Add(1*time.Second)).Do(f, &j1Counter, &j1Mutex) require.NoError(t, err) j1.LimitRunsTo(1) - var j2Counter int - j2, err := s.Every(1).StartAt(time.Now().UTC().Add(2*time.Second)).Do(f, &j2Counter) + j2, err := s.Every(1).StartAt(time.Now().UTC().Add(2*time.Second)).Do(f, &j2Counter, &j2Mutex) require.NoError(t, err) j2.LimitRunsTo(1) time.Sleep(3 * time.Second) + j1Mutex.RLock() + j1Mutex.RUnlock() assert.Exactly(t, 1, j1Counter) + + j2Mutex.RLock() + j2Mutex.RUnlock() assert.Exactly(t, 1, j2Counter) }