cron

package module
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: May 8, 2026 License: MIT Imports: 16 Imported by: 0

README

cron

Doc Go Release Test Report Card Stars License

A modern, focused Go cron scheduler with no third-party dependencies.

Install

go get github.com/libtnb/cron

Quick start

package main

import (
	"context"
	"fmt"
	"log/slog"
	"os/signal"
	"syscall"
	"time"

	"github.com/libtnb/cron"
	"github.com/libtnb/cron/wrap"
)

func main() {
	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer stop()

	c := cron.New(
		cron.WithLogger(slog.Default()),
		cron.WithChain(wrap.Recover(), wrap.Timeout(30*time.Second)),
	)
	_, _ = c.Add("@every 5s", cron.JobFunc(func(ctx context.Context) error {
		fmt.Println("tick", time.Now())
		return nil
	}), cron.WithName("heartbeat"))

	if err := c.Start(); err != nil {
		panic(err)
	}
	<-ctx.Done()

	shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	_ = c.Stop(shutdownCtx)
}

Packages

Path Purpose
github.com/libtnb/cron Scheduler, parser, schedules, hooks, recorders, retry policy.
github.com/libtnb/cron/wrap Job wrappers: Recover, Timeout, SkipIfRunning, DelayIfRunning, Retry.
github.com/libtnb/cron/workflow DAG jobs with OnSuccess, OnFailure, OnSkipped, OnComplete.
github.com/libtnb/cron/parserext Quartz tokens (L, N#M, NL).

Specs

Five fields:

minute hour day-of-month month day-of-week

Plus descriptors @hourly, @daily, @every 10s, and the TZ= / CRON_TZ= prefixes.

WithSeconds() adds an optional leading seconds field. Both 5- and 6-field specs parse; a 5-field spec means second=0. Pass WithSeconds(true) to require six.

Missed fires

If a firing runs late by more than WithMissedTolerance (default 1s):

  • MissedSkip (default) drops it and waits for the next scheduled time.
  • MissedRunOnce runs the job once at the most recent missed time, then resumes.
c := cron.New(
	cron.WithLocation(time.UTC),
	cron.WithParser(cron.NewStandardParser(cron.WithSeconds())),
	cron.WithMissedFire(cron.MissedRunOnce),
	cron.WithMaxConcurrent(32),
	cron.WithRetry(cron.Retry(3, cron.RetryInitial(time.Second))),
)

id, err := c.Add(
	"0 0 9 * * *",
	emailJob,
	cron.WithName("daily-digest"),
	cron.WithTimeout(time.Minute),
)

For a programmatic schedule:

id, err := c.AddSchedule(cron.ConstantDelay(time.Hour), job)

Triggering and removal

if err := c.Trigger(id); err != nil {
	switch {
	case errors.Is(err, cron.ErrEntryNotFound):
	case errors.Is(err, cron.ErrSchedulerNotRunning):
	case errors.Is(err, cron.ErrConcurrencyLimit):
	}
}

count, err := c.TriggerByName("daily-digest") // err joins per-entry failures

c.Remove(id) // false if id is unknown

_ = c.Stop(shutdownCtx)

Remove blocks future fires and future Trigger calls. Jobs already dispatched keep running. Stop halts the loop and waits for in-flight jobs and the hook dispatcher, capped by the context.

Reading entries

Entry and Entries return copies and never block.

if entry, ok := c.Entry(id); ok {
	fmt.Println(entry.Name, entry.Next)
}

for e := range c.Entries() {
	fmt.Println(e.Name, e.Prev, e.Next)
}

Schedule helpers

These work on any Schedule without a running scheduler:

next := cron.NextN(schedule, time.Now(), 10)
window := cron.Between(schedule, start, end)

Hooks and recorders

Hook and recorder interfaces are split per event. A subscriber implements only the methods it cares about:

type metrics struct{}

func (*metrics) OnJobComplete(e cron.EventJobComplete) {
	// record duration, error, etc.
}

c := cron.New(cron.WithHooks(&metrics{}))

The four hook interfaces are ScheduleHook, JobStartHook, JobCompleteHook, MissedHook. Recorder interfaces follow the same pattern (JobScheduledRecorder, JobStartedRecorder, ...).

Workflow

workflow.Workflow is a cron.Job, so a DAG schedules like anything else. workflow.New returns an error (ErrDuplicateStep / ErrUnknownDep / ErrCycle); workflow.MustNew panics on misconfiguration.

w := workflow.MustNew(
	workflow.NewStep("download", downloadJob),
	workflow.NewStep("transform", transformJob,
		workflow.After("download", workflow.OnSuccess)),
	workflow.NewStep("notify_failure", notifyJob,
		workflow.After("transform", workflow.OnFailure)),
)
_, _ = c.Add("@hourly", w, cron.WithName("etl"))

Quartz tokens

parserext.NewQuartzParser accepts standard specs plus L, N#M, and NL.

c := cron.New(cron.WithParser(parserext.NewQuartzParser(time.UTC)))

_, _ = c.Add("0 0 18 L * ?", reportJob)    // last day of every month
_, _ = c.Add("0 0 9 ? * 5#3", standupJob)  // third Friday
_, _ = c.Add("0 30 22 ? * 5L", payrollJob) // last Friday

Migrating from robfig/cron

robfig/cron libtnb/cron
cron.New(cron.WithSeconds()) cron.New(cron.WithParser(cron.NewStandardParser(cron.WithSeconds())))
Job.Run() Job.Run(context.Context) error
c.AddFunc(spec, func()) c.Add(spec, cron.JobFunc(func(ctx) error { ... }))
cron.WithLogger(custom) cron.WithLogger(*slog.Logger)
cron.Recover(logger) wrap.Recover(wrap.WithLogger(logger))
cron.SkipIfStillRunning(logger) wrap.SkipIfRunning()
cron.DelayIfStillRunning(logger) wrap.DelayIfRunning()
c.Start() c.Start() error
c.Stop() c.Stop(ctx) error
c.Entries() c.Entries() as iter.Seq[Entry]

Credits

Documentation

Overview

Package cron is a cron scheduler.

Register jobs with Add or AddSchedule, then call Start. Stop cancels the loop and waits for in-flight jobs, capped by its context.

Specs use five fields. WithSeconds adds an optional leading seconds field; WithSeconds(true) requires six.

Subpackages

  • wrap: job wrappers (Recover, Timeout, SkipIfRunning, DelayIfRunning, Retry).
  • workflow: DAG jobs with conditional dependencies.
  • parserext: Quartz tokens (L, N#M, NL).

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrCapacityReached     = errors.New("cron: capacity reached")    // Add: WithMaxEntries exceeded
	ErrAlreadyRunning      = errors.New("cron: job already running") // wrap.SkipIfRunning
	ErrJobTimeout          = errors.New("cron: job timeout")         // ctx cause from WithTimeout
	ErrCronStopping        = errors.New("cron: scheduler stopping")  // ctx cause from Stop
	ErrEntryNotFound       = errors.New("cron: entry not found")     // Trigger
	ErrSchedulerNotRunning = errors.New("cron: scheduler not running")
	ErrConcurrencyLimit    = errors.New("cron: max concurrent reached")
	ErrSchedulerStopped    = errors.New("cron: scheduler stopped") // Start: Stop already ran
)

Functions

func Between

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

Between returns every firing in (start, end].

func IsTriggered

func IsTriggered(s Schedule) bool

IsTriggered reports whether s came from TriggeredSchedule.

func NextN

func NextN(s Schedule, from time.Time, n int) []time.Time

NextN returns the next n firings strictly after from.

func ValidateSpec

func ValidateSpec(spec string) error

ValidateSpec returns nil iff spec parses with the standard parser.

func ValidateSpecWith

func ValidateSpecWith(spec string, p Parser) error

ValidateSpecWith is ValidateSpec with a custom parser.

Types

type ConstantDelay

type ConstantDelay time.Duration

ConstantDelay is a fixed-interval Schedule.

func (ConstantDelay) Next

func (d ConstantDelay) Next(now time.Time) time.Time

func (ConstantDelay) String

func (d ConstantDelay) String() string

type Cron

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

Cron is a job scheduler. Construct one with New, register jobs, then Start.

func New

func New(opts ...Option) *Cron

New constructs a Cron. It does not start scheduling until Start is called.

func (*Cron) Add

func (c *Cron) Add(spec string, j Job, opts ...EntryOption) (EntryID, error)

Add parses spec and registers j. It returns a *ParseError for invalid specs or ErrCapacityReached when WithMaxEntries rejects the registration.

func (*Cron) AddSchedule

func (c *Cron) AddSchedule(s Schedule, j Job, opts ...EntryOption) (EntryID, error)

AddSchedule registers j against a programmatic Schedule.

func (*Cron) Entries

func (c *Cron) Entries() iter.Seq[Entry]

Entries returns registered entry snapshots ordered by Next.

func (*Cron) Entry

func (c *Cron) Entry(id EntryID) (Entry, bool)

Entry returns the current snapshot for id.

func (*Cron) Remove

func (c *Cron) Remove(id EntryID) bool

Remove deregisters id. In-flight invocations continue; future automatic fires and future Trigger calls for id are rejected.

func (*Cron) Running

func (c *Cron) Running() bool

Running reports whether the scheduler is running. It is observational; use Trigger's returned error for race-free dispatch decisions.

func (*Cron) Start

func (c *Cron) Start() error

Start launches the scheduler. It is idempotent while running and returns ErrSchedulerStopped after Stop has been called.

func (*Cron) Stop

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

Stop halts the scheduler and waits for the loop, in-flight jobs and hook dispatcher to drain, capped by ctx. Returns ctx.Err() on timeout. Do not call it from inside a Job.

func (*Cron) Trigger

func (c *Cron) Trigger(id EntryID) error

Trigger fires id immediately. It returns ErrSchedulerNotRunning, ErrEntryNotFound, or ErrConcurrencyLimit when dispatch is rejected.

func (*Cron) TriggerByName

func (c *Cron) TriggerByName(name string) (int, error)

TriggerByName fires every entry whose Name matches name. Returns the successful dispatch count and errors.Join of per-Trigger failures. No match returns (0, nil); not running returns (0, ErrSchedulerNotRunning).

type Entry

type Entry struct {
	ID       EntryID
	Name     string
	Spec     string // empty for AddSchedule entries
	Schedule Schedule
	Prev     time.Time // zero if never fired
	Next     time.Time // zero if exhausted or TriggeredSchedule
}

Entry is the public read-only view of a scheduled item. Safe to copy.

func (Entry) LogValue

func (e Entry) LogValue() slog.Value

func (Entry) Valid

func (e Entry) Valid() bool

Valid reports whether e refers to a registered entry. Zero is invalid.

type EntryID

type EntryID uint64

EntryID is an opaque, process-local identifier.

func (EntryID) LogValue

func (id EntryID) LogValue() slog.Value

func (EntryID) String

func (id EntryID) String() string

type EntryOption

type EntryOption func(*entryConfig)

EntryOption configures one entry.

func WithEntryChain

func WithEntryChain(wrappers ...Wrapper) EntryOption

WithEntryChain installs per-entry wrappers inside the global chain.

func WithEntryRetry

func WithEntryRetry(p RetryPolicy) EntryOption

WithEntryRetry overrides the global retry for one entry. A zero policy disables retry for that entry.

func WithName

func WithName(name string) EntryOption

WithName labels an entry.

func WithTimeout

func WithTimeout(d time.Duration) EntryOption

WithTimeout caps a Job's runtime with ErrJobTimeout as the cancel cause.

type EventJobComplete

type EventJobComplete struct {
	EntryID     EntryID
	Name        string
	ScheduledAt time.Time
	FireAt      time.Time
	Duration    time.Duration
	Err         error
}

EventJobComplete is emitted after the chain returns. Err is the chain result.

type EventJobStart

type EventJobStart struct {
	EntryID     EntryID
	Name        string
	ScheduledAt time.Time
	FireAt      time.Time
}

EventJobStart is emitted just before the chain runs. ScheduledAt is the schedule-selected time; FireAt is the actual start time after jitter/queueing.

type EventMissed

type EventMissed struct {
	EntryID     EntryID
	Name        string
	ScheduledAt time.Time
	Lateness    time.Duration
	Policy      MissedFirePolicy
}

EventMissed is emitted for missed fires and MaxConcurrent rejections.

type EventSchedule

type EventSchedule struct {
	EntryID  EntryID
	Name     string
	Schedule Schedule
	Next     time.Time
}

EventSchedule is emitted when an entry is added or its next firing is recomputed after a fire.

type HookDroppedRecorder

type HookDroppedRecorder interface{ HookDropped() }

type Job

type Job interface {
	Run(ctx context.Context) error
}

Job is the unit of work executed by the scheduler.

type JobCompleteHook

type JobCompleteHook interface{ OnJobComplete(EventJobComplete) }

type JobCompletedRecorder

type JobCompletedRecorder interface {
	JobCompleted(name string, dur time.Duration, err error)
}

type JobFunc

type JobFunc func(ctx context.Context) error

JobFunc adapts a function to Job.

Example
package main

import (
	"context"
	"fmt"

	"github.com/libtnb/cron"
)

func main() {
	j := cron.JobFunc(func(ctx context.Context) error {
		fmt.Println("hello")
		return nil
	})
	_ = j.Run(context.Background())
}
Output:
hello

func (JobFunc) Run

func (f JobFunc) Run(ctx context.Context) error

type JobMissedRecorder

type JobMissedRecorder interface {
	JobMissed(name string, lateness time.Duration)
}

type JobScheduledRecorder

type JobScheduledRecorder interface{ JobScheduled(name string) }

Recorder sub-interfaces. WithRecorder subscribers may implement any subset.

type JobStartHook

type JobStartHook interface{ OnJobStart(EventJobStart) }

type JobStartedRecorder

type JobStartedRecorder interface{ JobStarted(name string) }

type MissedFirePolicy

type MissedFirePolicy uint8

MissedFirePolicy controls behaviour when a fire is later than WithMissedTolerance. OnMissedFire fires regardless of policy.

const (
	// MissedSkip ignores missed firings and resumes from the next
	// scheduled time. This is the default.
	MissedSkip MissedFirePolicy = iota

	// MissedRunOnce runs the job once for the most recent missed firing
	// (latest schedule.Next <= now), then resumes normally.
	MissedRunOnce
)

func (MissedFirePolicy) String

func (p MissedFirePolicy) String() string

type MissedHook

type MissedHook interface{ OnMissedFire(EventMissed) }

type Option

type Option func(*config)

Option configures a Cron.

func WithChain

func WithChain(wrappers ...Wrapper) Option

WithChain installs global wrappers. First wrapper is outermost.

func WithHookBuffer

func WithHookBuffer(n int) Option

WithHookBuffer sets the hook event buffer size. Full buffers drop new events.

func WithHooks

func WithHooks(hooks ...any) Option

WithHooks installs async hook subscribers. Values may implement any subset of ScheduleHook, JobStartHook, JobCompleteHook, and MissedHook.

func WithJitter

func WithJitter(max time.Duration) Option

WithJitter adds a random delay in [0, max) to each firing.

func WithLocation

func WithLocation(loc *time.Location) Option

WithLocation sets the default schedule timezone. Default is time.Local.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger sets the slog.Logger. Default slog.Default().

func WithMaxConcurrent

func WithMaxConcurrent(n int) Option

WithMaxConcurrent caps in-flight jobs. Zero means unlimited.

func WithMaxEntries

func WithMaxEntries(n int) Option

WithMaxEntries caps registered entries. Zero means unlimited.

func WithMissedFire

func WithMissedFire(p MissedFirePolicy) Option

WithMissedFire selects the missed-fire policy. Default MissedSkip.

func WithMissedTolerance

func WithMissedTolerance(d time.Duration) Option

WithMissedTolerance sets the lateness threshold for "missed". Default 1s.

func WithParser

func WithParser(p Parser) Option

WithParser installs a parser.

func WithRecorder

func WithRecorder(r any) Option

WithRecorder installs a metrics subscriber. Values may implement any subset of the recorder sub-interfaces.

func WithRetry

func WithRetry(p RetryPolicy) Option

WithRetry sets the default RetryPolicy. Overridden by WithEntryRetry.

type ParseError

type ParseError struct {
	Spec   string
	Field  string // e.g. "minute"; "" if not applicable
	Pos    int    // 0-based byte offset; -1 if unknown
	Reason string
	Err    error
}

ParseError describes a failure parsing a cron specification.

func (*ParseError) Error

func (e *ParseError) Error() string

func (*ParseError) Unwrap

func (e *ParseError) Unwrap() error

type Parser

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

Parser turns a textual spec into a Schedule. Cron caches parser results.

type ParserOption

type ParserOption func(*parserConfig)

ParserOption configures NewStandardParser.

func WithDefaultLocation

func WithDefaultLocation(loc *time.Location) ParserOption

WithDefaultLocation sets the default timezone for specs without TZ=/CRON_TZ=. nil means time.Local.

func WithParserExt

func WithParserExt(ext Parser) ParserOption

WithParserExt installs a pre-parse hook. Returning (nil, nil) falls through to the standard parser.

func WithSeconds

func WithSeconds(strict ...bool) ParserOption

WithSeconds enables a leading seconds field. By default the parser accepts both 5- and 6-field specs (a 5-field spec is parsed with second=0). Pass true to require exactly 6 fields.

type QueueDepthRecorder

type QueueDepthRecorder interface{ QueueDepth(n int) }

type RetryOption

type RetryOption func(*RetryPolicy)

RetryOption configures a RetryPolicy built by Retry.

func RetryInitial

func RetryInitial(d time.Duration) RetryOption

RetryInitial sets the first retry delay (default 1s).

func RetryJitterFrac

func RetryJitterFrac(f float64) RetryOption

RetryJitterFrac is fractional uniform jitter (e.g. 0.1 = ±10%).

func RetryMaxDelay

func RetryMaxDelay(d time.Duration) RetryOption

RetryMaxDelay caps backoff (zero = uncapped).

func RetryMultiplier

func RetryMultiplier(m float64) RetryOption

RetryMultiplier is the per-attempt growth factor (<=1 stays constant).

type RetryPolicy

type RetryPolicy struct {
	MaxRetries int
	Initial    time.Duration
	MaxDelay   time.Duration
	Multiplier float64
	JitterFrac float64
}

RetryPolicy describes exponential backoff with optional jitter. MaxRetries == 0 disables retry; negative means unlimited until ctx cancellation. Fields are exported for config-driven assembly; use Retry(...) for programmatic construction.

func Retry

func Retry(maxRetries int, opts ...RetryOption) RetryPolicy

Retry builds a RetryPolicy. maxRetries is the number of retries after the initial attempt; negative retries until ctx cancellation.

func (RetryPolicy) IsZero

func (p RetryPolicy) IsZero() bool

IsZero is keyed only on MaxRetries so half-filled policies (e.g. only Initial set) don't produce a useless wrapper.

func (RetryPolicy) Wrapper

func (p RetryPolicy) Wrapper() Wrapper

Wrapper returns a Wrapper that retries on error per p. Attempt errors are joined via errors.Join; ctx cancellation aborts.

type Schedule

type Schedule interface {
	Next(now time.Time) time.Time
}

Schedule yields successive firing times. Next must return the first firing strictly after now, or zero when exhausted.

func TriggeredSchedule

func TriggeredSchedule() Schedule

TriggeredSchedule never fires automatically. Combine with Trigger.

type ScheduleHook

type ScheduleHook interface{ OnSchedule(EventSchedule) }

Hook sub-interfaces. WithHooks subscribers may implement any subset.

type SpecAnalysis

type SpecAnalysis struct {
	Spec        string
	Valid       bool
	Err         error
	IsTriggered bool
	Descriptor  string         // "@every", "@hourly", ... or "" for 5/6-field specs
	Interval    time.Duration  // set when Descriptor == "@every"
	Location    *time.Location // schedule timezone
	NextRun     time.Time      // upcoming firing relative to the now passed in
}

SpecAnalysis is the result of AnalyzeSpec. Most fields are populated only when Valid is true.

func AnalyzeSpec

func AnalyzeSpec(spec string, now time.Time) SpecAnalysis

AnalyzeSpec parses spec and returns a structured description.

func AnalyzeSpecWith

func AnalyzeSpecWith(spec string, p Parser, now time.Time) SpecAnalysis

AnalyzeSpecWith is AnalyzeSpec with a custom parser.

type SpecSchedule

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

SpecSchedule is a parsed cron expression.

func (*SpecSchedule) Location

func (s *SpecSchedule) Location() *time.Location

Location returns the evaluation timezone.

func (*SpecSchedule) LogValue

func (s *SpecSchedule) LogValue() slog.Value

func (*SpecSchedule) Next

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

Next returns the next firing after t, or zero if none is found.

func (*SpecSchedule) Upcoming

func (s *SpecSchedule) Upcoming(from time.Time) iter.Seq[time.Time]

Upcoming is a lazy iterator over future firings.

type StandardParser

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

StandardParser is stateless and concurrent-safe.

func NewStandardParser

func NewStandardParser(opts ...ParserOption) *StandardParser

NewStandardParser handles 5/6-field specs, descriptors, and TZ prefixes.

func (*StandardParser) Parse

func (p *StandardParser) Parse(spec string) (Schedule, error)

type Upcoming

type Upcoming interface {
	Upcoming(from time.Time) iter.Seq[time.Time]
}

Upcoming is an optional lazy iteration capability.

type Wrapper

type Wrapper func(Job) Job

Wrapper decorates a Job.

func Chain

func Chain(wrappers ...Wrapper) Wrapper

Chain composes wrappers so the first wraps outermost.

Example
package main

import (
	"context"
	"fmt"

	"github.com/libtnb/cron"
)

func main() {
	mk := func(name string) cron.Wrapper {
		return func(j cron.Job) cron.Job {
			return cron.JobFunc(func(ctx context.Context) error {
				fmt.Println("enter", name)
				err := j.Run(ctx)
				fmt.Println("leave", name)
				return err
			})
		}
	}
	core := cron.JobFunc(func(ctx context.Context) error {
		fmt.Println("run core")
		return nil
	})
	_ = cron.Chain(mk("outer"), mk("inner"))(core).Run(context.Background())
}
Output:
enter outer
enter inner
run core
leave inner
leave outer

Directories

Path Synopsis
examples
hello command
quartz command
seconds command
slog command
workflow command
internal
heap
Package heap provides a typed min-heap with addressable items.
Package heap provides a typed min-heap with addressable items.
parsecache
Package parsecache memoises parser results and never evicts.
Package parsecache memoises parser results and never evicts.
Package parserext provides optional cron parser extensions.
Package parserext provides optional cron parser extensions.
Package workflow runs DAGs of cron.Jobs.
Package workflow runs DAGs of cron.Jobs.
Package wrap supplies Job decorators.
Package wrap supplies Job decorators.

Jump to

Keyboard shortcuts

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