jobs

package
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Jul 7, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Overview

Package jobs is the agent's durable work queue: a port for work that must survive a restart and be retried until it succeeds or is exhausted. Scheduled automations, delayed and retried side effects, and fan-out work are enqueued here rather than run inline, so a crash between "decide to do X" and "X done" loses nothing.

Like state, spine, and observe, the queue is a PORT with a zero-dependency default and heavy backends as opt-in adapters. The default is SQLite-backed (durable, single file, no setup); a fast in-memory queue is the reference for tests and ephemeral use. River (Postgres) and NATS/JetStream (cross-process, fleet, k8s) attach later as adapters held to the same jobstest conformance suite. Workers claim jobs under a lease, so a crashed worker's in-flight jobs return to the queue when the lease expires rather than being lost.

The queue is distinct from its neighbours: the bus is at-most-once ambient signalling, the spine is the durable ordered truth log. The queue is durable, at-least-once, retried work that the bus and spine are not.

Index

Constants

View Source
const DefaultMaxAttempts = 25

DefaultMaxAttempts is the retry ceiling applied when EnqueueParams.MaxAttempts is unset. The job runs once and is retried up to this many times in total.

View Source
const DefaultQueue = "default"

DefaultQueue is the lane a job lands in when EnqueueParams.Queue is empty.

Variables

View Source
var (
	// ErrNotFound is returned by Get/Complete/Fail for an unknown job ID.
	ErrNotFound = fault.New(fault.Terminal, "job_not_found", "job not found")
	// ErrNotRunning is returned by Complete/Fail when the job is not currently
	// leased (already done, dead, or never claimed).
	ErrNotRunning = fault.New(fault.Terminal, "job_not_running", "job is not running")
	// ErrInvalidJob is returned by Enqueue for params that cannot describe a job
	// (an empty Kind).
	ErrInvalidJob = fault.New(fault.Terminal, "job_invalid", "invalid job")
)

Sentinel errors, fault-classified so callers branch on the class rather than on string matching.

Functions

func Backoff

func Backoff(attempt int, base, ceiling int64) int64

Backoff returns the delay before the attempt-th retry (1-based), doubling from base and capped at ceiling. It is the default exponential policy a worker applies when computing Fail's retryAt; callers may substitute their own. attempt <= 0 is treated as 1.

func ClaimDefaults

func ClaimDefaults(p ClaimParams) (queue string, limit int)

ClaimDefaults resolves a ClaimParams to a concrete queue and limit.

func Claimable

func Claimable(j Job, now int64) bool

Claimable reports whether a job may be claimed at time now: a pending job whose RunAt has arrived, or a running job whose lease has expired and still has attempts left (crash recovery). Each claim is an attempt, so a job whose lease expires after its final attempt is not reclaimed (it is timed out, see ExpiredExhausted) rather than retried past MaxAttempts.

func ExpiredExhausted

func ExpiredExhausted(j Job, now int64) bool

ExpiredExhausted reports whether a running job has timed out on its final attempt: its lease has expired and no attempts remain. Such a job is reaped to dead by Claim rather than left as a zombie or retried forever.

func MarkClaimed

func MarkClaimed(j *Job, now, leaseFor int64)

MarkClaimed transitions a job into a fresh lease: running, one more attempt, and a lease ending leaseFor nanos from now.

func MarkDone

func MarkDone(j *Job, now int64)

MarkDone transitions a job to done and clears its lease.

func MarkFailed

func MarkFailed(j *Job, errMsg string, retryAt, now int64)

MarkFailed transitions a running job after a failed attempt: back to pending (RunAt = retryAt) while attempts remain, or dead once they are exhausted.

func MarkRecovered

func MarkRecovered(j *Job, now int64)

MarkRecovered expires a running job's lease so the next Claim reclaims it at once, leaving the attempt count untouched. Startup crash recovery uses it: under the single-instance lock a job still running belongs to a dead predecessor, so its lease need not be waited out. A job whose attempts are already spent is not revived; the next Claim reaps it to dead (see ExpiredExhausted).

func MarkTimedOut

func MarkTimedOut(j *Job, now int64)

MarkTimedOut transitions a job that exhausted its attempts by lease expiry to dead, recording why. Used by Claim's reap step.

func Notify

func Notify(ch chan<- struct{})

Notify sends the coalesced, non-blocking ready signal a Waker delivers: if the channel's buffered slot is free the signal is queued, otherwise one is already pending and this one merges into it. Shared so every backend coalesces identically and none can block a writer on a slow worker.

Types

type ClaimParams

type ClaimParams struct {
	Queue    string // empty means DefaultQueue
	Limit    int    // max jobs to claim; <= 0 means 1
	LeaseFor int64  // lease duration in nanos; the claim is held this long
}

ClaimParams describes a worker's request to lease ready jobs.

type EnqueueParams

type EnqueueParams struct {
	Queue       string
	Kind        string
	Payload     []byte
	Scope       state.Scope
	MaxAttempts int   // <= 0 uses DefaultMaxAttempts
	RunAt       int64 // unix nanos; 0 means as soon as possible
}

EnqueueParams describes a job to enqueue. Only Kind is required; the queue fills in safe defaults (DefaultQueue, MaxAttempts, RunAt = now).

type Job

type Job struct {
	ID      string
	Queue   string
	Kind    string // selects the handler; the queue itself never interprets it
	Payload []byte
	Scope   state.Scope

	State       State
	Attempt     int    // attempts started so far (0 until first claim)
	MaxAttempts int    // attempts allowed before the job goes dead
	LastError   string // fault class + message from the most recent failure

	RunAt        int64 // unix nanos: earliest time the job may be claimed
	LeaseExpires int64 // unix nanos: when a running job's claim lapses (0 if not running)

	OriginInstanceID string // instance that enqueued the job, for fleet attribution
	CreatedAt        int64
	UpdatedAt        int64
}

Job is one unit of durable work. Payload is opaque bytes so the queue is agnostic to encoding; the typed-handler layer marshals it.

func BuildJob

func BuildJob(p EnqueueParams, now int64, id, instanceID string) Job

BuildJob constructs the job an Enqueue stores, applying defaults (DefaultQueue, DefaultMaxAttempts, RunAt = now). The caller validates that p.Kind is non-empty and supplies the assigned id, now (unix nanos), and origin instance.

type MemoryQueue

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

MemoryQueue is the in-process reference Queue: a map guarded by a mutex, no persistence. It is the fast default for tests and ephemeral runs, and the reference semantics the durable SQLite backend must match (both run jobstest.RunSuite). Lease expiry is honoured, so it exercises the same crash-recovery path as a durable backend, just without surviving a restart.

func NewMemory

func NewMemory(opts ...Option) *MemoryQueue

NewMemory constructs an in-process Queue ready to use with zero configuration.

func (*MemoryQueue) Claim

func (q *MemoryQueue) Claim(_ context.Context, p ClaimParams) ([]Job, error)

Claim implements Queue.

func (*MemoryQueue) Close

func (q *MemoryQueue) Close() error

Close implements Queue.

func (*MemoryQueue) Complete

func (q *MemoryQueue) Complete(_ context.Context, id string) error

Complete implements Queue.

func (*MemoryQueue) Enqueue

func (q *MemoryQueue) Enqueue(_ context.Context, p EnqueueParams) (Job, error)

Enqueue implements Queue.

func (*MemoryQueue) Fail

func (q *MemoryQueue) Fail(_ context.Context, id, errMsg string, retryAt int64) error

Fail implements Queue.

func (*MemoryQueue) Get

func (q *MemoryQueue) Get(_ context.Context, id string) (Job, error)

Get implements Queue.

func (*MemoryQueue) Ready

func (q *MemoryQueue) Ready() <-chan struct{}

Ready implements Waker: the channel signalled after Enqueue and after a Fail that returns a job to pending.

func (*MemoryQueue) Recover

func (q *MemoryQueue) Recover(context.Context) (int, error)

Recover implements Queue: it expires the lease of every job left running, so the next Claim reclaims (or, if attempts are spent, reaps) it at once. Only live jobs are scanned; terminal jobs already left the active index.

type Option

type Option func(*MemoryQueue)

Option configures a MemoryQueue.

func WithClock

func WithClock(c clock.Clock) Option

WithClock sets the time source (default: clock.System). Tests and deterministic replay pass a clock.Manual.

func WithIDGenerator

func WithIDGenerator(g *ids.Generator) Option

WithIDGenerator sets the job-ID source (default: a generator on the queue's clock). A seeded generator makes enqueued IDs reproducible.

func WithInstanceID

func WithInstanceID(id string) Option

WithInstanceID sets the instance stamped onto enqueued jobs (default "local").

type Queue

type Queue interface {
	// Enqueue persists a new job and returns it with its assigned ID and defaults
	// applied.
	Enqueue(ctx context.Context, p EnqueueParams) (Job, error)
	// Claim leases up to Limit ready jobs from a queue: jobs that are pending with
	// RunAt in the past, or running jobs whose lease has expired (a crashed
	// worker's work). Claimed jobs move to StateRunning with their Attempt
	// incremented and a fresh lease, and are returned to the caller.
	Claim(ctx context.Context, p ClaimParams) ([]Job, error)
	// Complete marks a claimed job done. It is an error to complete a job that is
	// not running.
	Complete(ctx context.Context, id string) error
	// Fail records a failed attempt. If attempts remain the job returns to pending
	// with RunAt = retryAt (the caller computes backoff); otherwise it goes dead. A
	// negative retryAt fails the job permanently (dead now, no further attempts),
	// for a cause the caller knows is not retryable.
	Fail(ctx context.Context, id, errMsg string, retryAt int64) error
	// Get returns a job by ID.
	Get(ctx context.Context, id string) (Job, error)
	// Recover makes jobs left running by a crashed worker immediately claimable,
	// returning how many were reset. Startup calls it once under the single-instance
	// lock, where no live worker holds a lease, so a run interrupted mid-step is
	// re-dispatched at once instead of waiting out its lease (up to several minutes). It
	// does not change a job's attempt count; a job that already exhausted its attempts
	// is reaped to dead by the next Claim rather than retried.
	Recover(ctx context.Context) (int, error)
	// Close releases the queue's resources.
	Close() error
}

Queue is the durable work-queue port. Implementations must be safe for concurrent use and must claim each ready job to at most one worker at a time.

type State

type State string

State is a job's lifecycle position.

const (
	// StatePending is ready to run at or after RunAt, awaiting a worker.
	StatePending State = "pending"
	// StateRunning is claimed by a worker under an unexpired lease.
	StateRunning State = "running"
	// StateDone is completed successfully; terminal.
	StateDone State = "done"
	// StateDead is failed and out of attempts; terminal, retained for inspection.
	StateDead State = "dead"
)

type Waker

type Waker interface {
	// Ready returns the channel signalled after a job is enqueued or returned to
	// pending for retry. Every call returns the same channel.
	Ready() <-chan struct{}
}

Waker is an optional Queue capability: a queue that can observe its own in-process writes exposes a ready channel, so a worker wakes the moment work is enqueued instead of discovering it on its next idle poll. The signal is advisory and coalesced (one buffered slot): receiving one does not guarantee a claimable job, and a missed one costs nothing because the worker's poll remains the fallback. The poll is also the only path for scheduled RunAt arrivals and for writes from other processes, which a queue cannot signal.

Directories

Path Synopsis
Package jobstest is the conformance suite for jobs.Queue.
Package jobstest is the conformance suite for jobs.Queue.

Jump to

Keyboard shortcuts

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