Documentation
¶
Overview ¶
Package cron implements a cron spec parser and job runner.
Installation ¶
To download the package, run:
go get github.com/netresearch/go-cron
Import it in your program as:
import "github.com/netresearch/go-cron"
It requires Go 1.25 or later.
Usage ¶
Callers may register Funcs to be invoked on a given schedule. Cron will run them in their own goroutines.
c := cron.New()
c.AddFunc("30 * * * *", func() { fmt.Println("Every hour on the half hour") })
c.AddFunc("30 3-6,20-23 * * *", func() { fmt.Println(".. in the range 3-6am, 8-11pm") })
c.AddFunc("CRON_TZ=Asia/Tokyo 30 04 * * *", func() { fmt.Println("Runs at 04:30 Tokyo time every day") })
c.AddFunc("@hourly", func() { fmt.Println("Every hour, starting an hour from now") })
c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty, starting an hour thirty from now") })
c.Start()
..
// Funcs are invoked in their own goroutine, asynchronously.
...
// Funcs may also be added to a running Cron
c.AddFunc("@daily", func() { fmt.Println("Every day") })
..
// Inspect the cron job entries' next and previous run times.
inspect(c.Entries())
..
c.Stop() // Stop the scheduler (does not stop any jobs already running).
CRON Expression Format ¶
A cron expression represents a set of times, using 5 space-separated fields.
Field name | Mandatory? | Allowed values | Allowed special characters ---------- | ---------- | -------------- | -------------------------- Minutes | Yes | 0-59 | * / , - Hours | Yes | 0-23 | * / , - Day of month | Yes | 1-31 | * / , - ? Month | Yes | 1-12 or JAN-DEC | * / , - Day of week | Yes | 0-6 or SUN-SAT | * / , - ?
Month and Day-of-week field values are case insensitive. "SUN", "Sun", and "sun" are equally accepted.
The specific interpretation of the format is based on the Cron Wikipedia page: https://en.wikipedia.org/wiki/Cron
Alternative Formats ¶
Alternative Cron expression formats support other fields like seconds. You can implement that by creating a custom Parser as follows.
cron.New( cron.WithParser( cron.NewParser( cron.SecondOptional | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)))
Since adding Seconds is the most common modification to the standard cron spec, cron provides a builtin function to do that, which is equivalent to the custom parser you saw earlier, except that its seconds field is REQUIRED:
cron.New(cron.WithSeconds())
That emulates Quartz, the most popular alternative Cron schedule format: http://www.quartz-scheduler.org/documentation/quartz-2.x/tutorials/crontrigger.html
Special Characters ¶
Asterisk ( * )
The asterisk indicates that the cron expression will match for all values of the field; e.g., using an asterisk in the 5th field (month) would indicate every month.
Slash ( / )
Slashes are used to describe increments of ranges. For example 3-59/15 in the 1st field (minutes) would indicate the 3rd minute of the hour and every 15 minutes thereafter. The form "*\/..." is equivalent to the form "first-last/...", that is, an increment over the largest possible range of the field. The form "N/..." is accepted as meaning "N-MAX/...", that is, starting at N, use the increment until the end of that specific range. It does not wrap around.
Comma ( , )
Commas are used to separate items of a list. For example, using "MON,WED,FRI" in the 5th field (day of week) would mean Mondays, Wednesdays and Fridays.
Hyphen ( - )
Hyphens are used to define ranges. For example, 9-17 would indicate every hour between 9am and 5pm inclusive.
Question mark ( ? )
Question mark may be used instead of '*' for leaving either day-of-month or day-of-week blank.
Predefined schedules ¶
You may use one of several pre-defined schedules in place of a cron expression.
Entry | Description | Equivalent To ----- | ----------- | ------------- @yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 1 1 * @monthly | Run once a month, midnight, first of month | 0 0 1 * * @weekly | Run once a week, midnight between Sat/Sun | 0 0 * * 0 @daily (or @midnight) | Run once a day, midnight | 0 0 * * * @hourly | Run once an hour, beginning of hour | 0 * * * *
Intervals ¶
You may also schedule a job to execute at fixed intervals, starting at the time it's added or cron is run. This is supported by formatting the cron spec like this:
@every <duration>
where "duration" is a string accepted by time.ParseDuration (http://golang.org/pkg/time/#ParseDuration).
For example, "@every 1h30m10s" would indicate a schedule that activates after 1 hour, 30 minutes, 10 seconds, and then every interval after that.
Note: The interval does not take the job runtime into account. For example, if a job takes 3 minutes to run, and it is scheduled to run every 5 minutes, it will have only 2 minutes of idle time between each run.
Time zones ¶
By default, all interpretation and scheduling is done in the machine's local time zone (time.Local). You can specify a different time zone on construction:
cron.New(
cron.WithLocation(time.UTC))
Individual cron schedules may also override the time zone they are to be interpreted in by providing an additional space-separated field at the beginning of the cron spec, of the form "CRON_TZ=Asia/Tokyo".
For example:
# Runs at 6am in time.Local
cron.New().AddFunc("0 6 * * ?", ...)
# Runs at 6am in America/New_York
nyc, _ := time.LoadLocation("America/New_York")
c := cron.New(cron.WithLocation(nyc))
c.AddFunc("0 6 * * ?", ...)
# Runs at 6am in Asia/Tokyo
cron.New().AddFunc("CRON_TZ=Asia/Tokyo 0 6 * * ?", ...)
# Runs at 6am in Asia/Tokyo, overriding the cron's default location
tokyo, _ := time.LoadLocation("Asia/Tokyo")
c := cron.New(cron.WithLocation(tokyo))
c.AddFunc("0 6 * * ?", ...)
The prefix "TZ=(TIME ZONE)" is also supported for legacy compatibility.
Jobs scheduled during daylight-savings leap-ahead transitions will run immediately after the skipped hour (ISC cron-compatible behavior).
Daylight Saving Time (DST) Handling ¶
This library follows ISC cron-compatible DST behavior. Understanding these edge cases is critical for time-sensitive scheduling.
Spring Forward (clocks skip an hour):
- Jobs scheduled during the skipped hour run immediately after the transition
- Example: A 2:30 AM job during US spring DST runs at 3:00 AM
- Jobs scheduled exactly at the transition boundary may run immediately
Fall Back (clocks repeat an hour):
- Jobs run only during the first occurrence of the repeated hour
- The second occurrence is skipped to prevent duplicate runs
- ⚠️ Note: This means jobs scheduled in the repeated hour run once, not twice
Midnight Doesn't Exist:
- Some DST transitions skip midnight entirely (e.g., São Paulo, Brazil)
- Jobs scheduled at midnight run at the first valid time after transition
- This affects daily (@daily) and midnight-scheduled jobs in those timezones
30-Minute Offset Timezones:
- Some regions (e.g., Lord Howe Island, Australia) use 30-minute DST changes
- The same DST handling rules apply, but at 30-minute boundaries
⚠️ Important Edge Cases:
- Jobs during spring-forward gap: Run immediately after transition
- Jobs during fall-back repeat: Run only on first occurrence
- Multi-timezone systems: Each job uses its configured timezone independently
- Leap seconds: Not handled; use NTP-synced systems for best results
Testing DST scenarios:
// Use FakeClock for deterministic DST testing
loc, _ := time.LoadLocation("America/New_York")
// Start just before spring DST transition (2024: March 10, 2:00 AM)
clock := cron.NewFakeClock(time.Date(2024, 3, 10, 1, 59, 0, 0, loc))
c := cron.New(cron.WithClock(clock), cron.WithLocation(loc))
// ... test behavior
Best practices for DST-sensitive schedules:
- Use UTC (CRON_TZ=UTC) for critical jobs that must run exactly once
- Use explicit timezones (CRON_TZ=America/New_York) rather than local time
- Avoid scheduling jobs between 2:00-3:00 AM in DST-observing timezones
- Test with FakeClock around DST transitions before production deployment
- Consider using @every intervals for tasks where exact wall-clock time is less important
- Monitor job execution times during DST transition periods
Error Handling ¶
Jobs in go-cron signal failure by panicking rather than returning errors. This design:
- Keeps the Job interface simple (Run() has no return value)
- Enables consistent recovery and retry behavior via wrapper chains
- Allows adding retry/circuit-breaker logic without modifying job code
- Matches Go's convention of panicking for unrecoverable errors
Best practices:
- Use panic() for transient failures that should trigger retries
- Use log-and-continue for errors that shouldn't affect the next run
- Always wrap jobs with Recover() to prevent scheduler crashes
- Combine with RetryWithBackoff for automatic retry of transient failures
- Use CircuitBreaker to prevent hammering failing external services
Error flow through wrapper chain:
Recover → CircuitBreaker → RetryWithBackoff → Job ↑ ↑ ↑ │ │ │ └── catches ───┤ (panic) │ └── tracks/opens ────────────────┤ (panic) └── logs/swallows ───────────────────────────┘ (panic)
Job Wrappers ¶
A Cron runner may be configured with a chain of job wrappers to add cross-cutting functionality to all submitted jobs. For example, they may be used to achieve the following effects:
- Recover any panics from jobs
- Delay a job's execution if the previous run hasn't completed yet
- Skip a job's execution if the previous run hasn't completed yet
- Log each job's invocations
Install wrappers for all jobs added to a cron using the `cron.WithChain` option:
cron.New(cron.WithChain( cron.Recover(logger), // Recommended: recover panics to prevent crashes cron.SkipIfStillRunning(logger), ))
Install wrappers for individual jobs by explicitly wrapping them:
job = cron.NewChain( cron.SkipIfStillRunning(logger), ).Then(job)
Wrapper Composition Patterns ¶
Wrappers are applied in reverse order (outermost first). Understanding the correct ordering is critical for proper behavior:
Production-Ready Chain (recommended):
c := cron.New(cron.WithChain( cron.Recover(logger), // 1. Outermost: catches all panics cron.RetryWithBackoff(logger, 3, // 2. Retry transient failures time.Second, time.Minute, 2.0), cron.CircuitBreaker(logger, 5, // 3. Stop hammering failing services 5*time.Minute), cron.SkipIfStillRunning(logger), // 4. Innermost: prevent overlap ))
Context-Aware Chain (for graceful shutdown):
c := cron.New(cron.WithChain(
cron.Recover(logger),
cron.TimeoutWithContext(logger, 5*time.Minute),
))
c.AddJob("@every 1h", cron.FuncJobWithContext(func(ctx context.Context) {
select {
case <-ctx.Done():
return // Shutdown or timeout - exit gracefully
case <-doWork():
// Work completed
}
}))
Wrapper Ordering Pitfalls:
// BAD: Retry inside Recover loses panic information cron.NewChain(cron.RetryWithBackoff(...), cron.Recover(logger)) // GOOD: Recover catches re-panics from exhausted retries cron.NewChain(cron.Recover(logger), cron.RetryWithBackoff(...))
Available Wrappers:
- Recover: Catches panics and logs them
- SkipIfStillRunning: Skip if previous run is still active
- DelayIfStillRunning: Queue runs, serializing execution
- Timeout: Abandon long-running jobs (see caveats below)
- TimeoutWithContext: True cancellation via context
- RetryWithBackoff: Retry panicking jobs with exponential backoff
- CircuitBreaker: Stop execution after consecutive failures
Timeout Wrapper Caveats ¶
The Timeout wrapper uses an "abandonment model" - when a job exceeds its timeout, the wrapper returns but the job's goroutine continues running in the background. This design has important implications:
- The job is NOT canceled; it runs to completion even after timeout
- Resources held by the job are not released until the job naturally completes
- Side effects (database writes, API calls) still occur after timeout
- Multiple abandoned goroutines can accumulate if jobs consistently timeout
This is the only practical approach without context.Context support in the Job interface. For jobs that need true cancellation:
- Implement your own cancellation mechanism using channels or atomic flags
- Have your job check for cancellation signals at safe points
- Consider using shorter timeout values as a circuit breaker rather than for cancellation
Example of a cancellable job pattern:
type CancellableJob struct {
cancel chan struct{}
}
func (j *CancellableJob) Run() {
for {
select {
case <-j.cancel:
return // Clean exit on cancellation
default:
// Do work in small chunks
if done := doWorkChunk(); done {
return
}
}
}
}
Thread Safety ¶
Cron is safe for concurrent use. Multiple goroutines may call methods on a Cron instance simultaneously without external synchronization.
Specific guarantees:
- AddJob/AddFunc: Safe to call while scheduler is running
- Remove: Safe to call while scheduler is running
- Entries: Returns a snapshot; safe but may be stale
- Start/Stop: Safe to call multiple times (idempotent)
- Entry: Safe to call; returns copy of entry data
Job Execution:
- Jobs may run concurrently by default
- Use SkipIfStillRunning or DelayIfStillRunning for serialization
- Jobs should not block indefinitely (use Timeout or TimeoutWithContext)
The scheduler uses an internal channel-based synchronization model. All operations that modify scheduler state are serialized through this channel.
Logging ¶
Cron defines a Logger interface that is a subset of the one defined in github.com/go-logr/logr. It has two logging levels (Info and Error), and parameters are key/value pairs. This makes it possible for cron logging to plug into structured logging systems. An adapter, [Verbose]PrintfLogger, is provided to wrap the standard library *log.Logger.
For additional insight into Cron operations, verbose logging may be activated which will record job runs, scheduling decisions, and added or removed jobs. Activate it with a one-off logger as follows:
cron.New( cron.WithLogger( cron.VerbosePrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags))))
Resource Management ¶
Use WithMaxEntries to limit the number of scheduled jobs and prevent resource exhaustion:
c := cron.New(cron.WithMaxEntries(100))
id, err := c.AddFunc("@every 1m", myJob)
if errors.Is(err, cron.ErrMaxEntriesReached) {
// Handle limit reached - remove old jobs or reject new ones
}
Behavior when limit is reached:
- AddFunc, AddJob, ScheduleJob return ErrMaxEntriesReached
- Existing jobs continue running normally
- Counter decrements when jobs are removed via Remove(id)
The entry limit is checked atomically but may briefly exceed the limit during concurrent additions by the number of in-flight ScheduleJob calls.
Observability Hooks ¶
ObservabilityHooks provide integration points for metrics, tracing, and monitoring:
hooks := cron.ObservabilityHooks{
OnSchedule: func(entryID cron.EntryID, name string, nextRun time.Time) {
// Called when a job's next execution time is calculated
log.Printf("Job %d (%s) scheduled for %v", entryID, name, nextRun)
},
OnJobStart: func(entryID cron.EntryID, name string, scheduledTime time.Time) {
// Called just before a job starts running
metrics.IncrCounter("cron.job.started", "job", name)
},
OnJobComplete: func(entryID cron.EntryID, name string, duration time.Duration, recovered any) {
// Called after a job completes (successfully or with panic)
metrics.RecordDuration("cron.job.duration", duration, "job", name)
if recovered != nil {
metrics.IncrCounter("cron.job.panic", "job", name)
}
},
}
c := cron.New(cron.WithObservability(hooks))
Testing with FakeClock ¶
FakeClock enables deterministic time control for testing cron jobs:
func TestJobExecution(t *testing.T) {
clock := cron.NewFakeClock(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC))
c := cron.New(cron.WithClock(clock))
executed := make(chan struct{})
c.AddFunc("@every 1h", func() { close(executed) })
c.Start()
defer c.Stop()
// Advance time to trigger job
clock.Advance(time.Hour)
select {
case <-executed:
// Success
case <-time.After(time.Second):
t.Fatal("job not executed")
}
}
FakeClock methods:
- NewFakeClock(initial time.Time): Create clock at specific time
- Advance(d time.Duration): Move time forward, triggering timers
- Set(t time.Time): Jump to specific time
- BlockUntil(n int): Wait for n timers to be registered
- Now(), Since(), After(), AfterFunc(), NewTicker(), Sleep(): Standard time operations
Use BlockUntil for synchronization in tests with multiple timers:
clock.BlockUntil(2) // Wait for 2 timers to be registered clock.Advance(time.Hour) // Now safely advance
Security Considerations ¶
Input Validation:
- Cron specifications are limited to 1024 characters (MaxSpecLength)
- Timezone specifications are validated against Go's time.LoadLocation
- Path traversal attempts in timezone strings (e.g., "../etc/passwd") are rejected
Resource Protection:
- Use WithMaxEntries to limit scheduled jobs in multi-tenant environments
- Use WithMaxSearchYears to limit schedule search time for complex expressions
- Timeout wrappers prevent runaway jobs from consuming resources indefinitely
Recommended Patterns:
- Validate user-provided cron expressions before scheduling
- Use named jobs with duplicate prevention for user-defined schedules
- Monitor entry counts and job durations in production
- Run the cron service with minimal privileges
Migration from robfig/cron ¶
This library is a maintained fork of github.com/robfig/cron/v3 with full backward compatibility. To migrate:
// Before import "github.com/robfig/cron/v3" // After import "github.com/netresearch/go-cron"
New features available after migration:
- RetryWithBackoff: Automatic retry with exponential backoff
- CircuitBreaker: Protect failing jobs from overwhelming services
- TimeoutWithContext: True cancellation support via context
- ObservabilityHooks: Integrated metrics and tracing support
- FakeClock: Deterministic time control for testing
- WithMaxEntries: Resource protection for entry limits
- WithMaxSearchYears: Configurable schedule search limits
- Named jobs: Unique job names with duplicate prevention
- Tagged jobs: Categorization and bulk operations
- Context support: Graceful shutdown via context cancellation
All existing code will work unchanged. The migration is a drop-in replacement.
Implementation ¶
Cron entries are stored in a min-heap ordered by their next activation time, providing O(log n) insertion/removal and O(1) access to the next entry. Cron sleeps until the next job is due to be run.
Upon waking:
- it runs each entry that is active on that second
- it calculates the next run times for the jobs that were run
- it re-heapifies the entries by next activation time
- it goes to sleep until the soonest job.
Example ¶
This example demonstrates basic cron usage.
package main
import (
"fmt"
cron "github.com/netresearch/go-cron"
)
func main() {
c := cron.New()
// Add a job that runs every minute
c.AddFunc("* * * * *", func() {
fmt.Println("Every minute")
})
// Start the scheduler
c.Start()
// Stop the scheduler when done
c.Stop()
}
Index ¶
- Constants
- Variables
- type Chain
- type Clock
- type ConstantDelaySchedule
- type Cron
- func (c *Cron) AddFunc(spec string, cmd func(), opts ...JobOption) (EntryID, error)
- func (c *Cron) AddJob(spec string, cmd Job, opts ...JobOption) (EntryID, error)
- func (c *Cron) Entries() []Entry
- func (c *Cron) EntriesByTag(tag string) []Entry
- func (c *Cron) Entry(id EntryID) Entry
- func (c *Cron) EntryByName(name string) Entry
- func (c *Cron) Location() *time.Location
- func (c *Cron) Remove(id EntryID)
- func (c *Cron) RemoveByName(name string) bool
- func (c *Cron) RemoveByTag(tag string) int
- func (c *Cron) Run()
- func (c *Cron) Schedule(schedule Schedule, cmd Job) EntryIDdeprecated
- func (c *Cron) ScheduleJob(schedule Schedule, cmd Job, opts ...JobOption) (EntryID, error)
- func (c *Cron) Start()
- func (c *Cron) Stop() context.Context
- func (c *Cron) StopAndWait()
- func (c *Cron) StopWithTimeout(timeout time.Duration) bool
- type Entry
- type EntryID
- type FakeClock
- type FuncJob
- type FuncJobWithContext
- type Job
- type JobOption
- type JobWithContext
- type JobWrapper
- func CircuitBreaker(logger Logger, threshold int, cooldown time.Duration) JobWrapper
- func DelayIfStillRunning(logger Logger) JobWrapper
- func Recover(logger Logger, opts ...RecoverOption) JobWrapper
- func RetryWithBackoff(logger Logger, maxRetries int, initialDelay, maxDelay time.Duration, ...) JobWrapper
- func SkipIfStillRunning(logger Logger) JobWrapper
- func Timeout(logger Logger, timeout time.Duration, opts ...TimeoutOption) JobWrapper
- func TimeoutWithContext(logger Logger, timeout time.Duration, opts ...TimeoutOption) JobWrapper
- type LogLevel
- type Logger
- type NamedJob
- type ObservabilityHooks
- type Option
- func WithChain(wrappers ...JobWrapper) Option
- func WithClock(clock Clock) Option
- func WithContext(ctx context.Context) Option
- func WithLocation(loc *time.Location) Option
- func WithLogger(logger Logger) Option
- func WithMaxEntries(max int) Option
- func WithMaxSearchYears(years int) Option
- func WithMinEveryInterval(d time.Duration) Option
- func WithObservability(hooks ObservabilityHooks) Option
- func WithParser(p ScheduleParser) Option
- func WithSeconds() Option
- type PanicWithStack
- type ParseOption
- type Parser
- type RealClock
- type RecoverOption
- type Schedule
- type ScheduleParser
- type SlogLogger
- type SpecSchedule
- type TimeoutOption
- type Timer
Examples ¶
- Package
- CircuitBreaker
- Cron.AddFunc
- Cron.AddFunc (Timezone)
- Cron.AddJob
- Cron.Entries
- Cron.Remove
- Cron.Stop
- Every
- EveryWithMin
- NamedJob
- New
- New (WithLocation)
- New (WithSeconds)
- NewChain
- ParseStandard
- RetryWithBackoff
- RetryWithBackoff (NoRetries)
- Timeout
- Timeout (Cancellable)
- Timeout (WithContext)
- TimeoutWithContext
- VerbosePrintfLogger
- WithChain
- WithMaxEntries
- WithMinEveryInterval
- WithMinEveryInterval (RateLimit)
- WithObservability
Constants ¶
const MaxSpecLength = 1024
MaxSpecLength is the maximum allowed length for a cron spec string. This limit prevents potential resource exhaustion from extremely long inputs.
Variables ¶
var DefaultLogger = PrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags))
DefaultLogger is used by Cron if none is specified.
var DiscardLogger = PrintfLogger(log.New(io.Discard, "", 0))
DiscardLogger can be used by callers to discard all log messages.
var ErrDuplicateName = errors.New("cron: duplicate entry name")
ErrDuplicateName is returned when adding an entry with a name that already exists.
var ErrMaxEntriesReached = errors.New("cron: max entries limit reached")
ErrMaxEntriesReached is returned when adding an entry would exceed the configured maximum number of entries (see WithMaxEntries).
var ErrMultipleOptionals = fmt.Errorf("multiple optionals may not be configured")
ErrMultipleOptionals is returned when more than one optional field is configured.
var ErrNoFields = fmt.Errorf("at least one field or Descriptor must be configured")
ErrNoFields is returned when no fields or Descriptor are configured.
Functions ¶
This section is empty.
Types ¶
type Chain ¶
type Chain struct {
// contains filtered or unexported fields
}
Chain is a sequence of JobWrappers that decorates submitted jobs with cross-cutting behaviors like logging or synchronization.
func NewChain ¶
func NewChain(c ...JobWrapper) Chain
NewChain returns a Chain consisting of the given JobWrappers.
Example ¶
This example demonstrates wrapping individual jobs with chains.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
c := cron.New()
// Create a chain for specific jobs
chain := cron.NewChain(
cron.DelayIfStillRunning(cron.DefaultLogger),
)
// Wrap a job with the chain
wrappedJob := chain.Then(cron.FuncJob(func() {
fmt.Println("This job will queue if still running")
}))
c.Schedule(cron.Every(time.Minute), wrappedJob)
c.Start()
defer c.Stop()
}
type Clock ¶ added in v0.6.0
Clock provides time-related operations that can be mocked for testing. This interface allows deterministic testing of scheduled jobs by controlling time advancement and timer firing.
type ConstantDelaySchedule ¶
ConstantDelaySchedule represents a simple recurring duty cycle, e.g. "Every 5 minutes". It does not support jobs more frequent than once a second.
func Every ¶
func Every(duration time.Duration) ConstantDelaySchedule
Every returns a crontab Schedule that activates once every duration. Delays of less than a second are not supported (will round up to 1 second). Any fields less than a Second are truncated.
For custom minimum intervals, use EveryWithMin instead.
Example ¶
This example demonstrates creating a constant delay schedule.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
c := cron.New()
// Run every 5 minutes
c.Schedule(cron.Every(5*time.Minute), cron.FuncJob(func() {
fmt.Println("Every 5 minutes")
}))
c.Start()
defer c.Stop()
}
func EveryWithMin ¶ added in v0.6.0
func EveryWithMin(duration, minInterval time.Duration) ConstantDelaySchedule
EveryWithMin returns a crontab Schedule that activates once every duration, with a configurable minimum interval.
The minInterval parameter controls the minimum allowed duration:
- If minInterval > 0, durations below minInterval are rounded up to minInterval
- If minInterval <= 0, no minimum is enforced (allows sub-second intervals)
Any fields less than a Second are truncated unless minInterval allows sub-second.
Example:
// Standard usage (1 second minimum) sched := EveryWithMin(500*time.Millisecond, time.Second) // rounds to 1s // Sub-second intervals (for testing) sched := EveryWithMin(100*time.Millisecond, 0) // allows 100ms // Enforce minimum 1-minute intervals sched := EveryWithMin(30*time.Second, time.Minute) // rounds to 1m
Example ¶
This example demonstrates using EveryWithMin to create schedules with custom minimum intervals. This is useful for testing (sub-second) or rate limiting.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
c := cron.New()
// Allow sub-second intervals (useful for testing)
// The second parameter (0) disables the minimum interval check
schedule := cron.EveryWithMin(100*time.Millisecond, 0)
c.Schedule(schedule, cron.FuncJob(func() {
fmt.Println("Running every 100ms")
}))
// Enforce minimum 1-minute intervals (useful for rate limiting)
// If duration < minInterval, it's rounded up to minInterval
rateLimited := cron.EveryWithMin(30*time.Second, time.Minute)
c.Schedule(rateLimited, cron.FuncJob(func() {
fmt.Println("Running every minute (30s was rounded up)")
}))
c.Start()
defer c.Stop()
}
func (ConstantDelaySchedule) Next ¶
func (schedule ConstantDelaySchedule) Next(t time.Time) time.Time
Next returns the next time this should be run. For delays of 1 second or more, this rounds to the next second boundary. For sub-second delays, no rounding is performed.
If the delay is zero or negative (invalid), returns t + 1 second as a safe fallback to prevent CPU spin loops in the scheduler.
type Cron ¶
type Cron struct {
// contains filtered or unexported fields
}
Cron keeps track of any number of entries, invoking the associated func as specified by the schedule. It may be started, stopped, and the entries may be inspected while running.
Entries are stored in a min-heap ordered by next execution time, providing O(log n) insertion/removal and O(1) access to the next entry to run. An index map provides O(1) entry lookup by ID.
func New ¶
New returns a new Cron job runner, modified by the given options.
Available Settings
Time Zone Description: The time zone in which schedules are interpreted Default: time.Local Parser Description: Parser converts cron spec strings into cron.Schedules. Default: Accepts this spec: https://en.wikipedia.org/wiki/Cron Chain Description: Wrap submitted jobs to customize behavior. Default: A chain that recovers panics and logs them to stderr.
See "cron.With*" to modify the default behavior.
Example ¶
This example demonstrates creating a new Cron instance with default settings.
package main
import (
"fmt"
cron "github.com/netresearch/go-cron"
)
func main() {
c := cron.New()
c.AddFunc("@hourly", func() {
fmt.Println("Every hour")
})
c.Start()
defer c.Stop()
}
Example (WithLocation) ¶
This example demonstrates timezone-aware scheduling.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
nyc, _ := time.LoadLocation("America/New_York")
c := cron.New(cron.WithLocation(nyc))
// Run at 9 AM New York time
c.AddFunc("0 9 * * *", func() {
fmt.Println("Good morning, New York!")
})
c.Start()
defer c.Stop()
}
Example (WithSeconds) ¶
This example demonstrates using WithSeconds to enable second-granularity scheduling.
package main
import (
"fmt"
cron "github.com/netresearch/go-cron"
)
func main() {
// Enable seconds field (Quartz-style 6-field expressions)
c := cron.New(cron.WithSeconds())
// Run every 30 seconds
c.AddFunc("*/30 * * * * *", func() {
fmt.Println("Every 30 seconds")
})
c.Start()
defer c.Stop()
}
func (*Cron) AddFunc ¶
AddFunc adds a func to the Cron to be run on the given schedule. The spec is parsed using the time zone of this Cron instance as the default. An opaque ID is returned that can be used to later remove it.
Optional JobOption arguments can be provided to set metadata like Name and Tags:
c.AddFunc("@every 1h", cleanup, cron.WithName("cleanup"), cron.WithTags("maintenance"))
Returns ErrDuplicateName if a name is provided and already exists.
Example ¶
This example demonstrates adding a function to the cron scheduler.
package main
import (
"fmt"
"log"
cron "github.com/netresearch/go-cron"
)
func main() {
c := cron.New()
// Standard 5-field cron expression
_, err := c.AddFunc("30 * * * *", func() {
fmt.Println("Every hour at minute 30")
})
if err != nil {
log.Fatal(err)
}
// Using predefined schedule
_, err = c.AddFunc("@daily", func() {
fmt.Println("Once a day at midnight")
})
if err != nil {
log.Fatal(err)
}
// Using interval
_, err = c.AddFunc("@every 1h30m", func() {
fmt.Println("Every 1.5 hours")
})
if err != nil {
log.Fatal(err)
}
c.Start()
defer c.Stop()
}
Example (Timezone) ¶
This example demonstrates inline timezone specification.
package main
import (
"fmt"
cron "github.com/netresearch/go-cron"
)
func main() {
c := cron.New()
// Specify timezone inline with CRON_TZ prefix
c.AddFunc("CRON_TZ=Asia/Tokyo 0 9 * * *", func() {
fmt.Println("Good morning, Tokyo!")
})
// Legacy TZ prefix is also supported
c.AddFunc("TZ=Europe/London 0 17 * * *", func() {
fmt.Println("Good evening, London!")
})
c.Start()
defer c.Stop()
}
func (*Cron) AddJob ¶
AddJob adds a Job to the Cron to be run on the given schedule. The spec is parsed using the time zone of this Cron instance as the default. An opaque ID is returned that can be used to later remove it.
Optional JobOption arguments can be provided to set metadata like Name and Tags:
c.AddJob("@every 1h", myJob, cron.WithName("my-job"), cron.WithTags("critical"))
Returns ErrMaxEntriesReached if the maximum entry limit has been reached. Returns ErrDuplicateName if a name is provided and already exists.
Example ¶
This example demonstrates implementing the Job interface for complex job logic.
package main
import (
"fmt"
"log"
cron "github.com/netresearch/go-cron"
)
func main() {
c := cron.New()
// Define a job type
type cleanupJob struct {
name string
}
// Implement the Job interface
job := &cleanupJob{name: "temp files"}
// AddJob accepts any type implementing cron.Job
_, err := c.AddJob("0 0 * * *", cron.FuncJob(func() {
fmt.Printf("Cleaning up %s\n", job.name)
}))
if err != nil {
log.Fatal(err)
}
c.Start()
defer c.Stop()
}
func (*Cron) Entries ¶
Entries returns a snapshot of the cron entries.
Example ¶
This example demonstrates retrieving all scheduled entries.
package main
import (
"fmt"
cron "github.com/netresearch/go-cron"
)
func main() {
c := cron.New()
c.AddFunc("0 * * * *", func() { fmt.Println("hourly") })
c.AddFunc("0 0 * * *", func() { fmt.Println("daily") })
c.Start()
// Get all entries
entries := c.Entries()
fmt.Printf("Number of jobs: %d\n", len(entries))
}
Output: Number of jobs: 2
func (*Cron) EntriesByTag ¶ added in v0.6.0
EntriesByTag returns snapshots of all entries that have the given tag. Returns an empty slice if no entries match.
func (*Cron) Entry ¶
Entry returns a snapshot of the given entry, or nil if it couldn't be found. This operation is O(1) in all cases using the internal index map.
func (*Cron) EntryByName ¶ added in v0.6.0
EntryByName returns a snapshot of the entry with the given name, or an invalid Entry (Entry.Valid() == false) if not found.
This operation is O(1) in all cases using the internal name index.
func (*Cron) Remove ¶
Remove an entry from being run in the future.
Example ¶
This example demonstrates removing a scheduled job.
package main
import (
"fmt"
cron "github.com/netresearch/go-cron"
)
func main() {
c := cron.New()
// AddFunc returns an entry ID
entryID, _ := c.AddFunc("* * * * *", func() {
fmt.Println("This will be removed")
})
c.Start()
// Remove the job using its ID
c.Remove(entryID)
fmt.Printf("Jobs after removal: %d\n", len(c.Entries()))
}
Output: Jobs after removal: 0
func (*Cron) RemoveByName ¶ added in v0.6.0
RemoveByName removes the entry with the given name. Returns true if an entry was removed, false if no entry had that name.
func (*Cron) RemoveByTag ¶ added in v0.6.0
RemoveByTag removes all entries that have the given tag. Returns the number of entries removed.
func (*Cron) Schedule
deprecated
Schedule adds a Job to the Cron to be run on the given schedule. The job is wrapped with the configured Chain.
If a maximum entry limit is configured (via WithMaxEntries) and the limit has been reached, Schedule returns 0 (an invalid EntryID) and logs a warning. Use AddJob or AddFunc to get an error return when the limit is exceeded.
Note: When the cron is running, the limit check is approximate due to concurrent entry additions. The actual count may briefly exceed the limit by the number of concurrent Schedule calls in flight.
Deprecated: Use ScheduleJob instead for error handling and metadata support.
func (*Cron) ScheduleJob ¶ added in v0.6.0
ScheduleJob adds a Job to the Cron to be run on the given schedule. The job is wrapped with the configured Chain.
Optional JobOption arguments can be provided to set metadata like Name and Tags:
c.ScheduleJob(schedule, myJob, cron.WithName("my-job"), cron.WithTags("critical"))
Returns ErrMaxEntriesReached if the maximum entry limit has been reached. Returns ErrDuplicateName if a name is provided and already exists.
Note: When the cron is running, the limit check is approximate due to concurrent entry additions. The actual count may briefly exceed the limit by the number of concurrent ScheduleJob calls in flight.
func (*Cron) Start ¶
func (c *Cron) Start()
Start the cron scheduler in its own goroutine, or no-op if already started.
func (*Cron) Stop ¶
Stop stops the cron scheduler if it is running; otherwise it does nothing. A context is returned so the caller can wait for running jobs to complete.
When Stop is called, the base context is canceled, signaling all running jobs that implement JobWithContext to shut down gracefully. Jobs should check ctx.Done() and return promptly when canceled.
Example ¶
This example demonstrates graceful shutdown with job completion.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
c := cron.New()
c.AddFunc("* * * * *", func() {
time.Sleep(time.Second)
fmt.Println("Job completed")
})
c.Start()
// Stop returns a context that completes when all running jobs finish
ctx := c.Stop()
// Wait for running jobs to complete
<-ctx.Done()
fmt.Println("All jobs completed")
}
Output: All jobs completed
func (*Cron) StopAndWait ¶ added in v0.6.0
func (c *Cron) StopAndWait()
StopAndWait stops the cron scheduler and blocks until all running jobs complete. This is a convenience method equivalent to:
ctx := c.Stop() <-ctx.Done()
For timeout-based shutdown, use StopWithTimeout() or use Stop() directly:
ctx := c.Stop()
select {
case <-ctx.Done():
// All jobs completed
case <-time.After(5 * time.Second):
// Timeout - some jobs may still be running
}
func (*Cron) StopWithTimeout ¶ added in v0.6.0
StopWithTimeout stops the cron scheduler and waits for running jobs to complete with a timeout. Returns true if all jobs completed within the timeout, false if the timeout was reached and some jobs may still be running.
When the timeout is reached, jobs that implement JobWithContext should already have received context cancellation and should be in the process of shutting down. Jobs that don't check their context may continue running in the background.
A timeout of zero or negative waits indefinitely (equivalent to StopAndWait).
Example:
if !c.StopWithTimeout(30 * time.Second) {
log.Println("Warning: some jobs did not complete within 30s")
}
type Entry ¶
type Entry struct {
// ID is the cron-assigned ID of this entry, which may be used to look up a
// snapshot or remove it.
ID EntryID
// Name is an optional human-readable identifier for this entry.
// If set, names must be unique within a Cron instance.
// Use WithName() when adding an entry to set this field.
Name string
// Tags is an optional set of labels for categorizing and filtering entries.
// Multiple entries can share the same tags.
// Use WithTags() when adding an entry to set this field.
Tags []string
// Schedule on which this job should be run.
Schedule Schedule
// Next time the job will run, or the zero time if Cron has not been
// started or this entry's schedule is unsatisfiable
Next time.Time
// Prev is the last time this job was run, or the zero time if never.
Prev time.Time
// WrappedJob is the thing to run when the Schedule is activated.
WrappedJob Job
// Job is the thing that was submitted to cron.
// It is kept around so that user code that needs to get at the job later,
// e.g. via Entries() can do so.
Job Job
// contains filtered or unexported fields
}
Entry consists of a schedule and the func to execute on that schedule.
func (Entry) Run ¶
func (e Entry) Run()
Run executes the entry's job through the configured chain wrappers. This ensures that chain decorators like SkipIfStillRunning, DelayIfStillRunning, and Recover are properly applied. Use this method instead of Entry.Job.Run() when you need chain behavior to be respected. Fix for issue #551: Provides a proper way to run jobs with chain decorators.
type EntryID ¶
type EntryID uint64
EntryID identifies an entry within a Cron instance. Using uint64 prevents overflow and ID collisions on all platforms.
type FakeClock ¶ added in v0.6.0
type FakeClock struct {
// contains filtered or unexported fields
}
FakeClock provides a controllable clock for testing. It allows advancing time manually and fires timers deterministically.
func NewFakeClock ¶ added in v0.6.0
NewFakeClock creates a new FakeClock initialized to the given time.
func (*FakeClock) Advance ¶ added in v0.6.0
Advance moves the fake clock forward by the specified duration and fires any timers whose deadlines have passed.
func (*FakeClock) BlockUntil ¶ added in v0.6.0
BlockUntil blocks until at least n timers are waiting on the clock. This is useful for synchronizing tests with timer creation.
func (*FakeClock) NewTimer ¶ added in v0.6.0
NewTimer creates a fake timer that fires when the clock advances past its deadline.
func (*FakeClock) Set ¶ added in v0.6.0
Set sets the fake clock to the specified time. If the new time is after the current time, fires any timers whose deadlines fall between the old and new times.
func (*FakeClock) TimerCount ¶ added in v0.6.0
TimerCount returns the number of active timers. Useful for test assertions.
type FuncJobWithContext ¶ added in v0.6.0
FuncJobWithContext is a wrapper that turns a func(context.Context) into a JobWithContext. This enables context-aware jobs using simple functions.
Example:
c.AddJob("@every 1m", cron.FuncJobWithContext(func(ctx context.Context) {
select {
case <-ctx.Done():
return // Canceled
default:
// Do work
}
}))
func (FuncJobWithContext) Run ¶ added in v0.6.0
func (f FuncJobWithContext) Run()
Run implements Job interface by calling RunWithContext with context.Background().
func (FuncJobWithContext) RunWithContext ¶ added in v0.6.0
func (f FuncJobWithContext) RunWithContext(ctx context.Context)
RunWithContext implements JobWithContext interface.
type JobOption ¶ added in v0.6.0
type JobOption func(*Entry)
JobOption configures an Entry when adding a job to Cron.
func WithName ¶ added in v0.6.0
WithName sets a unique name for the job entry. Names must be unique within a Cron instance; adding a job with a duplicate name will return ErrDuplicateName.
Named jobs can be retrieved with EntryByName() or removed with RemoveByName().
Example:
c.AddFunc("@every 1h", cleanup, cron.WithName("hourly-cleanup"))
func WithTags ¶ added in v0.6.0
WithTags sets tags for categorizing the job entry. Multiple entries can share the same tags, enabling group operations.
Tagged jobs can be filtered with EntriesByTag() or removed with RemoveByTag().
Example:
c.AddFunc("@every 1h", cleanup, cron.WithTags("maintenance", "hourly"))
type JobWithContext ¶ added in v0.6.0
JobWithContext is an optional interface for jobs that support context.Context. If a job implements this interface, RunWithContext is called instead of Run, allowing the job to:
- Receive cancellation signals when Stop() is called
- Respect deadlines and timeouts
- Access request-scoped values (trace IDs, correlation IDs, etc.)
Jobs that don't implement this interface will continue to work unchanged via their Run() method.
Example:
type MyJob struct{}
func (j *MyJob) Run() { j.RunWithContext(context.Background()) }
func (j *MyJob) RunWithContext(ctx context.Context) {
select {
case <-ctx.Done():
return // Job canceled
case <-time.After(time.Minute):
// Do work
}
}
type JobWrapper ¶
JobWrapper decorates the given Job with some behavior.
func CircuitBreaker ¶ added in v0.6.0
func CircuitBreaker(logger Logger, threshold int, cooldown time.Duration) JobWrapper
Example ¶
This example demonstrates CircuitBreaker to prevent cascading failures. After consecutive failures, the circuit opens and skips execution until cooldown.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
logger := cron.DefaultLogger
c := cron.New(cron.WithChain(
// Outermost: catches re-panic from circuit breaker
cron.Recover(logger),
// Open circuit after 3 consecutive failures, cooldown for 5 minutes
cron.CircuitBreaker(logger, 3, 5*time.Minute),
))
c.AddFunc("@every 1m", func() {
// Simulate a job calling an external service
if err := callExternalAPI(); err != nil {
panic(err) // After 3 failures, circuit opens for 5 minutes
}
fmt.Println("API call succeeded")
})
c.Start()
defer c.Stop()
}
// callExternalAPI is a mock function for the CircuitBreaker example
func callExternalAPI() error {
return nil
}
func DelayIfStillRunning ¶
func DelayIfStillRunning(logger Logger) JobWrapper
DelayIfStillRunning serializes jobs, delaying subsequent runs until the previous one is complete. Jobs running after a delay of more than a minute have the delay logged at Info.
func Recover ¶
func Recover(logger Logger, opts ...RecoverOption) JobWrapper
Recover panics in wrapped jobs and log them with the provided logger.
By default, panics are logged at Error level. Use WithLogLevel to change this behavior, for example when combined with retry wrappers.
Example:
// Default behavior - logs at Error level cron.NewChain(cron.Recover(logger)).Then(job) // Log at Info level (useful with retries) cron.NewChain(cron.Recover(logger, cron.WithLogLevel(cron.LogLevelInfo))).Then(job)
func RetryWithBackoff ¶ added in v0.6.0
func RetryWithBackoff(logger Logger, maxRetries int, initialDelay, maxDelay time.Duration, multiplier float64) JobWrapper
Example ¶
This example demonstrates RetryWithBackoff for jobs that may fail transiently. The wrapper catches panics and retries with exponential backoff.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
logger := cron.DefaultLogger
c := cron.New(cron.WithChain(
// Outermost: catches final re-panic after retries exhausted
cron.Recover(logger),
// Retry up to 3 times with exponential backoff
// Initial delay: 1s, max delay: 30s, multiplier: 2.0
cron.RetryWithBackoff(logger, 3, time.Second, 30*time.Second, 2.0),
))
attempts := 0
c.AddFunc("@hourly", func() {
attempts++
// Simulate transient failure that succeeds on 3rd attempt
if attempts < 3 {
panic(fmt.Sprintf("attempt %d failed", attempts))
}
fmt.Printf("Succeeded on attempt %d\n", attempts)
})
c.Start()
defer c.Stop()
}
Example (NoRetries) ¶
This example demonstrates RetryWithBackoff with maxRetries=0 (no retries). This is the safe default - jobs execute once and fail immediately on panic.
package main
import (
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
logger := cron.DefaultLogger
c := cron.New(cron.WithChain(
cron.Recover(logger),
// maxRetries=0 means no retries - execute once, fail immediately
cron.RetryWithBackoff(logger, 0, time.Second, 30*time.Second, 2.0),
))
c.AddFunc("@hourly", func() {
// This will execute once, panic, and not retry
panic("immediate failure")
})
c.Start()
defer c.Stop()
}
func SkipIfStillRunning ¶
func SkipIfStillRunning(logger Logger) JobWrapper
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 Timeout ¶ added in v0.6.0
func Timeout(logger Logger, timeout time.Duration, opts ...TimeoutOption) JobWrapper
Timeout wraps a job with a timeout. If the job takes longer than the given duration, the wrapper returns and logs an error, but the underlying job goroutine continues running until completion.
⚠️ IMPORTANT: Abandonment Model ¶
This wrapper implements an "abandonment model" - when a timeout occurs, the wrapper returns but the job's goroutine is NOT canceled. The job will continue executing in the background until it naturally completes. This means:
- Resources held by the job will not be released until completion
- Side effects will still occur even after timeout
- Multiple abandoned goroutines can accumulate if jobs consistently timeout
Goroutine Accumulation Risk ¶
If a job consistently takes longer than its schedule interval, abandoned goroutines will accumulate:
// DANGER: This pattern causes goroutine accumulation!
c.AddFunc("@every 1s", func() {
time.Sleep(5 * time.Second) // Takes 5x longer than schedule
})
// With Timeout(2s), a new abandoned goroutine is created every second
Tracking Abandoned Goroutines ¶
Use WithTimeoutCallback to track timeout events for metrics and alerting:
cron.Timeout(logger, 5*time.Minute,
cron.WithTimeoutCallback(func(timeout time.Duration) {
abandonedGoroutines.Inc() // Prometheus counter
}),
)
Recommended Alternatives ¶
For jobs that need true cancellation support, use TimeoutWithContext with jobs that implement JobWithContext:
c := cron.New(cron.WithChain(
cron.TimeoutWithContext(logger, 5*time.Minute),
))
c.AddJob("@every 1h", cron.FuncJobWithContext(func(ctx context.Context) {
select {
case <-ctx.Done():
return // Timeout - clean up and exit
case <-doWork():
// Work completed
}
}))
To prevent accumulation without context support, combine with SkipIfStillRunning:
c := cron.New(cron.WithChain(
cron.Recover(logger),
cron.Timeout(logger, 5*time.Minute),
cron.SkipIfStillRunning(logger), // Prevents overlapping executions
))
A timeout of zero or negative disables the timeout and returns the job unchanged.
Example ¶
This example demonstrates the Timeout wrapper and its limitations. Note: Timeout uses an "abandonment model" - the job continues running in the background even after the timeout is reached.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
logger := cron.DefaultLogger
c := cron.New(cron.WithChain(
// Jobs that exceed 30 seconds will be "abandoned" (wrapper returns,
// but the goroutine keeps running until the job completes naturally)
cron.Timeout(logger, 30*time.Second),
// Recover panics from timed-out jobs
cron.Recover(logger),
))
c.AddFunc("@hourly", func() {
// This job may run longer than 30 seconds.
// If it does, the timeout wrapper will return early and log an error,
// but this goroutine continues until completion.
fmt.Println("Starting long job")
time.Sleep(45 * time.Second) // Exceeds timeout
fmt.Println("Job completed (even after timeout)")
})
c.Start()
defer c.Stop()
}
Example (Cancellable) ¶
This example demonstrates a job pattern that supports true cancellation using channels. This approach works well for simple cancellation needs.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
// CancellableWorker demonstrates a job that can be cleanly canceled
type CancellableWorker struct {
cancel chan struct{}
done chan struct{}
}
worker := &CancellableWorker{
cancel: make(chan struct{}),
done: make(chan struct{}),
}
c := cron.New()
// Wrap the worker in a FuncJob
c.Schedule(cron.Every(time.Minute), cron.FuncJob(func() {
defer close(worker.done)
for i := 0; i < 100; i++ {
select {
case <-worker.cancel:
fmt.Println("Job canceled cleanly")
return
default:
// Do a small chunk of work
time.Sleep(100 * time.Millisecond)
}
}
fmt.Println("Job completed normally")
}))
c.Start()
// Later, to cancel the job:
// close(worker.cancel)
// <-worker.done // Wait for clean shutdown
defer c.Stop()
}
Example (WithContext) ¶
This example demonstrates the recommended pattern for cancellable jobs using context.Context. This is the idiomatic Go approach for jobs that need to respect cancellation signals, especially when calling external services or performing long-running operations.
package main
import (
"context"
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
// ContextAwareJob wraps job execution with context-based cancellation.
// This pattern is recommended when jobs make external calls (HTTP, DB, etc.)
// that accept context for cancellation.
type ContextAwareJob struct {
ctx context.Context
cancel context.CancelFunc
}
// Create a job with its own cancellation context
ctx, cancel := context.WithCancel(context.Background())
job := &ContextAwareJob{ctx: ctx, cancel: cancel}
c := cron.New()
c.Schedule(cron.Every(time.Minute), cron.FuncJob(func() {
// Create a timeout context for this execution
execCtx, execCancel := context.WithTimeout(job.ctx, 30*time.Second)
defer execCancel()
// Use NewTimer instead of time.After to avoid timer leak on early return
workTimer := time.NewTimer(10 * time.Second)
defer workTimer.Stop()
// Simulate work that respects context cancellation
select {
case <-execCtx.Done():
if execCtx.Err() == context.DeadlineExceeded {
fmt.Println("Job timed out")
} else {
fmt.Println("Job canceled")
}
return
case <-workTimer.C:
// Simulated work completed
fmt.Println("Job completed successfully")
}
}))
c.Start()
// To gracefully shutdown:
// job.cancel() // Signal cancellation to all running jobs
// c.Stop() // Stop scheduling new jobs
defer c.Stop()
}
func TimeoutWithContext ¶ added in v0.6.0
func TimeoutWithContext(logger Logger, timeout time.Duration, opts ...TimeoutOption) JobWrapper
TimeoutWithContext wraps a job with a timeout that supports true cancellation. Unlike Timeout, this wrapper passes a context with deadline to jobs that implement JobWithContext, allowing them to check for cancellation and clean up gracefully.
When the timeout expires:
- Jobs implementing JobWithContext receive a canceled context and can stop gracefully
- Jobs implementing only Job continue running (same as Timeout wrapper)
Use WithTimeoutCallback to track timeout/abandonment events:
cron.TimeoutWithContext(logger, 5*time.Minute,
cron.WithTimeoutCallback(func(timeout time.Duration) {
timeoutCounter.Inc()
}),
)
A timeout of zero or negative disables the timeout and returns the job unchanged.
Example:
c := cron.New(cron.WithChain(
cron.TimeoutWithContext(cron.DefaultLogger, 5*time.Minute),
))
c.AddJob("@every 1h", cron.FuncJobWithContext(func(ctx context.Context) {
// This job will receive the timeout context
select {
case <-ctx.Done():
// Timeout or shutdown - clean up and return
return
case <-doWork():
// Work completed
}
}))
Example ¶
This example demonstrates TimeoutWithContext for true context-based cancellation. Jobs implementing JobWithContext receive a context that is canceled on timeout.
package main
import (
"context"
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
logger := cron.DefaultLogger
c := cron.New(cron.WithChain(
// Outermost: catches any panics
cron.Recover(logger),
// Jobs have 5 minutes to complete; context is canceled on timeout
cron.TimeoutWithContext(logger, 5*time.Minute),
))
// Use FuncJobWithContext for jobs that need context support
c.AddJob("@hourly", cron.FuncJobWithContext(func(ctx context.Context) {
// Create a timer for simulated work
workTimer := time.NewTimer(10 * time.Minute)
defer workTimer.Stop()
select {
case <-ctx.Done():
// Context canceled - clean up and exit gracefully
fmt.Println("Job canceled:", ctx.Err())
return
case <-workTimer.C:
// Work completed normally
fmt.Println("Job completed")
}
}))
c.Start()
defer c.Stop()
}
type LogLevel ¶ added in v0.6.0
type LogLevel int
LogLevel defines the severity level for logging recovered panics.
type Logger ¶
type Logger interface {
// Info logs routine messages about cron's operation.
Info(msg string, keysAndValues ...any)
// Error logs an error condition.
Error(err error, msg string, keysAndValues ...any)
}
Logger is the interface used in this package for logging, so that any backend can be plugged in. It is a subset of the github.com/go-logr/logr interface.
func PrintfLogger ¶
PrintfLogger wraps a Printf-based logger (such as the standard library "log") into an implementation of the Logger interface which logs errors only.
func VerbosePrintfLogger ¶
VerbosePrintfLogger wraps a Printf-based logger (such as the standard library "log") into an implementation of the Logger interface which logs everything.
Example ¶
This example demonstrates verbose logging for debugging.
package main
import (
"fmt"
"log"
cron "github.com/netresearch/go-cron"
)
func main() {
logger := cron.VerbosePrintfLogger(log.Default())
c := cron.New(cron.WithLogger(logger))
c.AddFunc("@hourly", func() {
fmt.Println("hourly job")
})
c.Start()
defer c.Stop()
}
type NamedJob ¶ added in v0.6.0
NamedJob is an optional interface that jobs can implement to provide a name for observability purposes. If a job doesn't implement this interface, an empty string is used for the name in hook callbacks.
Example ¶
This example demonstrates implementing NamedJob for better observability. Named jobs have their name passed to observability hooks, which is useful for metrics labeling (e.g., Prometheus labels).
package main
import (
"fmt"
)
func main() {
// myJob implements both Job and NamedJob interfaces
type myJob struct {
name string
}
// Run implements cron.Job
run := func(j *myJob) {
fmt.Printf("Running %s\n", j.name)
}
_ = run
// Name implements cron.NamedJob
name := func(j *myJob) string {
return j.name
}
_ = name
// When used with observability hooks, the name is passed to callbacks:
// OnJobStart(id, "my-job-name", scheduledTime)
// OnJobComplete(id, "my-job-name", duration, recovered)
fmt.Println("NamedJob provides names for observability hooks")
}
Output: NamedJob provides names for observability hooks
type ObservabilityHooks ¶ added in v0.6.0
type ObservabilityHooks struct {
// OnJobStart is called immediately before a job begins execution.
// Parameters:
// - entryID: the unique identifier for the scheduled entry
// - name: job name (from NamedJob interface, or empty string)
// - scheduledTime: the time the job was scheduled to run
OnJobStart func(entryID EntryID, name string, scheduledTime time.Time)
// OnJobComplete is called when a job finishes execution.
// Parameters:
// - entryID: the unique identifier for the scheduled entry
// - name: job name (from NamedJob interface, or empty string)
// - duration: how long the job took to execute
// - recovered: the value from recover() if the job panicked, or nil
OnJobComplete func(entryID EntryID, name string, duration time.Duration, recovered any)
// OnSchedule is called when a job's next execution time is calculated.
// Parameters:
// - entryID: the unique identifier for the scheduled entry
// - name: job name (from NamedJob interface, or empty string)
// - nextRun: the next scheduled execution time
OnSchedule func(entryID EntryID, name string, nextRun time.Time)
}
ObservabilityHooks provides callbacks for monitoring cron operations. All callbacks are optional; nil callbacks are safely ignored.
Hooks are called asynchronously in separate goroutines to prevent slow callbacks from blocking the scheduler. This means:
- Callbacks may execute slightly after the event occurred
- Callback execution order is not guaranteed across events
- Callbacks should be safe for concurrent execution
If you need synchronous execution, use channels or sync primitives within your callback implementation.
Example with Prometheus:
hooks := cron.ObservabilityHooks{
OnJobStart: func(id cron.EntryID, name string, scheduled time.Time) {
jobsStarted.WithLabelValues(name).Inc()
},
OnJobComplete: func(id cron.EntryID, name string, dur time.Duration, recovered any) {
jobDuration.WithLabelValues(name).Observe(dur.Seconds())
if recovered != nil {
jobPanics.WithLabelValues(name).Inc()
}
},
}
c := cron.New(cron.WithObservability(hooks))
type Option ¶
type Option func(*Cron)
Option represents a modification to the default behavior of a Cron.
func WithChain ¶
func WithChain(wrappers ...JobWrapper) Option
WithChain specifies Job wrappers to apply to all jobs added to this cron. Refer to the Chain* functions in this package for provided wrappers.
Example ¶
This example demonstrates using job wrappers (middleware) with WithChain.
package main
import (
"fmt"
cron "github.com/netresearch/go-cron"
)
func main() {
// Create cron with job wrappers applied to all jobs
c := cron.New(
cron.WithChain(
// Recover from panics and log them
cron.Recover(cron.DefaultLogger),
// Skip job execution if the previous run hasn't completed
cron.SkipIfStillRunning(cron.DefaultLogger),
),
)
c.AddFunc("* * * * *", func() {
fmt.Println("This job is protected by Recover and SkipIfStillRunning")
})
c.Start()
defer c.Stop()
}
func WithClock ¶ added in v0.6.0
WithClock uses the provided Clock implementation instead of the default RealClock. This is useful for testing time-dependent behavior without waiting.
The Clock interface provides both Now() for current time and NewTimer() for creating timers, enabling fully deterministic testing of scheduled jobs.
Example usage:
fakeClock := cron.NewFakeClock(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)) c := cron.New(cron.WithClock(fakeClock)) // ... add jobs ... c.Start() fakeClock.Advance(time.Hour) // Advance time and trigger jobs deterministically
func WithContext ¶ added in v0.6.0
WithContext sets the base context for all job executions. When Stop() is called, this context is canceled, signaling all running jobs that implement JobWithContext to shut down gracefully.
If not specified, context.Background() is used as the base context.
Use cases:
- Propagate application-wide cancellation to cron jobs
- Attach tracing context or correlation IDs to all jobs
- Integrate with application lifecycle management
Example:
// Create cron with application context
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
c := cron.New(cron.WithContext(ctx))
// Jobs implementing JobWithContext will receive this context
c.AddJob("@every 1m", cron.FuncJobWithContext(func(ctx context.Context) {
select {
case <-ctx.Done():
return // Application shutting down
default:
// Do work
}
}))
func WithLocation ¶
WithLocation overrides the timezone of the cron instance.
func WithMaxEntries ¶ added in v0.6.0
WithMaxEntries limits the maximum number of entries that can be added to the Cron. When the limit is reached:
- AddFunc and AddJob return ErrMaxEntriesReached
- Schedule returns 0 (invalid EntryID) and logs an error
A limit of 0 (the default) means unlimited entries.
This option provides protection against memory exhaustion from excessive entry additions, which could occur from buggy code or untrusted input.
Note: When the cron is running, the limit enforcement is approximate due to concurrent entry additions. The actual count may briefly exceed the limit.
Example usage:
c := cron.New(cron.WithMaxEntries(1000))
for i := 0; i < 2000; i++ {
_, err := c.AddFunc("* * * * *", func() {})
if errors.Is(err, cron.ErrMaxEntriesReached) {
log.Println("Entry limit reached")
break
}
}
Example ¶
This example demonstrates using WithMaxEntries to limit the number of jobs. This provides protection against memory exhaustion from excessive entry additions.
package main
import (
"fmt"
cron "github.com/netresearch/go-cron"
)
func main() {
c := cron.New(cron.WithMaxEntries(2))
// Add first job - succeeds
_, err := c.AddFunc("@hourly", func() { fmt.Println("Job 1") })
if err != nil {
fmt.Println("Job 1 failed:", err)
}
// Add second job - succeeds
_, err = c.AddFunc("@hourly", func() { fmt.Println("Job 2") })
if err != nil {
fmt.Println("Job 2 failed:", err)
}
// Add third job - fails (limit reached)
_, err = c.AddFunc("@hourly", func() { fmt.Println("Job 3") })
if err != nil {
fmt.Println("Job 3 failed:", err)
}
fmt.Printf("Total jobs: %d\n", len(c.Entries()))
}
Output: Job 3 failed: cron: max entries limit reached Total jobs: 2
func WithMaxSearchYears ¶ added in v0.6.0
WithMaxSearchYears configures the maximum years into the future that schedule matching will search before giving up. This prevents infinite loops for unsatisfiable schedules (e.g., Feb 30).
The default is 5 years. Values <= 0 will use the default.
Use cases:
- Shorter limits for faster failure detection: WithMaxSearchYears(1)
- Longer limits for rare schedules: WithMaxSearchYears(10)
Note: This option replaces the current parser. If you need custom parser options along with a custom max search years, use WithParser with a manually configured parser:
p := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor).
WithMaxSearchYears(10)
c := cron.New(cron.WithParser(p))
Example:
// Allow searching up to 10 years for rare schedules
c := cron.New(cron.WithMaxSearchYears(10))
c.AddFunc("0 0 13 * 5", func() { ... }) // Friday the 13th
func WithMinEveryInterval ¶ added in v0.6.0
WithMinEveryInterval configures the minimum interval allowed for @every expressions. This allows overriding the default 1-second minimum.
Use cases:
- Sub-second intervals for testing: WithMinEveryInterval(0) or WithMinEveryInterval(100*time.Millisecond)
- Enforce longer minimums for rate limiting: WithMinEveryInterval(time.Minute)
The interval affects:
- Parsing of "@every <duration>" expressions
- The EveryWithMin function when called via the parser
Note: This option replaces the current parser. If you need custom parser options along with a custom minimum interval, use WithParser with a manually configured parser:
p := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor).
WithMinEveryInterval(100 * time.Millisecond)
c := cron.New(cron.WithParser(p))
Example:
// Allow sub-second intervals (useful for testing)
c := cron.New(cron.WithMinEveryInterval(0))
c.AddFunc("@every 100ms", func() { ... })
// Enforce minimum 1-minute intervals
c := cron.New(cron.WithMinEveryInterval(time.Minute))
c.AddFunc("@every 30s", func() { ... }) // Error: must be at least 1 minute
Example ¶
This example demonstrates using WithMinEveryInterval to configure the minimum interval for @every expressions at the cron level.
package main
import (
"fmt"
"log"
cron "github.com/netresearch/go-cron"
)
func main() {
// Allow sub-second @every intervals (useful for testing)
c := cron.New(cron.WithMinEveryInterval(0))
_, err := c.AddFunc("@every 100ms", func() {
fmt.Println("Running every 100ms")
})
if err != nil {
log.Fatal(err)
}
// With default settings, sub-second would fail:
// c := cron.New() // default minimum is 1 second
// _, err := c.AddFunc("@every 100ms", ...) // returns error
c.Start()
defer c.Stop()
}
Example (RateLimit) ¶
This example demonstrates using WithMinEveryInterval to enforce longer minimum intervals for rate limiting purposes.
package main
import (
"fmt"
"log"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
// Enforce minimum 1-minute intervals
c := cron.New(cron.WithMinEveryInterval(time.Minute))
// This will fail because 30s < 1m minimum
_, err := c.AddFunc("@every 30s", func() {
fmt.Println("This won't be added")
})
if err != nil {
fmt.Println("Error:", err)
}
// This will succeed because 2m >= 1m minimum
_, err = c.AddFunc("@every 2m", func() {
fmt.Println("Running every 2 minutes")
})
if err != nil {
log.Fatal(err)
}
c.Start()
defer c.Stop()
}
Output: Error: @every duration must be at least 1m0s: "@every 30s"
func WithObservability ¶ added in v0.6.0
func WithObservability(hooks ObservabilityHooks) Option
WithObservability configures observability hooks for monitoring cron operations. Hooks are called asynchronously in separate goroutines to prevent slow callbacks from blocking the scheduler. This means callback execution order is not guaranteed.
All hook callbacks are optional; nil callbacks are safely ignored.
Example with Prometheus metrics:
hooks := cron.ObservabilityHooks{
OnJobStart: func(id cron.EntryID, name string, scheduled time.Time) {
jobsStarted.WithLabelValues(name).Inc()
},
OnJobComplete: func(id cron.EntryID, name string, dur time.Duration, recovered any) {
jobDuration.WithLabelValues(name).Observe(dur.Seconds())
if recovered != nil {
jobPanics.WithLabelValues(name).Inc()
}
},
}
c := cron.New(cron.WithObservability(hooks))
Example ¶
This example demonstrates using observability hooks for metrics collection. In production, you would integrate with Prometheus, StatsD, or similar systems.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
var jobsStarted, jobsCompleted int
hooks := cron.ObservabilityHooks{
OnJobStart: func(id cron.EntryID, name string, scheduled time.Time) {
// In production: prometheus.Counter.Inc()
jobsStarted++
},
OnJobComplete: func(id cron.EntryID, name string, duration time.Duration, recovered any) {
// In production: prometheus.Histogram.Observe(duration.Seconds())
jobsCompleted++
},
OnSchedule: func(id cron.EntryID, name string, nextRun time.Time) {
// In production: prometheus.Gauge.Set(nextRun.Unix())
},
}
c := cron.New(cron.WithObservability(hooks))
c.AddFunc("@hourly", func() {
// Job logic here
})
c.Start()
c.Stop()
fmt.Println("Hooks configured successfully")
}
Output: Hooks configured successfully
func WithParser ¶
func WithParser(p ScheduleParser) Option
WithParser overrides the parser used for interpreting job schedules.
func WithSeconds ¶
func WithSeconds() Option
WithSeconds overrides the parser used for interpreting job schedules to include a seconds field as the first one.
type PanicWithStack ¶ added in v0.6.0
type PanicWithStack struct {
Value any // The original panic value
Stack []byte // Stack trace at point of panic
}
PanicWithStack wraps a panic value with the stack trace at the point of panic. This allows re-panicking to preserve the original stack trace for debugging.
func (*PanicWithStack) Error ¶ added in v0.6.0
func (p *PanicWithStack) Error() string
Error implements the error interface for PanicWithStack.
func (*PanicWithStack) String ¶ added in v0.6.0
func (p *PanicWithStack) String() string
String returns a detailed representation including the stack trace.
func (*PanicWithStack) Unwrap ¶ added in v0.6.0
func (p *PanicWithStack) Unwrap() error
Unwrap returns the original panic value if it was an error.
type ParseOption ¶
type ParseOption int
ParseOption represents configuration options for creating a parser. Most options specify which fields should be included, while others enable features. If a field is not included the parser will assume a default value. These options do not change the order fields are parsed in.
const ( Second ParseOption = 1 << iota // Seconds field, default 0 SecondOptional // Optional seconds field, default 0 Minute // Minutes field, default 0 Hour // Hours field, default 0 Dom // Day of month field, default * Month // Month field, default * Dow // Day of week field, default * DowOptional // Optional day of week field, default * Descriptor // Allow descriptors such as @monthly, @weekly, etc. )
ParseOption constants define which fields are included in parsing.
type Parser ¶
type Parser struct {
// contains filtered or unexported fields
}
Parser is a custom cron expression parser that can be configured.
func MustNewParser ¶ added in v0.6.0
func MustNewParser(options ParseOption) Parser
MustNewParser is like TryNewParser but panics if the options are invalid. This follows the Go convention of Must* functions for cases where failure indicates a programming error rather than a runtime condition.
Use MustNewParser when:
- Parser options are hardcoded constants
- Invalid configuration is a bug that should fail fast
Use TryNewParser when:
- Parser options come from config files, environment, or user input
- You want to handle configuration errors gracefully
Note: In v2.0, NewParser will return (Parser, error) and MustNewParser will be the only panicking variant. Using MustNewParser now ensures forward compatibility with v2.0.
Example:
// Panics if options are invalid (hardcoded, so invalid = bug) var parser = cron.MustNewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
func NewParser
deprecated
func NewParser(options ParseOption) Parser
NewParser creates a Parser with custom options.
Deprecated: NewParser will change to return (Parser, error) in v2.0. Use MustNewParser for panic-on-error behavior (forward compatible), or TryNewParser for explicit error handling.
It panics if more than one Optional is given, since it would be impossible to correctly infer which optional is provided or missing in general.
Examples
// Standard parser without descriptors
specParser := NewParser(Minute | Hour | Dom | Month | Dow)
sched, err := specParser.Parse("0 0 15 */3 *")
// Same as above, just excludes time fields
specParser := NewParser(Dom | Month | Dow)
sched, err := specParser.Parse("15 */3 *")
// Same as above, just makes Dow optional
specParser := NewParser(Dom | Month | DowOptional)
sched, err := specParser.Parse("15 */3")
func StandardParser ¶ added in v0.6.0
func StandardParser() Parser
StandardParser returns a copy of the standard parser used by ParseStandard. This can be used as a base for creating custom parsers with modified settings.
Example:
// Create parser allowing sub-second @every intervals p := StandardParser().WithMinEveryInterval(0) c := cron.New(cron.WithParser(p))
func TryNewParser ¶ added in v0.6.0
func TryNewParser(options ParseOption) (Parser, error)
TryNewParser creates a Parser with custom options, returning an error if the configuration is invalid. This is the safe alternative to NewParser for cases where parser options come from runtime configuration rather than hardcoded values.
Use TryNewParser when:
- Parser options come from config files, environment variables, or user input
- You want to handle configuration errors gracefully
Use NewParser when:
- Parser options are hardcoded constants (invalid config = bug)
- You want to fail fast during initialization
Returns ErrNoFields if no fields or Descriptor are configured. Returns ErrMultipleOptionals if more than one optional field is configured.
Example:
// Safe parsing from config
opts := loadParserOptionsFromConfig()
parser, err := TryNewParser(opts)
if err != nil {
return fmt.Errorf("invalid parser config: %w", err)
}
func (Parser) Parse ¶
Parse returns a new crontab schedule representing the given spec. It returns a descriptive error if the spec is not valid. It accepts crontab specs and features configured by NewParser.
If caching is enabled via WithCache(), repeated calls with the same spec will return the cached result.
func (Parser) WithCache ¶ added in v0.6.0
WithCache returns a new Parser with caching enabled for parsed schedules. When caching is enabled, repeated calls to Parse with the same spec string will return the cached result instead of re-parsing.
Caching is particularly beneficial when:
- The same cron expressions are parsed repeatedly
- Multiple cron instances share the same parser
- Configuration is reloaded frequently
The cache is thread-safe and grows unbounded. For applications with many unique spec strings, consider using a single shared parser instance.
Example:
// Create a caching parser for improved performance
p := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor).
WithCache()
// Subsequent parses of the same spec return cached results
sched1, _ := p.Parse("0 * * * *") // parsed
sched2, _ := p.Parse("0 * * * *") // cached (same reference)
func (Parser) WithMaxSearchYears ¶ added in v0.6.0
WithMaxSearchYears returns a new Parser with the specified maximum search years for finding the next schedule time. This limits how far into the future the Next() method will search before giving up and returning zero time.
The default is 5 years. Values <= 0 will use the default.
Use cases:
- Shorter limits for faster failure detection on invalid schedules
- Longer limits for rare schedules (e.g., "Friday the 13th in February")
- Testing scenarios that need predictable behavior
Example:
// Allow searching up to 10 years for rare schedules
p := NewParser(Minute | Hour | Dom | Month | Dow | Descriptor).
WithMaxSearchYears(10)
// Fail faster on invalid schedules (1 year max)
p := NewParser(Minute | Hour | Dom | Month | Dow | Descriptor).
WithMaxSearchYears(1)
func (Parser) WithMinEveryInterval ¶ added in v0.6.0
WithMinEveryInterval returns a new Parser with the specified minimum interval for @every expressions. This allows overriding the default 1-second minimum.
Use 0 or negative values to disable the minimum check entirely. Use values larger than 1 second to enforce longer minimum intervals.
Example:
// Allow sub-second intervals (for testing)
p := NewParser(Minute | Hour | Dom | Month | Dow | Descriptor).
WithMinEveryInterval(100 * time.Millisecond)
// Enforce minimum 1-minute intervals (for rate limiting)
p := NewParser(Minute | Hour | Dom | Month | Dow | Descriptor).
WithMinEveryInterval(time.Minute)
type RealClock ¶ added in v0.6.0
type RealClock struct{}
RealClock implements Clock using the standard time package. This is the default clock used in production.
type RecoverOption ¶ added in v0.6.0
type RecoverOption func(*recoverOpts)
RecoverOption configures the Recover wrapper.
func WithLogLevel ¶ added in v0.6.0
func WithLogLevel(level LogLevel) RecoverOption
WithLogLevel sets the log level for recovered panics. Default is LogLevelError. Use LogLevelInfo to reduce noise when combined with retry wrappers like RetryWithBackoff.
Example:
cron.Recover(logger, cron.WithLogLevel(cron.LogLevelInfo))
type Schedule ¶
type Schedule interface {
// Next returns the next activation time, later than the given time.
// Next is invoked initially, and then each time the job is run.
Next(time.Time) time.Time
}
Schedule describes a job's duty cycle.
func ParseStandard ¶
ParseStandard returns a new crontab schedule representing the given standardSpec (https://en.wikipedia.org/wiki/Cron). It requires 5 entries representing: minute, hour, day of month, month and day of week, in that order. It returns a descriptive error if the spec is not valid.
It accepts
- Standard crontab specs, e.g. "* * * * ?"
- Descriptors, e.g. "@midnight", "@every 1h30m"
Example ¶
This example demonstrates parsing a cron expression.
package main
import (
"fmt"
"log"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
schedule, err := cron.ParseStandard("0 9 * * MON-FRI")
if err != nil {
log.Fatal(err)
}
// Get the next scheduled time
now := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) // Wednesday
next := schedule.Next(now)
fmt.Printf("Next run: %s\n", next.Format("Mon 15:04"))
}
Output: Next run: Wed 09:00
type ScheduleParser ¶
ScheduleParser is an interface for schedule spec parsers that return a Schedule
type SlogLogger ¶ added in v0.6.0
type SlogLogger struct {
// contains filtered or unexported fields
}
SlogLogger adapts log/slog to the Logger interface. This allows integration with Go 1.21+ structured logging.
func NewSlogLogger ¶ added in v0.6.0
func NewSlogLogger(l *slog.Logger) *SlogLogger
NewSlogLogger creates a Logger that writes to the given slog.Logger. If l is nil, slog.Default() is used.
func (*SlogLogger) Error ¶ added in v0.6.0
func (s *SlogLogger) Error(err error, msg string, keysAndValues ...any)
Error logs an error condition using slog.
func (*SlogLogger) Info ¶ added in v0.6.0
func (s *SlogLogger) Info(msg string, keysAndValues ...any)
Info logs routine messages about cron's operation using slog.
type SpecSchedule ¶
type SpecSchedule struct {
Second, Minute, Hour, Dom, Month, Dow uint64
// Override location for this schedule.
Location *time.Location
// MaxSearchYears limits how many years into the future Next() will search
// before giving up and returning zero time. This prevents infinite loops
// for unsatisfiable schedules (e.g., Feb 30). Zero means use the default (5 years).
MaxSearchYears int
}
SpecSchedule specifies a duty cycle (to the second granularity), based on a traditional crontab specification. It is computed initially and stored as bit sets.
type TimeoutOption ¶ added in v0.6.0
type TimeoutOption func(*timeoutConfig)
TimeoutOption configures Timeout and TimeoutWithContext wrappers.
func WithTimeoutCallback ¶ added in v0.6.0
func WithTimeoutCallback(fn func(timeout time.Duration)) TimeoutOption
WithTimeoutCallback sets a callback invoked when a job times out and is abandoned. This is useful for metrics collection and alerting on goroutine accumulation.
Example with Prometheus:
abandonedGoroutines := prometheus.NewCounter(prometheus.CounterOpts{
Name: "cron_abandoned_goroutines_total",
Help: "Number of job goroutines abandoned due to timeout",
})
c := cron.New(cron.WithChain(
cron.Timeout(logger, 5*time.Minute,
cron.WithTimeoutCallback(func(timeout time.Duration) {
abandonedGoroutines.Inc()
}),
),
))
type Timer ¶ added in v0.6.0
type Timer interface {
// C returns the channel on which the timer fires.
C() <-chan time.Time
// Stop prevents the Timer from firing. Returns true if the call stops
// the timer, false if the timer has already expired or been stopped.
Stop() bool
// Reset changes the timer to expire after duration d.
// Returns true if the timer had been active, false if it had expired or been stopped.
Reset(d time.Duration) bool
}
Timer represents a single event timer, similar to time.Timer. It provides the same core operations needed for scheduling.