cronjob

package
v0.0.0-...-8c930d8 Latest Latest
Warning

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

Go to latest
Published: Nov 11, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

Documentation

Overview

Package cronjob runs the jobs a project registers on a schedule.

A job is a func(ctx) error registered from a package init function with a cron expression — six fields, seconds first — or a descriptor such as "@hourly" or "@every 5m". Importing the package is what enables scheduling: the scheduler joins the process lifecycle from init, starts once the tables are ready and stops as soon as the process begins to drain; a project that registers no job never links it.

Every schedule is read in UTC, and "@every" runs on the multiples of its period counted from the Unix epoch, so every replica of a deployment computes the same instants for a job; the period is a whole number of seconds, at least one, and a schedule that never fires is refused at registration. An expression that must follow another wall clock says so itself, with a CRON_TZ= prefix. The instants are read off each replica's own clock — the lease decides who runs one, on the database's clock — so the replicas' clocks must agree to within the schedule's granularity, as any clock-synchronized deployment's do: a replica running ahead claims an instant early, and the others find it taken when their clocks reach it.

Each instant of a job is claimed once across the deployment: the replicas share the instant's lease through the primary database (see the lease package), the first to claim it runs the round while the others skip the instant, and a round still holding the lease — a second run included — keeps the next instants from everyone. A job that must run on every replica — refreshing a process-local cache, cleaning a local directory — registers with RegisterPerInstance and runs without a lease; the lease table comes with the package all the same, and stays empty for such a project. A ClickHouse primary database cannot carry leases, so a job under a lease fails the start there.

A round counts once it has run to its end: the job returned nil or an error of its own, or panicked. A round cut short — the job returned the ending of its context, ctx.Err() or context.Cause(ctx), as the process shut down or the lease was lost, or never returned because its process crashed or failed — runs a second time, on whichever replica finds it first: every replica looks for such rounds every 15 seconds or so, with one query for all its jobs. A failure of the job's own returned beside the ending counts only joined with it by errors.Join: attached by errors.CombineErrors it goes unseen, and the round reads as cut short. A round a shutdown cut short gives its lease back as it stops, and runs again within about 15 seconds; one whose process died waits for its lease to expire first, up to 15 seconds more, and so does one that ran to its end but whose end the database failed to record. The rule, exactly: the round did not run to its end, or its end is not on record; the instant has not run a second time already (a second round cut short is given up, so a job that brings its process down cannot bring the replicas down one after another); and no later instant has been claimed since (the next instant starting first gives the unfinished one up, with a warning naming it). A job under a lease may therefore run twice for one instant, and must be idempotent: the second round starts over, whatever the first did before it was cut short.

On start-up the scheduler catches up the most recent instant of a job when no replica claimed it, which is what a rolling deployment or an outage owes the job. The rule, exactly: the job has run before (its lease row exists — a job never run starts with its next instant, the instants before its first deployment were never its to run); the most recent instant that passed lies within the last day (an older one is history, not a missed round); and no replica claimed that instant, nor did a round that ran to its end overrun it (such a round records, as it ends, the instants it overran, and they stay skipped). The catch-up is one round, on one replica, and never repeats an instant already claimed; cut short, it runs a second time like any other round.

Each job runs in a loop of its own: the loop waits for the next instant of the schedule, or for a round of the job cut short that its replica found, runs the job on a context that ends when the process begins shutting down or the lease is lost, then computes the next instant from the moment the run ended — an instant that passed while a run was still in flight is skipped, never piled on top of it, and a round that ran to its end puts the skip on record, so no replica starting afterwards catches the instant up. The parser is the cron library's; the loop is this package's, because the library's runtime neither tells a job which instant it runs for nor lets a test drive the clock.

A registration that cannot be honored — no name, no schedule, a schedule that does not parse or names the process's own zone, a name already taken — fails the process at startup rather than dropping the job: the name is the job's identity in every log line about it and the name of its lease.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Register

func Register(fn func(ctx context.Context) error, spec string, name string)

Register declares fn as the job named name, run on spec: a six-field cron expression, seconds first, or a descriptor such as "@hourly" or "@every 5m". Schedules are read in UTC — an expression that must follow another wall clock carries a CRON_TZ= prefix naming a fixed zone; Local, the process's own zone, is refused because replicas need not share it — and "@every" runs on the multiples of its period from the Unix epoch, so every replica computes the same instants; its period is a whole number of seconds, at least one, the resolution of the scheduler's instants, and a shorter, fractional, zero or negative one is a registration error, as is a schedule that never fires. Registration belongs in package init functions: the scheduler starts with the process, and a registration after that panics.

Each instant of the job is claimed once across the deployment: the replicas share the instant's lease through the primary database, the first to claim it runs the round, the others skip the instant, and a round still holding the lease keeps the next instants from everyone. A round cut short by a shutdown, a crash or a lost lease runs a second time, on this replica or another, unless the next instant starts first, so fn must be idempotent; on start-up the most recent instant that no replica claimed, and no round that ran to its end overran, is caught up once, when the job has run before and the instant lies within the last day. See the package documentation for both rules in full. A job that must run on every replica registers with RegisterPerInstance instead.

fn receives the context of the round it runs in. The context ends when the process begins shutting down or the round's lease is lost, so a long round can stop early, and a database.Transaction opened on it refuses to run once the lease is gone — a plain write is not checked, the context is what stops it; a round still running 5 seconds after its lease was lost fails the process, which exits without waiting for it — another replica may be running the job's next instant by then. The context carries the round's identity — the job name and a trace id of the round's own, see execctx — and, with tracing on, the round's root span, so every statement, log line and span the job produces is annotated with the round and can be found again from any of them. An instant that passes while the previous round is still in flight is skipped.

The framework opens a single connection to SQLite, so there a transaction of the job blocks the renewal of the round's lease: keep each transaction under 8 seconds, and under 5 when transactions run back to back — a longer one may hold the renewal back until the lease counts as lost, which ends the round; one over 10 seconds always does — or register work that only ever runs in one process with RegisterPerInstance.

A registration that cannot be honored — no name, no schedule, a schedule that does not parse or names the Local zone, a name already taken, a nil fn — fails the process at startup.

func RegisterPerInstance

func RegisterPerInstance(fn func(ctx context.Context) error, spec string, name string)

RegisterPerInstance is Register for a job every replica runs on its own, without a lease: work that belongs to the process, such as refreshing a process-local cache or cleaning a local directory. It never catches up an instant, nor runs a round cut short a second time. Everything else is as for Register.

Types

This section is empty.

Jump to

Keyboard shortcuts

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