pax_global_header00006660000000000000000000000064136205426520014517gustar00rootroot0000000000000052 comment=54ea67f38a0f70f1a48365b234c3618233c48e7e golang-github-rican7-retry-0.1.0/000077500000000000000000000000001362054265200165705ustar00rootroot00000000000000golang-github-rican7-retry-0.1.0/.travis.yml000066400000000000000000000005631362054265200207050ustar00rootroot00000000000000language: go go: - 1.6 - tip sudo: false install: # Get all imported packages - make install-deps install-deps-dev # Basic build errors - make build script: # Lint - make format-lint - make import-lint - make copyright-lint # Run tests - make test matrix: allow_failures: - go: tip fast_finish: true golang-github-rican7-retry-0.1.0/LICENSE000066400000000000000000000020551362054265200175770ustar00rootroot00000000000000Copyright (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.1.0/Makefile000066400000000000000000000041201362054265200202250ustar00rootroot00000000000000# Define some VCS context PARENT_BRANCH ?= master # Set a default `min_confidence` value for `golint` GOLINT_MIN_CONFIDENCE ?= 0.3 # Set flags for `gofmt` GOFMT_FLAGS ?= -s all: install-deps build install clean: go clean -i -x ./... build: go build -v ./... install: go install ./... install-deps: go get -d -t ./... install-deps-dev: install-deps go get github.com/golang/lint/golint go get golang.org/x/tools/cmd/goimports update-deps: go get -d -t -u ./... update-deps-dev: update-deps go get -u github.com/golang/lint/golint go get -u golang.org/x/tools/cmd/goimports test: go test -v ./... test-with-coverage: go test -cover ./... test-with-coverage-formatted: go test -cover ./... | column -t | sort -r format-lint: errors=$$(gofmt -l ${GOFMT_FLAGS} .); if [ "$${errors}" != "" ]; then echo "$${errors}"; exit 1; fi import-lint: errors=$$(goimports -l .); if [ "$${errors}" != "" ]; then echo "$${errors}"; exit 1; fi style-lint: errors=$$(golint -min_confidence=${GOLINT_MIN_CONFIDENCE} ./...); if [ "$${errors}" != "" ]; then echo "$${errors}"; exit 1; fi copyright-lint: @old_dates=$$(git diff --diff-filter=ACMRTUXB --name-only "${PARENT_BRANCH}" | xargs grep -E '[Cc]opyright(\s+)[©Cc]?(\s+)[0-9]{4}' | grep -E -v "[Cc]opyright(\s+)[©Cc]?(\s+)$$(date '+%Y')"); if [ "$${old_dates}" != "" ]; then printf "The following files contain outdated copyrights:\n$${old_dates}\n\nThis can be fixed with 'make copyright-fix'\n"; exit 1; fi lint: install-deps-dev format-lint import-lint style-lint copyright-lint format-fix: gofmt -w ${GOFMT_FLAGS} . import-fix: goimports -w . copyright-fix: @git diff --diff-filter=ACMRTUXB --name-only "${PARENT_BRANCH}" | xargs -I '_FILENAME' -- sh -c 'sed -i.bak "s/\([Cc]opyright\([[:space:]][©Cc]\{0,1\}[[:space:]]*\)\)[0-9]\{4\}/\1"$$(date '+%Y')"/g" _FILENAME && rm _FILENAME.bak' vet: go vet ./... .PHONY: all clean build install install-deps install-deps-dev update-deps update-deps-dev test test-with-coverage test-with-coverage-formatted format-lint import-lint style-lint copyright-lint lint format-fix import-fix copyright-fix vet golang-github-rican7-retry-0.1.0/README.md000066400000000000000000000041451362054265200200530ustar00rootroot00000000000000# retry [![Build Status](https://travis-ci.org/Rican7/retry.svg?branch=master)](https://travis-ci.org/Rican7/retry) [![GoDoc](https://godoc.org/github.com/Rican7/retry?status.png)](https://godoc.org/github.com/Rican7/retry) [![Go Report Card](https://goreportcard.com/badge/Rican7/retry)](http://goreportcard.com/report/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. Vendor (commit or lock) this dependency if you plan on using it. ## Install `go get github.com/Rican7/retry` ## Examples ### Basic ```go 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(func(attempt uint) error { var err error logFile, err = os.Open(logFilePath) return err }) if nil != err { log.Fatalf("Unable to open file %q with error %q", logFilePath, err) } ``` ### 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 nil == err && nil != response && response.StatusCode > 200 { err = fmt.Errorf("failed to fetch (attempt #%d) with status code: %d", attempt, response.StatusCode) } return err } err := Retry( action, strategy.Limit(5), strategy.Backoff(backoff.Fibonacci(10*time.Millisecond)), ) if nil != err { 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( action, strategy.Limit(5), strategy.BackoffWithJitter( backoff.BinaryExponential(10*time.Millisecond), jitter.Deviation(random, 0.5), ), ) ``` golang-github-rican7-retry-0.1.0/backoff/000077500000000000000000000000001362054265200201635ustar00rootroot00000000000000golang-github-rican7-retry-0.1.0/backoff/backoff.go000066400000000000000000000042261362054265200221110ustar00rootroot00000000000000// 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 0 == n { return 0 } else if 1 == n { return 1 } else { return fibonacciNumber(n-1) + fibonacciNumber(n-2) } } golang-github-rican7-retry-0.1.0/backoff/backoff_test.go000066400000000000000000000074511362054265200231530ustar00rootroot00000000000000package 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 + (increment * time.Duration(i)) 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 := duration * time.Duration(i) 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 := duration * time.Duration(math.Pow(base, float64(i))) 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 := duration * time.Duration(math.Pow(2, float64(i))) 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 := duration * time.Duration(fibonacciNumber(i)) 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.1.0/jitter/000077500000000000000000000000001362054265200200715ustar00rootroot00000000000000golang-github-rican7-retry-0.1.0/jitter/jitter.go000066400000000000000000000062051362054265200217240ustar00rootroot00000000000000// 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 nil != random { return random } seed := time.Now().UnixNano() return rand.New(rand.NewSource(seed)) } golang-github-rican7-retry-0.1.0/jitter/jitter_test.go000066400000000000000000000047321362054265200227660ustar00rootroot00000000000000package 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); nil == result { t.Error("recieved unexpected nil result") } } golang-github-rican7-retry-0.1.0/retry.go000066400000000000000000000021411362054265200202620ustar00rootroot00000000000000// 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); (0 == attempt || nil != err) && shouldAttempt(attempt, strategies...); attempt++ { err = action(attempt) } 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.1.0/retry_test.go000066400000000000000000000071701362054265200213300ustar00rootroot00000000000000package retry import ( "errors" "fmt" "log" "math/rand" "net/http" "os" "testing" "time" "github.com/Rican7/retry/backoff" "github.com/Rican7/retry/jitter" "github.com/Rican7/retry/strategy" ) func TestRetry(t *testing.T) { action := func(attempt uint) error { return nil } err := Retry(action) if nil != err { t.Error("expected a nil error") } } 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 nil != err { 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") } } func Example() { 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(func(attempt uint) error { var err error logFile, err = os.Open(logFilePath) return err }) if nil != err { log.Fatalf("Unable to open file %q with error %q", logFilePath, err) } } 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 nil == err && nil != response && response.StatusCode > 200 { err = fmt.Errorf("failed to fetch (attempt #%d) with status code: %d", attempt, response.StatusCode) } return err } err := Retry( action, strategy.Limit(5), strategy.Backoff(backoff.Fibonacci(10*time.Millisecond)), ) if nil != err { log.Fatalf("Failed to fetch repository with error %q", err) } } func Example_withBackoffJitter() { action := func(attempt uint) error { return errors.New("something happened") } seed := time.Now().UnixNano() random := rand.New(rand.NewSource(seed)) Retry( action, strategy.Limit(5), strategy.BackoffWithJitter( backoff.BinaryExponential(10*time.Millisecond), jitter.Deviation(random, 0.5), ), ) } golang-github-rican7-retry-0.1.0/strategy/000077500000000000000000000000001362054265200204325ustar00rootroot00000000000000golang-github-rican7-retry-0.1.0/strategy/strategy.go000066400000000000000000000050311362054265200226220ustar00rootroot00000000000000// 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 on each successive retry // iteration, starting with a `0` value before the first attempt is actually // made. This allows for a pre-action 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 0 == attempt { 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 0 < attempt && 0 < len(durations) { 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 0 < attempt { 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.1.0/strategy/strategy_test.go000066400000000000000000000111051362054265200236600ustar00rootroot00000000000000package 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) { const attemptLimit = 3 strategy := Limit(attemptLimit) 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 true") } if strategy(4) { t.Error("strategy expected to return false") } } func TestDelay(t *testing.T) { const delayDuration = time.Duration(10 * timeMarginOfError) strategy := Delay(delayDuration) if now := time.Now(); !strategy(0) || delayDuration > time.Since(now) { t.Errorf( "strategy expected to return true in %s", time.Duration(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 = time.Duration(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", time.Duration(waitDuration), ) } } func TestWaitWithMultipleDurations(t *testing.T) { waitDurations := []time.Duration{ time.Duration(10 * timeMarginOfError), time.Duration(20 * timeMarginOfError), time.Duration(30 * timeMarginOfError), time.Duration(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", time.Duration(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 backoffDuration = time.Duration(10 * timeMarginOfError) const algorithmDurationBase = timeMarginOfError algorithm := func(attempt uint) time.Duration { return backoffDuration - (algorithmDurationBase * time.Duration(attempt)) } 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 < 10; i++ { expectedResult := algorithm(i) 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 backoffDuration = time.Duration(10 * timeMarginOfError) const algorithmDurationBase = timeMarginOfError algorithm := func(attempt uint) time.Duration { return backoffDuration - (algorithmDurationBase * time.Duration(attempt)) } transformation := func(duration time.Duration) time.Duration { return duration - time.Duration(10*timeMarginOfError) } 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 < 10; i++ { expectedResult := transformation(algorithm(i)) 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) } } }