├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── error.go ├── options.go └── retry.go /.gitignore: -------------------------------------------------------------------------------- 1 | .gobuild/ 2 | retry-go 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Giant Swarm GmbH 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT=retry-go 2 | 3 | BUILD_PATH := $(shell pwd)/.gobuild 4 | 5 | PROJECT_PATH := "$(BUILD_PATH)/src/github.com/giantswarm" 6 | 7 | BIN=$(PROJECT) 8 | 9 | .PHONY:all clean get-deps fmt run-tests 10 | 11 | GOPATH := $(BUILD_PATH) 12 | 13 | SOURCE=$(shell find . -name '*.go') 14 | 15 | all: get-deps $(BIN) 16 | 17 | clean: 18 | rm -rf $(BUILD_PATH) $(BIN) 19 | 20 | get-deps: .gobuild 21 | 22 | .gobuild: 23 | mkdir -p $(PROJECT_PATH) 24 | cd "$(PROJECT_PATH)" && ln -s ../../../.. $(PROJECT) 25 | 26 | # 27 | # Fetch private packages first (so `go get` skips them later) 28 | 29 | # 30 | # Fetch public dependencies via `go get` 31 | GOPATH=$(GOPATH) go get -d -v github.com/giantswarm/$(PROJECT) 32 | 33 | $(BIN): $(SOURCE) 34 | GOPATH=$(GOPATH) go build -o $(BIN) 35 | 36 | run-tests: 37 | GOPATH=$(GOPATH) go test ./... 38 | 39 | fmt: 40 | gofmt -l -w . 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | retry-go 2 | ======== 3 | 4 | Small helper library to retry operations automatically on certain errors. 5 | 6 | ## Usage 7 | 8 | The `retry` package provides a `Do()` function which can be used to execute a provided function 9 | until it succeds. 10 | 11 | ``` 12 | op := func() error { 13 | // Do something that can fail and should be retried here 14 | return httpClient.CreateUserOnRemoteServer() 15 | } 16 | retry.Do(op, 17 | retry.RetryChecker(IsNetOpErr), 18 | retry.Timeout(15 * time.Second)) 19 | ``` 20 | 21 | Besides the `op` itself, you can provide a few options: 22 | 23 | * RetryChecker(func(err error) bool) - If this func returns true for the returned error, the operation is tried again (default: nil - no retries) 24 | * MaxTries(int) - Maximum number of calls to op() before aborting with MaxRetriesReachedErr 25 | * Timeout(time.Duration) - Maximum number of time to try to perform this op before aborting with TimeoutReachedErr 26 | * Sleep(time.Duration) - time to sleep after every failed op() 27 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.5.0+git -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package retry 2 | 3 | import ( 4 | "github.com/juju/errgo" 5 | ) 6 | 7 | var ( 8 | TimeoutError = errgo.New("Operation aborted. Timeout occured") 9 | MaxRetriesReachedError = errgo.New("Operation aborted. Too many errors.") 10 | ) 11 | 12 | // IsTimeout returns true if the cause of the given error is a TimeoutError. 13 | func IsTimeout(err error) bool { 14 | return errgo.Cause(err) == TimeoutError 15 | } 16 | 17 | // IsMaxRetriesReached returns true if the cause of the given error is a MaxRetriesReachedError. 18 | func IsMaxRetriesReached(err error) bool { 19 | return errgo.Cause(err) == MaxRetriesReachedError 20 | } 21 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package retry 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/juju/errgo" 7 | ) 8 | 9 | const ( 10 | DefaultMaxTries = 3 11 | DefaultTimeout = time.Duration(15 * time.Second) 12 | ) 13 | 14 | // Not is a helper to invert another . 15 | func Not(checker func(err error) bool) func(err error) bool { 16 | return func(err error) bool { 17 | return !checker(err) 18 | } 19 | } 20 | 21 | type RetryOption func(options *retryOptions) 22 | 23 | // Timeout specifies the maximum time that should be used before aborting the retry loop. 24 | // Note that this does not abort the operation in progress. 25 | func Timeout(d time.Duration) RetryOption { 26 | return func(options *retryOptions) { 27 | options.Timeout = d 28 | } 29 | } 30 | 31 | // MaxTries specifies the maximum number of times op will be called by Do(). 32 | func MaxTries(tries int) RetryOption { 33 | return func(options *retryOptions) { 34 | options.MaxTries = tries 35 | } 36 | } 37 | 38 | // RetryChecker defines whether the given error is an error that can be retried. 39 | func RetryChecker(checker func(err error) bool) RetryOption { 40 | return func(options *retryOptions) { 41 | options.Checker = checker 42 | } 43 | } 44 | 45 | func Sleep(d time.Duration) RetryOption { 46 | return func(options *retryOptions) { 47 | options.Sleep = d 48 | } 49 | } 50 | 51 | // AfterRetry is called after a retry and can be used e.g. to emit events. 52 | func AfterRetry(afterRetry func(err error)) RetryOption { 53 | return func(options *retryOptions) { 54 | options.AfterRetry = afterRetry 55 | } 56 | } 57 | 58 | // AfterRetryLimit is called after a retry limit is reached and can be used 59 | // e.g. to emit events. 60 | func AfterRetryLimit(afterRetryLimit func(err error)) RetryOption { 61 | return func(options *retryOptions) { 62 | options.AfterRetryLimit = afterRetryLimit 63 | } 64 | } 65 | 66 | type retryOptions struct { 67 | Timeout time.Duration 68 | MaxTries int 69 | Checker func(err error) bool 70 | Sleep time.Duration 71 | AfterRetry func(err error) 72 | AfterRetryLimit func(err error) 73 | } 74 | 75 | func newRetryOptions(options ...RetryOption) retryOptions { 76 | state := retryOptions{ 77 | Timeout: DefaultTimeout, 78 | MaxTries: DefaultMaxTries, 79 | Checker: errgo.Any, 80 | AfterRetry: func(err error) {}, 81 | AfterRetryLimit: func(err error) {}, 82 | } 83 | 84 | for _, option := range options { 85 | option(&state) 86 | } 87 | 88 | return state 89 | } 90 | -------------------------------------------------------------------------------- /retry.go: -------------------------------------------------------------------------------- 1 | package retry 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/juju/errgo" 7 | ) 8 | 9 | // Do performs the given operation. Based on the options, it can retry the operation, 10 | // if it failed. 11 | // 12 | // The following options are supported: 13 | // * RetryChecker(func(err error) bool) - If this func returns true for the returned error, the operation is tried again 14 | // * MaxTries(int) - Maximum number of calls to op() before aborting with MaxRetriesReached 15 | // * Timeout(time.Duration) - Maximum number of time to try to perform this op before aborting with TimeoutReached 16 | // * Sleep(time.Duration) - time to sleep after error failed op() 17 | // 18 | // Defaults: 19 | // Timeout = 15 seconds 20 | // MaxRetries = 5 21 | // Retryer = errgo.Any 22 | // Sleep = No sleep 23 | // 24 | func Do(op func() error, retryOptions ...RetryOption) error { 25 | options := newRetryOptions(retryOptions...) 26 | 27 | var timeout <-chan time.Time 28 | if options.Timeout > 0 { 29 | timeout = time.After(options.Timeout) 30 | } 31 | 32 | tryCounter := 0 33 | for { 34 | // Check if we reached the timeout 35 | select { 36 | case <-timeout: 37 | return errgo.Mask(TimeoutError, errgo.Any) 38 | default: 39 | } 40 | 41 | // Execute the op 42 | tryCounter++ 43 | lastError := op() 44 | options.AfterRetry(lastError) 45 | 46 | if lastError != nil { 47 | if options.Checker != nil && options.Checker(lastError) { 48 | // Check max retries 49 | if tryCounter >= options.MaxTries { 50 | options.AfterRetryLimit(lastError) 51 | return errgo.WithCausef(lastError, MaxRetriesReachedError, "retry limit reached (%d/%d)", tryCounter, options.MaxTries) 52 | } 53 | 54 | if options.Sleep > 0 { 55 | time.Sleep(options.Sleep) 56 | } 57 | continue 58 | } 59 | 60 | return errgo.Mask(lastError, errgo.Any) 61 | } 62 | return nil 63 | } 64 | } 65 | --------------------------------------------------------------------------------