Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(internal/gensupport): update shouldRetry for GCS uploads #2634

Merged
merged 4 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 18 additions & 16 deletions internal/gensupport/retry.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"io"
"net"
"net/url"
"strings"
"time"

Expand All @@ -29,8 +30,6 @@ var (
backoff = func() Backoff {
return &gax.Backoff{Initial: 100 * time.Millisecond}
}
// syscallRetryable is a platform-specific hook, specified in retryable_linux.go
syscallRetryable func(error) bool = func(err error) bool { return false }
)

const (
Expand All @@ -56,30 +55,33 @@ func shouldRetry(status int, err error) bool {
if status == statusTooManyRequests || status == statusRequestTimeout {
return true
}
if err == io.ErrUnexpectedEOF {
if errors.Is(err, io.ErrUnexpectedEOF) {
return true
}
// Transient network errors should be retried.
if syscallRetryable(err) {
if errors.Is(err, net.ErrClosed) {
return true
}
if err, ok := err.(interface{ Temporary() bool }); ok {
if err.Temporary() {
return true
switch e := err.(type) {
case *net.OpError, *url.Error:
// Retry socket-level errors ECONNREFUSED and ECONNRESET (from syscall).
// Unfortunately the error type is unexported, so we resort to string
// matching.
retriable := []string{"connection refused", "connection reset", "broken pipe"}
for _, s := range retriable {
if strings.Contains(e.Error(), s) {
return true
}
}
}
var opErr *net.OpError
if errors.As(err, &opErr) {
if strings.Contains(opErr.Error(), "use of closed network connection") {
// TODO: check against net.ErrClosed (go 1.16+) instead of string
case interface{ Temporary() bool }:
if e.Temporary() {
return true
}
}

// If Go 1.13 error unwrapping is available, use this to examine wrapped
// If error unwrapping is available, use this to examine wrapped
// errors.
if err, ok := err.(interface{ Unwrap() error }); ok {
return shouldRetry(status, err.Unwrap())
if e, ok := err.(interface{ Unwrap() error }); ok {
return shouldRetry(status, e.Unwrap())
}
return false
}
Expand Down
97 changes: 97 additions & 0 deletions internal/gensupport/retry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright 2024 Google LLC.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package gensupport

import (
"errors"
"fmt"
"io"
"net"
"net/url"
"testing"
)

func TestShouldRetry(t *testing.T) {
t.Parallel()

for _, test := range []struct {
desc string
code int
inputErr error
shouldRetry bool
}{
{
desc: "nil error",
inputErr: nil,
shouldRetry: false,
},
{
desc: "429 error",
inputErr: fmt.Errorf("too many requests"),
code: 429,
shouldRetry: true,
},
{
desc: "408 error",
inputErr: fmt.Errorf("request timed out"),
code: 408,
shouldRetry: true,
},
{
desc: "unknown error",
inputErr: errors.New("foo"),
shouldRetry: false,
},
{
desc: "503 error",
inputErr: fmt.Errorf("service unavailable"),
code: 503,
shouldRetry: true,
},
{
desc: "403 error",
inputErr: fmt.Errorf("forbidden"),
code: 403,
shouldRetry: false,
},
{
desc: "connection refused",
inputErr: &url.Error{Op: "blah", URL: "blah", Err: errors.New("connection refused")},
shouldRetry: true,
},
{
desc: "connection reset",
inputErr: &net.OpError{Op: "blah", Net: "tcp", Err: errors.New("connection reset by peer")},
shouldRetry: true,
},
{
desc: "io.ErrUnexpectedEOF",
inputErr: io.ErrUnexpectedEOF,
shouldRetry: true,
},
{
desc: "wrapped retryable error",
inputErr: fmt.Errorf("Test unwrapping of a temporary error: %w", io.ErrUnexpectedEOF),
shouldRetry: true,
},
{
desc: "wrapped non-retryable error",
inputErr: fmt.Errorf("Test unwrapping of a non-retriable error: %w", io.EOF),
shouldRetry: false,
},
{
desc: "wrapped net.ErrClosed",
inputErr: &net.OpError{Err: net.ErrClosed},
shouldRetry: true,
},
} {
t.Run(test.desc, func(s *testing.T) {
got := shouldRetry(test.code, test.inputErr)
if got != test.shouldRetry {
s.Errorf("got %v, want %v", got, test.shouldRetry)
}
})
}
}
16 changes: 0 additions & 16 deletions internal/gensupport/retryable_linux.go

This file was deleted.