Skip to content

Commit 13e883d

Browse files
committed
check context not nil, and update test and docs
1 parent 50966c7 commit 13e883d

File tree

5 files changed

+126
-0
lines changed

5 files changed

+126
-0
lines changed

errors.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ var (
3939
ErrWeeklyJobMinutesSeconds = fmt.Errorf("gocron: WeeklyJob: atTimes minutes and seconds must be between 0 and 59 inclusive")
4040
ErrPanicRecovered = fmt.Errorf("gocron: panic recovered")
4141
ErrWithClockNil = fmt.Errorf("gocron: WithClock: clock must not be nil")
42+
ErrWithContextNil = fmt.Errorf("gocron: WithContext: context must not be nil")
4243
ErrWithDistributedElectorNil = fmt.Errorf("gocron: WithDistributedElector: elector must not be nil")
4344
ErrWithDistributedLockerNil = fmt.Errorf("gocron: WithDistributedLocker: locker must not be nil")
4445
ErrWithDistributedJobLockerNil = fmt.Errorf("gocron: WithDistributedJobLocker: locker must not be nil")

example_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,21 @@ func ExampleNewScheduler() {
354354
fmt.Println(s.Jobs())
355355
}
356356

357+
func ExampleNewTask() {
358+
s, _ := gocron.NewScheduler()
359+
defer func() { _ = s.Shutdown() }()
360+
361+
_, _ = s.NewJob(
362+
gocron.DurationJob(time.Second),
363+
gocron.NewTask(
364+
func(ctx context.Context) {
365+
// gocron will pass in a context to the job and will cancel on shutdown.
366+
// this allows you to listen for and handle cancellation within your job.
367+
},
368+
),
369+
)
370+
}
371+
357372
func ExampleOneTimeJob() {
358373
s, _ := gocron.NewScheduler()
359374
defer func() { _ = s.Shutdown() }()
@@ -598,6 +613,27 @@ func ExampleWithClock() {
598613
// one, 2
599614
}
600615

616+
func ExampleWithContext() {
617+
s, _ := gocron.NewScheduler()
618+
defer func() { _ = s.Shutdown() }()
619+
620+
ctx, cancel := context.WithCancel(context.Background())
621+
defer cancel()
622+
623+
_, _ = s.NewJob(
624+
gocron.DurationJob(
625+
time.Second,
626+
),
627+
gocron.NewTask(
628+
func(ctx context.Context) {
629+
// gocron will pass in a context to the job and will cancel on shutdown.
630+
// this allows you to listen for and handle cancellation within your job.
631+
},
632+
),
633+
gocron.WithContext(ctx),
634+
)
635+
}
636+
601637
func ExampleWithDisabledDistributedJobLocker() {
602638
// var _ gocron.Locker = (*myLocker)(nil)
603639
//

job.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ type task struct {
8383
type Task func() task
8484

8585
// NewTask provides the job's task function and parameters.
86+
// If you set the first argument of your Task func to be a context.Context,
87+
// gocron will pass in a context to the job and will cancel on shutdown.
88+
// This allows you to listen for and handle cancellation within your job.
8689
func NewTask(function any, parameters ...any) Task {
8790
return func() task {
8891
return task{
@@ -705,8 +708,14 @@ func WithIdentifier(id uuid.UUID) JobOption {
705708
}
706709

707710
// WithContext sets the parent context for the job
711+
// If you set the first argument of your Task func to be a context.Context,
712+
// gocron will pass in a context to the job and will cancel on shutdown.
713+
// This allows you to listen for and handle cancellation within your job.
708714
func WithContext(ctx context.Context) JobOption {
709715
return func(j *internalJob, _ time.Time) error {
716+
if ctx == nil {
717+
return ErrWithContextNil
718+
}
710719
j.parentCtx = ctx
711720
return nil
712721
}

scheduler.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ type Scheduler interface {
2121
// NewJob creates a new job in the Scheduler. The job is scheduled per the provided
2222
// definition when the Scheduler is started. If the Scheduler is already running
2323
// the job will be scheduled when the Scheduler is started.
24+
// If you set the first argument of your Task func to be a context.Context,
25+
// gocron will pass in a context to the job and will cancel on shutdown.
26+
// This allows you to listen for and handle cancellation within your job.
2427
NewJob(JobDefinition, Task, ...JobOption) (Job, error)
2528
// RemoveByTags removes all jobs that have at least one of the provided tags.
2629
RemoveByTags(...string)

scheduler_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,77 @@ func TestScheduler_StopLongRunningJobs(t *testing.T) {
394394
require.NoError(t, s.StopJobs())
395395
time.Sleep(100 * time.Millisecond)
396396
})
397+
t.Run("start, run job, stop jobs before job is completed - manual context cancel", func(t *testing.T) {
398+
s := newTestScheduler(t,
399+
WithStopTimeout(50*time.Millisecond),
400+
)
401+
402+
ctx, cancel := context.WithCancel(context.Background())
403+
404+
_, err := s.NewJob(
405+
DurationJob(
406+
50*time.Millisecond,
407+
),
408+
NewTask(
409+
func(ctx context.Context) {
410+
select {
411+
case <-ctx.Done():
412+
case <-time.After(100 * time.Millisecond):
413+
t.Fatal("job can not been canceled")
414+
}
415+
}, ctx,
416+
),
417+
WithStartAt(
418+
WithStartImmediately(),
419+
),
420+
WithSingletonMode(LimitModeReschedule),
421+
)
422+
require.NoError(t, err)
423+
424+
s.Start()
425+
426+
time.Sleep(20 * time.Millisecond)
427+
// the running job is canceled, no unexpected timeout error
428+
cancel()
429+
require.NoError(t, s.StopJobs())
430+
time.Sleep(100 * time.Millisecond)
431+
})
432+
t.Run("start, run job, stop jobs before job is completed - manual context cancel WithContext", func(t *testing.T) {
433+
s := newTestScheduler(t,
434+
WithStopTimeout(50*time.Millisecond),
435+
)
436+
437+
ctx, cancel := context.WithCancel(context.Background())
438+
439+
_, err := s.NewJob(
440+
DurationJob(
441+
50*time.Millisecond,
442+
),
443+
NewTask(
444+
func(ctx context.Context) {
445+
select {
446+
case <-ctx.Done():
447+
case <-time.After(100 * time.Millisecond):
448+
t.Fatal("job can not been canceled")
449+
}
450+
},
451+
),
452+
WithStartAt(
453+
WithStartImmediately(),
454+
),
455+
WithSingletonMode(LimitModeReschedule),
456+
WithContext(ctx),
457+
)
458+
require.NoError(t, err)
459+
460+
s.Start()
461+
462+
time.Sleep(20 * time.Millisecond)
463+
// the running job is canceled, no unexpected timeout error
464+
cancel()
465+
require.NoError(t, s.StopJobs())
466+
time.Sleep(100 * time.Millisecond)
467+
})
397468
}
398469

399470
func TestScheduler_Shutdown(t *testing.T) {
@@ -576,6 +647,12 @@ func TestScheduler_NewJobErrors(t *testing.T) {
576647
nil,
577648
ErrCronJobInvalid,
578649
},
650+
{
651+
"context nil",
652+
DurationJob(time.Second),
653+
[]JobOption{WithContext(nil)}, //nolint:staticcheck
654+
ErrWithContextNil,
655+
},
579656
{
580657
"duration job time interval is zero",
581658
DurationJob(0 * time.Second),

0 commit comments

Comments
 (0)