Repository: go-co-op/gocron
Pull request #137 (2021-03-08 05:15:29)

a bunch of things
### What does this do?
- renamed job.Err to job. Error
- remove, removeAfterLastRun - just remove when a job hits the limit by default. we don't have any way to restart a job, so there is not point in keeping them in the scheduler once they're stopped
- add milliseconds explicitly as a unit (already available using the Every(time.Duration) option)
- allow At() to accept an interface, string or time.Time (parsing hours down if time.Time)

### Which issue(s) does this PR fix/relate to?
<!--- Put `Resolves #XXX` here to auto-close the issue that your PR fixes (if such) --->


### 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 4ab0c59..d811340 100644
--- a/gocron.go
+++ b/gocron.go
@@ -24,9 +24,10 @@ var (
 	ErrInvalidInterval               = errors.New(".Every() interval must be greater than 0")
 	ErrInvalidIntervalType           = errors.New(".Every() interval must be int, time.Duration, or string")
 	ErrInvalidIntervalUnitsSelection = errors.New("an .Every() duration interval cannot be used with units (e.g. .Seconds())")
-	ErrAtTimeNotSupported            = errors.New("the At() not supported for time unit")
-	ErrWeekdayNotSupported           = errors.New("weekday is not supported for time unit")
-	ErrTagsUnique                    = func(tag string) error { return fmt.Errorf("a non-unique tag was set on the job: %s", tag) }
+
+	ErrAtTimeNotSupported  = errors.New("the At() method is not supported for this time unit")
+	ErrWeekdayNotSupported = errors.New("weekday is not supported for time unit")
+	ErrTagsUnique          = func(tag string) error { return fmt.Errorf("a non-unique tag was set on the job: %s", tag) }
 )
 
 func wrapOrError(toWrap error, err error) error {
@@ -49,7 +50,8 @@ type timeUnit int
 
 const (
 	// default unit is seconds
-	seconds timeUnit = iota
+	milliseconds timeUnit = iota
+	seconds
 	minutes
 	hours
 	days
diff --git a/job.go b/job.go
index f92f395..162b169 100644
--- a/job.go
+++ b/job.go
@@ -19,7 +19,7 @@ type Job struct {
 	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
-	err               error         // error related to Job
+	error             error         // error related to Job
 	lastRun           time.Time     // datetime of last run
 	nextRun           time.Time     // datetime of next run
 	scheduledWeekday  *time.Weekday // Specific day of the week to start on
@@ -40,10 +40,9 @@ type jobFunction struct {
 }
 
 type runConfig struct {
-	finiteRuns         bool
-	maxRuns            int
-	mode               mode
-	removeAfterLastRun bool
+	finiteRuns bool
+	maxRuns    int
+	mode       mode
 }
 
 // mode is the Job's running mode
@@ -62,6 +61,7 @@ func NewJob(interval int) *Job {
 	ctx, cancel := context.WithCancel(context.Background())
 	return &Job{
 		interval: interval,
+		unit:     seconds,
 		lastRun:  time.Time{},
 		nextRun:  time.Time{},
 		jobFunction: jobFunction{
@@ -109,9 +109,11 @@ func (j *Job) setStartAtTime(t time.Time) {
 	j.startAtTime = t
 }
 
-// Err returns an error if one occurred while creating the Job
-func (j *Job) Err() error {
-	return j.err
+// Error returns an error if one occurred while creating the Job.
+// If multiple errors occurred, they will be wrapped and can be
+// checked using the standard unwrap options.
+func (j *Job) Error() error {
+	return j.error
 }
 
 // Tag allows you to add arbitrary labels to a Job that do not
@@ -184,16 +186,6 @@ func (j *Job) SingletonMode() {
 
 }
 
-// RemoveAfterLastRun sets the job to be removed after it's last run (when limited)
-func (j *Job) RemoveAfterLastRun() *Job {
-	j.runConfig.removeAfterLastRun = true
-	return j
-}
-
-func (j *Job) getRemoveAfterLastRun() bool {
-	return j.runConfig.removeAfterLastRun
-}
-
 // shouldRun evaluates if this job should run again
 // based on the runConfig
 func (j *Job) shouldRun() bool {
diff --git a/scheduler.go b/scheduler.go
index 79ed56c..b8b6aba 100644
--- a/scheduler.go
+++ b/scheduler.go
@@ -160,9 +160,7 @@ func (s *Scheduler) scheduleNextRun(job *Job) {
 	}
 
 	if !job.shouldRun() {
-		if job.getRemoveAfterLastRun() {
-			s.RemoveByReference(job)
-		}
+		s.RemoveByReference(job)
 		return
 	}
 
@@ -182,7 +180,7 @@ func (s *Scheduler) durationToNextRun(lastRun time.Time, job *Job) time.Duration
 
 	var d time.Duration
 	switch job.unit {
-	case seconds, minutes, hours:
+	case milliseconds, seconds, minutes, hours:
 		d = s.calculateDuration(job)
 	case days:
 		d = s.calculateDays(job, lastRun)
@@ -282,6 +280,8 @@ func (s *Scheduler) calculateDuration(job *Job) time.Duration {
 
 	interval := job.interval
 	switch job.unit {
+	case milliseconds:
+		return time.Duration(interval) * time.Millisecond
 	case seconds:
 		return time.Duration(interval) * time.Second
 	case minutes:
@@ -328,7 +328,7 @@ func (s *Scheduler) Every(interval interface{}) *Scheduler {
 	case int:
 		job := NewJob(interval)
 		if interval <= 0 {
-			job.err = wrapOrError(job.err, ErrInvalidInterval)
+			job.error = wrapOrError(job.error, ErrInvalidInterval)
 		}
 		s.setJobs(append(s.Jobs(), job))
 	case time.Duration:
@@ -340,14 +340,14 @@ func (s *Scheduler) Every(interval interface{}) *Scheduler {
 		job := NewJob(0)
 		d, err := time.ParseDuration(interval)
 		if err != nil {
-			job.err = wrapOrError(job.err, err)
+			job.error = wrapOrError(job.error, err)
 		}
 		job.duration = d
 		job.unit = duration
 		s.setJobs(append(s.Jobs(), job))
 	default:
 		job := NewJob(0)
-		job.err = wrapOrError(job.err, ErrInvalidIntervalType)
+		job.error = wrapOrError(job.error, ErrInvalidIntervalType)
 		s.setJobs(append(s.Jobs(), job))
 	}
 	return s
@@ -444,8 +444,8 @@ func removeAtIndex(jobs []*Job, i int) []*Job {
 	return jobs
 }
 
-// LimitRunsTo limits the number of executions of this job to n. However,
-// the job will still remain in the scheduler
+// LimitRunsTo limits the number of executions of this job to n.
+// Upon reaching the limit, the job is removed from the scheduler.
 func (s *Scheduler) LimitRunsTo(i int) *Scheduler {
 	job := s.getCurrentJob()
 	job.LimitRunsTo(i)
@@ -460,13 +460,6 @@ func (s *Scheduler) SingletonMode() *Scheduler {
 	return s
 }
 
-// RemoveAfterLastRun sets the job to be removed after it's last run (when limited)
-func (s *Scheduler) RemoveAfterLastRun() *Scheduler {
-	job := s.getCurrentJob()
-	job.RemoveAfterLastRun()
-	return s
-}
-
 // TaskPresent checks if specific job's function was added to the scheduler.
 func (s *Scheduler) TaskPresent(j interface{}) bool {
 	s.jobsMutex.RLock()
@@ -515,18 +508,18 @@ func (s *Scheduler) Do(jobFun interface{}, params ...interface{}) (*Job, error)
 	job := s.getCurrentJob()
 
 	if job.atTime != 0 && job.unit <= hours {
-		job.err = wrapOrError(job.err, ErrAtTimeNotSupported)
+		job.error = wrapOrError(job.error, ErrAtTimeNotSupported)
 	}
 
 	if job.scheduledWeekday != nil && job.unit != weeks {
-		job.err = wrapOrError(job.err, ErrWeekdayNotSupported)
+		job.error = wrapOrError(job.error, ErrWeekdayNotSupported)
 	}
 
-	if job.err != nil {
+	if job.error != nil {
 		// delete the job from the scheduler as this job
 		// cannot be executed
 		s.RemoveByReference(job)
-		return nil, job.err
+		return nil, job.error
 	}
 
 	typ := reflect.TypeOf(jobFun)
@@ -550,15 +543,24 @@ func (s *Scheduler) Do(jobFun interface{}, params ...interface{}) (*Job, error)
 }
 
 // At schedules the Job at a specific time of day in the form "HH:MM:SS" or "HH:MM"
-func (s *Scheduler) At(t string) *Scheduler {
+// or time.Time (note that only the hours, minutes, seconds and nanos are used).
+func (s *Scheduler) At(i interface{}) *Scheduler {
 	job := s.getCurrentJob()
-	hour, min, sec, err := parseTime(t)
-	if err != nil {
-		job.err = wrapOrError(job.err, err)
-		return s
+
+	switch t := i.(type) {
+	case string:
+		hour, min, sec, err := parseTime(t)
+		if err != nil {
+			job.error = wrapOrError(job.error, err)
+			return s
+		}
+		// save atTime start as duration from midnight
+		job.setAtTime(time.Duration(hour)*time.Hour + time.Duration(min)*time.Minute + time.Duration(sec)*time.Second)
+	case time.Time:
+		job.setAtTime(time.Duration(t.Hour())*time.Hour + time.Duration(t.Minute())*time.Minute + time.Duration(t.Second())*time.Second + time.Duration(t.Nanosecond())*time.Nanosecond)
+	default:
+		job.error = wrapOrError(job.error, ErrUnsupportedTimeFormat)
 	}
-	// save atTime start as duration from midnight
-	job.setAtTime(time.Duration(hour)*time.Hour + time.Duration(min)*time.Minute + time.Duration(sec)*time.Second)
 	job.startsImmediately = false
 	return s
 }
@@ -570,7 +572,7 @@ func (s *Scheduler) Tag(t ...string) *Scheduler {
 	if s.tags != nil {
 		for _, tag := range t {
 			if _, ok := s.tags[tag]; ok {
-				job.err = wrapOrError(job.err, ErrTagsUnique(tag))
+				job.error = wrapOrError(job.error, ErrTagsUnique(tag))
 				return s
 			}
 			s.tags[tag] = struct{}{}
@@ -594,11 +596,22 @@ func (s *Scheduler) StartAt(t time.Time) *Scheduler {
 func (s *Scheduler) setUnit(unit timeUnit) {
 	job := s.getCurrentJob()
 	if job.unit == duration {
-		job.err = wrapOrError(job.err, ErrInvalidIntervalUnitsSelection)
+		job.error = wrapOrError(job.error, ErrInvalidIntervalUnitsSelection)
 	}
 	job.unit = unit
 }
 
+// Second sets the unit with seconds
+func (s *Scheduler) Millisecond() *Scheduler {
+	return s.Milliseconds()
+}
+
+// Seconds sets the unit with seconds
+func (s *Scheduler) Milliseconds() *Scheduler {
+	s.setUnit(milliseconds)
+	return s
+}
+
 // Second sets the unit with seconds
 func (s *Scheduler) Second() *Scheduler {
 	return s.Seconds()


Test patch:
diff --git a/.github/workflows/go_test.yml b/.github/workflows/go_test.yml
index 1e976ba..7749fe4 100644
--- a/.github/workflows/go_test.yml
+++ b/.github/workflows/go_test.yml
@@ -12,8 +12,6 @@ jobs:
     strategy:
       matrix:
         go-version:
-          - 1.14
-          - 1.15
           - 1.16
     name: lint and test
     runs-on: ubuntu-latest
diff --git a/example_test.go b/example_test.go
index d7d4180..891ab9d 100644
--- a/example_test.go
+++ b/example_test.go
@@ -13,11 +13,11 @@ var task = func() {}
 // ----------------------JOB-FUNCTIONS----------------------------------
 // ---------------------------------------------------------------------
 
-func ExampleJob_Err() {
+func ExampleJob_Error() {
 	s := gocron.NewScheduler(time.UTC)
 	s.Every(1).Day().At("bad time")
 	j := s.Jobs()[0]
-	fmt.Println(j.Err())
+	fmt.Println(j.Error())
 	// Output:
 	// the given time format is not supported
 }
@@ -49,14 +49,6 @@ func ExampleJob_NextRun() {
 	s.StartAsync()
 }
 
-func ExampleJob_RemoveAfterLastRun() {
-	s := gocron.NewScheduler(time.UTC)
-	job, _ := s.Every(1).Second().Do(task)
-	job.LimitRunsTo(1)
-	job.RemoveAfterLastRun()
-	s.StartAsync()
-}
-
 func ExampleJob_RunCount() {
 	s := gocron.NewScheduler(time.UTC)
 	job, _ := s.Every(1).Second().Do(task)
@@ -361,19 +353,6 @@ func ExampleScheduler_Remove() {
 	// 0
 }
 
-func ExampleScheduler_RemoveAfterLastRun() {
-	s := gocron.NewScheduler(time.UTC)
-
-	j, _ := s.Every(1).Second().LimitRunsTo(1).RemoveAfterLastRun().Do(task)
-	s.StartAsync()
-	s.Stop()
-	fmt.Println(j.RunCount())
-	fmt.Println(s.Len())
-	// Output:
-	// 1
-	// 0
-}
-
 func ExampleScheduler_RemoveByReference() {
 	s := gocron.NewScheduler(time.UTC)
 
diff --git a/scheduler_test.go b/scheduler_test.go
index b2d7db7..3288ce9 100644
--- a/scheduler_test.go
+++ b/scheduler_test.go
@@ -793,7 +793,6 @@ func TestRunJobsWithLimit(t *testing.T) {
 		})
 		require.NoError(t, err)
 		j.LimitRunsTo(1)
-		j.RemoveAfterLastRun()
 
 		s.StartAsync()
 		time.Sleep(2 * time.Second)
@@ -1033,32 +1032,6 @@ func TestScheduler_SetMaxConcurrentJobs(t *testing.T) {
 	}
 }
 
-func TestScheduler_RemoveAfterLastRun(t *testing.T) {
-	t.Run("job removed after the last run", func(t *testing.T) {
-		semaphore := make(chan bool)
-
-		s := NewScheduler(time.UTC)
-
-		_, err := s.Every(1).Second().LimitRunsTo(1).RemoveAfterLastRun().Do(func() {
-			semaphore <- true
-		})
-		require.NoError(t, err)
-
-		s.StartAsync()
-		time.Sleep(2 * time.Second)
-
-		var counter int
-		select {
-		case <-time.After(2 * time.Second):
-			assert.Equal(t, 1, counter)
-			assert.Zero(t, s.Len())
-		case <-semaphore:
-			counter++
-			require.LessOrEqual(t, counter, 1)
-		}
-	})
-}
-
 func TestScheduler_TagsUnique(t *testing.T) {
 	const (
 		foo = "foo"