Repository: go-co-op/gocron
Pull request #229 (2021-09-08 16:37:10)

add start immediately func
### What does this do?

- adds the func `StartImmediately` to allow overriding the default behavior of a job, for example, starting a Cron job immediately vs. waiting for it's regular schedule, which is the default behavior of Cron
- renames the job mutex to be a private struct field `mu` so that a user can't lock/unlock the job


### Which issue(s) does this PR fix/relate to?
<!--- Put `Resolves #XXX` here to auto-close the issue that your PR fixes (if such) --->
#227 

### List any changes that modify/break current functionality


### Have you included tests for your changes?


### Did you document any new/modified functionality?

- [ ] Updated `example_test.go`
- [ ] Updated `README.md`

### Notes



Patch:
diff --git a/gocron.go b/gocron.go
index 427cb6c..09d6462 100644
--- a/gocron.go
+++ b/gocron.go
@@ -17,7 +17,7 @@ import (
 
 // Error declarations for gocron related errors
 var (
-	ErrNotAFunction                  = errors.New("only functions can be schedule into the job queue")
+	ErrNotAFunction                  = errors.New("only functions can be scheduled into the job queue")
 	ErrNotScheduledWeekday           = errors.New("job not scheduled weekly on a weekday")
 	ErrJobNotFoundWithTag            = errors.New("no jobs found with given tag")
 	ErrUnsupportedTimeFormat         = errors.New("the given time format is not supported")
diff --git a/job.go b/job.go
index 9c58dc8..dac8a43 100644
--- a/job.go
+++ b/job.go
@@ -12,18 +12,18 @@ import (
 
 // Job struct stores the information necessary to run a Job
 type Job struct {
-	sync.RWMutex
+	mu sync.RWMutex
 	jobFunction
 	interval          int            // pause interval * unit between runs
 	duration          time.Duration  // time duration between runs
-	unit              schedulingUnit // time units, ,e.g. 'minutes', 'hours'...
+	unit              schedulingUnit // time units, e.g. 'minutes', 'hours'...
 	startsImmediately bool           // if the Job should run upon scheduler start
 	atTime            time.Duration  // optional time at which this Job runs when interval is day
 	startAtTime       time.Time      // optional time at which the Job starts
 	error             error          // error related to Job
 	lastRun           time.Time      // datetime of last run
 	nextRun           time.Time      // datetime of next run
-	scheduledWeekday  []time.Weekday // Specific days of the week to start on
+	scheduledWeekdays []time.Weekday // Specific days of the week to start on
 	daysOfTheMonth    []int          // Specific days of the month to run the job
 	tags              []string       // allow the user to tag Jobs with certain labels
 	runCount          int            // number of times the job ran
@@ -88,8 +88,8 @@ func (j *Job) setStartsImmediately(b bool) {
 }
 
 func (j *Job) setTimer(t *time.Timer) {
-	j.Lock()
-	defer j.Unlock()
+	j.mu.Lock()
+	defer j.mu.Unlock()
 	j.timer = t
 }
 
@@ -110,26 +110,26 @@ func (j *Job) setStartAtTime(t time.Time) {
 }
 
 func (j *Job) getUnit() schedulingUnit {
-	j.RLock()
-	defer j.RUnlock()
+	j.mu.RLock()
+	defer j.mu.RUnlock()
 	return j.unit
 }
 
 func (j *Job) setUnit(t schedulingUnit) {
-	j.Lock()
-	defer j.Unlock()
+	j.mu.Lock()
+	defer j.mu.Unlock()
 	j.unit = t
 }
 
 func (j *Job) getDuration() time.Duration {
-	j.RLock()
-	defer j.RUnlock()
+	j.mu.RLock()
+	defer j.mu.RUnlock()
 	return j.duration
 }
 
 func (j *Job) setDuration(t time.Duration) {
-	j.Lock()
-	defer j.Unlock()
+	j.mu.Lock()
+	defer j.mu.Unlock()
 	j.duration = t
 }
 
@@ -165,8 +165,8 @@ func (j *Job) Tags() []string {
 
 // ScheduledTime returns the time of the Job's next scheduled run
 func (j *Job) ScheduledTime() time.Time {
-	j.RLock()
-	defer j.RUnlock()
+	j.mu.RLock()
+	defer j.mu.RUnlock()
 	return j.nextRun
 }
 
@@ -178,21 +178,21 @@ func (j *Job) ScheduledAtTime() string {
 // 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) {
-	if len(j.scheduledWeekday) == 0 {
+	if len(j.scheduledWeekdays) == 0 {
 		return time.Sunday, ErrNotScheduledWeekday
 	}
-	return j.scheduledWeekday[0], nil
+	return j.scheduledWeekdays[0], nil
 }
 
 // Weekdays returns a slice of time.Weekday that the Job will run in a week and
 // will return an error if the Job is not scheduled weekly
 func (j *Job) Weekdays() []time.Weekday {
-	// appending on j.scheduledWeekday may cause a side effect
-	if len(j.scheduledWeekday) == 0 {
+	// appending on j.scheduledWeekdays may cause a side effect
+	if len(j.scheduledWeekdays) == 0 {
 		return []time.Weekday{time.Sunday}
 	}
 
-	return j.scheduledWeekday
+	return j.scheduledWeekdays
 }
 
 // LimitRunsTo limits the number of executions of this job to n.
@@ -204,8 +204,8 @@ func (j *Job) Weekdays() []time.Weekday {
 // LimitRunsTo() func on the scheduler chain when scheduling the job.
 // For example: scheduler.LimitRunsTo(1).Do()
 func (j *Job) LimitRunsTo(n int) {
-	j.Lock()
-	defer j.Unlock()
+	j.mu.Lock()
+	defer j.mu.Unlock()
 	j.runConfig.finiteRuns = true
 	j.runConfig.maxRuns = n
 }
@@ -217,8 +217,8 @@ func (j *Job) LimitRunsTo(n int) {
 // by default upon being added to the scheduler. It is recommended to use the
 // SingletonMode() func on the scheduler chain when scheduling the job.
 func (j *Job) SingletonMode() {
-	j.Lock()
-	defer j.Unlock()
+	j.mu.Lock()
+	defer j.mu.Unlock()
 	j.runConfig.mode = singletonMode
 	j.jobFunction.limiter = &singleflight.Group{}
 
@@ -227,8 +227,8 @@ func (j *Job) SingletonMode() {
 // shouldRun evaluates if this job should run again
 // based on the runConfig
 func (j *Job) shouldRun() bool {
-	j.RLock()
-	defer j.RUnlock()
+	j.mu.RLock()
+	defer j.mu.RUnlock()
 	return !j.runConfig.finiteRuns || j.runCount < j.runConfig.maxRuns
 }
 
@@ -243,14 +243,14 @@ func (j *Job) setLastRun(t time.Time) {
 
 // NextRun returns the time the job will run next
 func (j *Job) NextRun() time.Time {
-	j.RLock()
-	defer j.RUnlock()
+	j.mu.RLock()
+	defer j.mu.RUnlock()
 	return j.nextRun
 }
 
 func (j *Job) setNextRun(t time.Time) {
-	j.Lock()
-	defer j.Unlock()
+	j.mu.Lock()
+	defer j.mu.Unlock()
 	j.nextRun = t
 }
 
@@ -260,8 +260,8 @@ func (j *Job) RunCount() int {
 }
 
 func (j *Job) stop() {
-	j.Lock()
-	defer j.Unlock()
+	j.mu.Lock()
+	defer j.mu.Unlock()
 	if j.timer != nil {
 		j.timer.Stop()
 	}
diff --git a/scheduler.go b/scheduler.go
index 16b42d2..8201450 100644
--- a/scheduler.go
+++ b/scheduler.go
@@ -14,8 +14,8 @@ import (
 
 type limitMode int8
 
-// 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
+// Scheduler struct stores a list of Jobs and the location of time used by the Scheduler,
+// and implements the sort.Interface{} for sorting Jobs, by the time of nextRun
 type Scheduler struct {
 	jobsMutex sync.RWMutex
 	jobs      []*Job
@@ -222,7 +222,7 @@ func (s *Scheduler) durationToNextRun(lastRun time.Time, job *Job) nextRun {
 	case days:
 		next = s.calculateDays(job, lastRun)
 	case weeks:
-		if len(job.scheduledWeekday) != 0 { // weekday selected, Every().Monday(), for example
+		if len(job.scheduledWeekdays) != 0 { // weekday selected, Every().Monday(), for example
 			next = s.calculateWeekday(job, lastRun)
 		} else {
 			next = s.calculateWeeks(job, lastRun)
@@ -496,8 +496,8 @@ func (s *Scheduler) run(job *Job) {
 		return
 	}
 
-	job.Lock()
-	defer job.Unlock()
+	job.mu.Lock()
+	defer job.mu.Unlock()
 	job.setLastRun(s.now())
 	job.runCount++
 	s.executor.jobFunctions <- job.jobFunction
@@ -556,8 +556,8 @@ func (s *Scheduler) Remove(job interface{}) {
 func (s *Scheduler) RemoveByReference(job *Job) {
 	s.removeJobsUniqueTags(job)
 	s.removeByCondition(func(someJob *Job) bool {
-		job.RLock()
-		defer job.RUnlock()
+		job.mu.RLock()
+		defer job.mu.RUnlock()
 		return someJob == job
 	})
 }
@@ -706,7 +706,7 @@ func (s *Scheduler) Do(jobFun interface{}, params ...interface{}) (*Job, error)
 		job.error = wrapOrError(job.error, ErrAtTimeNotSupported)
 	}
 
-	if len(job.scheduledWeekday) != 0 && jobUnit != weeks {
+	if len(job.scheduledWeekdays) != 0 && jobUnit != weeks {
 		job.error = wrapOrError(job.error, ErrWeekdayNotSupported)
 	}
 
@@ -738,7 +738,7 @@ func (s *Scheduler) Do(jobFun interface{}, params ...interface{}) (*Job, error)
 		job.name = fname
 	}
 
-	// we should not schedule if not running since we cant foresee how long it will take for the scheduler to start
+	// we should not schedule if not running since we can't foresee how long it will take for the scheduler to start
 	if s.IsRunning() {
 		s.scheduleNextRun(job)
 	}
@@ -928,12 +928,12 @@ func (s *Scheduler) Months(daysOfTheMonth ...int) *Scheduler {
 // spill over to the next month. Similarly, if it's less than 0,
 // it will go back to the month before
 
-// Weekday sets the scheduledWeekday with a specifics weekdays
+// Weekday sets the scheduledWeekdays with a specifics weekdays
 func (s *Scheduler) Weekday(weekDay time.Weekday) *Scheduler {
 	job := s.getCurrentJob()
 
-	if in := in(job.scheduledWeekday, weekDay); !in {
-		job.scheduledWeekday = append(job.scheduledWeekday, weekDay)
+	if in := in(job.scheduledWeekdays, weekDay); !in {
+		job.scheduledWeekdays = append(job.scheduledWeekdays, weekDay)
 	}
 
 	job.startsImmediately = false
@@ -1085,3 +1085,18 @@ func (s *Scheduler) WaitForSchedule() *Scheduler {
 	job.startsImmediately = false
 	return s
 }
+
+// StartImmediately sets the job to run immediately upon
+// starting the scheduler or adding the job to a running
+// scheduler. This overrides the jobs start status of any
+// previously called methods in the chain.
+//
+// Note: This is the default behavior of the scheduler
+// for most jobs, but is useful for overriding the default
+// behavior of Cron scheduled jobs which default to
+// WaitForSchedule.
+func (s *Scheduler) StartImmediately() *Scheduler {
+	job := s.getCurrentJob()
+	job.startsImmediately = true
+	return s
+}


Test patch:
diff --git a/example_test.go b/example_test.go
index 84ace51..5606a0a 100644
--- a/example_test.go
+++ b/example_test.go
@@ -453,6 +453,22 @@ func ExampleScheduler_RunAllWithDelay() {
 	s.RunAllWithDelay(10 * time.Second)
 }
 
+func ExampleScheduler_RunByTag() {
+	s := gocron.NewScheduler(time.UTC)
+	_, _ = s.Every(1).Day().At("10:00").Do(task)
+	_, _ = s.Every(2).Day().Tag("tag").At("10:00").Do(task)
+	s.StartAsync()
+	_ = s.RunByTag("tag")
+}
+
+func ExampleScheduler_RunByTagWithDelay() {
+	s := gocron.NewScheduler(time.UTC)
+	_, _ = s.Every(1).Day().Tag("tag").At("10:00").Do(task)
+	_, _ = s.Every(2).Day().Tag("tag").At("10:00").Do(task)
+	s.StartAsync()
+	_ = s.RunByTagWithDelay("tag", 2*time.Second)
+}
+
 func ExampleScheduler_Saturday() {
 	s := gocron.NewScheduler(time.UTC)
 	j, _ := s.Every(1).Day().Saturday().Do(task)
@@ -502,12 +518,6 @@ func ExampleScheduler_SingletonMode() {
 	_, _ = s.Every(1).Second().SingletonMode().Do(task)
 }
 
-func ExampleScheduler_StartBlocking() {
-	s := gocron.NewScheduler(time.UTC)
-	_, _ = s.Every(3).Seconds().Do(task)
-	s.StartBlocking()
-}
-
 func ExampleScheduler_StartAsync() {
 	s := gocron.NewScheduler(time.UTC)
 	_, _ = s.Every(3).Seconds().Do(task)
@@ -521,6 +531,18 @@ func ExampleScheduler_StartAt() {
 	s.StartBlocking()
 }
 
+func ExampleScheduler_StartBlocking() {
+	s := gocron.NewScheduler(time.UTC)
+	_, _ = s.Every(3).Seconds().Do(task)
+	s.StartBlocking()
+}
+
+func ExampleScheduler_StartImmediately() {
+	s := gocron.NewScheduler(time.UTC)
+	_, _ = s.Cron("0 0 * * 6,0").StartImmediately().Do(task)
+	s.StartBlocking()
+}
+
 func ExampleScheduler_Stop() {
 	s := gocron.NewScheduler(time.UTC)
 	_, _ = s.Every(1).Second().Do(task)
@@ -671,19 +693,3 @@ func ExampleScheduler_Weeks() {
 
 	_, _ = s.Every(2).Weeks().Monday().Wednesday().Friday().Do(task)
 }
-
-func ExampleScheduler_RunByTag() {
-	s := gocron.NewScheduler(time.UTC)
-	_, _ = s.Every(1).Day().At("10:00").Do(task)
-	_, _ = s.Every(2).Day().Tag("tag").At("10:00").Do(task)
-	s.StartAsync()
-	_ = s.RunByTag("tag")
-}
-
-func ExampleScheduler_RunByTagWithDelay() {
-	s := gocron.NewScheduler(time.UTC)
-	_, _ = s.Every(1).Day().Tag("tag").At("10:00").Do(task)
-	_, _ = s.Every(2).Day().Tag("tag").At("10:00").Do(task)
-	s.StartAsync()
-	_ = s.RunByTagWithDelay("tag", 2*time.Second)
-}
diff --git a/scheduler_test.go b/scheduler_test.go
index a6331a3..8ba5b6f 100644
--- a/scheduler_test.go
+++ b/scheduler_test.go
@@ -737,12 +737,12 @@ func TestScheduler_CalculateNextRun(t *testing.T) {
 		{name: "every 2 months at day 1, starting at day 2, should run in 2 months - 1 day", job: &Job{interval: 2, unit: months, daysOfTheMonth: []int{1}, lastRun: januaryFirst2020At(0, 0, 0).AddDate(0, 0, 1)}, wantTimeUntilNextRun: 30*day + 29*day}, // 2020 january and february
 		{name: "every 13 months at day 1, starting at day 2 run in 13 months - 1 day", job: &Job{interval: 13, unit: months, daysOfTheMonth: []int{1}, lastRun: januaryFirst2020At(0, 0, 0).AddDate(0, 0, 1)}, wantTimeUntilNextRun: januaryFirst2020At(0, 0, 0).AddDate(0, 13, -1).Sub(januaryFirst2020At(0, 0, 0))},
 		//// WEEKDAYS
-		{name: "every weekday starting on one day before it should run this weekday", job: &Job{interval: 1, unit: weeks, scheduledWeekday: []time.Weekday{*_tuesdayWeekday()}, lastRun: mondayAt(0, 0, 0)}, wantTimeUntilNextRun: 1 * day},
-		{name: "every weekday starting on same weekday should run on same immediately", job: &Job{interval: 1, unit: weeks, scheduledWeekday: []time.Weekday{*_tuesdayWeekday()}, lastRun: mondayAt(0, 0, 0).AddDate(0, 0, 1)}, wantTimeUntilNextRun: 0},
-		{name: "every 2 weekdays counting this week's weekday should run next weekday", job: &Job{interval: 2, unit: weeks, scheduledWeekday: []time.Weekday{*_tuesdayWeekday()}, lastRun: mondayAt(0, 0, 0)}, wantTimeUntilNextRun: day},
-		{name: "every weekday starting on one day after should count days remaining", job: &Job{interval: 1, unit: weeks, scheduledWeekday: []time.Weekday{*_tuesdayWeekday()}, lastRun: mondayAt(0, 0, 0).AddDate(0, 0, 2)}, wantTimeUntilNextRun: 6 * day},
-		{name: "every weekday starting before jobs .At() time should run at same day at time", job: &Job{interval: 1, unit: weeks, atTime: _getHours(9) + _getMinutes(30), scheduledWeekday: []time.Weekday{*_tuesdayWeekday()}, lastRun: mondayAt(0, 0, 0).AddDate(0, 0, 1)}, wantTimeUntilNextRun: _getHours(9) + _getMinutes(30)},
-		{name: "every weekday starting at same day at time that already passed should run at next week at time", job: &Job{interval: 1, unit: weeks, atTime: _getHours(9) + _getMinutes(30), scheduledWeekday: []time.Weekday{*_tuesdayWeekday()}, lastRun: mondayAt(10, 30, 0).AddDate(0, 0, 1)}, wantTimeUntilNextRun: 6*day + _getHours(23) + _getMinutes(0)},
+		{name: "every weekday starting on one day before it should run this weekday", job: &Job{interval: 1, unit: weeks, scheduledWeekdays: []time.Weekday{*_tuesdayWeekday()}, lastRun: mondayAt(0, 0, 0)}, wantTimeUntilNextRun: 1 * day},
+		{name: "every weekday starting on same weekday should run on same immediately", job: &Job{interval: 1, unit: weeks, scheduledWeekdays: []time.Weekday{*_tuesdayWeekday()}, lastRun: mondayAt(0, 0, 0).AddDate(0, 0, 1)}, wantTimeUntilNextRun: 0},
+		{name: "every 2 weekdays counting this week's weekday should run next weekday", job: &Job{interval: 2, unit: weeks, scheduledWeekdays: []time.Weekday{*_tuesdayWeekday()}, lastRun: mondayAt(0, 0, 0)}, wantTimeUntilNextRun: day},
+		{name: "every weekday starting on one day after should count days remaining", job: &Job{interval: 1, unit: weeks, scheduledWeekdays: []time.Weekday{*_tuesdayWeekday()}, lastRun: mondayAt(0, 0, 0).AddDate(0, 0, 2)}, wantTimeUntilNextRun: 6 * day},
+		{name: "every weekday starting before jobs .At() time should run at same day at time", job: &Job{interval: 1, unit: weeks, atTime: _getHours(9) + _getMinutes(30), scheduledWeekdays: []time.Weekday{*_tuesdayWeekday()}, lastRun: mondayAt(0, 0, 0).AddDate(0, 0, 1)}, wantTimeUntilNextRun: _getHours(9) + _getMinutes(30)},
+		{name: "every weekday starting at same day at time that already passed should run at next week at time", job: &Job{interval: 1, unit: weeks, atTime: _getHours(9) + _getMinutes(30), scheduledWeekdays: []time.Weekday{*_tuesdayWeekday()}, lastRun: mondayAt(10, 30, 0).AddDate(0, 0, 1)}, wantTimeUntilNextRun: 6*day + _getHours(23) + _getMinutes(0)},
 	}
 
 	for _, tc := range testCases {
@@ -1534,7 +1534,7 @@ func TestScheduler_LenWeekDays(t *testing.T) {
 			}
 			j, err := s.Do(func() {})
 			require.NoError(t, err)
-			assert.Equal(t, len(j.scheduledWeekday), tc.finalLen)
+			assert.Equal(t, len(j.scheduledWeekdays), tc.finalLen)
 		})
 	}
 
@@ -1673,6 +1673,29 @@ func TestScheduler_CheckEveryWeekHigherThanOne(t *testing.T) {
 
 }
 
+func TestScheduler_StartImmediately(t *testing.T) {
+
+	testCases := []struct {
+		description                string
+		scheduler                  *Scheduler
+		expectedToStartImmediately bool
+	}{
+		{"true cron", NewScheduler(time.UTC).Cron("0 0 * * 6,0").StartImmediately(), true},
+		{"true default", NewScheduler(time.UTC).Every("1m"), true},
+		{"true every", NewScheduler(time.UTC).Every("1m").StartImmediately(), true},
+		{"false cron default", NewScheduler(time.UTC).Cron("0 0 * * 6,0"), false},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.description, func(t *testing.T) {
+			j, err := tc.scheduler.Do(func() {})
+			require.NoError(t, err)
+
+			assert.Exactly(t, tc.expectedToStartImmediately, j.startsImmediately)
+		})
+	}
+}
+
 func TestScheduler_CheckCalculateDaysOfMonth(t *testing.T) {
 	lastRunFirstCaseDate := time.Date(2021, 9, 10, 0, 0, 0, 0, time.UTC)
 	lastRunSecondCaseDate := time.Date(2021, 9, 10, 5, 0, 0, 0, time.UTC)