pax_global_header00006660000000000000000000000064142200277400014510gustar00rootroot0000000000000052 comment=e965ea36f7a0e81650665f74fd090cfc035ada83 golang-github-rican7-retry-0.3.1/000077500000000000000000000000001422002774000165645ustar00rootroot00000000000000golang-github-rican7-retry-0.3.1/.github/000077500000000000000000000000001422002774000201245ustar00rootroot00000000000000golang-github-rican7-retry-0.3.1/.github/workflows/000077500000000000000000000000001422002774000221615ustar00rootroot00000000000000golang-github-rican7-retry-0.3.1/.github/workflows/main.yml000066400000000000000000000022061422002774000236300ustar00rootroot00000000000000name: Main on: push: branches: ['*'] tags: ['v*'] pull_request: branches: ['*'] env: GO_TEST_COVERAGE_FILE_NAME: coverage.out jobs: build: runs-on: ubuntu-latest strategy: matrix: go: ['1.15.x', '1.16.x'] steps: - name: Setup Go uses: actions/setup-go@v2 with: go-version: ${{ matrix.go }} - name: Checkout code uses: actions/checkout@v2 - name: Load cached dependencies uses: actions/cache@v1 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Download dependencies run: make install-deps install-deps-dev - name: Lint run: make lint - name: Vet run: make vet - name: Test run: make test-with-coverage-profile - name: Send code coverage to coveralls if: ${{ matrix.go == '1.16.x' }} env: COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | go install github.com/mattn/goveralls@latest goveralls -coverprofile="$GO_TEST_COVERAGE_FILE_NAME" -service=github golang-github-rican7-retry-0.3.1/.gitignore000066400000000000000000000001001422002774000205430ustar00rootroot00000000000000# Tools binaries /tools/bin # Code coverage files coverage.out golang-github-rican7-retry-0.3.1/LICENSE000066400000000000000000000020551422002774000175730ustar00rootroot00000000000000Copyright (C) 2016 Trevor N. Suarez (Rican7) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. golang-github-rican7-retry-0.3.1/Makefile000066400000000000000000000034441422002774000202310ustar00rootroot00000000000000# Define directories ROOT_DIR ?= ${CURDIR} TOOLS_DIR ?= ${ROOT_DIR}/tools # Set a local GOBIN, since the default value can't be trusted # (See https://github.com/golang/go/issues/23439) export GOBIN ?= ${TOOLS_DIR}/bin # Set the mode for code-coverage GO_TEST_COVERAGE_MODE ?= count GO_TEST_COVERAGE_FILE_NAME ?= coverage.out # Set flags for `gofmt` GOFMT_FLAGS ?= -s # Set a default `min_confidence` value for `golint` GOLINT_MIN_CONFIDENCE ?= 0.1 all: build clean: go clean -i -x ./... build: go build -v ./... install-deps: go mod download tools install-deps-dev: cd tools && go install \ golang.org/x/lint/golint \ golang.org/x/tools/cmd/goimports \ honnef.co/go/tools/cmd/staticcheck update-deps: go get ./... test: go test -v ./... test-with-coverage: go test -cover -covermode ${GO_TEST_COVERAGE_MODE} ./... test-with-coverage-formatted: go test -cover -covermode ${GO_TEST_COVERAGE_MODE} ./... | column -t | sort -r test-with-coverage-profile: go test -covermode ${GO_TEST_COVERAGE_MODE} -coverprofile ${GO_TEST_COVERAGE_FILE_NAME} ./... format-lint: errors=$$(gofmt -l ${GOFMT_FLAGS} .); if [ "$${errors}" != "" ]; then echo "$${errors}"; exit 1; fi import-lint: install-deps-dev errors=$$(${GOBIN}/goimports -l .); if [ "$${errors}" != "" ]; then echo "$${errors}"; exit 1; fi style-lint: install-deps-dev ${GOBIN}/golint -min_confidence=${GOLINT_MIN_CONFIDENCE} -set_exit_status ./... ${GOBIN}/staticcheck ./... lint: install-deps-dev format-lint import-lint style-lint vet: go vet ./... format-fix: gofmt -w ${GOFMT_FLAGS} . import-fix: goimports -w . .PHONY: all clean build install-deps tools install-deps-dev update-deps test test-with-coverage test-with-coverage-formatted test-with-coverage-profile format-lint import-lint style-lint lint vet format-fix import-fix golang-github-rican7-retry-0.3.1/README.md000066400000000000000000000050111422002774000200400ustar00rootroot00000000000000# retry [![Build Status](https://github.com/Rican7/retry/actions/workflows/main.yml/badge.svg?branch=master)](https://github.com/Rican7/retry/actions/workflows/main.yml) [![Coverage Status](https://coveralls.io/repos/github/Rican7/retry/badge.svg)](https://coveralls.io/github/Rican7/retry) [![Go Report Card](https://goreportcard.com/badge/Rican7/retry)](http://goreportcard.com/report/Rican7/retry) [![Go Reference](https://pkg.go.dev/badge/github.com/Rican7/retry.svg)](https://pkg.go.dev/github.com/Rican7/retry) [![Latest Stable Version](https://img.shields.io/github/release/Rican7/retry.svg?style=flat)](https://github.com/Rican7/retry/releases) A simple, stateless, functional mechanism to perform actions repetitively until successful. ## Project Status This project is currently in "pre-release". While the code is heavily tested, the API may change. Use a tagged version or vendor this dependency if you plan on using it. That said, this code has been used in production without issue for years, and has been used by some relatively [high-profile projects/codebases](https://pkg.go.dev/github.com/Rican7/retry?tab=importedby). ## Examples ### Basic ```go retry.Retry(func(attempt uint) error { return nil // Do something that may or may not cause an error }) ``` ### File Open ```go const logFilePath = "/var/log/myapp.log" var logFile *os.File err := retry.Retry(func(attempt uint) error { var err error logFile, err = os.Open(logFilePath) return err }) if err != nil { log.Fatalf("Unable to open file %q with error %q", logFilePath, err) } logFile.Chdir() // Do something with the file ``` ### HTTP request with strategies and backoff ```go var response *http.Response action := func(attempt uint) error { var err error response, err = http.Get("https://api.github.com/repos/Rican7/retry") if err == nil && response != nil && response.StatusCode > 200 { err = fmt.Errorf("failed to fetch (attempt #%d) with status code: %d", attempt, response.StatusCode) } return err } err := retry.Retry( action, strategy.Limit(5), strategy.Backoff(backoff.Fibonacci(10*time.Millisecond)), ) if err != nil { log.Fatalf("Failed to fetch repository with error %q", err) } ``` ### Retry with backoff jitter ```go action := func(attempt uint) error { return errors.New("something happened") } seed := time.Now().UnixNano() random := rand.New(rand.NewSource(seed)) retry.Retry( action, strategy.Limit(5), strategy.BackoffWithJitter( backoff.BinaryExponential(10*time.Millisecond), jitter.Deviation(random, 0.5), ), ) ``` golang-github-rican7-retry-0.3.1/backoff/000077500000000000000000000000001422002774000201575ustar00rootroot00000000000000golang-github-rican7-retry-0.3.1/backoff/backoff.go000066400000000000000000000041671422002774000221110ustar00rootroot00000000000000// Package backoff provides stateless methods of calculating durations based on // a number of attempts made. // // Copyright © 2016 Trevor N. Suarez (Rican7) package backoff import ( "math" "time" ) // Algorithm defines a function that calculates a time.Duration based on // the given retry attempt number. type Algorithm func(attempt uint) time.Duration // Incremental creates a Algorithm that increments the initial duration // by the given increment for each attempt. func Incremental(initial, increment time.Duration) Algorithm { return func(attempt uint) time.Duration { return initial + (increment * time.Duration(attempt)) } } // Linear creates a Algorithm that linearly multiplies the factor // duration by the attempt number for each attempt. func Linear(factor time.Duration) Algorithm { return func(attempt uint) time.Duration { return (factor * time.Duration(attempt)) } } // Exponential creates a Algorithm that multiplies the factor duration by // an exponentially increasing factor for each attempt, where the factor is // calculated as the given base raised to the attempt number. func Exponential(factor time.Duration, base float64) Algorithm { return func(attempt uint) time.Duration { return (factor * time.Duration(math.Pow(base, float64(attempt)))) } } // BinaryExponential creates a Algorithm that multiplies the factor // duration by an exponentially increasing factor for each attempt, where the // factor is calculated as `2` raised to the attempt number (2^attempt). func BinaryExponential(factor time.Duration) Algorithm { return Exponential(factor, 2) } // Fibonacci creates a Algorithm that multiplies the factor duration by // an increasing factor for each attempt, where the factor is the Nth number in // the Fibonacci sequence. func Fibonacci(factor time.Duration) Algorithm { return func(attempt uint) time.Duration { return (factor * time.Duration(fibonacciNumber(attempt))) } } // fibonacciNumber calculates the Fibonacci sequence number for the given // sequence position. func fibonacciNumber(n uint) uint { if n == 0 || n == 1 { return n } return fibonacciNumber(n-1) + fibonacciNumber(n-2) } golang-github-rican7-retry-0.3.1/backoff/backoff_test.go000066400000000000000000000074201422002774000231430ustar00rootroot00000000000000package backoff import ( "fmt" "math" "testing" "time" ) func TestIncremental(t *testing.T) { const duration = time.Millisecond const increment = time.Nanosecond algorithm := Incremental(duration, increment) for i := uint(0); i < 10; i++ { result := algorithm(i) expected := duration + (time.Duration(i) * increment) if result != expected { t.Errorf("algorithm expected to return a %s duration, but received %s instead", expected, result) } } } func TestLinear(t *testing.T) { const duration = time.Millisecond algorithm := Linear(duration) for i := uint(0); i < 10; i++ { result := algorithm(i) expected := time.Duration(i) * duration if result != expected { t.Errorf("algorithm expected to return a %s duration, but received %s instead", expected, result) } } } func TestExponential(t *testing.T) { const duration = time.Second const base = 3 algorithm := Exponential(duration, base) for i := uint(0); i < 10; i++ { result := algorithm(i) expected := time.Duration(math.Pow(base, float64(i))) * duration if result != expected { t.Errorf("algorithm expected to return a %s duration, but received %s instead", expected, result) } } } func TestBinaryExponential(t *testing.T) { const duration = time.Second algorithm := BinaryExponential(duration) for i := uint(0); i < 10; i++ { result := algorithm(i) expected := time.Duration(math.Pow(2, float64(i))) * duration if result != expected { t.Errorf("algorithm expected to return a %s duration, but received %s instead", expected, result) } } } func TestFibonacci(t *testing.T) { const duration = time.Millisecond algorithm := Fibonacci(duration) for i := uint(0); i < 10; i++ { result := algorithm(i) expected := time.Duration(fibonacciNumber(i)) * duration if result != expected { t.Errorf("algorithm expected to return a %s duration, but received %s instead", expected, result) } } } func TestFibonacciNumber(t *testing.T) { // Fibonacci sequence expectedSequence := []uint{0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233} for i, expected := range expectedSequence { result := fibonacciNumber(uint(i)) if result != expected { t.Errorf("fibonacci %d number expected %d, but got %d", i, expected, result) } } } func ExampleIncremental() { algorithm := Incremental(15*time.Millisecond, 10*time.Millisecond) for i := uint(1); i <= 5; i++ { duration := algorithm(i) fmt.Printf("#%d attempt: %s\n", i, duration) } // Output: // #1 attempt: 25ms // #2 attempt: 35ms // #3 attempt: 45ms // #4 attempt: 55ms // #5 attempt: 65ms } func ExampleLinear() { algorithm := Linear(15 * time.Millisecond) for i := uint(1); i <= 5; i++ { duration := algorithm(i) fmt.Printf("#%d attempt: %s\n", i, duration) } // Output: // #1 attempt: 15ms // #2 attempt: 30ms // #3 attempt: 45ms // #4 attempt: 60ms // #5 attempt: 75ms } func ExampleExponential() { algorithm := Exponential(15*time.Millisecond, 3) for i := uint(1); i <= 5; i++ { duration := algorithm(i) fmt.Printf("#%d attempt: %s\n", i, duration) } // Output: // #1 attempt: 45ms // #2 attempt: 135ms // #3 attempt: 405ms // #4 attempt: 1.215s // #5 attempt: 3.645s } func ExampleBinaryExponential() { algorithm := BinaryExponential(15 * time.Millisecond) for i := uint(1); i <= 5; i++ { duration := algorithm(i) fmt.Printf("#%d attempt: %s\n", i, duration) } // Output: // #1 attempt: 30ms // #2 attempt: 60ms // #3 attempt: 120ms // #4 attempt: 240ms // #5 attempt: 480ms } func ExampleFibonacci() { algorithm := Fibonacci(15 * time.Millisecond) for i := uint(1); i <= 5; i++ { duration := algorithm(i) fmt.Printf("#%d attempt: %s\n", i, duration) } // Output: // #1 attempt: 15ms // #2 attempt: 15ms // #3 attempt: 30ms // #4 attempt: 45ms // #5 attempt: 75ms } golang-github-rican7-retry-0.3.1/example_test.go000066400000000000000000000033661422002774000216150ustar00rootroot00000000000000package retry_test import ( "errors" "fmt" "log" "math/rand" "net/http" "os" "time" "github.com/Rican7/retry" "github.com/Rican7/retry/backoff" "github.com/Rican7/retry/jitter" "github.com/Rican7/retry/strategy" ) func Example() { retry.Retry(func(attempt uint) error { return nil // Do something that may or may not cause an error }) } func Example_fileOpen() { const logFilePath = "/var/log/myapp.log" var logFile *os.File err := retry.Retry(func(attempt uint) error { var err error logFile, err = os.Open(logFilePath) return err }) if err != nil { log.Fatalf("Unable to open file %q with error %q", logFilePath, err) } logFile.Chdir() // Do something with the file } func Example_httpGetWithStrategies() { var response *http.Response action := func(attempt uint) error { var err error response, err = http.Get("https://api.github.com/repos/Rican7/retry") if err == nil && response != nil && response.StatusCode > 200 { err = fmt.Errorf("failed to fetch (attempt #%d) with status code: %d", attempt, response.StatusCode) } return err } err := retry.Retry( action, strategy.Limit(5), strategy.Backoff(backoff.Fibonacci(10*time.Millisecond)), ) if err != nil { log.Fatalf("Failed to fetch repository with error %q", err) } } func Example_withBackoffJitter() { action := func(attempt uint) error { fmt.Println("attempt", attempt) return errors.New("something happened") } seed := time.Now().UnixNano() random := rand.New(rand.NewSource(seed)) retry.Retry( action, strategy.Limit(5), strategy.BackoffWithJitter( backoff.BinaryExponential(10*time.Millisecond), jitter.Deviation(random, 0.5), ), ) // Output: // attempt 1 // attempt 2 // attempt 3 // attempt 4 // attempt 5 } golang-github-rican7-retry-0.3.1/go.mod000066400000000000000000000000501422002774000176650ustar00rootroot00000000000000module github.com/Rican7/retry go 1.11 golang-github-rican7-retry-0.3.1/jitter/000077500000000000000000000000001422002774000200655ustar00rootroot00000000000000golang-github-rican7-retry-0.3.1/jitter/jitter.go000066400000000000000000000062051422002774000217200ustar00rootroot00000000000000// Package jitter provides methods of transforming durations. // // Copyright © 2016 Trevor N. Suarez (Rican7) package jitter import ( "math" "math/rand" "time" ) // Transformation defines a function that calculates a time.Duration based on // the given duration. type Transformation func(duration time.Duration) time.Duration // Full creates a Transformation that transforms a duration into a result // duration in [0, n) randomly, where n is the given duration. // // The given generator is what is used to determine the random transformation. // If a nil generator is passed, a default one will be provided. // // Inspired by https://www.awsarchitectureblog.com/2015/03/backoff.html func Full(generator *rand.Rand) Transformation { random := fallbackNewRandom(generator) return func(duration time.Duration) time.Duration { return time.Duration(random.Int63n(int64(duration))) } } // Equal creates a Transformation that transforms a duration into a result // duration in [n/2, n) randomly, where n is the given duration. // // The given generator is what is used to determine the random transformation. // If a nil generator is passed, a default one will be provided. // // Inspired by https://www.awsarchitectureblog.com/2015/03/backoff.html func Equal(generator *rand.Rand) Transformation { random := fallbackNewRandom(generator) return func(duration time.Duration) time.Duration { return (duration / 2) + time.Duration(random.Int63n(int64(duration))/2) } } // Deviation creates a Transformation that transforms a duration into a result // duration that deviates from the input randomly by a given factor. // // The given generator is what is used to determine the random transformation. // If a nil generator is passed, a default one will be provided. // // Inspired by https://developers.google.com/api-client-library/java/google-http-java-client/backoff func Deviation(generator *rand.Rand, factor float64) Transformation { random := fallbackNewRandom(generator) return func(duration time.Duration) time.Duration { min := int64(math.Floor(float64(duration) * (1 - factor))) max := int64(math.Ceil(float64(duration) * (1 + factor))) return time.Duration(random.Int63n(max-min) + min) } } // NormalDistribution creates a Transformation that transforms a duration into a // result duration based on a normal distribution of the input and the given // standard deviation. // // The given generator is what is used to determine the random transformation. // If a nil generator is passed, a default one will be provided. func NormalDistribution(generator *rand.Rand, standardDeviation float64) Transformation { random := fallbackNewRandom(generator) return func(duration time.Duration) time.Duration { return time.Duration(random.NormFloat64()*standardDeviation + float64(duration)) } } // fallbackNewRandom returns the passed in random instance if it's not nil, // and otherwise returns a new random instance seeded with the current time. func fallbackNewRandom(random *rand.Rand) *rand.Rand { // Return the passed in value if it's already not null if random != nil { return random } seed := time.Now().UnixNano() return rand.New(rand.NewSource(seed)) } golang-github-rican7-retry-0.3.1/jitter/jitter_test.go000066400000000000000000000047321422002774000227620ustar00rootroot00000000000000package jitter import ( "math/rand" "testing" "time" ) func TestFull(t *testing.T) { const seed = 0 const duration = time.Millisecond generator := rand.New(rand.NewSource(seed)) transformation := Full(generator) // Based on constant seed expectedDurations := []time.Duration{165505, 393152, 995827, 197794, 376202} for _, expected := range expectedDurations { result := transformation(duration) if result != expected { t.Errorf("transformation expected to return a %s duration, but received %s instead", expected, result) } } } func TestEqual(t *testing.T) { const seed = 0 const duration = time.Millisecond generator := rand.New(rand.NewSource(seed)) transformation := Equal(generator) // Based on constant seed expectedDurations := []time.Duration{582752, 696576, 997913, 598897, 688101} for _, expected := range expectedDurations { result := transformation(duration) if result != expected { t.Errorf("transformation expected to return a %s duration, but received %s instead", expected, result) } } } func TestDeviation(t *testing.T) { const seed = 0 const duration = time.Millisecond const factor = 0.5 generator := rand.New(rand.NewSource(seed)) transformation := Deviation(generator, factor) // Based on constant seed expectedDurations := []time.Duration{665505, 893152, 1495827, 697794, 876202} for _, expected := range expectedDurations { result := transformation(duration) if result != expected { t.Errorf("transformation expected to return a %s duration, but received %s instead", expected, result) } } } func TestNormalDistribution(t *testing.T) { const seed = 0 const duration = time.Millisecond const standardDeviation = float64(duration / 2) generator := rand.New(rand.NewSource(seed)) transformation := NormalDistribution(generator, standardDeviation) // Based on constant seed expectedDurations := []time.Duration{859207, 1285466, 153990, 1099811, 1959759} for _, expected := range expectedDurations { result := transformation(duration) if result != expected { t.Errorf("transformation expected to return a %s duration, but received %s instead", expected, result) } } } func TestFallbackNewRandom(t *testing.T) { generator := rand.New(rand.NewSource(0)) if result := fallbackNewRandom(generator); generator != result { t.Errorf("result expected to match parameter, received %+v instead", result) } if result := fallbackNewRandom(nil); result == nil { t.Error("received unexpected nil result") } } golang-github-rican7-retry-0.3.1/retry.go000066400000000000000000000021451422002774000202620ustar00rootroot00000000000000// Package retry provides a simple, stateless, functional mechanism to perform // actions repetitively until successful. // // Copyright © 2016 Trevor N. Suarez (Rican7) package retry import "github.com/Rican7/retry/strategy" // Action defines a callable function that package retry can handle. type Action func(attempt uint) error // Retry takes an action and performs it, repetitively, until successful. // // Optionally, strategies may be passed that assess whether or not an attempt // should be made. func Retry(action Action, strategies ...strategy.Strategy) error { var err error for attempt := uint(0); (attempt == 0 || err != nil) && shouldAttempt(attempt, strategies...); attempt++ { err = action(attempt + 1) } return err } // shouldAttempt evaluates the provided strategies with the given attempt to // determine if the Retry loop should make another attempt. func shouldAttempt(attempt uint, strategies ...strategy.Strategy) bool { shouldAttempt := true for i := 0; shouldAttempt && i < len(strategies); i++ { shouldAttempt = shouldAttempt && strategies[i](attempt) } return shouldAttempt } golang-github-rican7-retry-0.3.1/retry_test.go000066400000000000000000000054231422002774000213230ustar00rootroot00000000000000package retry import ( "errors" "testing" ) func TestRetry(t *testing.T) { action := func(attempt uint) error { return nil } err := Retry(action) if err != nil { t.Error("expected a nil error") } } func TestRetryAttemptNumberIsAccurate(t *testing.T) { var strategyAttemptNumber uint var actionAttemptNumber uint strategy := func(attempt uint) bool { strategyAttemptNumber = attempt return true } action := func(attempt uint) error { actionAttemptNumber = attempt return nil } err := Retry(action, strategy) if err != nil { t.Error("expected a nil error") } if strategyAttemptNumber != 0 { t.Errorf( "expected strategy to receive 0, received %v instead", strategyAttemptNumber, ) } if actionAttemptNumber != 1 { t.Errorf( "expected action to receive 1, received %v instead", actionAttemptNumber, ) } } func TestRetryRetriesUntilNoErrorReturned(t *testing.T) { const errorUntilAttemptNumber = 5 var attemptsMade uint action := func(attempt uint) error { attemptsMade = attempt if errorUntilAttemptNumber == attempt { return nil } return errors.New("erroring") } err := Retry(action) if err != nil { t.Error("expected a nil error") } if errorUntilAttemptNumber != attemptsMade { t.Errorf( "expected %d attempts to be made, but %d were made instead", errorUntilAttemptNumber, attemptsMade, ) } } func TestShouldAttempt(t *testing.T) { shouldAttempt := shouldAttempt(1) if !shouldAttempt { t.Error("expected to return true") } } func TestShouldAttemptWithStrategy(t *testing.T) { const attemptNumberShouldReturnFalse = 7 strategy := func(attempt uint) bool { return (attemptNumberShouldReturnFalse != attempt) } should := shouldAttempt(1, strategy) if !should { t.Error("expected to return true") } should = shouldAttempt(1+attemptNumberShouldReturnFalse, strategy) if !should { t.Error("expected to return true") } should = shouldAttempt(attemptNumberShouldReturnFalse, strategy) if should { t.Error("expected to return false") } } func TestShouldAttemptWithMultipleStrategies(t *testing.T) { trueStrategy := func(attempt uint) bool { return true } falseStrategy := func(attempt uint) bool { return false } should := shouldAttempt(1, trueStrategy) if !should { t.Error("expected to return true") } should = shouldAttempt(1, falseStrategy) if should { t.Error("expected to return false") } should = shouldAttempt(1, trueStrategy, trueStrategy, trueStrategy) if !should { t.Error("expected to return true") } should = shouldAttempt(1, falseStrategy, falseStrategy, falseStrategy) if should { t.Error("expected to return false") } should = shouldAttempt(1, trueStrategy, trueStrategy, falseStrategy) if should { t.Error("expected to return false") } } golang-github-rican7-retry-0.3.1/strategy/000077500000000000000000000000001422002774000204265ustar00rootroot00000000000000golang-github-rican7-retry-0.3.1/strategy/strategy.go000066400000000000000000000050471422002774000226250ustar00rootroot00000000000000// Package strategy provides a way to change the way that retry is performed. // // Copyright © 2016 Trevor N. Suarez (Rican7) package strategy import ( "time" "github.com/Rican7/retry/backoff" "github.com/Rican7/retry/jitter" ) // Strategy defines a function that Retry calls before every successive attempt // to determine whether it should make the next attempt or not. Returning `true` // allows for the next attempt to be made. Returning `false` halts the retrying // process and returns the last error returned by the called Action. // // The strategy will be passed an "attempt" number before each successive retry // iteration, starting with a `0` value before the first attempt is actually // made. This allows for a pre-action, such as a delay, etc. type Strategy func(attempt uint) bool // Limit creates a Strategy that limits the number of attempts that Retry will // make. func Limit(attemptLimit uint) Strategy { return func(attempt uint) bool { return (attempt < attemptLimit) } } // Delay creates a Strategy that waits the given duration before the first // attempt is made. func Delay(duration time.Duration) Strategy { return func(attempt uint) bool { if attempt == 0 { time.Sleep(duration) } return true } } // Wait creates a Strategy that waits the given durations for each attempt after // the first. If the number of attempts is greater than the number of durations // provided, then the strategy uses the last duration provided. func Wait(durations ...time.Duration) Strategy { return func(attempt uint) bool { if attempt > 0 && len(durations) > 0 { durationIndex := int(attempt - 1) if len(durations) <= durationIndex { durationIndex = len(durations) - 1 } time.Sleep(durations[durationIndex]) } return true } } // Backoff creates a Strategy that waits before each attempt, with a duration as // defined by the given backoff.Algorithm. func Backoff(algorithm backoff.Algorithm) Strategy { return BackoffWithJitter(algorithm, noJitter()) } // BackoffWithJitter creates a Strategy that waits before each attempt, with a // duration as defined by the given backoff.Algorithm and jitter.Transformation. func BackoffWithJitter(algorithm backoff.Algorithm, transformation jitter.Transformation) Strategy { return func(attempt uint) bool { if attempt > 0 { time.Sleep(transformation(algorithm(attempt))) } return true } } // noJitter creates a jitter.Transformation that simply returns the input. func noJitter() jitter.Transformation { return func(duration time.Duration) time.Duration { return duration } } golang-github-rican7-retry-0.3.1/strategy/strategy_test.go000066400000000000000000000114341422002774000236610ustar00rootroot00000000000000package strategy import ( "testing" "time" ) // timeMarginOfError represents the acceptable amount of time that may pass for // a time-based (sleep) unit before considering invalid. const timeMarginOfError = time.Millisecond func TestLimit(t *testing.T) { // Strategy attempts are 0-based. // Treat this functionally as n+1. const attemptLimit = 3 strategy := Limit(attemptLimit) if !strategy(0) { t.Error("strategy expected to return true") } if !strategy(1) { t.Error("strategy expected to return true") } if !strategy(2) { t.Error("strategy expected to return true") } if strategy(3) { t.Error("strategy expected to return false") } } func TestDelay(t *testing.T) { const delayDuration = 10 * timeMarginOfError strategy := Delay(delayDuration) if now := time.Now(); !strategy(0) || delayDuration > time.Since(now) { t.Errorf( "strategy expected to return true in %s", delayDuration, ) } if now := time.Now(); !strategy(5) || (delayDuration/10) < time.Since(now) { t.Error("strategy expected to return true in ~0 time") } } func TestWait(t *testing.T) { strategy := Wait() if now := time.Now(); !strategy(0) || timeMarginOfError < time.Since(now) { t.Error("strategy expected to return true in ~0 time") } if now := time.Now(); !strategy(999) || timeMarginOfError < time.Since(now) { t.Error("strategy expected to return true in ~0 time") } } func TestWaitWithDuration(t *testing.T) { const waitDuration = 10 * timeMarginOfError strategy := Wait(waitDuration) if now := time.Now(); !strategy(0) || timeMarginOfError < time.Since(now) { t.Error("strategy expected to return true in ~0 time") } if now := time.Now(); !strategy(1) || waitDuration > time.Since(now) { t.Errorf( "strategy expected to return true in %s", waitDuration, ) } } func TestWaitWithMultipleDurations(t *testing.T) { waitDurations := []time.Duration{ 10 * timeMarginOfError, 20 * timeMarginOfError, 30 * timeMarginOfError, 40 * timeMarginOfError, } strategy := Wait(waitDurations...) if now := time.Now(); !strategy(0) || timeMarginOfError < time.Since(now) { t.Error("strategy expected to return true in ~0 time") } if now := time.Now(); !strategy(1) || waitDurations[0] > time.Since(now) { t.Errorf( "strategy expected to return true in %s", waitDurations[0], ) } if now := time.Now(); !strategy(3) || waitDurations[2] > time.Since(now) { t.Errorf( "strategy expected to return true in %s", waitDurations[2], ) } if now := time.Now(); !strategy(999) || waitDurations[len(waitDurations)-1] > time.Since(now) { t.Errorf( "strategy expected to return true in %s", waitDurations[len(waitDurations)-1], ) } } func TestBackoff(t *testing.T) { const testCycles = 10 const backoffDuration = testCycles * timeMarginOfError const algorithmDurationBase = timeMarginOfError algorithm := func(attempt uint) time.Duration { return backoffDuration - (time.Duration(attempt) * algorithmDurationBase) } strategy := Backoff(algorithm) if now := time.Now(); !strategy(0) || timeMarginOfError < time.Since(now) { t.Error("strategy expected to return true in ~0 time") } for i := uint(1); i < testCycles; i++ { expectedResult := algorithm(i) if expectedResult < 0 { t.Errorf( "algorithm returned a negative duration %s", expectedResult, ) } if now := time.Now(); !strategy(i) || expectedResult > time.Since(now) { t.Errorf( "strategy expected to return true in %s", expectedResult, ) } } } func TestBackoffWithJitter(t *testing.T) { const testCycles = 10 const backoffDuration = 2 * testCycles * timeMarginOfError const algorithmDurationBase = timeMarginOfError algorithm := func(attempt uint) time.Duration { return backoffDuration - (time.Duration(attempt) * algorithmDurationBase) } transformation := func(duration time.Duration) time.Duration { return duration - (backoffDuration / 2) } strategy := BackoffWithJitter(algorithm, transformation) if now := time.Now(); !strategy(0) || timeMarginOfError < time.Since(now) { t.Error("strategy expected to return true in ~0 time") } for i := uint(1); i < testCycles; i++ { expectedResult := transformation(algorithm(i)) if expectedResult < 0 { t.Errorf( "transformation returned a negative duration %s", expectedResult, ) } if now := time.Now(); !strategy(i) || expectedResult > time.Since(now) { t.Errorf( "strategy expected to return true in %s", expectedResult, ) } } } func TestNoJitter(t *testing.T) { transformation := noJitter() for i := uint(0); i < 10; i++ { duration := time.Duration(i) * timeMarginOfError result := transformation(duration) expected := duration if result != expected { t.Errorf("transformation expected to return a %s duration, but received %s instead", expected, result) } } } golang-github-rican7-retry-0.3.1/tools/000077500000000000000000000000001422002774000177245ustar00rootroot00000000000000golang-github-rican7-retry-0.3.1/tools/go.mod000066400000000000000000000002471422002774000210350ustar00rootroot00000000000000module github.com/Rican7/retry/tools go 1.11 require ( golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 golang.org/x/tools v0.1.5 honnef.co/go/tools v0.2.0 ) golang-github-rican7-retry-0.3.1/tools/go.sum000066400000000000000000000102271422002774000210610ustar00rootroot00000000000000github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= honnef.co/go/tools v0.2.0 h1:ws8AfbgTX3oIczLPNPCu5166oBg9ST2vNs0rcht+mDE= honnef.co/go/tools v0.2.0/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= golang-github-rican7-retry-0.3.1/tools/tools.go000066400000000000000000000007701422002774000214170ustar00rootroot00000000000000// Package tools provides tools for development. // // It follows the pattern set-forth in the wiki: // - https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module // - https://github.com/go-modules-by-example/index/tree/4ea90b07f9/010_tools // // Copyright © 2021 Trevor N. Suarez (Rican7) // // +build tools package tools import ( // Tools for development _ "golang.org/x/lint/golint" _ "golang.org/x/tools/cmd/goimports" _ "honnef.co/go/tools/cmd/staticcheck" )