cron

package module
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Sep 2, 2026 License: MIT Imports: 17 Imported by: 0

README ΒΆ

cron

Go Version License Build Status Go Report Card Go Reference

A context-aware job scheduler for Go with explicit lifecycle control, missed-fire policies, typed workflows and optional distributed coordination. Requires Go 1.27.

c := cron.MustNew(cron.WithLocation(time.UTC))

_, err := c.Add("0 9 * * MON-FRI", cron.JobFunc(func(ctx context.Context) error {
	return sendDigest(ctx)
}), cron.WithName("digest"))
if err != nil {
	log.Fatal(err)
}

if err := c.Start(); err != nil {
	log.Fatal(err)
}
defer func() {
	if err := c.Stop(context.Background()); err != nil {
		log.Printf("stop cron: %v", err)
	}
}()

πŸš€ Getting Started

go get github.com/libtnb/cron
package main

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

	"github.com/libtnb/cron"
)

func main() {
	c := cron.MustNew(cron.WithLocation(time.UTC))

	_, err := c.Add("@every 5s", cron.JobFunc(func(ctx context.Context) error {
		fmt.Println("tick", time.Now().Format(time.RFC3339))
		return nil
	}), cron.WithName("heartbeat"))
	if err != nil {
		log.Fatal(err)
	}
	if err := c.Start(); err != nil {
		log.Fatal(err)
	}

	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer stop()
	<-ctx.Done()

	shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	if err := c.Stop(shutdownCtx); err != nil {
		log.Printf("stop: %v", err)
	}
}

Runnable programs for seconds fields, Quartz tokens, structured logging and workflows live in _examples; executable Example functions are on pkg.go.dev.

✨ Features

Scheduler lifecycle
  • New / MustNew build a scheduler from options; nothing fires until Start.
  • Stop(ctx) cancels in-flight jobs (context cause ErrCronStopping) and waits for the loop, the jobs and the observer queue; Drain(ctx) stops scheduling and lets running jobs finish. Both are bounded by ctx and return ctx.Err() on timeout.
  • A stopped scheduler cannot be restarted: Start returns ErrSchedulerStopped.
  • WithBaseContext ties the run context to a parent; WithMaxConcurrent caps in-flight jobs (excess automatic fires are rejected, not queued); WithMaxEntries caps registrations.
  • The loop goroutine never runs user code: Schedule.Next, coordination calls and jobs run on their own goroutines, so a slow schedule or job delays only its own entry.
Entries and options

Add (cron spec) and AddSchedule (programmatic Schedule) return a stable EntryID. Entries can be inspected (Entry, Entries), paused, resumed, updated (Update, UpdateSchedule), removed and triggered (Trigger, TriggerAndWait, TriggerByName). Remove, Pause and Resume return ErrEntryNotFound for unknown IDs.

id, err := c.Add("*/15 * * * *", job,
	cron.WithName("sync"),
	cron.WithTimeout(time.Minute),             // ErrJobTimeout as the cancel cause
	cron.WithEntryRetry(cron.Retry(3)),        // exponential backoff, 1s initial delay
	cron.WithEntryChain(wrap.SkipIfRunning()), // drop overlapping runs
)

Scheduler-wide defaults (WithChain, WithRetry, WithJitter, WithMissedFire, WithClaimer) can be overridden per entry (WithEntryChain, WithEntryRetry, WithEntryJitter, WithEntryMissedFire, WithEntryClaimer). Jobs read their identity with EntryInfoFromContext(ctx). Panics are recovered into ErrJobPanic unless WithoutRecover is set.

Schedules and parser

The built-in parser accepts five fields (minute hour dom month dow), names (JAN, MON-FRI), ranges and steps (1-5, */10), descriptors (@hourly, @daily, @weekly, @monthly, @yearly, @every 90s) and a TZ= / CRON_TZ= prefix. WithSecondsField adds an optional leading seconds field. NewStandardParser exposes the same grammar standalone with WithOptionalSeconds, WithRequiredSeconds, WithDefaultLocation and WithParserExt. Day-of-month and day-of-week follow the classic rule: when either is *, both must match; otherwise either may.

Programmatic schedules: ConstantDelay (process-anchored interval), AlignedDelay (epoch-aligned, identical across replicas), OnceAt, TriggeredSchedule (manual only), Union and Filter. Any type implementing Schedule works; Next must return a time strictly after its argument and be safe for concurrent use.

Quartz day tokens (L, L-n, LW, nW, N#M, NL) live in parserext; numeric day-of-week stays cron-style 0-6, Sunday first:

c := cron.MustNew(cron.WithParser(parserext.NewQuartzParser(time.UTC)))
_, err := c.Add("0 0 22 ? * 5L", job) // 22:00 on the last Friday of each month

Introspection: ValidateSpec, AnalyzeSpec (descriptor, interval, location, next run), NextN and Between.

s, _ := cron.NewStandardParser(cron.WithDefaultLocation(time.UTC)).Parse("0 9 * * MON-FRI")
for _, t := range cron.NextN(s, time.Now(), 3) {
	fmt.Println(t)
}
Missed fires

A fire that runs later than WithMissedTolerance (default: one minute) invokes the entry's missed-fire policy and publishes a MissedFireEvent:

Policy Behaviour
MissedRunOnce (default) runs the most recent missed instant once
MissedRunAll replays every missed instant, newest 1000 at most
MissedSkip drops the backlog and resumes from the next instant

Catch-up bisects over Schedule.Next: MissedRunOnce costs about 60 Next calls however long the outage, and MissedRunAll walks only the newest window. Seed WithLastRun from persisted state to catch up across restarts:

_, err := c.Add("0 * * * *", job,
	cron.WithLastRun(lastRunFromDB),
	cron.WithEntryMissedFire(cron.MissedRunAll),
)
Distributed coordination
c := cron.MustNew(cron.WithClaimer(claimer), cron.WithElector(elector))
_, err := c.Add("0 * * * *", job, cron.WithKey("hourly-report"))
  • Claimer.Claim elects one replica per keyed fire. The key combines WithKey with the UTC scheduled instant, so replicas in different time zones agree.
  • Elector.IsLeader restricts automatic fires to the current leader.
  • false, nil is the normal contention or follower state; backend errors fail closed. Every suppressed fire publishes a SkippedFireEvent with a SkipReason.
  • WithKey is the stable cross-replica identity and is required when a claimer applies; WithName is only a display label. Manual triggers bypass coordination.
  • ConstantDelay is process-local; use AlignedDelay or a cron expression with a claimer.

Redis and PostgreSQL implementations are separate modules:

go get github.com/libtnb/cron/coordination/redis
go get github.com/libtnb/cron/coordination/postgres
Events, observers and recorder

Every scheduler activity publishes a typed Event: ScheduleEvent, JobStartEvent, JobCompleteEvent, MissedFireEvent, RejectedFireEvent, CanceledFireEvent, SkippedFireEvent, QueueDepthEvent (after every heap change, including each committed fire) and ObserverDropEvent.

  • WithObservers delivers events asynchronously, in order, through one bounded queue (WithObserverBuffer, default 1024). A full queue drops events instead of blocking the scheduler.
  • WithRecorder receives every event inline on the publishing goroutine; it must be concurrency-safe and fast. Panics in observers and recorders are recovered and logged.
c := cron.MustNew(cron.WithObservers(cron.ObserverFunc(func(ev cron.Event) {
	if done, ok := ev.(cron.JobCompleteEvent); ok && done.Err != nil {
		log.Printf("%s failed after %s: %v", done.Entry.Name, done.Duration, done.Err)
	}
})))
Workflows

workflow builds typed DAGs that run as a single cron.Job. Go 1.27 generic methods keep the data flow between steps type-safe:

b := workflow.New(workflow.WithMaxParallelism(8))

download := b.Step[[]byte]("download", func(ctx context.Context, _ workflow.Inputs) ([]byte, error) {
	return fetch(ctx)
})

b.Step[int]("store", func(ctx context.Context, in workflow.Inputs) (int, error) {
	data, ok := in.Get(download)
	if !ok {
		return 0, errors.New("download output unavailable")
	}
	return save(ctx, data)
}, workflow.After(download, workflow.OnSuccess))

wf, err := b.Build() // validates names, dependencies and cycles; freezes the builder

Dependencies use OnSuccess, OnFailure, OnSkipped or OnComplete; a step whose conditions are not met is skipped. Steps accept WithTimeout and WithRetry. Execute returns an Execution with per-step results, typed outputs (Get) and a joined error; at most 32 steps run concurrently by default.

Wrappers

wrap supplies cron.Wrapper decorators: Recover, Timeout, Retry, SkipIfRunning and DelayIfRunning. Install them globally with WithChain or per entry with WithEntryChain; Chain composes wrappers with the first outermost. Retry policies (Retry(n, RetryInitial(...), RetryMaxDelay(...), RetryMultiplier(...), RetryJitterFrac(...))) join every attempt's error and stop on context cancellation.

Contrib
Module Purpose
github.com/libtnb/cron/contrib/prometheus cron.Recorder exposing job counters, duration and lateness histograms, queue depth and dropped events
github.com/libtnb/cron/contrib/otel cron.Wrapper tracing each invocation as an OpenTelemetry span
Packages
Package Purpose
cron Scheduler, parser, lifecycle, events
cron/workflow Typed bounded DAG executor
cron/wrap Job wrappers
cron/parserext Quartz day tokens
cron/coordination/redis Redis claimer and elector (separate module)
cron/coordination/postgres PostgreSQL claimer and elector (separate module)
cron/contrib/prometheus Prometheus recorder (separate module)
cron/contrib/otel OpenTelemetry wrapper (separate module)

🀝 Contributing

Please read the contributing guide before submitting a PR.

πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

Documentation ΒΆ

Overview ΒΆ

Package cron schedules context-aware jobs from cron expressions or programmatic Schedule implementations.

Lifecycle ΒΆ

New builds a scheduler from options; Cron.Add and Cron.AddSchedule register jobs and return a stable EntryID; Cron.Start launches the loop. Entries can be added, removed, paused, resumed and updated before or after Start; Cron.Trigger needs a running scheduler. The loop goroutine only pops due entries: Schedule.Next, distributed coordination and jobs each run on their own goroutines, so one slow schedule or job never delays another entry.

Cron.Stop cancels in-flight jobs (context cause ErrCronStopping) and waits for them; Cron.Drain stops scheduling and lets running jobs finish. Both are bounded by the supplied context. A stopped scheduler cannot be restarted.

Schedules ΒΆ

The built-in parser accepts five-field specs (minute hour dom month dow), an optional leading seconds field (see WithSecondsField), descriptors such as "@hourly" and "@every 10s", and a "TZ=" or "CRON_TZ=" prefix. Programmatic schedules include ConstantDelay, AlignedDelay, OnceAt, TriggeredSchedule, Union and Filter. Quartz day tokens (L, W, #) live in the parserext subpackage.

Missed fires ΒΆ

A fire that runs later than WithMissedTolerance (one minute by default) invokes the entry's MissedFirePolicy: MissedRunOnce (the default) runs the most recent missed instant once, MissedRunAll replays the backlog and MissedSkip drops it. Seed WithLastRun from persisted state to catch up across restarts.

Distributed coordination ΒΆ

WithClaimer elects one replica per keyed fire and WithElector restricts automatic fires to the current leader. Fire keys combine WithKey with the UTC scheduled instant, so replicas in different time zones agree. Manual triggers bypass coordination. Redis and PostgreSQL backends are separate modules under coordination/.

Events ΒΆ

WithObservers delivers Event values asynchronously through a bounded, lossy queue; WithRecorder receives the same events inline for metrics. Prometheus and OpenTelemetry integrations live under contrib/.

Index ΒΆ

Examples ΒΆ

Constants ΒΆ

This section is empty.

Variables ΒΆ

View Source
var (
	// ErrCapacityReached is returned by Add and AddSchedule when the
	// WithMaxEntries limit is reached.
	ErrCapacityReached = errors.New("cron: capacity reached")
	// ErrAlreadyRunning is returned by wrap.SkipIfRunning when an invocation
	// overlaps one still in progress.
	ErrAlreadyRunning = errors.New("cron: job already running")
	// ErrJobTimeout is the context cancellation cause when WithTimeout or
	// wrap.Timeout expires; read it with context.Cause.
	ErrJobTimeout = errors.New("cron: job timeout")
	// ErrCronStopping is the context cancellation cause for jobs cancelled by
	// Stop; read it with context.Cause.
	ErrCronStopping = errors.New("cron: scheduler stopping")
	// ErrEntryNotFound is returned by Remove, Pause, Resume, Update,
	// UpdateSchedule, Trigger and TriggerAndWait for an unknown or removed id.
	ErrEntryNotFound = errors.New("cron: entry not found")
	// ErrSchedulerNotRunning is returned by Trigger, TriggerAndWait and
	// TriggerByName before Start or after Stop and Drain.
	ErrSchedulerNotRunning = errors.New("cron: scheduler not running")
	// ErrConcurrencyLimit is returned by Trigger when WithMaxConcurrent has no
	// free slot. Automatic fires publish RejectedFireEvent instead.
	ErrConcurrencyLimit = errors.New("cron: max concurrent reached")
	// ErrSchedulerStopped is returned by Start after Stop or Drain.
	ErrSchedulerStopped = errors.New("cron: scheduler stopped")
	// ErrNilJob is returned by Add and AddSchedule for a nil Job, including a
	// typed nil, or when the wrapper chain produced one.
	ErrNilJob = errors.New("cron: nil job")
	// ErrNilSchedule is returned when a Parser produces no Schedule or when
	// AddSchedule or UpdateSchedule receives a nil one.
	ErrNilSchedule = errors.New("cron: nil schedule")
	// ErrJobPanic wraps the recovered panic value in a job's result unless
	// WithoutRecover is set.
	ErrJobPanic = errors.New("cron: job panicked")
	// ErrClaimerRequiresKey is returned by Add and AddSchedule when a Claimer
	// applies to an entry registered without WithKey.
	ErrClaimerRequiresKey = errors.New("cron: distributed claimer requires WithKey")
	// ErrDuplicateKey is returned, wrapped with the key, when WithKey repeats
	// a key already registered in this scheduler.
	ErrDuplicateKey = errors.New("cron: duplicate entry key")
	// ErrNilContext is returned by Stop, Drain and TriggerAndWait for a nil
	// context.
	ErrNilContext = errors.New("cron: nil context")
	// ErrInvalidOption is wrapped by New, Add and AddSchedule when an Option,
	// an EntryOption, or its argument is invalid.
	ErrInvalidOption = errors.New("cron: invalid option")
)

Sentinel errors. They may be returned wrapped; match them with errors.Is.

Functions ΒΆ

func Between ΒΆ

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

Between lazily yields every firing of s in (start, end], in order. The sequence is empty when s is nil or end is not after start. Iteration stops at the first firing after end, so an unbounded schedule is safe to query.

Example ΒΆ
package main

import (
	"fmt"
	"time"

	"github.com/libtnb/cron"
)

func main() {
	s, err := cron.NewStandardParser(cron.WithDefaultLocation(time.UTC)).Parse("0 */6 * * *")
	if err != nil {
		fmt.Println(err)
		return
	}
	start := time.Date(2026, time.March, 1, 0, 0, 0, 0, time.UTC)
	for t := range cron.Between(s, start, start.Add(24*time.Hour)) {
		fmt.Println(t.Format(time.RFC3339))
	}
}
Output:
2026-03-01T06:00:00Z
2026-03-01T12:00:00Z
2026-03-01T18:00:00Z
2026-03-02T00:00:00Z

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 up to n firings of s strictly after from, in order; fewer are returned when the schedule exhausts first. It returns nil when s is nil or n is not positive. Schedules implementing Upcoming are iterated lazily; others are walked with repeated Next calls.

Example ΒΆ
package main

import (
	"fmt"
	"time"

	"github.com/libtnb/cron"
)

func main() {
	s, err := cron.NewStandardParser(cron.WithDefaultLocation(time.UTC)).Parse("0 9 * * MON-FRI")
	if err != nil {
		fmt.Println(err)
		return
	}
	from := time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC) // a Thursday
	for _, t := range cron.NextN(s, from, 3) {
		fmt.Println(t.Format("Mon 2006-01-02 15:04"))
	}
}
Output:
Thu 2026-01-01 09:00
Fri 2026-01-02 09:00
Mon 2026-01-05 09:00

func ValidateSpec ΒΆ

func ValidateSpec(spec string) error

ValidateSpec checks spec against the built-in five-field parser in time.Local; use ValidateSpecWith to match a scheduler configured with WithSecondsField or WithParser. Returns a *ParseError describing the fault, or nil.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/libtnb/cron"
)

func main() {
	fmt.Println(cron.ValidateSpec("*/15 * * * *"))
	fmt.Println(cron.ValidateSpec("61 * * * *"))
}
Output:
<nil>
cron: parse "61 * * * *": field "minute": 61 above maximum 59

func ValidateSpecWith ΒΆ

func ValidateSpecWith(spec string, p Parser) error

ValidateSpecWith checks spec with p. Returns p's error (a *ParseError for the built-in parsers), ErrNilSchedule when p returns no schedule, or an error for a nil p.

Types ΒΆ

type AlignedDelay ΒΆ added in v0.5.1

type AlignedDelay time.Duration

AlignedDelay fires at multiples of d since the Unix epoch, so replicas evaluating the same interval compute identical instants and can share a Claimer key. Alignment follows time.Truncate, which works on absolute time: a 24h AlignedDelay fires at 00:00 UTC, not local midnight. Non-positive intervals never fire.

func (AlignedDelay) Next ΒΆ added in v0.5.1

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

Next returns the next multiple of the interval strictly after now, or zero for non-positive intervals.

func (AlignedDelay) String ΒΆ added in v0.5.1

func (d AlignedDelay) String() string

String renders the interval in "@aligned" form, for example "@aligned 5m".

type CanceledFireEvent ΒΆ added in v0.5.3

type CanceledFireEvent struct {
	Entry       EntryRef
	ScheduledAt time.Time
	Cause       error
}

CanceledFireEvent reports a fire that held a concurrency slot but was cancelled before its job started: the scheduler stopped while the fire was waiting out its jitter. Cause is the run context's cancellation cause, typically ErrCronStopping.

type Claimer ΒΆ added in v0.5.3

type Claimer interface {
	// Claim reserves fireKey for this instance. It returns true when the
	// caller may run the fire, false when another instance already holds it,
	// and a non-nil error only for backend failures.
	Claim(ctx context.Context, fireKey string) (bool, error)
}

Claimer elects one scheduler instance per fire across replicas. The scheduler calls Claim on the fire goroutine, after jitter and any Elector check, with a key unique to the entry key and the UTC scheduled instant ("report@2026-01-02T15:04:05Z"), so replicas in different time zones agree.

Implementations must be safe for concurrent use and should keep a claim reserved long enough that a delayed replica cannot run the same fire later; they own their timeout and retention policy. A false, nil result is the normal "another instance won" outcome and produces SkipAlreadyClaimed; a non-nil error fails closed, skipping the fire with SkipClaimError. Manual triggers never claim.

type ConstantDelay ΒΆ

type ConstantDelay time.Duration

ConstantDelay fires every d, measured from each evaluation: the scheduler computes the next fire from the current time, so the phase is anchored to this process and drifts with job lateness. Whole-second periods snap to the second boundary; sub-second periods keep their exact length. Non-positive intervals never fire. "@every 10s" parses to ConstantDelay.

Because the phase is process-local, replicas sharing a Claimer key never agree on fire instants; use AlignedDelay or a cron expression instead.

func (ConstantDelay) Next ΒΆ

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

Next returns now plus the interval, snapped to a whole second for whole-second intervals, or zero for non-positive intervals.

func (ConstantDelay) String ΒΆ

func (d ConstantDelay) String() string

String renders the interval in "@every" form, for example "@every 1m30s".

type Cron ΒΆ

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

Cron is a job scheduler. Construct one with New, register jobs with Add or AddSchedule, then call Start. All methods are safe for concurrent use.

The loop goroutine only pops due entries. Schedule.Next, distributed coordination and jobs all run on their own goroutines, so one slow schedule or job never delays another entry.

func MustNew ΒΆ added in v0.5.3

func MustNew(opts ...Option) *Cron

MustNew is New that panics instead of returning an error, for configuration that is fixed at build time.

func New ΒΆ

func New(opts ...Option) (*Cron, error)

New constructs a Cron from opts; nothing fires until Start is called.

Defaults: time.Local, slog.Default(), the five-field standard parser, MissedRunOnce with a one-minute tolerance, no jitter, no retry, unlimited concurrency and entries, and job panics recovered into ErrJobPanic.

Returns an error wrapping ErrInvalidOption when an option is nil or rejects its argument. WithLocation is ignored, with a warning, when WithParser is also set: a custom parser owns time-zone resolution.

Example ΒΆ

ExampleNew registers a job, starts the scheduler, runs the job on demand and shuts down.

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/libtnb/cron"
)

func main() {
	c, err := cron.New(cron.WithLocation(time.UTC))
	if err != nil {
		fmt.Println("new:", err)
		return
	}
	id, err := c.Add("0 9 * * MON-FRI", cron.JobFunc(func(ctx context.Context) error {
		info, _ := cron.EntryInfoFromContext(ctx)
		fmt.Println("running", info.Name)
		return nil
	}), cron.WithName("digest"))
	if err != nil {
		fmt.Println("add:", err)
		return
	}
	if err := c.Start(); err != nil {
		fmt.Println("start:", err)
		return
	}
	// Run the entry now instead of waiting for 09:00.
	if err := c.TriggerAndWait(context.Background(), id); err != nil {
		fmt.Println("trigger:", err)
	}
	if err := c.Stop(context.Background()); err != nil {
		fmt.Println("stop:", err)
	}
}
Output:
running digest

func (*Cron) Add ΒΆ

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

Add parses spec with the configured parser and registers j. The first fire is computed from now, or from WithLastRun when set. Parsed specs are memoized, so repeated Add calls with the same expression share one Schedule.

Returns a *ParseError for an invalid spec, ErrNilSchedule if the parser returned no schedule, ErrNilJob, an error wrapping ErrInvalidOption for a rejected entry option, ErrCapacityReached when WithMaxEntries is exhausted, ErrClaimerRequiresKey when a Claimer applies without WithKey, and an error wrapping ErrDuplicateKey when the key is already registered.

func (*Cron) AddSchedule ΒΆ

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

AddSchedule is Add for a programmatic Schedule; Entry.Spec stays empty for such entries. It returns the same errors as Add except *ParseError.

func (*Cron) Drain ΒΆ added in v0.3.0

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

Drain is Stop without cancelling in-flight jobs: it stops scheduling new fires and waits for running jobs to finish naturally, capped by ctx. Returns ErrNilContext or ctx.Err() like Stop. Do not call it from inside a Job.

func (*Cron) Entries ΒΆ

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

Entries yields a snapshot of every registered entry ordered by Next, with exhausted, paused and triggered-only entries (zero Next) last. The snapshot is taken when iteration starts; changes made during iteration are not reflected.

func (*Cron) Entry ΒΆ

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

Entry returns the current snapshot for id. ok is false after Remove or for an id that was never registered. Reading a snapshot never blocks the scheduler.

func (*Cron) Pause ΒΆ added in v0.3.0

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

Pause suspends automatic fires for id, keeping the entry and its Prev. Trigger still works while paused. Pausing a paused entry is a no-op; an unknown id yields ErrEntryNotFound.

func (*Cron) Remove ΒΆ

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

Remove deregisters id. Invocations already running continue; no further automatic fires happen and Trigger rejects the id. Returns ErrEntryNotFound for an unknown or already removed id.

func (*Cron) Resume ΒΆ added in v0.3.0

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

Resume re-enables automatic fires for id, computing the next fire from now rather than from the pause instant, so nothing is replayed. Resuming an entry that is not paused is a no-op; an unknown id yields ErrEntryNotFound.

func (*Cron) Running ΒΆ

func (c *Cron) Running() bool

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

func (*Cron) Start ΒΆ

func (c *Cron) Start() error

Start launches the scheduler loop; entries registered earlier fire once it runs. Start is idempotent while running and returns ErrSchedulerStopped once Stop or Drain has been called, even before any Start: a Cron cannot be restarted.

func (*Cron) Stop ΒΆ

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

Stop halts the scheduler, cancels in-flight jobs with ErrCronStopping as the context cause, and waits for the loop, the jobs and the observer queue to drain, capped by ctx. Returns ErrNilContext for a nil ctx and ctx.Err() when the wait times out; jobs still running at that point keep their cancelled context. Calling Stop before Start marks the Cron stopped. Do not call it from inside a Job: it waits for that job.

func (*Cron) Trigger ΒΆ

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

Trigger fires id immediately on this process, bypassing jitter and distributed coordination; paused entries can be triggered. The invocation runs asynchronously and does not advance Entry.Prev. Returns ErrSchedulerNotRunning, ErrEntryNotFound, or ErrConcurrencyLimit when WithMaxConcurrent has no free slot (a RejectedFireEvent is published too).

func (*Cron) TriggerAndWait ΒΆ added in v0.3.0

func (c *Cron) TriggerAndWait(ctx context.Context, id EntryID) error

TriggerAndWait fires id like Trigger and blocks until the invocation returns, yielding the job's error (including ErrJobPanic and retry aggregates). ctx bounds only the wait: on cancellation it returns ctx.Err() while the job keeps running under the scheduler's context. Returns ErrNilContext for a nil ctx and the same dispatch errors as Trigger.

func (*Cron) TriggerByName ΒΆ

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

TriggerByName fires every entry whose Name equals name; names are not unique. It returns the number of successful dispatches and the errors.Join of the failed ones. No match yields (0, nil); a scheduler that is not running yields (0, ErrSchedulerNotRunning).

func (*Cron) Update ΒΆ added in v0.3.0

func (c *Cron) Update(id EntryID, spec string) error

Update re-parses spec and swaps id's schedule in place, keeping the job, entry options, ID and Prev. The next fire is recomputed from now; a paused entry stays paused; a fire already being planned under the old schedule is discarded. Returns a *ParseError for an invalid spec, ErrNilSchedule, or ErrEntryNotFound.

func (*Cron) UpdateSchedule ΒΆ added in v0.3.0

func (c *Cron) UpdateSchedule(id EntryID, s Schedule) error

UpdateSchedule is Update for a programmatic Schedule. Returns ErrNilSchedule or ErrEntryNotFound.

type Elector ΒΆ added in v0.3.0

type Elector interface {
	// IsLeader reports whether this instance currently leads. A non-nil error
	// means the answer is unknown, not that the instance is a follower.
	IsLeader(ctx context.Context) (bool, error)
}

Elector gates automatic fires on leadership. IsLeader is called on every fire goroutine, so it must be safe for concurrent use and cheap enough to run once per fire; lease-renewing implementations fit well. A false, nil result is the normal follower state and produces SkipNotLeader; a non-nil error fails closed with SkipElectionError. Manual triggers bypass the elector.

type Entry ΒΆ

type Entry struct {
	ID       EntryID
	Name     string // display label from WithName; not unique
	Key      string // stable identity for distributed fire claims (WithKey)
	Spec     string // source expression; empty for AddSchedule entries
	Schedule Schedule
	Prev     time.Time // last automatic fire or the WithLastRun seed; zero if never fired
	Next     time.Time // zero if exhausted, paused, or TriggeredSchedule
	Paused   bool
}

Entry is a point-in-time snapshot of a registered entry, as returned by Cron.Entry and Cron.Entries. It is a plain value: copying it or holding it across scheduler changes is safe, but it does not update.

func (Entry) LogValue ΒΆ

func (e Entry) LogValue() slog.Value

LogValue renders the snapshot as a slog group, omitting zero fields.

func (Entry) Valid ΒΆ

func (e Entry) Valid() bool

Valid reports whether e describes a registered entry. The zero Entry, which Cron.Entry returns with ok == false, is not valid.

type EntryID ΒΆ

type EntryID uint64

EntryID identifies one registration for the lifetime of a Cron. IDs are process-local, allocated sequentially from 1 and never reused, so a stale ID yields ErrEntryNotFound rather than another entry. Zero is never a valid ID; see Entry.Valid.

func (EntryID) LogValue ΒΆ

func (id EntryID) LogValue() slog.Value

LogValue renders the ID as a string attribute so log output does not depend on integer formatting.

func (EntryID) String ΒΆ

func (id EntryID) String() string

String formats the ID as a decimal number.

type EntryInfo ΒΆ added in v0.4.0

type EntryInfo struct {
	ID          EntryID
	Name        string
	Key         string
	ScheduledAt time.Time
}

EntryInfo identifies the invocation a job is serving; retrieve it with EntryInfoFromContext. ScheduledAt is the fire instant the job was dispatched for, or the wall-clock time for manual triggers.

func EntryInfoFromContext ΒΆ added in v0.4.0

func EntryInfoFromContext(ctx context.Context) (EntryInfo, bool)

EntryInfoFromContext returns the identity of the entry whose job is running under ctx. The scheduler injects it for every dispatch, including manual triggers, so wrappers and jobs can tell which entry and which fire they serve. ok is false when ctx did not come from the scheduler, for example when a Job is run directly in tests.

type EntryOption ΒΆ

type EntryOption func(*entryConfig) error

EntryOption configures one entry at Add or AddSchedule time. Options are applied in order; one that rejects its argument makes Add fail with an error wrapping ErrInvalidOption.

func WithEntryChain ΒΆ

func WithEntryChain(wrappers ...Wrapper) EntryOption

WithEntryChain installs wrappers for this entry, first outermost, inside the WithChain wrappers and outside the retry policy. A nil wrapper is rejected.

func WithEntryClaimer ΒΆ added in v0.5.3

func WithEntryClaimer(claimer Claimer) EntryOption

WithEntryClaimer overrides the scheduler's Claimer for one entry. A nil claimer disables distributed claims for the entry, so it fires on every replica; a non-nil claimer requires WithKey.

func WithEntryJitter ΒΆ added in v0.3.0

func WithEntryJitter(max time.Duration) EntryOption

WithEntryJitter overrides the scheduler's jitter for one entry. Zero disables jitter for the entry; a negative max is rejected.

func WithEntryMissedFire ΒΆ added in v0.3.0

func WithEntryMissedFire(p MissedFirePolicy) EntryOption

WithEntryMissedFire overrides the scheduler's missed-fire policy for one entry. An unknown policy value is rejected.

func WithEntryRetry ΒΆ

func WithEntryRetry(p RetryPolicy) EntryOption

WithEntryRetry overrides the scheduler's RetryPolicy for one entry. A zero policy (MaxRetries == 0) disables retry for the entry even when WithRetry is set. An invalid policy is rejected.

func WithKey ΒΆ added in v0.5.3

func WithKey(key string) EntryOption

WithKey sets the stable identity used for distributed fire claims. Keys are trimmed, must be non-empty, must be unique within the scheduler (Add returns ErrDuplicateKey otherwise) and should be the same on every replica for the same logical job. Key is independent from the display-oriented Name.

func WithLastRun ΒΆ added in v0.3.0

func WithLastRun(t time.Time) EntryOption

WithLastRun seeds the entry's schedule anchor, usually the persisted time of the last run before a restart. The first fire is computed from t instead of now, so an instant already in the past is popped immediately and, when it is later than WithMissedTolerance, handed to the missed-fire policy, which catches up work missed while the process was down. It also seeds Entry.Prev. A zero t means "anchor at now".

func WithName ΒΆ

func WithName(name string) EntryOption

WithName labels an entry for Entry.Name, events, logs and TriggerByName. Names need not be unique; use WithKey for identity.

func WithTimeout ΒΆ

func WithTimeout(d time.Duration) EntryOption

WithTimeout caps one invocation's runtime; the job context is cancelled with ErrJobTimeout as its cause. The clock starts after jitter and coordination. Zero (the default) disables the timeout; a negative d is rejected.

type EntryRef ΒΆ added in v0.5.3

type EntryRef struct {
	ID   EntryID
	Key  string // WithKey value; empty when unset
	Name string // WithName value; empty when unset
}

EntryRef identifies the entry an event concerns. It is a copy taken at publication; the entry may already have been removed when the event is observed.

type Event ΒΆ added in v0.5.3

type Event interface {
	// contains filtered or unexported methods
}

Event is one scheduler notification, delivered to Observers and the Recorder. The set of concrete types is closed to this package, so consumers can switch on them exhaustively:

switch ev := ev.(type) {
case cron.JobCompleteEvent:
	// ...
}

type Job ΒΆ

type Job interface {
	// Run performs the work. The returned error is reported through
	// JobCompleteEvent and, for TriggerAndWait, to the caller; a panic is
	// recovered into ErrJobPanic unless WithoutRecover is set.
	Run(ctx context.Context) error
}

Job is the unit of work the scheduler runs. Run receives a context that is cancelled by Stop (cause ErrCronStopping), by WithTimeout (cause ErrJobTimeout) or by the WithBaseContext parent, and that carries EntryInfo (see EntryInfoFromContext). Implementations must be safe for concurrent use: overlapping fires of one entry run concurrently unless a wrapper such as wrap.SkipIfRunning prevents it.

type JobCompleteEvent ΒΆ added in v0.5.3

type JobCompleteEvent struct {
	Entry       EntryRef
	ScheduledAt time.Time
	StartedAt   time.Time
	Duration    time.Duration
	Err         error
}

JobCompleteEvent reports the result of an invocation's wrapper chain. Err is the job's error, nil on success, including ErrJobPanic and retry aggregates.

type JobFunc ΒΆ

type JobFunc func(ctx context.Context) error

JobFunc adapts a plain 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

Run calls f.

type JobStartEvent ΒΆ added in v0.5.3

type JobStartEvent struct {
	Entry       EntryRef
	ScheduledAt time.Time
	StartedAt   time.Time
}

JobStartEvent reports an invocation about to enter its wrapper chain, after jitter, coordination and concurrency admission. ScheduledAt is the fire instant; StartedAt is the wall-clock start.

type MissedFireEvent ΒΆ added in v0.5.3

type MissedFireEvent struct {
	Entry       EntryRef
	ScheduledAt time.Time
	Lateness    time.Duration
	Policy      MissedFirePolicy
}

MissedFireEvent reports a fire that ran later than WithMissedTolerance and therefore invoked the entry's missed-fire policy. It is published once per late pop, whatever the policy, including MissedSkip.

type MissedFirePolicy ΒΆ

type MissedFirePolicy uint8

MissedFirePolicy decides what happens when an entry fires later than WithMissedTolerance allows: after a scheduler stall, a suspended process, or a restart seeded with WithLastRun. A MissedFireEvent is published whatever the policy. Set it per scheduler with WithMissedFire or per entry with WithEntryMissedFire.

const (
	// MissedRunOnce runs the job once for the most recent missed firing (the
	// latest Schedule.Next result not after now), then resumes normally. It
	// is the default: late work still happens, but a backlog never turns into
	// a burst.
	MissedRunOnce MissedFirePolicy = iota

	// MissedSkip drops the missed firings and resumes from the next scheduled
	// time.
	MissedSkip

	// MissedRunAll runs the job once per missed firing, keeping only the
	// newest 1000 when the backlog is larger. The replays are dispatched
	// oldest first as separate concurrent invocations, each subject to
	// WithMaxConcurrent.
	MissedRunAll
)

func (MissedFirePolicy) String ΒΆ

func (p MissedFirePolicy) String() string

String returns "run-once", "skip", "run-all", or "unknown".

type Observer ΒΆ added in v0.5.3

type Observer interface {
	// Observe handles one event.
	Observe(Event)
}

Observer receives scheduler events asynchronously, in publication order, on one goroutine shared by all observers of a Cron. Observe therefore need not be safe for concurrent use, but it must not block for long: while one observer is slow the shared queue (WithObserverBuffer) fills and new events are dropped. Panics are recovered and logged. Stop and Drain wait for queued events to be delivered.

type ObserverDropEvent ΒΆ added in v0.5.3

type ObserverDropEvent struct {
	Dropped int64
}

ObserverDropEvent reports that the observer queue was full and an event was dropped. Dropped is the cumulative count for this Cron. It reaches only the Recorder: observers cannot receive it because their queue is the one that overflowed.

type ObserverFunc ΒΆ added in v0.5.3

type ObserverFunc func(Event)

ObserverFunc adapts a function to Observer.

func (ObserverFunc) Observe ΒΆ added in v0.5.3

func (f ObserverFunc) Observe(event Event)

Observe calls f.

type Option ΒΆ

type Option func(*config) error

Option configures a Cron at construction. New applies options in order; an option that rejects its argument makes New fail with an error wrapping ErrInvalidOption.

func WithBaseContext ΒΆ added in v0.3.0

func WithBaseContext(ctx context.Context) Option

WithBaseContext sets the parent of the run context that jobs inherit. Cancelling it stops the loop and cancels in-flight jobs, like Stop but without waiting; still call Stop or Drain to wait for them. A nil ctx is rejected.

func WithChain ΒΆ

func WithChain(wrappers ...Wrapper) Option

WithChain installs wrappers applied to every job, first outermost; repeated calls append. Entry wrappers (WithEntryChain) and the retry policy sit inside them. A nil wrapper is rejected.

func WithClaimer ΒΆ added in v0.5.3

func WithClaimer(claimer Claimer) Option

WithClaimer sets the Claimer consulted before every automatic fire. Entries then require WithKey (Add returns ErrClaimerRequiresKey) and may opt out or switch backends with WithEntryClaimer. Trigger bypasses claims. A nil claimer is rejected.

func WithElector ΒΆ added in v0.3.0

func WithElector(e Elector) Option

WithElector gates automatic fires on leadership. A follower answer and a backend failure both skip the fire, with SkipNotLeader and SkipElectionError respectively. Trigger bypasses the elector. A nil e is rejected.

func WithJitter ΒΆ

func WithJitter(max time.Duration) Option

WithJitter delays each automatic fire by a random duration in [0, max) so a fleet of schedulers does not fire in lockstep. The delay is waited on the run context, not the job timeout, and Trigger skips it. Zero (the default) disables jitter; a negative max is rejected. Override per entry with WithEntryJitter.

func WithLocation ΒΆ

func WithLocation(loc *time.Location) Option

WithLocation sets the time zone for specs without a TZ=/CRON_TZ= prefix. The default is time.Local; a nil loc is rejected. It is ignored, with a warning from New, when WithParser is set, because a custom parser resolves time zones itself.

func WithLogger ΒΆ

func WithLogger(l *slog.Logger) Option

WithLogger sets the logger for warnings and recovered panics. The default is slog.Default(); a nil l is rejected.

func WithMaxConcurrent ΒΆ

func WithMaxConcurrent(n int) Option

WithMaxConcurrent caps in-flight jobs across all entries. Zero (the default) means unlimited; a negative n is rejected. Automatic fires above the cap publish RejectedFireEvent and are not retried; Trigger returns ErrConcurrencyLimit.

func WithMaxEntries ΒΆ

func WithMaxEntries(n int) Option

WithMaxEntries caps registered entries; Add returns ErrCapacityReached beyond it. Zero (the default) means unlimited; a negative n is rejected.

func WithMissedFire ΒΆ

func WithMissedFire(p MissedFirePolicy) Option

WithMissedFire sets the scheduler-wide missed-fire policy. The default is MissedRunOnce; an unknown policy value is rejected. Override per entry with WithEntryMissedFire.

func WithMissedTolerance ΒΆ

func WithMissedTolerance(d time.Duration) Option

WithMissedTolerance sets how late a fire may run before the missed-fire policy applies. The default is one minute, the same threshold Quartz uses, so a brief scheduling stall runs the job late instead of invoking the policy. A non-positive d is rejected.

func WithObserverBuffer ΒΆ added in v0.5.3

func WithObserverBuffer(n int) Option

WithObserverBuffer sets the observer queue capacity. Zero selects the default of 1024; a negative n is rejected. When the queue is full new events are dropped for observers and an ObserverDropEvent is recorded, while the Recorder still receives every event.

func WithObservers ΒΆ added in v0.5.3

func WithObservers(observers ...Observer) Option

WithObservers installs asynchronous event observers, notified in the order given through one bounded queue (see Observer). Repeated calls append. A nil observer is rejected.

func WithParser ΒΆ

func WithParser(p Parser) Option

WithParser replaces the built-in parser used by Add and Update, for example with parserext.NewQuartzParser. The parser then owns time-zone and seconds handling, so WithLocation and WithSecondsField no longer apply. Results are memoized per spec, so p must be deterministic and safe for concurrent use. A nil p is rejected.

func WithRecorder ΒΆ

func WithRecorder(r Recorder) Option

WithRecorder installs the inline event Recorder, typically a metrics adapter such as contrib/prometheus; see Recorder for the concurrency contract. A nil r is rejected.

func WithRetry ΒΆ

func WithRetry(p RetryPolicy) Option

WithRetry sets the default RetryPolicy, applied innermost in every job's wrapper chain. A policy with negative delays or a jitter fraction outside [0, 1] is rejected. Override per entry with WithEntryRetry.

func WithSecondsField ΒΆ added in v0.2.2

func WithSecondsField() Option

WithSecondsField makes the built-in parser accept an optional leading seconds field (six fields) alongside five-field specs, so seconds and WithLocation compose without WithParser. It has no effect when WithParser is set.

func WithoutRecover ΒΆ added in v0.3.0

func WithoutRecover() Option

WithoutRecover disables the built-in job panic recovery. By default a panicking job is recovered into an ErrJobPanic-wrapped error and logged with its stack; with this option the panic propagates and crashes the process.

type ParseError ΒΆ

type ParseError struct {
	Spec   string // the specification being parsed
	Field  string // "second", "minute", "hour", "dom", "month", "dow", "@every", "TZ" or "CRON_TZ"; "" if not applicable
	Pos    int    // 0-based byte offset; -1 if unknown
	Reason string // human-readable cause
	Err    error  // underlying error, if any (for example from time.LoadLocation)
}

ParseError describes why a cron specification was rejected. It is returned as *ParseError by StandardParser.Parse, the parserext parsers, and through them by Add, Update, ValidateSpec and AnalyzeSpec; match it with errors.As.

func (*ParseError) Error ΒΆ

func (e *ParseError) Error() string

Error formats the spec together with whichever of Field and Pos are known, for example: cron: parse "61 * * * *": field "minute": 61 above maximum 59.

func (*ParseError) Unwrap ΒΆ

func (e *ParseError) Unwrap() error

Unwrap exposes the underlying error, such as a time.LoadLocation failure, to errors.Is and errors.As.

type Parser ΒΆ

type Parser interface {
	// Parse compiles spec. Implementations should return *ParseError for
	// rejected specs so callers can inspect the fault.
	Parse(spec string) (Schedule, error)
}

Parser turns a textual spec into a Schedule; see WithParser and ValidateSpecWith. Cron memoizes results per spec, so Parse must be deterministic and safe for concurrent use, and the returned Schedule may back several entries. Parse must return a non-nil Schedule or an error; a nil, nil result is reported as ErrNilSchedule.

type ParserOption ΒΆ

type ParserOption func(*parserConfig)

ParserOption configures NewStandardParser.

func WithDefaultLocation ΒΆ

func WithDefaultLocation(loc *time.Location) ParserOption

WithDefaultLocation sets the time zone for specs without a TZ=/CRON_TZ= prefix. nil (the default) means time.Local.

func WithOptionalSeconds ΒΆ added in v0.5.3

func WithOptionalSeconds() ParserOption

WithOptionalSeconds accepts both five- and six-field specs; a five-field spec is parsed with second 0. Without it six-field specs are rejected.

func WithParserExt ΒΆ

func WithParserExt(ext Parser) ParserOption

WithParserExt consults ext before the standard grammar, for custom descriptors or syntax. ext returning (nil, nil) falls through to standard parsing; a non-nil error is returned as is. A nil ext is ignored.

func WithRequiredSeconds ΒΆ added in v0.5.3

func WithRequiredSeconds() ParserOption

WithRequiredSeconds requires exactly six fields with a leading seconds field, rejecting five-field specs. It takes precedence over WithOptionalSeconds.

type QueueDepthEvent ΒΆ added in v0.5.3

type QueueDepthEvent struct {
	Depth int
}

QueueDepthEvent reports the number of entries waiting in the scheduling heap, that is, entries with a non-zero Next. It is published after Add, Remove, Pause, Resume, Update and each committed fire.

type Recorder ΒΆ added in v0.5.3

type Recorder interface {
	// Record handles one event synchronously.
	Record(Event)
}

Recorder receives every scheduler event inline on the publishing goroutine, before the observers, and never misses one. Record is called concurrently from the loop, planner and job goroutines, so it must be safe for concurrent use and fast: it sits on the dispatch path. A panic is recovered and logged. One Recorder can be installed with WithRecorder.

type RecorderFunc ΒΆ added in v0.5.3

type RecorderFunc func(Event)

RecorderFunc adapts a function to Recorder.

func (RecorderFunc) Record ΒΆ added in v0.5.3

func (f RecorderFunc) Record(event Event)

Record calls f.

type RejectReason ΒΆ added in v0.5.3

type RejectReason uint8

RejectReason classifies a fire refused before job execution; it is carried by RejectedFireEvent.

const (
	// RejectUnknown is the zero value; the scheduler never publishes it.
	RejectUnknown RejectReason = iota
	// RejectConcurrencyLimit reports that WithMaxConcurrent had no free slot.
	RejectConcurrencyLimit
)

func (RejectReason) String ΒΆ added in v0.5.3

func (r RejectReason) String() string

String returns "concurrency-limit" or "unknown".

type RejectedFireEvent ΒΆ added in v0.5.3

type RejectedFireEvent struct {
	Entry       EntryRef
	ScheduledAt time.Time
	Reason      RejectReason
}

RejectedFireEvent reports a fire refused before job execution, for both automatic fires and Trigger calls (which also return ErrConcurrencyLimit). The instant is not retried.

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. The default is one second.

func RetryJitterFrac ΒΆ

func RetryJitterFrac(f float64) RetryOption

RetryJitterFrac sets uniform jitter as a fraction of the delay, e.g. 0.1 for Β±10%. Valid values are in [0, 1].

func RetryMaxDelay ΒΆ

func RetryMaxDelay(d time.Duration) RetryOption

RetryMaxDelay caps the backoff delay. Zero, the default, is uncapped.

func RetryMultiplier ΒΆ

func RetryMultiplier(m float64) RetryOption

RetryMultiplier sets the per-attempt growth factor. Values of 1 or less keep the delay constant.

type RetryPolicy ΒΆ

type RetryPolicy struct {
	MaxRetries int           // retries after the initial attempt; 0 disables, negative is unlimited
	Initial    time.Duration // first retry delay; non-positive means one second
	MaxDelay   time.Duration // backoff cap; zero means uncapped
	Multiplier float64       // per-attempt growth factor; 1 or less keeps the delay constant
	JitterFrac float64       // uniform jitter fraction in [0, 1], e.g. 0.1 for Β±10%
}

RetryPolicy describes exponential backoff with optional jitter for jobs that return an error. MaxRetries == 0 disables retry; negative means retry until the context is cancelled. Fields are exported for config-driven assembly; use Retry for programmatic construction. Install a policy with WithRetry, WithEntryRetry, wrap.Retry or workflow.WithRetry.

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 the context is cancelled. Values are not validated here: WithRetry and WithEntryRetry reject invalid ones.

func (RetryPolicy) IsZero ΒΆ

func (p RetryPolicy) IsZero() bool

IsZero reports whether the policy disables retry. It is keyed only on MaxRetries, so a half-filled policy (for example only Initial set) does not produce a useless wrapper.

func (RetryPolicy) Wrapper ΒΆ

func (p RetryPolicy) Wrapper() Wrapper

Wrapper returns a Wrapper that re-runs the job on error according to p. The returned error joins every attempt's error with errors.Join, keeping at most the first and the 15 most recent when retries are unlimited. Context cancellation aborts before the next attempt and appends context.Cause, so ErrJobTimeout and ErrCronStopping survive into the joined error and remain matchable with errors.Is. A zero policy runs the job exactly once.

type Schedule ΒΆ

type Schedule interface {
	// Next returns the first firing strictly after now, or zero if none.
	Next(now time.Time) time.Time
}

Schedule yields successive firing times. Next must return the first firing strictly after now, or the zero time when the schedule is exhausted, and must be monotone: a later argument never yields an earlier result. Implementations must be safe for concurrent use, because the scheduler evaluates schedules on per-fire planner goroutines and one parsed schedule may back several entries. Next should be cheap: besides one call per fire it runs at Add, Update and Resume, and missed-fire catch-up bisects over it (about 60 calls per late fire).

A Next that returns a non-zero time not after its argument breaks the contract; the scheduler logs the fault and treats the entry as exhausted rather than spinning.

func Filter ΒΆ added in v0.3.0

func Filter(s Schedule, keep func(time.Time) bool) Schedule

Filter wraps s, skipping firings for which keep returns false, for example a holiday calendar. A nil keep passes everything through. The search gives up and reports exhaustion (zero time) after 100000 consecutive rejections, so a filter that rejects everything still terminates.

func OnceAt ΒΆ added in v0.3.0

func OnceAt(t time.Time) Schedule

OnceAt fires exactly once, at t, then exhausts. If t is already past when the entry is added it never fires, unless WithLastRun seeds an anchor before t, in which case the missed-fire policy decides.

func TriggeredSchedule ΒΆ

func TriggeredSchedule() Schedule

TriggeredSchedule never fires on its own; the entry runs only through Trigger, TriggerAndWait or TriggerByName. Entry.Next stays zero for such entries and AnalyzeSpec reports IsTriggered.

func Union ΒΆ added in v0.3.0

func Union(schedules ...Schedule) Schedule

Union fires whenever any member fires; coincident instants fire once. Nil members are ignored and an empty union never fires. The union exhausts only when every member has.

type ScheduleEvent ΒΆ added in v0.5.3

type ScheduleEvent struct {
	Entry    EntryRef
	Schedule Schedule
	Next     time.Time
}

ScheduleEvent reports an entry's newly computed next fire: on Add, Update, Resume and after every committed fire. Next is zero only on Add, when the schedule has no future fire; later recomputations that exhaust an entry publish no ScheduleEvent.

type SkipReason ΒΆ added in v0.3.0

type SkipReason uint8

SkipReason classifies why distributed coordination suppressed a fire; it is carried by SkippedFireEvent.

const (
	// SkipUnknown is the zero value; the scheduler never publishes it.
	SkipUnknown SkipReason = iota
	// SkipNotLeader reports that the Elector answered false, nil.
	SkipNotLeader
	// SkipElectionError reports that the Elector returned an error.
	SkipElectionError
	// SkipAlreadyClaimed reports that the Claimer answered false, nil.
	SkipAlreadyClaimed
	// SkipClaimError reports that the Claimer returned an error.
	SkipClaimError
)

func (SkipReason) String ΒΆ added in v0.3.0

func (r SkipReason) String() string

String returns a kebab-case label suitable for metric labels, or "unknown".

type SkippedFireEvent ΒΆ added in v0.5.3

type SkippedFireEvent struct {
	Entry       EntryRef
	ScheduledAt time.Time
	Reason      SkipReason
	Err         error
}

SkippedFireEvent reports a fire suppressed by distributed coordination. Err is non-nil only for SkipElectionError and SkipClaimError.

type SpecAnalysis ΒΆ

type SpecAnalysis struct {
	Spec        string
	Valid       bool
	Err         error          // *ParseError or ErrNilSchedule when Valid is false
	IsTriggered bool           // the spec parsed to TriggeredSchedule (custom parsers only)
	Descriptor  string         // "@every", "@hourly", ... or "" for field specs
	Interval    time.Duration  // set when Descriptor == "@every"
	Location    *time.Location // schedule time zone when the Schedule exposes one; nil otherwise
	NextRun     time.Time      // first firing after the now passed in; zero if none or IsTriggered
}

SpecAnalysis is the result of AnalyzeSpec. When Valid is false only Spec and Err are meaningful.

func AnalyzeSpec ΒΆ

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

AnalyzeSpec describes spec relative to now using the built-in five-field parser in time.Local. It never fails: a rejected spec is reported through SpecAnalysis.Valid and Err. Use AnalyzeSpecWith to match a custom parser.

Example ΒΆ
package main

import (
	"fmt"
	"time"

	"github.com/libtnb/cron"
)

func main() {
	now := time.Date(2026, time.January, 1, 12, 0, 0, 0, time.UTC)

	a := cron.AnalyzeSpec("CRON_TZ=UTC 30 8 * * *", now)
	fmt.Println(a.Valid, a.Location, a.NextRun.Format(time.RFC3339))

	a = cron.AnalyzeSpec("@every 90s", now)
	fmt.Println(a.Valid, a.Descriptor, a.Interval)
}
Output:
true UTC 2026-01-02T08:30:00Z
true @every 1m30s

func AnalyzeSpecWith ΒΆ

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

AnalyzeSpecWith describes spec relative to now using p. Location is set when the Schedule has a Location() *time.Location method, as SpecSchedule and parserext.QuartzSchedule do.

type SpecSchedule ΒΆ

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

SpecSchedule is a compiled cron expression as produced by StandardParser. It is immutable and safe for concurrent use; one instance may back several entries. The zero value never fires.

func (*SpecSchedule) Location ΒΆ

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

Location returns the time zone the expression is evaluated in: the TZ= prefix if present, otherwise the parser's default. AnalyzeSpec reports it.

func (*SpecSchedule) LogValue ΒΆ

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

LogValue renders the schedule's kind and location for slog.

func (*SpecSchedule) Next ΒΆ

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

Next returns the first firing strictly after t, evaluated in the schedule's Location and returned in t's location, or zero when nothing matches within the next five years.

The hour branch reconstructs the wall clock at the target hour instead of adding an absolute (h-hour)*time.Hour, which would overshoot a DST spring-forward gap and skip the day. The minute/second branches keep their absolute jumps: they never straddle a whole-hour DST boundary, and stepping through the repeated hour on fall-back keeps both firings observable.

func (*SpecSchedule) Upcoming ΒΆ

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

Upcoming lazily yields firings strictly after from, in order, until the schedule exhausts. NextN and Between use it.

type StandardParser ΒΆ

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

StandardParser parses the classic cron grammar. It is immutable after construction and safe for concurrent use. New builds one from WithLocation and WithSecondsField unless WithParser is set; ValidateSpec and AnalyzeSpec use a five-field instance in time.Local.

func NewStandardParser ΒΆ

func NewStandardParser(opts ...ParserOption) *StandardParser

NewStandardParser builds a parser; without options it accepts five-field specs and descriptors in time.Local.

func (*StandardParser) Parse ΒΆ

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

Parse compiles spec into a *SpecSchedule, or a ConstantDelay for "@every". Surrounding whitespace is ignored. It returns a *ParseError for an empty spec, a wrong field count, an out-of-range or malformed field, an unknown descriptor, an unknown or empty TZ= zone, or an "@every" interval below one millisecond.

type Upcoming ΒΆ

type Upcoming interface {
	// Upcoming yields every firing strictly after from, in order.
	Upcoming(from time.Time) iter.Seq[time.Time]
}

Upcoming is an optional Schedule capability used by NextN and Between to iterate firings lazily. Upcoming must yield strictly increasing times after from and stop when the schedule exhausts.

type Wrapper ΒΆ

type Wrapper func(Job) Job

Wrapper decorates a Job, for example with a timeout, retries or tracing. Wrappers are applied once per entry at Add time, so a wrapper that keeps state (such as wrap.SkipIfRunning) gets one instance per entry.

func Chain ΒΆ

func Chain(wrappers ...Wrapper) Wrapper

Chain composes wrappers so the first one is outermost: Chain(a, b)(j) runs a around b around j. Chain with no wrappers returns j unchanged. Cron builds each entry's chain as WithChain, then WithEntryChain, then the retry policy, from the outside in.

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
contrib
otel module
prometheus module
coordination
postgres module
redis module
internal
bitmask
Package bitmask provides the bit-set scan shared by the schedule implementations, which store each cron field as a uint64 mask.
Package bitmask provides the bit-set scan shared by the schedule implementations, which store each cron field as a uint64 mask.
heap
Package heap provides a typed min-heap with addressable items, so the scheduler can re-key or remove an entry in O(log n) without searching.
Package heap provides a typed min-heap with addressable items, so the scheduler can re-key or remove an entry in O(log n) without searching.
parsecache
Package parsecache memoizes parser results so repeated registrations of the same spec share one parsed value and pay the parse cost once.
Package parsecache memoizes parser results so repeated registrations of the same spec share one parsed value and pay the parse cost once.
Package parserext provides cron.Parser extensions beyond the standard grammar.
Package parserext provides cron.Parser extensions beyond the standard grammar.
Package workflow builds typed directed acyclic graphs of cron jobs and runs them as one cron.Job.
Package workflow builds typed directed acyclic graphs of cron jobs and runs them as one cron.Job.
Package wrap supplies reusable cron.Wrapper decorators: panic recovery, timeouts, retry and overlap control.
Package wrap supplies reusable cron.Wrapper decorators: panic recovery, timeouts, retry and overlap control.

Jump to

Keyboard shortcuts

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