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.
Extended Syntax (Optional) ¶
The following extended syntax is available when enabled via parser options. These provide Quartz/Jenkins-style cron expression features.
To enable extended syntax, use the Extended parser option:
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Extended) c := cron.New(cron.WithParser(parser))
Or enable individual features:
// Enable only L syntax for last day of month parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.DomL) // Enable nth weekday syntax (#n) parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.DowNth)
Hash-Number ( #n ) - Day of Week Field
Used in the day-of-week field to specify the nth occurrence of a weekday in a month. Requires DowNth option to be enabled.
FRI#3 - Third Friday of every month MON#1 - First Monday of every month 0#2 - Second Sunday of every month
Hash-L ( #L ) - Day of Week Field
Used in the day-of-week field to specify the last occurrence of a weekday in a month. Requires DowLast option to be enabled.
FRI#L - Last Friday of every month SUN#L - Last Sunday of every month 1#L - Last Monday of every month
L ( L ) - Day of Month Field ¶
Specifies the last day of the month. Requires DomL option to be enabled.
L - Last day of every month (Jan 31, Feb 28/29, etc.) L-3 - Third from last day of month L-1 - Second to last day of month
W ( W ) - Day of Month Field ¶
Specifies the nearest weekday to a given day. Requires DomW option to be enabled.
15W - Nearest weekday to the 15th 1W - Nearest weekday to the 1st (could be Mon/Tue/Wed if 1st is weekend) LW - Last weekday of the month 31W - Nearest weekday to the 31st (only runs in 31-day months!)
Important nW Behavior:
- If the target day doesn't exist (e.g., 31W in February), the month is skipped. Use LW instead if you want "last weekday of every month."
- If the target day is a weekend, the nearest weekday within the same month is used (following Quartz behavior - won't cross month boundaries).
Examples:
- 31W in February: No day 31 exists → skip to March
- 31W in March (31st is Sunday): Uses Friday March 29 (stays in month)
- 1W in March (1st is Saturday): Uses Monday March 3 (stays in month)
Combined Examples
0 12 L * * - Noon on the last day of every month 0 12 L-3 * * - Noon on the third from last day of every month 0 12 LW * * - Noon on the last weekday of every month 0 12 15W * * - Noon on the nearest weekday to the 15th 0 12 * * FRI#3 - Noon on the third Friday of every month 0 12 * * MON#L - Noon on the last Monday of every month 0 12 1,15,L * * - Noon on the 1st, 15th, and last day of every month
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
- Add random delay (jitter) to prevent thundering herd
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
- Jitter: Add random delay to prevent thundering herd
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))))
Run-Once Jobs ¶
Run-once jobs execute exactly once at their scheduled time and are automatically removed from the scheduler after execution. This is useful for:
- One-time maintenance tasks (schema migrations, cleanup jobs)
- Deferred execution triggered by user actions
- Temporary scheduled events (promotions, time-limited features)
- Testing and debugging scheduled behavior
Using the WithRunOnce option:
c := cron.New()
c.Start()
// Job runs at next matching time, then removes itself
c.AddFunc("0 3 * * *", migrateDatabase, cron.WithRunOnce())
// Combining with other options
c.AddFunc("@every 5m", sendReminder, cron.WithRunOnce(), cron.WithName("reminder"))
Convenience methods for cleaner code:
// These are equivalent:
c.AddFunc("@hourly", task, cron.WithRunOnce())
c.AddOnceFunc("@hourly", task)
// For Job interface implementations:
c.AddOnceJob("@daily", myJob)
// For pre-parsed schedules:
c.ScheduleOnceJob(cron.Every(time.Hour), myJob)
Run-once with immediate execution:
// Run immediately AND only once - useful for deferred tasks
c.AddFunc("@hourly", processOrder, cron.WithRunOnce(), cron.WithRunImmediately())
Behavior notes:
- The entry is removed AFTER the job is dispatched (job continues in its goroutine)
- Works correctly with Recover, RetryWithBackoff, and other wrappers
- Entry removal is logged at Info level: "run-once", "entry", id, "removed", true
- Manual Remove() before execution prevents the job from running
- Entry count decrements immediately upon removal
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
- Run-once jobs: Single-execution jobs that auto-remove after running
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
- func Between(schedule Schedule, start, end time.Time) []time.Time
- func BetweenWithLimit(schedule Schedule, start, end time.Time, limit int) []time.Time
- func Count(schedule Schedule, start, end time.Time) int
- func CountWithLimit(schedule Schedule, start, end time.Time, limit int) int
- func Matches(schedule Schedule, t time.Time) bool
- func NextN(schedule Schedule, t time.Time, n int) []time.Time
- func NormalizeDOW(bits uint64) uint64
- func ValidateSpec(spec string, options ...ParseOption) error
- func ValidateSpecs(specs []string, options ...ParseOption) map[int]error
- 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) AddOnceFunc(spec string, cmd func(), opts ...JobOption) (EntryID, error)
- func (c *Cron) AddOnceJob(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) IsRunning() bool
- 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) ScheduleOnceJob(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 DomConstraint
- type DomConstraintType
- type DowConstraint
- 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 Jitter(maxJitter time.Duration) JobWrapper
- func JitterWithLogger(logger Logger, maxJitter time.Duration) 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(maxEntries int) Option
- func WithMaxSearchYears(years int) Option
- func WithMinEveryInterval(d time.Duration) Option
- func WithObservability(hooks ObservabilityHooks) Option
- func WithParser(p ScheduleParser) Option
- func WithSecondOptional() Option
- func WithSeconds() Option
- type PanicError
- type PanicWithStackdeprecated
- type ParseOption
- type Parser
- func (p Parser) Parse(spec string) (Schedule, error)
- func (p Parser) ParseWithHashKey(spec, hashKey string) (Schedule, error)
- func (p Parser) WithCache() Parser
- func (p Parser) WithHashKey(key string) Parser
- func (p Parser) WithMaxSearchYears(years int) Parser
- func (p Parser) WithMinEveryInterval(d time.Duration) Parser
- func (p Parser) WithSecondOptional() Parser
- type RealClock
- type RecoverOption
- type Schedule
- type ScheduleParser
- type ScheduleWithPrev
- type SlogLogger
- type SpecAnalysis
- type SpecSchedule
- type TimeoutOption
- type Timer
- type ValidationError
Examples ¶
- Package
- CircuitBreaker
- ConstantDelaySchedule.Prev
- Cron.AddFunc
- Cron.AddFunc (Timezone)
- Cron.AddJob
- Cron.AddOnceFunc
- Cron.Entries
- Cron.IsRunning
- Cron.Remove
- Cron.Stop
- Every
- EveryWithMin
- Jitter
- Jitter (PerJob)
- JitterWithLogger
- NamedJob
- New
- New (WithLocation)
- New (WithSeconds)
- NewChain
- NewParser (Hash)
- NewParser (HashRange)
- NewParser (HashStep)
- NewParser (YearField)
- NewParser (YearRange)
- ParseStandard
- ParseStandard (SundayFormats)
- ParseStandard (WeekendRange)
- Parser.WithHashKey
- RetryWithBackoff
- RetryWithBackoff (NoRetries)
- Timeout
- Timeout (Cancellable)
- Timeout (WithContext)
- TimeoutWithContext
- VerbosePrintfLogger
- WithChain
- WithMaxEntries
- WithMinEveryInterval
- WithMinEveryInterval (RateLimit)
- WithObservability
- WithPrev
- WithPrev (CombinedWithRunImmediately)
- WithRunImmediately
- WithRunOnce
- WithRunOnce (WithRecover)
- WithRunOnce (WithRunImmediately)
Constants ¶
const Extended = DowNth | DowLast | DomL | DomW
Extended is a convenience flag that enables all extended cron syntax options: DowNth, DowLast, DomL, and DomW. This provides Quartz/Jenkins-style extensions.
Example:
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor | cron.Extended) // Now supports: FRI#3 (3rd Friday), MON#L (last Monday), L (last day), 15W (nearest weekday)
const MaxSpecLength = 1024
MaxSpecLength is the maximum allowed length for a cron spec string. This limit prevents potential resource exhaustion from extremely long inputs.
const YearBase = 1
YearBase is the minimum valid year for the Year field. Set to 1 CE to allow any reasonable historical or future date.
const YearMax = 1<<31 - 1 // 2147483647
YearMax is the maximum valid year for the Year field. With sparse map[int]struct{} storage, there is no technical limit. Using math.MaxInt32 (2147483647) ensures compatibility across platforms while being effectively unlimited for any practical scheduling use.
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 ErrEmptySpec = &ValidationError{Message: "empty spec string"}
ErrEmptySpec is returned when an empty spec string is provided.
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 = errors.New("multiple optionals may not be configured")
ErrMultipleOptionals is returned when more than one optional field is configured.
var ErrNoFields = errors.New("at least one field or Descriptor must be configured")
ErrNoFields is returned when no fields or Descriptor are configured.
Functions ¶
func Between ¶ added in v0.7.0
Between returns all execution times in the range [start, end). The end time is exclusive. Returns nil if schedule is nil.
WARNING: For high-frequency schedules over long ranges, this can return many results. Use BetweenWithLimit for bounded queries.
Example:
schedule, _ := cron.ParseStandard("0 9 * * *")
start := time.Now()
end := start.AddDate(0, 1, 0) // Next month
times := cron.Between(schedule, start, end)
func BetweenWithLimit ¶ added in v0.7.0
BetweenWithLimit returns execution times in the range [start, end) up to limit. If limit is 0 or negative, no limit is applied. Returns nil if schedule is nil.
Example:
schedule, _ := cron.ParseStandard("* * * * *") // Every minute
times := cron.BetweenWithLimit(schedule, start, end, 100) // Max 100 results
func Count ¶ added in v0.7.0
Count returns the number of executions in the range [start, end). The end time is exclusive. Returns 0 if schedule is nil.
WARNING: For high-frequency schedules over long ranges, this may take significant time. Use CountWithLimit for bounded counting.
Example:
schedule, _ := cron.ParseStandard("0 * * * *")
count := cron.Count(schedule, start, end)
fmt.Printf("Will run %d times\n", count)
func CountWithLimit ¶ added in v0.7.0
CountWithLimit counts executions in the range [start, end) up to limit. If limit is 0 or negative, no limit is applied. Returns the count, which will be at most limit if a limit was specified. Returns 0 if schedule is nil.
Example:
schedule, _ := cron.ParseStandard("* * * * *")
count := cron.CountWithLimit(schedule, start, end, 10000)
if count == 10000 {
fmt.Println("At least 10000 executions")
}
func Matches ¶ added in v0.7.0
Matches reports whether the given time matches the schedule. This checks if t would be an execution time for the schedule.
For minute-level schedules, seconds and nanoseconds in t are ignored. For second-level schedules, nanoseconds are ignored.
Returns false if schedule is nil or doesn't implement ScheduleWithPrev.
Example:
schedule, _ := cron.ParseStandard("0 9 * * MON-FRI")
if cron.Matches(schedule, time.Now()) {
fmt.Println("Now is a scheduled execution time!")
}
func NextN ¶ added in v0.7.0
NextN returns the next n execution times for the schedule, starting after t. Returns nil if schedule is nil or n <= 0.
This is useful for:
- Calendar previews showing upcoming executions
- Capacity planning
- Debugging schedule expressions
Example:
schedule, _ := cron.ParseStandard("0 9 * * MON-FRI")
times := cron.NextN(schedule, time.Now(), 10)
for _, t := range times {
fmt.Println("Next run:", t)
}
func NormalizeDOW ¶ added in v0.7.0
NormalizeDOW normalizes the day-of-week bitmask by mapping bit 7 (Sunday as 7) to bit 0 (Sunday as 0). This allows both "0" and "7" to represent Sunday, matching the behavior of many cron implementations.
func ValidateSpec ¶ added in v0.7.0
func ValidateSpec(spec string, options ...ParseOption) error
ValidateSpec validates a cron expression without scheduling a job. It returns nil if the spec is valid, or an error describing the problem.
By default, it uses the standard parser (5-field cron + descriptors). Pass a ParseOption to customize validation (e.g., to require seconds field).
Example:
// Validate user input
if err := cron.ValidateSpec(userInput); err != nil {
return fmt.Errorf("invalid cron expression: %w", err)
}
// Validate with seconds field
if err := cron.ValidateSpec(userInput, cron.Second|cron.Minute|cron.Hour|cron.Dom|cron.Month|cron.Dow); err != nil {
// Handle error
}
func ValidateSpecs ¶ added in v0.7.0
func ValidateSpecs(specs []string, options ...ParseOption) map[int]error
ValidateSpecs validates multiple cron expressions at once. It returns a map of index to error for any invalid specs. If all specs are valid, returns an empty map (not nil).
This is useful for:
- Validating configuration files before deployment
- Bulk validation with detailed error reporting
- Pre-flight checks before registering multiple jobs
Example:
specs := []string{"* * * * *", "invalid", "0 9 * * MON-FRI", "bad"}
errors := cron.ValidateSpecs(specs)
if len(errors) > 0 {
for idx, err := range errors {
log.Printf("Spec %d is invalid: %v", idx, err)
}
}
// For all-or-nothing validation:
if len(errors) > 0 {
return fmt.Errorf("invalid specs: %v", errors)
}
// Now safe to add all specs
for _, spec := range specs {
c.AddFunc(spec, handler)
}
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.
func (ConstantDelaySchedule) Prev ¶ added in v0.7.0
func (schedule ConstantDelaySchedule) Prev(t time.Time) time.Time
Prev returns the previous activation time, earlier than the given time. For ConstantDelaySchedule, this simply subtracts the delay. If the delay is zero or negative (invalid), returns t - 1 second as a safe fallback.
Example ¶
This example demonstrates using ConstantDelaySchedule.Prev(). For constant delays, Prev() simply subtracts the delay interval.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
// Create a schedule that runs every 5 minutes
schedule := cron.Every(5 * time.Minute)
now := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)
// Get the previous execution time
prev := schedule.Prev(now)
fmt.Printf("Previous: %s\n", prev.Format("15:04:05"))
// Chain to get earlier times
prev2 := schedule.Prev(prev)
fmt.Printf("Before that: %s\n", prev2.Format("15:04:05"))
}
Output: Previous: 11:55:00 Before that: 11:50:00
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) AddOnceFunc ¶ added in v0.7.0
AddOnceFunc adds a func to run once on the given schedule, then automatically remove itself. This is a convenience wrapper that combines AddFunc with WithRunOnce().
Example:
// Send reminder in 24 hours
c.AddOnceFunc("@in 24h", sendReminder)
// Run at specific time
c.AddOnceFunc("0 9 25 12 *", sendChristmasGreeting, cron.WithName("christmas"))
Example ¶
This example demonstrates AddOnceFunc as a convenience method. It's equivalent to AddFunc with WithRunOnce().
package main
import (
"fmt"
cron "github.com/netresearch/go-cron"
)
func main() {
c := cron.New()
done := make(chan struct{})
// AddOnceFunc is shorthand for AddFunc(..., WithRunOnce())
c.AddOnceFunc("@every 1s", func() {
fmt.Println("One-time job executed")
close(done)
})
c.Start()
<-done
c.Stop()
fmt.Println("Job completed and removed")
}
Output: One-time job executed Job completed and removed
func (*Cron) AddOnceJob ¶ added in v0.7.0
AddOnceJob adds a Job to run once on the given schedule, then automatically remove itself. This is a convenience wrapper that combines AddJob with WithRunOnce().
Example:
c.AddOnceJob("@in 1h", myJob, cron.WithName("one-time-task"))
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) IsRunning ¶ added in v0.7.0
IsRunning returns true if the cron scheduler is currently running. This can be used for health checks, conditional starts, or debugging.
Example ¶
This example demonstrates checking if the scheduler is running.
package main
import (
"fmt"
cron "github.com/netresearch/go-cron"
)
func main() {
c := cron.New()
fmt.Printf("Before Start: %v\n", c.IsRunning())
c.Start()
fmt.Printf("After Start: %v\n", c.IsRunning())
c.Stop()
fmt.Printf("After Stop: %v\n", c.IsRunning())
}
Output: Before Start: false After Start: true After Stop: false
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) ScheduleOnceJob ¶ added in v0.7.0
ScheduleOnceJob adds a Job to run once on the given schedule, then automatically remove itself. This is a convenience wrapper that combines ScheduleJob with WithRunOnce().
Example:
// Run once at a specific time
schedule := cron.Every(24 * time.Hour)
c.ScheduleOnceJob(schedule, myJob, cron.WithName("one-time"))
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 DomConstraint ¶ added in v0.7.0
type DomConstraint struct {
Type DomConstraintType
N int // For DomLastOffset: offset; for DomNearestWeekday: day number
}
DomConstraint represents a dynamic day-of-month constraint.
type DomConstraintType ¶ added in v0.7.0
type DomConstraintType uint8
DomConstraintType identifies the type of day-of-month constraint.
const ( // DomLast represents 'L' - the last day of the month. DomLast DomConstraintType = iota // DomLastOffset represents 'L-n' - n days before the last day of month. DomLastOffset // DomLastWeekday represents 'LW' - the last weekday (Mon-Fri) of month. DomLastWeekday // DomNearestWeekday represents 'nW' - the nearest weekday to day n. DomNearestWeekday )
type DowConstraint ¶ added in v0.7.0
type DowConstraint struct {
Weekday int // 0-6 (Sunday=0, Saturday=6)
N int // 1-5 for nth occurrence, -1 for last occurrence
}
DowConstraint represents an nth-weekday-of-month constraint. For example, FRI#3 means "3rd Friday of the month".
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 WithPrev ¶ added in v0.7.0
WithPrev sets the previous execution time for an entry. This is useful for:
- Schedule migration: preserving execution history when moving jobs between schedulers
- Missed execution detection: combined with schedule analysis to detect missed runs
- Process restarts: preserving interval-based job continuity across restarts
Example:
// Migrate job with preserved history
lastRun := loadLastRunFromDB()
c.AddFunc("@every 1h", cleanup, cron.WithPrev(lastRun))
Example ¶
This example demonstrates WithPrev to preserve previous execution time. This is useful for schedule migration or detecting missed executions.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
c := cron.New()
defer c.Stop()
// Simulate migrating a job from another scheduler
// Set the previous execution time to preserve history
lastRun := time.Date(2025, 1, 1, 9, 0, 0, 0, time.UTC)
id, _ := c.AddFunc("@hourly", func() {
fmt.Println("Running hourly job")
}, cron.WithPrev(lastRun))
entry := c.Entry(id)
fmt.Printf("Previous run preserved: %v\n", entry.Prev.Equal(lastRun))
}
Output: Previous run preserved: true
Example (CombinedWithRunImmediately) ¶
This example demonstrates combining WithPrev and WithRunImmediately. This is useful for process restarts where you want to preserve history but also ensure the job runs immediately after restart.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
c := cron.New()
lastRun := time.Date(2025, 1, 1, 9, 0, 0, 0, time.UTC)
done := make(chan struct{})
id, _ := c.AddFunc("@hourly", func() {
fmt.Println("Job ran")
close(done)
}, cron.WithPrev(lastRun), cron.WithRunImmediately())
entry := c.Entry(id)
fmt.Printf("Prev preserved: %v\n", entry.Prev.Equal(lastRun))
c.Start()
<-done // Wait for job to complete
c.Stop()
fmt.Println("Both options work together")
}
Output: Prev preserved: true Job ran Both options work together
func WithRunImmediately ¶ added in v0.7.0
func WithRunImmediately() JobOption
WithRunImmediately causes the job to run immediately upon registration, then follow the normal schedule thereafter. This is useful for:
- Initial sync: running a sync job once at startup before regular schedule
- Health checks: ensuring service connectivity is verified immediately
- Cache warming: populating caches before the first scheduled refresh
Example:
// Run immediately, then every hour
c.AddFunc("@every 1h", syncData, cron.WithRunImmediately())
Example ¶
This example demonstrates WithRunImmediately to run a job once at registration. The job runs immediately, then follows the normal schedule.
package main
import (
"fmt"
cron "github.com/netresearch/go-cron"
)
func main() {
c := cron.New()
done := make(chan struct{})
c.AddFunc("@hourly", func() {
fmt.Println("Job executed immediately")
close(done)
}, cron.WithRunImmediately())
c.Start()
<-done // Wait for job to complete
c.Stop()
fmt.Println("Done")
}
Output: Job executed immediately Done
func WithRunOnce ¶ added in v0.7.0
func WithRunOnce() JobOption
WithRunOnce causes the job to be automatically removed after its first execution. This is useful for:
- One-time scheduled tasks: "send reminder in 24 hours"
- Deferred execution: schedule a task for later without manual cleanup
- Temporary events: schedule something for a specific time, then forget it
The job is removed from the scheduler after it is dispatched, regardless of whether the job succeeds or fails. The job's goroutine continues to run independently after the entry is removed.
WithRunOnce works correctly with job wrappers like Recover and RetryWithBackoff: the entry is removed after dispatch, but retries happen within the job's goroutine.
Can be combined with WithRunImmediately to run once immediately:
// Run once right now
c.AddFunc("@every 1h", task, cron.WithRunOnce(), cron.WithRunImmediately())
Example:
// Send reminder in 24 hours, then remove from scheduler
c.AddFunc("@in 24h", sendReminder, cron.WithRunOnce())
// Run at specific time, then remove
c.AddFunc("0 9 25 12 *", sendChristmasGreeting, cron.WithRunOnce())
Example ¶
This example demonstrates WithRunOnce for single-execution jobs. Run-once jobs execute at their scheduled time and are automatically removed from the scheduler after execution.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
c := cron.New()
executed := make(chan struct{})
// Add a job that will only run once, then auto-remove
id, _ := c.AddFunc("@every 1s", func() {
fmt.Println("Job executed")
close(executed)
}, cron.WithRunOnce())
c.Start()
<-executed // Wait for job to run
// Give scheduler time to remove the entry
time.Sleep(10 * time.Millisecond)
// The entry no longer exists
entry := c.Entry(id)
fmt.Printf("Entry exists after execution: %v\n", entry.ID != 0)
c.Stop()
}
Output: Job executed Entry exists after execution: false
Example (WithRecover) ¶
This example demonstrates run-once jobs work correctly with Recover wrapper. The entry is removed after dispatch, even if the job panics.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
c := cron.New(cron.WithChain(
cron.Recover(cron.DefaultLogger),
))
done := make(chan struct{})
id, _ := c.AddFunc("@every 1s", func() {
close(done)
panic("intentional panic")
}, cron.WithRunOnce())
c.Start()
<-done
time.Sleep(50 * time.Millisecond) // Wait for scheduler to process removal
// Entry is still removed even though job panicked
entry := c.Entry(id)
fmt.Printf("Entry removed after panic: %v\n", entry.ID == 0)
c.Stop()
}
Output: Entry removed after panic: true
Example (WithRunImmediately) ¶
This example demonstrates combining WithRunOnce with WithRunImmediately. The job runs immediately when added and is then removed from the scheduler.
package main
import (
"fmt"
cron "github.com/netresearch/go-cron"
)
func main() {
c := cron.New()
done := make(chan struct{})
// Run immediately AND only once - useful for deferred tasks
c.AddFunc("@hourly", func() {
fmt.Println("Immediate one-time execution")
close(done)
}, cron.WithRunOnce(), cron.WithRunImmediately())
c.Start()
<-done
c.Stop()
fmt.Println("Job ran immediately and was removed")
}
Output: Immediate one-time execution Job ran immediately and was removed
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 Jitter ¶ added in v0.7.0
func Jitter(maxJitter time.Duration) JobWrapper
Jitter adds a random delay before job execution to prevent thundering herd. When many jobs are scheduled at the same time (e.g., @hourly), they would all execute simultaneously, causing database connection spikes, API rate limiting, and resource contention. Jitter spreads out the execution times.
The delay is uniformly distributed in the range [0, maxJitter). A maxJitter of 0 or negative disables jitter (no delay).
Example:
// Add 0-30s random delay before each execution
cron.NewChain(cron.Jitter(30 * time.Second)).Then(myJob)
// Compose with other wrappers
cron.NewChain(
cron.Recover(logger),
cron.Jitter(30 * time.Second),
cron.SkipIfStillRunning(logger),
).Then(myJob)
// Use via WithChain option
c.AddFunc("@hourly", syncData, cron.WithChain(cron.Jitter(30*time.Second)))
Example ¶
This example demonstrates using Jitter to prevent thundering herd. When many jobs are scheduled at the same time (e.g., @hourly), they would all execute simultaneously. Jitter adds a random delay to spread them out.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
c := cron.New(cron.WithChain(
cron.Recover(cron.DefaultLogger),
// Add 0-30s random delay before each job execution
cron.Jitter(30*time.Second),
))
c.AddFunc("@hourly", func() {
// This job will start between 0-30 seconds after the hour
// Each execution gets a new random delay
fmt.Println("Processing hourly task")
})
c.Start()
defer c.Stop()
}
Example (PerJob) ¶
This example demonstrates applying Jitter to individual jobs. Different jobs can have different jitter ranges.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
c := cron.New(cron.WithChain(
cron.Recover(cron.DefaultLogger),
))
// High-priority job: minimal jitter (0-5s)
highPriorityJob := cron.NewChain(cron.Jitter(5 * time.Second)).Then(cron.FuncJob(func() {
fmt.Println("High priority task")
}))
c.Schedule(cron.Every(time.Hour), highPriorityJob)
// Low-priority job: larger jitter (0-60s)
lowPriorityJob := cron.NewChain(cron.Jitter(60 * time.Second)).Then(cron.FuncJob(func() {
fmt.Println("Low priority task")
}))
c.Schedule(cron.Every(time.Hour), lowPriorityJob)
c.Start()
defer c.Stop()
}
func JitterWithLogger ¶ added in v0.7.0
func JitterWithLogger(logger Logger, maxJitter time.Duration) JobWrapper
JitterWithLogger is like Jitter but logs the applied delay. This is useful for debugging and observability to verify jitter is working.
Example:
cron.NewChain(cron.JitterWithLogger(logger, 30 * time.Second)).Then(myJob)
Example ¶
This example demonstrates JitterWithLogger for observability. The logger records the actual delay applied to each execution.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
logger := cron.DefaultLogger
c := cron.New(cron.WithChain(
cron.Recover(logger),
// Log the jitter delay for each execution
cron.JitterWithLogger(logger, 30*time.Second),
))
c.AddFunc("@hourly", func() {
fmt.Println("Task with logged jitter")
})
c.Start()
defer c.Stop()
}
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 WithSecondOptional ¶ added in v0.7.0
func WithSecondOptional() Option
WithSecondOptional overrides the parser used for interpreting job schedules to accept an optional seconds field as the first one. When provided, expressions can have either 5 fields (standard) or 6 fields (with seconds). If 5 fields are given, the seconds field defaults to 0.
This is useful when you want to support both standard 5-field cron expressions and extended 6-field expressions with seconds precision in the same cron instance.
Examples:
c := cron.New(cron.WithSecondOptional())
c.AddFunc("* * * * *", job) // 5 fields: runs every minute at :00 seconds
c.AddFunc("30 * * * * *", job) // 6 fields: runs every minute at :30 seconds
c.AddFunc("*/10 * * * * *", job) // 6 fields: runs every 10 seconds
func WithSeconds ¶
func WithSeconds() Option
WithSeconds overrides the parser used for interpreting job schedules to include a seconds field as the first one.
type PanicError ¶ added in v0.7.0
type PanicError struct {
Value any // The original panic value
Stack []byte // Stack trace at point of panic
}
PanicError 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 (*PanicError) Error ¶ added in v0.7.0
func (p *PanicError) Error() string
Error implements the error interface for PanicError.
func (*PanicError) String ¶ added in v0.7.0
func (p *PanicError) String() string
String returns a detailed representation including the stack trace.
func (*PanicError) Unwrap ¶ added in v0.7.0
func (p *PanicError) Unwrap() error
Unwrap returns the original panic value if it was an error.
type PanicWithStack
deprecated
added in
v0.6.0
type PanicWithStack = PanicError
PanicWithStack is a type alias for backward compatibility.
Deprecated: Use PanicError instead. This alias will be removed in a future release.
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. Year // Year field, default * (any year) YearOptional // Optional year field, auto-detected by value >= 100 Hash // Allow Jenkins-style 'H' hash expressions for load distribution DowNth // Allow #n syntax in DOW (e.g., FRI#3 for 3rd Friday) DowLast // Allow #L syntax in DOW (e.g., FRI#L for last Friday) DomL // Allow L syntax in DOM (e.g., L for last day, L-3 for 3rd last day) DomW // Allow W syntax in DOM (e.g., 15W for nearest weekday, LW for last weekday) )
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 FullParser ¶ added in v0.8.0
func FullParser() Parser
FullParser returns a parser that accepts all cron syntax variants including optional seconds, year field, descriptors, hash expressions, and extended day-of-month/day-of-week syntax.
This parser supports:
- Standard 5-field cron: minute, hour, day-of-month, month, day-of-week
- Optional seconds prefix (6 fields): second, minute, hour, dom, month, dow
- Optional year suffix (7 fields with seconds, 6 without)
- Descriptors: @yearly, @monthly, @weekly, @daily, @hourly, @every <duration>
- Hash expressions: H for load-distributed scheduling
- Extended syntax: FRI#3 (3rd Friday), MON#L (last Monday), L (last day), 15W (nearest weekday)
Example:
c := cron.New(cron.WithParser(cron.FullParser()))
c.AddFunc("0 30 14 25 12 2025", myFunc) // Run at 14:30 on Dec 25, 2025
c.AddFunc("0 0 0 1 1 * 2030", myFunc) // Run at midnight on Jan 1, 2030
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")
Example (Hash) ¶
This example demonstrates using Hash expressions to distribute jobs. Different hash keys produce different execution times for the same spec.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Hash)
// Same spec with different keys produces different schedules
sched1, _ := parser.ParseWithHashKey("H * * * *", "job-a")
sched2, _ := parser.ParseWithHashKey("H * * * *", "job-b")
from := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
next1 := sched1.Next(from)
next2 := sched2.Next(from)
// The two jobs run at different minutes
fmt.Printf("job-a runs at minute: %d\n", next1.Minute())
fmt.Printf("job-b runs at minute: %d\n", next2.Minute())
fmt.Printf("Different times: %v\n", next1.Minute() != next2.Minute())
}
Output: job-a runs at minute: 54 job-b runs at minute: 23 Different times: true
Example (HashRange) ¶
This example demonstrates using H(range) to constrain the hash.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Hash)
// H(0-29) picks a hash-based minute in the first half hour
schedule, _ := parser.ParseWithHashKey("H(0-29) * * * *", "early-job")
from := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
next := schedule.Next(from)
fmt.Printf("Minute: %d\n", next.Minute())
fmt.Printf("In range 0-29: %v\n", next.Minute() <= 29)
}
Output: Minute: 18 In range 0-29: true
Example (HashStep) ¶
This example demonstrates using H/step for distributed interval scheduling.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Hash)
// H/15 runs every 15 minutes, but starts at a hash-determined offset
schedule, _ := parser.ParseWithHashKey("H/15 * * * *", "my-job")
from := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
// Get the first 4 execution times
for i := 0; i < 4; i++ {
next := schedule.Next(from)
fmt.Printf("%s\n", next.Format("15:04"))
from = next
}
}
Output: 00:07 00:22 00:37 00:52
Example (YearField) ¶
This example demonstrates parsing cron expressions with a year field. The Year option enables 6-field expressions (minute hour dom month dow year) or 7-field expressions when combined with Second.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
// Create parser with Year field support (6 fields total)
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Year)
// Schedule for January 1, 2025 at midnight
schedule, err := parser.Parse("0 0 1 1 * 2025")
if err != nil {
fmt.Println("Error:", err)
return
}
// Find next execution from December 2024
from := time.Date(2024, 12, 15, 0, 0, 0, 0, time.UTC)
next := schedule.Next(from)
fmt.Printf("Next execution: %s\n", next.Format("2006-01-02 15:04:05"))
}
Output: Next execution: 2025-01-01 00:00:00
Example (YearRange) ¶
This example demonstrates using year ranges to limit schedule execution to specific years. This is useful for temporary schedules or migrations.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
// Create parser with Year field support
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Year)
// Schedule for January 1 at midnight, only in 2024-2026
schedule, err := parser.Parse("0 0 1 1 * 2024-2026")
if err != nil {
fmt.Println("Error:", err)
return
}
// Check from 2023 - should skip to 2024
from := time.Date(2023, 6, 15, 0, 0, 0, 0, time.UTC)
next := schedule.Next(from)
fmt.Printf("First in range: %d\n", next.Year())
// Check after 2026 - should return zero time
from2 := time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC)
next2 := schedule.Next(from2)
fmt.Printf("After range is zero: %v\n", next2.IsZero())
}
Output: First in range: 2024 After range is zero: true
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) ParseWithHashKey ¶ added in v0.7.0
ParseWithHashKey returns a new crontab schedule using the specified hash key for Jenkins-style 'H' expressions. The hash key is used to deterministically compute the offset for H fields, allowing different jobs to be distributed across the time range.
This method must be used when the spec contains 'H' expressions and no default hash key was set via WithHashKey().
Example:
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Hash)
// Each job runs at a different minute based on its name
sched1, _ := parser.ParseWithHashKey("H * * * *", "job-a")
sched2, _ := parser.ParseWithHashKey("H * * * *", "job-b")
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) WithHashKey ¶ added in v0.7.0
WithHashKey returns a new Parser configured with a default hash key for Jenkins-style 'H' expressions. The hash key is used to deterministically distribute execution times across the allowed range.
When a hash key is set, the Parse method can handle H expressions without requiring ParseWithHashKey to be called explicitly.
Example:
// Parser with default hash key for all H expressions
p := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Hash).
WithHashKey("my-service")
// H resolves based on "my-service" hash
sched, _ := p.Parse("H * * * *")
Example ¶
This example demonstrates using WithHashKey for default hash configuration.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
// Configure parser with a default hash key
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Hash).
WithHashKey("default-service")
// Parse can now handle H expressions without explicit hash key
schedule, err := parser.Parse("H * * * *")
if err != nil {
fmt.Println("Error:", err)
return
}
from := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
next := schedule.Next(from)
fmt.Printf("Next at minute: %d\n", next.Minute())
}
Output: Next at minute: 16
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)
func (Parser) WithSecondOptional ¶ added in v0.7.0
WithSecondOptional returns a new Parser configured to accept an optional seconds field as the first field. This allows the parser to accept both 5-field (standard) and 6-field (with seconds) expressions.
When 5 fields are provided, the seconds field defaults to 0. When 6 fields are provided, the first field is interpreted as seconds.
This method enables composable parser configuration when you need both SecondOptional and other parser customizations (like WithMinEveryInterval or WithMaxSearchYears).
Example:
// Parser accepting optional seconds with custom minimum @every interval
p := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor).
WithSecondOptional().
WithMinEveryInterval(100 * time.Millisecond)
// Both expressions are valid:
sched1, _ := p.Parse("* * * * *") // 5 fields, seconds=0
sched2, _ := p.Parse("30 * * * * *") // 6 fields, seconds=30
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
Example (SundayFormats) ¶
This example demonstrates that Sunday can be specified as either 0 or 7 in the day-of-week field, matching traditional cron behavior.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
// Both "0" and "7" represent Sunday
schedSun0, _ := cron.ParseStandard("0 9 * * 0") // Sunday as 0
schedSun7, _ := cron.ParseStandard("0 9 * * 7") // Sunday as 7
// Start from Saturday
saturday := time.Date(2025, 1, 4, 0, 0, 0, 0, time.UTC) // Saturday
// Both find the same Sunday
next0 := schedSun0.Next(saturday)
next7 := schedSun7.Next(saturday)
fmt.Printf("Using 0: %s\n", next0.Format("Mon Jan 2"))
fmt.Printf("Using 7: %s\n", next7.Format("Mon Jan 2"))
fmt.Printf("Same day: %v\n", next0.Equal(next7))
}
Output: Using 0: Sun Jan 5 Using 7: Sun Jan 5 Same day: true
Example (WeekendRange) ¶
This example demonstrates using 7 in day-of-week ranges. The range "5-7" means Friday through Sunday.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
// "5-7" covers Friday(5), Saturday(6), Sunday(7->0)
schedule, _ := cron.ParseStandard("0 10 * * 5-7")
// Start from Wednesday
wednesday := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
next := schedule.Next(wednesday)
fmt.Printf("Next after Wed: %s\n", next.Format("Mon"))
next = schedule.Next(next)
fmt.Printf("Then: %s\n", next.Format("Mon"))
next = schedule.Next(next)
fmt.Printf("Then: %s\n", next.Format("Mon"))
}
Output: Next after Wed: Fri Then: Sat Then: Sun
type ScheduleParser ¶
ScheduleParser is an interface for schedule spec parsers that return a Schedule.
type ScheduleWithPrev ¶ added in v0.7.0
type ScheduleWithPrev interface {
Schedule
// Prev returns the previous activation time, earlier than the given time.
// Returns zero time if no previous time can be found.
Prev(time.Time) time.Time
}
ScheduleWithPrev is an optional interface that schedules can implement to support backward time traversal. This is useful for detecting missed executions or determining the last scheduled run time.
Built-in schedules (SpecSchedule, ConstantDelaySchedule) implement this interface. Custom Schedule implementations may optionally implement it.
Use type assertion to check for support:
if sp, ok := schedule.(ScheduleWithPrev); ok {
prev := sp.Prev(time.Now())
}
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 SpecAnalysis ¶ added in v0.7.0
type SpecAnalysis struct {
// Valid indicates whether the spec was successfully parsed.
Valid bool
// Error contains the parsing error if Valid is false.
Error error
// NextRun is the next scheduled execution time from now.
// Zero if the spec is invalid or represents a one-time past event.
NextRun time.Time
// Location is the timezone for the schedule.
// Defaults to time.Local unless TZ= or CRON_TZ= is specified.
Location *time.Location
// Fields contains the original field values from the spec.
// Keys: "second" (if applicable), "minute", "hour", "day_of_month", "month", "day_of_week"
// For descriptors, this will be empty.
Fields map[string]string
// IsDescriptor indicates if the spec uses a descriptor (@hourly, @every, etc.)
IsDescriptor bool
// Interval is the duration for @every expressions.
// Zero for non-@every specs.
Interval time.Duration
// Schedule is the parsed schedule, available for further introspection.
// Nil if the spec is invalid.
Schedule Schedule
}
SpecAnalysis contains detailed information about a parsed cron specification. It provides insight into the schedule without requiring job registration.
func AnalyzeSpec ¶ added in v0.7.0
func AnalyzeSpec(spec string, options ...ParseOption) SpecAnalysis
AnalyzeSpec provides detailed analysis of a cron expression. It returns a SpecAnalysis struct containing validation status, next run time, parsed fields, and other metadata.
This is useful for:
- Configuration validation with detailed feedback
- UI previews showing when a job will run
- Debugging cron expressions
- Import/migration validation
Example:
result := cron.AnalyzeSpec("0 9 * * MON-FRI")
if !result.Valid {
log.Printf("Invalid: %v", result.Error)
} else {
log.Printf("Next run: %v", result.NextRun)
log.Printf("Fields: %v", result.Fields)
}
func AnalyzeSpecWithHash ¶ added in v0.7.0
func AnalyzeSpecWithHash(spec string, options ParseOption, hashSeed string) SpecAnalysis
AnalyzeSpecWithHash analyzes a cron expression containing H hash expressions. This is like AnalyzeSpec but takes a hash seed for resolving H expressions. The seed should be a unique identifier (like a job name) that produces deterministic, distributed scheduling times.
Example:
result := AnalyzeSpecWithHash("H H * * *", Minute|Hour|Dom|Month|Dow|Hash, "my-job")
if result.Valid {
log.Printf("Next run: %v", result.NextRun)
}
type SpecSchedule ¶
type SpecSchedule struct {
Second, Minute, Hour, Dom, Month, Dow uint64
// Year stores valid years using sparse storage for unlimited range.
// nil means any year (wildcard). An empty map means no valid years.
// Uses map[int]struct{} for O(1) lookup with minimal memory overhead.
Year map[int]struct{}
// 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
// DomConstraints holds dynamic day-of-month constraints that cannot be
// pre-computed into bitmasks (L, L-n, LW, nW). These are evaluated at
// match time because they depend on the specific month.
DomConstraints []DomConstraint
// DowConstraints holds nth-weekday-of-month constraints (e.g., FRI#3, MON#L).
// These are evaluated at match time because they depend on which dates
// fall on which weekdays in the specific month.
DowConstraints []DowConstraint
}
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.
type ValidationError ¶ added in v0.7.0
type ValidationError struct {
Message string
Field string // Optional: which field caused the error
Value string // Optional: the invalid value
}
ValidationError represents a cron expression validation error.
func (*ValidationError) Error ¶ added in v0.7.0
func (e *ValidationError) Error() string