Documentation
¶
Overview ¶
Package gotasks is a persistent, atomic, asynchronous task queue for Go. Tasks live in a pluggable store (MongoDB first); a pool of workers claims them atomically (one claim = one attempt), runs a typed handler per task type, and retries failures with backoff. Scheduling is done via run_at; every claim takes a lease (leased_until + lease_token) so a task whose worker died is reclaimed once the lease expires — never run twice at once.
Index ¶
- Constants
- Variables
- func Enqueue[T any](ctx context.Context, m *Manager, queue, taskType string, payload T, ...) (string, error)
- func EnqueueMany[T any](ctx context.Context, m *Manager, queue, taskType string, payloads []T, ...) ([]string, error)
- func RegisterHandler[T any](m *Manager, taskType string, ...) error
- type Backoff
- type BackoffFunc
- type ClaimOptions
- type Config
- type DuplicateTaskError
- type EnqueueFailure
- type HandlerFunc
- type HandlerOption
- type Hooks
- type JanitorConfig
- type Manager
- func (m *Manager) Close(ctx context.Context) error
- func (m *Manager) ExtendLease(ctx context.Context, t *Task) error
- func (m *Manager) Requeue(ctx context.Context, id string) error
- func (m *Manager) RequeueDead(ctx context.Context, taskType string) (int64, error)
- func (m *Manager) Run(ctx context.Context) error
- func (m *Manager) Start() error
- func (m *Manager) Stop(ctx context.Context) error
- type MetricsConfig
- type Middleware
- type Option
- func WithBackoff(b Backoff) Option
- func WithDefaultMaxAttempts(n int) Option
- func WithDefaultTimeout(d time.Duration) Option
- func WithFIFO() Option
- func WithFallbackPoll(d time.Duration) Option
- func WithHeartbeat() Option
- func WithHeartbeatInterval(d time.Duration) Option
- func WithHooks(h Hooks) Option
- func WithJanitor(jc JanitorConfig) Option
- func WithLeaseTime(d time.Duration) Option
- func WithLogger(l *slog.Logger) Option
- func WithManagerName(name string) Option
- func WithMiddleware(mw ...Middleware) Option
- func WithPipelineMode(pc PipelineConfig) Option
- func WithPollInterval(d time.Duration) Option
- func WithQueues(qs ...QueuePolicy) Option
- func WithWorkers(n int) Option
- func WithoutChangeStream() Option
- type Outcome
- type PartialEnqueueError
- type PipelineConfig
- type QueueCounters
- type QueuePolicy
- type Status
- type Store
- type Task
- type TaskError
- type TaskPolicy
- type Watcher
Constants ¶
const HeartbeatAuto time.Duration = -1
HeartbeatAuto selects the automatic heartbeat cadence: LeaseTime/3.
Variables ¶
var ( // ErrNoTask is returned by Store.Claim when no runnable task exists. ErrNoTask = errors.New("gotasks: no task available") // ErrLeaseLost is returned by Complete/Fail/ExtendLease when the task is // no longer held under the given lease token (it was reclaimed by another // worker after the lease expired, or already finalized). ErrLeaseLost = errors.New("gotasks: task lease lost or task already finalized") // ErrNotFound is returned when a task id does not exist (or is malformed // for the store's id scheme), or is not in the state the call requires // (e.g. Requeue on a task that is not dead). ErrNotFound = errors.New("gotasks: task not found") // ErrDuplicateTask is matched (via errors.Is) by the *DuplicateTaskError // returned when enqueueing with a unique key that already has an active // (pending/running) task. ErrDuplicateTask = errors.New("gotasks: duplicate unique key") // ErrQueueConflict is returned when a manager declares a queue whose // policy differs from what the queues registry already records — policy // drift across deployments is a startup error, never silent behavior. // Deliberate changes go through Store.SetQueuePolicy. ErrQueueConflict = errors.New("gotasks: queue policy conflicts with the registry") )
Functions ¶
func Enqueue ¶
func Enqueue[T any](ctx context.Context, m *Manager, queue, taskType string, payload T, policy ...TaskPolicy) (string, error)
Enqueue inserts one task into queue (which must be declared on this manager) with a typed payload — any value that marshals to a JSON object; use struct{}{} for no payload. The optional TaskPolicy carries scheduling, attempts, unique-key and TTL knobs. Returns the task id. With TaskPolicy.UniqueKey, a conflict returns the EXISTING active task's id along with an error matching ErrDuplicateTask — treat it as idempotent success when that suits the caller.
func EnqueueMany ¶
func EnqueueMany[T any](ctx context.Context, m *Manager, queue, taskType string, payloads []T, policy ...TaskPolicy) ([]string, error)
EnqueueMany inserts many tasks of one type into queue in chunked store round trips. All share the same TaskPolicy.
func RegisterHandler ¶
func RegisterHandler[T any](m *Manager, taskType string, fn func(ctx context.Context, task *Task, payload T) (any, error), opts ...HandlerOption) error
RegisterHandler wires fn to run tasks of taskType, decoding the payload into T. A non-nil returned value is stored as the task's result (wrapped as {"value": ...} when it isn't a JSON object). A returned error (or panic) consumes an attempt and triggers retry with backoff, or failed when attempts are exhausted. Register everything before Start.
Types ¶
type Backoff ¶
Backoff decides how long to wait before retry attempt+1, given that `attempt` attempts have already happened (attempt >= 1 on first failure).
func ExponentialBackoff ¶
ExponentialBackoff waits base*2^(attempt-1) capped at max, with up to 25% random jitter added to spread thundering herds. This is the default (base=30s, max=1h).
func FixedBackoff ¶
FixedBackoff waits the same duration after every failure.
func LinearBackoff ¶
LinearBackoff waits base*attempt (30s, 60s, 90s, ... for base=30s).
type BackoffFunc ¶
BackoffFunc adapts a function to the Backoff interface.
type ClaimOptions ¶
type ClaimOptions struct {
// WorkerID is recorded on the task as leased_by.
WorkerID string
// Types restricts claims to these task types; empty = all types.
Types []string
// Queues restricts claims to these queues. Empty = all queues, a
// store-level convenience for tests/tools: the manager always supplies
// its declared queue names, and the claim index requires the clause.
Queues []string
// FIFO requests oldest-first claim order ((run_at, id) ascending).
// False (the default) lets the store pick whichever runnable task is
// cheapest to take — faster and less contended, but task staleness is
// unbounded under sustained overload.
FIFO bool
// Lease is how long the claim holds the task before it becomes
// reclaimable.
Lease time.Duration
}
ClaimOptions parameterizes Claim and ClaimBatch.
type Config ¶
type Config struct {
// Workers is the number of concurrent worker goroutines. Default 4.
Workers int
// PollInterval is how long a worker sleeps after finding no task
// (railway loop: while tasks keep coming, workers never sleep). Default 2s.
PollInterval time.Duration
// LeaseTime is how long a claim holds a task before it becomes
// reclaimable by other workers. Must exceed your longest handler run
// unless the heartbeat is enabled (WithHeartbeat). Default 60s.
LeaseTime time.Duration
// DefaultMaxAttempts for enqueued tasks (override per queue via
// QueuePolicy.MaxAttempts or per task via TaskPolicy.MaxAttempts).
// 1 = at-most-once. Default 3.
DefaultMaxAttempts int
// DefaultTimeout bounds each handler run via context (per-type override
// with WithHandlerTimeout). 0 disables. Default 60s.
DefaultTimeout time.Duration
// Backoff computes retry delays. Default ExponentialBackoff(30s, 1h).
Backoff Backoff
// Janitor configures the maintenance goroutine's duties (reaping,
// metrics, future work). The zero value is a working default: reap
// every 60s, metrics off. See WithJanitor.
Janitor JanitorConfig
// HeartbeatInterval controls automatic lease extension while a handler
// runs, letting handlers outlive LeaseTime without being reclaimed.
// 0 (default) disables the heartbeat — handlers must then finish within
// LeaseTime, or call Manager.ExtendLease themselves. HeartbeatAuto
// derives LeaseTime/3; a positive value is used as-is. When the
// heartbeat discovers the lease was lost (the task was reclaimed), the
// handler's context is cancelled.
HeartbeatInterval time.Duration
// Pipeline enables pipeline mode when non-nil (set via
// WithPipelineMode). nil (default) = single mode: each worker claims,
// handles, and writes its result independently — no fetcher, no
// channel, no buffering, immediate visibility of every state change.
// See PipelineConfig for what pipeline mode changes.
Pipeline *PipelineConfig
// DisableChangeStream forces polling even when the store supports
// change-stream wakeup. Default false: the manager tries the stream at
// Start and falls back to polling silently if unavailable.
DisableChangeStream bool
// FallbackPoll is the safety-net poll cadence used while a change
// stream is active (it catches anything a stream gap missed). Default
// 30s. Ignored when no stream is active — PollInterval applies then.
FallbackPoll time.Duration
// FIFO makes claims take the oldest runnable task first ((run_at, id)
// ascending). Off by default: unsorted claiming is faster and spreads
// contention, but doesn't bound how stale a task can get under
// sustained overload. Scheduling (run_at) is enforced by the claim
// filter in both modes — FIFO only orders already-due tasks.
FIFO bool
// Queues is the explicit set of queues this manager works with — like
// every queue system (Kafka topics, SQS queues), there is no default
// queue and nothing is consumed implicitly. Declare each queue with
// WithQueue; at least one is required. Enqueueing to an undeclared
// queue is an error (catches typos at the call site), and Start
// consumes every declared queue (a produce-only manager simply never
// calls Start; an app producing to A while consuming B uses two
// managers).
Queues []QueuePolicy
// ManagerName is a developer-chosen, non-unique label for this manager
// instance (default "manager"). The framework derives a unique,
// dot-separated ManagerID = "{name}.{random}" per instance; workers
// lease as "{managerID}.worker.{n}" (fetcher: "{managerID}.fetcher"),
// and metrics counters are attributed to the ManagerID.
ManagerName string
// Hooks are observe-only lifecycle callbacks (see Hooks). They run on
// worker goroutines: keep them fast and non-blocking. A panicking hook
// is recovered and logged; hooks never affect a task's outcome.
Hooks Hooks
// Middleware wraps handler execution (outermost first): tracing spans,
// timing, log enrichment. Applied identically to every handler at
// registration time.
Middleware []Middleware
// Logger receives worker/reaper diagnostics. Default slog.Default().
Logger *slog.Logger
}
Config holds manager settings. Zero values are replaced by defaults in New.
type DuplicateTaskError ¶
DuplicateTaskError reports a unique-key conflict on enqueue. errors.Is(err, ErrDuplicateTask) matches it; ExistingID is the id of the active task holding the key ("" if it finished between insert and lookup).
func (*DuplicateTaskError) Error ¶
func (e *DuplicateTaskError) Error() string
func (*DuplicateTaskError) Is ¶
func (e *DuplicateTaskError) Is(target error) bool
type EnqueueFailure ¶ added in v0.4.0
EnqueueFailure is one failed entry of a batch enqueue.
type HandlerFunc ¶ added in v0.6.0
HandlerFunc is the raw executable form of a handler that Middleware wraps: the payload is still JSON, the result is whatever the (typed) handler returned.
type HandlerOption ¶
type HandlerOption func(*handlerOptions)
HandlerOption configures a handler registration.
func WithHandlerTimeout ¶
func WithHandlerTimeout(d time.Duration) HandlerOption
WithHandlerTimeout bounds every run of this handler via context deadline.
type Hooks ¶ added in v0.6.0
type Hooks struct {
// OnEnqueue fires once per successfully inserted task.
OnEnqueue func(t *Task)
// OnClaim fires when this manager takes ownership of a task.
OnClaim func(t *Task)
// OnComplete fires when a handler succeeds; d is the handler duration.
OnComplete func(t *Task, d time.Duration)
// OnFail fires on every failed attempt; willRetry=false means the
// task is going to the dead-letter state (OnDead fires too).
OnFail func(t *Task, err error, willRetry bool)
// OnDead fires when a worker's terminal failure sends a task to the
// dead-letter state. (Reaper-detected zombie deaths don't fire it —
// no worker context exists there; watch the dead gauge for those.)
OnDead func(t *Task, err error)
}
Hooks are observe-only lifecycle callbacks. Every field is optional. They observe, they never decide: returning is their only effect, and a panic inside a hook is recovered and logged without touching the task.
type JanitorConfig ¶ added in v0.6.0
type JanitorConfig struct {
// ReapInterval is how often stale+exhausted tasks are swept to dead.
// 0 = default 60s; disable with NoReap.
ReapInterval time.Duration
// NoReap disables the reap duty (zombies then stay visible as running
// docs with expired leases).
NoReap bool
// Metrics enables the metrics duty when non-nil. nil (default) = off:
// smaller projects pay nothing.
Metrics *MetricsConfig
}
JanitorConfig groups the janitor's duties. The janitor itself is a deadline scheduler — every duty keeps its own clock, and the goroutine sleeps until the earliest next deadline (no fixed tick, no drift on the wall-aligned metrics windows).
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager owns the enqueue API and the worker pool. Create with New, wire handlers with RegisterHandler, then Start (or Run). Enqueue works from any process, including ones that never start workers.
func (*Manager) ExtendLease ¶
ExtendLease pushes the task's lease forward by the manager's LeaseTime. Rarely needed directly — enable the heartbeat (WithHeartbeat) to do this automatically for long-running handlers.
func (*Manager) Requeue ¶
Requeue resets one dead task to pending (attempts back to 0, runnable now); its errors array is kept as history. Returns ErrNotFound if the id doesn't exist or the task isn't dead.
func (*Manager) RequeueDead ¶
RequeueDead requeues every dead task of taskType ("" = all types) with the same resets as Requeue, returning how many were requeued.
func (*Manager) Run ¶
Run is a convenience blocking loop: Start, wait for ctx cancellation, then drain with a 30s grace period.
type MetricsConfig ¶ added in v0.6.0
type MetricsConfig struct {
// Interval is the snapshot window size (default 60s, min 1s).
Interval time.Duration
// Retention is how long metrics documents are kept (TTL index on the
// metrics collection; default 7 days).
Retention time.Duration
}
MetricsConfig configures Mongo-native metrics (see JanitorConfig). Zero values take defaults.
type Middleware ¶ added in v0.6.0
type Middleware func(next HandlerFunc) HandlerFunc
Middleware wraps a HandlerFunc. The first middleware passed to WithMiddleware is the outermost.
type Option ¶
type Option func(*Config)
Option configures the Manager.
func WithBackoff ¶
func WithDefaultMaxAttempts ¶
func WithDefaultTimeout ¶
func WithFIFO ¶
func WithFIFO() Option
WithFIFO makes claims take the oldest runnable task first (see Config.FIFO). Off by default.
func WithFallbackPoll ¶
WithFallbackPoll sets the safety-net poll cadence used while a change stream is active (see Config.FallbackPoll).
func WithHeartbeat ¶
func WithHeartbeat() Option
WithHeartbeat enables automatic lease extension at the auto cadence (LeaseTime/3). Off by default.
func WithHeartbeatInterval ¶
WithHeartbeatInterval enables automatic lease extension at an explicit cadence (or HeartbeatAuto for LeaseTime/3; 0 keeps it disabled).
func WithHooks ¶ added in v0.6.0
WithHooks sets the lifecycle hooks (see Hooks). Last call wins whole.
func WithJanitor ¶ added in v0.6.0
func WithJanitor(jc JanitorConfig) Option
WithJanitor configures the maintenance duties (last call wins whole). Metrics duty: per-window gauge snapshots per queue (status counts + oldest due-task age; deduplicated cluster-wide, first manager wins each window) and this manager's exact per-queue throughput counters.
func WithLeaseTime ¶
func WithLogger ¶
func WithManagerName ¶ added in v0.6.0
WithManagerName sets the developer-chosen label for this manager instance (see Config.ManagerName).
func WithMiddleware ¶ added in v0.6.0
func WithMiddleware(mw ...Middleware) Option
WithMiddleware appends handler middleware; the first registered is the outermost.
func WithPipelineMode ¶ added in v0.4.0
func WithPipelineMode(pc PipelineConfig) Option
WithPipelineMode switches the manager from single mode to pipeline mode (see PipelineConfig). Single mode — the default — keeps every worker claiming, handling, and acknowledging independently with immediate visibility; pipeline mode trades bounded acknowledgment latency for amortized round trips and is meant for high-throughput pipelines.
func WithPollInterval ¶
func WithQueues ¶
func WithQueues(qs ...QueuePolicy) Option
WithQueues declares the manager's queues (appendable across calls).
func WithWorkers ¶
func WithoutChangeStream ¶
func WithoutChangeStream() Option
WithoutChangeStream forces polling even when the store supports change-stream wakeup (see Config.DisableChangeStream).
type Outcome ¶
type Outcome struct {
Task *Task
Result json.RawMessage // success only; nil = no result
Failure *TaskError // non-nil = this attempt failed
RetryAt time.Time // failure, non-terminal: the next run_at
Terminal bool // failure: attempts exhausted -> dead
}
Outcome is one finished attempt, carried between a worker and (batched) finalization. Failure nil = success.
type PartialEnqueueError ¶ added in v0.4.0
type PartialEnqueueError struct {
Failures []EnqueueFailure
}
PartialEnqueueError reports a partially successful batch enqueue: entries not listed in Failures were inserted (chunked writes are not atomic). The ids returned alongside this error stay aligned with the input, with "" at each failed index.
func (*PartialEnqueueError) Error ¶ added in v0.4.0
func (e *PartialEnqueueError) Error() string
type PipelineConfig ¶ added in v0.4.0
type PipelineConfig struct {
// ClaimBatch is how many tasks the fetcher claims per round trip
// (1..100). Default 32.
ClaimBatch int
// QueueLease budgets how long a task may wait in the fetcher's channel
// before becoming reclaimable by other processes. Default: LeaseTime.
QueueLease time.Duration
// FinalizeBatch is how many outcomes a finalize flush carries at most
// (2..1000). Default: 2x ClaimBatch.
FinalizeBatch int
// FinalizeInterval is the longest a finished task may wait in the
// finalize buffer before its outcome is written (default 300ms). A
// latency bound, not a polling period: the flusher sleeps while the
// buffer is empty, and on busy queues the size trigger fires first.
FinalizeInterval time.Duration
// NoFinalize keeps batched claiming but writes every outcome
// immediately (single-mode acknowledgment semantics).
NoFinalize bool
}
PipelineConfig configures pipeline mode: the high-throughput architecture where one fetcher claims tasks in batches under a QUEUE lease and feeds workers through a bounded channel (workers revalidate ownership with one point-write that starts the task lease), and finished tasks are acknowledged in batched bulk writes.
Zero values take defaults, so WithPipelineMode(PipelineConfig{}) is a complete, sensible setup.
Finalize safety policy: outcomes are NEVER buffered when risky — at-most-once tasks (max_attempts=1) and tasks whose remaining lease is under the safety margin (max(4x FinalizeInterval, 2s)) are written immediately, so a buffered outcome cannot outlive its lease while the process is alive. The trade-off: a crash loses up to FinalizeInterval of finished-but-unflushed outcomes — those tasks are reclaimed and re-run (the normal at-least-once contract, slightly widened).
type QueueCounters ¶ added in v0.6.0
type QueueCounters struct {
Enqueued int64
Claimed int64
Done int64
Failed int64 // failed attempts that will retry
Dead int64 // terminal failures
}
QueueCounters is one manager instance's exact per-queue event counts for one metrics window. Done/Failed/Dead are counted when the worker decides the outcome (batched finalization may land the write moments later).
type QueuePolicy ¶ added in v0.6.0
type QueuePolicy struct {
Name string
// TTL is the queue's total-lifetime default (see above). 0 = no expiry.
TTL time.Duration
// MaxAttempts is the queue's default attempts budget. 0 inherits the
// manager's DefaultMaxAttempts. TaskPolicy.MaxAttempts overrides both.
MaxAttempts int
}
QueuePolicy declares one queue and its policies. TTL is the queue's task lifetime, measured from each task's run_at (its eligible-to-run time): expires_at = run_at + TTL is stamped at creation, and MongoDB purges the document when it passes — whether or not the task ran (the SQS retention model; budget TTL for queue wait + retries + how long you want the result visible; scheduled tasks don't burn lifetime while waiting to become due). 0 = no expiry. TTL is queue-scoped by design: tasks needing a different lifetime belong in a different queue.
type Status ¶
type Status string
Status is the lifecycle state of a task.
const ( StatusPending Status = "pending" // waiting for a worker (run_at may be in the future) StatusRunning Status = "running" // claimed under a live lease StatusDone Status = "done" // handler succeeded // StatusDead is the dead-letter state: attempts exhausted (or lease // expired with none left). Dead tasks sit inspectable until requeued // (Manager.Requeue / RequeueDead) or pruned by retention. StatusDead Status = "dead" )
type Store ¶
type Store interface {
// Enqueue inserts tasks (batch; callers pass a single-element slice for
// one task) and returns their assigned ids aligned with the input.
// Large batches may be written in chunks; on partial failure the ids
// slice carries "" at failed indexes and the error is a
// *PartialEnqueueError (single-task unique-key conflicts keep returning
// *DuplicateTaskError).
Enqueue(ctx context.Context, tasks []*Task) ([]string, error)
// Claim atomically takes one runnable task and returns it with
// Status=running, Attempts incremented, and a fresh LeaseToken/LeasedUntil.
// Runnable means: (pending AND run_at <= now) OR
// (running AND leased_until < now AND attempts < max_attempts).
// Returns ErrNoTask when nothing matches.
Claim(ctx context.Context, opts ClaimOptions) (*Task, error)
// ClaimBatch atomically takes up to k runnable tasks (same runnable
// definition and ordering as Claim) under a single lease token, in a
// constant number of round trips. Concurrent callers may split the
// candidates: a short (even empty) result with a nil error means others
// won some or all of them — more work may still exist, so callers should
// retry promptly. ErrNoTask means no runnable candidates existed at all.
ClaimBatch(ctx context.Context, opts ClaimOptions, k int) ([]*Task, error)
// Complete marks t done, stores the handler result (nil = none), and
// releases t's unique key if it has one.
Complete(ctx context.Context, t *Task, result json.RawMessage) error
// Fail records taskErr on t's errors array. If terminal is false the
// task is rescheduled: status back to pending with run_at = retryAt
// (unique key kept — the task is still active). If terminal is true the
// task becomes dead and its unique key is released.
Fail(ctx context.Context, t *Task, taskErr TaskError, retryAt time.Time, terminal bool) error
// ExtendLease pushes t's leased_until to now+lease and returns the new
// deadline. Used by the manager's heartbeat for long-running handlers.
ExtendLease(ctx context.Context, t *Task, lease time.Duration) (time.Time, error)
// FinalizeBatch applies many Complete/Fail outcomes in one round trip.
// Each entry is fenced individually, exactly like Complete/Fail; an
// entry whose lease was lost is skipped, not an error. Returns how many
// entries were skipped that way (for observability).
FinalizeBatch(ctx context.Context, outcomes []Outcome) (lost int64, err error)
// Requeue resets one dead task to pending: attempts back to 0, runnable
// now, retention expiry cleared. The errors array is kept as history.
// Returns ErrNotFound if the id doesn't exist or the task isn't dead.
Requeue(ctx context.Context, id string) error
// RequeueDead requeues every dead task (of taskType, or all types when
// taskType is ""), with the same resets as Requeue. Returns the count.
RequeueDead(ctx context.Context, taskType string) (int64, error)
// RegisterQueues records the declared queue policies in the queues
// registry. A queue not yet registered is inserted; an identical
// redeclaration is a no-op; a DIFFERENT policy for a registered queue
// fails with an error matching ErrQueueConflict (drift guard).
RegisterQueues(ctx context.Context, queues []QueuePolicy) error
// Queues lists every registered queue policy (tools/UI/introspection).
Queues(ctx context.Context) ([]QueuePolicy, error)
// SetQueuePolicy deliberately overwrites (or creates) one registered
// queue policy — the explicit ops action for changing a policy;
// managers still running the old declaration will then fail their
// next registration check.
SetQueuePolicy(ctx context.Context, q QueuePolicy) error
// EnsureMetrics prepares metrics storage (indexes incl. the retention
// TTL). Called once by the janitor when metrics are enabled; a changed
// retention updates the TTL in place.
EnsureMetrics(ctx context.Context, retention time.Duration) error
// SnapshotMetrics computes and stores one cluster-wide gauges document
// per queue for the window starting at windowStart (status counts +
// oldest DUE-task age). Managers race per window; the first insert
// wins and the rest are silently skipped.
SnapshotMetrics(ctx context.Context, queues []string, windowStart time.Time, window time.Duration) error
// RecordCounters stores one document per queue with this manager
// instance's exact throughput counters for the window (no dedup —
// every manager writes its own; readers sum across managers).
RecordCounters(ctx context.Context, managerID string, windowStart time.Time, window time.Duration, counters map[string]QueueCounters) error
// ReapExpired marks running tasks whose lease expired AND whose attempts
// are exhausted as dead (with a "lease expired" error entry), so dropped
// tasks are visible instead of zombie "running" docs. Tasks with
// attempts remaining are NOT touched — Claim reclaims those naturally.
// Returns the number of tasks reaped.
ReapExpired(ctx context.Context) (int64, error)
// Close releases the store's resources.
Close(ctx context.Context) error
}
Store is the persistence backend. Implementations must make Claim atomic: under concurrent claims, each runnable task is handed to exactly one caller.
Fencing contract: Claim generates a fresh lease token per claim. Complete/Fail/ExtendLease must only match a task that is still running under the token carried by the passed *Task, and return ErrLeaseLost otherwise. Matching must NOT require the lease to still be unexpired — finishing after expiry but before anyone reclaimed the task is a success, not a conflict.
Unique-key contract: at most one task with a given UniqueKey may be active (pending or running) at a time; a conflicting enqueue returns *DuplicateTaskError. The key is released when the task reaches done/dead.
type Task ¶
type Task struct {
ID string `json:"id"`
Queue string `json:"queue"` // logical queue name (explicit; no default queue exists)
Type string `json:"type"`
UniqueKey string `json:"unique_key,omitempty"` // dedup key: at most one pending/running task per key
Payload json.RawMessage `json:"payload,omitempty"`
Status Status `json:"status"`
Attempts int `json:"attempts"` // claims so far (a claim IS an attempt)
MaxAttempts int `json:"max_attempts"` // 1 = at-most-once: never retried, stale never reclaimed
RunAt time.Time `json:"run_at"` // eligible-to-run time (scheduling / retry backoff)
ExpiresAt *time.Time `json:"expires_at,omitempty"` // total-lifetime purge deadline, stamped at creation; nil = never
LeasedBy string `json:"leased_by,omitempty"`
LeasedUntil time.Time `json:"leased_until,omitempty"`
LeaseToken string `json:"-"` // fencing token; regenerated on every claim
Errors []TaskError `json:"errors,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Task is one unit of work. Payload and Result are JSON; use the typed Enqueue / RegisterHandler API instead of touching them directly.
type TaskError ¶
type TaskError struct {
At time.Time `bson:"at" json:"at"`
Attempt int `bson:"attempt" json:"attempt"`
Worker string `bson:"worker" json:"worker"`
Message string `bson:"message" json:"message"`
}
TaskError is one failed attempt, kept on the task in order.
type TaskPolicy ¶ added in v0.6.0
type TaskPolicy struct {
// RunAt schedules the task for an absolute time (wins over Delay).
RunAt time.Time
// Delay schedules the task for now+Delay.
Delay time.Duration
// MaxAttempts overrides the manager's DefaultMaxAttempts; 1 =
// at-most-once.
MaxAttempts int
// UniqueKey makes the enqueue idempotent: at most one active
// (pending/running) task per key; conflicts return the existing id
// with an error matching ErrDuplicateTask. Single-task enqueues only.
UniqueKey string
}
TaskPolicy carries the optional per-task knobs for Enqueue/EnqueueMany. The zero value means "all defaults": run now, inherited max attempts (queue's, else manager's), no unique key.
type Watcher ¶
type Watcher interface {
WatchRunnable(ctx context.Context, types, queues []string) (<-chan struct{}, error)
}
Watcher is an optional Store capability: pushing "work may be available" wakeup signals so managers don't rely on polling while idle. The manager type-asserts for it at Start; stores without it (or whose deployment can't support it) are simply polled.
Contract: the returned channel is a coalesced, level-triggered nudge — capacity 1, dropped when full; a receive means "claim now, something may be runnable". False positives are fine (one empty claim); implementations should be conservative the other way. The implementation is responsible for reconnection (nudging after any blind window) and for signaling when known future run_at times come due. The channel closes when ctx ends. An error return means watching is unavailable (e.g. no replica set) and the caller should fall back to polling.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package dashboard serves the gotasks dashboard: an embeddable http.Handler exposing a JSON API and a single-page frontend over the tasks, queues and metrics collections.
|
Package dashboard serves the gotasks dashboard: an embeddable http.Handler exposing a JSON API and a single-page frontend over the tasks, queues and metrics collections. |
|
examples
|
|
|
atmostonce
command
At-most-once: TaskPolicy{MaxAttempts: 1} is for non-idempotent, credit-spending work (provisioning, charging a card).
|
At-most-once: TaskPolicy{MaxAttempts: 1} is for non-idempotent, credit-spending work (provisioning, charging a card). |
|
batch
command
Batch fan-out and throughput: EnqueueMany inserts a whole batch in one store round-trip (this is how broadcast-style workloads fan one message out to thousands of recipients), and WithPipelineMode enables pipeline consumption: one fetcher claims up to 16 tasks per ~3 round trips and feeds the workers through a bounded channel, instead of each worker paying a contended claim query per task.
|
Batch fan-out and throughput: EnqueueMany inserts a whole batch in one store round-trip (this is how broadcast-style workloads fan one message out to thousands of recipients), and WithPipelineMode enables pipeline consumption: one fetcher claims up to 16 tasks per ~3 round trips and feeds the workers through a bounded channel, instead of each worker paying a contended claim query per task. |
|
dashboard
command
The gotasks dashboard, live: mounts dashboard.Handler on :8080 and runs a small workload generator (steady emails, occasional failures, scheduled reports) so there is something to look at.
|
The gotasks dashboard, live: mounts dashboard.Handler on :8080 and runs a small workload generator (steady emails, occasional failures, scheduled reports) so there is something to look at. |
|
heartbeat
command
Heartbeat: with WithHeartbeat, a worker extends its task's lease every LeaseTime/3 while the handler runs, so handlers may run far longer than LeaseTime without another worker reclaiming the task.
|
Heartbeat: with WithHeartbeat, a worker extends its task's lease every LeaseTime/3 while the handler runs, so handlers may run far longer than LeaseTime without another worker reclaiming the task. |
|
queues
command
Named queues: tasks carry a queue label (WithQueue at enqueue; "default" otherwise), and each manager can be pointed at specific queues with WithQueues.
|
Named queues: tasks carry a queue label (WithQueue at enqueue; "default" otherwise), and each manager can be pointed at specific queues with WithQueues. |
|
retention
command
Task lifetime (TTL): expires_at = run_at + QueuePolicy.TTL, stamped at creation; MongoDB purges the document when it passes — done, dead, or never consumed (the SQS retention model).
|
Task lifetime (TTL): expires_at = run_at + QueuePolicy.TTL, stamped at creation; MongoDB purges the document when it passes — done, dead, or never consumed (the SQS retention model). |
|
retries
command
Retries, backoff, dead-letter, and requeue: a flaky handler fails a configurable number of times per task.
|
Retries, backoff, dead-letter, and requeue: a flaky handler fails a configurable number of times per task. |
|
scheduled
command
Scheduled tasks: TaskPolicy.Delay and TaskPolicy.RunAt defer when a task becomes runnable.
|
Scheduled tasks: TaskPolicy.Delay and TaskPolicy.RunAt defer when a task becomes runnable. |
|
simple
command
A minimal end-to-end example: enqueue a few email tasks (one scheduled), run a worker pool against local MongoDB, stop cleanly on Ctrl-C.
|
A minimal end-to-end example: enqueue a few email tasks (one scheduled), run a worker pool against local MongoDB, stop cleanly on Ctrl-C. |
|
unique
command
Unique keys: TaskPolicy.UniqueKey guarantees at most one active (pending or running) task per key.
|
Unique keys: TaskPolicy.UniqueKey guarantees at most one active (pending or running) task per key. |
|
internal
|
|
|
chaosworker
command
chaosworker is the chaos test's fixture: a real gotasks worker process meant to be SIGKILLed mid-flight.
|
chaosworker is the chaos test's fixture: a real gotasks worker process meant to be SIGKILLed mid-flight. |
|
stores
|
|
|
memstore
Package memstore is an in-memory gotasks.Store for unit tests and local development: run real managers, handlers and enqueue flows with no MongoDB, no Docker, in milliseconds, deterministically.
|
Package memstore is an in-memory gotasks.Store for unit tests and local development: run real managers, handlers and enqueue flows with no MongoDB, no Docker, in milliseconds, deterministically. |
|
mongostore
Package mongostore implements the gotasks Store on MongoDB (>= 4.2).
|
Package mongostore implements the gotasks Store on MongoDB (>= 4.2). |