Documentation
¶
Overview ¶
Package quacker is an embeddable task and workflow orchestration engine — a single-process alternative to Hatchet. State lives in in-memory SQLite (or a file, if you want durability), tasks are plain Go functions, and execution state is readable at any moment without pausing execution:
q := quacker.Open() // ephemeral in-memory state
task := quacker.NewTask("greet", func(ctx context.Context, in Input) (string, error) {
return "hi " + in.Name, nil
})
h, _ := quacker.Enqueue(ctx, q, task, Input{Name: "world"})
out, _ := h.Result(ctx)
snap, _ := q.Execution(ctx, h.RunID()) // QUEUED/RUNNING/... at any instant
Example ¶
Example is the smallest useful program: define a task, enqueue it, await the result.
q, _ := Open(WithStorage(Memory()))
defer q.Close(context.Background())
greet := NewTask("greet", func(ctx context.Context, name string) (string, error) {
return "hello " + name, nil
})
h, _ := Enqueue(context.Background(), q, greet, "world")
out, _ := h.Result(context.Background())
fmt.Println(out)
Output: hello world
Index ¶
- Constants
- Variables
- func Cron[I any, O any](q *Quacker, name, spec string, t *Task[I, O], input I) error
- func DepOutput[T any](ctx context.Context, name string) (T, error)
- func EnqueueBatchTx[I any, O any](ctx context.Context, tx *sql.Tx, q *Quacker, t *Task[I, O], inputs []I, ...) ([]string, error)
- func EnqueueTx[I any, O any](ctx context.Context, tx *sql.Tx, q *Quacker, t *Task[I, O], input I, ...) (string, error)
- func Off[I any, O any](q *Quacker, event string, t *Task[I, O]) error
- func On[I any, O any](q *Quacker, event string, t *Task[I, O]) error
- func Register[I, O any](q *Quacker, t *Task[I, O])
- func RunIDFromContext(ctx context.Context) string
- func RunOnce[T any](ctx context.Context, key string, fn func() (T, error)) (T, error)
- func SleepDurable(ctx context.Context, d time.Duration) error
- func TaskLogger(ctx context.Context) *slog.Logger
- func WaitFor[T any](ctx context.Context, event string, timeout time.Duration) (T, error)
- func WithLogSink(ctx context.Context, fn func(LogEntry)) context.Context
- type Backoff
- type Codec
- type ConcurrencyStrategy
- type CronInfo
- type DAG
- type DAGEdge
- type DAGGroup
- type DAGNode
- type DeadLetterFilter
- type DebugRecord
- type EmitInfo
- type EnqueueInfo
- type EnqueueOption
- type Event
- type EventRecord
- type Execution
- type Group
- type Handler
- type Hooks
- type JSONCodec
- type Kind
- type LogEntry
- type Metrics
- type MetricsSnapshot
- type Middleware
- type Option
- func WithCheckpointInterval(d time.Duration) Option
- func WithCodec(c Codec) Option
- func WithLeaseTTL(d time.Duration) Option
- func WithLogStorage(enabled bool) Option
- func WithLogger(l *slog.Logger) Option
- func WithMetricsFunc(fn func(*Metrics)) Option
- func WithMetricsInterval(d time.Duration) Option
- func WithMiddleware(mw ...Middleware) Option
- func WithPayloadKey(key []byte) Option
- func WithPlugin(p Plugin) Option
- func WithPollInterval(d time.Duration) Option
- func WithQueue(name string, concurrency int) Option
- func WithRate(queue string, n int, per time.Duration) Option
- func WithRetention(p RetentionPolicy) Option
- func WithStorage(s Storage) Option
- func WithTaskLogSink(fn func(LogEntry)) Option
- func WithTracerProvider(tp trace.TracerProvider) Option
- func WithWorkerID(id string) Option
- func WithWorkerLabels(labels ...string) Option
- type Plugin
- type PurgeOptions
- type PurgeResult
- type Quacker
- func (q *Quacker) Cancel(runID string) error
- func (q *Quacker) Close(ctx context.Context) error
- func (q *Quacker) Crons() []CronInfo
- func (q *Quacker) DAG(ctx context.Context, runID string) (*DAG, error)
- func (q *Quacker) DAGJSON(ctx context.Context, runID string) ([]byte, error)
- func (q *Quacker) DAGSVG(ctx context.Context, runID string) ([]byte, error)
- func (q *Quacker) DAGTree(ctx context.Context, runID string) (*DAG, error)
- func (q *Quacker) DAGTreeJSON(ctx context.Context, runID string) ([]byte, error)
- func (q *Quacker) DAGTreeSVG(ctx context.Context, runID string) ([]byte, error)
- func (q *Quacker) DeadLetters(ctx context.Context, f DeadLetterFilter) ([]RunSummary, error)
- func (q *Quacker) DebugLogger() *slog.Logger
- func (q *Quacker) DebugLogs() <-chan DebugRecord
- func (q *Quacker) DismissDeadLetter(ctx context.Context, runID string) error
- func (q *Quacker) Emit(ctx context.Context, event string, payload any) (int, error)
- func (q *Quacker) Events(ctx context.Context, limit int) ([]EventRecord, error)
- func (q *Quacker) Execution(ctx context.Context, runID string) (*Execution, error)
- func (q *Quacker) Logs(ctx context.Context, runID string, limit int) ([]LogEntry, error)
- func (q *Quacker) Metrics(ctx context.Context) (*Metrics, error)
- func (q *Quacker) PauseQueue(ctx context.Context, name string) error
- func (q *Quacker) PauseRun(ctx context.Context, runID string) error
- func (q *Quacker) PausedQueues(ctx context.Context) ([]string, error)
- func (q *Quacker) Purge(ctx context.Context, opts PurgeOptions) (PurgeResult, error)
- func (q *Quacker) RemoveCron(name string) error
- func (q *Quacker) ResumeQueue(ctx context.Context, name string) error
- func (q *Quacker) ResumeRun(ctx context.Context, runID string) error
- func (q *Quacker) RetryDeadLetter(ctx context.Context, runID string) error
- func (q *Quacker) Runs(ctx context.Context, f RunFilter) ([]RunSummary, error)
- func (q *Quacker) SetQueue(name string, concurrency int)
- func (q *Quacker) SetRateLimit(name string, n int, window time.Duration)
- func (q *Quacker) Snooze(ctx context.Context, runID string, until time.Time) error
- func (q *Quacker) SnoozeFor(ctx context.Context, runID string, d time.Duration) error
- func (q *Quacker) Subscribe(runID string) (<-chan Event, func())
- func (q *Quacker) Use(mw ...Middleware)
- type QueueStats
- type RetentionPolicy
- type RunError
- type RunFilter
- type RunGroup
- type RunHandle
- func Enqueue[I any, O any](ctx context.Context, q *Quacker, t *Task[I, O], input I, opts ...EnqueueOption) (*RunHandle[O], error)
- func EnqueueBatch[I any, O any](ctx context.Context, q *Quacker, t *Task[I, O], inputs []I, ...) ([]*RunHandle[O], error)
- func EnqueueChild[I any, O any](ctx context.Context, q *Quacker, t *Task[I, O], input I, opts ...EnqueueOption) (*RunHandle[O], error)
- func EnqueueWorkflow[O any, I any](ctx context.Context, q *Quacker, wf *Workflow[I], input I, ...) (*RunHandle[O], error)
- func EnqueueWorkflowChild[O any, I any](ctx context.Context, q *Quacker, wf *Workflow[I], input I, ...) (*RunHandle[O], error)
- type RunInfo
- type RunSummary
- type Status
- type StepContext
- type StepDef
- type StepInfo
- type StepState
- type Storage
- type Task
- type TaskOption
- func BackoffPolicy(b Backoff) TaskOption
- func Queue(name string) TaskOption
- func Retries(n int) TaskOption
- func Timeout(d time.Duration) TaskOption
- func WithDeadLetter() TaskOption
- func WithEphemeral() TaskOption
- func WithKey[I any](fn func(I) string) TaskOption
- func WithKeyConcurrency(n int) TaskOption
- func WithKeyLimit[I any](name string, fn func(I) string, limit int) TaskOption
- func WithKeyStrategy(s ConcurrencyStrategy) TaskOption
- func WithLabels(labels ...string) TaskOption
- func WithSequence[I any](fn func(I) string) TaskOption
- func WithUnique[I any](fn func(I) string) TaskOption
- func WithUniqueConflict(m UniqueConflict) TaskOption
- func Wrap(mw ...Middleware) TaskOption
- type UniqueConflict
- type Workflow
Examples ¶
Constants ¶
const ( // UniqueReuse returns the live run's handle and creates nothing. UniqueReuse = store.UniqueReuse // UniqueError fails the enqueue with ErrDuplicateJob. UniqueError = store.UniqueError // UniqueReplace cancels the live run and enqueues the new one. UniqueReplace = store.UniqueReplace )
const ( // ConcurrencyHold queues the run until a key slot frees (default). ConcurrencyHold = store.ConcurrencyHold // ConcurrencyCancelInProgress cancels running (else oldest) same-key runs // to make room for the new one. ConcurrencyCancelInProgress = store.ConcurrencyCancelInProgress // ConcurrencyCancelNewest cancels the incoming run. ConcurrencyCancelNewest = store.ConcurrencyCancelNewest // ConcurrencyCancelQueuedExceptNewest keeps the newest keyLimit queued // same-key runs and cancels older ones. ConcurrencyCancelQueuedExceptNewest = store.ConcurrencyCancelQueuedExceptNewest // ConcurrencyCancelQueuedExceptOldest keeps the oldest keyLimit queued // same-key runs and cancels newer ones. ConcurrencyCancelQueuedExceptOldest = store.ConcurrencyCancelQueuedExceptOldest )
const Version = "v1.4.0"
Version is the released semantic version of this build. Releases are tagged vMAJOR.MINOR.PATCH (see docs/STABILITY.md); fixes bump the patch component.
Variables ¶
var ( // ErrRunCancelled is returned by Result when the run was cancelled. ErrRunCancelled = engine.ErrRunCancelled // ErrRunInterrupted is returned by Result when the engine shut down // before the run finished. ErrRunInterrupted = engine.ErrRunInterrupted // ErrNotFound is returned by Execution for unknown run IDs. ErrNotFound = store.ErrNotFound // ErrClosed is returned by enqueue calls after Close has begun. ErrClosed = engine.ErrClosed // ErrRunRunning is returned by Snooze for a run with a RUNNING step. ErrRunRunning = store.ErrRunRunning // ErrRunTerminal is returned by Snooze (and PauseRun) for a finished run. ErrRunTerminal = store.ErrRunTerminal // ErrNotDeadLetter is returned by RetryDeadLetter/DismissDeadLetter for a // run that is not dead-lettered. ErrNotDeadLetter = store.ErrNotDeadLetter // ErrRunGone is returned by Result for an ephemeral run that was deleted // before its result could be observed. ErrRunGone = engine.ErrRunGone )
Sentinel errors surfaced by Result and Cancel.
var ErrDuplicateJob = store.ErrDuplicateJob
ErrDuplicateJob is returned by an enqueue under UniqueError when a live run already holds the unique key.
var ErrExternalTxUnsupported = engine.ErrExternalTxUnsupported
ErrExternalTxUnsupported is returned by EnqueueTx/EnqueueBatchTx on storage that cannot share a caller transaction (SQLite).
var ErrJournalMisaligned = engine.ErrJournalMisaligned
ErrJournalMisaligned is returned when a durable task's replayed durable calls do not match its persisted journal — almost always because task code changed while runs were suspended. It fails the step loudly instead of resuming with wrong data.
var ErrNonTerminalPurge = store.ErrNonTerminalPurge
ErrNonTerminalPurge is returned when a purge is asked to delete a non-terminal status. Only terminal runs may ever be purged.
var ErrWaitTimeout = engine.ErrWaitTimeout
ErrWaitTimeout is returned by WaitFor when its timeout elapses before the awaited event arrives.
Functions ¶
func Cron ¶
Cron registers a trigger that enqueues task with input on the cron spec (standard 5-field syntax plus descriptors like "@hourly" and "@every 5m"). Crons are in-memory: re-register at startup, ideally before Open resumes work.
func DepOutput ¶
DepOutput decodes the recorded output of a succeeded dependency step. Only usable inside workflow steps that declared the named step as a dependency via Step(name, task, "dependency-name").
func EnqueueBatchTx ¶
func EnqueueBatchTx[I any, O any](ctx context.Context, tx *sql.Tx, q *Quacker, t *Task[I, O], inputs []I, opts ...EnqueueOption) ([]string, error)
EnqueueBatchTx is EnqueueTx for several inputs on the caller's *sql.Tx. It returns the new run ids in input order.
func EnqueueTx ¶
func EnqueueTx[I any, O any](ctx context.Context, tx *sql.Tx, q *Quacker, t *Task[I, O], input I, opts ...EnqueueOption) (string, error)
EnqueueTx inserts one run of task on the caller's *sql.Tx and returns its id without a handle. It never commits: the caller's Commit makes the run and any business writes in the same transaction visible atomically, which is the outbox/idempotency pattern. The run may execute on any engine once committed, so wait via Execution rather than a handle. The caller's tx must target the same Postgres/MySQL database as the engine; SQLite returns ErrExternalTxUnsupported.
func On ¶
On binds an event name to a task: every Emit of that event enqueues one run of t with the event payload as the task input. The binding is persisted, so with File storage it re-arms on the next Open once the task is registered (typically via Register). Registering the same (event, task) pair again is idempotent and refreshes the task definition.
func Register ¶
Register makes a task executable without enqueuing a run. Needed when using File storage: register tasks at startup so runs recovered from the previous process can execute. Registration also happens automatically on Enqueue and Cron.
func RunIDFromContext ¶
RunIDFromContext returns the current run's ID inside a task function (empty otherwise).
func RunOnce ¶
RunOnce runs fn at most once per call site across suspensions and retries, memoizing its result. Use it for side effects that must not repeat when a durable task replays after a SleepDurable or WaitFor. fn's error is propagated without being memoized, so a retry re-runs fn (exactly-once on success, at-least-once on failure). It never suspends. Only valid inside a task or workflow step.
func SleepDurable ¶
SleepDurable suspends the running step for at least d, durably. It does not consume an attempt or hold a queue or per-key slot while sleeping, and it survives a process restart (File storage): when the wake time passes the step is claimed again and the task is re-invoked from the top, at which point this call returns nil immediately.
Because the task re-runs from the beginning after a suspension, any side effects before a durable await happen again on resume; wrap them in RunOnce to execute exactly once. Do not recover() around a durable helper. Only valid inside a task or workflow step.
Example ¶
ExampleSleepDurable suspends a run without holding a worker slot and resumes it; RunOnce makes the side effect happen once across the replay.
q, _ := Open(WithStorage(Memory()), WithPollInterval(time.Millisecond))
defer q.Close(context.Background())
task := NewTask("job", func(ctx context.Context, in string) (string, error) {
reservations, err := RunOnce(ctx, "reserve", func() (int, error) { return 1, nil })
if err != nil {
return "", err
}
if err := SleepDurable(ctx, time.Millisecond); err != nil {
return "", err
}
return fmt.Sprintf("done %s (%d)", in, reservations), nil
})
h, _ := Enqueue(context.Background(), q, task, "x")
out, _ := h.Result(context.Background())
fmt.Println(out)
Output: done x (1)
func TaskLogger ¶
TaskLogger returns a logger whose lines are persisted with the run and readable via Logs. Outside a task function it returns slog's default.
func WaitFor ¶
WaitFor suspends the running step until an event named event is emitted (via Quacker.Emit), then decodes the event's payload into T. A timeout > 0 bounds the wait and returns ErrWaitTimeout; timeout <= 0 waits indefinitely.
Delivery is durable: when an event is emitted the engine records it and wakes every waiting step in one transaction, so a wait survives a restart. Only events emitted after the wait registers count — an event emitted earlier does not satisfy a later WaitFor. Emitting also triggers any On bindings as usual. Only valid inside a task or workflow step.
func WithLogSink ¶
WithLogSink returns a context whose TaskLogger writes each line to fn instead of the engine's configured sink. Outside a task function it is a no-op; a nil fn is ignored. Middleware uses it to route task logs to its own storage or forwarder.
fn runs synchronously in the task's goroutine, so it must be fast — buffer or hand off if your destination can block.
Types ¶
type Backoff ¶
Backoff configures retry delays. A zero value is not "no delay": empty fields fall back to the library defaults (base 500ms, factor 2, cap 30s, no jitter) — use Exponential or Constant (or set fields) for real policies.
func Exponential ¶
Exponential returns a backoff that doubles from base, capped at 30s, with the default 10% upward jitter.
type Codec ¶
Codec (de)serializes user payloads: task inputs and outputs, dependency outputs, event payloads, and durable journal values. Schema structures (steps.depends_on and steps.labels) are always JSON, because the SQL gates read them, so a custom codec does not affect them.
The same codec must be used across restarts for a given database, since journal values and dependency outputs are decoded by the engine that encoded them.
func NewEncryptedJSONCodec ¶
NewEncryptedJSONCodec returns a Codec that encrypts payloads with AES-256-GCM under a 32-byte key; it errors on any other key length.
type ConcurrencyStrategy ¶
type ConcurrencyStrategy = store.ConcurrencyStrategy
ConcurrencyStrategy selects what an enqueue does when a key is at capacity, set per task with WithKeyStrategy.
type CronInfo ¶
type CronInfo struct {
Name string `json:"name"`
Spec string `json:"spec"`
Task string `json:"task"`
Next time.Time `json:"next"`
}
CronInfo describes a registered cron trigger.
type DAG ¶
type DAG struct {
RunID string `json:"run_id"`
Workflow string `json:"workflow"`
Kind Kind `json:"kind"`
Status Status `json:"status"`
Queue string `json:"queue"`
Nodes []DAGNode `json:"nodes"`
Edges []DAGEdge `json:"edges"`
Groups []DAGGroup `json:"groups,omitempty"`
}
DAG is a run's step graph with the current state of every node. It is a point-in-time snapshot: reads never pause execution, so a later call sees later states.
type DAGGroup ¶ added in v1.1.0
type DAGGroup struct {
Name string `json:"name"` // run id
Label string `json:"label"` // human label, e.g. "partition_jobs #3"
Status Status `json:"status"`
// Parent is the enclosing group's Name, or "" when this group hangs off
// the root run. It lets the renderer nest clusters instead of guessing
// containment from node coordinates.
Parent string `json:"parent,omitempty"`
// Deps are group-level dependencies: names of other groups this whole
// group waits on. They render as box-to-box edges; individual member
// steps do not repeat them.
Deps []string `json:"deps,omitempty"`
// Steps are the group's member step names (a flat grouped workflow). Empty
// for a run-tree group, whose members are the child run's own nodes.
Steps []string `json:"steps,omitempty"`
}
DAGGroup is one run's cluster of nodes within a DAGTree — the root run or a child run — drawn as a labelled box behind its nodes. A plain single-run DAG has no groups.
type DAGNode ¶
type DAGNode struct {
Name string `json:"name"`
// Task is the registered task that executes the step (differs from Name
// for named workflow steps).
Task string `json:"task,omitempty"`
Status Status `json:"status"`
Deps []string `json:"deps,omitempty"`
Attempts int `json:"attempts"`
MaxAttempts int `json:"max_attempts"`
Error string `json:"error,omitempty"`
// Group is the run this node belongs to, when the DAG spans several runs
// (a DAGTree). Empty for a single-run DAG.
Group string `json:"group,omitempty"`
}
DAGNode is one step of a run's DAG with its current state.
type DeadLetterFilter ¶
type DeadLetterFilter struct {
// Workflow and Queue, when non-empty, restrict the result.
Workflow string
Queue string
// Limit caps the result (default 100); Offset skips that many.
Limit int
Offset int
}
DeadLetterFilter selects dead-lettered runs for DeadLetters.
type DebugRecord ¶
type DebugRecord = engine.DebugRecord
DebugRecord is one engine debug log line, streamed via DebugLogs.
type EnqueueInfo ¶
type EnqueueInfo = engine.EnqueueInfo
EnqueueInfo describes a run about to be enqueued.
type EnqueueOption ¶
type EnqueueOption func(*enqueueConfig)
EnqueueOption configures a single enqueued run.
func WithDelay ¶
func WithDelay(d time.Duration) EnqueueOption
WithDelay schedules the run to start at least d from now.
func WithPriority ¶
func WithPriority(p int64) EnqueueOption
WithPriority orders the run ahead of lower-priority work in the same queue. Higher numbers run first.
func WithRunAt ¶
func WithRunAt(t time.Time) EnqueueOption
WithRunAt schedules the run to start no earlier than t.
func WithUniqueKey ¶
func WithUniqueKey(key string) EnqueueOption
WithUniqueKey sets the run's unique key explicitly, overriding a task's WithUnique. The key is scoped to the task/workflow name, and "" means the run is not unique.
type Event ¶
type Event struct {
RunID string `json:"run_id"`
Step string `json:"step,omitempty"` // empty for run-level transitions
From Status `json:"from"`
To Status `json:"to"`
At time.Time `json:"at"`
Error string `json:"error,omitempty"`
}
Event is a live status transition delivered by Subscribe.
type EventRecord ¶
type EventRecord struct {
Name string `json:"name"`
Payload json.RawMessage `json:"payload,omitempty"`
At time.Time `json:"at"`
}
EventRecord is one persisted emit, returned by Events.
type Execution ¶
type Execution struct {
RunID string `json:"run_id"`
Workflow string `json:"workflow"`
Kind Kind `json:"kind"`
Status Status `json:"status"`
Queue string `json:"queue"`
Priority int64 `json:"priority"`
Attempts int `json:"attempts"`
MaxAttempts int `json:"max_attempts"`
Input json.RawMessage `json:"input,omitempty"`
Output json.RawMessage `json:"output,omitempty"`
Error string `json:"error,omitempty"`
// Key mirrors the first step's concurrency key (per-step keys in
// workflows may differ).
Key string `json:"key,omitempty"`
// TraceParent is the W3C traceparent captured at enqueue ("" without
// tracing), for correlating this run with your traces.
TraceParent string `json:"trace_parent,omitempty"`
RunAt time.Time `json:"run_at"`
CreatedAt time.Time `json:"created_at"`
StartedAt time.Time `json:"started_at,omitempty"`
CompletedAt time.Time `json:"completed_at,omitempty"`
Steps []StepState `json:"steps"`
// Groups are the run's logical groups (from a grouped workflow), each with
// its group-level dependencies and member step names. Empty for a plain
// workflow.
Groups []RunGroup `json:"groups,omitempty"`
// Children are runs enqueued from inside this run (via EnqueueChild),
// oldest first. They run independently; the parent does not wait for them.
Children []RunSummary `json:"children,omitempty"`
}
Execution is a consistent point-in-time view of a run. Reads never pause task execution.
type Group ¶ added in v1.2.0
type Group[I any] struct { // contains filtered or unexported fields }
Group is a named set of workflow steps with a group-level dependency: the whole group runs after the groups named in After, and member steps only declare their intra-group dependencies. Group deps expand to member steps at enqueue, so nothing extra runs and no gate step is needed.
func NewGroup ¶ added in v1.2.0
NewGroup creates a group named name from step declarations. A group can be passed to NewGroupWorkflow and chained with After and Concurrency.
func (*Group[I]) After ¶ added in v1.2.0
After declares that this whole group runs only after the named groups have completed. It returns the group so calls can be chained.
func (*Group[I]) Concurrency ¶ added in v1.4.0
Concurrency limits simultaneously RUNNING member steps per workflow run. A free slot admits the next eligible step immediately, without waiting for a whole batch. Zero (the default) adds no group limit; negative values are rejected at enqueue. One serializes execution but does not guarantee order. Queue and task key limits still apply. Retry backoff and durable suspension release the slot; each retry or resume must acquire it again. NewGroupWorkflow snapshots this setting when constructing the workflow.
type Handler ¶
Handler is a task body at the engine's JSON level: it receives the step context and the JSON input and returns the JSON output. Middleware wraps Handlers.
type Hooks ¶
Hooks is the set of engine lifecycle callbacks a plugin may provide; only the non-nil fields are invoked. Before* hooks run in registration order and may return an error to veto; After* hooks are observe-only, run in reverse order, and never change the outcome (their panics are recovered and logged).
A step veto (BeforeStep returning an error) fails the step immediately with no retries. Hooks are invoked concurrently for parallel steps, so a plugin must be safe for concurrent use. In-process plugins are trusted code.
type LogEntry ¶
type LogEntry struct {
RunID string `json:"run_id"`
Step string `json:"step"`
At time.Time `json:"at"`
Level string `json:"level"`
Message string `json:"message"`
}
LogEntry is one task log line captured via TaskLogger.
type Metrics ¶
type Metrics struct {
Runs map[Status]int64 `json:"runs"`
Queues map[string]QueueStats `json:"queues"`
}
Metrics is a global state snapshot.
type MetricsSnapshot ¶
type MetricsSnapshot = Metrics
MetricsSnapshot is the value handed to the WithMetricsFunc callback; it has the same shape as Metrics.
type Middleware ¶
type Middleware = engine.Middleware
Middleware decorates task execution. Compose with WithMiddleware / Quacker.Use (engine-wide) or Wrap (per task). The first middleware registered is the outermost: global middlewares run before per-task ones, which run before the task body. A middleware may short-circuit by returning without calling next.
Inside a middleware the step identity is available via StepFromContext, dependency outputs via DepOutput, and task logs can be redirected with WithLogSink:
q.Use(func(next quacker.Handler) quacker.Handler {
return func(ctx context.Context, in json.RawMessage) (json.RawMessage, error) {
started := time.Now()
ctx = quacker.WithLogSink(ctx, myLogStore)
out, err := next(ctx, in)
observe(quacker.RunIDFromContext(ctx), time.Since(started), err)
return out, err
}
})
type Option ¶
type Option func(*config)
Option configures Open.
func WithCheckpointInterval ¶
WithCheckpointInterval sets how often WAL-backed storage (Ephemeral, File) runs a passive wal_checkpoint to bound WAL growth. The checkpoint never blocks readers or the writer. <=0 uses the 60s default; values below 10ms are clamped to 10ms. Memory storage has no WAL and never checkpoints.
func WithCodec ¶
WithCodec sets the payload codec (default JSONCodec). It applies engine-wide and is used for task input/output encoding and decoding, DepOutput, event payloads, and durable RunOnce/WaitFor values.
func WithLeaseTTL ¶
WithLeaseTTL sets how long a claimed step's lease lasts before a reaper may requeue it (multi-instance leases only). Keep it comfortably longer than a scheduler stall; a running step's lease is extended by a heartbeat at a third of this interval. Default 30s.
func WithLogStorage ¶
WithLogStorage enables the built-in SQLite task-log store (and q.Logs). It is off by default so a long-lived engine does not grow logs it never reads; when off, lines go to the base sink (engine logger unless WithTaskLogSink is set). When on and no custom sink is set, logs persist to SQLite only. Log lines are written through the same batched path as before.
func WithLogger ¶
WithLogger sets the engine logger (default slog.Default()).
func WithMetricsFunc ¶
WithMetricsFunc registers a callback invoked with a fresh snapshot every WithMetricsInterval (default 15s). Panics in the callback are recovered and logged. The callback should not block for long; it runs on its own goroutine and never delays the scheduler.
func WithMetricsInterval ¶
WithMetricsInterval sets how often the metrics callback runs. <=0 uses the 15s default; values below 100ms are clamped to 100ms. Without WithMetricsFunc no loop is started.
func WithMiddleware ¶
func WithMiddleware(mw ...Middleware) Option
WithMiddleware registers engine-wide middleware, applied outside every task's own Wrap middleware. The first registered is outermost.
func WithPayloadKey ¶
WithPayloadKey encrypts user payloads at rest with AES-256-GCM. key must be 32 bytes; Open rejects any other length, and rejects combining it with WithCodec.
Every user payload goes through the codec — task input/output, DepOutput, event payloads, and durable RunOnce/WaitFor values — so all of it is encrypted. Schema structures the SQL gates read (steps.depends_on, steps.labels, runs.unique_key, sequence keys) and task logs stay plaintext, and introspection (Execution, Events) returns ciphertext. Use the same key across restarts.
func WithPlugin ¶
WithPlugin registers a plugin's hooks with the engine. Plugins are invoked in registration order (Before* forward, After* reverse). It is the compile-time extension point: implement Plugin in your own package and pass it here — no dynamic loading, no out-of-process server.
Example ¶
ExampleWithPlugin registers a plugin that observes run completion.
finished := make(chan string, 1)
q, _ := Open(WithStorage(Memory()), WithPlugin(examplePlugin{finished: finished}))
defer q.Close(context.Background())
task := NewTask("greet", func(ctx context.Context, in string) (string, error) {
return "hi " + in, nil
})
h, _ := Enqueue(context.Background(), q, task, "world")
out, _ := h.Result(context.Background())
fmt.Println(out, <-finished) // OnRunFinished fires around Result
Output: hi world SUCCEEDED
func WithPollInterval ¶
WithPollInterval sets how often the scheduler scans for due work while runs are in flight. When the engine is idle it does not poll at all: it sleeps until the next step's due time, and a wake (an enqueue, retry, resume, or completion) interrupts that sleep. So the interval bounds how often the scheduler re-checks under load, not how late an idle run starts. Default 50ms.
func WithQueue ¶
WithQueue sets a queue's maximum concurrency. Queues that are never configured run with a default concurrency of 10.
func WithRate ¶
WithRate caps how many runs a queue may start per window — a sliding window counted over claim timestamps, so the cap survives a File-mode restart instead of allowing a fresh-process burst. The scheduler simply holds due work QUEUED while the window is full; nothing is rejected. n<=0 disables the cap; per<=0 is treated as one second.
func WithRetention ¶
func WithRetention(p RetentionPolicy) Option
WithRetention enables automatic purging of terminal runs older than the policy's OlderThan, on its own interval. Purging is storage-agnostic and bounds growth in every mode (Memory included). OlderThan must be > 0 for the loop to act; interval defaults to one minute.
Call it more than once to layer policies: a global one (empty Queue) plus per-queue overrides with their own cutoff. Each runs on its own interval and purges only the rows it selects.
func WithStorage ¶
WithStorage selects the storage backend (default Ephemeral).
func WithTaskLogSink ¶
WithTaskLogSink sets the base destination for task log lines, replacing the default engine-logger sink. It composes with WithLogStorage(true), which adds SQLite persistence in addition. A nil fn is ignored.
func WithTracerProvider ¶
func WithTracerProvider(tp trace.TracerProvider) Option
WithTracerProvider enables OpenTelemetry tracing: a span per enqueue, one per step execution (a new root linked to the enqueue span, the standard async shape), and one per emit. The library imports only the OTel API — pass the provider from your SDK/exporter setup, or omit this to disable tracing entirely (no overhead). When enabled, the W3C traceparent active at enqueue is persisted on the run, so execution links back across restarts.
func WithWorkerID ¶
WithWorkerID sets this engine's identity for multi-instance step leases. Must be unique per running engine against a shared database; empty generates one. Only meaningful for leases backends (Postgres).
func WithWorkerLabels ¶
WithWorkerLabels declares this engine's worker labels. It claims a task's steps only when their WithLabels set is a subset of these; an engine with no labels claims only unlabeled tasks. Use it to route work to the workers that can run it (e.g. "gpu", "linux").
type Plugin ¶
type Plugin interface {
// Name identifies the plugin (for logging/diagnostics).
Name() string
// Hooks returns the callbacks to install; leave fields nil to ignore them.
Hooks() Hooks
}
Plugin contributes lifecycle hooks to an engine.
type PurgeOptions ¶
type PurgeOptions struct {
// OlderThan is how long a terminal run must have been finished before it
// is eligible. Required (> 0). Purging is storage-agnostic: it bounds
// growth in every mode, including Memory.
OlderThan time.Duration
// Statuses restricts the purge to these terminal statuses; empty means
// all four. A non-terminal status returns ErrNonTerminalPurge.
Statuses []Status
// Queue, when non-empty, restricts the purge to runs in that queue.
Queue string
// ExcludeDeadLettered keeps dead-lettered runs out of the purge.
ExcludeDeadLettered bool
// KeepLogs retains the logs of purged runs (and skips the orphan sweep).
// The default (false) deletes them with the run.
KeepLogs bool
// BatchSize bounds rows deleted per transaction. <= 0 uses 500.
BatchSize int
}
PurgeOptions selects terminal runs to delete with Quacker.Purge.
type PurgeResult ¶
type PurgeResult = store.PurgeResult
PurgeResult reports how many rows a purge removed.
type Quacker ¶
type Quacker struct {
// contains filtered or unexported fields
}
Quacker is an embedded orchestration engine. Create with Open; safe for concurrent use.
func Open ¶
Open starts an engine. State is stored per WithStorage (default Ephemeral: a temp-file WAL database deleted on Close).
func (*Quacker) Cancel ¶
Cancel cancels a run. Queued/blocked steps become CANCELLED immediately; running steps have their contexts cancelled. Waiters receive ErrRunCancelled. Cancelling an unknown or finished run is a no-op.
func (*Quacker) Close ¶
Close shuts down gracefully: no new work is claimed, in-flight tasks drain until ctx expires (30s if ctx has no deadline), leftovers are marked INTERRUPTED, and pending Results return ErrRunInterrupted. Ephemeral and Memory storage are deleted; File storage is kept for the next Open.
func (*Quacker) DAG ¶
DAG returns the step graph of a run with the current state of every node. For a single-task run it is one node with no edges. Returns ErrNotFound for an unknown run. It covers only this run's own steps; use DAGTree to include child runs (spawned with EnqueueChild).
For a grouped workflow (NewGroupWorkflow), member steps carry their group in DAGNode.Group, group-level dependencies become group-to-group edges, and the expanded cross-group step edges are omitted so the graph reads as groups rather than a step mesh.
func (*Quacker) DAGJSON ¶
DAGJSON returns a run's DAG, with the current state of every step, as indented JSON — easy to persist or expose over HTTP.
func (*Quacker) DAGSVG ¶
DAGSVG returns a run's DAG rendered as a standalone SVG document: steps as status-coloured boxes in dependency order, edges as arrows.
func (*Quacker) DAGTree ¶ added in v1.1.0
DAGTree expands a run's DAG to include its child runs: each child run's steps are added, namespaced by a short child-run id to keep names unique, and attached to the step that spawned them (Recorded as runs.parent_step). It recurses into grandchildren, bounded by depth and node count.
Groups are first-class: a step that spawns child runs is contracted into the group those runs form (the step node disappears; the group box stands in for it), and the group inherits that step's dependencies as group-level edges. A group therefore runs after the groups/nodes it depends on without every member step repeating the dependency. Intra-group task edges are kept.
This is the run-level view: DAG/DAGSVG show only one run's own steps, because a child run is an independent run (EnqueueChild does not join it to the parent). DAGTree is what draws the whole family.
func (*Quacker) DAGTreeJSON ¶ added in v1.1.0
DAGTreeJSON renders the run tree (parent + child runs, with current state) as indented JSON.
func (*Quacker) DAGTreeSVG ¶ added in v1.1.0
DAGTreeSVG renders the run tree as a standalone SVG document — the parent DAG with each child run's steps attached to the step that spawned them.
func (*Quacker) DeadLetters ¶
func (q *Quacker) DeadLetters(ctx context.Context, f DeadLetterFilter) ([]RunSummary, error)
DeadLetters lists runs dead-lettered by a task with WithDeadLetter, newest first.
func (*Quacker) DebugLogger ¶
DebugLogger returns a logger whose records join the engine's debug stream. Use it to interleave your own diagnostics with the engine's.
func (*Quacker) DebugLogs ¶
func (q *Quacker) DebugLogs() <-chan DebugRecord
DebugLogs returns a bounded stream of engine debug records — claims, retries, suspensions, run completions, and anything logged through DebugLogger — so the engine can be observed without serving HTTP:
for rec := range q.DebugLogs() {
fmt.Println(rec.Level, rec.Message, rec.Attrs)
}
The stream drops records when the consumer falls behind (it never blocks the engine) and closes on Close, ending a range. Only one consumer should read it; use Subscribe for a lossless-per-subscriber transition stream.
func (*Quacker) DismissDeadLetter ¶
DismissDeadLetter clears a run's dead-letter marker without retrying it.
func (*Quacker) Emit ¶
Emit persists an event and dispatches it to every bound task, one run each, returning how many runs were enqueued. payload is marshaled as JSON (a json.RawMessage is used as-is) and becomes each task's input; a payload that does not decode to a bound task's input type fails that run at execution, like any other input. Delivery is best-effort and in-process: unbound events still persist, but a crash between the persist and the enqueues can lose those dispatches.
func (*Quacker) Execution ¶
Execution returns a snapshot of a run. Safe to call at any moment: it reads through the read-only connection pool and never blocks (or is blocked by) the workers' write path. The run row is read before its steps and step statuses only advance forward, so a terminal run is never shown with non-terminal steps. Returns ErrNotFound for unknown runs.
For workflow runs, Attempts counts total step attempts across the DAG and MaxAttempts mirrors the first step's setting; per-step values are on Steps.
func (*Quacker) Logs ¶
Logs returns up to limit task log lines for a run, oldest first. Lines are emitted with TaskLogger inside task functions and are batched (~50ms), so very recent lines may lag slightly.
func (*Quacker) PauseQueue ¶
PauseQueue stops claims from a queue until ResumeQueue. Running steps finish and new runs accumulate QUEUED. The pause is persisted, so every engine on the database observes it. Pausing an unknown queue is allowed.
func (*Quacker) PauseRun ¶
PauseRun pauses a run: its steps stop being claimed, and a currently-running step is interrupted and re-queued on resume. Idempotent; a terminal run returns ErrRunTerminal and an unknown run ErrNotFound.
func (*Quacker) PausedQueues ¶
PausedQueues returns the paused queue names.
func (*Quacker) Purge ¶
func (q *Quacker) Purge(ctx context.Context, opts PurgeOptions) (PurgeResult, error)
Purge deletes terminal runs older than opts.OlderThan, along with their steps and (unless KeepLogs) their logs, in batches. It is safe to call while work is running: a run is never eligible while any of its steps is still RUNNING. Use it to bound a long-lived process's growth.
func (*Quacker) RemoveCron ¶
RemoveCron deletes a cron trigger.
func (*Quacker) ResumeQueue ¶
ResumeQueue re-enables claims for a queue and wakes the scheduler.
func (*Quacker) ResumeRun ¶
ResumeRun returns a paused run to QUEUED. A run that is not paused is a no-op; a terminal run returns ErrRunTerminal.
func (*Quacker) RetryDeadLetter ¶
RetryDeadLetter reopens a dead-lettered run in place and returns it to the queue. It returns ErrNotDeadLetter if the run exists but is not dead-lettered, and ErrNotFound if it does not exist.
func (*Quacker) SetRateLimit ¶
SetRateLimit adjusts a queue's start-rate cap at runtime — at most n runs started per sliding window. n<=0 disables the cap; window<=0 is one second.
func (*Quacker) Snooze ¶
Snooze reschedules a non-running run to start no earlier than until. A run with a RUNNING step returns ErrRunRunning (cancel or pause it first); a terminal run returns ErrRunTerminal.
func (*Quacker) Subscribe ¶
Subscribe returns a channel of live status transitions for runID (pass "" to observe all runs) and a cancel function that stops the stream and closes the channel. Events are dropped rather than delivered late if a consumer stalls — use Execution for an authoritative snapshot.
func (*Quacker) Use ¶
func (q *Quacker) Use(mw ...Middleware)
Use appends engine-wide middleware at runtime; safe before or after work starts. The first registered middleware is the outermost wrapper.
type QueueStats ¶
type QueueStats struct {
Queued int64 `json:"queued"`
Running int64 `json:"running"`
Blocked int64 `json:"blocked"`
Suspended int64 `json:"suspended"`
}
QueueStats is a per-queue depth snapshot.
type RetentionPolicy ¶
type RetentionPolicy struct {
// OlderThan must be > 0 for the loop to act.
OlderThan time.Duration
// Statuses restricts which terminal statuses may be purged; empty means
// all of them.
Statuses []Status
// Queue, when non-empty, restricts the policy to runs in that queue. Use
// several WithRetention policies (a global one plus per-queue overrides)
// for different cutoffs per queue.
Queue string
// ExcludeDeadLettered keeps dead-lettered runs out of this policy.
ExcludeDeadLettered bool
// KeepLogs retains the logs of purged runs.
KeepLogs bool
// Interval is how often the purge runs; <= 0 uses one minute (min 1s).
Interval time.Duration
}
RetentionPolicy configures automatic purging via WithRetention.
type RunFilter ¶
type RunFilter struct {
Status Status
Queue string
Workflow string
// ParentID limits the result to a run's children.
ParentID string
Limit int
Offset int
}
RunFilter selects runs for Runs.
type RunGroup ¶ added in v1.2.0
type RunGroup struct {
Name string `json:"name"`
// Deps are the names of groups this group runs after.
Deps []string `json:"deps,omitempty"`
// Steps are the member step names.
Steps []string `json:"steps"`
// MaxConcurrency caps RUNNING member steps in this run. Zero adds no limit.
MaxConcurrency int `json:"max_concurrency,omitempty"`
}
RunGroup is a named set of steps that share a group-level dependency. Group deps gate the whole group; member steps do not repeat them.
type RunHandle ¶
type RunHandle[O any] struct { // contains filtered or unexported fields }
RunHandle tracks an enqueued run.
func Enqueue ¶
func Enqueue[I any, O any](ctx context.Context, q *Quacker, t *Task[I, O], input I, opts ...EnqueueOption) (*RunHandle[O], error)
Enqueue enqueues one run of task with input.
func EnqueueBatch ¶
func EnqueueBatch[I any, O any](ctx context.Context, q *Quacker, t *Task[I, O], inputs []I, opts ...EnqueueOption) ([]*RunHandle[O], error)
EnqueueBatch enqueues one run of task per input in a single write transaction, returning handles in input order. It is all-or-nothing: a validation or insert failure creates no runs. Use it to fan out many runs cheaply instead of calling Enqueue in a loop.
func EnqueueChild ¶
func EnqueueChild[I any, O any](ctx context.Context, q *Quacker, t *Task[I, O], input I, opts ...EnqueueOption) (*RunHandle[O], error)
EnqueueChild enqueues one run of task as a child of the run currently executing (from RunIDFromContext). It is only valid inside a task or workflow step; the child runs independently (no implicit join), and Execution exposes it under Children.
func EnqueueWorkflow ¶
func EnqueueWorkflow[O any, I any](ctx context.Context, q *Quacker, wf *Workflow[I], input I, opts ...EnqueueOption) (*RunHandle[O], error)
EnqueueWorkflow enqueues a workflow run. All steps receive input; the handle's Result decodes the output of the last declared step, so callers specify O explicitly while I is inferred from the workflow:
h, err := quacker.EnqueueWorkflow[Shipment](ctx, q, wf, order)
func EnqueueWorkflowChild ¶
func EnqueueWorkflowChild[O any, I any](ctx context.Context, q *Quacker, wf *Workflow[I], input I, opts ...EnqueueOption) (*RunHandle[O], error)
EnqueueWorkflowChild enqueues a workflow run as a child of the currently executing run. Only valid inside a task or workflow step.
func (*RunHandle[O]) Done ¶
func (h *RunHandle[O]) Done() <-chan struct{}
Done returns a channel closed when the run reaches a terminal status.
type RunSummary ¶
type RunSummary struct {
RunID string `json:"run_id"`
Workflow string `json:"workflow"`
Kind Kind `json:"kind"`
Status Status `json:"status"`
Queue string `json:"queue"`
Priority int64 `json:"priority"`
Error string `json:"error,omitempty"`
ParentID string `json:"parent_id,omitempty"`
// ParentStep is the step (in ParentID) that spawned this run.
ParentStep string `json:"parent_step,omitempty"`
CreatedAt time.Time `json:"created_at"`
StartedAt time.Time `json:"started_at,omitempty"`
CompletedAt time.Time `json:"completed_at,omitempty"`
// DeadLetteredAt is set when the run was dead-lettered (zero otherwise).
DeadLetteredAt time.Time `json:"dead_lettered_at,omitempty"`
// Ephemeral is true for a run that is deleted on terminal.
Ephemeral bool `json:"ephemeral,omitempty"`
}
RunSummary is a run without payloads, for list views.
type Status ¶
type Status string
Status is the lifecycle state of a run or step.
const ( StatusQueued Status = store.StatusQueued StatusRunning Status = store.StatusRunning StatusBlocked Status = store.StatusBlocked StatusSuspended Status = store.StatusSuspended StatusPaused Status = store.StatusPaused StatusSucceeded Status = store.StatusSucceeded StatusFailed Status = store.StatusFailed StatusCancelled Status = store.StatusCancelled StatusInterrupted Status = store.StatusInterrupted )
type StepContext ¶
type StepContext = engine.StepContext
StepContext identifies the currently executing step inside a task function.
func StepFromContext ¶
func StepFromContext(ctx context.Context) (StepContext, bool)
StepFromContext returns the executing step's identity inside a task function (ok=false otherwise).
type StepDef ¶
type StepDef[I any] struct { // contains filtered or unexported fields }
StepDef declares one step of a workflow. Create with Step.
type StepState ¶
type StepState struct {
Name string `json:"name"`
// Task is the registered task that executes the step; it differs from
// Name for named workflow steps.
Task string `json:"task,omitempty"`
Status Status `json:"status"`
Deps []string `json:"deps,omitempty"`
// Key is the step's concurrency key (empty when unkeyed). Steps
// sharing a key run at most KeyConcurrency at a time.
Key string `json:"key,omitempty"`
Attempts int `json:"attempts"`
MaxAttempts int `json:"max_attempts"`
Timeout time.Duration `json:"timeout,omitempty"`
Input json.RawMessage `json:"input,omitempty"`
Output json.RawMessage `json:"output,omitempty"`
Error string `json:"error,omitempty"`
RunAt time.Time `json:"run_at"`
CreatedAt time.Time `json:"created_at"`
StartedAt time.Time `json:"started_at,omitempty"`
CompletedAt time.Time `json:"completed_at,omitempty"`
// WaitEvent is the event a SUSPENDED step awaits ("" for a sleep or a
// non-suspended step); ResumeAt is when a sleeping/suspended step becomes
// claimable again (zero for event-only waits).
WaitEvent string `json:"wait_event,omitempty"`
ResumeAt time.Time `json:"resume_at,omitempty"`
// Labels are the worker labels the step requires (empty = any engine).
Labels []string `json:"labels,omitempty"`
}
StepState is a point-in-time snapshot of one step.
type Storage ¶
type Storage struct {
// contains filtered or unexported fields
}
Storage selects where run state lives. Construct with Memory, Ephemeral, File, Postgres, or Driver (for a third-party storage driver).
func Driver ¶
Driver selects a registered storage driver by name with a connection string. Register drivers from a package init (see the driver package); the built-in names are "sqlite", "postgres", and "mysql". Without the driver's import, Open fails with a clear "driver is not registered" error.
q, _ := quacker.Open(quacker.WithStorage(quacker.Driver("mysql", dsn)))
func Ephemeral ¶
func Ephemeral() Storage
Ephemeral keeps state in a temporary WAL-backed database that is deleted on Close. Durability semantics are identical to Memory (nothing survives the process), but readers never block the write path. This is the default.
func File ¶
File persists state to path (WAL mode) so runs survive restarts. Runs interrupted by a previous process are handled according to RecoverRunningOnBoot.
func Memory ¶
func Memory() Storage
Memory keeps all state in a pure in-memory SQLite database. Nothing is written to the filesystem and everything is lost on Close. Note: SQLite :memory: databases cannot use WAL journaling, so under heavy write load introspection reads may briefly wait for an in-flight write transaction.
func Postgres ¶
Postgres persists state to the database at dsn. It requires importing the driver package for its side effect:
import _ "github.com/kartikbazzad/quacker/postgres"
Without that import, Open fails with a clear "driver not registered" error.
func PostgresWithDB ¶
PostgresWithDB persists state to an existing pgx-backed pool that the caller owns; shorthand for Postgres("").WithDB(db).
func (Storage) RecoverRunningOnBoot ¶
RecoverRunningOnBoot configures startup recovery for File storage: when true (default) runs left RUNNING/INTERRUPTED by a previous process are re-queued; when false they are marked FAILED.
func (Storage) WithDB ¶
WithDB hands quacker an existing database/sql pool the caller owns instead of one it dials from a DSN. Supported by single-pool drivers (Postgres via the pgx stdlib driver, MySQL via the go-sql-driver). quacker runs its migrations on the pool but never closes it, so the caller keeps using it after Quacker.Close; pool sizing and lifetime stay the caller's.
db, _ := sql.Open("pgx", dsn)
q, _ := quacker.Open(quacker.WithStorage(quacker.Postgres("").WithDB(db)))
type Task ¶
type Task[I, O any] struct { // contains filtered or unexported fields }
Task is a named, typed unit of work. Create with NewTask; the same value is used to enqueue runs, register for restart recovery, and attach crons.
func NewTask ¶
func NewTask[I, O any](name string, fn func(ctx context.Context, in I) (O, error), opts ...TaskOption) *Task[I, O]
NewTask creates a task named name. The name is the task's identity (see ID) and must be unique per process; it is persisted with run state, so it is part of the storage format — renaming a task orphans its in-flight runs, and registering two different functions under one name silently keeps the last.
func (*Task[I, O]) ID ¶ added in v1.1.0
ID returns the task's stable identity — its name, which is also what is persisted with each run and used by Register, Cron, and On. Use it wherever you would otherwise repeat the name as a string (a step name, a workflow dependency), so the name is defined once and cannot be misspelled:
charge := quacker.NewTask("charge", chargeFn)
wf := quacker.NewWorkflow[Order]("fulfill",
quacker.Step("charge", charge),
quacker.Step("ship", ship, charge.ID()), // not the literal "charge"
)
type TaskOption ¶
type TaskOption func(*taskConfig)
TaskOption configures a Task.
func BackoffPolicy ¶
func BackoffPolicy(b Backoff) TaskOption
Backoff sets the retry delay policy (default: exponential from 500ms, capped at 30s, with 10% jitter).
func Queue ¶
func Queue(name string) TaskOption
Queue routes the task to a named queue with its own concurrency limit. Default: "default".
func Retries ¶
func Retries(n int) TaskOption
Retries sets how many times a failed attempt is retried (maxAttempts = retries + 1). Default: no retries.
func Timeout ¶
func Timeout(d time.Duration) TaskOption
Timeout bounds each attempt. A timed-out attempt is treated like any other failure (retried when retries remain). Default: no timeout.
func WithDeadLetter ¶
func WithDeadLetter() TaskOption
WithDeadLetter marks a run dead-lettered when it exhausts its retries, so it appears in DeadLetters and can be retried with RetryDeadLetter. Runs of a task without it that fail are ordinary FAILED runs.
func WithEphemeral ¶
func WithEphemeral() TaskOption
WithEphemeral makes runs of this task ephemeral: persisted while in flight (so claiming, retries, and introspection work) but deleted when they reach a terminal state and discarded on restart rather than recovered. No history remains; a cross-engine Result returns ErrRunGone if the run was deleted before it could be observed.
func WithKey ¶
func WithKey[I any](fn func(I) string) TaskOption
WithKey extracts a concurrency key from the task input. Steps sharing a key are capped at WithKeyConcurrency (default 1) simultaneous executions across all queues — the scheduler holds a due step QUEUED until its key has a free slot. The cap is enforced by counting RUNNING rows at claim time, so it survives restarts and needs no per-key bookkeeping. A task without WithKey is never key-gated; a key function returning "" leaves that run unkeyed.
func WithKeyConcurrency ¶
func WithKeyConcurrency(n int) TaskOption
WithKeyConcurrency sets how many runs sharing one WithKey may execute concurrently (default 1 = strict serialization per key). Meaningless without WithKey; values <1 are treated as 1.
func WithKeyLimit ¶
func WithKeyLimit[I any](name string, fn func(I) string, limit int) TaskOption
WithKeyLimit declares an additional named concurrency key with its own limit. A step is claimed only when every key it declares has a free slot, so declaring several gates on multiple dimensions at once. Keys are scoped by name and value: two tasks using the same name and equal values share one budget, which is how a limit is "shared" across tasks.
func WithKeyStrategy ¶
func WithKeyStrategy(s ConcurrencyStrategy) TaskOption
WithKeyStrategy selects what happens when a run is enqueued while its key is already at WithKeyConcurrency: ConcurrencyHold (default) queues it, while the cancel strategies cancel running or queued same-key runs (or the incoming run) instead. Meaningless without WithKey.
func WithLabels ¶
func WithLabels(labels ...string) TaskOption
WithLabels requires worker labels for this task: only an engine opened with WithWorkerLabels covering all of them claims its steps (labels are a subset — an engine with extra labels may still run it). Without a matching engine the runs stay QUEUED. No labels means any engine.
func WithSequence ¶
func WithSequence[I any](fn func(I) string) TaskOption
WithSequence makes runs of this task execute strictly one-at-a-time in insertion order within a sequence key derived from the input, while different keys run in parallel. An earlier run that is still queued, running, retrying, or suspended holds the line (head-of-line blocking); a key returning "" is unsequenced. Sequences are a stronger guarantee than WithKey's concurrency cap, which does not fix execution order across retries.
func WithUnique ¶
func WithUnique[I any](fn func(I) string) TaskOption
WithUnique makes runs of this task unique per derived key among non-terminal runs: while a run with the key is QUEUED/RUNNING/BLOCKED/SUSPENDED, another enqueue with the same key resolves by the conflict policy (default UniqueReuse). The key is computed from input at enqueue; returning "" leaves that run not unique. Uniqueness is scoped to the task name.
func WithUniqueConflict ¶
func WithUniqueConflict(m UniqueConflict) TaskOption
WithUniqueConflict sets what a unique run does when its key is already held: UniqueReuse (default) returns the live run's handle, UniqueError fails with ErrDuplicateJob, UniqueReplace cancels the live run and enqueues the new one.
func Wrap ¶
func Wrap(mw ...Middleware) TaskOption
Wrap attaches per-task middleware around this task's body. Engine-wide middleware (WithMiddleware / Quacker.Use) still wraps the result, so the order is global → per-task → body.
type UniqueConflict ¶
type UniqueConflict = store.UniqueConflict
UniqueConflict selects what a unique run does when its key is already held by a live run. Set the default per task with WithUniqueConflict.
type Workflow ¶
type Workflow[I any] struct { // contains filtered or unexported fields }
Workflow is a DAG of steps sharing one input. Steps run as soon as their dependencies succeed; the run output is the output of the last declared step. If any step exhausts its retries the run fails and remaining steps are cancelled.
func NewGroupWorkflow ¶ added in v1.2.0
NewGroupWorkflow creates a workflow from groups. Each group's After dependencies expand to the member steps of those groups, so a group runs only after its dependencies; member steps never repeat the group dep. Group structure is persisted with the run so DAG/DAGSVG draw group boxes and group-to-group edges instead of the expanded step mesh.
func NewWorkflow ¶
NewWorkflow creates a workflow from step declarations. Step names must be unique and dependencies must form a DAG (cycles are rejected at enqueue).
Example ¶
ExampleNewWorkflow runs a two-step DAG where the second step reads the first step's output.
q, _ := Open(WithStorage(Memory()))
defer q.Close(context.Background())
charge := NewTask("charge", func(ctx context.Context, in string) (string, error) {
return "charged:" + in, nil
})
ship := NewTask("ship", func(ctx context.Context, in string) (string, error) {
rec, err := DepOutput[string](ctx, "charge")
if err != nil {
return "", err
}
return "shipped(" + rec + ")", nil
})
wf := NewWorkflow[string]("fulfill",
Step("charge", charge),
Step("ship", ship, "charge"),
)
h, _ := EnqueueWorkflow[string](context.Background(), q, wf, "o-1")
out, _ := h.Result(context.Background())
fmt.Println(out)
Output: shipped(charged:o-1)
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package driver defines quacker's storage-driver contract.
|
Package driver defines quacker's storage-driver contract. |
|
examples
|
|
|
children
command
Child runs: a parent task fans out to per-item child runs, and the lineage is visible through Execution/Runs.
|
Child runs: a parent task fans out to per-item child runs, and the lineage is visible through Execution/Runs. |
|
cron
command
Cron and delayed triggers: a task that runs on a schedule, plus one-off delayed runs, cancellable while waiting.
|
Cron and delayed triggers: a task that runs on a schedule, plus one-off delayed runs, cancellable while waiting. |
|
dag
command
A workflow DAG: charge, then ship, then notify — with data flowing from step to step via dependency outputs.
|
A workflow DAG: charge, then ship, then notify — with data flowing from step to step via dependency outputs. |
|
dagsvg
command
DAG visualization: run a workflow, then emit its current step graph as JSON and as a standalone SVG file.
|
DAG visualization: run a workflow, then emit its current step graph as JSON and as a standalone SVG file. |
|
durable
command
Durable execution: a task that sleeps and waits for an event without holding a worker slot, resuming where it left off with side effects that run exactly once.
|
Durable execution: a task that sleeps and waits for an event without holding a worker slot, resuming where it left off with side effects that run exactly once. |
|
eltdag
command
ELT DAG visualization: build an extract -> transform -> stage -> load pipeline (parallel per-shard fan-out, a per-shard series chain, a fan-in, and per-partition child workflows), run it, then save the run's step graph as dag.json and dag.svg.
|
ELT DAG visualization: build an extract -> transform -> stage -> load pipeline (parallel per-shard fan-out, a per-shard series chain, a fan-in, and per-partition child workflows), run it, then save the run's step graph as dag.json and dag.svg. |
|
eltgroups
command
Grouped ELT DAG: four groups — extract, transform, load, report — where each group fans out into parallel steps, the next group runs only after the previous group (declared once with After), and a step may depend on a sibling in its own group.
|
Grouped ELT DAG: four groups — extract, transform, load, report — where each group fans out into parallel steps, the next group runs only after the previous group (declared once with After), and a step may depend on a sibling in its own group. |
|
eltnested
command
Prototype: ELT groups as nested (child) workflows.
|
Prototype: ELT groups as nested (child) workflows. |
|
events
command
In-process events: emit an event and every bound task runs with the event payload as its input.
|
In-process events: emit an event and every bound task runs with the event payload as its input. |
|
introspect
command
Live introspection: watch a slow workflow's state change in real time while it keeps executing — nothing is paused or slowed down.
|
Live introspection: watch a slow workflow's state change in real time while it keeps executing — nothing is paused or slowed down. |
|
simple
command
Simplest possible quacker app: define a task, enqueue it, await the result.
|
Simplest possible quacker app: define a task, enqueue it, await the result. |
|
internal
|
|
|
bus
Package bus is a tiny in-process pub/sub for run state transitions.
|
Package bus is a tiny in-process pub/sub for run state transitions. |
|
engine
Package engine is quacker's execution core: a scheduler that claims due steps from SQLite, per-queue worker pools, retries, DAG progression, cron firing, and graceful shutdown.
|
Package engine is quacker's execution core: a scheduler that claims due steps from SQLite, per-queue worker pools, retries, DAG progression, cron firing, and graceful shutdown. |
|
store
Package store owns all persistence for quacker.
|
Package store owns all persistence for quacker. |
|
Package mysql registers the MySQL/MariaDB storage driver for quacker.
|
Package mysql registers the MySQL/MariaDB storage driver for quacker. |
|
Package postgres registers the PostgreSQL storage backend for quacker.
|
Package postgres registers the PostgreSQL storage backend for quacker. |