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.
Wraparound Ranges ¶
For cyclic fields (seconds, minutes, hours, day-of-week, month), ranges where the start value is greater than the end value are interpreted as wraparound ranges that span across the field boundary. For example:
22-2 (hours) = 22, 23, 0, 1, 2 (spans midnight) FRI-MON (dow) = FRI, SAT, SUN, MON (spans the weekend) NOV-FEB (month) = NOV, DEC, JAN, FEB (spans year boundary) 55-5 (minutes) = 55, 56, 57, 58, 59, 0, 1, 2, 3, 4, 5
This is useful for schedules that span midnight, weekends, or year boundaries.
Wraparound ranges also support step values:
22-2/2 (hours) = 22, 0, 2 (every 2 hours from 10pm to 2am)
Day-of-month wraparound works correctly even for months with fewer days:
25-5 (dom) = 25, 26, 27, 28, 29, 30, 31, 1, 2, 3, 4, 5
In February (28 days), days 29-31 simply don't match and are skipped.
Question mark ( ? )
Question mark may be used instead of '*' for leaving either day-of-month or day-of-week blank.
Day Matching (DOM/DOW) ¶
When both day-of-month and day-of-week are specified (non-wildcard), both must match (AND logic). This is consistent with how all other cron fields work.
0 0 25-31 * FRI - Last Friday of month (days 25-31 AND Friday) 0 0 1-7 * MON - First Monday of month (days 1-7 AND Monday) 0 0 13 * FRI - Friday the 13th
When either field is a wildcard (*), only the restricted field matters:
0 0 * * FRI - Every Friday (any day-of-month that is Friday) 0 0 15 * * - 15th of every month (any day-of-week)
This differs from some cron implementations (Vixie cron, robfig/cron) that use OR logic when both fields are restricted. For legacy OR behavior:
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.DowOrDom)
With DowOrDom enabled, the schedule matches if either field matches (OR logic):
0 0 15 * FRI - 15th of month OR any Friday (legacy behavior)
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 CircuitBreakerWithHandle(logger Logger, threshold int, cooldown time.Duration, ...) (JobWrapper, *CircuitBreakerHandle)
- func Count(schedule Schedule, start, end time.Time) int
- func CountWithLimit(schedule Schedule, start, end time.Time, limit int) int
- func IsTriggered(s Schedule) bool
- 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 PrevN(schedule Schedule, t time.Time, n int) []time.Time
- func ValidateSpec(spec string, options ...ParseOption) error
- func ValidateSpecWith(spec string, parser ScheduleParser) error
- func ValidateSpecs(specs []string, options ...ParseOption) map[int]error
- func WorkflowExecutionID(ctx context.Context) string
- type Chain
- type CircuitBreakerEvent
- type CircuitBreakerHandle
- type CircuitBreakerOption
- type CircuitBreakerState
- type Clock
- type ConstantDelaySchedule
- type Cron
- func (c *Cron) ActiveWorkflows() []WorkflowExecution
- func (c *Cron) AddDependency(child, parent EntryID, condition TriggerCondition) error
- func (c *Cron) AddDependencyByName(child, parent string, condition TriggerCondition) error
- 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) AddWorkflow(w *Workflow) error
- func (c *Cron) Dependencies(id EntryID) []Dependency
- func (c *Cron) DependenciesByName(name string) []Dependency
- 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) IsEntryPaused(id EntryID) bool
- func (c *Cron) IsEntryPausedByName(name string) bool
- func (c *Cron) IsJobRunning(id EntryID) bool
- func (c *Cron) IsJobRunningByName(name string) bool
- func (c *Cron) IsRunning() bool
- func (c *Cron) Location() *time.Location
- func (c *Cron) PauseEntry(id EntryID) error
- func (c *Cron) PauseEntryByName(name string) error
- func (c *Cron) Remove(id EntryID)
- func (c *Cron) RemoveByName(name string) bool
- func (c *Cron) RemoveByTag(tag string) int
- func (c *Cron) RemoveDependency(child, parent EntryID) error
- func (c *Cron) RemoveDependencyByName(child, parent string) error
- func (c *Cron) ResumeEntry(id EntryID) error
- func (c *Cron) ResumeEntryByName(name string) error
- 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
- func (c *Cron) TriggerEntry(id EntryID) error
- func (c *Cron) TriggerEntryByName(name string) error
- func (c *Cron) UpdateEntry(id EntryID, schedule Schedule, job Job) error
- func (c *Cron) UpdateEntryByName(name string, schedule Schedule, job Job) error
- func (c *Cron) UpdateEntryJob(id EntryID, spec string, job Job) error
- func (c *Cron) UpdateEntryJobByName(name, spec string, job Job) error
- func (c *Cron) UpdateJob(id EntryID, spec string) error
- func (c *Cron) UpdateJobByName(name, spec string) error
- func (c *Cron) UpdateSchedule(id EntryID, schedule Schedule) error
- func (c *Cron) UpdateScheduleByName(name string, schedule Schedule) error
- func (c *Cron) UpsertJob(spec string, cmd Job, opts ...JobOption) (EntryID, error)
- func (c *Cron) ValidateSpec(spec string) error
- func (c *Cron) WaitForJob(id EntryID)
- func (c *Cron) WaitForJobByName(name string)
- func (c *Cron) WorkflowStatus(executionID string) *WorkflowExecution
- type Dependency
- type DomConstraint
- type DomConstraintType
- type DowConstraint
- type Entry
- type EntryID
- type ErrorJob
- type FakeClock
- type FuncErrorJob
- type FuncJob
- type FuncJobWithContext
- type Job
- type JobOption
- func WithMissedGracePeriod(d time.Duration) JobOption
- func WithMissedPolicy(policy MissedPolicy) JobOption
- func WithName(name string) JobOption
- func WithPaused() JobOption
- func WithPrev(prev time.Time) JobOption
- func WithRunImmediately() JobOption
- func WithRunOnce() JobOption
- func WithTags(tags ...string) JobOption
- type JobResult
- 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 MaxConcurrent(n int) JobWrapper
- func MaxConcurrentSkip(logger Logger, n int) JobWrapper
- func Recover(logger Logger, opts ...RecoverOption) JobWrapper
- func RetryOnError(logger Logger, maxRetries int, initialDelay, maxDelay time.Duration, ...) 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 MissedPolicy
- type NamedJob
- type ObservabilityHooks
- type Option
- func WithCapacity(n int) 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
- func WithWorkflowRetention(n int) 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 RetryAttempt
- type RetryOption
- type Schedule
- type ScheduleParser
- type ScheduleWithPrev
- type SlogLogger
- type SpecAnalysis
- type SpecSchedule
- type TimeoutOption
- type Timer
- type TriggerCondition
- type TriggeredSchedule
- type ValidationError
- type Workflow
- type WorkflowExecution
- type WorkflowStep
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 ErrCycleDetected = errors.New("cron: dependency would create a cycle")
ErrCycleDetected is returned by AddDependency when the new edge would create a cycle.
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 ErrEmptyWorkflow = errors.New("cron: workflow has no steps")
ErrEmptyWorkflow is returned by AddWorkflow when the workflow has no steps.
var ErrEntryNotFound = errors.New("cron: entry not found")
ErrEntryNotFound is returned by UpdateSchedule, UpdateScheduleByName, UpdateJob, UpdateJobByName, UpdateEntry, UpdateEntryByName, UpdateEntryJob, UpdateEntryJobByName, UpsertJob, PauseEntry, PauseEntryByName, ResumeEntry, ResumeEntryByName, TriggerEntry, and TriggerEntryByName when the specified entry does not exist in this Cron instance.
var ErrEntryPaused = errors.New("cron: entry is paused")
ErrEntryPaused is returned by TriggerEntry and TriggerEntryByName when attempting to trigger a paused entry. Resume the entry first.
var ErrInvalidCondition = errors.New("cron: invalid trigger condition")
ErrInvalidCondition is returned by AddDependency when the trigger condition is not valid.
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 ErrMultipleFinalSteps = errors.New("cron: workflow has multiple final steps")
ErrMultipleFinalSteps is returned by AddWorkflow when more than one step is marked Final.
var ErrMultipleOptionals = errors.New("multiple optionals may not be configured")
ErrMultipleOptionals is returned when more than one optional field is configured.
var ErrNameRequired = errors.New("cron: UpsertJob requires WithName option")
ErrNameRequired is returned by UpsertJob when no WithName option is provided. UpsertJob requires a name to determine whether to create or update.
var ErrNilJob = errors.New("cron: job must not be nil; use UpdateSchedule to update only the schedule")
ErrNilJob is returned by UpdateEntry and UpdateEntryByName when a nil job is passed. Use UpdateSchedule to update only the schedule.
var ErrNoFields = errors.New("at least one field or Descriptor must be configured")
ErrNoFields is returned when no fields or Descriptor are configured.
var ErrNotRunning = errors.New("cron: scheduler is not running")
ErrNotRunning is returned by TriggerEntry and TriggerEntryByName when the scheduler is not running. Start the scheduler first.
var ErrUnknownStep = errors.New("cron: workflow step references unknown parent")
ErrUnknownStep is returned by AddWorkflow when a step references an unknown parent via After.
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 CircuitBreakerWithHandle ¶ added in v0.12.0
func CircuitBreakerWithHandle(logger Logger, threshold int, cooldown time.Duration, opts ...CircuitBreakerOption) (JobWrapper, *CircuitBreakerHandle)
CircuitBreakerWithHandle is like CircuitBreaker but also returns a handle for querying the circuit breaker's internal state. The handle is safe for concurrent use and can be used from health checks, dashboards, or metrics exporters.
Example:
wrapper, handle := cron.CircuitBreakerWithHandle(logger, 5, 5*time.Minute)
c := cron.New(cron.WithChain(cron.Recover(logger), wrapper))
// In a health check endpoint:
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
state := handle.State()
if state == cron.CircuitOpen {
http.Error(w, "circuit open", http.StatusServiceUnavailable)
return
}
w.Write([]byte("ok"))
})
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 IsTriggered ¶ added in v0.12.0
IsTriggered reports whether the given schedule is a TriggeredSchedule. This can be used to distinguish triggered entries from regularly scheduled ones.
if cron.IsTriggered(entry.Schedule) {
fmt.Println("This entry only runs when triggered manually")
}
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 PrevN ¶ added in v0.12.0
PrevN returns the previous n execution times for the schedule, before t. Returns nil if schedule is nil, n <= 0, or schedule doesn't implement ScheduleWithPrev.
Times are returned in reverse chronological order (most recent first). Stops early if Prev() returns zero time (no earlier execution exists).
This is useful for:
- Audit logs showing recent executions
- Debugging missed executions
- Historical schedule analysis
Example:
schedule, _ := cron.ParseStandard("0 9 * * MON-FRI")
times := cron.PrevN(schedule, time.Now(), 10)
for _, t := range times {
fmt.Println("Previous run:", t)
}
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 ValidateSpecWith ¶ added in v0.10.0
func ValidateSpecWith(spec string, parser ScheduleParser) error
ValidateSpecWith validates a cron expression using any ScheduleParser implementation. This is useful when you have a custom parser or a pre-configured Parser instance.
Example:
// Validate with a custom parser instance
parser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Hash).
WithHashKey("my-job")
if err := cron.ValidateSpecWith("H * * * * *", parser); 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)
}
func WorkflowExecutionID ¶ added in v0.12.0
WorkflowExecutionID returns the workflow execution ID from the context, or empty string if the job is not part of a workflow.
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 CircuitBreakerEvent ¶ added in v0.12.0
type CircuitBreakerEvent struct {
OldState CircuitBreakerState // State before the transition
NewState CircuitBreakerState // State after the transition
Failures int64 // Current consecutive failure count
Err any // Panic value that caused the transition (nil on success)
}
CircuitBreakerEvent represents a state transition in the circuit breaker. This is passed to the callback configured via WithStateChangeCallback.
type CircuitBreakerHandle ¶ added in v0.12.0
type CircuitBreakerHandle struct {
// contains filtered or unexported fields
}
CircuitBreakerHandle provides read-only access to the internal state of a circuit breaker. Obtain one via CircuitBreakerWithHandle.
All methods are safe for concurrent use.
func (*CircuitBreakerHandle) CooldownEnds ¶ added in v0.12.0
func (h *CircuitBreakerHandle) CooldownEnds() time.Time
CooldownEnds returns when the current cooldown period expires. Returns the zero time if the circuit is not open.
func (*CircuitBreakerHandle) Failures ¶ added in v0.12.0
func (h *CircuitBreakerHandle) Failures() int64
Failures returns the current consecutive failure count.
func (*CircuitBreakerHandle) LastFailure ¶ added in v0.12.0
func (h *CircuitBreakerHandle) LastFailure() time.Time
LastFailure returns the time of the last recorded failure. Returns the zero time if no failures have been recorded.
func (*CircuitBreakerHandle) State ¶ added in v0.12.0
func (h *CircuitBreakerHandle) State() CircuitBreakerState
State returns the current circuit breaker state.
type CircuitBreakerOption ¶ added in v0.12.0
type CircuitBreakerOption func(*circuitBreakerConfig)
CircuitBreakerOption configures optional behavior for CircuitBreaker.
func WithStateChangeCallback ¶ added in v0.12.0
func WithStateChangeCallback(fn func(CircuitBreakerEvent)) CircuitBreakerOption
WithStateChangeCallback sets a callback invoked on circuit breaker state transitions. This enables external monitoring and metrics collection.
State transitions that trigger the callback:
- Closed → Open (threshold failures reached)
- Open → HalfOpen (cooldown expired, probe attempted)
- HalfOpen → Closed (probe succeeded)
- HalfOpen → Open (probe failed)
The callback is invoked synchronously within the job execution goroutine. Keep callbacks fast to avoid delaying job execution.
Example with Prometheus:
CircuitBreaker(logger, 5, 5*time.Minute,
cron.WithStateChangeCallback(func(e cron.CircuitBreakerEvent) {
circuitState.WithLabelValues(e.NewState.String()).Set(1)
if e.NewState == cron.CircuitOpen {
circuitTrips.Inc()
}
}),
)
type CircuitBreakerState ¶ added in v0.12.0
type CircuitBreakerState int
CircuitBreakerState represents the current state of a circuit breaker.
const ( // CircuitClosed means the circuit is operating normally. // Failures increment the counter; threshold failures will open the circuit. CircuitClosed CircuitBreakerState = iota // CircuitOpen means the circuit has tripped. Executions are skipped // until the cooldown period expires. CircuitOpen // CircuitHalfOpen means the cooldown has expired and one probe execution // is allowed. Success closes the circuit; failure reopens it. CircuitHalfOpen )
func (CircuitBreakerState) String ¶ added in v0.12.0
func (s CircuitBreakerState) String() string
String returns the human-readable name of the circuit breaker state.
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) ActiveWorkflows ¶ added in v0.12.0
func (c *Cron) ActiveWorkflows() []WorkflowExecution
ActiveWorkflows returns copies of all in-progress workflow executions. Returns an empty slice if no workflows are currently executing.
func (*Cron) AddDependency ¶ added in v0.12.0
func (c *Cron) AddDependency(child, parent EntryID, condition TriggerCondition) error
AddDependency adds a dependency edge: child waits for parent with the given condition.
func (*Cron) AddDependencyByName ¶ added in v0.12.0
func (c *Cron) AddDependencyByName(child, parent string, condition TriggerCondition) error
AddDependencyByName is the name-based variant of AddDependency.
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) AddWorkflow ¶ added in v0.12.0
AddWorkflow validates and registers all steps of a Workflow atomically. It parses all specs, checks for duplicate names, validates the DAG structure (no cycles, at most one final step, all After references exist), and then registers all entries via AddJob and wires dependency edges via AddDependency. On any failure, already-registered entries are rolled back.
Failure model: the workflow engine detects job failure via panics. Since Job.Run() has no return value, steps that need to signal errors should use FuncErrorJob (which converts errors to panics) or wrappers like RetryOnError / RetryWithBackoff. The Recover wrapper is workflow-aware and re-panics in workflow context so failures propagate correctly.
Returns:
- ErrEmptyWorkflow if the workflow has no steps
- ErrMultipleFinalSteps if more than one step is marked Final
- ErrUnknownStep if a step references an unknown parent via After
- ErrCycleDetected if the step dependencies form a cycle
- ErrDuplicateName if a step name conflicts with an existing entry
- Parse errors if any spec is invalid for the configured parser
func (*Cron) Dependencies ¶ added in v0.12.0
func (c *Cron) Dependencies(id EntryID) []Dependency
Dependencies returns the dependency edges for an entry.
func (*Cron) DependenciesByName ¶ added in v0.12.0
func (c *Cron) DependenciesByName(name string) []Dependency
DependenciesByName is the name-based variant of Dependencies.
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) IsEntryPaused ¶ added in v0.12.0
IsEntryPaused reports whether the entry with the given ID is currently paused. Returns false if the entry does not exist.
func (*Cron) IsEntryPausedByName ¶ added in v0.12.0
IsEntryPausedByName reports whether the named entry is currently paused. Returns false if no entry has the given name.
func (*Cron) IsJobRunning ¶ added in v0.11.0
IsJobRunning reports whether the entry with the given ID has any invocations currently in flight. Returns false if the entry does not exist.
func (*Cron) IsJobRunningByName ¶ added in v0.11.0
IsJobRunningByName reports whether the named entry has any invocations currently in flight. Returns false if no entry has the given name.
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) PauseEntry ¶ added in v0.12.0
PauseEntry temporarily suspends the entry identified by id. While paused, the entry remains registered and its schedule advances, but execution is skipped. Use ResumeEntry to re-enable execution.
Pausing an already-paused entry is a no-op (returns nil).
Returns ErrEntryNotFound if no entry with the given id exists.
func (*Cron) PauseEntryByName ¶ added in v0.12.0
PauseEntryByName temporarily suspends the entry identified by its Name. Lookup is O(1) via the internal name index.
Returns ErrEntryNotFound if no entry with the given name exists.
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) RemoveDependency ¶ added in v0.12.0
RemoveDependency removes a dependency edge between child and parent.
func (*Cron) RemoveDependencyByName ¶ added in v0.12.0
RemoveDependencyByName is the name-based variant of RemoveDependency.
func (*Cron) ResumeEntry ¶ added in v0.12.0
ResumeEntry re-enables execution of a previously paused entry. The entry's schedule is preserved; it will execute at its next scheduled time.
Resuming an already-active entry is a no-op (returns nil).
Returns ErrEntryNotFound if no entry with the given id exists.
func (*Cron) ResumeEntryByName ¶ added in v0.12.0
ResumeEntryByName re-enables execution of a previously paused entry identified by its Name. Lookup is O(1) via the internal name index.
Returns ErrEntryNotFound if no entry with the given name exists.
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")
}
func (*Cron) TriggerEntry ¶ added in v0.12.0
TriggerEntry immediately executes the entry with the given ID, regardless of its schedule. The entry's middleware chain (Recover, SkipIfStillRunning, etc.) is applied as usual. This works on both triggered (@triggered) and regularly scheduled entries — providing a "run now" capability for any entry.
The scheduler must be running; returns ErrNotRunning otherwise. Returns ErrEntryPaused if the entry is paused. Returns ErrEntryNotFound if no entry with the given ID exists.
Example:
id, _ := c.AddFunc("@triggered", deploy, cron.WithName("deploy"))
c.Start()
c.TriggerEntry(id) // Run on demand
func (*Cron) TriggerEntryByName ¶ added in v0.12.0
TriggerEntryByName immediately executes the entry identified by its Name. Lookup is O(1) via the internal name index.
Returns ErrNotRunning if the scheduler is not running. Returns ErrEntryPaused if the entry is paused. Returns ErrEntryNotFound if no entry with the given name exists.
Example:
c.AddFunc("@triggered", deploy, cron.WithName("deploy"))
c.Start()
c.TriggerEntryByName("deploy") // Run on demand
func (*Cron) UpdateEntry ¶ added in v0.11.0
UpdateEntry atomically replaces both the Schedule and the Job of an existing entry identified by id. The new job is re-wrapped through the configured Chain, so middleware (Recover, SkipIfStillRunning, etc.) is applied to the replacement job. The job parameter must not be nil; to update only the schedule, use UpdateSchedule instead.
This is useful when rescheduling requires a new closure—for example, a fresh context.WithCancel per schedule change (the weaviate pattern).
Concurrency semantics are the same as UpdateSchedule.
Returns ErrEntryNotFound if no entry with the given id exists. Returns ErrNilJob if job is nil.
func (*Cron) UpdateEntryByName ¶ added in v0.11.0
UpdateEntryByName atomically replaces both the Schedule and the Job of an existing entry identified by its Name. Lookup is O(1) via the internal name index. Delegates to UpdateEntry for the actual update.
Returns ErrEntryNotFound if no entry with the given name exists.
func (*Cron) UpdateEntryJob ¶ added in v0.11.0
UpdateEntryJob parses spec with the Cron's configured parser, then atomically replaces both schedule and job. This eliminates the need for callers to construct their own parser matching the Cron's configuration.
Returns a parse error if spec is invalid for the configured parser. Returns ErrEntryNotFound if the id does not correspond to an existing entry. Returns ErrNilJob if job is nil.
func (*Cron) UpdateEntryJobByName ¶ added in v0.11.0
UpdateEntryJobByName is the name-based variant of UpdateEntryJob. It parses spec with the Cron's configured parser, then atomically replaces both schedule and job of the entry identified by name.
Returns a parse error if spec is invalid for the configured parser. Returns ErrEntryNotFound if the name does not correspond to an existing entry. Returns ErrNilJob if job is nil.
func (*Cron) UpdateJob ¶ added in v0.10.0
UpdateJob updates the schedule of an existing entry identified by id, parsing the provided cron spec string using this Cron's configured parser.
If the scheduler is running, the update is applied safely via the run loop and takes effect immediately for next-run computation. If stopped, the schedule is updated directly in place.
Returns ErrEntryNotFound if the id does not correspond to an existing entry. Returns a parse error if spec is invalid for the configured parser.
func (*Cron) UpdateJobByName ¶ added in v0.10.0
UpdateJobByName updates the schedule of an existing entry identified by its Name, parsing the provided cron spec using this Cron's configured parser.
Returns ErrEntryNotFound if the name does not correspond to an existing entry. Returns a parse error if the spec is invalid for the configured parser.
func (*Cron) UpdateSchedule ¶ added in v0.10.0
UpdateSchedule updates the Schedule of an existing entry identified by id.
Concurrency semantics:
- If the scheduler is running, the change is routed through the run loop to avoid races, and the heap is adjusted atomically. The new schedule is used to recompute the entry's next run immediately.
- If the scheduler is stopped, the schedule is updated directly.
Returns ErrEntryNotFound if no entry with the given id exists.
func (*Cron) UpdateScheduleByName ¶ added in v0.10.0
UpdateScheduleByName updates the Schedule of an existing entry identified by its Name. Lookup is O(1) via the internal name index. If the scheduler is running, the actual update is delegated to UpdateSchedule which routes through the run loop safely.
Returns ErrEntryNotFound if no entry with the given name exists.
func (*Cron) UpsertJob ¶ added in v0.11.0
UpsertJob creates or updates a named job entry. If an entry with the given name already exists, its schedule and job are atomically replaced via UpdateEntry. If no entry with that name exists, a new one is created via AddJob. The name is determined from the opts; a WithName option is required.
This eliminates the common "try update, fallback to add" boilerplate pattern:
// Before (manual upsert):
if err := c.UpdateEntryJobByName(name, spec, job); errors.Is(err, cron.ErrEntryNotFound) {
c.AddJob(spec, job, cron.WithName(name))
}
// After:
c.UpsertJob(spec, job, cron.WithName(name))
Returns:
- ErrNameRequired if no WithName option is provided
- Parse errors if spec is invalid for the configured parser
- ErrMaxEntriesReached if creating a new entry would exceed the limit
func (*Cron) ValidateSpec ¶ added in v0.10.0
ValidateSpec validates a cron expression using this Cron instance's configured parser. It returns nil if the spec is valid, or an error describing the problem.
This is useful for pre-validating user input before calling AddFunc or AddJob, especially when the Cron instance uses a custom parser (e.g., with seconds or hash support).
Example:
c := cron.New(cron.WithSeconds())
if err := c.ValidateSpec("0 30 * * * *"); err != nil {
return fmt.Errorf("invalid cron expression: %w", err)
}
func (*Cron) WaitForJob ¶ added in v0.11.0
WaitForJob blocks until all currently-running invocations of the given entry complete. Returns immediately if the entry is not currently running or if the entry does not exist.
This is useful for graceful job replacement: callers can wait for the current execution to finish before replacing the job via UpsertJob or UpdateEntry.
cr.WaitForJob(id)
cr.UpsertJob(newSpec, newJob, WithName("my-job"))
func (*Cron) WaitForJobByName ¶ added in v0.11.0
WaitForJobByName blocks until all currently-running invocations of the named entry complete. Returns immediately if the entry is not currently running or if no entry has the given name.
cr.WaitForJobByName("my-job")
cr.UpsertJob(newSpec, newJob, WithName("my-job"))
func (*Cron) WorkflowStatus ¶ added in v0.12.0
func (c *Cron) WorkflowStatus(executionID string) *WorkflowExecution
WorkflowStatus returns the execution state for the given workflow execution ID. It searches active executions first, then completed executions. Returns nil if no execution with the given ID exists.
type Dependency ¶ added in v0.12.0
type Dependency struct {
ParentID EntryID
Condition TriggerCondition
}
Dependency represents a directed edge in the workflow DAG.
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
// MissedPolicy defines how to handle missed executions when the scheduler
// starts or when an entry is added. See MissedPolicy constants.
// Set via WithMissedPolicy(). Default is MissedSkip.
MissedPolicy MissedPolicy
// MissedGracePeriod defines the maximum age of a missed execution that
// should be caught up. If zero, all missed executions (within safety limits)
// are eligible for catch-up. Set via WithMissedGracePeriod().
MissedGracePeriod time.Duration
// Paused indicates whether this entry is temporarily suspended.
// When true, the scheduler skips this entry during execution but keeps
// it registered with its schedule intact. Use PauseEntry/ResumeEntry
// to toggle. Visible in Entry snapshots.
Paused bool
// Triggered indicates whether this entry uses a TriggeredSchedule
// (@triggered, @manual, @none). Triggered entries never fire automatically;
// they must be executed via TriggerEntry or TriggerEntryByName.
// Visible in Entry snapshots.
Triggered bool
// 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 ErrorJob ¶ added in v0.10.0
ErrorJob is an optional interface for jobs that return errors instead of panicking. Jobs implementing this interface can use error-based retry wrappers like RetryOnError, which is more idiomatic Go than the panic-based RetryWithBackoff.
When a job implements ErrorJob, wrappers that understand errors (like RetryOnError) will call RunE() and use the returned error for retry decisions. The standard Run() method should still be implemented (typically delegating to RunE and panicking on error) for compatibility with wrappers that don't understand ErrorJob.
Example:
type APIJob struct{ url string }
func (j *APIJob) Run() {
if err := j.RunE(); err != nil {
panic(err)
}
}
func (j *APIJob) RunE() error {
resp, err := http.Get(j.url)
if err != nil {
return fmt.Errorf("API call failed: %w", err)
}
defer resp.Body.Close()
return nil
}
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 FuncErrorJob ¶ added in v0.10.0
type FuncErrorJob func() error
FuncErrorJob is a wrapper that turns a func() error into an ErrorJob. This enables error-returning jobs using simple functions.
Example:
c.AddJob("@every 5m", cron.FuncErrorJob(func() error {
return callExternalAPI()
}))
func (FuncErrorJob) Run ¶ added in v0.10.0
func (f FuncErrorJob) Run()
Run implements Job by calling RunE and panicking on error. This ensures compatibility with panic-based wrappers like Recover.
func (FuncErrorJob) RunE ¶ added in v0.10.0
func (f FuncErrorJob) RunE() error
RunE implements ErrorJob by calling the wrapped function.
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 WithMissedGracePeriod ¶ added in v0.10.0
WithMissedGracePeriod sets the maximum age of a missed execution that should be caught up. If zero (default), all missed executions are eligible for catch-up (subject to the safety limit of 100 executions for MissedRunAll).
This is useful to avoid running very old missed jobs that are no longer relevant. For example, if a daily report job was missed 3 days ago, you might not want to generate reports for all those days.
Example:
// Only catch up if missed within the last 2 hours
c.AddFunc("0 9 * * *", dailyReport,
cron.WithPrev(lastRun),
cron.WithMissedPolicy(cron.MissedRunOnce),
cron.WithMissedGracePeriod(2*time.Hour),
)
func WithMissedPolicy ¶ added in v0.10.0
func WithMissedPolicy(policy MissedPolicy) JobOption
WithMissedPolicy configures how the scheduler handles missed job executions. A job is considered "missed" if it was scheduled to run while the scheduler was not running (e.g., during application restart).
This feature requires WithPrev() to provide the last run time. Without a known last run time, no catch-up will occur regardless of the policy.
Available policies:
- MissedSkip (default): Do not catch up; wait for next scheduled time
- MissedRunOnce: Run once immediately if any executions were missed
- MissedRunAll: Run for every missed execution (use with caution)
Example:
lastRun := loadFromDatabase("daily-report")
c.AddFunc("0 9 * * *", dailyReport,
cron.WithPrev(lastRun),
cron.WithMissedPolicy(cron.MissedRunOnce),
)
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 WithPaused ¶ added in v0.12.0
func WithPaused() JobOption
WithPaused causes the entry to be added in a paused state. Paused entries remain registered with their schedule intact but are skipped during execution. Use ResumeEntry to activate the entry later.
This is useful for:
- Pre-registering jobs that should only run after explicit activation
- Maintenance windows: add jobs paused, resume when ready
- Feature flags: register jobs that are enabled externally
Example:
id, _ := c.AddFunc("@every 5m", syncData, cron.WithPaused(), cron.WithName("sync"))
// Later, when ready:
c.ResumeEntry(id)
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 JobResult ¶ added in v0.12.0
type JobResult int
JobResult represents the outcome of a job within a workflow execution.
const ( // ResultPending means the job has not yet completed. ResultPending JobResult = iota // ResultSuccess means the job completed without error. ResultSuccess // ResultFailure means the job failed (returned an error or panicked). ResultFailure // ResultSkipped means the job was skipped because its trigger condition was not met. ResultSkipped )
func (JobResult) IsTerminal ¶ added in v0.12.0
IsTerminal reports whether the result represents a final state.
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, opts ...CircuitBreakerOption) JobWrapper
CircuitBreaker wraps jobs with a circuit breaker that opens after threshold consecutive panics, skipping execution for the cooldown duration. After cooldown, a single probe attempt is allowed (half-open state); if it succeeds the circuit closes, otherwise it re-opens. State is shared across all jobs wrapped by the same wrapper instance. Use CircuitBreakerWithHandle for monitoring access.
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.
The returned wrapper implements JobWithContext, propagating the incoming context to the inner job if it also implements JobWithContext.
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).
The returned wrapper implements JobWithContext, propagating the incoming context to the inner job if it also implements JobWithContext.
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.
The returned wrapper implements JobWithContext, propagating the incoming context to the inner job if it also implements JobWithContext.
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 MaxConcurrent ¶ added in v0.12.0
func MaxConcurrent(n int) JobWrapper
MaxConcurrent limits the total number of jobs that can run concurrently across all entries wrapped by this chain. When all slots are occupied, new job executions wait until a slot becomes available or the context is canceled (e.g., during scheduler shutdown).
This is useful when many jobs are scheduled at the same time (e.g., many @hourly jobs at minute 0) to limit concurrent resource usage (database connections, API rate limits, CPU).
Goroutine Accumulation ¶
The scheduler spawns a goroutine for each due job. MaxConcurrent blocks those goroutines while waiting for a slot, so goroutines still accumulate if jobs are triggered faster than they complete. If this is a concern, use MaxConcurrentSkip instead, which drops excess executions immediately.
Unlike SkipIfStillRunning (which limits per-job), MaxConcurrent limits across all jobs sharing the same wrapper instance.
A limit of 0 or negative panics — use no wrapper if you want no limit.
The returned wrapper implements JobWithContext, propagating the incoming context to the inner job if it also implements JobWithContext. If the context is canceled while waiting for a slot, the job is abandoned.
Example:
// Limit all jobs to 10 concurrent executions
c := cron.New(cron.WithChain(
cron.Recover(logger),
cron.MaxConcurrent(10),
))
// Compose with jitter to prevent thundering herd AND limit concurrency
c := cron.New(cron.WithChain(
cron.Recover(logger),
cron.Jitter(30 * time.Second),
cron.MaxConcurrent(10),
))
func MaxConcurrentSkip ¶ added in v0.12.0
func MaxConcurrentSkip(logger Logger, n int) JobWrapper
MaxConcurrentSkip is like MaxConcurrent but skips execution instead of waiting when the concurrency limit is reached. This is useful when you prefer to drop executions rather than queue them up.
Unlike SkipIfStillRunning (which limits per-job), MaxConcurrentSkip limits across all jobs sharing the same wrapper instance.
The returned wrapper implements JobWithContext, propagating the incoming context to the inner job if it also implements JobWithContext.
Example:
c := cron.New(cron.WithChain(
cron.Recover(logger),
cron.MaxConcurrentSkip(logger, 5),
))
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.
The returned wrapper implements JobWithContext, propagating the incoming context to the inner job if it also implements JobWithContext.
Workflow-aware: when running inside a workflow execution, Recover logs the panic and then re-panics so the workflow engine correctly detects the failure. The scheduler catches the re-panic without crashing.
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 RetryOnError ¶ added in v0.10.0
func RetryOnError(logger Logger, maxRetries int, initialDelay, maxDelay time.Duration, multiplier float64, opts ...RetryOption) JobWrapper
RetryOnError wraps an ErrorJob to retry on returned errors with exponential backoff. Unlike RetryWithBackoff which catches panics, this wrapper uses Go-idiomatic error returns for retry decisions. Jobs must implement the ErrorJob interface to benefit from this wrapper; regular Jobs that only implement Run() are passed through unchanged.
Parameters:
- logger: For logging retry attempts
- maxRetries: Maximum retry attempts:
- 0 = no retries (execute once, log error if it fails) - SAFE DEFAULT
- >0 = retry up to N times (N+1 total attempts)
- -1 = unlimited retries (use with caution - can cause resource exhaustion)
- initialDelay: First retry delay
- maxDelay: Maximum delay cap (prevents exponential explosion)
- multiplier: Delay multiplier per retry (typically 2.0)
When all retries are exhausted, the final error is logged and then panicked, propagating the failure through the middleware chain (e.g., CircuitBreaker, Recover). This is consistent with RetryWithBackoff and ensures downstream wrappers see the failure.
Example usage:
c := cron.New(
cron.WithChain(
cron.Recover(logger), // Outermost: catches panics from non-ErrorJob jobs
cron.RetryOnError(logger, 3, time.Second, time.Minute, 2.0),
),
)
c.AddJob("@every 5m", cron.FuncErrorJob(func() error {
return callAPI() // Returned errors trigger retry
}))
Retry behavior for maxRetries=3, initialDelay=1s, multiplier=2.0:
| Attempt | Delay | Action | |---------|-------|--------------------| | 1 | 0 | Execute | | 2 | 1s | Retry after delay | | 3 | 2s | Retry after delay | | 4 | 4s | Final retry | | - | - | Log + panic (done) |
func RetryWithBackoff ¶ added in v0.6.0
func RetryWithBackoff(logger Logger, maxRetries int, initialDelay, maxDelay time.Duration, multiplier float64, opts ...RetryOption) 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.
The returned wrapper implements JobWithContext, propagating the incoming context to the inner job if it also implements JobWithContext.
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 MissedPolicy ¶ added in v0.10.0
type MissedPolicy int
MissedPolicy defines how to handle jobs that were scheduled to run while the scheduler was not running (e.g., application restart).
This feature requires the user to provide the last run time via WithPrev(). The scheduler does NOT persist state - users are responsible for storing and loading last run times from their own persistence layer.
Important interactions:
- WithRunOnce: Run-once jobs skip catch-up to avoid unintended duplicate runs
- SkipIfStillRunning wrapper: With MissedRunAll, catch-up jobs start nearly simultaneously, so most may be skipped. Use MissedRunOnce for predictable behavior.
Example usage:
// Load last run time from your database
lastRun := loadFromDatabase("daily-report")
c.AddFunc("0 9 * * *", dailyReport,
cron.WithPrev(lastRun), // When it last ran
cron.WithMissedPolicy(cron.MissedRunOnce), // Run once if missed
cron.WithMissedGracePeriod(2*time.Hour), // Only if within 2 hours
)
const ( // MissedSkip does not catch up on missed executions (default). // The job simply waits for its next scheduled time. MissedSkip MissedPolicy = iota // MissedRunOnce runs the job once immediately if any executions were missed. // Only the most recent missed execution time is used. // This is the safest catch-up policy for most use cases. // // After catch-up, Entry.Prev is updated to the caught-up time to prevent // duplicate catch-ups on subsequent restarts. MissedRunOnce // MissedRunAll executes the job for every missed execution time. // Use with caution: this can cause a burst of executions if the scheduler // was down for a long time. Consider using MissedRunOnce instead. // // A safety limit of 100 missed executions is enforced to prevent // runaway loops from misconfigured schedules. // // Note: All catch-up jobs start nearly simultaneously. If the job uses // SkipIfStillRunning wrapper, most catch-up runs will be skipped. // For sequential catch-up execution, implement custom logic using // DelayIfStillRunning or manage execution order in your job. // // After catch-up, Entry.Prev is updated to the most recent caught-up time. MissedRunAll )
func (MissedPolicy) String ¶ added in v0.10.0
func (p MissedPolicy) String() string
String returns a human-readable representation of the MissedPolicy.
func (MissedPolicy) Valid ¶ added in v0.10.0
func (p MissedPolicy) Valid() bool
Valid returns true if the policy is a known valid value.
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)
// OnWorkflowComplete is called when all jobs in a workflow execution
// have resolved (success, failure, or skipped).
OnWorkflowComplete func(executionID string, rootID EntryID, results map[EntryID]JobResult)
}
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 WithCapacity ¶ added in v0.10.0
WithCapacity pre-allocates internal data structures for the expected number of entries. This reduces map rehashing and slice growth during bulk additions, improving performance when adding many jobs at startup.
Pre-allocates:
- entryIndex map with capacity n (O(1) lookup by ID)
- nameIndex map with capacity n (O(1) lookup by name)
- entries heap slice with capacity n
For applications adding fewer than 100 jobs, the default allocation is sufficient. Use this option when bulk-loading hundreds or thousands of jobs.
A capacity of 0 or negative has no effect (uses default allocation).
Example:
// Expect ~1000 jobs at startup
c := cron.New(cron.WithCapacity(1000))
for _, job := range jobs {
c.AddFunc(job.Schedule, job.Func)
}
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.
func WithWorkflowRetention ¶ added in v0.12.0
WithWorkflowRetention sets the maximum number of completed workflow executions to retain for query via WorkflowStatus/ActiveWorkflows. Default is 100. Set to 0 for unlimited retention (not recommended for long-running services).
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) DowOrDom // Use legacy OR logic for DOW/DOM (default: AND) )
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 RetryAttempt ¶ added in v0.12.0
type RetryAttempt struct {
Attempt int // 1-based attempt number (1 = first execution)
Delay time.Duration // Delay before this attempt (0 for first attempt)
Err any // Panic value (RetryWithBackoff) or error (RetryOnError)
WillRetry bool // True if another attempt will follow
}
RetryAttempt contains metadata about a single retry attempt. This is passed to the callback configured via WithRetryCallback.
type RetryOption ¶ added in v0.12.0
type RetryOption func(*retryConfig)
RetryOption configures optional behavior for RetryWithBackoff and RetryOnError.
func WithRetryCallback ¶ added in v0.12.0
func WithRetryCallback(fn func(RetryAttempt)) RetryOption
WithRetryCallback sets a callback invoked after each attempt (including the initial execution). This enables external monitoring and metrics collection for retry behavior.
Example with Prometheus:
RetryWithBackoff(logger, 3, time.Second, time.Minute, 2.0,
cron.WithRetryCallback(func(a cron.RetryAttempt) {
retryCounter.WithLabelValues(fmt.Sprint(a.Attempt)).Inc()
if !a.WillRetry && a.Err != nil {
retryExhausted.Inc()
}
}),
)
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
// Warnings contains non-fatal warnings about the schedule.
// These don't prevent parsing but may indicate unexpected behavior.
// Example: "DOM and DOW both restricted - using AND logic (use DowOrDom for OR)"
Warnings []string
}
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
// DowOrDom enables legacy OR logic for day-of-week and day-of-month matching.
// When false (default), both DOM and DOW must match (AND logic), consistent
// with all other cron fields. When true, the schedule matches if either
// DOM or DOW matches (OR logic), for robfig/cron compatibility.
DowOrDom bool
}
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 TriggerCondition ¶ added in v0.12.0
type TriggerCondition int
TriggerCondition defines when a dependent job should be triggered relative to its parent's outcome.
const ( // OnSuccess triggers when the parent job completes without error. OnSuccess TriggerCondition = iota // OnFailure triggers when the parent job fails (error or panic). OnFailure // OnSkipped triggers when the parent job was skipped // (its own trigger condition was not met). OnSkipped // OnComplete triggers after the parent job resolves to any terminal state // (success, failure, or skipped). Use for cleanup/finalization steps. OnComplete )
func (TriggerCondition) Matches ¶ added in v0.12.0
func (c TriggerCondition) Matches(result JobResult) bool
Matches reports whether the given parent result satisfies this condition.
func (TriggerCondition) String ¶ added in v0.12.0
func (c TriggerCondition) String() string
String returns the human-readable name for the trigger condition.
func (TriggerCondition) Valid ¶ added in v0.12.0
func (c TriggerCondition) Valid() bool
Valid reports whether c is a known trigger condition.
type TriggeredSchedule ¶ added in v0.12.0
type TriggeredSchedule struct{}
TriggeredSchedule is a schedule that never fires automatically. Entries using this schedule remain dormant until explicitly triggered via TriggerEntry or TriggerEntryByName. This enables manual/on-demand job execution while still benefiting from the scheduler's middleware chain (retry, timeout, skip-if-running, etc.).
Use the @triggered, @manual, or @none descriptors to create entries with this schedule:
c.AddFunc("@triggered", myJob, cron.WithName("deploy"))
c.TriggerEntryByName("deploy") // Run on demand
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
type Workflow ¶ added in v0.12.0
type Workflow struct {
Name string
// contains filtered or unexported fields
}
Workflow defines a multi-step DAG of named jobs with dependency edges. Use NewWorkflow to create a workflow, then Step/StepFunc to add steps, and AddWorkflow on a Cron instance to register it atomically.
func NewWorkflow ¶ added in v0.12.0
NewWorkflow creates a new Workflow with the given name.
func (*Workflow) Step ¶ added in v0.12.0
func (w *Workflow) Step(name, spec string, job Job) *WorkflowStep
Step adds a named step to the workflow with the given schedule spec and Job.
func (*Workflow) StepFunc ¶ added in v0.12.0
func (w *Workflow) StepFunc(name, spec string, fn func()) *WorkflowStep
StepFunc adds a named step with a plain function as its job.
type WorkflowExecution ¶ added in v0.12.0
type WorkflowExecution struct {
ID string
RootID EntryID
StartTime time.Time
Results map[EntryID]JobResult
}
WorkflowExecution tracks the state of a single workflow run. All fields are owned exclusively by the run() goroutine — no mutex needed.
func (*WorkflowExecution) IsComplete ¶ added in v0.12.0
func (we *WorkflowExecution) IsComplete() bool
IsComplete reports whether every job in the execution has reached a terminal state.
type WorkflowStep ¶ added in v0.12.0
type WorkflowStep struct {
// contains filtered or unexported fields
}
WorkflowStep is a single step within a Workflow.
func (*WorkflowStep) After ¶ added in v0.12.0
func (s *WorkflowStep) After(parentName string, condition TriggerCondition) *WorkflowStep
After declares that this step depends on the named parent step with the given condition. Multiple After calls can be chained to create fan-in dependencies.
func (*WorkflowStep) Final ¶ added in v0.12.0
func (s *WorkflowStep) Final() *WorkflowStep
Final marks this step as a finalization step. A final step receives an OnComplete edge from every non-final step, ensuring it runs after all other steps have resolved regardless of their outcome. At most one step per workflow may be marked Final.