diff --git a/src/internal/common/readers/retry_handler.go b/src/internal/common/readers/retry_handler.go index b52389f83..9b20dfce1 100644 --- a/src/internal/common/readers/retry_handler.go +++ b/src/internal/common/readers/retry_handler.go @@ -28,6 +28,7 @@ const ( // between callers. var retryErrs = []error{ syscall.ECONNRESET, + io.ErrUnexpectedEOF, } type Getter interface { diff --git a/src/internal/common/readers/retry_handler_test.go b/src/internal/common/readers/retry_handler_test.go index e6bca2585..89376c22f 100644 --- a/src/internal/common/readers/retry_handler_test.go +++ b/src/internal/common/readers/retry_handler_test.go @@ -1,4 +1,4 @@ -package readers_test +package readers import ( "bytes" @@ -12,7 +12,6 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - "github.com/alcionai/corso/src/internal/common/readers" "github.com/alcionai/corso/src/internal/tester" ) @@ -141,7 +140,7 @@ func (suite *ResetRetryHandlerUnitSuite) TestResetRetryHandler() { expectErr error }{ { - name: "OnlyFirstGetErrors NoRangeSupport", + name: "OnlyFirstGetErrors ECONNRESET NoRangeSupport", getterResps: map[int]getterResp{ 0: { err: syscall.ECONNRESET, @@ -149,6 +148,15 @@ func (suite *ResetRetryHandlerUnitSuite) TestResetRetryHandler() { }, expectData: data, }, + { + name: "OnlyFirstGetErrors ErrUnexpectedEOF NoRangeSupport", + getterResps: map[int]getterResp{ + 0: { + err: io.ErrUnexpectedEOF, + }, + }, + expectData: data, + }, { name: "OnlyFirstReadErrors RangeSupport", supportsRange: true, @@ -223,6 +231,29 @@ func (suite *ResetRetryHandlerUnitSuite) TestResetRetryHandler() { }, expectData: data, }, + { + name: "MultipleRetriableErrorTypesInMiddle RangeSupport", + supportsRange: true, + getterResps: map[int]getterResp{ + 1: {offset: 12}, + 2: {offset: 20}, + }, + getterExpectHeaders: map[int]map[string]string{ + 1: {"Range": "bytes=12-"}, + 2: {"Range": "bytes=20-"}, + }, + readerResps: map[int]readResp{ + 3: { + read: 0, + err: io.ErrUnexpectedEOF, + }, + 6: { + read: 0, + err: syscall.ECONNRESET, + }, + }, + expectData: data, + }, { name: "ShortReadWithError NoRangeSupport", readerResps: map[int]readResp{ @@ -450,7 +481,7 @@ func (suite *ResetRetryHandlerUnitSuite) TestResetRetryHandler() { resData = make([]byte, len(data)) ) - rrh, err := readers.NewResetRetryHandler(ctx, getter) + rrh, err := NewResetRetryHandler(ctx, getter) require.NoError(t, err, "making reader wrapper: %v", clues.ToCore(err)) for err == nil && offset < len(data) { @@ -475,3 +506,65 @@ func (suite *ResetRetryHandlerUnitSuite) TestResetRetryHandler() { }) } } + +func (suite *ResetRetryHandlerUnitSuite) TestIsRetriable() { + table := []struct { + name string + err error + expect bool + }{ + { + name: "nil", + err: nil, + expect: false, + }, + { + name: "Connection Reset Error", + err: syscall.ECONNRESET, + expect: true, + }, + { + name: "Unexpected EOF Error", + err: io.ErrUnexpectedEOF, + expect: true, + }, + { + name: "Not Retriable Error", + err: assert.AnError, + expect: false, + }, + { + name: "Chained Errors With No Retriables", + err: clues.Stack(assert.AnError, clues.New("another error")), + expect: false, + }, + { + name: "Chained Errors With ECONNRESET", + err: clues.Stack(assert.AnError, syscall.ECONNRESET, assert.AnError), + expect: true, + }, + { + name: "Chained Errors With ErrUnexpectedEOF", + err: clues.Stack(assert.AnError, io.ErrUnexpectedEOF, assert.AnError), + expect: true, + }, + { + name: "Wrapped ECONNRESET Error", + err: clues.Wrap(syscall.ECONNRESET, "wrapped error"), + expect: true, + }, + { + name: "Wrapped ErrUnexpectedEOF Error", + err: clues.Wrap(io.ErrUnexpectedEOF, "wrapped error"), + expect: true, + }, + } + + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + assert.Equal(t, test.expect, isRetriable(test.err)) + }) + } +}