Documentation
¶
Overview ¶
Package crontask provides a production-grade cron and task scheduling engine for the replify ecosystem. It is designed to be the canonical scheduling sub-package for long-running Go services that require reliable, expressive, and observable periodic job execution.
Design Goals ¶
crontask was built with three primary goals in mind:
- Correctness — schedules must fire at the right time, every time, across DST transitions, leap years, and timezone boundaries.
- Expressiveness — operators support the full standard five-field cron syntax, an optional leading seconds field, semantic aliases such as @daily and @weekly, step-based interval expressions, and per-job jitter to spread load across a fleet.
- Observability — every registered job exposes metadata (last run, next run, run count, last error) and the scheduler surface accepts hook interfaces for pre/post-execution callbacks, success/failure notifications, and metrics instrumentation.
Architectural Philosophy ¶
The package is structured into four distinct layers, each with a single responsibility:
Expression layer (expression.go, parser.go) — converts a raw string such as "0 9 * * 1-5" or "@weekdays" into a typed Schedule that can compute the next activation time for any reference instant.
Job layer (job.go) — holds the function to execute, its configuration (retry policy, timeout, hooks, jitter), and live runtime statistics. The in-memory registry is guarded by a read/write mutex so that the scheduler loop and external callers can safely inspect or mutate the job list at any time.
Execution layer (executor.go) — wraps a job invocation with timeout enforcement, retry-with-backoff, context propagation, and hook dispatch. Every invocation runs in its own goroutine so that a slow or stuck job never delays the scheduler tick.
Scheduler layer (scheduler.go) — owns the main goroutine, advances a monotonic clock, queries each registered job's next-fire time, and dispatches due jobs through the executor. The scheduler is fully concurrent-safe and supports graceful shutdown via Shutdown(ctx).
Comparison with robfig/cron and gronx ¶
robfig/cron is the de-facto standard cron library for Go. crontask adopts its scheduler-loop design (sub-second precision, heap-ordered next-fire times) but extends it with a richer job model (retry, backoff, hooks, jitter, per-job context) and replaces its terse API with idiomatic option-function constructors documented in the style used throughout replify.
gronx is primarily an expression parser and evaluator. crontask borrows its flexible field-parsing ideas, the @alias vocabulary, and the concept of validating an expression without running a scheduler. Unlike gronx, crontask ships a complete execution engine, so consumers do not need a separate library.
Quick Start ¶
s, err := crontask.New()
if err != nil {
log.Fatal(err)
}
s.Start()
_, err = s.Register("0 * * * *", func(ctx context.Context) error {
fmt.Println("every hour:", time.Now())
return nil
})
if err != nil {
log.Fatal(err)
}
// Shut down cleanly after a signal.
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
s.Shutdown(ctx)
Thread Safety ¶
All exported methods on Scheduler are safe for concurrent use from multiple goroutines. The job registry, scheduler loop, and executor each acquire their own locks independently to minimise contention.
Index ¶
- Variables
- func DeleteAlias(name string) error
- func Explain(expr string) (string, error)
- func IsDue(expr string, at time.Time) bool
- func IsValidCronExpr(expr string) bool
- func NextRun(expr string, from time.Time) (time.Time, error)
- func NextRuns(expr string, from time.Time, n int) ([]time.Time, error)
- func RegisterAlias(name, expr string) error
- func Validate(expr string) error
- func ValidateCronExpr(expr string) error
- type Alias
- type BackoffPolicy
- type ConcurrencyLimiterHookInstance
- type Expression
- type ExpressionError
- type Hooks
- type JobContext
- type JobError
- type JobFunc
- type JobInfo
- type JobOption
- func WithBackoff(p BackoffPolicy) JobOption
- func WithContext(ctx context.Context) JobOption
- func WithHooks(hooks ...Hooks) JobOption
- func WithJitter(max time.Duration) JobOption
- func WithJobID(id string) JobOption
- func WithJobName(name string) JobOption
- func WithMaxRetries(n int) JobOption
- func WithTimeout(d time.Duration) JobOption
- type MetricsHookInstance
- func (m *MetricsHookInstance) FailureCount() int64
- func (m *MetricsHookInstance) OnFailure(_ context.Context, _ string, d time.Duration, _ error)
- func (m *MetricsHookInstance) OnPanic(_ context.Context, _ string, _ any)
- func (m *MetricsHookInstance) OnSuccess(_ context.Context, _ string, d time.Duration)
- func (m *MetricsHookInstance) PanicCount() int64
- func (m *MetricsHookInstance) SuccessCount() int64
- func (m *MetricsHookInstance) TotalDuration() time.Duration
- type NoopHooks
- type PanicHook
- type RetryHook
- type Schedule
- type Scheduler
- func (s *Scheduler) IsRunning() bool
- func (s *Scheduler) Jobs() []JobInfo
- func (s *Scheduler) Location() *time.Location
- func (s *Scheduler) NextRuns(id string, t time.Time, n int) ([]time.Time, error)
- func (s *Scheduler) Register(expr string, fn JobFunc, opts ...JobOption) (string, error)
- func (s *Scheduler) Remove(id string) error
- func (s *Scheduler) Shutdown(ctx context.Context) error
- func (s *Scheduler) Start() error
- func (s *Scheduler) Stop()
- func (s *Scheduler) WithSecondsEnabled() bool
- type SchedulerOption
Constants ¶
This section is empty.
Variables ¶
var ( // ErrInvalidExpression is returned by Parse and Validate when the provided // cron expression is syntactically or semantically invalid. ErrInvalidExpression = errors.New("crontask: invalid cron expression") // ErrJobNotFound is returned by Remove and similar methods when the given // job ID does not exist in the registry. ErrJobNotFound = errors.New("crontask: job not found") // ErrSchedulerRunning is returned by Start when the scheduler is already // in a running state. ErrSchedulerRunning = errors.New("crontask: scheduler is already running") // ErrSchedulerStopped is returned by Register and similar methods when the // caller attempts to mutate a scheduler that has been permanently shut down. ErrSchedulerStopped = errors.New("crontask: scheduler has been stopped") // ErrJobTimeout is wrapped into the error returned by the executor when a // job's execution deadline is exceeded. ErrJobTimeout = errors.New("crontask: job execution timed out") // ErrMaxRetriesExceeded is returned when a job exhausts its configured // retry budget without succeeding. ErrMaxRetriesExceeded = errors.New("crontask: maximum retries exceeded") )
Sentinel errors returned by the crontask package. Callers can test for these values with errors.Is when they need to handle a specific condition.
Functions ¶
func DeleteAlias ¶
DeleteAlias removes a previously registered alias by name. The name comparison is case-insensitive and the "@" prefix is required.
Built-in aliases can be deleted if needed, though doing so is not recommended for production code. An error is returned when the alias does not exist in the registry.
Example:
crontask.RegisterAlias("@nightly", "0 2 * * *")
// ... use @nightly ...
crontask.DeleteAlias("@nightly")
func Explain ¶
Explain converts a cron expression into a natural English description.
Supported input forms:
- "@every 5m" → "Every 5 minutes"
- "@daily", "@hourly", etc → predefined descriptions for built-in aliases
- "*/30 * * * * *" → "Every 30 seconds"
- "0 9 * * 1-5" → "At 09:00, Monday through Friday"
- "TZ=..." prefixes → described without the timezone qualifier
Custom aliases registered via RegisterAlias are described by expanding them to their underlying expression and applying the field-based explainer.
Explain returns ErrInvalidExpression (wrapped) for invalid input and never panics.
Example:
desc, err := crontask.Explain("0 0 * * 1-5")
// desc == "At 00:00, Monday through Friday"
func IsDue ¶
IsDue reports whether the cron expression expr was due at the given time at.
The check uses second-level granularity: IsDue returns true when the first activation of the schedule after (at − 1 second) is at or before at. For standard five-field expressions this means the function returns true exactly at the start of a matching minute; for six-field or @every expressions it returns true at the matching second.
IsDue returns false for invalid expressions without panicking.
Thread-safe; does not require a Scheduler instance.
Example:
now := time.Now().Truncate(time.Minute)
if crontask.IsDue("0 9 * * 1-5", now) {
sendDailyReport()
}
func IsValidCronExpr ¶
IsValidCronExpr reports whether expr is a syntactically and semantically valid cron expression recognised by this package. It is equivalent to Validate(expr) == nil.
Example:
valid := crontask.IsValidCronExpr("0 9 * * 1-5")
Thread-safe; does not require a Scheduler instance.
func NextRun ¶
NextRun returns the first activation time for expr after the reference time from. It is a convenience wrapper around Parse and Schedule.Next that avoids the need to create a Scheduler or call Parse manually.
Thread-safe; does not require a Scheduler instance.
Example:
next, err := crontask.NextRun("0 9 * * 1-5", time.Now())
func NextRuns ¶
NextRuns returns the next n activation times for expr starting from from. It is a convenience wrapper around Parse and Schedule.Next.
When n ≤ 0, NextRuns returns a nil slice without an error. When the schedule has fewer than n future activations, the returned slice is shorter than n.
Thread-safe; does not require a Scheduler instance.
Example:
runs, err := crontask.NextRuns("0 9 * * 1-5", time.Now(), 5)
func RegisterAlias ¶
RegisterAlias registers a custom alias that can subsequently be used anywhere a cron expression is accepted (Register, Parse, IsDue, etc.).
name must begin with "@". expr must be a valid five-field or six-field cron expression; @every and nested alias expressions are not accepted as the right-hand side of a registration.
If name already exists (built-in or previously registered), it is silently overwritten with the new expression. Names are matched case-insensitively.
Example:
err := crontask.RegisterAlias("@nightly", "0 2 * * *")
func Validate ¶
Validate reports whether the given expression is syntactically and semantically valid. It returns nil on success or a typed *ExpressionError (which also satisfies errors.Is(err, ErrInvalidExpression)).
func ValidateCronExpr ¶
ValidateCronExpr validates expr and returns a descriptive error when it is invalid. The returned error, if non-nil, is an *ExpressionError that also satisfies errors.Is(err, ErrInvalidExpression).
ValidateCronExpr is a convenience wrapper around Validate and is provided for callers who prefer the longer, self-documenting name.
Example:
err := crontask.ValidateCronExpr("0 9 * * 1-5")
Thread-safe; does not require a Scheduler instance.
Types ¶
type Alias ¶
type Alias = string
Alias is a short-hand name for a commonly used cron schedule, inspired by both Vixie-cron and gronx. Aliases are resolved by the parser before field-level parsing occurs.
The following aliases are recognised out of the box:
@yearly (or @annually) — "0 0 1 1 *" @monthly — "0 0 1 * *" @weekly — "0 0 * * 0" @daily (or @midnight) — "0 0 * * *" @hourly — "0 * * * *" @minutely — "* * * * *" @weekdays — "0 0 * * 1-5" @weekends — "0 0 * * 0,6"
Business-oriented aliases:
@businessDaily — "0 9 * * 1-5" (09:00 on weekdays) @businessHourly — "0 9-17 * * 1-5" (top of each hour, 09–17, weekdays) @quarterly — "0 0 1 1,4,7,10 *" (midnight, first day of each quarter) @semiMonthly — "0 0 1,15 * *" (midnight, 1st and 15th of each month) @workhours — "* 9-17 * * 1-5" (every minute 09:00–17:59, weekdays) @marketOpen — "30 9 * * 1-5" (09:30, weekdays) @marketClose — "0 16 * * 1-5" (16:00, weekdays)
Additional aliases can be registered at runtime with RegisterAlias.
When using the six-field (seconds-first) format, the above expansions gain a leading "0" second field automatically.
const ( // AliasYearly fires once a year, at midnight on 1 January. AliasYearly Alias = "@yearly" // AliasAnnually is a synonym for AliasYearly. AliasAnnually Alias = "@annually" // AliasMonthly fires once a month, at midnight on the 1st. AliasMonthly Alias = "@monthly" // AliasWeekly fires once a week, at midnight on Sunday. AliasWeekly Alias = "@weekly" // AliasDaily fires once a day, at midnight. AliasDaily Alias = "@daily" // AliasMidnight is a synonym for AliasDaily. AliasMidnight Alias = "@midnight" // AliasHourly fires once an hour, at the top of the hour. AliasHourly Alias = "@hourly" // AliasMinutely fires once a minute, at the top of each minute. AliasMinutely Alias = "@minutely" // AliasWeekdays fires at midnight on every weekday (Monday–Friday). AliasWeekdays Alias = "@weekdays" // AliasWeekends fires at midnight on Saturday and Sunday. AliasWeekends Alias = "@weekends" // AliasBusinessDaily fires at 09:00 on every weekday (Monday–Friday). // It is suitable for once-per-business-day jobs that run at the start of // the working day. AliasBusinessDaily Alias = "@businessDaily" // AliasBusinessHourly fires at the top of each hour from 09:00 to 17:00 // on every weekday. It is suitable for tasks that should run once per // business hour during core working hours. AliasBusinessHourly Alias = "@businessHourly" // AliasQuarterly fires at midnight on the first day of each calendar // quarter (January, April, July, and October). AliasQuarterly Alias = "@quarterly" // AliasSemiMonthly fires at midnight on the 1st and 15th of every month, // giving two activations per month. AliasSemiMonthly Alias = "@semiMonthly" // AliasWorkHours fires every minute during business hours: 09:00–17:59 // on every weekday. Suitable for polling tasks that should only run // during office hours. AliasWorkHours Alias = "@workhours" // AliasMarketOpen fires at 09:30 on every weekday, aligned with the // standard US equity market open time. AliasMarketOpen Alias = "@marketOpen" // AliasMarketClose fires at 16:00 on every weekday, aligned with the // standard US equity market close time. AliasMarketClose Alias = "@marketClose" // AliasEndOfDay fires at 17:00 on every weekday — useful for daily // close-of-business summaries, digest emails, or EOD reports. AliasEndOfDay Alias = "@endOfDay" // AliasStartOfDay fires at 08:00 on every weekday — a minute before most // staff arrive, ideal for pre-loading caches, warming services, or sending // morning briefings. AliasStartOfDay Alias = "@startOfDay" // AliasLunchtime fires at 12:00 on every weekday — suitable for mid-day // digest notifications or low-priority batch jobs run during off-peak hours. AliasLunchtime Alias = "@lunchtime" // AliasEndOfWeek fires at 17:00 every Friday — perfect for weekly summary // emails, cleanup jobs, or end-of-week reporting pipelines. AliasEndOfWeek Alias = "@endOfWeek" // AliasStartOfWeek fires at 08:00 every Monday — ideal for weekly planning // notifications, metric resets, or Monday morning digest generation. AliasStartOfWeek Alias = "@startOfWeek" // AliasEndOfMonth fires at 23:59 on the last day of each month (28th used // as safe cross-month anchor). For true last-day logic, use a job-level // calendar check. Suitable for monthly billing runs and invoicing triggers. AliasEndOfMonth Alias = "@endOfMonth" // AliasPayroll fires at 08:00 on the 1st and 15th of every month — // matching the two most common semi-monthly payroll schedules. AliasPayroll Alias = "@payroll" // AliasNightlyMaintenance fires at 02:00 every day — a low-traffic window // suited for database vacuums, index rebuilds, and nightly backup jobs. AliasNightlyMaintenance Alias = "@nightlyMaintenance" // AliasPreMarket fires at 08:00 on every weekday — one hour before the US // equity market opens, useful for pre-market data ingestion or alert checks. AliasPreMarket Alias = "@preMarket" // AliasAfterMarket fires at 17:00 on every weekday — one hour after the US // equity market closes, suitable for after-hours reconciliation or reporting. AliasAfterMarket Alias = "@afterMarket" // AliasMidMarket fires at 12:30 on every weekday — the midpoint of the US // trading day, useful for intraday snapshot jobs or mid-session risk checks. AliasMidMarket Alias = "@midMarket" // AliasQuarterEnd fires at 23:59 on the last day of each fiscal quarter // (March, June, September, December). Useful for quarter-close accounting // jobs, regulatory filings, or board report generation. AliasQuarterEnd Alias = "@quarterEnd" // AliasTaxDeadline fires at 08:00 on April 15th — the standard US federal // tax filing deadline. Useful for annual compliance reminder pipelines. AliasTaxDeadline Alias = "@taxDeadline" // AliasRegulatoryOpen fires at 07:00 on every weekday — before market open, // aligned with common regulatory reporting windows (e.g. FINRA, SEC). AliasRegulatoryOpen Alias = "@regulatoryOpen" // AliasOffPeakHourly fires once an hour between 20:00 and 06:00 every day — // useful for infrastructure jobs, bulk imports, or ML training runs that // should avoid peak business hours. AliasOffPeakHourly Alias = "@offPeakHourly" // AliasDeploymentWindow fires at 22:00 on Tuesday and Thursday — a // conventional low-risk deployment window outside business hours but not // on a weekend, giving a full working day before and after for monitoring. AliasDeploymentWindow Alias = "@deploymentWindow" // AliasDatabaseBackup fires at 01:00 every day — a quiet early-morning // window well-suited for full or incremental database backup jobs. AliasDatabaseBackup Alias = "@databaseBackup" // AliasWeeklyReport fires at 08:00 every Monday — delivers weekly KPI // summaries, analytics digests, or newsletter generation at the start of // the work week, before most users are active. AliasWeeklyReport Alias = "@weeklyReport" // AliasMonthlyReport fires at 08:00 on the 1st of every month — aligns // with standard monthly reporting cycles for finance, product, or ops teams. AliasMonthlyReport Alias = "@monthlyReport" // AliasCustomerDigest fires at 09:00 on every weekday — sends customer // activity digests, CRM summaries, or support queue snapshots at the start // of each business day. AliasCustomerDigest Alias = "@customerDigest" // AliasSLACheck fires every 15 minutes during business hours on weekdays — // useful for SLA breach detection, ticket-age monitors, or uptime heartbeat // checks that need sub-hourly granularity without running 24/7. AliasSLACheck Alias = "@slaCheck" )
Cron expression aliases are short-hand names for commonly used cron schedules. They are resolved by the parser before field-level parsing occurs.
type BackoffPolicy ¶
BackoffPolicy is a function that receives the one-based attempt number and returns the duration to wait before the next attempt. Returning zero means the retry fires immediately.
func ConstantBackoff ¶
func ConstantBackoff(delay time.Duration) BackoffPolicy
ConstantBackoff returns a BackoffPolicy that waits the same fixed delay between every retry attempt.
Example:
crontask.ConstantBackoff(5 * time.Second)
func ExponentialBackoff ¶
func ExponentialBackoff(base time.Duration) BackoffPolicy
ExponentialBackoff returns a BackoffPolicy that doubles the base delay on each successive attempt (base, 2×base, 4×base, …).
Example:
crontask.ExponentialBackoff(time.Second)
type ConcurrencyLimiterHookInstance ¶
type ConcurrencyLimiterHookInstance struct {
NoopHooks
// contains filtered or unexported fields
}
ConcurrencyLimiterHookInstance is the concrete type returned by ConcurrencyLimiterHook. It exposes a Hooks-compatible API and can also be interrogated for current concurrency at runtime.
func ConcurrencyLimiterHook ¶
func ConcurrencyLimiterHook(maxConcurrent int) *ConcurrencyLimiterHookInstance
ConcurrencyLimiterHook returns a Hooks implementation that limits the number of concurrent executions to maxConcurrent. Additional executions block in OnStart until a slot becomes available or the job's context is cancelled.
This is most useful when the same scheduler runs many instances of a heavy job and you want to avoid overwhelming downstream resources:
limiter := crontask.ConcurrencyLimiterHook(3)
for i := 0; i < 10; i++ {
s.Register("@every 1m", heavyFn, crontask.WithHooks(limiter))
}
If maxConcurrent is ≤ 0 it is treated as 1 (serial execution).
The same ConcurrencyLimiterHook instance must be shared across all jobs that should count against the same limit; passing different instances to different jobs creates independent limits.
func (*ConcurrencyLimiterHookInstance) Active ¶
func (h *ConcurrencyLimiterHookInstance) Active() int
Active returns the number of job invocations currently holding a slot.
func (*ConcurrencyLimiterHookInstance) OnComplete ¶
OnComplete releases the previously acquired concurrency slot. If no slot was acquired (context was cancelled in OnStart), the non-blocking select ensures the semaphore is not under-released.
func (*ConcurrencyLimiterHookInstance) OnStart ¶
func (h *ConcurrencyLimiterHookInstance) OnStart(ctx context.Context, _ string)
OnStart acquires one concurrency slot. It blocks until a slot is available or the context is done. When the context is cancelled before a slot is acquired the method returns without acquiring, and OnComplete becomes a no-op for this invocation (the semaphore remains unmodified).
type Expression ¶
type Expression struct {
// contains filtered or unexported fields
}
Expression is a parsed cron expression that exposes schedule utilities without requiring a running Scheduler instance. Obtain one via MustParse.
Expression is immutable and safe for concurrent use.
func MustParse ¶
func MustParse(expr string) Expression
MustParse is like Parse but panics instead of returning an error when the expression is invalid. It is intended for use in package-level variable initializers where the expression is a compile-time constant.
Example:
var reportSchedule = crontask.MustParse("0 9 * * 1-5")
func (Expression) IsDue ¶
func (e Expression) IsDue(at time.Time) bool
IsDue reports whether the expression is due at the given time using second-level granularity. See the package-level IsDue for the exact semantics.
Example:
now := time.Now().Truncate(time.Minute)
if expr.IsDue(now) {
sendDailyReport()
}
func (Expression) Next ¶
func (e Expression) Next(from time.Time) time.Time
Next returns the first activation time strictly after from. It returns the zero time when no future activation exists (e.g. the schedule is exhausted).
Example:
next := expr.Next(time.Now())
func (Expression) NextN ¶
NextN returns the next n activation times starting from from. If fewer than n future activations exist, the slice is shorter than n.
Example:
next := expr.NextN(time.Now(), 5)
func (Expression) Raw ¶
func (e Expression) Raw() string
Raw returns the original expression string as passed to MustParse.
type ExpressionError ¶
type ExpressionError struct {
// Expression is the raw string that triggered the error.
Expression string
// Field is the zero-based index of the field within the expression that
// caused the error, or -1 when the error is not field-specific.
Field int
// Reason is a human-readable description of why the expression is invalid.
Reason string
}
ExpressionError describes a parse or validation error for a specific cron expression. It implements the error interface and can be unwrapped to ErrInvalidExpression for sentinel matching.
func (*ExpressionError) Error ¶
func (e *ExpressionError) Error() string
Error implements the error interface.
Example:
for _, run := range runs {
if errors.Is(run.Error, crontask.ErrJobTimeout) {
// Handle timeout specifically
}
}
func (*ExpressionError) Unwrap ¶
func (e *ExpressionError) Unwrap() error
Unwrap returns ErrInvalidExpression so that callers can test for the sentinel value with errors.Is.
Example:
for _, run := range runs {
if errors.Is(run.Error, crontask.ErrJobTimeout) {
// Handle timeout specifically
}
}
type Hooks ¶
type Hooks interface {
// OnStart is called immediately before the job function is invoked,
// after jitter has been applied and after the execution context has been
// derived. The jobID parameter identifies the job being dispatched.
OnStart(ctx context.Context, jobID string)
// OnSuccess is called when the job function returns nil. The duration
// parameter is the wall-clock time of the invocation, excluding jitter.
OnSuccess(ctx context.Context, jobID string, duration time.Duration)
// OnFailure is called when the job function returns a non-nil error after
// all retry attempts are exhausted. err is the final error.
OnFailure(ctx context.Context, jobID string, duration time.Duration, err error)
// OnComplete is called after OnSuccess or OnFailure and regardless of the
// outcome. It is useful for releasing resources that were acquired in
// OnStart.
OnComplete(ctx context.Context, jobID string, duration time.Duration)
}
Hooks is the interface that callers may implement to observe the lifecycle of a job invocation. All methods are optional — embed NoopHooks to satisfy the interface without implementing every method.
Hook methods are called synchronously within the executor goroutine. Hooks must not block for long periods; spawn a goroutine if you need to perform expensive work (e.g. remote metric writes) without slowing the executor.
func ChainHooks ¶
ChainHooks composes multiple Hooks implementations into a single Hooks that dispatches each method call to all members in order. Nil members are silently skipped.
The returned chain also implements RetryHook and PanicHook by delegating to any member that implements those optional interfaces; members that do not implement them are skipped.
When zero non-nil hooks are supplied ChainHooks returns NoopHooks. When exactly one non-nil hook is supplied it is returned as-is (no wrapping).
Example:
hooks := crontask.ChainHooks(
crontask.LoggingHook(),
crontask.MetricsHook(),
&myCustomHook{},
)
s.Register("@daily", fn, crontask.WithHooks(hooks))
func LoggingHook ¶
func LoggingHook() Hooks
LoggingHook returns a Hooks implementation that logs each lifecycle event (start, success, failure, complete) using the standard log package. All messages are prefixed with "crontask" for easy filtering.
Logging is synchronous and lightweight; it delegates directly to log.Printf which is safe for concurrent use.
Example:
s, _ := crontask.New(
crontask.WithSchedulerHooks(crontask.LoggingHook()),
)
func RecoverPanicHook ¶
func RecoverPanicHook() Hooks
RecoverPanicHook returns a Hooks implementation that silently recovers panics in job functions and logs them with the standard log package. The scheduler loop is never interrupted by a panicking job.
To supply a custom panic handler (e.g. to send to Sentry or PagerDuty), use RecoverPanicHookWithHandler.
Example:
s.Register("@daily", riskyFn, crontask.WithHooks(crontask.RecoverPanicHook()))
func RecoverPanicHookWithHandler ¶
func RecoverPanicHookWithHandler(handler func(ctx context.Context, jobID string, recovered any)) Hooks
RecoverPanicHookWithHandler returns a Hooks implementation that calls handler whenever the job function panics. handler is called synchronously inside the executor goroutine; it must not panic itself.
Example:
hook := crontask.RecoverPanicHookWithHandler(func(_ context.Context, id string, r any) {
alerting.Send(fmt.Sprintf("job %s panicked: %v", id, r))
})
s.Register("@daily", fn, crontask.WithHooks(hook))
func RetryLoggerHook ¶
func RetryLoggerHook() Hooks
RetryLoggerHook returns a Hooks implementation that logs every retry attempt via the standard log package. It implements the optional RetryHook interface so it receives per-attempt callbacks rather than only the final failure.
Example:
s.Register("@every 5m", fn,
crontask.WithMaxRetries(3),
crontask.WithHooks(crontask.RetryLoggerHook()),
)
func TimeoutLoggerHook ¶
func TimeoutLoggerHook() Hooks
TimeoutLoggerHook returns a Hooks implementation that logs a warning whenever a job invocation is terminated due to a timeout (context.DeadlineExceeded). Non-timeout failures are passed through without logging.
Example:
s.Register("@minutely", fn,
crontask.WithTimeout(10*time.Second),
crontask.WithHooks(crontask.TimeoutLoggerHook()),
)
type JobContext ¶
type JobContext struct {
// JobID is the unique identifier of the executing job.
JobID string
// Expression is the raw cron expression string for the job.
Expression string
// ScheduledAt is the time the job was originally scheduled to fire.
ScheduledAt time.Time
// StartedAt is the time the job function was first invoked.
StartedAt time.Time
// FinishedAt is the time the job invocation completed (or panicked).
FinishedAt time.Time
// Attempt is the one-based attempt number for retry callbacks (1 = first
// try, 2 = first retry, etc.). Zero for non-retry callbacks.
Attempt int
// Error is the error from the most recent attempt, or nil on success.
Error error
// Duration is the elapsed time of the invocation.
Duration time.Duration
}
JobContext carries structured metadata about a single job invocation. It is a reference type for authors of custom hook wrappers and job middleware. The fields available depend on how and where the context is populated:
- JobID and Duration are always populated by the executor.
- Error is non-nil only in failure and retry contexts.
- Attempt is set for retry callbacks; it is zero in success/complete callbacks.
- Expression, ScheduledAt, StartedAt, and FinishedAt are populated by custom job wrappers that assemble this struct before calling downstream services.
The built-in hook interfaces (RetryHook, PanicHook) and the core Hooks interface use individual parameters rather than JobContext for backward compatibility. Use JobContext in your own custom job wrappers when you need to pass a richer snapshot through a middleware chain:
func instrumentedJob(meta crontask.JobContext, fn crontask.JobFunc) crontask.JobFunc {
return func(ctx context.Context) error {
meta.StartedAt = time.Now()
err := fn(ctx)
meta.FinishedAt = time.Now()
meta.Duration = meta.FinishedAt.Sub(meta.StartedAt)
meta.Error = err
myCustomMiddleware(ctx, meta)
return err
}
}
type JobError ¶
type JobError struct {
// JobID is the identifier of the job that failed.
JobID string
// Attempt is the one-based attempt number (1 = first try, 2 = first retry,
// etc.).
Attempt int
// Err is the underlying error returned by the job function.
Err error
}
JobError wraps a job execution error with the job ID and attempt number so that hook implementations and callers can correlate errors with their origin.
type JobFunc ¶
JobFunc is the function signature for a scheduled job. The context passed to JobFunc is derived from the job's base context (or context.Background when none is configured) and may carry a deadline when WithTimeout is set. Jobs should honour context cancellation for clean shutdown.
type JobInfo ¶
type JobInfo struct {
// ID is the unique identifier of the job, either supplied by the caller
// via WithJobID or generated automatically at registration time.
ID string
// Name is an optional human-readable label set via WithJobName.
Name string
// Expression is the raw cron expression string as supplied to Register.
Expression string
// NextRun is the next scheduled activation time in the scheduler's
// timezone. The zero value means the schedule has no future activations.
NextRun time.Time
// LastRun is the time the job was most recently dispatched. The zero
// value means the job has never been executed.
LastRun time.Time
// LastErr is the error returned by the most recent execution, or nil if
// the last execution succeeded or the job has never been run.
LastErr error
// RunCount is the total number of times the job has been dispatched
// (across all retries within a single schedule activation, only the
// initial dispatch is counted).
RunCount int64
}
JobInfo is an immutable snapshot of a registered job's metadata and runtime statistics. It is returned by Jobs() and used for introspection.
type JobOption ¶
type JobOption func(*jobConfig)
JobOption is a functional option applied to a job entry at registration time via Register.
func WithBackoff ¶
func WithBackoff(p BackoffPolicy) JobOption
WithBackoff sets the BackoffPolicy used between retry attempts. The default policy applies no delay between retries. Use ExponentialBackoff or ConstantBackoff for more controlled retry behaviour.
Example:
s.Register("@hourly", fn,
crontask.WithMaxRetries(3),
crontask.WithBackoff(crontask.ExponentialBackoff(time.Second)),
)
func WithContext ¶
WithContext associates a base context with the job. The executor derives a child context from this base for each invocation, allowing per-job cancellation or value propagation.
Example:
s.Register("@daily", fn, crontask.WithContext(reqCtx))
func WithHooks ¶
WithHooks attaches one or more Hooks implementations to the job. When multiple hooks are supplied they are composed into a chain that dispatches each hook method to all members in order. Nil values are silently ignored.
Backward-compatible: existing callers that supply a single Hooks value continue to work without modification.
Example:
s.Register("@daily", fn, crontask.WithHooks(myHooks))
s.Register("@daily", fn, crontask.WithHooks(crontask.LoggingHook(), crontask.MetricsHook()))
func WithJitter ¶
WithJitter adds a random delay in the range [0, max) before each job execution. Jitter is useful in distributed systems where many nodes share the same schedule and simultaneous load spikes are undesirable.
Example:
s.Register("@hourly", fn, crontask.WithJitter(30*time.Second))
func WithJobID ¶
WithJobID sets an explicit, caller-supplied identifier for the job. If not provided, a random UUID-like identifier is generated automatically.
Example:
s.Register("@hourly", fn, crontask.WithJobID("price-refresh"))
func WithJobName ¶
WithJobName attaches a human-readable display name to the job. The name appears in the JobInfo returned by Jobs() and is useful for dashboards.
Example:
s.Register("@daily", fn, crontask.WithJobName("Daily Report"))
func WithMaxRetries ¶
WithMaxRetries configures the number of times the executor will retry a failing job before recording a final error. A value of 0 (the default) means the job is attempted exactly once with no retries.
Example:
s.Register("0 * * * *", fn, crontask.WithMaxRetries(3))
func WithTimeout ¶
WithTimeout sets a per-invocation execution deadline for the job. If a single execution does not complete within the specified duration, the context passed to the job function is cancelled and ErrJobTimeout is recorded.
Example:
s.Register("@minutely", fn, crontask.WithTimeout(10*time.Second))
type MetricsHookInstance ¶
type MetricsHookInstance struct {
NoopHooks
// contains filtered or unexported fields
}
MetricsHookInstance is the concrete type returned by MetricsHook. It accumulates counters and duration totals using atomic operations and exposes them through accessor methods that are safe for concurrent use.
Typical usage is to retain a reference to the instance so that metrics can be scraped periodically (e.g. by a Prometheus collector or a /metrics HTTP handler):
m := crontask.MetricsHook()
s.Register("@hourly", fn, crontask.WithHooks(m))
// Elsewhere, in a metrics handler:
successes := m.SuccessCount()
failures := m.FailureCount()
func MetricsHook ¶
func MetricsHook() *MetricsHookInstance
MetricsHook returns a *MetricsHookInstance that implements Hooks, RetryHook, and PanicHook. Counters and durations are updated atomically and can be read at any time from any goroutine without external synchronisation.
Example:
m := crontask.MetricsHook()
s, _ := crontask.New(crontask.WithSchedulerHooks(m))
// ... later ...
fmt.Printf("successes=%d failures=%d", m.SuccessCount(), m.FailureCount())
func (*MetricsHookInstance) FailureCount ¶
func (m *MetricsHookInstance) FailureCount() int64
FailureCount returns the total number of failed job invocations (after all retries are exhausted).
func (*MetricsHookInstance) OnPanic ¶
func (m *MetricsHookInstance) OnPanic(_ context.Context, _ string, _ any)
OnPanic implements PanicHook for MetricsHookInstance.
func (*MetricsHookInstance) PanicCount ¶
func (m *MetricsHookInstance) PanicCount() int64
PanicCount returns the total number of job invocations that panicked.
func (*MetricsHookInstance) SuccessCount ¶
func (m *MetricsHookInstance) SuccessCount() int64
SuccessCount returns the total number of successful job invocations.
func (*MetricsHookInstance) TotalDuration ¶
func (m *MetricsHookInstance) TotalDuration() time.Duration
TotalDuration returns the cumulative execution time across all invocations.
type NoopHooks ¶
type NoopHooks struct{}
NoopHooks is a zero-value implementation of Hooks whose methods all do nothing. Embed NoopHooks into your own struct to selectively override only the methods you care about.
Example:
type MyHooks struct {
crontask.NoopHooks
}
func (h *MyHooks) OnFailure(_ context.Context, id string, _ time.Duration, err error) {
log.Printf("ALERT: job %s failed: %v", id, err)
}
func (NoopHooks) OnComplete ¶
OnComplete implements Hooks. It does nothing.
type PanicHook ¶
type PanicHook interface {
// OnPanic is called when the job function panics. recovered is the value
// passed to panic(). The job is marked as failed after OnPanic returns.
OnPanic(ctx context.Context, jobID string, recovered any)
}
PanicHook is an optional extension to the Hooks interface. If a Hooks implementation also satisfies PanicHook, the executor calls OnPanic when the job function panics, before re-recording the result. This enables structured panic reporting without crashing the process.
Example:
type MyHooks struct {
crontask.NoopHooks
}
func (h *MyHooks) OnPanic(_ context.Context, id string, recovered any) {
log.Printf("CRITICAL: job %s panicked: %v", id, recovered)
sentry.CaptureException(fmt.Errorf("panic in job %s: %v", id, recovered))
}
type RetryHook ¶
type RetryHook interface {
// OnRetry is called after a failed attempt when at least one retry remains.
// attempt is the one-based attempt number that just failed.
OnRetry(ctx context.Context, jobID string, attempt int, err error)
}
RetryHook is an optional extension to the Hooks interface. If a Hooks implementation also satisfies RetryHook, the executor calls OnRetry after each failed attempt that will be retried (i.e., not the final attempt). This allows retry-specific logging and alerting without polling OnFailure.
Example:
type MyHooks struct {
crontask.NoopHooks
}
func (h *MyHooks) OnRetry(_ context.Context, id string, attempt int, err error) {
log.Printf("job %s: attempt %d failed, will retry: %v", id, attempt, err)
}
type Schedule ¶
type Schedule interface {
// Next returns the next activation time after the given reference time t.
// If no further activation exists (e.g. a once-only schedule in the past),
// Next returns the zero time.Time.
Next(t time.Time) time.Time
}
Schedule is the interface implemented by any type that can compute the next activation time for a scheduled job. The single method Next receives the current (or reference) time and returns the earliest future time at which the job should run next.
Implementing Schedule allows users to inject custom scheduling logic — for example, a schedule driven by an external calendar API — without forking the package.
func Parse ¶
Parse converts a cron expression string into a Schedule that can be used to compute successive activation times.
Supported formats
- Five fields: "minute hour day-of-month month day-of-week"
- Six fields: "second minute hour day-of-month month day-of-week"
- @alias: see the Alias constants for the full list
- @every <d>: interval expression, e.g. "@every 5m"
Field syntax (per field)
- * — every value in the valid range
- n — exact value
- n-m — inclusive range
- n-m/step — range with step
- */step — every step values across the full range
- a,b,c — comma-separated list (each element may itself use any of the above forms)
Month and day-of-week fields additionally accept three-letter English abbreviations (jan-dec and sun-sat respectively), case-insensitive.
Timezone ¶
An optional IANA timezone specifier may appear at the front of the expression, separated from the fields by a space:
"TZ=America/New_York 0 9 * * 1-5"
When a timezone is provided, the returned Schedule activates at the specified local time. When omitted, UTC is used.
type Scheduler ¶
type Scheduler struct {
// contains filtered or unexported fields
}
Scheduler is the primary type exposed by the crontask package. It manages job registration, the scheduler loop, and graceful shutdown.
Create a Scheduler with New; do not use the zero value directly.
All methods on Scheduler are safe for concurrent use from multiple goroutines.
func New ¶
func New(opts ...SchedulerOption) (*Scheduler, error)
New constructs and returns a new Scheduler configured by the supplied SchedulerOptions.
The scheduler is created in a stopped state; call Start to begin processing jobs.
Example:
s, err := crontask.New(
crontask.WithLocation(time.UTC),
crontask.WithSeconds(),
)
func (*Scheduler) Jobs ¶
Jobs returns a snapshot of all registered jobs in an unspecified order. Each element is an immutable JobInfo that reflects the state at the time of the call.
func (*Scheduler) Location ¶
Location returns the timezone that this scheduler uses to compute job next-run times. The value is the location supplied via WithLocation; it defaults to time.UTC when no location option is provided.
func (*Scheduler) NextRuns ¶
NextRuns returns the next n activation times for the job identified by id, starting from the given reference time t. It returns ErrJobNotFound when the id is not registered.
Example:
runs, err := s.NextRuns("my-job-id", time.Now(), 5)
func (*Scheduler) Register ¶
Register adds a new job to the scheduler. The job fires according to the supplied cron expression and calls fn each time it is due.
Register is safe to call both before and after Start. Jobs registered after Start will begin firing at their next scheduled time.
It returns the job's ID (which may be supplied via WithJobID) or an error if the expression is invalid or the scheduler has been shut down.
Example:
id, err := s.Register("0 * * * *", func(ctx context.Context) error {
fmt.Println("every hour")
return nil
}, crontask.WithJobName("Hourly ping"))
func (*Scheduler) Remove ¶
Remove unregisters the job with the given id. It returns ErrJobNotFound when the id is not present in the registry.
func (*Scheduler) Shutdown ¶
Shutdown stops the scheduler loop and blocks until the loop goroutine has exited or ctx expires. After Shutdown returns the Scheduler cannot be restarted.
Example:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := s.Shutdown(ctx); err != nil {
log.Printf("shutdown timed out: %v", err)
}
func (*Scheduler) Start ¶
Start begins the scheduler loop in a background goroutine. It returns ErrSchedulerRunning if the scheduler is already active or ErrSchedulerStopped if Shutdown has already been called.
func (*Scheduler) Stop ¶
func (s *Scheduler) Stop()
Stop halts the scheduler loop without waiting for in-flight jobs to complete. To wait for all running jobs to finish, use Shutdown instead.
func (*Scheduler) WithSecondsEnabled ¶
WithSecondsEnabled reports whether the Scheduler was constructed with the WithSeconds option, meaning it accepts six-field cron expressions and ticks at one-second granularity.
type SchedulerOption ¶
type SchedulerOption func(*schedulerConfig)
SchedulerOption is a functional option applied to a Scheduler at construction time via New.
func WithErrorHandler ¶
func WithErrorHandler(fn func(id string, err error)) SchedulerOption
WithErrorHandler registers a callback that is invoked synchronously by the executor whenever a job returns a non-nil error (after all retries are exhausted). The callback receives the job ID and the final error.
Example:
s, _ := crontask.New(crontask.WithErrorHandler(func(id string, err error) {
log.Printf("job %s failed: %v", id, err)
}))
func WithLocation ¶
func WithLocation(loc *time.Location) SchedulerOption
WithLocation sets the default timezone for the scheduler. Jobs that do not carry their own timezone specifier will have their next-run times computed relative to loc.
Example:
tz, _ := time.LoadLocation("Europe/Paris")
s, _ := crontask.New(crontask.WithLocation(tz))
func WithSchedulerHooks ¶
func WithSchedulerHooks(hooks ...Hooks) SchedulerOption
WithSchedulerHooks registers one or more default Hooks that are applied to every job that does not supply its own per-job hooks via WithHooks. This provides a convenient way to enable logging, metrics, or other observability for all jobs in one place at scheduler construction time.
When multiple hooks are supplied they are composed into a chain (equivalent to calling ChainHooks). Nil values are silently ignored.
Per-job hooks set via WithHooks always take precedence over the scheduler- level default. To combine both, call ChainHooks explicitly:
s.Register("@daily", fn, crontask.WithHooks(
crontask.ChainHooks(schedulerDefaultHooks, myJobSpecificHook),
))
Example:
s, _ := crontask.New(
crontask.WithSchedulerHooks(
crontask.LoggingHook(),
crontask.MetricsHook(),
),
)
func WithSeconds ¶
func WithSeconds() SchedulerOption
WithSeconds enables the six-field cron format where the first field represents seconds. When this option is set, Parse is called in six-field mode and the scheduler ticks at one-second granularity.
Example:
s, _ := crontask.New(crontask.WithSeconds())