-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 1389370
Showing
8 changed files
with
338 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.DS_STORE | ||
.idea/ | ||
vendor/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module MethodCallRetrier | ||
|
||
require github.com/stretchr/testify v1.3.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |