Skip to content

Retries ​

When a Step or a handler fails, Runcoil tries again after a delay. You choose how many tries and how long the delays are.

Default policy ​

The server applies a default policy to every Task.

SettingDefaultServer variable
Tries in total, the first one included4ENGINE__RETRY__MAX_ATTEMPTS
Delay before each retry5s,10s,30s,1m,3m,10m,30m,1hENGINE__RETRY__DELAYS
Jitter0.2ENGINE__RETRY__JITTER

The delay for the first retry is the first value, for the second retry the second value, and so on. The last value repeats. With the defaults, a Step is tried four times and waits about 5 seconds, then 10 seconds, then 30 seconds. Each delay varies by up to 20% either way.

Set a policy for a Task ​

Pass runcoil.Retry when you add the Task. The policy applies to every Step of the Task. This code is from the onboarding example:

go
worker.NewTask("create-account", runcoil.HandlerFunc(handler.createAccount),
	runcoil.Retry(runcoil.RetryPolicy{MaxAttempts: 3, Delays: []time.Duration{time.Second}}),
)

MaxAttempts counts the first try. Delays is the wait before each retry, and its last value repeats. A field left at zero takes the default. Each delay must be greater than zero.

A failure outside any Step, such as an error that your handler returns, also uses the policy of the Task.

Set a policy for a Step ​

Pass step.Retry to step.Do to override the policy for one Step. It takes the same RetryPolicy.

go
messageID, err := step.Do(ctx, "send-welcome-email", func(ctx context.Context) (string, error) {
	return o.emails.SendWelcome(ctx, created)
}, step.Retry(step.RetryPolicy{MaxAttempts: 5}))

Each Step counts its own tries. When a Step has used all of its tries, it has failed for good, and step.Do returns a *step.Error.

Fail at once ​

A plain error is retryable. To stop retrying, wrap the error in runcoil.Permanent. A Step whose function returns it fails at once. A handler that returns it fails the attempt without a retry.

This code is from the onboarding example. A request with missing fields cannot succeed on a second try:

go
if strings.TrimSpace(in.AccountID) == "" || strings.TrimSpace(in.Name) == "" || strings.TrimSpace(in.Email) == "" {
	return onboardingOutput{}, runcoil.Permanent(errors.New("account_id, name, and email are required"))
}

A Step function that panics also fails at once. So does an output that cannot be encoded to JSON.

Choose the next delay ​

To replace the policy's delay for the next try, wrap the error in runcoil.RetryAfter. A rate-limited API often tells you how long to wait.

go
return "", runcoil.RetryAfter(err, 30*time.Second)

A failure marked this way still counts as a try.

When a worker dies ​

A worker renews a lease on each Run it holds. By default it sends a heartbeat every 10 seconds, and a lease lasts 30 seconds. If the worker dies, the lease expires and the server takes the attempt back.

The server counts the lost attempt like a failed try. If tries are left, the Run waits for its next attempt, and another worker runs it. Steps that were recorded before the worker died return their stored results. If no tries are left, the Run fails with the code attempt_lost.

If a worker never acknowledged the dispatch, the server gives the attempt back and counts no try.

After a Step fails for good ​

When a Step has used all of its tries, step.Do returns a *step.Error. A handler that returns it fails the Run. The onboarding example does this when the welcome email fails for good:

go
messageID, err := step.Do(ctx, "send-welcome-email", func(ctx context.Context) (string, error) {
	return o.emails.SendWelcome(ctx, created)
})
if err != nil {
	// Keep the account when delivery fails finally; fail the Run.
	return onboardingOutput{}, err
}

The Run fails and the account stays created, because the handler does not undo the first Step. If you want a fallback or an undo instead, check the error with errors.As for *step.Error and run more Steps before you return. Each of those Steps is recorded like any other.

Quick reference ​

You want toUse
Change the tries or delays of every Step in a Taskruncoil.Retry on NewTask
Change them for one Stepstep.Retry on step.Do
Stop retryingruncoil.Permanent(err)
Pick the next delayruncoil.RetryAfter(err, d)

Runcoil is pre-release. The API may change before 1.0.