jobs

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Nov 5, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package jobs makes scheduled jobs safe to run from several processes and visible when they stop working.

A Guard wraps each run of a job. Before the job starts it takes a Postgres advisory lock named after the job, so a job never runs twice at the same time, whether the second run comes from another process or from the same scheduler firing again while the previous run is still going. After the run it records the outcome in a table (job_runs by default): last start, last success, last error, consecutive failures, duration and skips. Statuses and Handler turn that table into a health report, so a job that silently stopped succeeding shows up even when it never logs an error.

The Guard is scheduler-agnostic: call Run from gocron, robfig/cron, a time.Ticker or anything else.

Index

Constants

This section is empty.

Variables

View Source
var ErrSkipped = errors.New("jobs: another run of this job holds the lock")

ErrSkipped is returned by Run when another run of the same job holds the lock. The job body was not called.

Functions

func Interval

func Interval(schedule string, now time.Time) (time.Duration, error)

Interval is the time between two consecutive runs of schedule after now.

Types

type Guard

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

Guard runs jobs under a per-job lock and records every run.

func New

func New(db *sql.DB, opts ...Option) (*Guard, error)

New returns a Guard over db, which must be a Postgres database.

func (*Guard) Handler

func (g *Guard) Handler() http.Handler

Handler serves the health summary: 200 {"status":"ok",...} when every job is healthy, 503 {"status":"degraded",...} otherwise, and 503 {"status":"db_unavailable"} when the table cannot be read. It reports counts only, never job names or errors, so it is safe to expose publicly.

func (*Guard) Migrate

func (g *Guard) Migrate(ctx context.Context) error

Migrate creates the runs table if it does not exist. It is safe to call from every process at startup.

func (*Guard) Run

func (g *Guard) Run(ctx context.Context, name, schedule string, fn func(context.Context) error) (err error)

Run runs fn under the job's lock and records the outcome. It returns ErrSkipped without calling fn when another run holds the lock, the error from taking the lock if the database is unreachable, and otherwise fn's error. If fn panics, the run is recorded as failed, the lock is released and the panic continues.

schedule is the job's cron expression ("*/5 * * * *", "@every 10m", "@daily", ...). It is stored with the run and used to decide when the job counts as stale.

func (*Guard) Statuses

func (g *Guard) Statuses(ctx context.Context, now time.Time) ([]Status, error)

Statuses reads every recorded job and judges it at now. A missing table reads as no jobs.

type Hooks

type Hooks struct {
	// Start is called after the lock is taken, right before the job body.
	Start func(name, schedule string)
	// Finish is called after the run has been recorded. err is the job's
	// error, or an error wrapping the panic value if the job panicked.
	Finish func(name, schedule string, took time.Duration, err error)
	// Skip is called when a run is skipped because the lock is held.
	Skip func(name string)
}

Hooks let a service attach its own monitoring, such as cron check-ins in an error tracker. Every field is optional.

type Option

type Option func(*Guard) error

Option configures a Guard.

func WithHooks

func WithHooks(h Hooks) Option

WithHooks attaches monitoring callbacks.

func WithLockNamespace

func WithLockNamespace(ns int32) Option

WithLockNamespace sets the first key of every advisory lock the Guard takes, keeping its locks apart from other advisory lock users in the same database. Default 7275.

func WithLogger

func WithLogger(logf func(format string, args ...any)) Option

WithLogger sets where the Guard reports its own problems (a failed lock or record), at most once a minute per job and kind. Default log.Printf.

func WithRules

func WithRules(r Rules) Option

WithRules overrides the thresholds used by Statuses and Handler.

func WithTable

func WithTable(name string) Option

WithTable sets the table runs are recorded in. Default "job_runs".

type Rules

type Rules struct {
	// A job is stale once it has gone two of its intervals plus StaleMargin
	// without a successful run. Default 10 minutes.
	StaleMargin time.Duration
	// A job is failing after this many failed runs in a row. Default 3.
	FailingAfter int
	// A job that has not even started for this long was removed from the
	// code and is left out of the health summary. Default 30 days.
	RetiredAfter time.Duration
}

Rules decide when a job is unhealthy. Zero fields take the defaults.

type State

type State string

State is a job's health as judged by the Guard's Rules.

const (
	StateOK      State = "ok"
	StateStale   State = "stale"   // no success for two intervals plus the margin
	StateFailing State = "failing" // failed FailingAfter times in a row
	StateRetired State = "retired" // not started for RetiredAfter; the job is gone
)

type Status

type Status struct {
	Name                string        `json:"name"`
	Schedule            string        `json:"schedule"`
	State               State         `json:"state"`
	LastStartedAt       time.Time     `json:"last_started_at"`
	LastSuccessAt       *time.Time    `json:"last_success_at,omitempty"`
	LastErrorAt         *time.Time    `json:"last_error_at,omitempty"`
	LastError           string        `json:"last_error,omitempty"`
	LastDuration        time.Duration `json:"last_duration_ns"`
	ConsecutiveFailures int64         `json:"consecutive_failures"`
	Runs                int64         `json:"runs"`
	SkippedLocked       int64         `json:"skipped_locked"`
}

Status is the recorded state of one job.

type Summary

type Summary struct {
	Jobs    int `json:"jobs"`
	Stale   int `json:"stale"`
	Failing int `json:"failing"`
}

Summary counts jobs by state.

func Summarize

func Summarize(statuses []Status) Summary

Summarize counts statuses, leaving retired jobs out.

func (Summary) Healthy

func (s Summary) Healthy() bool

Healthy is true when no job is stale or failing.

Jump to

Keyboard shortcuts

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