Files
seaweedfs/weed/util/retry.go
T
Chris LuandGitHub 19ce7c0b6f consolidate the duplicated transient-error classifiers onto util.IsTransientError (#10429)
* util: match transient error messages case-insensitively, expose the message form

The same condition reaches different layers capitalized differently -- a
volume server relays its idle timeout as "I/O timeout" inside a JSON string --
and the callers that grew their own substring lists all lower-case first.

Also split out IsTransientErrorMessage for the paths that carry only the text,
such as the per-file status strings in a batch delete response, and pick up
"no route to host" and "network is unreachable" from the gRPC classifier.

* filersink: classify transient network errors through util.IsTransientError

The local list caught i/o timeout, connection reset, and broken pipe but not
connection refused, no such host, unexpected EOF, the syscall errnos, or the
gRPC and S3 overload codes. Keep only the bare io.EOF case, which is transient
here -- a truncated chunk read -- but a clean stream end elsewhere.

* filer deletion: reuse util.IsTransientErrorMessage for the network patterns

Six of the sixteen patterns were already covered. Keep the ones specific to
this pipeline -- read-only volumes, lookup failures, backpressure -- and note
why context cancellation stays retryable here: it decides whether to requeue
the deletion, not whether to retry a call.

* wdclient: fold the shared classifier into the volume lookup retry check

The string tail duplicated the shared list and missed the syscall errnos and
net.Error timeouts. Keep "connection" and "timeout", which are broader than
the shared classifier on purpose: a volume lookup is a cheap read-only call.
2026-07-24 11:08:18 -07:00

252 lines
6.2 KiB
Go

package util
import (
"context"
"errors"
"io"
"net"
"strings"
"syscall"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
)
var RetryWaitTime = 6 * time.Second
// transientErrorMessages are substrings of failures that a later attempt is
// likely to get past: connection resets, timeouts, and the throttling or
// overload replies S3 and gRPC hand back. Cloud SDKs bury the underlying net
// error in an opaque wrapper with no Unwrap, so the message is often all
// that is left to match on. Compared case-insensitively, so entries are lower
// case: the same condition reaches different layers capitalized differently
// (a volume server relays its idle timeout as "I/O timeout" inside JSON).
var transientErrorMessages = []string{
"transport",
"connection reset",
"connection refused",
"broken pipe",
"unexpected eof",
"i/o timeout",
"tls handshake timeout",
"no such host",
"no route to host",
"network is unreachable",
"client.timeout",
"requesterror",
"requesttimeout",
"slowdown",
"throttling",
"internalerror",
"resourceexhausted",
"unavailable",
}
// IsTransientErrorMessage reports whether an error message describes a network
// or service condition worth retrying. Callers holding an error should use
// IsTransientError; this is for paths that only carry the text, such as the
// per-file status strings in a batch delete response.
func IsTransientErrorMessage(msg string) bool {
lower := strings.ToLower(msg)
for _, transient := range transientErrorMessages {
if strings.Contains(lower, transient) {
return true
}
}
return false
}
// IsTransientError reports whether err is a network or service condition worth
// retrying. A cancelled or expired context never is: the caller is already gone.
func IsTransientError(err error) bool {
if err == nil {
return false
}
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return false
}
if errors.Is(err, io.ErrUnexpectedEOF) ||
errors.Is(err, syscall.ECONNRESET) || errors.Is(err, syscall.ECONNABORTED) ||
errors.Is(err, syscall.ECONNREFUSED) || errors.Is(err, syscall.EPIPE) ||
errors.Is(err, syscall.ETIMEDOUT) {
return true
}
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
return true
}
return IsTransientErrorMessage(err.Error())
}
func Retry(name string, job func() error) (err error) {
waitTime := time.Second
hasErr := false
for waitTime < RetryWaitTime {
err = job()
if err == nil {
if hasErr {
glog.V(0).Infof("retry %s successfully", name)
}
waitTime = time.Second
break
}
if IsTransientError(err) {
hasErr = true
glog.V(0).Infof("retry %s: err: %v", name, err)
} else {
break
}
time.Sleep(waitTime)
waitTime += waitTime / 2
}
return err
}
func MultiRetry(name string, errList []string, job func() error) (err error) {
waitTime := time.Second
hasErr := false
for waitTime < RetryWaitTime {
err = job()
if err == nil {
if hasErr {
glog.V(0).Infof("retry %s successfully", name)
}
waitTime = time.Second
break
}
if containErr(err.Error(), errList) {
hasErr = true
glog.V(0).Infof("retry %s: err: %v", name, err)
} else {
break
}
time.Sleep(waitTime)
waitTime += waitTime / 2
}
return err
}
// RetryOnError retries job with the same bounded backoff as MultiRetry, but
// decides retriability with a predicate instead of an error-substring list.
func RetryOnError(name string, shouldRetry func(error) bool, job func() error) (err error) {
waitTime := time.Second
hasErr := false
for waitTime < RetryWaitTime {
err = job()
if err == nil {
if hasErr {
glog.V(0).Infof("retry %s successfully", name)
}
break
}
if shouldRetry(err) {
hasErr = true
glog.V(0).Infof("retry %s: err: %v", name, err)
} else {
break
}
time.Sleep(waitTime)
waitTime += waitTime / 2
}
return err
}
// RetryUntil retries until the job returns no error or onErrFn returns false
func RetryUntil(name string, job func() error, onErrFn func(err error) (shouldContinue bool)) error {
waitTime := time.Second
for {
err := job()
if err == nil {
waitTime = time.Second
return nil
}
if onErrFn(err) {
if strings.Contains(err.Error(), "transport") || strings.Contains(err.Error(), "ResourceExhausted") || strings.Contains(err.Error(), "Unavailable") {
glog.V(0).Infof("retry %s: err: %v", name, err)
}
time.Sleep(waitTime)
if waitTime < RetryWaitTime {
waitTime += waitTime / 2
}
continue
} else {
return err
}
}
}
// RetryWithBackoff retries an operation on codes.Unavailable errors with exponential
// backoff, respecting context cancellation and a maximum retry duration.
// Returns nil on success, ctx.Err() on context cancellation, or the last error
// when maxDuration is exceeded or a non-retriable error occurs.
func RetryWithBackoff(ctx context.Context, name string, maxDuration time.Duration, shouldRetry func(error) bool, operation func() error) error {
waitTime := time.Second
maxWaitTime := RetryWaitTime
deadline := time.Now().Add(maxDuration)
var lastErr error
for {
if ctx.Err() != nil {
return ctx.Err()
}
if time.Until(deadline) <= 0 {
if lastErr != nil {
glog.V(0).Infof("retry %s: giving up after %v: %v", name, maxDuration, lastErr)
return lastErr
}
}
err := operation()
if err == nil {
return nil
}
lastErr = err
if !shouldRetry(err) {
return err
}
remaining := time.Until(deadline)
if remaining <= 0 {
glog.V(0).Infof("retry %s: giving up after %v: %v", name, maxDuration, err)
return err
}
sleepTime := waitTime
if sleepTime > maxWaitTime {
sleepTime = maxWaitTime
}
if sleepTime > remaining {
sleepTime = remaining
}
glog.V(1).Infof("retry %s: retrying in %v: %v", name, sleepTime, err)
timer := time.NewTimer(sleepTime)
select {
case <-ctx.Done():
if !timer.Stop() {
<-timer.C
}
return ctx.Err()
case <-timer.C:
}
waitTime += waitTime / 2
if waitTime > maxWaitTime {
waitTime = maxWaitTime
}
}
}
// Nvl return the first non-empty string
func Nvl(values ...string) string {
for _, s := range values {
if s != "" {
return s
}
}
return ""
}
func containErr(err string, errList []string) bool {
for _, e := range errList {
if strings.Contains(err, e) {
return true
}
}
return false
}