Skip to content

Commit 3b653b9

Browse files
authored
fix nextRun with singleton mode reporting incorrect time (#705)
* fix nextRun with singleton mode reporting incorrect time * only remove past if >1, sort next scheduled * update test, remove no longer needed lastScheduledRun
1 parent f021cc4 commit 3b653b9

File tree

4 files changed

+96
-10
lines changed

4 files changed

+96
-10
lines changed

job.go

+11-3
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@ type internalJob struct {
2424
name string
2525
tags []string
2626
jobSchedule
27-
lastScheduledRun time.Time
28-
nextScheduled time.Time
27+
28+
// as some jobs may queue up, it's possible to
29+
// have multiple nextScheduled times
30+
nextScheduled []time.Time
31+
2932
lastRun time.Time
3033
function any
3134
parameters []any
@@ -894,7 +897,12 @@ func (j job) NextRun() (time.Time, error) {
894897
if ij == nil || ij.id == uuid.Nil {
895898
return time.Time{}, ErrJobNotFound
896899
}
897-
return ij.nextScheduled, nil
900+
if len(ij.nextScheduled) == 0 {
901+
return time.Time{}, nil
902+
}
903+
// the first element is the next scheduled run with subsequent
904+
// runs following after in the slice
905+
return ij.nextScheduled[0], nil
898906
}
899907

900908
func (j job) Tags() []string {

job_test.go

+56
Original file line numberDiff line numberDiff line change
@@ -492,3 +492,59 @@ func TestWithEventListeners(t *testing.T) {
492492
})
493493
}
494494
}
495+
496+
func TestJob_NextRun(t *testing.T) {
497+
tests := []struct {
498+
name string
499+
f func()
500+
}{
501+
{
502+
"simple",
503+
func() {},
504+
},
505+
{
506+
"sleep 3 seconds",
507+
func() {
508+
time.Sleep(300 * time.Millisecond)
509+
},
510+
},
511+
}
512+
513+
for _, tt := range tests {
514+
t.Run(tt.name, func(t *testing.T) {
515+
testTime := time.Now()
516+
517+
s := newTestScheduler(t)
518+
519+
// run a job every 10 milliseconds that starts 10 milliseconds after the current time
520+
j, err := s.NewJob(
521+
DurationJob(
522+
100*time.Millisecond,
523+
),
524+
NewTask(
525+
func() {},
526+
),
527+
WithStartAt(WithStartDateTime(testTime.Add(100*time.Millisecond))),
528+
WithSingletonMode(LimitModeReschedule),
529+
)
530+
require.NoError(t, err)
531+
532+
s.Start()
533+
nextRun, err := j.NextRun()
534+
require.NoError(t, err)
535+
536+
assert.Equal(t, testTime.Add(100*time.Millisecond), nextRun)
537+
538+
time.Sleep(150 * time.Millisecond)
539+
540+
nextRun, err = j.NextRun()
541+
assert.NoError(t, err)
542+
543+
assert.Equal(t, testTime.Add(200*time.Millisecond), nextRun)
544+
assert.Equal(t, 200*time.Millisecond, nextRun.Sub(testTime))
545+
546+
err = s.Shutdown()
547+
require.NoError(t, err)
548+
})
549+
}
550+
}

scheduler.go

+28-6
Original file line numberDiff line numberDiff line change
@@ -298,9 +298,18 @@ func (s *scheduler) selectExecJobsOutForRescheduling(id uuid.UUID) {
298298
// so we don't need to reschedule it.
299299
return
300300
}
301-
j.lastScheduledRun = j.nextScheduled
302-
303-
next := j.next(j.lastScheduledRun)
301+
var scheduleFrom time.Time
302+
if len(j.nextScheduled) > 0 {
303+
// always grab the last element in the slice as that is the furthest
304+
// out in the future and the time from which we want to calculate
305+
// the subsequent next run time.
306+
slices.SortStableFunc(j.nextScheduled, func(a, b time.Time) int {
307+
return a.Compare(b)
308+
})
309+
scheduleFrom = j.nextScheduled[len(j.nextScheduled)-1]
310+
}
311+
312+
next := j.next(scheduleFrom)
304313
if next.IsZero() {
305314
// the job's next function will return zero for OneTime jobs.
306315
// since they are one time only, they do not need rescheduling.
@@ -316,7 +325,7 @@ func (s *scheduler) selectExecJobsOutForRescheduling(id uuid.UUID) {
316325
next = j.next(next)
317326
}
318327
}
319-
j.nextScheduled = next
328+
j.nextScheduled = append(j.nextScheduled, next)
320329
j.timer = s.clock.AfterFunc(next.Sub(s.now()), func() {
321330
// set the actual timer on the job here and listen for
322331
// shut down events so that the job doesn't attempt to
@@ -340,6 +349,19 @@ func (s *scheduler) selectExecJobsOutCompleted(id uuid.UUID) {
340349
return
341350
}
342351

352+
// if the job has more than one nextScheduled time,
353+
// we need to remove any that are in the past.
354+
if len(j.nextScheduled) > 1 {
355+
var newNextScheduled []time.Time
356+
for _, t := range j.nextScheduled {
357+
if t.Before(s.now()) {
358+
continue
359+
}
360+
newNextScheduled = append(newNextScheduled, t)
361+
}
362+
j.nextScheduled = newNextScheduled
363+
}
364+
343365
// if the job has a limited number of runs set, we need to
344366
// check how many runs have occurred and stop running this
345367
// job if it has reached the limit.
@@ -400,7 +422,7 @@ func (s *scheduler) selectNewJob(in newJobIn) {
400422
}
401423
})
402424
}
403-
j.nextScheduled = next
425+
j.nextScheduled = append(j.nextScheduled, next)
404426
}
405427

406428
s.jobs[j.id] = j
@@ -451,7 +473,7 @@ func (s *scheduler) selectStart() {
451473
}
452474
})
453475
}
454-
j.nextScheduled = next
476+
j.nextScheduled = append(j.nextScheduled, next)
455477
s.jobs[id] = j
456478
}
457479
select {

scheduler_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1622,7 +1622,7 @@ func TestScheduler_RunJobNow(t *testing.T) {
16221622
WithSingletonMode(LimitModeReschedule),
16231623
},
16241624
func() time.Duration {
1625-
return 20 * time.Second
1625+
return 10 * time.Second
16261626
},
16271627
1,
16281628
},

0 commit comments

Comments
 (0)