Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
J7mbo committed Jan 27, 2019
0 parents commit 1389370
Show file tree
Hide file tree
Showing 8 changed files with 338 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.DS_STORE
.idea/
vendor/
17 changes: 17 additions & 0 deletions MaxRetriesError.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package MethodCallRetrier

import "fmt"

/* Custom error for differentiation. */
type MaxRetriesError struct {
methodName string
waitTime int64
maxRetries int64
}

/* Create a new instance of MaxRetriesError. */
func (e *MaxRetriesError) Error() string {
return fmt.Sprintf(
"Tried calling: '%s' %d times but reached max retries of: %d", e.methodName, e.waitTime, e.maxRetries,
)
}
124 changes: 124 additions & 0 deletions MethodCallRetrier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package MethodCallRetrier

import (
"reflect"
"time"
)

/* Handles the retrying of a function call when an error is given up to X times - useful for http requests. */
type MethodCallRetrier struct {
/* Wait time in seconds between each unsuccessful call. */
waitTime int64

/* Maximum number of retries to attempt before returning an error. */
maxRetries int64

/* Useful for incremental increases in the sleep time. Defaults to no exponent. */
exponent int64

/* Store the current number of retries; is always reset after ExecuteWithRetry() has finished. */
currentRetries int64

/* Store the errors retrieved as they may be different on each subsequent retry. */
errorList []error
}

/* MethodCallRetrier.New returns a new MethodCallRetrier. */
func New(waitTime int64, maxRetries int64, exponent *int64) *MethodCallRetrier {
if exponent == nil {
defaultInt := int64(1)
exponent = &defaultInt
}

if maxRetries <= 0 {
maxRetries = 0
}

return &MethodCallRetrier{waitTime: waitTime, maxRetries: maxRetries, exponent: *exponent}
}

/* 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{},
) ([]reflect.Value, []error) {
defer func() {
r.resetCurrentRetries()
r.resetErrorList()
}()

if r.currentRetries >= r.maxRetries {
r.errorList = append(
r.errorList, &MaxRetriesError{methodName: methodName, waitTime: r.waitTime, maxRetries: r.maxRetries},
)

return nil, r.errorList
}

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

errorFound := false

for i := 0; i < returnValueCount; i++ {
if err, ok := returnValues[i].Interface().(error); ok && err != nil {
r.errorList = append(r.errorList, err)

errorFound = true
}
}

if errorFound == true {
r.sleepAndIncrementRetries(r.waitTime)

return r.ExecuteWithRetry(object, methodName, args...)
}

results := make([]reflect.Value, returnValueCount)

for i := range results {
results[i] = returnValues[i]
}

return results, nil
}

/* callMethodOnObject calls a method dynamically on an object with arguments. */
func (r *MethodCallRetrier) callMethodOnObject(object interface{}, methodName string, args []interface{}) []reflect.Value {
var method reflect.Value

if r.objectIsAPointer(object) {
method = reflect.ValueOf(object).MethodByName(methodName)
} else {
method = reflect.New(reflect.TypeOf(object)).MethodByName(methodName)
}

arguments := make([]reflect.Value, method.Type().NumIn())

for i := 0; i < method.Type().NumIn(); i++ {
arguments[i] = reflect.ValueOf(args[i])
}

return method.Call(arguments)
}

/* If it's a pointer, we need to call the concrete instead */
func (r *MethodCallRetrier) objectIsAPointer(object interface{}) bool {
return reflect.ValueOf(object).Kind() == reflect.Ptr
}

/* Sleep for the given wait time and increment the retry count by 1. */
func (r *MethodCallRetrier) sleepAndIncrementRetries(waitTime int64) {
time.Sleep(time.Duration(waitTime*r.exponent) * time.Second)

r.currentRetries++
}

/* Reset the current retries back to zero so that we can re-use this object elsewhere. */
func (r *MethodCallRetrier) resetCurrentRetries() {
r.currentRetries = 0
}

/* Reset the error list back to zero so that we can re-use this object elsewhere. */
func (r *MethodCallRetrier) resetErrorList() {
r.errorList = nil
}
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
Features
-

Retry your method calls automatically when an `error` is returned up to a specified number of times, with a given wait time.

Extremely useful for retrying HTTP calls in distributed systems, or anything else over the network that can error sporadically.

Installation
-

`go get github.com/j7mbo/MethodCallRetrier`

Usage
-

Initialise the object with some options:

```
MethodCallRetrier.New(waitTime int64, maxRetries int64, exponent *int64, onError *func(err error, retryCount int64)
```

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

```
ExecuteWithRetry(
object interface{}, methodName string, args ...interface{},
) ([]reflect.Value, []error) {
```

You can use it as follows:

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

The results are an array of `reflect.Value` 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:

```
myInt := results[0].Interface().(int64)
```
10 changes: 10 additions & 0 deletions Retrier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package MethodCallRetrier

import "reflect"

/* Represents an object capable of retrying a call on an object X times after receiving an error. */
type Retrier interface {
ExecuteWithRetry(
maxRetries int64, waitTime int64, object interface{}, methodName string, args ...interface{},
) ([]reflect.Value, error)
}
132 changes: 132 additions & 0 deletions Retrier_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package MethodCallRetrier

import (
"errors"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
"testing"
)

type RetrierTestSuite struct {
suite.Suite

retrier *MethodCallRetrier
}

func (s *RetrierTestSuite) SetupTest() {
s.retrier = New(0, 1, nil)
}

func TestRetrierTestSuite(t *testing.T) {
suite.Run(t, new(RetrierTestSuite))
}

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

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

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

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

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

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

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

s.Assert().Nil(results)
}

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

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

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

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

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

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

func (s *RetrierTestSuite) TestRetrierRetriesCorrectNumberOfTimes() {
testObj := RetryMockObject{}
methodName := "MethodReturningError"

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

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

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

testObj.AssertExpectations(s.T())
}

func (s *RetrierTestSuite) TestRetrierReturnsAllErrorsPlusOurError() {
testObj := RetryMockObject{}
methodName := "MethodReturningError"

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

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

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

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

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

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

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

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

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

type RetryObject struct{}

func (m *RetryObject) MethodReturningNoValues() {}

func (m *RetryObject) MethodReturningString(anArgument string) string {
return anArgument
}

func (m *RetryObject) MethodReturningError(anArgument string) error {
return errors.New("")
}

func (m *RetryObject) MethodReturningErrorInRandomPosition() (string, error, string) {
return "", errors.New(""), ""
}

func (m *RetryObject) MethodReturningMultipleErrors() (string, error, error) {
return "", errors.New(""), errors.New("")
}

type RetryMockObject struct {
mock.Mock
}

func (m *RetryMockObject) MethodReturningError(anArgument string) error {
return m.Called(anArgument).Error(0)
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module MethodCallRetrier

require github.com/stretchr/testify v1.3.0
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=

0 comments on commit 1389370

Please sign in to comment.