package retryhandler import ( "context" "fmt" "io" "net" "syscall" "time" "github.com/containerd/containerd/images" remoteserrors "github.com/containerd/containerd/remotes/errors" ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" ) // MaxRetryBackoff is the maximum backoff time before giving up. This is a // variable so that code which embeds BuildKit can override the default value. var MaxRetryBackoff = 8 * time.Second func New(f images.HandlerFunc, logger func([]byte)) images.HandlerFunc { return func(ctx context.Context, desc ocispecs.Descriptor) ([]ocispecs.Descriptor, error) { backoff := time.Second for { descs, err := f(ctx, desc) if err != nil { select { case <-ctx.Done(): return nil, err default: if !retryError(err) { return nil, err } } if logger != nil { logger([]byte(fmt.Sprintf("error: %v\n", err.Error()))) } } else { return descs, nil } // backoff logic if backoff >= MaxRetryBackoff { return nil, err } if logger != nil { logger([]byte(fmt.Sprintf("retrying in %v\n", backoff))) } time.Sleep(backoff) backoff *= 2 } } } func retryError(err error) bool { // Retry on 5xx errors var errUnexpectedStatus remoteserrors.ErrUnexpectedStatus if errors.As(err, &errUnexpectedStatus) && errUnexpectedStatus.StatusCode >= 500 && errUnexpectedStatus.StatusCode <= 599 { return true } if errors.Is(err, io.EOF) || errors.Is(err, syscall.ECONNRESET) || errors.Is(err, syscall.EPIPE) || errors.Is(err, net.ErrClosed) { return true } // catches TLS timeout or other network-related temporary errors if ne, ok := errors.Cause(err).(net.Error); ok && ne.Temporary() { //nolint:staticcheck // ignoring "SA1019: Temporary is deprecated", continue to propagate net.Error through the "temporary" status return true } // https://github.com/containerd/containerd/pull/4724 if errors.Cause(err).Error() == "no response" { return true } return false }