Chains
A Chain runs a sequence of tasks strictly one after another — each link is
enqueued only once the previous one succeeds. There's no polling or manual
follow-up enqueue: the chain itself carries its remaining links forward, so
a failure partway through simply stops the sequence rather than skipping
ahead.
Recipe: build and enqueue a chain
info, err := chronos.NewChain().
Then(EncodeArgs{VideoID: "v1"}).
Then(ThumbnailArgs{VideoID: "v1"}, chronos.WithQueue("low")).
Then(NotifyArgs{UserID: "u1"}, chronos.WithRetention(time.Hour)).
Enqueue(ctx, client)
Then appends a link and accepts the usual per-task options — WithQueue,
WithMaxRetry, WithRetention, WithProcessIn (which delays a link
relative to its predecessor's completion, not its enqueue time). Enqueue
makes only the first link available for processing; info is that first
link's TaskInfo. Each later link becomes eligible only as its predecessor
finishes successfully.
Recipe: relaying a result between links
A handler registered with AddHandlerR (instead of AddHandler) returns a
typed result alongside its error, and that result becomes available to the
next link in the chain via PrevResult[R]:
chronos.AddHandlerR(mux, func(ctx context.Context, task *chronos.Task[EncodeArgs]) (EncodeResult, error) {
// ... encode ...
return EncodeResult{Path: "out.mp4"}, nil
})
chronos.AddHandlerR(mux, func(ctx context.Context, task *chronos.Task[ThumbnailArgs]) (EncodeResult, error) {
prev, err := chronos.PrevResult[EncodeResult](task)
if err != nil {
return EncodeResult{}, err
}
// prev.Path is the previous link's output
return generateThumbnail(prev)
})
This is the same result-relay mechanism used for parallel stages — see
Groups for how a ThenGroup stage fans results in from
multiple members via GroupResults.
Gotcha: failure stops the chain
When a link exhausts its retries and is dead-lettered, its successors don't
run — they wait, still attached to the dead-lettered link's message. The
Inspector's ChainPending field on that task shows how many links are still
queued behind it. To resume: fix whatever made the handler fail, then re-run
the dead-lettered link (chronos task run <queue> <id>, or the equivalent
Inspector call) — because each link carries its remaining tail, resuming
that one link continues the whole chain from there.
A few other constraints to keep in mind:
WithTaskIDandWithUniqueare rejected on chain links — the chain owns task IDs internally, and unique-dedup isn't supported inside a chain.- Handlers must stay idempotent, as everywhere in chronos-go: a redelivered
link can run its handler more than once, and if a predecessor is
redelivered after its successor already finished (and wasn't retained),
the successor can be recreated. Per-link
WithRetentioncloses that window for its duration. - Each link's message carries its entire remaining tail, so a very long chain grows message size with every link. Keep chains reasonably short.
Full signatures are on pkg.go.dev.