Documentation
¶
Overview ¶
Package queue runs typed background jobs. A job is a struct with a Kind; its handler is a plain function registered explicitly:
type SendInvoice struct{ OrderID int64 }
func (SendInvoice) Kind() string { return "send_invoice" }
q.Register(queue.Handle(func(ctx context.Context, j SendInvoice) error { ... },
queue.Retries(5), queue.Backoff(queue.Exponential(time.Second)), queue.Timeout(time.Minute)))
err := q.Dispatch(ctx, SendInvoice{OrderID: 1}, queue.OnQueue("billing"), queue.Delay(5*time.Minute))
A handler whose failure retrying cannot fix wraps ErrPermanent to skip its remaining retries; AttemptFrom tells it which attempt it is on and whether that is the last one.
Payloads are JSON. Drivers store messages: Sync runs jobs inline, Memory keeps them in process, the database driver (and Redis) persist them. Queue.Work runs a worker (vukem queue:work); Fake records dispatched jobs in tests. Apps get a queue from app.Queue(), configured by QUEUE_CONNECTION.
See https://github.com/AlexGalhardo/vukem/blob/main/docs/subsystems.md#queue.
Example ¶
package main
import (
"context"
"fmt"
"log/slog"
"time"
"github.com/AlexGalhardo/vukem/queue"
)
type SendInvoice struct{ OrderID int64 }
func (SendInvoice) Kind() string { return "send_invoice" }
func main() {
ctx := context.Background()
// Sync runs jobs inline; apps use the database or Redis driver and a
// worker (vukem queue:work).
q := queue.New(queue.Sync{}, slog.New(slog.DiscardHandler))
err := q.Register(queue.Handle(func(_ context.Context, j SendInvoice) error {
fmt.Println("sending invoice for order", j.OrderID)
return nil
}, queue.Retries(5), queue.Backoff(queue.Exponential(time.Second))))
if err != nil {
fmt.Println(err)
return
}
err = q.Dispatch(ctx, SendInvoice{OrderID: 1}, queue.OnQueue("billing"))
fmt.Println(err)
}
Output: sending invoice for order 1 <nil>
Index ¶
- Constants
- Variables
- func Constant(d time.Duration) func(int) time.Duration
- func Dispatched[J Job](f *Fake) []J
- func Exponential(base time.Duration) func(int) time.Duration
- func Migrations(o DatabaseOptions) []schema.Migration
- type Attempt
- type Clock
- type Database
- func (d *Database) Ack(ctx context.Context, m *Message) error
- func (d *Database) CreateTables(ctx context.Context) error
- func (d *Database) Fail(ctx context.Context, m *Message) error
- func (d *Database) Failed(ctx context.Context) ([]FailedJob, error)
- func (d *Database) Forget(ctx context.Context, ids ...string) (int, error)
- func (d *Database) Pending(ctx context.Context) (int, error)
- func (d *Database) Pop(ctx context.Context, queues []string) (*Message, error)
- func (d *Database) Push(ctx context.Context, m *Message) error
- func (d *Database) PushTx(ctx context.Context, ex database.Executor, m *Message) error
- func (d *Database) Release(ctx context.Context, m *Message) error
- func (d *Database) Retry(ctx context.Context, ids ...string) (int, error)
- func (d *Database) SetClock(now func() time.Time)
- type DatabaseOptions
- type Driver
- type FailedJob
- type FailedStore
- type Fake
- type Inline
- type Job
- type JobEvent
- type JobStatus
- type Memory
- func (d *Memory) Ack(_ context.Context, m *Message) error
- func (d *Memory) Fail(_ context.Context, m *Message) error
- func (d *Memory) Failed() []Message
- func (d *Memory) Pending() int
- func (d *Memory) Pop(_ context.Context, queues []string) (*Message, error)
- func (d *Memory) Push(_ context.Context, m *Message) error
- func (d *Memory) Release(_ context.Context, m *Message) error
- func (d *Memory) SetClock(now func() time.Time)
- type Message
- type Observer
- type Option
- type Queue
- func (q *Queue) AddObserver(obs Observer)
- func (q *Queue) Dispatch(ctx context.Context, j Job, opts ...Option) error
- func (q *Queue) DispatchKind(ctx context.Context, kind string, payload any, opts ...Option) error
- func (q *Queue) DispatchKindTx(ctx context.Context, ex database.Executor, kind string, payload any, ...) error
- func (q *Queue) DispatchTx(ctx context.Context, ex database.Executor, j Job, opts ...Option) error
- func (q *Queue) Driver() Driver
- func (q *Queue) Message(j Job, opts ...Option) (*Message, error)
- func (q *Queue) Process(ctx context.Context, m *Message) error
- func (q *Queue) Register(regs ...Registration) error
- func (q *Queue) SetClock(now func() time.Time)
- func (q *Queue) SetObserver(obs Observer)
- func (q *Queue) Work(ctx context.Context, o WorkOptions) error
- type Registration
- type Sync
- type TxPusher
- type WorkOptions
Examples ¶
Constants ¶
const ( DefaultTable = "jobs" DefaultFailedTable = "failed_jobs" )
DefaultTable and DefaultFailedTable are the tables the queue owns when DatabaseOptions leaves them empty. They are listed with every other table vukem owns in docs/database.md#tables-the-framework-owns; set QUEUE_TABLE and QUEUE_FAILED_TABLE to move them out of the way of an app's own tables.
const MaxFreeInterruptions = 3
MaxFreeInterruptions is how many shutdowns in a row may interrupt a job without using one of its attempts (see WorkOptions.StopTimeout).
const MigrationID = "20261018000000_vukem_create_queue_tables"
MigrationID is the id of the migration Migrations returns; it names the framework's own migration in vukem migrate:status.
Variables ¶
var ErrDuplicateKind = errors.New("queue: job kind already registered")
ErrDuplicateKind is returned by Register for a kind registered twice.
var ErrInlineDelay = errors.New(
"queue: the sync driver cannot delay jobs (use QUEUE_CONNECTION=memory or database, or queue.Fake in tests)",
)
ErrInlineDelay is returned when a delayed job is dispatched to an Inline driver (Sync): running it now would run it early, so it is refused.
var ErrNoTx = errors.New("queue: driver does not support DispatchTx")
ErrNoTx is returned by DispatchTx when ex is neither a *database.DB nor a *database.Tx and the driver cannot enqueue inside a transaction.
var ErrPermanent = errors.New("queue: permanent failure, will not be retried")
ErrPermanent marks a failure that retrying cannot fix (a corrupt payload, a deleted row, a rejected input). A handler wraps it in its own error:
if errors.Is(err, image.ErrFormat) {
return fmt.Errorf("convert %d: %w: %w", j.ID, err, queue.ErrPermanent)
}
The job skips its remaining retries and moves to the failed jobs with that error, so it stays visible to queue:failed and queue:retry instead of disappearing. With an Inline driver (Sync) there are no retries to skip and the error reaches the caller unchanged.
var ErrReservationLost = errors.New("queue: job reservation lost to another worker")
ErrReservationLost is returned by Ack, Release and Fail when the job's reservation expired and another worker reserved it again: the job belongs to that worker now, so the stale result is dropped.
var ErrUnknownKind = errors.New("queue: no handler for job kind")
ErrUnknownKind is returned by Dispatch for a kind without a handler.
Functions ¶
func Dispatched ¶
Dispatched returns the dispatched jobs of type J, decoded, in order.
func Exponential ¶
Exponential backs off base, 2·base, 4·base... capped at one hour.
func Migrations ¶ added in v0.29.0
func Migrations(o DatabaseOptions) []schema.Migration
Migrations returns the migration that creates the queue tables named by o, so an app can manage them with vukem migrate instead of leaving them to the boot-time CreateTables:
vukem.WithMigrations(slices.Concat(
stores.Migrations(stores.Options{}),
migrations.All(),
))
The migration runs the same statements as CreateTables, which stays as the fallback for apps that do not register it. On a database where the tables already exist (the driver created them before the app opted in) the migration records itself and changes nothing, so an upgrade never fails on a conflicting CREATE TABLE; Down drops both tables.
Types ¶
type Attempt ¶ added in v0.29.0
type Attempt struct {
// Number is the 1-based attempt number (1 on the first run).
Number int
// MaxRetries is how many retries the job was dispatched with, so the
// job gets at most MaxRetries+1 attempts. It is 0 with an Inline
// driver (Sync), which does not retry.
MaxRetries int
}
Attempt is what a handler knows about the attempt it is running; read it with AttemptFrom.
func AttemptFrom ¶ added in v0.29.0
AttemptFrom returns the attempt the handler is running on, and whether there is one: it is false outside a queue handler (a handler function called directly, say from a test).
Check the second value before using the first. A zero Attempt is Number 0 and MaxRetries 0, so Last reports 0 > 0, which is false: it reads as "not the last attempt", and a handler that settles the work it owns only on Last would never settle it when called outside a queue.
func (Attempt) Last ¶ added in v0.29.0
Last reports whether this attempt is the job's last, so a handler can settle the work it owns (mark a row failed, notify) instead of leaving it mid-flight when its retries run out. A handler that already knows retrying is pointless returns ErrPermanent rather than waiting for the last attempt.
type Clock ¶ added in v0.25.0
Clock is implemented by drivers whose availability checks read a clock (Memory, Database); app.Queue sets the app clock on them.
type Database ¶
type Database struct {
// contains filtered or unexported fields
}
Database stores jobs in SQL tables (SQLite, Postgres or MySQL). Workers reserve jobs atomically (Postgres and MySQL use FOR UPDATE SKIP LOCKED), so any number of worker processes can share the tables. A job whose worker died is reserved again once its timeout plus a grace period has passed: delivery is at least once, so handlers should be idempotent.
func NewDatabase ¶
func NewDatabase(db *database.DB, o DatabaseOptions) (*Database, error)
NewDatabase returns a database driver; call CreateTables (or a migration with the same schema) before use.
func (*Database) CreateTables ¶
CreateTables creates the jobs and failed jobs tables if missing. It is the fallback for apps that do not register Migrations: the statements are the same, so running it after the migration changes nothing.
func (*Database) Forget ¶
Forget implements FailedStore: it deletes failed jobs (all when ids is empty).
func (*Database) PushTx ¶
PushTx inserts m with ex, so it is enqueued only if ex's transaction commits (Queue.DispatchTx).
type DatabaseOptions ¶
type DatabaseOptions struct {
// Table holds pending jobs (default "jobs").
Table string
// FailedTable holds failed jobs (default "failed_jobs").
FailedTable string
// Grace is added to a job's timeout before an abandoned reservation is
// taken over (default 1 minute).
Grace time.Duration
// Now is the clock that decides when delayed and abandoned jobs are
// due (default time.Now); app.Queue passes the app clock.
Now func() time.Time
}
DatabaseOptions name the tables.
type Driver ¶
type Driver interface {
// Push stores m; it becomes available at m.AvailableAt.
Push(ctx context.Context, m *Message) error
// Pop reserves the next available message on one of queues (in
// order), or returns nil when there is none.
Pop(ctx context.Context, queues []string) (*Message, error)
// Ack deletes a processed message.
Ack(ctx context.Context, m *Message) error
// Release makes m available again at m.AvailableAt (a retry).
Release(ctx context.Context, m *Message) error
// Fail moves m to the failed jobs.
Fail(ctx context.Context, m *Message) error
}
Driver stores messages and hands them to workers.
type FailedStore ¶
type FailedStore interface {
// Failed lists failed jobs, newest first.
Failed(ctx context.Context) ([]FailedJob, error)
// Retry re-queues failed jobs (all when ids is empty).
Retry(ctx context.Context, ids ...string) (int, error)
// Forget deletes failed jobs (all when ids is empty).
Forget(ctx context.Context, ids ...string) (int, error)
}
FailedStore is implemented by drivers that keep failed jobs (Database).
type Fake ¶
type Fake struct {
*Memory
// contains filtered or unexported fields
}
Fake records dispatched jobs for tests; it stores them like Memory, so a test can also run them with Queue.Work.
fake := queue.NewFake()
app := vukemtest.New(t, bootstrap.New, vukemtest.Options{AppOptions: []vukem.Option{vukem.WithQueueDriver(fake)}})
... exercise the app ...
invoices := queue.Dispatched[jobs.SendInvoice](fake)
Example ¶
package main
import (
"context"
"fmt"
"log/slog"
"github.com/AlexGalhardo/vukem/queue"
)
type SendInvoice struct{ OrderID int64 }
func (SendInvoice) Kind() string { return "send_invoice" }
func main() {
ctx := context.Background()
fake := queue.NewFake()
q := queue.New(fake, slog.New(slog.DiscardHandler))
// Dispatch only accepts registered kinds; the Fake stores the job
// instead of running it.
_ = q.Register(queue.Handle(func(context.Context, SendInvoice) error { return nil }))
_ = q.Dispatch(ctx, SendInvoice{OrderID: 7})
for _, j := range queue.Dispatched[SendInvoice](fake) {
fmt.Println("dispatched order", j.OrderID)
}
}
Output: dispatched order 7
type Inline ¶
type Inline interface {
Inline() bool
}
Inline is implemented by drivers that run jobs during Dispatch (Sync).
type Job ¶
type Job interface {
Kind() string
}
Job is a unit of background work. Kind names its handler and must be stable across deploys (it is stored with the payload).
type JobEvent ¶ added in v0.19.0
type JobEvent struct {
ID string
Kind string
Queue string
// Status is the step reported.
Status JobStatus
// Attempt is the 1-based attempt number (0 on JobDispatched).
Attempt int
// MaxRetries is how many retries the job was dispatched with, so
// Attempt of MaxRetries+1 is its last one. It is 0 with an Inline
// driver (Sync), which does not retry.
MaxRetries int
// Final reports, on JobFailed, that the job was not retried: it moved
// to the failed jobs, either because the handler returned
// ErrPermanent or because its retries ran out. It is false on the
// other statuses, and false for an attempt a shutdown gave back (see
// MaxFreeInterruptions), which Attempt and MaxRetries alone cannot
// tell apart from an exhausted one.
Final bool
Duration time.Duration
Err error
}
JobEvent describes one step of a job for an Observer. It carries no payload: payloads may hold user data.
type JobStatus ¶ added in v0.19.0
type JobStatus string
JobStatus is the step a JobEvent reports.
const ( // JobDispatched reports that the job was stored (or is about to run inline). JobDispatched JobStatus = "dispatched" // JobProcessed reports that an attempt succeeded. JobProcessed JobStatus = "processed" // JobFailed reports that an attempt failed (it may still be retried). JobFailed JobStatus = "failed" )
Job steps reported to an Observer.
type Memory ¶
type Memory struct {
// contains filtered or unexported fields
}
Memory keeps messages in process: jobs are lost on restart. Use it for tests and single-process apps that can afford that.
func (*Memory) Pop ¶
Pop implements Driver: the earliest available message of the first queue in queues that has one.
type Message ¶
type Message struct {
ID string `json:"id"`
Kind string `json:"kind"`
Queue string `json:"queue"`
Payload json.RawMessage `json:"payload"`
Attempts int `json:"attempts"`
MaxRetries int `json:"max_retries"`
Timeout time.Duration `json:"timeout"`
AvailableAt time.Time `json:"available_at"`
CreatedAt time.Time `json:"created_at"`
// LastError is set when the job failed.
LastError string `json:"last_error,omitempty"`
}
Message is a stored job.
type Observer ¶ added in v0.19.0
Observer is notified when jobs are dispatched and attempted. It runs on the caller's goroutine, so it must be fast and safe for concurrent use.
type Option ¶
type Option func(*options)
Option configures a handler (defaults) or a single dispatch.
type Queue ¶
type Queue struct {
// contains filtered or unexported fields
}
Queue dispatches jobs to a driver and runs them in workers.
func (*Queue) AddObserver ¶ added in v0.25.0
AddObserver adds obs after the queue's current observers (devtools next to a tracing observer). Call it before the queue is used.
func (*Queue) Dispatch ¶
Dispatch enqueues j. Options override the handler's defaults for this dispatch. With an Inline driver the job runs now and its error is returned; Inline drivers cannot delay jobs, so Delay returns ErrInlineDelay there.
func (*Queue) DispatchKind ¶
DispatchKind enqueues payload (encoded as JSON) for the handler of kind.
func (*Queue) DispatchKindTx ¶ added in v0.25.0
func (q *Queue) DispatchKindTx(ctx context.Context, ex database.Executor, kind string, payload any, opts ...Option, ) error
DispatchKindTx is DispatchTx for a payload sent to the handler of kind (framework jobs such as queued mail, see DispatchKind).
func (*Queue) DispatchTx ¶
DispatchTx enqueues j with the transaction of ex: the job exists only if the transaction commits, so it never runs for rolled-back work and is never lost for committed work.
Drivers with transactional storage (Database) insert the job inside the transaction. Other drivers (Sync, Memory, Fake) dispatch it right after the commit (database.Tx.AfterCommit): Sync runs it then, and since the caller already returned, a handler error is logged instead of returned. Errors known up front (unknown kind, ErrInlineDelay) are returned inside the transaction. With a *database.DB (no open transaction) the job is dispatched now.
func (*Queue) Message ¶
Message builds the stored message for j (drivers with their own transactional push use it).
func (*Queue) Process ¶
Process runs one reserved message and acks, releases (retry with backoff) or fails it.
func (*Queue) Register ¶
func (q *Queue) Register(regs ...Registration) error
Register adds handlers; a kind may be registered once.
func (*Queue) SetClock ¶ added in v0.25.0
SetClock sets the clock that stamps dispatched jobs and schedules retries (default time.Now). Delays only follow it when the driver compares availability with the same clock (Memory.SetClock, DatabaseOptions.Now); app.Queue wires the app clock (WithClock) into both.
func (*Queue) SetObserver ¶ added in v0.19.0
SetObserver replaces the queue's observers with obs (development tooling such as devtools; nil, or a typed nil pointer, removes them). Call it before the queue is used; AddObserver keeps the existing ones.
func (*Queue) Work ¶
func (q *Queue) Work(ctx context.Context, o WorkOptions) error
Work processes jobs until ctx is cancelled. On cancellation it stops reserving new jobs and waits for running ones (graceful drain): they keep a context that is not cancelled, bounded by their timeout and by StopTimeout.
type Registration ¶
type Registration struct {
// contains filtered or unexported fields
}
Registration binds a handler to its job kind; create it with Handle.
func HandleKind ¶
func HandleKind[T any](kind string, fn func(ctx context.Context, v T) error, opts ...Option) Registration
HandleKind registers fn for jobs named kind whose payload decodes into T (framework jobs such as queued mail and listeners; DispatchKind sends them).
func (Registration) Kind ¶
func (r Registration) Kind() string
Kind returns the job kind of the registration.
type Sync ¶
type Sync struct{}
Sync runs jobs inline during Dispatch (development, tests, simple apps). Retries do not apply (the handler's error goes to the caller) and delayed jobs are refused with ErrInlineDelay; DispatchTx runs the job after the transaction commits.
type TxPusher ¶
TxPusher is implemented by drivers that can enqueue inside a database transaction (Database).
type WorkOptions ¶
type WorkOptions struct {
// Queues are polled in priority order (default ["default"]).
Queues []string
// Concurrency is the number of parallel workers (default 1).
Concurrency int
// Sleep is the wait when no job is available (default 1s).
Sleep time.Duration
// MaxJobs stops after processing this many jobs (0 = no limit), e.g.
// for "queue:work --once".
MaxJobs int
// StopWhenEmpty returns when no job is available. A reserve error then
// ends Work with that error (without it, the error is logged and the
// worker keeps polling).
StopWhenEmpty bool
// StopTimeout bounds the drain once ctx is cancelled: jobs still
// running after it have their context cancelled. A job that then fails
// with that cancellation (context.Canceled or DeadlineExceeded) goes
// back to the queue without using an attempt, MaxFreeInterruptions
// times in a row; later interruptions use attempts, and other errors
// are retried or failed as usual. Jobs run again from the start, so
// they must be idempotent. 0 waits for them (each is still bounded by
// its own timeout). Jobs must honor ctx; one that ignores it still
// holds Work.
StopTimeout time.Duration
}
WorkOptions configure Work.