cron

package module
v0.8.0 Latest Latest
Warning

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

Go to latest
Published: Dec 26, 2025 License: MIT Imports: 20 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.

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

Examples

Constants

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

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

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

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 ErrEmptySpec = &ValidationError{Message: "empty spec string"}

ErrEmptySpec is returned when an empty spec string is provided.

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 = errors.New("multiple optionals may not be configured")

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

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

ErrNoFields is returned when no fields or Descriptor are configured.

Functions

func Between added in v0.7.0

func Between(schedule Schedule, start, end time.Time) []time.Time

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

func BetweenWithLimit(schedule Schedule, start, end time.Time, limit int) []time.Time

BetweenWithLimit returns execution times in the range [start, end) up to limit. If limit is 0 or negative, no limit is applied. Returns nil if schedule is nil.

Example:

schedule, _ := cron.ParseStandard("* * * * *") // Every minute
times := cron.BetweenWithLimit(schedule, start, end, 100) // Max 100 results

func Count added in v0.7.0

func Count(schedule Schedule, start, end time.Time) int

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

func CountWithLimit(schedule Schedule, start, end time.Time, limit int) int

CountWithLimit counts executions in the range [start, end) up to limit. If limit is 0 or negative, no limit is applied. Returns the count, which will be at most limit if a limit was specified. Returns 0 if schedule is nil.

Example:

schedule, _ := cron.ParseStandard("* * * * *")
count := cron.CountWithLimit(schedule, start, end, 10000)
if count == 10000 {
    fmt.Println("At least 10000 executions")
}

func Matches added in v0.7.0

func Matches(schedule Schedule, t time.Time) bool

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

func NextN(schedule Schedule, t time.Time, n int) []time.Time

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

func NormalizeDOW(bits uint64) uint64

NormalizeDOW normalizes the day-of-week bitmask by mapping bit 7 (Sunday as 7) to bit 0 (Sunday as 0). This allows both "0" and "7" to represent Sunday, matching the behavior of many cron implementations.

func ValidateSpec added in v0.7.0

func ValidateSpec(spec string, options ...ParseOption) error

ValidateSpec validates a cron expression without scheduling a job. It returns nil if the spec is valid, or an error describing the problem.

By default, it uses the standard parser (5-field cron + descriptors). Pass a ParseOption to customize validation (e.g., to require seconds field).

Example:

// Validate user input
if err := cron.ValidateSpec(userInput); err != nil {
    return fmt.Errorf("invalid cron expression: %w", err)
}

// Validate with seconds field
if err := cron.ValidateSpec(userInput, cron.Second|cron.Minute|cron.Hour|cron.Dom|cron.Month|cron.Dow); err != nil {
    // Handle error
}

func ValidateSpecs added in v0.7.0

func ValidateSpecs(specs []string, options ...ParseOption) map[int]error

ValidateSpecs validates multiple cron expressions at once. It returns a map of index to error for any invalid specs. If all specs are valid, returns an empty map (not nil).

This is useful for:

  • Validating configuration files before deployment
  • Bulk validation with detailed error reporting
  • Pre-flight checks before registering multiple jobs

Example:

specs := []string{"* * * * *", "invalid", "0 9 * * MON-FRI", "bad"}
errors := cron.ValidateSpecs(specs)
if len(errors) > 0 {
    for idx, err := range errors {
        log.Printf("Spec %d is invalid: %v", idx, err)
    }
}

// For all-or-nothing validation:
if len(errors) > 0 {
    return fmt.Errorf("invalid specs: %v", errors)
}
// Now safe to add all specs
for _, spec := range specs {
    c.AddFunc(spec, handler)
}

Types

type Chain

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

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

func NewChain

func NewChain(c ...JobWrapper) Chain

NewChain returns a Chain consisting of the given JobWrappers.

Example

This example demonstrates wrapping individual jobs with chains.

package main

import (
	"fmt"
	"time"

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

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

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

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

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

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.

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

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) AddOnceFunc added in v0.7.0

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

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

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

AddOnceJob adds a Job to run once on the given schedule, then automatically remove itself. This is a convenience wrapper that combines AddJob with WithRunOnce().

Example:

c.AddOnceJob("@in 1h", myJob, cron.WithName("one-time-task"))

func (*Cron) Entries

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) IsRunning added in v0.7.0

func (c *Cron) IsRunning() bool

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) 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) ScheduleOnceJob added in v0.7.0

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

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

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 DomConstraint added in v0.7.0

type DomConstraint struct {
	Type DomConstraintType
	N    int // For DomLastOffset: offset; for DomNearestWeekday: day number
}

DomConstraint represents a dynamic day-of-month constraint.

type DomConstraintType added in v0.7.0

type DomConstraintType uint8

DomConstraintType identifies the type of day-of-month constraint.

const (
	// DomLast represents 'L' - the last day of the month.
	DomLast DomConstraintType = iota
	// DomLastOffset represents 'L-n' - n days before the last day of month.
	DomLastOffset
	// DomLastWeekday represents 'LW' - the last weekday (Mon-Fri) of month.
	DomLastWeekday
	// DomNearestWeekday represents 'nW' - the nearest weekday to day n.
	DomNearestWeekday
)

type DowConstraint added in v0.7.0

type DowConstraint struct {
	Weekday int // 0-6 (Sunday=0, Saturday=6)
	N       int // 1-5 for nth occurrence, -1 for last occurrence
}

DowConstraint represents an nth-weekday-of-month constraint. For example, FRI#3 means "3rd Friday of the month".

type Entry

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

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

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

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

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

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

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

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

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

func (Entry) Run

func (e Entry) Run()

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

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 WithPrev added in v0.7.0

func WithPrev(prev time.Time) JobOption

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

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 Jitter added in v0.7.0

func Jitter(maxJitter time.Duration) JobWrapper

Jitter adds a random delay before job execution to prevent thundering herd. When many jobs are scheduled at the same time (e.g., @hourly), they would all execute simultaneously, causing database connection spikes, API rate limiting, and resource contention. Jitter spreads out the execution times.

The delay is uniformly distributed in the range [0, maxJitter). A maxJitter of 0 or negative disables jitter (no delay).

Example:

// Add 0-30s random delay before each execution
cron.NewChain(cron.Jitter(30 * time.Second)).Then(myJob)

// Compose with other wrappers
cron.NewChain(
    cron.Recover(logger),
    cron.Jitter(30 * time.Second),
    cron.SkipIfStillRunning(logger),
).Then(myJob)

// Use via WithChain option
c.AddFunc("@hourly", syncData, cron.WithChain(cron.Jitter(30*time.Second)))
Example

This example demonstrates using Jitter to prevent thundering herd. When many jobs are scheduled at the same time (e.g., @hourly), they would all execute simultaneously. Jitter adds a random delay to spread them out.

package main

import (
	"fmt"
	"time"

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

func main() {
	c := cron.New(cron.WithChain(
		cron.Recover(cron.DefaultLogger),
		// Add 0-30s random delay before each job execution
		cron.Jitter(30*time.Second),
	))

	c.AddFunc("@hourly", func() {
		// This job will start between 0-30 seconds after the hour
		// Each execution gets a new random delay
		fmt.Println("Processing hourly task")
	})

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

This example demonstrates applying Jitter to individual jobs. Different jobs can have different jitter ranges.

package main

import (
	"fmt"
	"time"

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

func main() {
	c := cron.New(cron.WithChain(
		cron.Recover(cron.DefaultLogger),
	))

	// High-priority job: minimal jitter (0-5s)
	highPriorityJob := cron.NewChain(cron.Jitter(5 * time.Second)).Then(cron.FuncJob(func() {
		fmt.Println("High priority task")
	}))
	c.Schedule(cron.Every(time.Hour), highPriorityJob)

	// Low-priority job: larger jitter (0-60s)
	lowPriorityJob := cron.NewChain(cron.Jitter(60 * time.Second)).Then(cron.FuncJob(func() {
		fmt.Println("Low priority task")
	}))
	c.Schedule(cron.Every(time.Hour), lowPriorityJob)

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

func JitterWithLogger added in v0.7.0

func JitterWithLogger(logger Logger, maxJitter time.Duration) JobWrapper

JitterWithLogger is like Jitter but logs the applied delay. This is useful for debugging and observability to verify jitter is working.

Example:

cron.NewChain(cron.JitterWithLogger(logger, 30 * time.Second)).Then(myJob)
Example

This example demonstrates JitterWithLogger for observability. The logger records the actual delay applied to each execution.

package main

import (
	"fmt"
	"time"

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

func main() {
	logger := cron.DefaultLogger

	c := cron.New(cron.WithChain(
		cron.Recover(logger),
		// Log the jitter delay for each execution
		cron.JitterWithLogger(logger, 30*time.Second),
	))

	c.AddFunc("@hourly", func() {
		fmt.Println("Task with logged jitter")
	})

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

func Recover

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

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

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

Example:

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

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

func RetryWithBackoff added in v0.6.0

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

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

package main

import (
	"fmt"
	"time"

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

func main() {
	logger := cron.DefaultLogger

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

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

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

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

package main

import (
	"time"

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

func main() {
	logger := cron.DefaultLogger

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

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

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

func SkipIfStillRunning

func SkipIfStillRunning(logger Logger) JobWrapper

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

func Timeout added in v0.6.0

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

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

⚠️ IMPORTANT: Abandonment Model

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

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

Goroutine Accumulation Risk

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

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

Tracking Abandoned Goroutines

Use WithTimeoutCallback to track timeout events for metrics and alerting:

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

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(maxEntries 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 WithSecondOptional added in v0.7.0

func WithSecondOptional() Option

WithSecondOptional overrides the parser used for interpreting job schedules to accept an optional seconds field as the first one. When provided, expressions can have either 5 fields (standard) or 6 fields (with seconds). If 5 fields are given, the seconds field defaults to 0.

This is useful when you want to support both standard 5-field cron expressions and extended 6-field expressions with seconds precision in the same cron instance.

Examples:

c := cron.New(cron.WithSecondOptional())
c.AddFunc("* * * * *", job)        // 5 fields: runs every minute at :00 seconds
c.AddFunc("30 * * * * *", job)     // 6 fields: runs every minute at :30 seconds
c.AddFunc("*/10 * * * * *", job)   // 6 fields: runs every 10 seconds

func WithSeconds

func WithSeconds() Option

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

type PanicError added in v0.7.0

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

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

func (*PanicError) Error added in v0.7.0

func (p *PanicError) Error() string

Error implements the error interface for PanicError.

func (*PanicError) String added in v0.7.0

func (p *PanicError) String() string

String returns a detailed representation including the stack trace.

func (*PanicError) Unwrap added in v0.7.0

func (p *PanicError) Unwrap() error

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

type PanicWithStack deprecated added in v0.6.0

type PanicWithStack = PanicError

PanicWithStack is a type alias for backward compatibility.

Deprecated: Use PanicError instead. This alias will be removed in a future release.

type ParseOption

type ParseOption int

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

const (
	Second         ParseOption = 1 << iota // Seconds field, default 0
	SecondOptional                         // Optional seconds field, default 0
	Minute                                 // Minutes field, default 0
	Hour                                   // Hours field, default 0
	Dom                                    // Day of month field, default *
	Month                                  // Month field, default *
	Dow                                    // Day of week field, default *
	DowOptional                            // Optional day of week field, default *
	Descriptor                             // Allow descriptors such as @monthly, @weekly, etc.
	Year                                   // Year field, default * (any year)
	YearOptional                           // Optional year field, auto-detected by value >= 100
	Hash                                   // Allow Jenkins-style 'H' hash expressions for load distribution
	DowNth                                 // Allow #n syntax in DOW (e.g., FRI#3 for 3rd Friday)
	DowLast                                // Allow #L syntax in DOW (e.g., FRI#L for last Friday)
	DomL                                   // Allow L syntax in DOM (e.g., L for last day, L-3 for 3rd last day)
	DomW                                   // Allow W syntax in DOM (e.g., 15W for nearest weekday, LW for last weekday)
)

ParseOption constants define which fields are included in parsing.

type Parser

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

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

func FullParser added in v0.8.0

func FullParser() Parser

FullParser returns a parser that accepts all cron syntax variants including optional seconds, year field, descriptors, hash expressions, and extended day-of-month/day-of-week syntax.

This parser supports:

  • Standard 5-field cron: minute, hour, day-of-month, month, day-of-week
  • Optional seconds prefix (6 fields): second, minute, hour, dom, month, dow
  • Optional year suffix (7 fields with seconds, 6 without)
  • Descriptors: @yearly, @monthly, @weekly, @daily, @hourly, @every <duration>
  • Hash expressions: H for load-distributed scheduling
  • Extended syntax: FRI#3 (3rd Friday), MON#L (last Monday), L (last day), 15W (nearest weekday)

Example:

c := cron.New(cron.WithParser(cron.FullParser()))
c.AddFunc("0 30 14 25 12 2025", myFunc) // Run at 14:30 on Dec 25, 2025
c.AddFunc("0 0 0 1 1 * 2030", myFunc)   // Run at midnight on Jan 1, 2030

func MustNewParser added in v0.6.0

func MustNewParser(options ParseOption) Parser

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

Use MustNewParser when:

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

Use TryNewParser when:

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

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

Example:

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

func NewParser deprecated

func NewParser(options ParseOption) Parser

NewParser creates a Parser with custom options.

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

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

Examples

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

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

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

This example demonstrates using Hash expressions to distribute jobs. Different hash keys produce different execution times for the same spec.

package main

import (
	"fmt"
	"time"

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

func main() {
	parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Hash)

	// Same spec with different keys produces different schedules
	sched1, _ := parser.ParseWithHashKey("H * * * *", "job-a")
	sched2, _ := parser.ParseWithHashKey("H * * * *", "job-b")

	from := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
	next1 := sched1.Next(from)
	next2 := sched2.Next(from)

	// The two jobs run at different minutes
	fmt.Printf("job-a runs at minute: %d\n", next1.Minute())
	fmt.Printf("job-b runs at minute: %d\n", next2.Minute())
	fmt.Printf("Different times: %v\n", next1.Minute() != next2.Minute())
}
Output:

job-a runs at minute: 54
job-b runs at minute: 23
Different times: true
Example (HashRange)

This example demonstrates using H(range) to constrain the hash.

package main

import (
	"fmt"
	"time"

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

func main() {
	parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Hash)

	// H(0-29) picks a hash-based minute in the first half hour
	schedule, _ := parser.ParseWithHashKey("H(0-29) * * * *", "early-job")

	from := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
	next := schedule.Next(from)

	fmt.Printf("Minute: %d\n", next.Minute())
	fmt.Printf("In range 0-29: %v\n", next.Minute() <= 29)
}
Output:

Minute: 18
In range 0-29: true
Example (HashStep)

This example demonstrates using H/step for distributed interval scheduling.

package main

import (
	"fmt"
	"time"

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

func main() {
	parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Hash)

	// H/15 runs every 15 minutes, but starts at a hash-determined offset
	schedule, _ := parser.ParseWithHashKey("H/15 * * * *", "my-job")

	from := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)

	// Get the first 4 execution times
	for i := 0; i < 4; i++ {
		next := schedule.Next(from)
		fmt.Printf("%s\n", next.Format("15:04"))
		from = next
	}
}
Output:

00:07
00:22
00:37
00:52
Example (YearField)

This example demonstrates parsing cron expressions with a year field. The Year option enables 6-field expressions (minute hour dom month dow year) or 7-field expressions when combined with Second.

package main

import (
	"fmt"
	"time"

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

func main() {
	// Create parser with Year field support (6 fields total)
	parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Year)

	// Schedule for January 1, 2025 at midnight
	schedule, err := parser.Parse("0 0 1 1 * 2025")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	// Find next execution from December 2024
	from := time.Date(2024, 12, 15, 0, 0, 0, 0, time.UTC)
	next := schedule.Next(from)
	fmt.Printf("Next execution: %s\n", next.Format("2006-01-02 15:04:05"))
}
Output:

Next execution: 2025-01-01 00:00:00
Example (YearRange)

This example demonstrates using year ranges to limit schedule execution to specific years. This is useful for temporary schedules or migrations.

package main

import (
	"fmt"
	"time"

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

func main() {
	// Create parser with Year field support
	parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Year)

	// Schedule for January 1 at midnight, only in 2024-2026
	schedule, err := parser.Parse("0 0 1 1 * 2024-2026")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	// Check from 2023 - should skip to 2024
	from := time.Date(2023, 6, 15, 0, 0, 0, 0, time.UTC)
	next := schedule.Next(from)
	fmt.Printf("First in range: %d\n", next.Year())

	// Check after 2026 - should return zero time
	from2 := time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC)
	next2 := schedule.Next(from2)
	fmt.Printf("After range is zero: %v\n", next2.IsZero())
}
Output:

First in range: 2024
After range is zero: true

func StandardParser added in v0.6.0

func StandardParser() Parser

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

Example:

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

func TryNewParser added in v0.6.0

func TryNewParser(options ParseOption) (Parser, error)

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

Use TryNewParser when:

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

Use NewParser when:

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

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

Example:

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

func (Parser) Parse

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) ParseWithHashKey added in v0.7.0

func (p Parser) ParseWithHashKey(spec, hashKey string) (Schedule, error)

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

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) WithHashKey added in v0.7.0

func (p Parser) WithHashKey(key string) Parser

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

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)

func (Parser) WithSecondOptional added in v0.7.0

func (p Parser) WithSecondOptional() Parser

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.

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

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

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

type ScheduleWithPrev added in v0.7.0

type ScheduleWithPrev interface {
	Schedule
	// Prev returns the previous activation time, earlier than the given time.
	// Returns zero time if no previous time can be found.
	Prev(time.Time) time.Time
}

ScheduleWithPrev is an optional interface that schedules can implement to support backward time traversal. This is useful for detecting missed executions or determining the last scheduled run time.

Built-in schedules (SpecSchedule, ConstantDelaySchedule) implement this interface. Custom Schedule implementations may optionally implement it.

Use type assertion to check for support:

if sp, ok := schedule.(ScheduleWithPrev); ok {
    prev := sp.Prev(time.Now())
}

type SlogLogger added in v0.6.0

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

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

func NewSlogLogger added in v0.6.0

func NewSlogLogger(l *slog.Logger) *SlogLogger

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

func (*SlogLogger) Error added in v0.6.0

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

Error logs an error condition using slog.

func (*SlogLogger) Info added in v0.6.0

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

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

type SpecAnalysis added in v0.7.0

type SpecAnalysis struct {
	// Valid indicates whether the spec was successfully parsed.
	Valid bool

	// Error contains the parsing error if Valid is false.
	Error error

	// NextRun is the next scheduled execution time from now.
	// Zero if the spec is invalid or represents a one-time past event.
	NextRun time.Time

	// Location is the timezone for the schedule.
	// Defaults to time.Local unless TZ= or CRON_TZ= is specified.
	Location *time.Location

	// Fields contains the original field values from the spec.
	// Keys: "second" (if applicable), "minute", "hour", "day_of_month", "month", "day_of_week"
	// For descriptors, this will be empty.
	Fields map[string]string

	// IsDescriptor indicates if the spec uses a descriptor (@hourly, @every, etc.)
	IsDescriptor bool

	// Interval is the duration for @every expressions.
	// Zero for non-@every specs.
	Interval time.Duration

	// Schedule is the parsed schedule, available for further introspection.
	// Nil if the spec is invalid.
	Schedule Schedule
}

SpecAnalysis contains detailed information about a parsed cron specification. It provides insight into the schedule without requiring job registration.

func AnalyzeSpec added in v0.7.0

func AnalyzeSpec(spec string, options ...ParseOption) SpecAnalysis

AnalyzeSpec provides detailed analysis of a cron expression. It returns a SpecAnalysis struct containing validation status, next run time, parsed fields, and other metadata.

This is useful for:

  • Configuration validation with detailed feedback
  • UI previews showing when a job will run
  • Debugging cron expressions
  • Import/migration validation

Example:

result := cron.AnalyzeSpec("0 9 * * MON-FRI")
if !result.Valid {
    log.Printf("Invalid: %v", result.Error)
} else {
    log.Printf("Next run: %v", result.NextRun)
    log.Printf("Fields: %v", result.Fields)
}

func AnalyzeSpecWithHash added in v0.7.0

func AnalyzeSpecWithHash(spec string, options ParseOption, hashSeed string) SpecAnalysis

AnalyzeSpecWithHash analyzes a cron expression containing H hash expressions. This is like AnalyzeSpec but takes a hash seed for resolving H expressions. The seed should be a unique identifier (like a job name) that produces deterministic, distributed scheduling times.

Example:

result := AnalyzeSpecWithHash("H H * * *", Minute|Hour|Dom|Month|Dow|Hash, "my-job")
if result.Valid {
    log.Printf("Next run: %v", result.NextRun)
}

type SpecSchedule

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

	// Year stores valid years using sparse storage for unlimited range.
	// nil means any year (wildcard). An empty map means no valid years.
	// Uses map[int]struct{} for O(1) lookup with minimal memory overhead.
	Year map[int]struct{}

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

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

	// DomConstraints holds dynamic day-of-month constraints that cannot be
	// pre-computed into bitmasks (L, L-n, LW, nW). These are evaluated at
	// match time because they depend on the specific month.
	DomConstraints []DomConstraint

	// DowConstraints holds nth-weekday-of-month constraints (e.g., FRI#3, MON#L).
	// These are evaluated at match time because they depend on which dates
	// fall on which weekdays in the specific month.
	DowConstraints []DowConstraint
}

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

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.

func (*SpecSchedule) Prev added in v0.7.0

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

Prev returns the previous time this schedule was activated, earlier 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.

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

Jump to

Keyboard shortcuts

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