Add cross-cutting behavior to jobs using wrappers and chains. Job wrappers allow you to decorate jobs with additional functionality like panic recovery, execution control, and logging.
// JobWrapper decorates a Job with additional behavior
type JobWrapper func(Job) Job
// Chain is a sequence of JobWrappers that decorates submitted jobs
type Chain struct {
// contains unexported fields
}
type Job interface {
Run()
}// NewChain returns a Chain consisting of the given JobWrappers.
// Wrappers are applied in order, with the first wrapper being outermost.
func NewChain(c ...JobWrapper) ChainUsage:
import "github.com/robfig/cron/v3"
logger := cron.DefaultLogger
// Create a chain of wrappers
chain := cron.NewChain(
cron.Recover(logger), // Outermost: recovers panics
cron.SkipIfStillRunning(logger), // Innermost: skips if running
)Apply wrappers to all jobs via WithChain option:
// WithChain specifies Job wrappers to apply to all jobs
func WithChain(wrappers ...JobWrapper) OptionUsage:
logger := cron.DefaultLogger
c := cron.New(cron.WithChain(
cron.Recover(logger),
cron.SkipIfStillRunning(logger),
))
// All jobs added to this cron get wrapped automatically
c.AddFunc("@every 5s", func() {
// This job has panic recovery and skip-if-running behavior
doWork()
})Apply wrappers to individual jobs:
// Then decorates a job with all JobWrappers in the chain
func (c Chain) Then(j Job) JobUsage:
logger := cron.DefaultLogger
// Create cron without global chain
c := cron.New(cron.WithChain())
// Create wrapper chain
chain := cron.NewChain(
cron.Recover(logger),
cron.DelayIfStillRunning(logger),
)
// Wrap individual job
job := cron.FuncJob(func() {
doWork()
})
wrappedJob := chain.Then(job)
// Schedule the wrapped job
c.Schedule(cron.Every(5*time.Minute), wrappedJob)Recovers from panics in jobs and logs them.
// Recover panics in wrapped jobs and log them with the provided logger.
// Includes stack trace in error log.
func Recover(logger Logger) JobWrapperUsage:
logger := cron.VerbosePrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags))
c := cron.New(cron.WithChain(
cron.Recover(logger),
))
c.AddFunc("@every 1m", func() {
// If this panics, the panic is recovered and logged
// The cron scheduler continues running
riskyOperation()
})Without Recover:
// No panic recovery - a panic will crash the scheduler
c := cron.New(cron.WithChain())
c.AddFunc("@every 1m", func() {
panic("This will crash the entire scheduler!")
})Default Behavior:
New() includes Recover by default. To disable:
// Explicitly create cron without panic recovery
c := cron.New(cron.WithChain())Delays job execution if the previous run hasn't completed.
// DelayIfStillRunning serializes jobs, delaying subsequent runs until the
// previous one is complete. Jobs delayed by more than a minute have the
// delay logged at Info level.
func DelayIfStillRunning(logger Logger) JobWrapperUsage:
logger := cron.DefaultLogger
c := cron.New(cron.WithChain(
cron.Recover(logger),
cron.DelayIfStillRunning(logger),
))
c.AddFunc("@every 5s", func() {
fmt.Println("Starting long job...")
time.Sleep(10 * time.Second) // Takes 10 seconds
fmt.Println("Finished")
})
// Timeline:
// 0s - Job starts
// 5s - Schedule fires, but job still running, so wait
// 10s - Job finishes
// 10s - Delayed job starts immediately
// 15s - Schedule fires again, job still running
// 20s - Job finishes
// 20s - Delayed job startsDelay Logging:
If a job is delayed by more than 1 minute, the delay is logged:
// If job takes 90 seconds and runs every minute
c.AddFunc("@every 1m", func() {
time.Sleep(90 * time.Second)
})
// Log output:
// cron: delay, duration=30sSkips job execution if the previous run hasn't completed.
// SkipIfStillRunning skips an invocation of the Job if a previous invocation
// is still running. It logs skips to the given logger at Info level.
func SkipIfStillRunning(logger Logger) JobWrapperUsage:
logger := cron.DefaultLogger
c := cron.New(cron.WithChain(
cron.Recover(logger),
cron.SkipIfStillRunning(logger),
))
c.AddFunc("@every 5s", func() {
fmt.Println("Starting long job...")
time.Sleep(10 * time.Second)
fmt.Println("Finished")
})
// Timeline:
// 0s - Job starts
// 5s - Schedule fires, but job still running, so skip (logged)
// 10s - Job finishes
// 15s - Schedule fires, job not running, starts
// 20s - Schedule fires, job still running, skip
// 25s - Job finishesSkip Logging:
cron: skipUse Recover when:
Don't use Recover when:
Use DelayIfStillRunning when:
Use SkipIfStillRunning when:
Use neither when:
Implement custom job wrappers for your own cross-cutting concerns.
type JobWrapper func(Job) Job// WithTiming logs execution time of jobs
func WithTiming(logger cron.Logger) cron.JobWrapper {
return func(j cron.Job) cron.Job {
return cron.FuncJob(func() {
start := time.Now()
j.Run()
duration := time.Since(start)
logger.Info("job completed", "duration", duration)
})
}
}
// Usage
c := cron.New(cron.WithChain(
cron.Recover(logger),
WithTiming(logger),
))// WithTimeout enforces a maximum execution time
func WithTimeout(timeout time.Duration, logger cron.Logger) cron.JobWrapper {
return func(j cron.Job) cron.Job {
return cron.FuncJob(func() {
done := make(chan struct{})
go func() {
j.Run()
close(done)
}()
select {
case <-done:
// Job completed
case <-time.After(timeout):
logger.Error(fmt.Errorf("timeout"), "job exceeded timeout",
"timeout", timeout)
}
})
}
}
// Usage
c := cron.New(cron.WithChain(
cron.Recover(logger),
WithTimeout(5*time.Minute, logger),
))// WithContext provides context to jobs
type ContextJob interface {
RunWithContext(context.Context)
}
func WithContext(ctx context.Context) cron.JobWrapper {
return func(j cron.Job) cron.Job {
return cron.FuncJob(func() {
if ctxJob, ok := j.(ContextJob); ok {
ctxJob.RunWithContext(ctx)
} else {
j.Run()
}
})
}
}
// Define job with context
type MyJob struct {
Name string
}
func (j MyJob) Run() {
j.RunWithContext(context.Background())
}
func (j MyJob) RunWithContext(ctx context.Context) {
fmt.Printf("Running %s with context\n", j.Name)
// Use context for cancellation, deadlines, etc.
}
// Usage
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
c := cron.New(cron.WithChain(
WithContext(ctx),
))
c.AddJob("@every 1m", MyJob{Name: "ContextJob"})// WithMetrics records job execution metrics
func WithMetrics(name string, recorder MetricsRecorder) cron.JobWrapper {
return func(j cron.Job) cron.Job {
return cron.FuncJob(func() {
start := time.Now()
recorder.JobStarted(name)
defer func() {
duration := time.Since(start)
recorder.JobCompleted(name, duration)
}()
j.Run()
})
}
}
type MetricsRecorder interface {
JobStarted(name string)
JobCompleted(name string, duration time.Duration)
}Wrappers are composed in order, with earlier wrappers wrapping later ones:
chain := cron.NewChain(
wrapper1, // Outermost
wrapper2,
wrapper3, // Innermost
)
wrapped := chain.Then(job)
// Equivalent to:
// wrapped = wrapper1(wrapper2(wrapper3(job)))Execution Order:
chain := cron.NewChain(
LogBefore,
Recover,
LogAfter,
)
// Execution:
// 1. LogBefore (before)
// 2. Recover (before)
// 3. LogAfter (before)
// 4. Job.Run()
// 5. LogAfter (after)
// 6. Recover (after, if panic)
// 7. LogBefore (after)package main
import (
"fmt"
"log"
"os"
"time"
"github.com/robfig/cron/v3"
)
func main() {
logger := cron.VerbosePrintfLogger(
log.New(os.Stdout, "cron: ", log.LstdFlags),
)
c := cron.New(cron.WithChain(
cron.Recover(logger), // Always recover panics
cron.SkipIfStillRunning(logger), // Skip overlapping executions
))
// Job that might panic
c.AddFunc("@every 10s", func() {
fmt.Println("Risky job starting")
if time.Now().Unix()%3 == 0 {
panic("Random panic!")
}
fmt.Println("Risky job completed")
})
// Long-running job
c.AddFunc("@every 15s", func() {
fmt.Println("Long job starting")
time.Sleep(20 * time.Second)
fmt.Println("Long job completed")
})
c.Start()
defer c.Stop()
// Run for 2 minutes
time.Sleep(2 * time.Minute)
}logger := cron.VerbosePrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags))
c := cron.New(cron.WithChain(
cron.Recover(logger), // Always use in production
cron.SkipIfStillRunning(logger), // Prevent resource exhaustion
))Place wrappers in logical order:
// Good: Recover should be outermost to catch all panics
cron.WithChain(
cron.Recover(logger), // Outermost
WithMetrics("job"),
cron.SkipIfStillRunning(logger), // Innermost
)
// Bad: Inner wrappers' panics won't be caught
cron.WithChain(
WithMetrics("job"),
cron.SkipIfStillRunning(logger),
cron.Recover(logger), // Too deep
)WithChain) for common behavior// Global recovery and logging
c := cron.New(cron.WithChain(
cron.Recover(logger),
))
// Per-job execution control
longJob := cron.NewChain(
cron.DelayIfStillRunning(logger),
).Then(cron.FuncJob(func() {
doLongWork()
}))
quickJob := cron.NewChain(
cron.SkipIfStillRunning(logger),
).Then(cron.FuncJob(func() {
doQuickWork()
}))
c.Schedule(cron.Every(5*time.Minute), longJob)
c.Schedule(cron.Every(10*time.Second), quickJob)