cron

package module
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 3, 2025 License: MIT Imports: 17 Imported by: 0

README

Go Reference CI CodeQL OpenSSF Scorecard Go Report Card

go-cron

A maintained fork of robfig/cron — the most popular cron library for Go — with critical bug fixes, DST handling improvements, and modern toolchain support.

Why?

The original robfig/cron has been unmaintained since 2020, accumulating 50+ open PRs and several critical panic bugs that affect production systems. Rather than waiting indefinitely, this fork provides:

Issue Original This Fork
TZ= parsing panics Crashes on malformed input Fixed (#554, #555)
Chain decorators Entry.Run() bypasses chains Properly invokes wrappers (#551)
DST spring-forward Jobs silently skipped Runs immediately (ISC behavior, #541)
Go version Stuck on 1.13 Go 1.25+ with modern toolchain

Installation

go get github.com/netresearch/go-cron
import cron "github.com/netresearch/go-cron"

[!NOTE] Requires Go 1.25 or later.

Migrating from robfig/cron

Drop-in replacement — just change the import path:

// Before
import "github.com/robfig/cron/v3"

// After
import cron "github.com/netresearch/go-cron"

The API is 100% compatible with robfig/cron v3.

[!TIP] See docs/MIGRATION.md for a comprehensive migration guide including behavioral differences, type changes, and troubleshooting.

Quick Start

package main

import (
    "fmt"
    "time"

    cron "github.com/netresearch/go-cron"
)

func main() {
    c := cron.New()

    // Run every minute
    c.AddFunc("* * * * *", func() {
        fmt.Println("Every minute:", time.Now())
    })

    // Run at specific times
    c.AddFunc("30 3-6,20-23 * * *", func() {
        fmt.Println("In the range 3-6am, 8-11pm")
    })

    // With timezone
    c.AddFunc("CRON_TZ=Asia/Tokyo 30 04 * * *", func() {
        fmt.Println("4:30 AM Tokyo time")
    })

    c.Start()

    // Keep running...
    select {}
}

Cron Expression Format

Standard 5-field cron format (minute-first):

Field Required Values 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 * / , - ?
Predefined Schedules
Entry Description Equivalent
@yearly Once a year, midnight, Jan 1 0 0 1 1 *
@monthly Once a month, midnight, first day 0 0 1 * *
@weekly Once a week, midnight Sunday 0 0 * * 0
@daily Once a day, midnight 0 0 * * *
@hourly Once an hour, beginning of hour 0 * * * *
@every <duration> Every interval e.g., @every 1h30m
Seconds Field (Optional)

Enable Quartz-compatible seconds field:

// Seconds field required
cron.New(cron.WithSeconds())

// Seconds field optional
cron.New(cron.WithParser(cron.NewParser(
    cron.SecondOptional | cron.Minute | cron.Hour |
    cron.Dom | cron.Month | cron.Dow | cron.Descriptor,
)))

Timezone Support

Specify timezone per-schedule using CRON_TZ= prefix:

// Runs at 6am New York time
c.AddFunc("CRON_TZ=America/New_York 0 6 * * *", myFunc)

// Legacy TZ= prefix also supported
c.AddFunc("TZ=Europe/Berlin 0 9 * * *", myFunc)

Or set default timezone for all jobs:

nyc, _ := time.LoadLocation("America/New_York")
c := cron.New(cron.WithLocation(nyc))
Daylight Saving Time (DST) Handling

This library implements ISC cron-compatible DST behavior:

Transition Behavior
Spring Forward (hour skipped) Jobs in skipped hour run immediately after transition
Fall Back (hour repeats) Jobs run once, during first occurrence
Midnight DST (midnight doesn't exist) Automatically normalized to valid time

[!TIP] For DST-sensitive applications, schedule jobs outside typical transition hours (1-3 AM) or use UTC.

See docs/DST_HANDLING.md for comprehensive DST documentation including examples, testing strategies, and edge cases.

Job Wrappers (Middleware)

Add cross-cutting behavior using chains:

// Apply to all jobs
c := cron.New(cron.WithChain(
    cron.Recover(logger),              // Recover panics
    cron.SkipIfStillRunning(logger),   // Skip if previous still running
))

// Apply to specific job
job := cron.NewChain(
    cron.DelayIfStillRunning(logger),  // Queue if previous still running
).Then(myJob)

Available wrappers:

  • Recover — Catch panics, log, and continue
  • SkipIfStillRunning — Skip execution if previous run hasn't finished
  • DelayIfStillRunning — Queue execution until previous run finishes

Logging

Compatible with go-logr/logr:

// Verbose logging
c := cron.New(cron.WithLogger(
    cron.VerbosePrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags)),
))

API Reference

Full documentation: pkg.go.dev/github.com/netresearch/go-cron

Contributing

Contributions are welcome! Please read CONTRIBUTING.md before submitting PRs.

Security

For security issues, please see SECURITY.md.

License

MIT License — see LICENSE for details.


This fork is maintained by Netresearch. The original cron library was created by Rob Figueiredo.

Documentation

Overview

Package cron implements a cron spec parser and job runner.

Installation

To download the package, run:

go get github.com/netresearch/go-cron

Import it in your program as:

import "github.com/netresearch/go-cron"

It requires Go 1.25 or later.

Usage

Callers may register Funcs to be invoked on a given schedule. Cron will run them in their own goroutines.

c := cron.New()
c.AddFunc("30 * * * *", func() { fmt.Println("Every hour on the half hour") })
c.AddFunc("30 3-6,20-23 * * *", func() { fmt.Println(".. in the range 3-6am, 8-11pm") })
c.AddFunc("CRON_TZ=Asia/Tokyo 30 04 * * *", func() { fmt.Println("Runs at 04:30 Tokyo time every day") })
c.AddFunc("@hourly",      func() { fmt.Println("Every hour, starting an hour from now") })
c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty, starting an hour thirty from now") })
c.Start()
..
// Funcs are invoked in their own goroutine, asynchronously.
...
// Funcs may also be added to a running Cron
c.AddFunc("@daily", func() { fmt.Println("Every day") })
..
// Inspect the cron job entries' next and previous run times.
inspect(c.Entries())
..
c.Stop()  // Stop the scheduler (does not stop any jobs already running).

CRON Expression Format

A cron expression represents a set of times, using 5 space-separated fields.

Field name   | Mandatory? | Allowed values  | Allowed special characters
----------   | ---------- | --------------  | --------------------------
Minutes      | Yes        | 0-59            | * / , -
Hours        | Yes        | 0-23            | * / , -
Day of month | Yes        | 1-31            | * / , - ?
Month        | Yes        | 1-12 or JAN-DEC | * / , -
Day of week  | Yes        | 0-6 or SUN-SAT  | * / , - ?

Month and Day-of-week field values are case insensitive. "SUN", "Sun", and "sun" are equally accepted.

The specific interpretation of the format is based on the Cron Wikipedia page: https://en.wikipedia.org/wiki/Cron

Alternative Formats

Alternative Cron expression formats support other fields like seconds. You can implement that by creating a custom Parser as follows.

cron.New(
	cron.WithParser(
		cron.NewParser(
			cron.SecondOptional | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)))

Since adding Seconds is the most common modification to the standard cron spec, cron provides a builtin function to do that, which is equivalent to the custom parser you saw earlier, except that its seconds field is REQUIRED:

cron.New(cron.WithSeconds())

That emulates Quartz, the most popular alternative Cron schedule format: http://www.quartz-scheduler.org/documentation/quartz-2.x/tutorials/crontrigger.html

Special Characters

Asterisk ( * )

The asterisk indicates that the cron expression will match for all values of the field; e.g., using an asterisk in the 5th field (month) would indicate every month.

Slash ( / )

Slashes are used to describe increments of ranges. For example 3-59/15 in the 1st field (minutes) would indicate the 3rd minute of the hour and every 15 minutes thereafter. The form "*\/..." is equivalent to the form "first-last/...", that is, an increment over the largest possible range of the field. The form "N/..." is accepted as meaning "N-MAX/...", that is, starting at N, use the increment until the end of that specific range. It does not wrap around.

Comma ( , )

Commas are used to separate items of a list. For example, using "MON,WED,FRI" in the 5th field (day of week) would mean Mondays, Wednesdays and Fridays.

Hyphen ( - )

Hyphens are used to define ranges. For example, 9-17 would indicate every hour between 9am and 5pm inclusive.

Question mark ( ? )

Question mark may be used instead of '*' for leaving either day-of-month or day-of-week blank.

Predefined schedules

You may use one of several pre-defined schedules in place of a cron expression.

Entry                  | Description                                | Equivalent To
-----                  | -----------                                | -------------
@yearly (or @annually) | Run once a year, midnight, Jan. 1st        | 0 0 1 1 *
@monthly               | Run once a month, midnight, first of month | 0 0 1 * *
@weekly                | Run once a week, midnight between Sat/Sun  | 0 0 * * 0
@daily (or @midnight)  | Run once a day, midnight                   | 0 0 * * *
@hourly                | Run once an hour, beginning of hour        | 0 * * * *

Intervals

You may also schedule a job to execute at fixed intervals, starting at the time it's added or cron is run. This is supported by formatting the cron spec like this:

@every <duration>

where "duration" is a string accepted by time.ParseDuration (http://golang.org/pkg/time/#ParseDuration).

For example, "@every 1h30m10s" would indicate a schedule that activates after 1 hour, 30 minutes, 10 seconds, and then every interval after that.

Note: The interval does not take the job runtime into account. For example, if a job takes 3 minutes to run, and it is scheduled to run every 5 minutes, it will have only 2 minutes of idle time between each run.

Time zones

By default, all interpretation and scheduling is done in the machine's local time zone (time.Local). You can specify a different time zone on construction:

cron.New(
    cron.WithLocation(time.UTC))

Individual cron schedules may also override the time zone they are to be interpreted in by providing an additional space-separated field at the beginning of the cron spec, of the form "CRON_TZ=Asia/Tokyo".

For example:

# Runs at 6am in time.Local
cron.New().AddFunc("0 6 * * ?", ...)

# Runs at 6am in America/New_York
nyc, _ := time.LoadLocation("America/New_York")
c := cron.New(cron.WithLocation(nyc))
c.AddFunc("0 6 * * ?", ...)

# Runs at 6am in Asia/Tokyo
cron.New().AddFunc("CRON_TZ=Asia/Tokyo 0 6 * * ?", ...)

# Runs at 6am in Asia/Tokyo, overriding the cron's default location
tokyo, _ := time.LoadLocation("Asia/Tokyo")
c := cron.New(cron.WithLocation(tokyo))
c.AddFunc("0 6 * * ?", ...)

The prefix "TZ=(TIME ZONE)" is also supported for legacy compatibility.

Jobs scheduled during daylight-savings leap-ahead transitions will run immediately after the skipped hour (ISC cron-compatible behavior).

Daylight Saving Time (DST) Handling

This library follows ISC cron-compatible DST behavior. Understanding these edge cases is critical for time-sensitive scheduling.

Spring Forward (clocks skip an hour):

  • Jobs scheduled during the skipped hour run immediately after the transition
  • Example: A 2:30 AM job during US spring DST runs at 3:00 AM
  • Jobs scheduled exactly at the transition boundary may run immediately

Fall Back (clocks repeat an hour):

  • Jobs run only during the first occurrence of the repeated hour
  • The second occurrence is skipped to prevent duplicate runs
  • ⚠️ Note: This means jobs scheduled in the repeated hour run once, not twice

Midnight Doesn't Exist:

  • Some DST transitions skip midnight entirely (e.g., São Paulo, Brazil)
  • Jobs scheduled at midnight run at the first valid time after transition
  • This affects daily (@daily) and midnight-scheduled jobs in those timezones

30-Minute Offset Timezones:

  • Some regions (e.g., Lord Howe Island, Australia) use 30-minute DST changes
  • The same DST handling rules apply, but at 30-minute boundaries

⚠️ Important Edge Cases:

  • Jobs during spring-forward gap: Run immediately after transition
  • Jobs during fall-back repeat: Run only on first occurrence
  • Multi-timezone systems: Each job uses its configured timezone independently
  • Leap seconds: Not handled; use NTP-synced systems for best results

Testing DST scenarios:

// Use FakeClock for deterministic DST testing
loc, _ := time.LoadLocation("America/New_York")
// Start just before spring DST transition (2024: March 10, 2:00 AM)
clock := cron.NewFakeClock(time.Date(2024, 3, 10, 1, 59, 0, 0, loc))
c := cron.New(cron.WithClock(clock), cron.WithLocation(loc))
// ... test behavior

Best practices for DST-sensitive schedules:

  • Use UTC (CRON_TZ=UTC) for critical jobs that must run exactly once
  • Use explicit timezones (CRON_TZ=America/New_York) rather than local time
  • Avoid scheduling jobs between 2:00-3:00 AM in DST-observing timezones
  • Test with FakeClock around DST transitions before production deployment
  • Consider using @every intervals for tasks where exact wall-clock time is less important
  • Monitor job execution times during DST transition periods

Error Handling

Jobs in go-cron signal failure by panicking rather than returning errors. This design:

  • Keeps the Job interface simple (Run() has no return value)
  • Enables consistent recovery and retry behavior via wrapper chains
  • Allows adding retry/circuit-breaker logic without modifying job code
  • Matches Go's convention of panicking for unrecoverable errors

Best practices:

  • Use panic() for transient failures that should trigger retries
  • Use log-and-continue for errors that shouldn't affect the next run
  • Always wrap jobs with Recover() to prevent scheduler crashes
  • Combine with RetryWithBackoff for automatic retry of transient failures
  • Use CircuitBreaker to prevent hammering failing external services

Error flow through wrapper chain:

Recover → CircuitBreaker → RetryWithBackoff → Job
   ↑           ↑                 ↑              │
   │           │                 └── catches ───┤ (panic)
   │           └── tracks/opens ────────────────┤ (panic)
   └── logs/swallows ───────────────────────────┘ (panic)

Job Wrappers

A Cron runner may be configured with a chain of job wrappers to add cross-cutting functionality to all submitted jobs. For example, they may be used to achieve the following effects:

  • Recover any panics from jobs
  • Delay a job's execution if the previous run hasn't completed yet
  • Skip a job's execution if the previous run hasn't completed yet
  • Log each job's invocations

Install wrappers for all jobs added to a cron using the `cron.WithChain` option:

cron.New(cron.WithChain(
	cron.Recover(logger),  // Recommended: recover panics to prevent crashes
	cron.SkipIfStillRunning(logger),
))

Install wrappers for individual jobs by explicitly wrapping them:

job = cron.NewChain(
	cron.SkipIfStillRunning(logger),
).Then(job)

Wrapper Composition Patterns

Wrappers are applied in reverse order (outermost first). Understanding the correct ordering is critical for proper behavior:

Production-Ready Chain (recommended):

c := cron.New(cron.WithChain(
	cron.Recover(logger),              // 1. Outermost: catches all panics
	cron.RetryWithBackoff(logger, 3,   // 2. Retry transient failures
		time.Second, time.Minute, 2.0),
	cron.CircuitBreaker(logger, 5,     // 3. Stop hammering failing services
		5*time.Minute),
	cron.SkipIfStillRunning(logger),   // 4. Innermost: prevent overlap
))

Context-Aware Chain (for graceful shutdown):

c := cron.New(cron.WithChain(
	cron.Recover(logger),
	cron.TimeoutWithContext(logger, 5*time.Minute),
))
c.AddJob("@every 1h", cron.FuncJobWithContext(func(ctx context.Context) {
	select {
	case <-ctx.Done():
		return // Shutdown or timeout - exit gracefully
	case <-doWork():
		// Work completed
	}
}))

Wrapper Ordering Pitfalls:

// BAD: Retry inside Recover loses panic information
cron.NewChain(cron.RetryWithBackoff(...), cron.Recover(logger))

// GOOD: Recover catches re-panics from exhausted retries
cron.NewChain(cron.Recover(logger), cron.RetryWithBackoff(...))

Available Wrappers:

  • Recover: Catches panics and logs them
  • SkipIfStillRunning: Skip if previous run is still active
  • DelayIfStillRunning: Queue runs, serializing execution
  • Timeout: Abandon long-running jobs (see caveats below)
  • TimeoutWithContext: True cancellation via context
  • RetryWithBackoff: Retry panicking jobs with exponential backoff
  • CircuitBreaker: Stop execution after consecutive failures

Timeout Wrapper Caveats

The Timeout wrapper uses an "abandonment model" - when a job exceeds its timeout, the wrapper returns but the job's goroutine continues running in the background. This design has important implications:

  • The job is NOT canceled; it runs to completion even after timeout
  • Resources held by the job are not released until the job naturally completes
  • Side effects (database writes, API calls) still occur after timeout
  • Multiple abandoned goroutines can accumulate if jobs consistently timeout

This is the only practical approach without context.Context support in the Job interface. For jobs that need true cancellation:

  • Implement your own cancellation mechanism using channels or atomic flags
  • Have your job check for cancellation signals at safe points
  • Consider using shorter timeout values as a circuit breaker rather than for cancellation

Example of a cancellable job pattern:

type CancellableJob struct {
	cancel chan struct{}
}

func (j *CancellableJob) Run() {
	for {
		select {
		case <-j.cancel:
			return // Clean exit on cancellation
		default:
			// Do work in small chunks
			if done := doWorkChunk(); done {
				return
			}
		}
	}
}

Thread Safety

Cron is safe for concurrent use. Multiple goroutines may call methods on a Cron instance simultaneously without external synchronization.

Specific guarantees:

  • AddJob/AddFunc: Safe to call while scheduler is running
  • Remove: Safe to call while scheduler is running
  • Entries: Returns a snapshot; safe but may be stale
  • Start/Stop: Safe to call multiple times (idempotent)
  • Entry: Safe to call; returns copy of entry data

Job Execution:

  • Jobs may run concurrently by default
  • Use SkipIfStillRunning or DelayIfStillRunning for serialization
  • Jobs should not block indefinitely (use Timeout or TimeoutWithContext)

The scheduler uses an internal channel-based synchronization model. All operations that modify scheduler state are serialized through this channel.

Logging

Cron defines a Logger interface that is a subset of the one defined in github.com/go-logr/logr. It has two logging levels (Info and Error), and parameters are key/value pairs. This makes it possible for cron logging to plug into structured logging systems. An adapter, [Verbose]PrintfLogger, is provided to wrap the standard library *log.Logger.

For additional insight into Cron operations, verbose logging may be activated which will record job runs, scheduling decisions, and added or removed jobs. Activate it with a one-off logger as follows:

cron.New(
	cron.WithLogger(
		cron.VerbosePrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags))))

Resource Management

Use WithMaxEntries to limit the number of scheduled jobs and prevent resource exhaustion:

c := cron.New(cron.WithMaxEntries(100))
id, err := c.AddFunc("@every 1m", myJob)
if errors.Is(err, cron.ErrMaxEntriesReached) {
	// Handle limit reached - remove old jobs or reject new ones
}

Behavior when limit is reached:

  • AddFunc, AddJob, ScheduleJob return ErrMaxEntriesReached
  • Existing jobs continue running normally
  • Counter decrements when jobs are removed via Remove(id)

The entry limit is checked atomically but may briefly exceed the limit during concurrent additions by the number of in-flight ScheduleJob calls.

Observability Hooks

ObservabilityHooks provide integration points for metrics, tracing, and monitoring:

hooks := cron.ObservabilityHooks{
	OnSchedule: func(entryID cron.EntryID, name string, nextRun time.Time) {
		// Called when a job's next execution time is calculated
		log.Printf("Job %d (%s) scheduled for %v", entryID, name, nextRun)
	},
	OnJobStart: func(entryID cron.EntryID, name string, scheduledTime time.Time) {
		// Called just before a job starts running
		metrics.IncrCounter("cron.job.started", "job", name)
	},
	OnJobComplete: func(entryID cron.EntryID, name string, duration time.Duration, recovered any) {
		// Called after a job completes (successfully or with panic)
		metrics.RecordDuration("cron.job.duration", duration, "job", name)
		if recovered != nil {
			metrics.IncrCounter("cron.job.panic", "job", name)
		}
	},
}
c := cron.New(cron.WithObservability(hooks))

Testing with FakeClock

FakeClock enables deterministic time control for testing cron jobs:

func TestJobExecution(t *testing.T) {
	clock := cron.NewFakeClock(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC))
	c := cron.New(cron.WithClock(clock))

	executed := make(chan struct{})
	c.AddFunc("@every 1h", func() { close(executed) })
	c.Start()
	defer c.Stop()

	// Advance time to trigger job
	clock.Advance(time.Hour)

	select {
	case <-executed:
		// Success
	case <-time.After(time.Second):
		t.Fatal("job not executed")
	}
}

FakeClock methods:

  • NewFakeClock(initial time.Time): Create clock at specific time
  • Advance(d time.Duration): Move time forward, triggering timers
  • Set(t time.Time): Jump to specific time
  • BlockUntil(n int): Wait for n timers to be registered
  • Now(), Since(), After(), AfterFunc(), NewTicker(), Sleep(): Standard time operations

Use BlockUntil for synchronization in tests with multiple timers:

clock.BlockUntil(2) // Wait for 2 timers to be registered
clock.Advance(time.Hour) // Now safely advance

Security Considerations

Input Validation:

  • Cron specifications are limited to 1024 characters (MaxSpecLength)
  • Timezone specifications are validated against Go's time.LoadLocation
  • Path traversal attempts in timezone strings (e.g., "../etc/passwd") are rejected

Resource Protection:

  • Use WithMaxEntries to limit scheduled jobs in multi-tenant environments
  • Use WithMaxSearchYears to limit schedule search time for complex expressions
  • Timeout wrappers prevent runaway jobs from consuming resources indefinitely

Recommended Patterns:

  • Validate user-provided cron expressions before scheduling
  • Use named jobs with duplicate prevention for user-defined schedules
  • Monitor entry counts and job durations in production
  • Run the cron service with minimal privileges

Migration from robfig/cron

This library is a maintained fork of github.com/robfig/cron/v3 with full backward compatibility. To migrate:

// Before
import "github.com/robfig/cron/v3"

// After
import "github.com/netresearch/go-cron"

New features available after migration:

  • RetryWithBackoff: Automatic retry with exponential backoff
  • CircuitBreaker: Protect failing jobs from overwhelming services
  • TimeoutWithContext: True cancellation support via context
  • ObservabilityHooks: Integrated metrics and tracing support
  • FakeClock: Deterministic time control for testing
  • WithMaxEntries: Resource protection for entry limits
  • WithMaxSearchYears: Configurable schedule search limits
  • Named jobs: Unique job names with duplicate prevention
  • Tagged jobs: Categorization and bulk operations
  • Context support: Graceful shutdown via context cancellation

All existing code will work unchanged. The migration is a drop-in replacement.

Implementation

Cron entries are stored in a min-heap ordered by their next activation time, providing O(log n) insertion/removal and O(1) access to the next entry. Cron sleeps until the next job is due to be run.

Upon waking:

  • it runs each entry that is active on that second
  • it calculates the next run times for the jobs that were run
  • it re-heapifies the entries by next activation time
  • it goes to sleep until the soonest job.
Example

This example demonstrates basic cron usage.

package main

import (
	"fmt"

	cron "github.com/netresearch/go-cron"
)

func main() {
	c := cron.New()

	// Add a job that runs every minute
	c.AddFunc("* * * * *", func() {
		fmt.Println("Every minute")
	})

	// Start the scheduler
	c.Start()

	// Stop the scheduler when done
	c.Stop()
}

Index

Examples

Constants

View Source
const MaxSpecLength = 1024

MaxSpecLength is the maximum allowed length for a cron spec string. This limit prevents potential resource exhaustion from extremely long inputs.

Variables

View Source
var DefaultLogger = PrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags))

DefaultLogger is used by Cron if none is specified.

View Source
var DiscardLogger = PrintfLogger(log.New(io.Discard, "", 0))

DiscardLogger can be used by callers to discard all log messages.

View Source
var ErrDuplicateName = errors.New("cron: duplicate entry name")

ErrDuplicateName is returned when adding an entry with a name that already exists.

View Source
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).

View Source
var ErrMultipleOptionals = fmt.Errorf("multiple optionals may not be configured")

ErrMultipleOptionals is returned when more than one optional field is configured.

View Source
var ErrNoFields = fmt.Errorf("at least one field or Descriptor must be configured")

ErrNoFields is returned when no fields or Descriptor are configured.

Functions

This section is empty.

Types

type Chain

type Chain struct {
	// contains filtered or unexported fields
}

Chain is a sequence of JobWrappers that decorates submitted jobs with cross-cutting behaviors like logging or synchronization.

func NewChain

func NewChain(c ...JobWrapper) Chain

NewChain returns a Chain consisting of the given JobWrappers.

Example

This example demonstrates wrapping individual jobs with chains.

package main

import (
	"fmt"
	"time"

	cron "github.com/netresearch/go-cron"
)

func main() {
	c := cron.New()

	// Create a chain for specific jobs
	chain := cron.NewChain(
		cron.DelayIfStillRunning(cron.DefaultLogger),
	)

	// Wrap a job with the chain
	wrappedJob := chain.Then(cron.FuncJob(func() {
		fmt.Println("This job will queue if still running")
	}))

	c.Schedule(cron.Every(time.Minute), wrappedJob)
	c.Start()
	defer c.Stop()
}

func (Chain) Then

func (c Chain) Then(j Job) Job

Then decorates the given job with all JobWrappers in the chain.

This:

NewChain(m1, m2, m3).Then(job)

is equivalent to:

m1(m2(m3(job)))

type Clock added in v0.6.0

type Clock interface {
	Now() time.Time
	NewTimer(d time.Duration) Timer
}

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

type ConstantDelaySchedule struct {
	Delay time.Duration
}

ConstantDelaySchedule represents a simple recurring duty cycle, e.g. "Every 5 minutes". It does not support jobs more frequent than once a second.

func Every

func Every(duration time.Duration) ConstantDelaySchedule

Every returns a crontab Schedule that activates once every duration. Delays of less than a second are not supported (will round up to 1 second). Any fields less than a Second are truncated.

For custom minimum intervals, use EveryWithMin instead.

Example

This example demonstrates creating a constant delay schedule.

package main

import (
	"fmt"
	"time"

	cron "github.com/netresearch/go-cron"
)

func main() {
	c := cron.New()

	// Run every 5 minutes
	c.Schedule(cron.Every(5*time.Minute), cron.FuncJob(func() {
		fmt.Println("Every 5 minutes")
	}))

	c.Start()
	defer c.Stop()
}

func EveryWithMin added in v0.6.0

func EveryWithMin(duration, minInterval time.Duration) ConstantDelaySchedule

EveryWithMin returns a crontab Schedule that activates once every duration, with a configurable minimum interval.

The minInterval parameter controls the minimum allowed duration:

  • If minInterval > 0, durations below minInterval are rounded up to minInterval
  • If minInterval <= 0, no minimum is enforced (allows sub-second intervals)

Any fields less than a Second are truncated unless minInterval allows sub-second.

Example:

// Standard usage (1 second minimum)
sched := EveryWithMin(500*time.Millisecond, time.Second) // rounds to 1s

// Sub-second intervals (for testing)
sched := EveryWithMin(100*time.Millisecond, 0) // allows 100ms

// Enforce minimum 1-minute intervals
sched := EveryWithMin(30*time.Second, time.Minute) // rounds to 1m
Example

This example demonstrates using EveryWithMin to create schedules with custom minimum intervals. This is useful for testing (sub-second) or rate limiting.

package main

import (
	"fmt"
	"time"

	cron "github.com/netresearch/go-cron"
)

func main() {
	c := cron.New()

	// Allow sub-second intervals (useful for testing)
	// The second parameter (0) disables the minimum interval check
	schedule := cron.EveryWithMin(100*time.Millisecond, 0)
	c.Schedule(schedule, cron.FuncJob(func() {
		fmt.Println("Running every 100ms")
	}))

	// Enforce minimum 1-minute intervals (useful for rate limiting)
	// If duration < minInterval, it's rounded up to minInterval
	rateLimited := cron.EveryWithMin(30*time.Second, time.Minute)
	c.Schedule(rateLimited, cron.FuncJob(func() {
		fmt.Println("Running every minute (30s was rounded up)")
	}))

	c.Start()
	defer c.Stop()
}

func (ConstantDelaySchedule) Next

func (schedule ConstantDelaySchedule) Next(t time.Time) time.Time

Next returns the next time this should be run. For delays of 1 second or more, this rounds to the next second boundary. For sub-second delays, no rounding is performed.

If the delay is zero or negative (invalid), returns t + 1 second as a safe fallback to prevent CPU spin loops in the scheduler.

type Cron

type Cron struct {
	// contains filtered or unexported fields
}

Cron keeps track of any number of entries, invoking the associated func as specified by the schedule. It may be started, stopped, and the entries may be inspected while running.

Entries are stored in a min-heap ordered by next execution time, providing O(log n) insertion/removal and O(1) access to the next entry to run. An index map provides O(1) entry lookup by ID.

func New

func New(opts ...Option) *Cron

New returns a new Cron job runner, modified by the given options.

Available Settings

Time Zone
  Description: The time zone in which schedules are interpreted
  Default:     time.Local

Parser
  Description: Parser converts cron spec strings into cron.Schedules.
  Default:     Accepts this spec: https://en.wikipedia.org/wiki/Cron

Chain
  Description: Wrap submitted jobs to customize behavior.
  Default:     A chain that recovers panics and logs them to stderr.

See "cron.With*" to modify the default behavior.

Example

This example demonstrates creating a new Cron instance with default settings.

package main

import (
	"fmt"

	cron "github.com/netresearch/go-cron"
)

func main() {
	c := cron.New()

	c.AddFunc("@hourly", func() {
		fmt.Println("Every hour")
	})

	c.Start()
	defer c.Stop()
}
Example (WithLocation)

This example demonstrates timezone-aware scheduling.

package main

import (
	"fmt"
	"time"

	cron "github.com/netresearch/go-cron"
)

func main() {
	nyc, _ := time.LoadLocation("America/New_York")
	c := cron.New(cron.WithLocation(nyc))

	// Run at 9 AM New York time
	c.AddFunc("0 9 * * *", func() {
		fmt.Println("Good morning, New York!")
	})

	c.Start()
	defer c.Stop()
}
Example (WithSeconds)

This example demonstrates using WithSeconds to enable second-granularity scheduling.

package main

import (
	"fmt"

	cron "github.com/netresearch/go-cron"
)

func main() {
	// Enable seconds field (Quartz-style 6-field expressions)
	c := cron.New(cron.WithSeconds())

	// Run every 30 seconds
	c.AddFunc("*/30 * * * * *", func() {
		fmt.Println("Every 30 seconds")
	})

	c.Start()
	defer c.Stop()
}

func (*Cron) AddFunc

func (c *Cron) AddFunc(spec string, cmd func(), opts ...JobOption) (EntryID, error)

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

func (c *Cron) AddJob(spec string, cmd Job, opts ...JobOption) (EntryID, error)

AddJob adds a Job to the Cron to be run on the given schedule. The spec is parsed using the time zone of this Cron instance as the default. An opaque ID is returned that can be used to later remove it.

Optional JobOption arguments can be provided to set metadata like Name and Tags:

c.AddJob("@every 1h", myJob, cron.WithName("my-job"), cron.WithTags("critical"))

Returns ErrMaxEntriesReached if the maximum entry limit has been reached. Returns ErrDuplicateName if a name is provided and already exists.

Example

This example demonstrates implementing the Job interface for complex job logic.

package main

import (
	"fmt"
	"log"

	cron "github.com/netresearch/go-cron"
)

func main() {
	c := cron.New()

	// Define a job type
	type cleanupJob struct {
		name string
	}

	// Implement the Job interface
	job := &cleanupJob{name: "temp files"}

	// AddJob accepts any type implementing cron.Job
	_, err := c.AddJob("0 0 * * *", cron.FuncJob(func() {
		fmt.Printf("Cleaning up %s\n", job.name)
	}))
	if err != nil {
		log.Fatal(err)
	}

	c.Start()
	defer c.Stop()
}

func (*Cron) Entries

func (c *Cron) Entries() []Entry

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

func (c *Cron) EntriesByTag(tag string) []Entry

EntriesByTag returns snapshots of all entries that have the given tag. Returns an empty slice if no entries match.

func (*Cron) Entry

func (c *Cron) Entry(id EntryID) 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

func (c *Cron) EntryByName(name string) Entry

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) Location

func (c *Cron) Location() *time.Location

Location gets the time zone location

func (*Cron) Remove

func (c *Cron) Remove(id EntryID)

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

func (c *Cron) RemoveByName(name string) bool

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

func (c *Cron) RemoveByTag(tag string) int

RemoveByTag removes all entries that have the given tag. Returns the number of entries removed.

func (*Cron) Run

func (c *Cron) Run()

Run the cron scheduler, or no-op if already running.

func (*Cron) Schedule deprecated

func (c *Cron) Schedule(schedule Schedule, cmd Job) EntryID

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

func (c *Cron) ScheduleJob(schedule Schedule, cmd Job, opts ...JobOption) (EntryID, error)

ScheduleJob adds a Job to the Cron to be run on the given schedule. The job is wrapped with the configured Chain.

Optional JobOption arguments can be provided to set metadata like Name and Tags:

c.ScheduleJob(schedule, myJob, cron.WithName("my-job"), cron.WithTags("critical"))

Returns ErrMaxEntriesReached if the maximum entry limit has been reached. Returns ErrDuplicateName if a name is provided and already exists.

Note: When the cron is running, the limit check is approximate due to concurrent entry additions. The actual count may briefly exceed the limit by the number of concurrent ScheduleJob calls in flight.

func (*Cron) Start

func (c *Cron) Start()

Start the cron scheduler in its own goroutine, or no-op if already started.

func (*Cron) Stop

func (c *Cron) Stop() context.Context

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

func (c *Cron) StopWithTimeout(timeout time.Duration) bool

StopWithTimeout stops the cron scheduler and waits for running jobs to complete with a timeout. Returns true if all jobs completed within the timeout, false if the timeout was reached and some jobs may still be running.

When the timeout is reached, jobs that implement JobWithContext should already have received context cancellation and should be in the process of shutting down. Jobs that don't check their context may continue running in the background.

A timeout of zero or negative waits indefinitely (equivalent to StopAndWait).

Example:

if !c.StopWithTimeout(30 * time.Second) {
    log.Println("Warning: some jobs did not complete within 30s")
}

type Entry

type Entry struct {
	// ID is the cron-assigned ID of this entry, which may be used to look up a
	// snapshot or remove it.
	ID EntryID

	// Name is an optional human-readable identifier for this entry.
	// If set, names must be unique within a Cron instance.
	// Use WithName() when adding an entry to set this field.
	Name string

	// Tags is an optional set of labels for categorizing and filtering entries.
	// Multiple entries can share the same tags.
	// Use WithTags() when adding an entry to set this field.
	Tags []string

	// Schedule on which this job should be run.
	Schedule Schedule

	// Next time the job will run, or the zero time if Cron has not been
	// started or this entry's schedule is unsatisfiable
	Next time.Time

	// Prev is the last time this job was run, or the zero time if never.
	Prev time.Time

	// WrappedJob is the thing to run when the Schedule is activated.
	WrappedJob Job

	// Job is the thing that was submitted to cron.
	// It is kept around so that user code that needs to get at the job later,
	// e.g. via Entries() can do so.
	Job Job
	// contains filtered or unexported fields
}

Entry consists of a schedule and the func to execute on that schedule.

func (Entry) Run

func (e Entry) Run()

Run executes the entry's job through the configured chain wrappers. This ensures that chain decorators like SkipIfStillRunning, DelayIfStillRunning, and Recover are properly applied. Use this method instead of Entry.Job.Run() when you need chain behavior to be respected. Fix for issue #551: Provides a proper way to run jobs with chain decorators.

func (Entry) Valid

func (e Entry) Valid() bool

Valid returns true if this is not the zero entry.

type EntryID

type EntryID uint64

EntryID identifies an entry within a Cron instance. Using uint64 prevents overflow and ID collisions on all platforms.

type FakeClock added in v0.6.0

type FakeClock struct {
	// contains filtered or unexported fields
}

FakeClock provides a controllable clock for testing. It allows advancing time manually and fires timers deterministically.

func NewFakeClock added in v0.6.0

func NewFakeClock(t time.Time) *FakeClock

NewFakeClock creates a new FakeClock initialized to the given time.

func (*FakeClock) Advance added in v0.6.0

func (f *FakeClock) Advance(d time.Duration)

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

func (f *FakeClock) BlockUntil(n int)

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

func (f *FakeClock) NewTimer(d time.Duration) Timer

NewTimer creates a fake timer that fires when the clock advances past its deadline.

func (*FakeClock) Now added in v0.6.0

func (f *FakeClock) Now() time.Time

Now returns the fake clock's current time.

func (*FakeClock) Set added in v0.6.0

func (f *FakeClock) Set(t time.Time)

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

func (f *FakeClock) TimerCount() int

TimerCount returns the number of active timers. Useful for test assertions.

type FuncJob

type FuncJob func()

FuncJob is a wrapper that turns a func() into a cron.Job

func (FuncJob) Run

func (f FuncJob) Run()

Run calls the wrapped function.

type FuncJobWithContext added in v0.6.0

type FuncJobWithContext func(ctx context.Context)

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 Job

type Job interface {
	Run()
}

Job is an interface for submitted cron jobs.

type JobOption added in v0.6.0

type JobOption func(*Entry)

JobOption configures an Entry when adding a job to Cron.

func WithName added in v0.6.0

func WithName(name string) JobOption

WithName sets a unique name for the job entry. Names must be unique within a Cron instance; adding a job with a duplicate name will return ErrDuplicateName.

Named jobs can be retrieved with EntryByName() or removed with RemoveByName().

Example:

c.AddFunc("@every 1h", cleanup, cron.WithName("hourly-cleanup"))

func WithTags added in v0.6.0

func WithTags(tags ...string) JobOption

WithTags sets tags for categorizing the job entry. Multiple entries can share the same tags, enabling group operations.

Tagged jobs can be filtered with EntriesByTag() or removed with RemoveByTag().

Example:

c.AddFunc("@every 1h", cleanup, cron.WithTags("maintenance", "hourly"))

type JobWithContext added in v0.6.0

type JobWithContext interface {
	Job
	RunWithContext(ctx context.Context)
}

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

type JobWrapper func(Job) Job

JobWrapper decorates the given Job with some behavior.

func CircuitBreaker added in v0.6.0

func CircuitBreaker(logger Logger, threshold int, cooldown time.Duration) JobWrapper
Example

This example demonstrates CircuitBreaker to prevent cascading failures. After consecutive failures, the circuit opens and skips execution until cooldown.

package main

import (
	"fmt"
	"time"

	cron "github.com/netresearch/go-cron"
)

func main() {
	logger := cron.DefaultLogger

	c := cron.New(cron.WithChain(
		// Outermost: catches re-panic from circuit breaker
		cron.Recover(logger),
		// Open circuit after 3 consecutive failures, cooldown for 5 minutes
		cron.CircuitBreaker(logger, 3, 5*time.Minute),
	))

	c.AddFunc("@every 1m", func() {
		// Simulate a job calling an external service
		if err := callExternalAPI(); err != nil {
			panic(err) // After 3 failures, circuit opens for 5 minutes
		}
		fmt.Println("API call succeeded")
	})

	c.Start()
	defer c.Stop()
}

// callExternalAPI is a mock function for the CircuitBreaker example
func callExternalAPI() error {

	return nil
}

func DelayIfStillRunning

func DelayIfStillRunning(logger Logger) JobWrapper

DelayIfStillRunning serializes jobs, delaying subsequent runs until the previous one is complete. Jobs running after a delay of more than a minute have the delay logged at Info.

func Recover

func Recover(logger Logger, opts ...RecoverOption) JobWrapper

Recover panics in wrapped jobs and log them with the provided logger.

By default, panics are logged at Error level. Use WithLogLevel to change this behavior, for example when combined with retry wrappers.

Example:

// Default behavior - logs at Error level
cron.NewChain(cron.Recover(logger)).Then(job)

// Log at Info level (useful with retries)
cron.NewChain(cron.Recover(logger, cron.WithLogLevel(cron.LogLevelInfo))).Then(job)

func RetryWithBackoff added in v0.6.0

func RetryWithBackoff(logger Logger, maxRetries int, initialDelay, maxDelay time.Duration, multiplier float64) JobWrapper
Example

This example demonstrates RetryWithBackoff for jobs that may fail transiently. The wrapper catches panics and retries with exponential backoff.

package main

import (
	"fmt"
	"time"

	cron "github.com/netresearch/go-cron"
)

func main() {
	logger := cron.DefaultLogger

	c := cron.New(cron.WithChain(
		// Outermost: catches final re-panic after retries exhausted
		cron.Recover(logger),
		// Retry up to 3 times with exponential backoff
		// Initial delay: 1s, max delay: 30s, multiplier: 2.0
		cron.RetryWithBackoff(logger, 3, time.Second, 30*time.Second, 2.0),
	))

	attempts := 0
	c.AddFunc("@hourly", func() {
		attempts++
		// Simulate transient failure that succeeds on 3rd attempt
		if attempts < 3 {
			panic(fmt.Sprintf("attempt %d failed", attempts))
		}
		fmt.Printf("Succeeded on attempt %d\n", attempts)
	})

	c.Start()
	defer c.Stop()
}
Example (NoRetries)

This example demonstrates RetryWithBackoff with maxRetries=0 (no retries). This is the safe default - jobs execute once and fail immediately on panic.

package main

import (
	"time"

	cron "github.com/netresearch/go-cron"
)

func main() {
	logger := cron.DefaultLogger

	c := cron.New(cron.WithChain(
		cron.Recover(logger),
		// maxRetries=0 means no retries - execute once, fail immediately
		cron.RetryWithBackoff(logger, 0, time.Second, 30*time.Second, 2.0),
	))

	c.AddFunc("@hourly", func() {
		// This will execute once, panic, and not retry
		panic("immediate failure")
	})

	c.Start()
	defer c.Stop()
}

func SkipIfStillRunning

func SkipIfStillRunning(logger Logger) JobWrapper

SkipIfStillRunning skips an invocation of the Job if a previous invocation is still running. It logs skips to the given logger at Info level.

func Timeout added in v0.6.0

func Timeout(logger Logger, timeout time.Duration, opts ...TimeoutOption) JobWrapper

Timeout wraps a job with a timeout. If the job takes longer than the given duration, the wrapper returns and logs an error, but the underlying job goroutine continues running until completion.

⚠️ IMPORTANT: Abandonment Model

This wrapper implements an "abandonment model" - when a timeout occurs, the wrapper returns but the job's goroutine is NOT canceled. The job will continue executing in the background until it naturally completes. This means:

  • Resources held by the job will not be released until completion
  • Side effects will still occur even after timeout
  • Multiple abandoned goroutines can accumulate if jobs consistently timeout

Goroutine Accumulation Risk

If a job consistently takes longer than its schedule interval, abandoned goroutines will accumulate:

// DANGER: This pattern causes goroutine accumulation!
c.AddFunc("@every 1s", func() {
    time.Sleep(5 * time.Second) // Takes 5x longer than schedule
})
// With Timeout(2s), a new abandoned goroutine is created every second

Tracking Abandoned Goroutines

Use WithTimeoutCallback to track timeout events for metrics and alerting:

cron.Timeout(logger, 5*time.Minute,
    cron.WithTimeoutCallback(func(timeout time.Duration) {
        abandonedGoroutines.Inc() // Prometheus counter
    }),
)

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.

const (
	// LogLevelError logs panics at Error level (default).
	LogLevelError LogLevel = iota
	// LogLevelInfo logs panics at Info level.
	// Useful when combined with retry wrappers to reduce log noise
	// for expected transient failures.
	LogLevelInfo
)

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

func PrintfLogger(l interface{ Printf(string, ...any) }) Logger

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

func VerbosePrintfLogger(l interface{ Printf(string, ...any) }) Logger

VerbosePrintfLogger wraps a Printf-based logger (such as the standard library "log") into an implementation of the Logger interface which logs everything.

Example

This example demonstrates verbose logging for debugging.

package main

import (
	"fmt"
	"log"

	cron "github.com/netresearch/go-cron"
)

func main() {
	logger := cron.VerbosePrintfLogger(log.Default())

	c := cron.New(cron.WithLogger(logger))

	c.AddFunc("@hourly", func() {
		fmt.Println("hourly job")
	})

	c.Start()
	defer c.Stop()
}

type NamedJob added in v0.6.0

type NamedJob interface {
	Job
	Name() string
}

NamedJob is an optional interface that jobs can implement to provide a name for observability purposes. If a job doesn't implement this interface, an empty string is used for the name in hook callbacks.

Example

This example demonstrates implementing NamedJob for better observability. Named jobs have their name passed to observability hooks, which is useful for metrics labeling (e.g., Prometheus labels).

package main

import (
	"fmt"
)

func main() {
	// myJob implements both Job and NamedJob interfaces
	type myJob struct {
		name string
	}

	// Run implements cron.Job
	run := func(j *myJob) {
		fmt.Printf("Running %s\n", j.name)
	}
	_ = run

	// Name implements cron.NamedJob
	name := func(j *myJob) string {
		return j.name
	}
	_ = name

	// When used with observability hooks, the name is passed to callbacks:
	// OnJobStart(id, "my-job-name", scheduledTime)
	// OnJobComplete(id, "my-job-name", duration, recovered)
	fmt.Println("NamedJob provides names for observability hooks")
}
Output:

NamedJob provides names for observability hooks

type ObservabilityHooks added in v0.6.0

type ObservabilityHooks struct {
	// OnJobStart is called immediately before a job begins execution.
	// Parameters:
	//   - entryID: the unique identifier for the scheduled entry
	//   - name: job name (from NamedJob interface, or empty string)
	//   - scheduledTime: the time the job was scheduled to run
	OnJobStart func(entryID EntryID, name string, scheduledTime time.Time)

	// OnJobComplete is called when a job finishes execution.
	// Parameters:
	//   - entryID: the unique identifier for the scheduled entry
	//   - name: job name (from NamedJob interface, or empty string)
	//   - duration: how long the job took to execute
	//   - recovered: the value from recover() if the job panicked, or nil
	OnJobComplete func(entryID EntryID, name string, duration time.Duration, recovered any)

	// OnSchedule is called when a job's next execution time is calculated.
	// Parameters:
	//   - entryID: the unique identifier for the scheduled entry
	//   - name: job name (from NamedJob interface, or empty string)
	//   - nextRun: the next scheduled execution time
	OnSchedule func(entryID EntryID, name string, nextRun time.Time)
}

ObservabilityHooks provides callbacks for monitoring cron operations. All callbacks are optional; nil callbacks are safely ignored.

Hooks are called asynchronously in separate goroutines to prevent slow callbacks from blocking the scheduler. This means:

  • Callbacks may execute slightly after the event occurred
  • Callback execution order is not guaranteed across events
  • Callbacks should be safe for concurrent execution

If you need synchronous execution, use channels or sync primitives within your callback implementation.

Example with Prometheus:

hooks := cron.ObservabilityHooks{
    OnJobStart: func(id cron.EntryID, name string, scheduled time.Time) {
        jobsStarted.WithLabelValues(name).Inc()
    },
    OnJobComplete: func(id cron.EntryID, name string, dur time.Duration, recovered any) {
        jobDuration.WithLabelValues(name).Observe(dur.Seconds())
        if recovered != nil {
            jobPanics.WithLabelValues(name).Inc()
        }
    },
}
c := cron.New(cron.WithObservability(hooks))

type Option

type Option func(*Cron)

Option represents a modification to the default behavior of a Cron.

func WithChain

func WithChain(wrappers ...JobWrapper) Option

WithChain specifies Job wrappers to apply to all jobs added to this cron. Refer to the Chain* functions in this package for provided wrappers.

Example

This example demonstrates using job wrappers (middleware) with WithChain.

package main

import (
	"fmt"

	cron "github.com/netresearch/go-cron"
)

func main() {
	// Create cron with job wrappers applied to all jobs
	c := cron.New(
		cron.WithChain(
			// Recover from panics and log them
			cron.Recover(cron.DefaultLogger),
			// Skip job execution if the previous run hasn't completed
			cron.SkipIfStillRunning(cron.DefaultLogger),
		),
	)

	c.AddFunc("* * * * *", func() {
		fmt.Println("This job is protected by Recover and SkipIfStillRunning")
	})

	c.Start()
	defer c.Stop()
}

func WithClock added in v0.6.0

func WithClock(clock Clock) Option

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

func WithContext(ctx context.Context) Option

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

func WithLocation(loc *time.Location) Option

WithLocation overrides the timezone of the cron instance.

func WithLogger

func WithLogger(logger Logger) Option

WithLogger uses the provided logger.

func WithMaxEntries added in v0.6.0

func WithMaxEntries(max int) Option

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

func WithMaxSearchYears(years int) Option

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

func WithMinEveryInterval(d time.Duration) Option

WithMinEveryInterval configures the minimum interval allowed for @every expressions. This allows overriding the default 1-second minimum.

Use cases:

  • Sub-second intervals for testing: WithMinEveryInterval(0) or WithMinEveryInterval(100*time.Millisecond)
  • Enforce longer minimums for rate limiting: WithMinEveryInterval(time.Minute)

The interval affects:

  • Parsing of "@every <duration>" expressions
  • The EveryWithMin function when called via the parser

Note: This option replaces the current parser. If you need custom parser options along with a custom minimum interval, use WithParser with a manually configured parser:

p := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor).
    WithMinEveryInterval(100 * time.Millisecond)
c := cron.New(cron.WithParser(p))

Example:

// Allow sub-second intervals (useful for testing)
c := cron.New(cron.WithMinEveryInterval(0))
c.AddFunc("@every 100ms", func() { ... })

// Enforce minimum 1-minute intervals
c := cron.New(cron.WithMinEveryInterval(time.Minute))
c.AddFunc("@every 30s", func() { ... }) // Error: must be at least 1 minute
Example

This example demonstrates using WithMinEveryInterval to configure the minimum interval for @every expressions at the cron level.

package main

import (
	"fmt"
	"log"

	cron "github.com/netresearch/go-cron"
)

func main() {
	// Allow sub-second @every intervals (useful for testing)
	c := cron.New(cron.WithMinEveryInterval(0))

	_, err := c.AddFunc("@every 100ms", func() {
		fmt.Println("Running every 100ms")
	})
	if err != nil {
		log.Fatal(err)
	}

	// With default settings, sub-second would fail:
	// c := cron.New() // default minimum is 1 second
	// _, err := c.AddFunc("@every 100ms", ...) // returns error

	c.Start()
	defer c.Stop()
}
Example (RateLimit)

This example demonstrates using WithMinEveryInterval to enforce longer minimum intervals for rate limiting purposes.

package main

import (
	"fmt"
	"log"
	"time"

	cron "github.com/netresearch/go-cron"
)

func main() {
	// Enforce minimum 1-minute intervals
	c := cron.New(cron.WithMinEveryInterval(time.Minute))

	// This will fail because 30s < 1m minimum
	_, err := c.AddFunc("@every 30s", func() {
		fmt.Println("This won't be added")
	})
	if err != nil {
		fmt.Println("Error:", err)
	}

	// This will succeed because 2m >= 1m minimum
	_, err = c.AddFunc("@every 2m", func() {
		fmt.Println("Running every 2 minutes")
	})
	if err != nil {
		log.Fatal(err)
	}

	c.Start()
	defer c.Stop()
}
Output:

Error: @every duration must be at least 1m0s: "@every 30s"

func WithObservability added in v0.6.0

func WithObservability(hooks ObservabilityHooks) Option

WithObservability configures observability hooks for monitoring cron operations. Hooks are called asynchronously in separate goroutines to prevent slow callbacks from blocking the scheduler. This means callback execution order is not guaranteed.

All hook callbacks are optional; nil callbacks are safely ignored.

Example with Prometheus metrics:

hooks := cron.ObservabilityHooks{
    OnJobStart: func(id cron.EntryID, name string, scheduled time.Time) {
        jobsStarted.WithLabelValues(name).Inc()
    },
    OnJobComplete: func(id cron.EntryID, name string, dur time.Duration, recovered any) {
        jobDuration.WithLabelValues(name).Observe(dur.Seconds())
        if recovered != nil {
            jobPanics.WithLabelValues(name).Inc()
        }
    },
}
c := cron.New(cron.WithObservability(hooks))
Example

This example demonstrates using observability hooks for metrics collection. In production, you would integrate with Prometheus, StatsD, or similar systems.

package main

import (
	"fmt"
	"time"

	cron "github.com/netresearch/go-cron"
)

func main() {
	var jobsStarted, jobsCompleted int

	hooks := cron.ObservabilityHooks{
		OnJobStart: func(id cron.EntryID, name string, scheduled time.Time) {
			// In production: prometheus.Counter.Inc()
			jobsStarted++
		},
		OnJobComplete: func(id cron.EntryID, name string, duration time.Duration, recovered any) {
			// In production: prometheus.Histogram.Observe(duration.Seconds())
			jobsCompleted++
		},
		OnSchedule: func(id cron.EntryID, name string, nextRun time.Time) {
			// In production: prometheus.Gauge.Set(nextRun.Unix())
		},
	}

	c := cron.New(cron.WithObservability(hooks))

	c.AddFunc("@hourly", func() {
		// Job logic here
	})

	c.Start()
	c.Stop()

	fmt.Println("Hooks configured successfully")
}
Output:

Hooks configured successfully

func WithParser

func WithParser(p ScheduleParser) Option

WithParser overrides the parser used for interpreting job schedules.

func WithSeconds

func WithSeconds() Option

WithSeconds overrides the parser used for interpreting job schedules to include a seconds field as the first one.

type PanicWithStack added in v0.6.0

type PanicWithStack struct {
	Value any    // The original panic value
	Stack []byte // Stack trace at point of panic
}

PanicWithStack wraps a panic value with the stack trace at the point of panic. This allows re-panicking to preserve the original stack trace for debugging.

func (*PanicWithStack) Error added in v0.6.0

func (p *PanicWithStack) Error() string

Error implements the error interface for PanicWithStack.

func (*PanicWithStack) String added in v0.6.0

func (p *PanicWithStack) String() string

String returns a detailed representation including the stack trace.

func (*PanicWithStack) Unwrap added in v0.6.0

func (p *PanicWithStack) Unwrap() error

Unwrap returns the original panic value if it was an error.

type ParseOption

type ParseOption int

ParseOption represents configuration options for creating a parser. Most options specify which fields should be included, while others enable features. If a field is not included the parser will assume a default value. These options do not change the order fields are parsed in.

const (
	Second         ParseOption = 1 << iota // Seconds field, default 0
	SecondOptional                         // Optional seconds field, default 0
	Minute                                 // Minutes field, default 0
	Hour                                   // Hours field, default 0
	Dom                                    // Day of month field, default *
	Month                                  // Month field, default *
	Dow                                    // Day of week field, default *
	DowOptional                            // Optional day of week field, default *
	Descriptor                             // Allow descriptors such as @monthly, @weekly, etc.
)

ParseOption constants define which fields are included in parsing.

type Parser

type Parser struct {
	// contains filtered or unexported fields
}

Parser is a custom cron expression parser that can be configured.

func MustNewParser added in v0.6.0

func MustNewParser(options ParseOption) Parser

MustNewParser is like TryNewParser but panics if the options are invalid. This follows the Go convention of Must* functions for cases where failure indicates a programming error rather than a runtime condition.

Use MustNewParser when:

  • Parser options are hardcoded constants
  • Invalid configuration is a bug that should fail fast

Use TryNewParser when:

  • Parser options come from config files, environment, or user input
  • You want to handle configuration errors gracefully

Note: In v2.0, NewParser will return (Parser, error) and MustNewParser will be the only panicking variant. Using MustNewParser now ensures forward compatibility with v2.0.

Example:

// Panics if options are invalid (hardcoded, so invalid = bug)
var parser = cron.MustNewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)

func NewParser deprecated

func NewParser(options ParseOption) Parser

NewParser creates a Parser with custom options.

Deprecated: NewParser will change to return (Parser, error) in v2.0. Use MustNewParser for panic-on-error behavior (forward compatible), or TryNewParser for explicit error handling.

It panics if more than one Optional is given, since it would be impossible to correctly infer which optional is provided or missing in general.

Examples

// Standard parser without descriptors
specParser := NewParser(Minute | Hour | Dom | Month | Dow)
sched, err := specParser.Parse("0 0 15 */3 *")

// Same as above, just excludes time fields
specParser := NewParser(Dom | Month | Dow)
sched, err := specParser.Parse("15 */3 *")

// Same as above, just makes Dow optional
specParser := NewParser(Dom | Month | DowOptional)
sched, err := specParser.Parse("15 */3")

func StandardParser added in v0.6.0

func StandardParser() Parser

StandardParser returns a copy of the standard parser used by ParseStandard. This can be used as a base for creating custom parsers with modified settings.

Example:

// Create parser allowing sub-second @every intervals
p := StandardParser().WithMinEveryInterval(0)
c := cron.New(cron.WithParser(p))

func TryNewParser added in v0.6.0

func TryNewParser(options ParseOption) (Parser, error)

TryNewParser creates a Parser with custom options, returning an error if the configuration is invalid. This is the safe alternative to NewParser for cases where parser options come from runtime configuration rather than hardcoded values.

Use TryNewParser when:

  • Parser options come from config files, environment variables, or user input
  • You want to handle configuration errors gracefully

Use NewParser when:

  • Parser options are hardcoded constants (invalid config = bug)
  • You want to fail fast during initialization

Returns ErrNoFields if no fields or Descriptor are configured. Returns ErrMultipleOptionals if more than one optional field is configured.

Example:

// Safe parsing from config
opts := loadParserOptionsFromConfig()
parser, err := TryNewParser(opts)
if err != nil {
    return fmt.Errorf("invalid parser config: %w", err)
}

func (Parser) Parse

func (p Parser) Parse(spec string) (Schedule, error)

Parse returns a new crontab schedule representing the given spec. It returns a descriptive error if the spec is not valid. It accepts crontab specs and features configured by NewParser.

If caching is enabled via WithCache(), repeated calls with the same spec will return the cached result.

func (Parser) WithCache added in v0.6.0

func (p Parser) WithCache() Parser

WithCache returns a new Parser with caching enabled for parsed schedules. When caching is enabled, repeated calls to Parse with the same spec string will return the cached result instead of re-parsing.

Caching is particularly beneficial when:

  • The same cron expressions are parsed repeatedly
  • Multiple cron instances share the same parser
  • Configuration is reloaded frequently

The cache is thread-safe and grows unbounded. For applications with many unique spec strings, consider using a single shared parser instance.

Example:

// Create a caching parser for improved performance
p := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor).
    WithCache()

// Subsequent parses of the same spec return cached results
sched1, _ := p.Parse("0 * * * *") // parsed
sched2, _ := p.Parse("0 * * * *") // cached (same reference)

func (Parser) WithMaxSearchYears added in v0.6.0

func (p Parser) WithMaxSearchYears(years int) Parser

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

func (p Parser) WithMinEveryInterval(d time.Duration) Parser

WithMinEveryInterval returns a new Parser with the specified minimum interval for @every expressions. This allows overriding the default 1-second minimum.

Use 0 or negative values to disable the minimum check entirely. Use values larger than 1 second to enforce longer minimum intervals.

Example:

// Allow sub-second intervals (for testing)
p := NewParser(Minute | Hour | Dom | Month | Dow | Descriptor).
    WithMinEveryInterval(100 * time.Millisecond)

// Enforce minimum 1-minute intervals (for rate limiting)
p := NewParser(Minute | Hour | Dom | Month | Dow | Descriptor).
    WithMinEveryInterval(time.Minute)

type RealClock added in v0.6.0

type RealClock struct{}

RealClock implements Clock using the standard time package. This is the default clock used in production.

func (RealClock) NewTimer added in v0.6.0

func (RealClock) NewTimer(d time.Duration) Timer

NewTimer creates a new Timer that will send the current time on its channel after at least duration d.

func (RealClock) Now added in v0.6.0

func (RealClock) Now() time.Time

Now returns the current time.

type RecoverOption added in v0.6.0

type RecoverOption func(*recoverOpts)

RecoverOption configures the Recover wrapper.

func WithLogLevel added in v0.6.0

func WithLogLevel(level LogLevel) RecoverOption

WithLogLevel sets the log level for recovered panics. Default is LogLevelError. Use LogLevelInfo to reduce noise when combined with retry wrappers like RetryWithBackoff.

Example:

cron.Recover(logger, cron.WithLogLevel(cron.LogLevelInfo))

type Schedule

type Schedule interface {
	// Next returns the next activation time, later than the given time.
	// Next is invoked initially, and then each time the job is run.
	Next(time.Time) time.Time
}

Schedule describes a job's duty cycle.

func ParseStandard

func ParseStandard(standardSpec string) (Schedule, error)

ParseStandard returns a new crontab schedule representing the given standardSpec (https://en.wikipedia.org/wiki/Cron). It requires 5 entries representing: minute, hour, day of month, month and day of week, in that order. It returns a descriptive error if the spec is not valid.

It accepts

  • Standard crontab specs, e.g. "* * * * ?"
  • Descriptors, e.g. "@midnight", "@every 1h30m"
Example

This example demonstrates parsing a cron expression.

package main

import (
	"fmt"
	"log"
	"time"

	cron "github.com/netresearch/go-cron"
)

func main() {
	schedule, err := cron.ParseStandard("0 9 * * MON-FRI")
	if err != nil {
		log.Fatal(err)
	}

	// Get the next scheduled time
	now := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) // Wednesday
	next := schedule.Next(now)
	fmt.Printf("Next run: %s\n", next.Format("Mon 15:04"))
}
Output:

Next run: Wed 09:00

type ScheduleParser

type ScheduleParser interface {
	Parse(spec string) (Schedule, error)
}

ScheduleParser is an interface for schedule spec parsers that return a Schedule

type SlogLogger added in v0.6.0

type SlogLogger struct {
	// contains filtered or unexported fields
}

SlogLogger adapts log/slog to the Logger interface. This allows integration with Go 1.21+ structured logging.

func NewSlogLogger added in v0.6.0

func NewSlogLogger(l *slog.Logger) *SlogLogger

NewSlogLogger creates a Logger that writes to the given slog.Logger. If l is nil, slog.Default() is used.

func (*SlogLogger) Error added in v0.6.0

func (s *SlogLogger) Error(err error, msg string, keysAndValues ...any)

Error logs an error condition using slog.

func (*SlogLogger) Info added in v0.6.0

func (s *SlogLogger) Info(msg string, keysAndValues ...any)

Info logs routine messages about cron's operation using slog.

type SpecSchedule

type SpecSchedule struct {
	Second, Minute, Hour, Dom, Month, Dow uint64

	// Override location for this schedule.
	Location *time.Location

	// MaxSearchYears limits how many years into the future Next() will search
	// before giving up and returning zero time. This prevents infinite loops
	// for unsatisfiable schedules (e.g., Feb 30). Zero means use the default (5 years).
	MaxSearchYears int
}

SpecSchedule specifies a duty cycle (to the second granularity), based on a traditional crontab specification. It is computed initially and stored as bit sets.

func (*SpecSchedule) Next

func (s *SpecSchedule) Next(t time.Time) time.Time

Next returns the next time this schedule is activated, greater than the given time. If no time can be found to satisfy the schedule, returns the zero time.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL