add another out channel so we can properly report lastRun (#700)

* add another out channel so we can properly report lastRun

* don't block on channel send

* add tests
This commit is contained in:
John Roesler
2024-03-26 09:55:21 -05:00
committed by GitHub
parent 5b1cf9c619
commit 9ae7545c27
4 changed files with 153 additions and 49 deletions
+17 -12
View File
@@ -15,7 +15,8 @@ type executor struct {
logger Logger logger Logger
stopCh chan struct{} stopCh chan struct{}
jobsIn chan jobIn jobsIn chan jobIn
jobIDsOut chan uuid.UUID jobsOutForRescheduling chan uuid.UUID
jobsOutCompleted chan uuid.UUID
jobOutRequest chan jobOutRequest jobOutRequest chan jobOutRequest
stopTimeout time.Duration stopTimeout time.Duration
done chan error done chan error
@@ -122,7 +123,7 @@ func (e *executor) start() {
// all runners are busy, reschedule the work for later // all runners are busy, reschedule the work for later
// which means we just skip it here and do nothing // which means we just skip it here and do nothing
// TODO when metrics are added, this should increment a rescheduled metric // TODO when metrics are added, this should increment a rescheduled metric
e.sendOutToScheduler(&jIn) e.sendOutForRescheduling(&jIn)
} }
} else { } else {
// since we're not using LimitModeReschedule, but instead using LimitModeWait // since we're not using LimitModeReschedule, but instead using LimitModeWait
@@ -131,7 +132,7 @@ func (e *executor) start() {
// at which point this call would block. // at which point this call would block.
// TODO when metrics are added, this should increment a wait metric // TODO when metrics are added, this should increment a wait metric
e.limitMode.in <- jIn e.limitMode.in <- jIn
e.sendOutToScheduler(&jIn) e.sendOutForRescheduling(&jIn)
} }
} else { } else {
// no limit mode, so we're either running a regular job or // no limit mode, so we're either running a regular job or
@@ -167,17 +168,17 @@ func (e *executor) start() {
select { select {
case runner.rescheduleLimiter <- struct{}{}: case runner.rescheduleLimiter <- struct{}{}:
runner.in <- jIn runner.in <- jIn
e.sendOutToScheduler(&jIn) e.sendOutForRescheduling(&jIn)
default: default:
// runner is busy, reschedule the work for later // runner is busy, reschedule the work for later
// which means we just skip it here and do nothing // which means we just skip it here and do nothing
// TODO when metrics are added, this should increment a rescheduled metric // TODO when metrics are added, this should increment a rescheduled metric
e.sendOutToScheduler(&jIn) e.sendOutForRescheduling(&jIn)
} }
} else { } else {
// wait mode, fill up that queue (buffered channel, so it's ok) // wait mode, fill up that queue (buffered channel, so it's ok)
runner.in <- jIn runner.in <- jIn
e.sendOutToScheduler(&jIn) e.sendOutForRescheduling(&jIn)
} }
} else { } else {
select { select {
@@ -206,10 +207,10 @@ func (e *executor) start() {
} }
} }
func (e *executor) sendOutToScheduler(jIn *jobIn) { func (e *executor) sendOutForRescheduling(jIn *jobIn) {
if jIn.shouldSendOut { if jIn.shouldSendOut {
select { select {
case e.jobIDsOut <- jIn.id: case e.jobsOutForRescheduling <- jIn.id:
case <-e.ctx.Done(): case <-e.ctx.Done():
return return
} }
@@ -250,7 +251,7 @@ func (e *executor) limitModeRunner(name string, in chan jobIn, wg *waitGroupWith
return return
case <-j.ctx.Done(): case <-j.ctx.Done():
return return
case e.jobIDsOut <- j.id: case e.jobsOutForRescheduling <- j.id:
} }
} }
// remove the limiter block, as this particular job // remove the limiter block, as this particular job
@@ -331,20 +332,24 @@ func (e *executor) runJob(j internalJob, jIn jobIn) {
if e.elector != nil { if e.elector != nil {
if err := e.elector.IsLeader(j.ctx); err != nil { if err := e.elector.IsLeader(j.ctx); err != nil {
e.sendOutToScheduler(&jIn) e.sendOutForRescheduling(&jIn)
return return
} }
} else if e.locker != nil { } else if e.locker != nil {
lock, err := e.locker.Lock(j.ctx, j.name) lock, err := e.locker.Lock(j.ctx, j.name)
if err != nil { if err != nil {
e.sendOutToScheduler(&jIn) e.sendOutForRescheduling(&jIn)
return return
} }
defer func() { _ = lock.Unlock(j.ctx) }() defer func() { _ = lock.Unlock(j.ctx) }()
} }
_ = callJobFuncWithParams(j.beforeJobRuns, j.id, j.name) _ = callJobFuncWithParams(j.beforeJobRuns, j.id, j.name)
e.sendOutToScheduler(&jIn) e.sendOutForRescheduling(&jIn)
select {
case e.jobsOutCompleted <- j.id:
case <-e.ctx.Done():
}
startTime := time.Now() startTime := time.Now()
err := callJobFuncWithParams(j.function, j.parameters...) err := callJobFuncWithParams(j.function, j.parameters...)
+13 -11
View File
@@ -24,7 +24,9 @@ type internalJob struct {
name string name string
tags []string tags []string
jobSchedule jobSchedule
lastRun, nextRun time.Time lastScheduledRun time.Time
nextScheduled time.Time
lastRun time.Time
function any function any
parameters []any parameters []any
timer clockwork.Timer timer clockwork.Timer
@@ -681,18 +683,18 @@ func (d dailyJob) next(lastRun time.Time) time.Time {
func (d dailyJob) nextDay(lastRun time.Time, firstPass bool) time.Time { func (d dailyJob) nextDay(lastRun time.Time, firstPass bool) time.Time {
for _, at := range d.atTimes { for _, at := range d.atTimes {
// sub the at time hour/min/sec onto the lastRun's values // sub the at time hour/min/sec onto the lastScheduledRun's values
// to use in checks to see if we've got our next run time // to use in checks to see if we've got our next run time
atDate := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), at.Hour(), at.Minute(), at.Second(), lastRun.Nanosecond(), lastRun.Location()) atDate := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), at.Hour(), at.Minute(), at.Second(), lastRun.Nanosecond(), lastRun.Location())
if firstPass && atDate.After(lastRun) { if firstPass && atDate.After(lastRun) {
// checking to see if it is after i.e. greater than, // checking to see if it is after i.e. greater than,
// and not greater or equal as our lastRun day/time // and not greater or equal as our lastScheduledRun day/time
// will be in the loop, and we don't want to select it again // will be in the loop, and we don't want to select it again
return atDate return atDate
} else if !firstPass && !atDate.Before(lastRun) { } else if !firstPass && !atDate.Before(lastRun) {
// now that we're looking at the next day, it's ok to consider // now that we're looking at the next day, it's ok to consider
// the same at time that was last run (as lastRun has been incremented) // the same at time that was last run (as lastScheduledRun has been incremented)
return atDate return atDate
} }
} }
@@ -727,18 +729,18 @@ func (w weeklyJob) nextWeekDayAtTime(lastRun time.Time, firstPass bool) time.Tim
// weekDayDiff is used to add the correct amount to the atDate day below // weekDayDiff is used to add the correct amount to the atDate day below
weekDayDiff := wd - lastRun.Weekday() weekDayDiff := wd - lastRun.Weekday()
for _, at := range w.atTimes { for _, at := range w.atTimes {
// sub the at time hour/min/sec onto the lastRun's values // sub the at time hour/min/sec onto the lastScheduledRun's values
// to use in checks to see if we've got our next run time // to use in checks to see if we've got our next run time
atDate := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day()+int(weekDayDiff), at.Hour(), at.Minute(), at.Second(), lastRun.Nanosecond(), lastRun.Location()) atDate := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day()+int(weekDayDiff), at.Hour(), at.Minute(), at.Second(), lastRun.Nanosecond(), lastRun.Location())
if firstPass && atDate.After(lastRun) { if firstPass && atDate.After(lastRun) {
// checking to see if it is after i.e. greater than, // checking to see if it is after i.e. greater than,
// and not greater or equal as our lastRun day/time // and not greater or equal as our lastScheduledRun day/time
// will be in the loop, and we don't want to select it again // will be in the loop, and we don't want to select it again
return atDate return atDate
} else if !firstPass && !atDate.Before(lastRun) { } else if !firstPass && !atDate.Before(lastRun) {
// now that we're looking at the next week, it's ok to consider // now that we're looking at the next week, it's ok to consider
// the same at time that was last run (as lastRun has been incremented) // the same at time that was last run (as lastScheduledRun has been incremented)
return atDate return atDate
} }
} }
@@ -795,7 +797,7 @@ func (m monthlyJob) nextMonthDayAtTime(lastRun time.Time, days []int, firstPass
for _, day := range days { for _, day := range days {
if day >= lastRun.Day() { if day >= lastRun.Day() {
for _, at := range m.atTimes { for _, at := range m.atTimes {
// sub the day, and the at time hour/min/sec onto the lastRun's values // sub the day, and the at time hour/min/sec onto the lastScheduledRun's values
// to use in checks to see if we've got our next run time // to use in checks to see if we've got our next run time
atDate := time.Date(lastRun.Year(), lastRun.Month(), day, at.Hour(), at.Minute(), at.Second(), lastRun.Nanosecond(), lastRun.Location()) atDate := time.Date(lastRun.Year(), lastRun.Month(), day, at.Hour(), at.Minute(), at.Second(), lastRun.Nanosecond(), lastRun.Location())
@@ -807,12 +809,12 @@ func (m monthlyJob) nextMonthDayAtTime(lastRun time.Time, days []int, firstPass
if firstPass && atDate.After(lastRun) { if firstPass && atDate.After(lastRun) {
// checking to see if it is after i.e. greater than, // checking to see if it is after i.e. greater than,
// and not greater or equal as our lastRun day/time // and not greater or equal as our lastScheduledRun day/time
// will be in the loop, and we don't want to select it again // will be in the loop, and we don't want to select it again
return atDate return atDate
} else if !firstPass && !atDate.Before(lastRun) { } else if !firstPass && !atDate.Before(lastRun) {
// now that we're looking at the next month, it's ok to consider // now that we're looking at the next month, it's ok to consider
// the same at time that was lastRun (as lastRun has been incremented) // the same at time that was lastScheduledRun (as lastScheduledRun has been incremented)
return atDate return atDate
} }
} }
@@ -892,7 +894,7 @@ func (j job) NextRun() (time.Time, error) {
if ij == nil || ij.id == uuid.Nil { if ij == nil || ij.id == uuid.Nil {
return time.Time{}, ErrJobNotFound return time.Time{}, ErrJobNotFound
} }
return ij.nextRun, nil return ij.nextScheduled, nil
} }
func (j job) Tags() []string { func (j job) Tags() []string {
+22 -9
View File
@@ -110,7 +110,8 @@ func NewScheduler(options ...SchedulerOption) (Scheduler, error) {
logger: &noOpLogger{}, logger: &noOpLogger{},
jobsIn: make(chan jobIn), jobsIn: make(chan jobIn),
jobIDsOut: make(chan uuid.UUID), jobsOutForRescheduling: make(chan uuid.UUID),
jobsOutCompleted: make(chan uuid.UUID),
jobOutRequest: make(chan jobOutRequest, 1000), jobOutRequest: make(chan jobOutRequest, 1000),
done: make(chan error), done: make(chan error),
} }
@@ -147,8 +148,11 @@ func NewScheduler(options ...SchedulerOption) (Scheduler, error) {
s.logger.Info("gocron: new scheduler created") s.logger.Info("gocron: new scheduler created")
for { for {
select { select {
case id := <-s.exec.jobIDsOut: case id := <-s.exec.jobsOutForRescheduling:
s.selectExecJobIDsOut(id) s.selectExecJobsOutForRescheduling(id)
case id := <-s.exec.jobsOutCompleted:
s.selectExecJobsOutCompleted(id)
case in := <-s.newJobCh: case in := <-s.newJobCh:
s.selectNewJob(in) s.selectNewJob(in)
@@ -287,14 +291,14 @@ func (s *scheduler) selectRemoveJob(id uuid.UUID) {
// Jobs coming back from the executor to the scheduler that // Jobs coming back from the executor to the scheduler that
// need to evaluated for rescheduling. // need to evaluated for rescheduling.
func (s *scheduler) selectExecJobIDsOut(id uuid.UUID) { func (s *scheduler) selectExecJobsOutForRescheduling(id uuid.UUID) {
j, ok := s.jobs[id] j, ok := s.jobs[id]
if !ok { if !ok {
// the job was removed while it was running, and // the job was removed while it was running, and
// so we don't need to reschedule it. // so we don't need to reschedule it.
return return
} }
j.lastRun = j.nextRun j.lastScheduledRun = j.nextScheduled
// if the job has a limited number of runs set, we need to // if the job has a limited number of runs set, we need to
// check how many runs have occurred and stop running this // check how many runs have occurred and stop running this
@@ -313,7 +317,7 @@ func (s *scheduler) selectExecJobIDsOut(id uuid.UUID) {
} }
} }
next := j.next(j.lastRun) next := j.next(j.lastScheduledRun)
if next.IsZero() { if next.IsZero() {
// the job's next function will return zero for OneTime jobs. // the job's next function will return zero for OneTime jobs.
// since they are one time only, they do not need rescheduling. // since they are one time only, they do not need rescheduling.
@@ -329,7 +333,7 @@ func (s *scheduler) selectExecJobIDsOut(id uuid.UUID) {
next = j.next(next) next = j.next(next)
} }
} }
j.nextRun = next j.nextScheduled = next
j.timer = s.clock.AfterFunc(next.Sub(s.now()), func() { j.timer = s.clock.AfterFunc(next.Sub(s.now()), func() {
// set the actual timer on the job here and listen for // set the actual timer on the job here and listen for
// shut down events so that the job doesn't attempt to // shut down events so that the job doesn't attempt to
@@ -347,6 +351,15 @@ func (s *scheduler) selectExecJobIDsOut(id uuid.UUID) {
s.jobs[id] = j s.jobs[id] = j
} }
func (s *scheduler) selectExecJobsOutCompleted(id uuid.UUID) {
j, ok := s.jobs[id]
if !ok {
return
}
j.lastRun = s.now()
s.jobs[id] = j
}
func (s *scheduler) selectJobOutRequest(out jobOutRequest) { func (s *scheduler) selectJobOutRequest(out jobOutRequest) {
if j, ok := s.jobs[out.id]; ok { if j, ok := s.jobs[out.id]; ok {
select { select {
@@ -386,7 +399,7 @@ func (s *scheduler) selectNewJob(in newJobIn) {
} }
}) })
} }
j.nextRun = next j.nextScheduled = next
} }
s.jobs[j.id] = j s.jobs[j.id] = j
@@ -437,7 +450,7 @@ func (s *scheduler) selectStart() {
} }
}) })
} }
j.nextRun = next j.nextScheduled = next
s.jobs[id] = j s.jobs[id] = j
} }
select { select {
+85 -1
View File
@@ -1398,6 +1398,29 @@ func TestScheduler_RemoveLotsOfJobs(t *testing.T) {
} }
} }
func TestScheduler_RemoveJob_RemoveSelf(t *testing.T) {
goleak.VerifyNone(t)
s := newTestScheduler(t)
s.Start()
_, err := s.NewJob(
DurationJob(100*time.Millisecond),
NewTask(func() {}),
WithEventListeners(
BeforeJobRuns(
func(jobID uuid.UUID, jobName string) {
s.RemoveByTags("tag1")
},
),
),
WithTags("tag1"),
)
require.NoError(t, err)
time.Sleep(time.Millisecond * 400)
assert.NoError(t, s.Shutdown())
}
func TestScheduler_WithEventListeners(t *testing.T) { func TestScheduler_WithEventListeners(t *testing.T) {
goleak.VerifyNone(t) goleak.VerifyNone(t)
@@ -1673,6 +1696,67 @@ func TestScheduler_RunJobNow(t *testing.T) {
} }
} }
func TestScheduler_LastRunSingleton(t *testing.T) {
goleak.VerifyNone(t)
tests := []struct {
name string
f func(t *testing.T, j Job)
}{
{
"simple",
func(t *testing.T, j Job) {},
},
{
"with runNow",
func(t *testing.T, j Job) {
runTime := time.Now()
assert.NoError(t, j.RunNow())
// because we're using wait mode we need to wait here
// to make sure the job queued with RunNow has finished running
time.Sleep(time.Millisecond * 200)
lastRun, err := j.LastRun()
assert.NoError(t, err)
assert.LessOrEqual(t, lastRun.Sub(runTime), time.Millisecond*125)
assert.GreaterOrEqual(t, lastRun.Sub(runTime), time.Millisecond*75)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := newTestScheduler(t)
j, err := s.NewJob(
DurationJob(time.Millisecond*100),
NewTask(func() {
time.Sleep(time.Millisecond * 200)
}),
WithSingletonMode(LimitModeWait),
)
require.NoError(t, err)
startTime := time.Now()
s.Start()
lastRun, err := j.LastRun()
assert.NoError(t, err)
assert.True(t, lastRun.IsZero())
time.Sleep(time.Millisecond * 200)
lastRun, err = j.LastRun()
assert.NoError(t, err)
assert.LessOrEqual(t, lastRun.Sub(startTime), time.Millisecond*125)
assert.GreaterOrEqual(t, lastRun.Sub(startTime), time.Millisecond*75)
tt.f(t, j)
assert.NoError(t, s.Shutdown())
})
}
}
func TestScheduler_OneTimeJob(t *testing.T) { func TestScheduler_OneTimeJob(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -1796,7 +1880,7 @@ func TestScheduler_WithMonitor(t *testing.T) {
jobName string jobName string
}{ }{
{ {
"scheduler with monitorer", "scheduler with monitor",
DurationJob(time.Millisecond * 50), DurationJob(time.Millisecond * 50),
"job", "job",
}, },