A Golang job scheduling library that lets you run Go functions at pre-determined intervals using cron expressions, fixed durations, daily, weekly, monthly, or one-time schedules with support for distributed deployments.
Guide to implementing clean shutdown procedures for gocron applications.
Graceful shutdown ensures:
s, _ := gocron.NewScheduler()
defer s.Shutdown()
// Add jobs...
s.Start()
select {} // blocks, _ := gocron.NewScheduler()
defer s.Shutdown()
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
// Add jobs...
s.Start()
<-ctx.Done() // Wait for signal
log.Println("Shutting down...")
if err := s.Shutdown(); err != nil {
log.Printf("Shutdown error: %v", err)
}Configure how long to wait for running jobs:
s, _ := gocron.NewScheduler(
gocron.WithStopTimeout(30*time.Second),
)
defer s.Shutdown()Behavior:
Jobs should respect context cancellation:
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func(ctx context.Context) {
for {
select {
case <-ctx.Done():
log.Println("Job cancelled, cleaning up...")
cleanup()
return
default:
doWork()
time.Sleep(time.Second)
}
}
}),
)j, _ := s.NewJob(
gocron.DurationJob(time.Hour),
gocron.NewTask(func(ctx context.Context) error {
for i := 0; i < 1000; i++ {
select {
case <-ctx.Done():
// Save checkpoint
saveCheckpoint(i)
return ctx.Err()
default:
processItem(i)
}
}
return nil
}),
)j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func(ctx context.Context) error {
// Start work
workChan := make(chan workItem, 100)
go produceWork(ctx, workChan)
for {
select {
case <-ctx.Done():
// Finish current batch
drainQueue(workChan)
return ctx.Err()
case item := <-workChan:
processItem(item)
}
}
}),
)type jobContext struct {
db *sql.DB
}
func (c *jobContext) Close() error {
return c.db.Close()
}
// Setup
jc := &jobContext{db: openDB()}
defer jc.Close()
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func() {
// Use jc.db
}),
)j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func(ctx context.Context) error {
f, err := os.Open("data.txt")
if err != nil {
return err
}
defer f.Close()
// Process file
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
// Read and process
}
}
}),
)j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func(ctx context.Context) error {
conn, err := net.Dial("tcp", "example.com:80")
if err != nil {
return err
}
defer conn.Close()
// Use connection
select {
case <-ctx.Done():
return ctx.Err()
default:
// Network operations
}
return nil
}),
)func main() {
s, _ := gocron.NewScheduler(
gocron.WithStopTimeout(30*time.Second),
)
// Add jobs...
s.Start()
// Phase 1: Stop accepting new work
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
<-ctx.Done()
log.Println("Stopping scheduler...")
// Phase 2: Wait for completion
if err := s.Shutdown(); err != nil {
log.Printf("Shutdown error: %v", err)
}
// Phase 3: Cleanup
log.Println("Cleaning up resources...")
cleanup()
log.Println("Shutdown complete")
}func main() {
s1, _ := gocron.NewScheduler()
s2, _ := gocron.NewScheduler()
// Add jobs to both schedulers...
s1.Start()
s2.Start()
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
<-ctx.Done()
// Shutdown both in parallel
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
if err := s1.Shutdown(); err != nil {
log.Printf("S1 shutdown error: %v", err)
}
}()
go func() {
defer wg.Done()
if err := s2.Shutdown(); err != nil {
log.Printf("S2 shutdown error: %v", err)
}
}()
wg.Wait()
log.Println("All schedulers shutdown")
}func main() {
s, _ := gocron.NewScheduler(
gocron.WithStopTimeout(30*time.Second),
)
// Add jobs...
s.Start()
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
<-ctx.Done()
// Try graceful shutdown with timeout
done := make(chan bool)
go func() {
if err := s.Shutdown(); err != nil {
log.Printf("Shutdown error: %v", err)
}
done <- true
}()
select {
case <-done:
log.Println("Graceful shutdown complete")
case <-time.After(60 * time.Second):
log.Println("Shutdown timeout, forcing exit")
os.Exit(1)
}
}var shuttingDown atomic.Bool
func main() {
s, _ := gocron.NewScheduler()
// Health endpoint
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
if shuttingDown.Load() {
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte("shutting down"))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
go http.ListenAndServe(":8080", nil)
// Add jobs...
s.Start()
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
<-ctx.Done()
// Mark as shutting down
shuttingDown.Store(true)
// Wait for health checks to update
time.Sleep(time.Second)
// Shutdown
s.Shutdown()
}apiVersion: v1
kind: Pod
spec:
containers:
- name: app
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]func main() {
s, _ := gocron.NewScheduler(
gocron.WithStopTimeout(30*time.Second),
)
// Add jobs...
s.Start()
// Handle SIGTERM (K8s sends this)
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM)
defer cancel()
<-ctx.Done()
log.Println("Received SIGTERM, shutting down...")
if err := s.Shutdown(); err != nil {
log.Printf("Shutdown error: %v", err)
os.Exit(1)
}
log.Println("Shutdown complete")
}func TestGracefulShutdown(t *testing.T) {
s, _ := gocron.NewScheduler(
gocron.WithStopTimeout(5*time.Second),
)
var executed atomic.Bool
var cleaned atomic.Bool
j, _ := s.NewJob(
gocron.DurationJob(100*time.Millisecond),
gocron.NewTask(func(ctx context.Context) {
executed.Store(true)
time.Sleep(time.Second)
select {
case <-ctx.Done():
cleaned.Store(true)
default:
}
}),
)
s.Start()
time.Sleep(200 * time.Millisecond) // Let job start
err := s.Shutdown()
assert.NoError(t, err)
assert.True(t, executed.Load())
assert.True(t, cleaned.Load())
}s, _ := gocron.NewScheduler(
gocron.WithStopTimeout(30*time.Second),
)gocron.NewTask(func(ctx context.Context) {
// Check ctx.Done()
})j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func(ctx context.Context) error {
defer cleanup()
select {
case <-ctx.Done():
return ctx.Err()
default:
return doWork()
}
}),
)<-ctx.Done()
log.Println("Shutdown initiated")
if err := s.Shutdown(); err != nil {
log.Printf("Shutdown error: %v", err)
} else {
log.Println("Shutdown complete")
}Test graceful shutdown, timeout scenarios, and cleanup logic.
Cause: Jobs don't check context.
Solution: Always check ctx.Done() in loops.
Cause: Jobs blocking indefinitely.
Solution: Set appropriate timeout.
Cause: Missing cleanup in jobs.
Solution: Use defer for cleanup.
Install with Tessl CLI
npx tessl i tessl/golang-github-com-go-co-op-gocron-v2@2.19.1docs
api
examples
guides