Documentation
¶
Overview ¶
Package retry re-attempts work that failed for a reason that might not recur.
The whole subject is one judgement call: retrying the wrong thing is worse than not retrying at all. A retried POST that actually succeeded creates a second resource. A retry loop with no jitter turns a brief dependency blip into a synchronised stampede that keeps it down. A retry of a 400 is three requests that were always going to fail.
So this package defaults to NOT retrying, and asks you to say what is retryable. Retryable() covers the usual answer: timeouts, unavailability, and serialization conflicts — never a client error, never an unclassified one.
Example ¶
An unavailable dependency is worth another attempt.
package main
import (
"context"
"fmt"
"time"
"github.com/IncepTools/go/src/core/errs"
"github.com/IncepTools/go/src/reliability/retry"
)
func fast() retry.Policy {
return retry.Policy{
MaxAttempts: 3,
BaseDelay: time.Millisecond,
MaxDelay: 2 * time.Millisecond,
NoJitter: true,
}
}
func main() {
attempts := 0
quote, err := retry.Value(context.Background(), fast(),
func(context.Context) (string, error) {
attempts++
if attempts < 3 {
return "", errs.Unavailable("the pricing service is down")
}
return "£12.00", nil
})
fmt.Println(quote, err, attempts)
}
Output: £12.00 <nil> 3
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Do ¶
Do runs fn until it succeeds, the policy is exhausted, or the error is not retryable. It returns the LAST error.
The context is honoured between attempts: a cancelled caller stops waiting immediately rather than sleeping out the backoff.
Example (DoesNotRetryAClientError) ¶
A client error is not retried. Three requests that were always going to fail help nobody.
package main
import (
"context"
"fmt"
"time"
"github.com/IncepTools/go/src/core/errs"
"github.com/IncepTools/go/src/reliability/retry"
)
func fast() retry.Policy {
return retry.Policy{
MaxAttempts: 3,
BaseDelay: time.Millisecond,
MaxDelay: 2 * time.Millisecond,
NoJitter: true,
}
}
func main() {
attempts := 0
_ = retry.Do(context.Background(), fast(), func(context.Context) error {
attempts++
return errs.Invalid("price must be 0 or more")
})
fmt.Println("attempts:", attempts)
}
Output: attempts: 1
func Retryable ¶
Retryable reports whether an error is worth retrying: a timeout, an unavailable dependency, or a serialization conflict.
Everything else is not. In particular an unclassified error is NOT retryable — if nobody has decided what it means, it must not be assumed safe to repeat.
Example ¶
Neither is an error nobody classified: if nothing has decided what it means, it must not be assumed safe to repeat.
package main
import (
"errors"
"fmt"
"github.com/IncepTools/go/src/core/errs"
"github.com/IncepTools/go/src/reliability/retry"
)
func main() {
fmt.Println(retry.Retryable(errs.Unavailable("down")))
fmt.Println(retry.Retryable(errs.Timeout("slow")))
fmt.Println(retry.Retryable(errs.Conflict("raced").WithCode("CONCURRENT_MODIFICATION")))
fmt.Println(retry.Retryable(errs.Conflict("taken").WithCode("SKU_TAKEN")))
fmt.Println(retry.Retryable(errors.New("something happened")))
}
Output: true true true false false
Types ¶
type Policy ¶
type Policy struct {
// MaxAttempts counts the FIRST try. 3 means one try and two retries.
// Zero or one disables retrying.
MaxAttempts int
// BaseDelay is the first wait. Each subsequent wait doubles it.
BaseDelay time.Duration
// MaxDelay caps the exponential growth.
MaxDelay time.Duration
// Jitter randomises each wait by up to ±50%.
//
// Leave it ON. Without jitter, every client that failed at the same
// moment retries at the same moment, and the dependency that was briefly
// slow is now reliably overloaded.
NoJitter bool
// Retryable decides whether an error is worth another attempt. Defaults
// to Retryable.
Retryable func(error) bool
// OnRetry is called before each wait, for logging or a metric.
OnRetry func(attempt int, err error, wait time.Duration)
}
Policy describes how to retry.