From f60345f5617088951b3498ff640242b4977c1e57 Mon Sep 17 00:00:00 2001
From: John Roesler <johnrroesler@gmail.com>
Date: Tue, 13 Apr 2021 09:54:09 -0500
Subject: [PATCH 1/4] handle occasional occurence of double run on daily job

---
 scheduler.go      | 33 +++++++++++++++++++--------------
 scheduler_test.go |  6 ++++++
 2 files changed, 25 insertions(+), 14 deletions(-)

diff --git a/scheduler.go b/scheduler.go
index 1274cde6..785498d9 100644
--- a/scheduler.go
+++ b/scheduler.go
@@ -213,31 +213,31 @@ func (s *Scheduler) calculateMonths(job *Job, lastRun time.Time) time.Duration {
 		daysDifference := int(math.Abs(lastRun.Sub(jobDay).Hours()) / 24)
 		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)
+			nextRun = nextRun.AddDate(0, job.interval, -daysDifference)
 		} else {
 			if job.interval == 1 { // every month counts current month
-				nextRun = nextRun.AddDate(0, int(job.interval)-1, daysDifference)
+				nextRun = nextRun.AddDate(0, job.interval-1, daysDifference)
 			} else { // should run next month interval
-				nextRun = nextRun.AddDate(0, int(job.interval), daysDifference)
+				nextRun = nextRun.AddDate(0, job.interval, daysDifference)
 			}
 		}
-		return s.until(lastRun, nextRun)
+		return until(lastRun, nextRun)
 	}
-	nextRun := lastRunRoundedMidnight.Add(job.getAtTime()).AddDate(0, int(job.interval), 0)
-	return s.until(lastRunRoundedMidnight, nextRun)
+	nextRun := lastRunRoundedMidnight.Add(job.getAtTime()).AddDate(0, job.interval, 0)
+	return 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.getAtTime()).AddDate(0, 0, totalDaysDifference)
-	return s.until(lastRun, nextRun)
+	return 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.getAtTime()).AddDate(0, 0, totalDaysDifference)
-	return s.until(lastRun, nextRun)
+	return until(lastRun, nextRun)
 }
 
 func (s *Scheduler) calculateTotalDaysDifference(lastRun time.Time, daysToWeekday int, job *Job) int {
@@ -257,18 +257,23 @@ func (s *Scheduler) calculateTotalDaysDifference(lastRun time.Time, daysToWeekda
 }
 
 func (s *Scheduler) calculateDays(job *Job, lastRun time.Time) time.Duration {
+	// handle occasional occurrence of job running to quickly / too early such that last run was within a second of now
+	if lastRunUnix, nowUnix := lastRun.Unix(), s.time.Now(s.location).Unix(); lastRunUnix == nowUnix || lastRunUnix == nowUnix-1 || lastRunUnix == nowUnix+1 {
+		lastRun = time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, s.Location()).Add(job.getAtTime())
+	}
+
 	if job.interval == 1 {
 		lastRunDayPlusJobAtTime := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, s.Location()).Add(job.getAtTime())
 		if shouldRunToday(lastRun, lastRunDayPlusJobAtTime) {
-			return s.until(lastRun, s.roundToMidnight(lastRun).Add(job.getAtTime()))
+			return until(lastRun, s.roundToMidnight(lastRun).Add(job.getAtTime()))
 		}
 	}
 
 	nextRunAtTime := s.roundToMidnight(lastRun).Add(job.getAtTime()).AddDate(0, 0, int(job.interval)).In(s.Location())
-	return s.until(lastRun, nextRunAtTime)
+	return until(lastRun, nextRunAtTime)
 }
 
-func (s *Scheduler) until(from time.Time, until time.Time) time.Duration {
+func until(from time.Time, until time.Time) time.Duration {
 	return until.Sub(from)
 }
 
@@ -476,7 +481,7 @@ func (s *Scheduler) RemoveByTag(tag string) error {
 }
 
 func (s *Scheduler) findJobsByTag(tag string) ([]*Job, error) {
-	jobs := []*Job{}
+	var jobs []*Job
 	for _, job := range s.Jobs() {
 		if strings.Contains(strings.Join(job.Tags(), " "), tag) {
 			jobs = append(jobs, job)
@@ -657,12 +662,12 @@ func (s *Scheduler) setUnit(unit schedulingUnit) {
 	job.setUnit(unit)
 }
 
-// Second sets the unit with seconds
+// Millisecond sets the unit with seconds
 func (s *Scheduler) Millisecond() *Scheduler {
 	return s.Milliseconds()
 }
 
-// Seconds sets the unit with seconds
+// Milliseconds sets the unit with seconds
 func (s *Scheduler) Milliseconds() *Scheduler {
 	s.setUnit(milliseconds)
 	return s
diff --git a/scheduler_test.go b/scheduler_test.go
index 0bc690d4..f53146dd 100644
--- a/scheduler_test.go
+++ b/scheduler_test.go
@@ -618,6 +618,10 @@ func TestScheduler_StartAt(t *testing.T) {
 }
 
 func TestScheduler_CalculateNextRun(t *testing.T) {
+	ft := fakeTime{onNow: func(l *time.Location) time.Time {
+		return time.Date(1970, 1, 1, 12, 0, 0, 0, l)
+	}}
+
 	day := time.Hour * 24
 	januaryFirst2020At := func(hour, minute, second int) time.Time {
 		return time.Date(2020, time.January, 1, hour, minute, second, 0, time.UTC)
@@ -649,6 +653,7 @@ func TestScheduler_CalculateNextRun(t *testing.T) {
 		{name: "daily job just ran at 5:30AM and should be scheduled for today at 8:30AM", job: &Job{interval: 1, unit: days, atTime: 8*time.Hour + 30*time.Minute, lastRun: januaryFirst2020At(5, 30, 0)}, wantTimeUntilNextRun: 3 * time.Hour},
 		{name: "job runs every 2 days, just ran at 5:30AM and should be scheduled for 2 days at 8:30AM", job: &Job{interval: 2, unit: days, atTime: 8*time.Hour + 30*time.Minute, lastRun: januaryFirst2020At(5, 30, 0)}, wantTimeUntilNextRun: (2 * day) + 3*time.Hour},
 		{name: "job runs every 2 days, just ran at 8:30AM and should be scheduled for 2 days at 8:30AM", job: &Job{interval: 2, unit: days, atTime: 8*time.Hour + 30*time.Minute, lastRun: januaryFirst2020At(8, 30, 0)}, wantTimeUntilNextRun: 2 * day},
+		{name: "daily, last run was 1 second ago", job: &Job{interval: 1, unit: days, atTime: 12 * time.Hour, lastRun: ft.Now(time.UTC).Add(-time.Second)}, wantTimeUntilNextRun: 1 * day},
 		//// WEEKS
 		{name: "every week should run in 7 days", job: &Job{interval: 1, unit: weeks, lastRun: januaryFirst2020At(0, 0, 0)}, wantTimeUntilNextRun: 7 * day},
 		{name: "every week with .At time rule should run respect .At time rule", job: &Job{interval: 1, atTime: _getHours(9) + _getMinutes(30), unit: weeks, lastRun: januaryFirst2020At(9, 30, 0)}, wantTimeUntilNextRun: 7 * day},
@@ -679,6 +684,7 @@ func TestScheduler_CalculateNextRun(t *testing.T) {
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
 			s := NewScheduler(time.UTC)
+			s.time = ft
 			got := s.durationToNextRun(tc.job.LastRun(), tc.job)
 			assert.Equalf(t, tc.wantTimeUntilNextRun, got, fmt.Sprintf("expected %s / got %s", tc.wantTimeUntilNextRun.String(), got.String()))
 		})

From 0bfcdc2506e6d63682bec5191daf00b2fd089316 Mon Sep 17 00:00:00 2001
From: John Roesler <johnrroesler@gmail.com>
Date: Tue, 13 Apr 2021 10:33:48 -0500
Subject: [PATCH 2/4] use job.LastRun() instead of lastRun

---
 scheduler.go      | 11 +++++++----
 scheduler_test.go |  2 ++
 2 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/scheduler.go b/scheduler.go
index 785498d9..9284268f 100644
--- a/scheduler.go
+++ b/scheduler.go
@@ -258,18 +258,21 @@ func (s *Scheduler) calculateTotalDaysDifference(lastRun time.Time, daysToWeekda
 
 func (s *Scheduler) calculateDays(job *Job, lastRun time.Time) time.Duration {
 	// handle occasional occurrence of job running to quickly / too early such that last run was within a second of now
-	if lastRunUnix, nowUnix := lastRun.Unix(), s.time.Now(s.location).Unix(); lastRunUnix == nowUnix || lastRunUnix == nowUnix-1 || lastRunUnix == nowUnix+1 {
-		lastRun = time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, s.Location()).Add(job.getAtTime())
-	}
+	lastRunUnix, nowUnix := job.LastRun().Unix(), s.time.Now(s.location).Unix()
 
 	if job.interval == 1 {
 		lastRunDayPlusJobAtTime := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, s.Location()).Add(job.getAtTime())
+
+		if lastRunUnix == nowUnix || lastRunUnix == nowUnix-1 || lastRunUnix == nowUnix+1 {
+			lastRun = lastRunDayPlusJobAtTime
+		}
+
 		if shouldRunToday(lastRun, lastRunDayPlusJobAtTime) {
 			return until(lastRun, s.roundToMidnight(lastRun).Add(job.getAtTime()))
 		}
 	}
 
-	nextRunAtTime := s.roundToMidnight(lastRun).Add(job.getAtTime()).AddDate(0, 0, int(job.interval)).In(s.Location())
+	nextRunAtTime := s.roundToMidnight(lastRun).Add(job.getAtTime()).AddDate(0, 0, job.interval).In(s.Location())
 	return until(lastRun, nextRunAtTime)
 }
 
diff --git a/scheduler_test.go b/scheduler_test.go
index f53146dd..b040c0a3 100644
--- a/scheduler_test.go
+++ b/scheduler_test.go
@@ -228,6 +228,8 @@ func TestAt(t *testing.T) {
 
 		select {
 		case <-time.After(1 * time.Second):
+			log.Println(now.Add(time.Minute))
+			log.Println(dayJob.nextRun)
 			assert.Equal(t, now.Add(1*time.Minute), dayJob.nextRun)
 		case <-semaphore:
 			t.Fatal("job ran even though scheduled in future")

From 0dd25d6a1f45d6d1325e0f39d8b4e8cae99cf5fc Mon Sep 17 00:00:00 2001
From: John Roesler <johnrroesler@gmail.com>
Date: Tue, 13 Apr 2021 10:34:25 -0500
Subject: [PATCH 3/4] reorder code

---
 scheduler.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/scheduler.go b/scheduler.go
index 9284268f..56248719 100644
--- a/scheduler.go
+++ b/scheduler.go
@@ -257,12 +257,12 @@ func (s *Scheduler) calculateTotalDaysDifference(lastRun time.Time, daysToWeekda
 }
 
 func (s *Scheduler) calculateDays(job *Job, lastRun time.Time) time.Duration {
-	// handle occasional occurrence of job running to quickly / too early such that last run was within a second of now
-	lastRunUnix, nowUnix := job.LastRun().Unix(), s.time.Now(s.location).Unix()
 
 	if job.interval == 1 {
 		lastRunDayPlusJobAtTime := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, s.Location()).Add(job.getAtTime())
 
+		// handle occasional occurrence of job running to quickly / too early such that last run was within a second of now
+		lastRunUnix, nowUnix := job.LastRun().Unix(), s.time.Now(s.location).Unix()
 		if lastRunUnix == nowUnix || lastRunUnix == nowUnix-1 || lastRunUnix == nowUnix+1 {
 			lastRun = lastRunDayPlusJobAtTime
 		}

From e094dd92103fe22bf92d03e1f594c3eeca0e29b3 Mon Sep 17 00:00:00 2001
From: John Roesler <johnrroesler@gmail.com>
Date: Tue, 13 Apr 2021 10:52:25 -0500
Subject: [PATCH 4/4] Update scheduler.go

Co-authored-by: streppel <streppels@gmail.com>
---
 scheduler.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/scheduler.go b/scheduler.go
index 5d026d10..b436a133 100644
--- a/scheduler.go
+++ b/scheduler.go
@@ -263,7 +263,7 @@ func (s *Scheduler) calculateDays(job *Job, lastRun time.Time) time.Duration {
 		lastRunDayPlusJobAtTime := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, s.Location()).Add(job.getAtTime())
 
 		// handle occasional occurrence of job running to quickly / too early such that last run was within a second of now
-		lastRunUnix, nowUnix := job.LastRun().Unix(), s.time.Now(s.location).Unix()
+		lastRunUnix, nowUnix := job.LastRun().Unix(), s.now().Unix()
 		if lastRunUnix == nowUnix || lastRunUnix == nowUnix-1 || lastRunUnix == nowUnix+1 {
 			lastRun = lastRunDayPlusJobAtTime
 		}