Skip to content
zalgonoisePublic

About

This is my cron (micron); there are others like it but this one is mine. A cron library in Go.

Resources

Stars

3 stars

Watchers

1 watching

Forks

Repository files navigation

micron

This is my cron (micron); there are others like it but this one is mine. A cron-scheduler library in Go


Concept

micron is a Go library that allows adding cron-like scheduler(s) to Go apps, compatible with Unix's cron time/date strings, to execute actions within the context of the app.

By itself, cron is a fantastic tool released in the mid-70's, written in C, where the user defines a specification in a crontab, a file listing jobs with a reference of a time/date specification and a (Unix) command to execute.

Within Go, it provides the same set of features as the original binary, served in a library as a (blocking / background) pluggable service. This means full compatibility with cron strings for scheduling, support for multiple executable types or functions, and support for multiple job scheduling (with different time/date specifications). Additionally, the library extends this functionality to support definition of seconds' frequency in cron-strings, @reboot, @midnight, and @every <duration> descriptors, ? wildcards, a-b/step range-with-step expressions, runner middlewares (panic recovery, overlap protection, jitter, and retry), iter.Seq[time.Time] schedule iterators, context support, error returns, and full observability coverage (metrics, logs, and traces).


Motivation

In a work environment, we see cron many times, in different scenarios. From cron installations in bare-metal Linux servers to one-shot containers configured in Kubernetes deployments. Some use-cases are very simple, others complex, but the point is that it is a tool used for nearly 50 years at the time of writing.

But the original tool is a C binary that executes a Unix command. If we want to explore schedulers for Go applications (e.g. a script executed every # hours), this means that the app needs to be compiled as a binary, and then to configure a cron-job to execute the app in a command.

While this is fine, it raises the question -- what if I want to include it within the application? This should make a lot of sense to you if you're a fan of SQLite like me.

There were already two libraries with different implementations, in Go:

Don't get me wrong -- there is nothing inherently wrong about these implementations; I went through them carefully both for insight and to understand what could I explore differently. A very obvious change would be a more "modern" approach including a newer Go version (go1.27.1); which by itself includes log/slog, iter.Seq, sync.WaitGroup.Go, and all the other observability-related decorators that also leverage context.Context.

Another more obvious exploration path would be the parser logic, as I could use my generic lexer and generic parser in order to potentially improve it.

Lastly I could try to split the cron service's components to be more configurable even in future iterations, once I had decided on the general API for the library. There was enough ground to explore and to give it a go. :)

A personal project that I have for a Steam CLI app is currently using this cron library to regularly check for discounts in the Steam Store, for certain products on a certain frequency, as configured by the user.


Usage

Using micron is as layered and modular as you want it to be. This chapter describes how to use the library effectively.

Getting micron

You're able to fetch micron as a Go module by importing it in your project and running go mod tidy:

package main

import (
	"context"
	"fmt"

	"github.com/zalgonoise/micron/v3"
	"github.com/zalgonoise/micron/v3/executor"
)

func main() {
	fn := func(context.Context) error {
		fmt.Println("done!")

		return nil
	}

	c, err := micron.New(
		micron.WithJob("my-job", "* * * * *", fn, executor.WithRecover()),
	)
	if err != nil {
		panic(err)
	}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	go c.Run(ctx)
	// ...
}

Cron Runtime

The runtime is the component that controls how the module runs -- that is, controlling the flow of job selection and execution. The runtime allows cron to be executed as a goroutine, as its Runtime.Run method has no returns, and errors are channeled via its Runtime.Err method (which returns a receive-only error channel). The lifetime of the cron runtime is managed with a context.Context provided when calling Runtime.Run -- which can impose a cancellation or timeout strategy.

Creating a cron runtime starts with the micron.New constructor function:

func New(options ...cfg.Option[*Runtime]) (*Runtime, error)

This function accepts variadic cfg.Option[*Runtime] options, allowing full modularity in how you build your cron runtime, provided that it supplies either:

Below is a table with all the options available for creating a cron runtime:

Function Input Parameters Description
WithSelector sel Selector Configures the Runtime with the input Selector.
WithExecutors execs ...selector.Executor Configures the Runtime with pre-built executors, constructing a selector automatically if WithSelector is not set.
WithJob id, cron string, fn func(context.Context) error, execOpts ...cfg.Option[*executor.Executable] Registers a scheduled job directly on the Runtime with optional executor.Executable options.
WithBlockingSelector (none) Configures the Runtime to construct a selector.BlockingSelector when using WithExecutors or WithJob.
WithLocation loc *time.Location Sets the default time.Location used by jobs registered via WithJob.
WithErrorBufferSize size int Defines the capacity of the error channel exposed by Runtime.Err.
WithMetrics m Metrics Configures the Runtime with the input metrics registry.
WithLogger logger *slog.Logger Configures the Runtime with the input slog.Logger.
WithLogHandler handler slog.Handler Configures the Runtime with logging using the input slog.Handler.
WithTrace tracer trace.Tracer Configures the Runtime with the input OpenTelemetry trace.Tracer.

Cron Selector

This component is responsible for picking up the next job to execute, according to their schedule frequency. There are two implementations in the selector package:

  • Selector (selector.New(ctx, options...)): An asynchronous, non-blocking selector that runs each configured Executor in its own background goroutine, aggregating any errors atomically for retrieval via Next or Error.
  • BlockingSelector (selector.NewBlockingSelector(options...)): A synchronous selector that finds the nearest scheduled task(s) and blocks until they finish executing (running multiple same-time tasks concurrently via executor.Multi).
func New(ctx context.Context, options ...cfg.Option[*Config]) (*Selector, error)
func NewBlockingSelector(options ...cfg.Option[*Config]) (*BlockingSelector, error)

Below is a table with all the options available for creating a selector:

Function Input Parameters Description
WithExecutors executors ...Executor Configures the selector with the input Executor instances.
WithTimeout dur time.Duration Configures the poll/wait timeout duration on a non-blocking Selector.
WithClock clock Clock Configures a custom Clock implementation (defaults to time.Now).
WithMetrics m Metrics Configures the selector with the input metrics collector.
WithLogger logger *slog.Logger Configures the selector with the input slog.Logger.
WithLogHandler handler slog.Handler Configures the selector with a slog.Logger built from the input slog.Handler.
WithTrace tracer trace.Tracer Configures the selector with the input OpenTelemetry trace.Tracer.

Cron Executor

The Executable in package executor is the component that waits until the next scheduled timestamp and executes the configured Runner instances.

It exposes:

func New(id string, runners []Runner, options ...cfg.Option[*Executable]) (*Executable, error)

Below is a table with all the options available for creating an Executable:

Function Input Parameters Description
WithRunners runners ...Runner Appends one or more Runner instances to the executor.
WithRunnable fn func(context.Context) error Appends a function as a Runnable runner to the executor.
WithScheduler sched Scheduler Configures the executor with a custom or pre-built Scheduler.
WithSchedule cron string, loc *time.Location Parses cron (5/6-field cron, @reboot, @midnight, @every <duration>, etc.) and configures the scheduler.
WithTimeout dur time.Duration Sets the per-execution context timeout applied when invoking runners (default 10m).
WithRecover (none) Wraps all runners with panic recovery (ErrRecoveredPanic). Standalone decorator: Recover.
WithSkipIfRunning (none) Skips overlapping concurrent executions of a runner if the previous run is still active. Standalone decorator: SkipIfRunning.
WithJitter maxJitter time.Duration Adds a random delay in [0, maxJitter) before each runner execution. Standalone decorator: Jitter.
WithRetry maxRetries int, backoff time.Duration Retries failed runner executions up to maxRetries times with backoff delay. Standalone decorator: Retry.
WithMetrics m Metrics Configures the executor with the input metrics collector.
WithLogger logger *slog.Logger Configures the executor with the input slog.Logger.
WithLogHandler handler slog.Handler Configures the executor with a slog.Logger built from the input slog.Handler.
WithTrace tracer trace.Tracer Configures the executor with the input OpenTelemetry trace.Tracer.

Cron Scheduler

The schedule package provides three Scheduler implementations:

  • CronSchedule (schedule.New(options...)): Standard and extended cron schedule localized to a time.Location.
  • RebootSchedule (schedule.NewRebootSchedule(options...)): One-shot schedule (@reboot) that triggers immediately on its first call and returns time.Time{} thereafter.
  • IntervalSchedule (schedule.NewIntervalSchedule(interval, options...)): Fixed-duration interval schedule (@every <duration>).

Both CronSchedule and IntervalSchedule also expose:

  • Seq(ctx context.Context, from time.Time) iter.Seq[time.Time] — Go 1.23+ range-over-func iterator yielding upcoming execution times.
  • NextN(ctx context.Context, from time.Time, n int) []time.Time — returns a slice of the next n upcoming execution times.
func New(options ...cfg.Option[*CronSchedule]) (*CronSchedule, error)
func NewRebootSchedule(options ...cfg.Option[*RebootSchedule]) *RebootSchedule
func NewIntervalSchedule(interval time.Duration, options ...cfg.Option[*IntervalSchedule]) (*IntervalSchedule, error)

Below is a table with the options available for creating a CronSchedule:

Function Input Parameters Description
WithSchedule cron *cronlex.Schedule Configures the CronSchedule with a parsed or built *cronlex.Schedule.
WithCronString cron string Parses the cron expression string and configures the CronSchedule.
WithLocation loc *time.Location Configures the CronSchedule with the input time.Location (defaults to time.UTC).
WithObservability m Metrics, logger *slog.Logger, tracer trace.Tracer Configures metrics, logging, and tracing on the CronSchedule.

Programmatic schedule construction is also available via schedule/builder, including Every(), At(val), Between(from, to), RangeWithStep(from, to, step), and On(steps...).


Cron Schedule

Schedule is a data structure that holds a set of Resolvers, for each node, segment or unit of the schedule. This implementation focuses on the cron string specification with added support for a seconds definition (alongside minutes, hours, days-of-the-month, months and weekdays):

type Schedule struct {
	Sec      Resolver
	Min      Resolver
	Hour     Resolver
	DayMonth Resolver
	Month    Resolver
	DayWeek  Resolver
}

Schedule Resolver

The implementations of Resolver can be found in the resolve package. These are derived from parsing a cron string and assigned automatically when calling cronlex.Parse.

Supported cron syntax features include:

  • Every-value sequences using * (or ? in day-of-month and day-of-week fields)
  • Fixed values (e.g. 15, JAN, MON, 0 or 7 for Sunday)
  • Inclusive range value sequences using - (e.g. 0-15, MON-FRI)
  • Step value sequences using / (e.g. */3, 0/15)
  • Range-with-step sequences using - and / (e.g. 0-30/5)
  • Comma-separated lists combining values, ranges, and steps (e.g. 0-15/3,20,25)
  • Overrides: @reboot, @hourly, @daily, @midnight, @weekly, @monthly, @yearly, @annually, and @every <duration>

There are four Resolver types in schedule/resolve:

  • Everytime: Always returns 0 (matches any value).
  • FixedSchedule: Matches a single fixed value At within Max.
  • RangeSchedule: Matches any value inclusively within [From, To].
  • StepSchedule: Matches a sorted, deduplicated set of discrete Steps.

Schedule Parser

cronlex.Parse consumes a cron string and returns a *Schedule and an error:

func Parse(cron string) (*Schedule, error)
func ParseInterval(cron string) (time.Duration, error)

This is a process broken down in three phases built on zalgonoise/lex and zalgonoise/parse:

  • A lexer (StateFunc), that consumes bytes from the cron string and emits Token items.
  • A parser (ParseFunc), which consumes tokens and builds an abstract syntax tree.
  • A validator and processor (Validate and ProcessFunc), which validates all nodes and bounds and constructs the *Schedule.

Example

A working example is the Steam CLI app mentioned in the Motivation section above. This application exposes some commands, one of them being monitor. This file provides some insight on how the cron service is set up from a main.go / script-like approach.

You can also take a look at its runner.go file, that implements the executor.Runner interface.


Disclaimer

This is not a one-size-fits-all solution! Please take your time to evaluate it for your own needs with due diligence. While having a library for this and a library for that is pretty nice, it could potentially be only overhead hindering the true potential of your app! Be sure to read the code that you are using to be a better judge if it is a good fit for your project. With that in mind, I hope you enjoy this library. Feel free to contribute by filing either an issue or a pull request.

About

This is my cron (micron); there are others like it but this one is mine. A cron library in Go.

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages