Documentation
¶
Overview ¶
Package jobq is a Postgres-backed background job queue.
Delivery is at-least-once: every job runs to completion or is discarded after its final attempt, but after a crash a job may run more than once. Handlers must therefore be idempotent.
Index ¶
- Variables
- func DefaultBackoff(attempt int) time.Duration
- func Handle[T any](w *Worker, kind string, fn func(ctx context.Context, job *Job, args T) error) error
- func Permanent(err error) error
- type AttemptError
- type Client
- func (c *Client) CancelJob(ctx context.Context, id int64) error
- func (c *Client) DeleteFinishedJobs(ctx context.Context, state string, olderThan time.Duration) (int64, error)
- func (c *Client) Enqueue(ctx context.Context, kind string, args any, opts *EnqueueOpts) (int64, error)
- func (c *Client) EnqueueTx(ctx context.Context, tx pgx.Tx, kind string, args any, opts *EnqueueOpts) (int64, error)
- func (c *Client) GetJob(ctx context.Context, id int64) (JobInfo, error)
- func (c *Client) ListJobs(ctx context.Context, f ListFilter) ([]JobInfo, error)
- func (c *Client) QueueStats(ctx context.Context) ([]QueueStat, error)
- func (c *Client) RetryJob(ctx context.Context, id int64) error
- type EnqueueOpts
- type HandlerFunc
- type Job
- type JobInfo
- type ListFilter
- type PeriodicJob
- type QueueStat
- type Worker
- type WorkerConfig
- type WorkerStats
Constants ¶
This section is empty.
Variables ¶
var ( ErrJobNotFound = errors.New("jobq: job not found") ErrJobRunning = errors.New("jobq: job is running") ErrJobFinished = errors.New("jobq: job has already finished") )
var ( ErrEmptyKind = errors.New("jobq: kind is required") ErrEmptyArgs = errors.New("jobq: args is required") ErrInvalidMaxAttempts = errors.New("jobq: invalid max attempts value") // ErrDuplicateJob means a job with the same UniqueKey is already waiting // or running, so no new job was enqueued. ErrDuplicateJob = errors.New("jobq: a job with this unique key is already queued or running") )
var ErrNilPool = errors.New("jobq: NewClient called with nil")
Functions ¶
func DefaultBackoff ¶
DefaultBackoff is how long to wait before retrying a job that has failed `attempt` times: 1s, 2s, 4s, 8s... doubling up to a cap of one hour.
The result is randomised between half and all of that ("jitter"), so that many jobs failing at the same moment, say because an email provider went down, don't all retry at the same moment and knock it over again.
func Handle ¶
func Handle[T any](w *Worker, kind string, fn func(ctx context.Context, job *Job, args T) error) error
Handle registers a typed handler: the job's JSON args are decoded into T before fn is called, so handlers never deal with raw JSON.
jobq.Handle(w, "send_email", func(ctx context.Context, job *jobq.Job, args EmailArgs) error {
return mailer.Send(ctx, args.To)
})
It's a function rather than a method because Go methods can't have their own type parameters.
Types ¶
type AttemptError ¶
type AttemptError struct {
Attempt int `json:"attempt"`
At time.Time `json:"at"`
Error string `json:"error"`
}
AttemptError is one failed attempt in a job's history.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
func (*Client) CancelJob ¶
CancelJob discards a job that hasn't started yet. Running jobs can't be cancelled this way: stop them through your own handler logic.
func (*Client) DeleteFinishedJobs ¶
func (c *Client) DeleteFinishedJobs(ctx context.Context, state string, olderThan time.Duration) (int64, error)
DeleteFinishedJobs deletes jobs in state "completed" or "discarded" that finished more than olderThan ago, in batches, and returns how many it deleted. Workers' leaders call it automatically (see WorkerConfig).
func (*Client) Enqueue ¶
func (c *Client) Enqueue(ctx context.Context, kind string, args any, opts *EnqueueOpts) (int64, error)
Enqueue adds a job to the queue and returns its ID.
func (*Client) EnqueueTx ¶
func (c *Client) EnqueueTx(ctx context.Context, tx pgx.Tx, kind string, args any, opts *EnqueueOpts) (int64, error)
EnqueueTx adds a job as part of the caller's transaction. The job exists only if tx commits, so it can't run for a write that was rolled back, and can't be lost for a write that was committed:
tx, _ := pool.Begin(ctx)
defer tx.Rollback(ctx)
userID := createUser(ctx, tx, form)
client.EnqueueTx(ctx, tx, "send_welcome_email", WelcomeArgs{UserID: userID}, nil)
tx.Commit(ctx) // user and job are saved together, or not at all
Workers are notified when the transaction commits, not before.
func (*Client) QueueStats ¶
QueueStats returns counts per queue.
type EnqueueOpts ¶
type EnqueueOpts struct {
Queue string // zero value: "default"
Priority int16 // zero value: 0
RunAt time.Time // zero value: "run now"
MaxAttempts int // zero value: 20 (defaultMaxAttempts)
// UniqueKey, if set, prevents duplicates: while a job with this key is
// available or running, enqueuing another returns ErrDuplicateJob. Once
// it completes or is discarded, the key can be used again.
// Zero value: no uniqueness check.
UniqueKey string
}
type HandlerFunc ¶
HandlerFunc runs one job. Returning an error marks the attempt as failed.
type JobInfo ¶
type JobInfo struct {
ID int64
Queue string
Kind string
State string // available, running, completed or discarded
Args json.RawMessage
Priority int16
Attempt int
MaxAttempts int
RunAt time.Time
LockedBy string // the worker running it, if any
CreatedAt time.Time
FinalizedAt *time.Time
Errors []AttemptError
}
JobInfo is everything stored about a job, for inspection and admin tools.
type ListFilter ¶
ListFilter selects jobs for ListJobs. Empty fields match everything.
type PeriodicJob ¶
type PeriodicJob struct {
// Name identifies the schedule, e.g. "nightly-report". Every worker that
// defines the same Name shares one schedule.
Name string
// Every is the interval. Slots are aligned to multiples of Every since
// the Unix epoch (UTC): Every: time.Hour runs on the hour, and
// 24*time.Hour at midnight UTC.
Every time.Duration
Kind string
Args any
Opts *EnqueueOpts
}
PeriodicJob is a job enqueued automatically once per interval.
type QueueStat ¶
type QueueStat struct {
Queue string
Ready int64 // available and due now
Scheduled int64 // available, due later (including retries in backoff)
Running int64
Completed int64
Discarded int64
Lag time.Duration
}
QueueStat summarises one queue. Lag is how long the oldest runnable job has been waiting: the number to alert on, since a growing lag means workers aren't keeping up.
type Worker ¶
type Worker struct {
// contains filtered or unexported fields
}
Worker claims jobs from one queue and runs them, up to Concurrency at once.
func NewWorker ¶
func NewWorker(client *Client, workerConfig *WorkerConfig) *Worker
func (*Worker) AddPeriodic ¶
func (w *Worker) AddPeriodic(p PeriodicJob) error
AddPeriodic schedules a job to be enqueued once per interval. Call it before Run. Any number of workers may define the same periodic job: each time slot is enqueued exactly once, however many of them are running.
func (*Worker) IsLeader ¶
IsLeader reports whether this worker currently holds leadership, and so runs the database-wide maintenance (the rescuer, and cleanup of old jobs).
func (*Worker) Register ¶
func (w *Worker) Register(kind string, handler HandlerFunc) error
Register adds the handler for a kind of job. Call it before Run.
func (*Worker) Run ¶
Run claims and runs jobs until ctx is cancelled, then shuts down gracefully: it stops claiming, lets running jobs finish (for up to ShutdownTimeout), records their outcomes, and returns.
It returns an error only if some jobs were still running after the shutdown timeout and a further grace period. Those jobs' leases then expire and another worker retries them.
func (*Worker) Stats ¶
func (w *Worker) Stats() WorkerStats
Stats returns the worker's counters. Safe to call at any time.
type WorkerConfig ¶
type WorkerConfig struct {
Queue string // zero value: "default"
ID string // zero value: "<hostname>-<pid>"
PollInterval time.Duration // zero value: 1s
Concurrency int // zero value: 10 jobs at once
// Backoff returns how long to wait before retrying a job that has failed
// `attempt` times. Zero value: DefaultBackoff.
Backoff func(attempt int) time.Duration
// LeaseDuration is how long a claimed job stays reserved without a
// heartbeat. The worker extends it every LeaseDuration/3 while the job
// runs; if the worker dies, the job is rescued once it expires.
// Zero value: 30s.
LeaseDuration time.Duration
// RescueInterval is how often to look for jobs with expired leases.
// Zero value: LeaseDuration/2.
RescueInterval time.Duration
// ShutdownTimeout is how long Run waits, once its context is cancelled,
// for running jobs to finish on their own. After that, their contexts are
// cancelled too. Zero value: 30s.
ShutdownTimeout time.Duration
// ElectionInterval is how often a worker tries to become leader, and how
// often the leader checks it still is. One worker per database leads and
// runs the rescuer and cleaner. Zero value: 5s.
ElectionInterval time.Duration
// Logger receives structured logs: failed and discarded jobs, rescued
// jobs, leadership changes, connection problems. Zero value: slog.Default().
Logger *slog.Logger
// The leader deletes finished jobs once they're older than these.
// Zero values: 24h for completed, 7 days for discarded (kept longer, as
// they're the ones worth investigating). A negative value keeps them
// forever.
CompletedRetention time.Duration
DiscardedRetention time.Duration
// CleanupInterval is how often the leader deletes old jobs. Zero value: 5m.
CleanupInterval time.Duration
}
WorkerConfig configures a Worker. Every zero value means "use the default".
type WorkerStats ¶
type WorkerStats struct {
Running int // jobs executing right now
Completed int64 // jobs that succeeded
Retried int64 // failed attempts that will be retried
Discarded int64 // jobs given up on
Panics int64 // handler panics (also counted as retried or discarded)
IsLeader bool
}
WorkerStats are counters since the worker was created, for metrics. See the README for exporting them to Prometheus.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
demo
command
Command demo is a small, complete jobq app: it migrates the database, registers a typed handler and a periodic job, enqueues some work, and runs a worker until you press Ctrl-C (or the process gets SIGTERM).
|
Command demo is a small, complete jobq app: it migrates the database, registers a typed handler and a periodic job, enqueues some work, and runs a worker until you press Ctrl-C (or the process gets SIGTERM). |
|
jobqbench
command
Command jobqbench measures jobq's throughput: it inserts N no-op jobs, runs W workers against them, and reports jobs per second.
|
Command jobqbench measures jobq's throughput: it inserts N no-op jobs, runs W workers against them, and reports jobs per second. |
|
jobqctl
command
Command jobqctl inspects and manages a jobq database.
|
Command jobqctl inspects and manages a jobq database. |
|
internal
|
|
|
testdb
Package testdb gives each test its own throwaway Postgres database, so tests can run in parallel without seeing each other's jobs.
|
Package testdb gives each test its own throwaway Postgres database, so tests can run in parallel without seeing each other's jobs. |
|
Package store owns the database schema and every SQL query jobq runs.
|
Package store owns the database schema and every SQL query jobq runs. |