Events & Hooks
Events
(*Queue) Events() <-chan Event
Returns a channel that receives job events. Caller must call Unsubscribe when done.
(*Queue) Unsubscribe(ch <-chan Event)
Removes a subscriber channel. The channel is not closed; callers must stop reading before calling this.
(*Queue) Emit(event Event)
Emits an event to all subscribers. Non-blocking; drops events if a subscriber’s buffer is full.
(*Queue) EmitCustomEvent(jobID core.UUID, kind string, data map[string]any)
Emits a custom ephemeral event (not persisted).
Event Types
Every event implements the Event interface. Type-switch on the pointer type in your subscriber loop; each payload’s fields are listed below.
// Lifecycle
type JobStarted struct {
Job *Job
Timestamp time.Time
}
type JobCompleted struct {
Job *Job
Duration time.Duration
Timestamp time.Time
}
type JobFailed struct {
Job *Job
Error error
Timestamp time.Time
}
type JobRetrying struct {
Job *Job
Attempt int
Error error
NextRunAt time.Time
Timestamp time.Time
}
// DECLARED BUT NOT CURRENTLY EMITTED — see the note under this block.
type CheckpointSaved struct {
JobID UUID
CallIndex int
CallType string // e.g. "call", "fanout", "phase"
Timestamp time.Time
}
// Pause / resume
type JobPaused struct {
Job *Job
Mode PauseMode
Timestamp time.Time
}
type JobCancelled struct {
Job *Job
Timestamp time.Time
}
type JobResumed struct {
Job *Job
Timestamp time.Time
}
// Emitted when a signal wakes a waiting job (producer fast path or the
// worker's polling backstop). SignalName is best-effort and may be empty
// on backstop/recovery wakes. Durable-timer (Sleep) deadline wakes without
// a pending signal do NOT emit this event.
type JobResumedBySignal struct {
JobID UUID
SignalName string
Timestamp time.Time
}
// Emitted when a signal is successfully persisted for a job.
type SignalDelivered struct {
JobID UUID
Name string
Timestamp time.Time
}
// Emitted when a worker reclaims a job whose owner is presumed dead, or when a
// worker observes a peer reclaim one of its own in-flight jobs. WorkerID is the
// worker emitting the event; Reason is one of the ReclaimReason* constants:
//
// - ReclaimReasonStaleLock ("stale_lock") — THIS worker's stale-lock
// reaper recovered a job from a presumed-dead peer. This is the actor side
// and the true crash leading-indicator ("I recovered N jobs"); alert on it.
// - ReclaimReasonOwnershipAudit ("ownership_audit") — the ownership audit saw
// a peer reclaim a job THIS worker was still running. This is the victim
// side ("a peer took N of my in-flight jobs"), meaning this worker was
// wrongly presumed dead or stalled.
//
// WARNING: in a multi-process fleet the SAME logical reclaim can surface once
// on the reaper (stale_lock) and once on the victim (ownership_audit), emitted
// by DIFFERENT workers. Keep the two reasons separable and do NOT sum across
// them when counting reclaims.
type JobReclaimed struct {
JobID UUID
WorkerID string
Reason string
Timestamp time.Time
}
type QueuePaused struct {
Queue string
Timestamp time.Time
}
type QueueResumed struct {
Queue string
Timestamp time.Time
}
type WorkerPaused struct {
WorkerID string
Mode PauseMode
Timestamp time.Time
}
type WorkerResumed struct {
WorkerID string
Timestamp time.Time
}
// Ephemeral / custom
type CustomEvent struct {
JobID UUID
Kind string // "progress", "phase_change", "log", …
Data map[string]any
Timestamp time.Time
}CheckpointSaved is DECLARED BUT NOT CURRENTLY EMITTED. Every other type
above is published to Queue.Events() subscribers; CheckpointSaved is not.
Nothing in the module constructs one, so a case *jobs.CheckpointSaved: arm
never fires — no error, no warning, just an arm that is dead code. The
CallType annotation above can therefore never be observed.
Checkpoints themselves are written normally by jobs.Call, SaveCheckpoint and
SavePhaseCheckpoint. To follow workflow progress, poll
q.Storage().GetCheckpoints(ctx, jobID) rather than subscribing — note it is a
Storage method, not a Queue one.
The type stays exported because removing it would be a breaking change, and it
may start being emitted in a future minor. TestEveryDocumentedEventIsEmitted
fails if that happens without this notice coming off — and fails today if any
other event type loses its emitter.
Hooks
(*Queue) OnJobStart(fn func(context.Context, *Job))
Registers a callback for when a job starts processing.
(*Queue) OnJobStartCtx(fn func(context.Context, *Job) context.Context)
Registers a context-transforming callback that runs when a job starts. The returned context.Context is threaded into the handler, so hooks can attach values (OTel spans, tenant IDs, correlation IDs, …) that downstream code reads out of the context. This is the hook the built-in OTel instrumentation (pkg/otel.Instrument) uses to re-attach the enqueue-time trace span to the worker-side handler.
(*Queue) OnJobComplete(fn func(context.Context, *Job))
Registers a callback for when a job completes successfully.
(*Queue) OnJobFail(fn func(context.Context, *Job, error))
Registers a callback for when a job fails permanently.
(*Queue) OnRetry(fn func(context.Context, *Job, int, error))
Registers a callback for when a job is being retried.
(*Queue) OnJobWaiting(fn func(context.Context, *Job))
Registers a callback for when a job suspends into waiting — parking on a
signal, a durable sleep, or a fan-out. It fires on every suspension, so a
workflow that waits several times fires it several times; it is not a
once-per-job hook.
(*Queue) OnJobReclaimed(fn func(ctx context.Context, jobID core.UUID, reason string))
Registers a callback for when a job lease is reclaimed. It fires both when this worker’s stale-lock reaper recovers a job from a presumed-dead owner and when the ownership audit observes a peer reclaim a job this worker was running. reason is ReclaimReasonStaleLock ("stale_lock", the actor/crash-leading-indicator side) or ReclaimReasonOwnershipAudit ("ownership_audit", the victim side). See the JobReclaimed event above — the same caveat applies: do not sum across reasons, since one logical reclaim can fire on both sides on different workers.