Skip to content

Commit

Permalink
[#21] - Added new "wasSuccessful" return value
Browse files Browse the repository at this point in the history
  • Loading branch information
J7mbo committed Apr 15, 2019
1 parent ab927b1 commit cd4ffe6
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 58 deletions.
16 changes: 8 additions & 8 deletions MethodCallRetrier.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ ExecuteFuncWithRetry retries a function with a maximum number of retries and a w
ExecuteWithRetry() but accepts a function to maintain type safety in userland instead and removes the requirement of a
user type assertion.
*/
func (r *MethodCallRetrier) ExecuteFuncWithRetry(function func() error) []error {
func (r *MethodCallRetrier) ExecuteFuncWithRetry(function func() error) (errs []error, wasSuccessful bool) {
defer func() {
r.resetCurrentRetries()
r.resetErrorList()
Expand All @@ -63,7 +63,7 @@ func (r *MethodCallRetrier) ExecuteFuncWithRetry(function func() error) []error
},
)

return r.errorList
return r.errorList, false
}

err := function()
Expand All @@ -76,13 +76,13 @@ func (r *MethodCallRetrier) ExecuteFuncWithRetry(function func() error) []error
return r.ExecuteFuncWithRetry(function)
}

return r.errorList
return r.errorList, true
}

/* ExecuteWithRetry retries the call to object.methodName(...args) with a maximum number of retries and a wait time. */
func (r *MethodCallRetrier) ExecuteWithRetry(
object interface{}, methodName string, args ...interface{},
) ([]interface{}, []error) {
) ([]interface{}, []error, bool) {
defer func() {
r.resetCurrentRetries()
r.resetErrorList()
Expand All @@ -93,13 +93,13 @@ func (r *MethodCallRetrier) ExecuteWithRetry(
r.errorList, &MaxRetriesError{methodName: methodName, waitTime: r.waitTime, maxRetries: r.maxRetries},
)

return nil, r.errorList
return nil, r.errorList, false
}

returnValues, err := r.callMethodOnObject(object, methodName, args)

if err != nil {
return nil, []error{err}
return nil, []error{err}, false
}

returnValueCount := len(returnValues)
Expand Down Expand Up @@ -127,10 +127,10 @@ func (r *MethodCallRetrier) ExecuteWithRetry(
results[i] = returnValues[i].Interface()
}

return results, nil
return results, r.errorList, true
}

/* callMethodOnObject calls a method dynamically on an object with arguments. */
/* callMethodOnObject calls a method dynamically on an object with arguments - error returned here is our fault. */
func (r *MethodCallRetrier) callMethodOnObject(
object interface{},
methodName string,
Expand Down
27 changes: 14 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,48 +17,49 @@ be found [here](https://github.com/J7mbo/palmago-streetview).

Installation
-

`go get github.com/j7mbo/MethodCallRetrier/v2`
```bash
go get github.com/j7mbo/MethodCallRetrier/v2
```

Usage
-

Initialise the object with some options:

```
```go
MethodCallRetrier.New(waitTime time.Duration, maxRetries int64, exponent int64)
```

Call `ExecuteWithRetry` with your object and method you want to retry:

```
```go
ExecuteWithRetry(
object interface{}, methodName string, args ...interface{},
) ([]interface{}, []error)
) (results []interface{}, errs []error, wasSuccessful bool)
```

Alternatively, call `ExecuteFuncWithRetry` and pass in a function that returns `error` to retry.

```
ExecuteFuncWithRetry(func() error) []error
```go
ExecuteFuncWithRetry(func() error) (errs []error, wasSuccessful bool)
```

You can use it as follows:

```
results, errs := retrier.ExecuteWithRetry(yourObject, "MethodToCall", "Arg1", "Arg2", "etc")
```go
results, errs, wasSuccessful := retrier.ExecuteWithRetry(yourObject, "MethodToCall", "Arg1", "Arg2", "etc")
```

The results are an array of `interface{}` objects, (used for the dynamic method call), and an array of all errors.
To use the results, you must typecast the result to the expected type. In the case of an `int64`, for example:

```
```go
myInt := results[0].(int64)
```

Or, to maintain type safety in userland, you can pass a function in instead and do all your retriable work in there:

```
```go
var json string

functionToRetry := func() error {
Expand All @@ -71,8 +72,8 @@ functionToRetry := func() error {
return nil
}

if errs := retrier.ExecuteFuncWithRetry(funcToRetry); len(errs) > 0 {
/* Do something because we failed 3 times */
if wasSuccessful, errs := retrier.ExecuteFuncWithRetry(funcToRetry); !wasSuccesful {
/* Do something with errs because we failed 3 times */
return
}

Expand Down
138 changes: 101 additions & 37 deletions Retrier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,48 +24,48 @@ func TestRetrierTestSuite(t *testing.T) {
func (s *RetrierTestSuite) TestRetrierWorksWithPointer() {
arg := "TestArg"

results, _ := s.retrier.ExecuteWithRetry(&RetryObject{}, "MethodReturningString", arg)
results, _, _ := s.retrier.ExecuteWithRetry(&RetryObject{}, "MethodReturningString", arg)

s.Assert().EqualValues(results[0], arg)
s.EqualValues(results[0], arg)
}

func (s *RetrierTestSuite) TestRetrierWorksWithObject() {
arg := "TestArg"

results, _ := s.retrier.ExecuteWithRetry(RetryObject{}, "MethodReturningString", arg)
results, _, _ := s.retrier.ExecuteWithRetry(RetryObject{}, "MethodReturningString", arg)

s.Assert().EqualValues(results[0], arg)
s.EqualValues(results[0], arg)
}

func (s *RetrierTestSuite) TestRetrierReturnsErrorOnInvalidMethod() {
results, errs := s.retrier.ExecuteWithRetry(RetryObject{}, "InvalidMethodName")
results, errs, _ := s.retrier.ExecuteWithRetry(RetryObject{}, "InvalidMethodName")

s.Assert().Nil(results)
s.Assert().Error(errs[0])
s.Nil(results)
s.Error(errs[0])
}

func (s *RetrierTestSuite) TestRetrierThrowsErrorReturnsNilResults() {
results, _ := s.retrier.ExecuteWithRetry(RetryObject{}, "MethodReturningError", "TestArg")
results, _, _ := s.retrier.ExecuteWithRetry(RetryObject{}, "MethodReturningError", "TestArg")

s.Assert().Nil(results)
s.Nil(results)
}

func (s *RetrierTestSuite) TestRetrierThrowsErrorReturnsErrors() {
_, errs := s.retrier.ExecuteWithRetry(RetryObject{}, "MethodReturningError", "TestArg")
_, errs, _ := s.retrier.ExecuteWithRetry(RetryObject{}, "MethodReturningError", "TestArg")

s.Assert().IsType(errors.New(""), errs[0])
s.IsType(errors.New(""), errs[0])
}

func (s *RetrierTestSuite) TestRetrierThrowsErrorReturnsCorrectNumberOfErrors() {
_, errs := s.retrier.ExecuteWithRetry(RetryObject{}, "MethodReturningError", "TestArg")
_, errs, _ := s.retrier.ExecuteWithRetry(RetryObject{}, "MethodReturningError", "TestArg")

s.Assert().Len(errs, 2)
s.Len(errs, 2)
}

func (s *RetrierTestSuite) TestRetrierReturnsNilWhenGivenObjectWithNoReturnTypes() {
results, _ := s.retrier.ExecuteWithRetry(RetryObject{}, "MethodReturningNoValues")
results, _, _ := s.retrier.ExecuteWithRetry(RetryObject{}, "MethodReturningNoValues")

s.Assert().Len(results, 0)
s.Len(results, 0)
}

func (s *RetrierTestSuite) TestRetrierRetriesCorrectNumberOfTimes() {
Expand All @@ -74,7 +74,7 @@ func (s *RetrierTestSuite) TestRetrierRetriesCorrectNumberOfTimes() {

testObj.On(methodName, "").Return(errors.New(""))

_, _ = New(0, 5, 1).ExecuteWithRetry(&testObj, methodName, "")
_, _, _ = New(0, 5, 1).ExecuteWithRetry(&testObj, methodName, "")

testObj.AssertNumberOfCalls(s.T(), methodName, 5)

Expand All @@ -84,17 +84,17 @@ func (s *RetrierTestSuite) TestRetrierRetriesCorrectNumberOfTimes() {
func (s *RetrierTestSuite) TestRetrierWorksWithNegativeMaxRetries() {
arg := "testArg"

results, _ := New(-1, -1, 1).ExecuteWithRetry(RetryObject{}, "MethodReturningString", arg)
results, _, _ := New(-1, -1, 1).ExecuteWithRetry(RetryObject{}, "MethodReturningString", arg)

s.Assert().EqualValues(results[0], arg)
s.EqualValues(results[0], arg)
}

func (s *RetrierTestSuite) TestRetrierDefaultsToOneRetryGivenZeroMaxRetries() {
testObj := RetryMockObject{}

New(0, 0, 1).ExecuteFuncWithRetry(testObj.MethodToBeCalledToReturnResultAndError)
New(0, 0, 1).ExecuteFuncWithRetry(testObj.MethodToBeCalledToReturnErrorWithTimesCalledAvailable)

s.Assert().Equal(1, testObj.timesCalled)
s.Equal(1, testObj.timesCalled)
}

func (s *RetrierTestSuite) TestRetrierReturnsAllErrorsPlusOurError() {
Expand All @@ -103,56 +103,110 @@ func (s *RetrierTestSuite) TestRetrierReturnsAllErrorsPlusOurError() {

testObj.On(methodName, "").Return(errors.New(""))

_, errs := New(0, 5, 1).ExecuteWithRetry(&testObj, methodName, "")
_, errs, _ := New(0, 5, 1).ExecuteWithRetry(&testObj, methodName, "")

s.Assert().Len(errs, 6)
s.Len(errs, 6)
}

func (s *RetrierTestSuite) TestRetrierWorksWhenErrorIsNotLastReturnParamOnObject() {
testObj := RetryObject{}
methodName := "MethodReturningErrorInRandomPosition"

_, errs := New(1, 1, 1).ExecuteWithRetry(&testObj, methodName, "")
_, errs, _ := New(1, 1, 1).ExecuteWithRetry(&testObj, methodName, "")

s.Assert().IsType(errors.New(""), errs[0])
s.IsType(errors.New(""), errs[0])
}

func (s *RetrierTestSuite) TestRetrierWorksWhenMultipleReturnParamsAreErrors() {
testObj := RetryObject{}
methodName := "MethodReturningMultipleErrors"

_, errs := New(0, 5, 1).ExecuteWithRetry(&testObj, methodName, "")
_, errs, _ := New(0, 5, 1).ExecuteWithRetry(&testObj, methodName, "")

s.Assert().Len(errs, 11)
s.Len(errs, 11)
}

func (s *RetrierTestSuite) TestRetrierWorksWithUserFunction() {
var num int

errs := New(0, 3, 1).ExecuteFuncWithRetry(func() error {
errs, _ := New(0, 3, 1).ExecuteFuncWithRetry(func() error {
num = 42

return nil
})

s.Assert().Equal(42, num)
s.Assert().Len(errs, 0)
s.Equal(42, num)
s.Len(errs, 0)
}

func (s *RetrierTestSuite) TestRetrierWithUserFunctionReturnsCorrectNumberOfErrors() {
errs := New(0, 3, 1).ExecuteFuncWithRetry(func() error {
errs, _ := New(0, 3, 1).ExecuteFuncWithRetry(func() error {
return errors.New("")
})

s.Assert().Equal(4, len(errs))
s.Equal(4, len(errs))
}

func (s *RetrierTestSuite) TestRetrierWorksWithUserFunctionCalledCorrectNumberOfTimes() {
testObj := RetryMockObject{}

New(0, 3, 1).ExecuteFuncWithRetry(testObj.MethodToBeCalledToReturnResultAndError)
New(0, 3, 1).ExecuteFuncWithRetry(testObj.MethodToBeCalledToReturnErrorWithTimesCalledAvailable)

s.Assert().Equal(3, testObj.timesCalled)
s.Equal(3, testObj.timesCalled)
}

func (s *RetrierTestSuite) TestRetrierWithUserFunctionReturnsFalseWhenAllFailed() {
testObj := RetryMockObject{}

errs, wasSuccessful := New(0, 5, 1).ExecuteFuncWithRetry(testObj.MethodToBeCalledToReturnErrorWithTimesCalledAvailable)

s.Equal(5, testObj.timesCalled)
s.Equal(6, len(errs))
s.False(wasSuccessful)
}

func (s *RetrierTestSuite) TestRetrierWithUserFunctionReturnsTrueWhenOneSucceededButOthersFailedFirst() {
testObj := RetryMockObject{}
resultStr := ""

errs, wasSuccessful := New(0, 5, 1).ExecuteFuncWithRetry(func() error {
result, err := testObj.MethodToBeCalledToReturnSuccessOnFifthCall()

if err != nil {
return err
}

resultStr = result

return nil
})

s.Equal("omg", resultStr)
s.Equal(5, testObj.timesCalled)
s.Equal(4, len(errs))
s.True(wasSuccessful)
}

func (s *RetrierTestSuite) TestRetrierWithMethodNameReturnsFalseWhenAllFailed() {
testObj := RetryMockObject{}

results, errs, wasSuccessful := New(0, 5, 1).ExecuteWithRetry(&testObj, "MethodToBeCalledToReturnErrorWithTimesCalledAvailable")

s.Nil(results)
s.Equal(5, testObj.timesCalled)
s.Equal(6, len(errs))
s.False(wasSuccessful)
}

func (s *RetrierTestSuite) TestRetrierWithMethodNameReturnsTrueWhenOneSucceededButOthersFailedFirst() {
testObj := RetryMockObject{}

results, errs, wasSuccessful := New(0, 5, 1).ExecuteWithRetry(&testObj, "MethodToBeCalledToReturnSuccessOnFifthCall")

s.Equal("omg", results[0])
s.Equal(5, testObj.timesCalled)
s.Equal(4, len(errs))
s.True(wasSuccessful)
}

/* This really only exists for coverage */
Expand All @@ -163,9 +217,9 @@ func (s *RetrierTestSuite) TestMaxRetriesError() {

err := MaxRetriesError{methodName: methodName, waitTime: 42, maxRetries: 52}

s.Assert().Contains(err.Error(), methodName)
s.Assert().Contains(err.Error(), waitTime)
s.Assert().Contains(err.Error(), maxRetries)
s.Contains(err.Error(), methodName)
s.Contains(err.Error(), waitTime)
s.Contains(err.Error(), maxRetries)
}

type RetryObject struct{}
Expand Down Expand Up @@ -198,8 +252,18 @@ func (m *RetryMockObject) MethodReturningError(anArgument string) error {
return m.Called(anArgument).Error(0)
}

func (m *RetryMockObject) MethodToBeCalledToReturnResultAndError() error {
func (m *RetryMockObject) MethodToBeCalledToReturnErrorWithTimesCalledAvailable() error {
m.timesCalled += 1

return errors.New("")
}

func (m *RetryMockObject) MethodToBeCalledToReturnSuccessOnFifthCall() (string, error) {
m.timesCalled += 1

if m.timesCalled < 5 {
return "", errors.New("ah crap")
}

return "omg", nil
}

0 comments on commit cd4ffe6

Please sign in to comment.