pax_global_header00006660000000000000000000000064146473362230014524gustar00rootroot0000000000000052 comment=8aff8e4808d3f0bd001d02bbc2de190b8e0c4188 golang-github-lestrrat-go-backoff-2.0.8/000077500000000000000000000000001464733622300201145ustar00rootroot00000000000000golang-github-lestrrat-go-backoff-2.0.8/.github/000077500000000000000000000000001464733622300214545ustar00rootroot00000000000000golang-github-lestrrat-go-backoff-2.0.8/.github/workflows/000077500000000000000000000000001464733622300235115ustar00rootroot00000000000000golang-github-lestrrat-go-backoff-2.0.8/.github/workflows/ci.yml000066400000000000000000000017151464733622300246330ustar00rootroot00000000000000name: CI on: [push, pull_request] jobs: build: runs-on: ubuntu-latest strategy: matrix: go: [ '1.15', '1.14' ] name: Go ${{ matrix.go }} test steps: - name: Checkout repository uses: actions/checkout@v2 - name: Install Go stable version if: matrix.go != 'tip' uses: actions/setup-go@v2 with: go-version: ${{ matrix.go }} - name: Install Go tip if: matrix.go == 'tip' run: | git clone --depth=1 https://go.googlesource.com/go $HOME/gotip cd $HOME/gotip/src ./make.bash echo "::set-env name=GOROOT::$HOME/gotip" echo "::add-path::$HOME/gotip/bin" echo "::add-path::$(go env GOPATH)/bin" - name: Test run: go test -v -race ./... - name: Upload code coverage to codecov if: matrix.go == '1.15' uses: codecov/codecov-action@v1 with: file: ./coverage.out golang-github-lestrrat-go-backoff-2.0.8/.github/workflows/lint.yml000066400000000000000000000003451464733622300252040ustar00rootroot00000000000000name: lint on: [push, pull_request] jobs: golangci: name: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: golangci/golangci-lint-action@v2 with: version: v1.34.1 golang-github-lestrrat-go-backoff-2.0.8/.gitignore000066400000000000000000000004231464733622300221030ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ golang-github-lestrrat-go-backoff-2.0.8/.golangci.yml000066400000000000000000000001571464733622300225030ustar00rootroot00000000000000issues: exclude-rules: - path: /*_example_test.go linters: - errcheck - forbidigo golang-github-lestrrat-go-backoff-2.0.8/Changes000066400000000000000000000002571464733622300214130ustar00rootroot00000000000000v2.0.8 - 28 Feb 2021 * Fix possible goroutine leak (#30) v2.0.7 - 26 Jan 2021 * Cosmetic go.mod / go.sum changes v2.0.6 - 25 Jan 2021 * Add jitter to constant backoff golang-github-lestrrat-go-backoff-2.0.8/LICENSE000066400000000000000000000020511464733622300211170ustar00rootroot00000000000000MIT License Copyright (c) 2018 lestrrat 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-lestrrat-go-backoff-2.0.8/README.md000066400000000000000000000122071464733622300213750ustar00rootroot00000000000000# backoff ![](https://github.com/lestrrat-go/backoff/workflows/CI/badge.svg) [![Go Reference](https://pkg.go.dev/badge/github.com/lestrrat-go/backoff/v2.svg)](https://pkg.go.dev/github.com/lestrrat-go/backoff/v2) Idiomatic backoff for Go This library is an implementation of backoff algorithm for retrying operations in an idiomatic Go way. It respects `context.Context` natively, and the critical notifications are done through *channel operations*, allowing you to write code that is both more explicit and flexibile. For a longer discussion, [please read this article](https://medium.com/@lestrrat/yak-shaving-with-backoff-libraries-in-go-80240f0aa30c) # IMPORT ```go import "github.com/lestrrat-go/backoff/v2" ``` # SYNOPSIS ```go func ExampleExponential() { p := backoff.Exponential( backoff.WithMinInterval(time.Second), backoff.WithMaxInterval(time.Minute), backoff.WithJitterFactor(0.05), ) flakyFunc := func(a int) (int, error) { // silly function that only succeeds if the current call count is // divisible by either 3 or 5 but not both switch { case a%15 == 0: return 0, errors.New(`invalid`) case a%3 == 0 || a%5 == 0: return a, nil } return 0, errors.New(`invalid`) } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() retryFunc := func(v int) (int, error) { b := p.Start(ctx) for backoff.Continue(b) { x, err := flakyFunc(v) if err == nil { return x, nil } } return 0, errors.New(`failed to get value`) } retryFunc(15) } ``` # POLICIES Policy objects describe a backoff policy, and are factories to create backoff Controller objects. Controller objects does the actual coordination. Create a new controller for each invocation of a backoff enabled operation. This way the controller object is protected from concurrent access (if you have one) and can easily be discarded ## Null A null policy means there's no backoff. For example, if you were to support both using and not using a backoff in your code you can say ```go var p backoff.Policy if useBackoff { p = backoff.Exponential(...) } else { p = backoff.Null() } c := p.Start(ctx) for backoff.Continue(c) { if err := doSomething(); err != nil { continue } return } ``` Instead of ```go if useBackoff { p := backoff.Exponential(...) c := p.Start(ctx) for backoff.Continue(c) { if err := doSomething(); err != nil { continue } return } } else { if err := doSomething(); err != nil { continue } } ``` ## Constant A constant policy implements are backoff where the intervals are always the same ## Exponential This is the most "common" of the backoffs. Intervals between calls are spaced out such that as you keep retrying, the intervals keep increasing. # FAQ ## I'm getting "package github.com/lestrrat-go/backoff/v2: no Go files in /go/src/github.com/lestrrat-go/backoff/v2" You are using Go in GOPATH mode, which was the way before [Go Modules](https://blog.golang.org/using-go-modules) were introduced in Go 1.11 (Aug 2018). GOPATH has slowly been phased out, and in Go 1.14 onwards, Go Modules pretty much Just Work. Go 1.16 introduced more visible changes that forces users to be aware of the existance of go.mod files. The short answer when you you get the above error is: **Migrate to using Go Modules**. This is simple: All you need to do is to include a go.mod (and go.sum) file to your library or app. For example, if you have previously been doing this: ``` git clone git@github.com:myusername/myawesomeproject.git cd myawesomeproject go get ./... ``` First include go.mod and go.sum in your repository: ``` git clone git@github.com:myusername/myawesomeproject.git cd myawesomeproject go mod init go mod tidy git add go.mod go.sum git commit -m "Add go.mod and go.sum" -a git push ``` Then from subsequent calls: ``` git clone git@github.com:myusername/myawesomeproject.git cd myawesomeproject go build # or go test, or go run, or whatever. ``` This will tell go to respect tags, and will automatically pick up the latest version of github.com/lestrrat-go/backoff If you really can't do this, then the quick and dirty workaround is to just copy the files over to /v2 directory of this library ``` BACKOFF=github.com/lestrrat-go/backoff go get github.com/lestrrat-go/backoff if [[ if ! -d "$GOPATH/src/$BACKOFF/v2" ]]; then mkdir "$GOPATH/src/$BACKOFF/v2" # just to make sure it exists fi cp "$GOPATH/src/$BACKOFF/*.go" "$GOPATH/src/$BACKOFF/v2" git clone git@github.com:myusername/myawesomeproject.git cd myawesomeproject go get ./... ``` ## Why won't you just add the files in /v2? Because it's hard to maintain multiple sources of truth. Sorry, I don't get paid to do this. I will not hold anything against you if you decided to fork this to your repository, and move files to your own /v2 directory. Then, if you have a go.mod in your app, you can just do ``` go mod edit -replace github.com/lestrrat-go/backoff/v2=github.com/myusername/myawesomemfork/v2 ``` Oh, wait, then you already have go.mod, so this is a non-issue. ...Yeah, just migrate to using go modules, please? golang-github-lestrrat-go-backoff-2.0.8/backoff.go000066400000000000000000000012451464733622300220400ustar00rootroot00000000000000package backoff // Null creates a new NullPolicy object func Null() Policy { return NewNull() } // Constant creates a new ConstantPolicy object func Constant(options ...Option) Policy { return NewConstantPolicy(options...) } // Constant creates a new ExponentialPolicy object func Exponential(options ...ExponentialOption) Policy { return NewExponentialPolicy(options...) } // Continue is a convenience function to check when we can fire // the next invocation of the desired backoff code // // for backoff.Continue(c) { // ... your code ... // } func Continue(c Controller) bool { select { case <-c.Done(): return false case _, ok := <-c.Next(): return ok } } golang-github-lestrrat-go-backoff-2.0.8/backoff_example_test.go000066400000000000000000000031171464733622300246120ustar00rootroot00000000000000package backoff_test import ( "context" "errors" "time" backoff "github.com/lestrrat-go/backoff/v2" ) func ExampleConstant() { p := backoff.Constant(backoff.WithInterval(time.Second)) flakyFunc := func(a int) (int, error) { // silly function that only succeeds if the current call count is // divisible by either 3 or 5 but not both switch { case a%15 == 0: return 0, errors.New(`invalid`) case a%3 == 0 || a%5 == 0: return a, nil } return 0, errors.New(`invalid`) } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() retryFunc := func(v int) (int, error) { b := p.Start(ctx) for backoff.Continue(b) { x, err := flakyFunc(v) if err == nil { return x, nil } } return 0, errors.New(`failed to get value`) } retryFunc(15) } func ExampleExponential() { p := backoff.Exponential( backoff.WithMinInterval(time.Second), backoff.WithMaxInterval(time.Minute), backoff.WithJitterFactor(0.05), ) flakyFunc := func(a int) (int, error) { // silly function that only succeeds if the current call count is // divisible by either 3 or 5 but not both switch { case a%15 == 0: return 0, errors.New(`invalid`) case a%3 == 0 || a%5 == 0: return a, nil } return 0, errors.New(`invalid`) } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() retryFunc := func(v int) (int, error) { b := p.Start(ctx) for backoff.Continue(b) { x, err := flakyFunc(v) if err == nil { return x, nil } } return 0, errors.New(`failed to get value`) } retryFunc(15) } golang-github-lestrrat-go-backoff-2.0.8/backoff_test.go000066400000000000000000000124721464733622300231030ustar00rootroot00000000000000package backoff_test import ( "context" "fmt" "io/ioutil" "sync" "testing" "time" "github.com/lestrrat-go/backoff/v2" "github.com/stretchr/testify/assert" ) func TestNull(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() p := backoff.Null() c := p.Start(ctx) var retries int for backoff.Continue(c) { t.Logf("%s backoff.Continue", time.Now()) retries++ } if !assert.Equal(t, 1, retries, `should have done 1 retries`) { return } } func TestConstant(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() p := backoff.Constant( backoff.WithInterval(300*time.Millisecond), backoff.WithMaxRetries(4), ) c := p.Start(ctx) prev := time.Now() var retries int for backoff.Continue(c) { t.Logf("%s backoff.Continue", time.Now()) // make sure that we've executed this in more or less 300ms retries++ if retries > 1 { d := time.Since(prev) if !assert.True(t, 350*time.Millisecond >= d && d >= 250*time.Millisecond, `timing is about 300ms (%s)`, d) { return } } prev = time.Now() } // initial + 4 retries = 5 if !assert.Equal(t, 5, retries, `should have retried 5 times`) { return } } func isInErrorRange(expected, observed, margin time.Duration) bool { return expected+margin > observed && observed > expected-margin } func TestExponential(t *testing.T) { t.Run("Interval generator", func(t *testing.T) { expected := []float64{ 0.5, 0.75, 1.125, 1.6875, 2.53125, 3.796875, } ig := backoff.NewExponentialInterval() for i := 0; i < len(expected); i++ { if !assert.Equal(t, time.Duration(float64(time.Second)*expected[i]), ig.Next(), `interval for iteration %d`, i) { return } } }) t.Run("Jitter", func(t *testing.T) { ig := backoff.NewExponentialInterval( backoff.WithMaxInterval(time.Second), backoff.WithJitterFactor(0.02), ) testcases := []struct { Base time.Duration }{ {Base: 500 * time.Millisecond}, {Base: 750 * time.Millisecond}, {Base: time.Second}, } for i := 0; i < 10; i++ { dur := ig.Next() var base time.Duration if i > 2 { base = testcases[2].Base } else { base = testcases[i].Base } min := int64(float64(base) * 0.98) max := int64(float64(base) * 1.05) // should be 1.02, but give it a bit of leeway t.Logf("max = %s, min = %s", time.Duration(max), time.Duration(min)) if !assert.GreaterOrEqual(t, int64(dur), min, "value should be greater than minimum") { return } if !assert.GreaterOrEqual(t, max, int64(dur), "value should be less than maximum") { return } } }) t.Run("Back off, no jitter", func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() // These values are truncated to milliseconds, to make comparisons easier expected := []float64{ 0, 0.5, 0.7, 1.1, 1.6, 2.5, 3.7, } p := backoff.Exponential() count := 0 prev := time.Now() b := p.Start(ctx) for backoff.Continue(b) { now := time.Now() d := now.Sub(prev) d = d - d%(100*time.Millisecond) // Allow a flux of 100ms expectedDuration := time.Duration(expected[count] * float64(time.Second)) if !assert.True(t, isInErrorRange(expectedDuration, d, 100*time.Millisecond), `observed duration (%s) should be whthin error range (expected = %s, range = %s)`, d, expectedDuration, 100*time.Millisecond) { return } count++ if count == len(expected)-1 { break } prev = now } }) } func TestConcurrent(t *testing.T) { if testing.Short() { t.SkipNow() } t.Parallel() // Does not test anything useful, just puts it under stress testcases := []struct { Policy backoff.Policy Name string }{ {Name: "Null", Policy: backoff.Null()}, {Name: "Exponential", Policy: backoff.Exponential(backoff.WithMultiplier(0.01), backoff.WithMinInterval(time.Millisecond))}, } const max = 50 for _, tc := range testcases { tc := tc t.Run(tc.Name, func(t *testing.T) { t.Parallel() var wg sync.WaitGroup wg.Add(max) for i := 0; i < max; i++ { go func(wg *sync.WaitGroup, b backoff.Policy) { defer wg.Done() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() c := b.Start(ctx) for backoff.Continue(c) { fmt.Fprintf(ioutil.Discard, `Writing to the ether...`) } }(&wg, tc.Policy) } wg.Wait() }) } } func TestConstantWithJitter(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() p := backoff.Constant( backoff.WithInterval(300*time.Millisecond), backoff.WithJitterFactor(0.50), backoff.WithMaxRetries(999), ) c := p.Start(ctx) prev := time.Now() var retries int for backoff.Continue(c) { t.Logf("%s backoff.Continue", time.Now()) // make sure that we've executed this in more or less 300ms ± 50% retries++ if retries > 1 { d := time.Since(prev) // if the duration becomes out of the range values by jitter, it breaks loop if (150*time.Millisecond <= d && d < 250*time.Millisecond) || (350*time.Millisecond < d && d <= 450*time.Millisecond) { break } } prev = time.Now() } // initial + 999 retries = 1000 if !assert.NotEqual(t, 1000, retries, `should not have retried 1000 times; if the # of retries reaches 1000, probably jitter doesn't work'`) { return } } golang-github-lestrrat-go-backoff-2.0.8/bench/000077500000000000000000000000001464733622300211735ustar00rootroot00000000000000golang-github-lestrrat-go-backoff-2.0.8/bench/backoff_test.go000066400000000000000000000017341464733622300241610ustar00rootroot00000000000000package backoff_test import ( "context" "errors" "testing" cenkalti "github.com/cenkalti/backoff" lestrrat "github.com/lestrrat-go/backoff/v2" ) func Benchmark(b *testing.B) { // This is a dummy function fn := func() int { var v int for i := 1; i <= 10; i++ { v += i } return v } b.Run("cenkalti", func(b *testing.B) { for i := 0; i < b.N; i++ { b.StopTimer() backoff := cenkalti.NewExponentialBackOff() b.StartTimer() cenkalti.Retry(func() error { _ = fn() return errors.New(`dummy`) }, cenkalti.WithMaxRetries(backoff, 5)) } }) b.Run("lestrrat", func(b *testing.B) { b.StopTimer() policy := lestrrat.Exponential(lestrrat.WithMaxRetries(5), lestrrat.WithJitterFactor(1.2)) for i := 0; i < b.N; i++ { b.StartTimer() backoff := policy.Start(context.Background()) MAIN: for { fn() select { case <-backoff.Done(): break MAIN case <-backoff.Next(): _ = fn() } } b.StopTimer() } }) } golang-github-lestrrat-go-backoff-2.0.8/bench/go.mod000066400000000000000000000003141464733622300222770ustar00rootroot00000000000000module github.com/lestrrat-go/backoff/bench go 1.16 require ( github.com/cenkalti/backoff v2.2.1+incompatible github.com/lestrrat-go/backoff v2.0.7 ) replace github.com/lestrrat-go/backoff/v2 => ../ golang-github-lestrrat-go-backoff-2.0.8/constant.go000066400000000000000000000025421464733622300222770ustar00rootroot00000000000000package backoff import ( "context" "time" ) type ConstantInterval struct { interval time.Duration jitter jitter } func NewConstantInterval(options ...ConstantOption) *ConstantInterval { jitterFactor := 0.0 interval := time.Minute var rng Random for _, option := range options { switch option.Ident() { case identInterval{}: interval = option.Value().(time.Duration) case identJitterFactor{}: jitterFactor = option.Value().(float64) case identRNG{}: rng = option.Value().(Random) } } return &ConstantInterval{ interval: interval, jitter: newJitter(jitterFactor, rng), } } func (g *ConstantInterval) Next() time.Duration { return time.Duration(g.jitter.apply(float64(g.interval))) } type ConstantPolicy struct { cOptions []ControllerOption igOptions []ConstantOption } func NewConstantPolicy(options ...Option) *ConstantPolicy { var cOptions []ControllerOption var igOptions []ConstantOption for _, option := range options { switch opt := option.(type) { case ControllerOption: cOptions = append(cOptions, opt) default: igOptions = append(igOptions, opt.(ConstantOption)) } } return &ConstantPolicy{ cOptions: cOptions, igOptions: igOptions, } } func (p *ConstantPolicy) Start(ctx context.Context) Controller { ig := NewConstantInterval(p.igOptions...) return newController(ctx, ig, p.cOptions...) } golang-github-lestrrat-go-backoff-2.0.8/controller.go000066400000000000000000000034131464733622300226270ustar00rootroot00000000000000package backoff import ( "context" "sync" "time" ) type controller struct { ctx context.Context cancel func() ig IntervalGenerator maxRetries int mu *sync.RWMutex next chan struct{} // user-facing channel resetTimer chan time.Duration retries int timer *time.Timer } func newController(ctx context.Context, ig IntervalGenerator, options ...ControllerOption) *controller { cctx, cancel := context.WithCancel(ctx) // DO NOT fire this cancel here maxRetries := 10 for _, option := range options { switch option.Ident() { case identMaxRetries{}: maxRetries = option.Value().(int) } } c := &controller{ cancel: cancel, ctx: cctx, ig: ig, maxRetries: maxRetries, mu: &sync.RWMutex{}, next: make(chan struct{}, 1), resetTimer: make(chan time.Duration, 1), timer: time.NewTimer(ig.Next()), } // enqueue a single fake event so the user gets to retry once c.next <- struct{}{} go c.loop() return c } func (c *controller) loop() { for { select { case <-c.ctx.Done(): return case d := <-c.resetTimer: if !c.timer.Stop() { select { case <-c.timer.C: default: } } c.timer.Reset(d) case <-c.timer.C: select { case <-c.ctx.Done(): return case c.next <- struct{}{}: } if c.maxRetries > 0 { c.retries++ } if !c.check() { c.cancel() return } c.resetTimer <- c.ig.Next() } } } func (c *controller) check() bool { if c.maxRetries > 0 && c.retries >= c.maxRetries { return false } return true } func (c *controller) Done() <-chan struct{} { c.mu.RLock() defer c.mu.RUnlock() return c.ctx.Done() } func (c *controller) Next() <-chan struct{} { c.mu.RLock() defer c.mu.RUnlock() return c.next } golang-github-lestrrat-go-backoff-2.0.8/controller_test.go000066400000000000000000000013471464733622300236720ustar00rootroot00000000000000package backoff_test import ( "context" "runtime" "sync" "testing" "time" "github.com/lestrrat-go/backoff/v2" ) func TestLeak(t *testing.T) { beforeGoroutine := runtime.NumGoroutine() var wg sync.WaitGroup tasks := 100 wg.Add(tasks) for range make([]struct{}, tasks) { go func() { defer wg.Done() exp := backoff.Exponential() ctx, cancel := context.WithCancel(context.Background()) defer cancel() c := exp.Start(ctx) for backoff.Continue(c) { time.Sleep(1300 * time.Millisecond) cancel() } }() } wg.Wait() afterGoroutine := runtime.NumGoroutine() if afterGoroutine > beforeGoroutine+10 { t.Errorf("goroutines seem to be leaked. before: %d, after: %d", beforeGoroutine, afterGoroutine) } } golang-github-lestrrat-go-backoff-2.0.8/doc.go000066400000000000000000000003631464733622300212120ustar00rootroot00000000000000// Package backoff implments backoff algorithms for retrying operations. // // Users first create an appropriate `Policy` object, and when the operation // that needs retrying is about to start, they kick the actual backoff // package backoffgolang-github-lestrrat-go-backoff-2.0.8/exponential.go000066400000000000000000000044321464733622300227740ustar00rootroot00000000000000package backoff import ( "context" "time" ) type ExponentialInterval struct { current float64 maxInterval float64 minInterval float64 multiplier float64 jitter jitter } const ( defaultMaxInterval = float64(time.Minute) defaultMinInterval = float64(500 * time.Millisecond) defaultMultiplier = 1.5 ) func NewExponentialInterval(options ...ExponentialOption) *ExponentialInterval { jitterFactor := 0.0 maxInterval := defaultMaxInterval minInterval := defaultMinInterval multiplier := defaultMultiplier var rng Random for _, option := range options { switch option.Ident() { case identJitterFactor{}: jitterFactor = option.Value().(float64) case identMaxInterval{}: maxInterval = float64(option.Value().(time.Duration)) case identMinInterval{}: minInterval = float64(option.Value().(time.Duration)) case identMultiplier{}: multiplier = option.Value().(float64) case identRNG{}: rng = option.Value().(Random) } } if minInterval > maxInterval { minInterval = maxInterval } if multiplier <= 1 { multiplier = defaultMultiplier } return &ExponentialInterval{ maxInterval: maxInterval, minInterval: minInterval, multiplier: multiplier, jitter: newJitter(jitterFactor, rng), } } func (g *ExponentialInterval) Next() time.Duration { var next float64 if g.current == 0 { next = g.minInterval } else { next = g.current * g.multiplier } if next > g.maxInterval { next = g.maxInterval } if next < g.minInterval { next = g.minInterval } // Apply jitter *AFTER* we calculate the base interval next = g.jitter.apply(next) g.current = next return time.Duration(next) } type ExponentialPolicy struct { cOptions []ControllerOption igOptions []ExponentialOption } func NewExponentialPolicy(options ...ExponentialOption) *ExponentialPolicy { var cOptions []ControllerOption var igOptions []ExponentialOption for _, option := range options { switch opt := option.(type) { case ControllerOption: cOptions = append(cOptions, opt) default: igOptions = append(igOptions, opt) } } return &ExponentialPolicy{ cOptions: cOptions, igOptions: igOptions, } } func (p *ExponentialPolicy) Start(ctx context.Context) Controller { ig := NewExponentialInterval(p.igOptions...) return newController(ctx, ig, p.cOptions...) } golang-github-lestrrat-go-backoff-2.0.8/exponential_test.go000066400000000000000000000024241464733622300240320ustar00rootroot00000000000000package backoff import ( "math/rand" "testing" "time" "github.com/stretchr/testify/assert" ) func TestNewExponentialIntervalWithDefaultOptions(t *testing.T) { p := NewExponentialInterval() assert.Equal(t, defaultMaxInterval, p.maxInterval) assert.Equal(t, defaultMinInterval, p.minInterval) assert.Equal(t, defaultMultiplier, p.multiplier) assert.Equal(t, &nopJitter{}, p.jitter) } func TestNewExponentialIntervalWithCustomOptions(t *testing.T) { jitter := 0.99 maxInterval := 24 * time.Hour minInterval := time.Nanosecond multiplier := float64(99999) rng := rand.New(rand.NewSource(time.Now().UnixNano())) p := NewExponentialInterval( WithJitterFactor(jitter), WithMaxInterval(maxInterval), WithMinInterval(minInterval), WithMultiplier(multiplier), WithRNG(rng), ) assert.Equal(t, maxInterval, time.Duration(p.maxInterval)) assert.Equal(t, minInterval, time.Duration(p.minInterval)) assert.Equal(t, multiplier, p.multiplier) assert.Equal(t, newRandomJitter(jitter, rng), p.jitter) } func TestNewExponentialIntervalWithOnlyJitterOptions(t *testing.T) { jitter := 0.99 p := NewExponentialInterval( WithJitterFactor(jitter), ) generatedRandomJitter := p.jitter.(*randomJitter) assert.Equal(t, newRandomJitter(jitter, generatedRandomJitter.rng), p.jitter) } golang-github-lestrrat-go-backoff-2.0.8/go.mod000066400000000000000000000002111464733622300212140ustar00rootroot00000000000000module github.com/lestrrat-go/backoff/v2 go 1.16 require ( github.com/lestrrat-go/option v1.0.0 github.com/stretchr/testify v1.6.1 ) golang-github-lestrrat-go-backoff-2.0.8/go.sum000066400000000000000000000022611464733622300212500ustar00rootroot00000000000000github.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/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 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/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= golang-github-lestrrat-go-backoff-2.0.8/interface.go000066400000000000000000000010261464733622300224020ustar00rootroot00000000000000package backoff import ( "context" "time" "github.com/lestrrat-go/option" ) type Option = option.Interface type Controller interface { Done() <-chan struct{} Next() <-chan struct{} } type IntervalGenerator interface { Next() time.Duration } // Policy is an interface for the backoff policies that this package // implements. Users must create a controller object from this // policy to actually do anything with it type Policy interface { Start(context.Context) Controller } type Random interface { Float64() float64 } golang-github-lestrrat-go-backoff-2.0.8/internal_test.go000066400000000000000000000012731464733622300233210ustar00rootroot00000000000000package backoff import ( "math/rand" "testing" "time" "github.com/stretchr/testify/assert" ) func TestOptionPassing(t *testing.T) { cOptions := []ControllerOption{ WithMaxRetries(9999999999999), } igOptions := []ExponentialOption{ WithJitterFactor(0.99), WithMaxInterval(24 * time.Hour), WithMinInterval(time.Nanosecond), WithMultiplier(99999), WithRNG(rand.New(rand.NewSource(time.Now().UnixNano()))), } merged := igOptions for _, option := range cOptions { merged = append(merged, option.(ExponentialOption)) } p := NewExponentialPolicy(merged...) if !assert.Equal(t, cOptions, p.cOptions) { return } if !assert.Equal(t, igOptions, p.igOptions) { return } } golang-github-lestrrat-go-backoff-2.0.8/jitter.go000066400000000000000000000027211464733622300217460ustar00rootroot00000000000000package backoff import ( "math/rand" "time" ) type jitter interface { apply(interval float64) float64 } func newJitter(jitterFactor float64, rng Random) jitter { if jitterFactor <= 0 || jitterFactor >= 1 { return newNopJitter() } return newRandomJitter(jitterFactor, rng) } type nopJitter struct{} func newNopJitter() *nopJitter { return &nopJitter{} } func (j *nopJitter) apply(interval float64) float64 { return interval } type randomJitter struct { jitterFactor float64 rng Random } func newRandomJitter(jitterFactor float64, rng Random) *randomJitter { if rng == nil { // if we have a jitter factor, and no RNG is provided, create one. // This is definitely not "secure", but well, if you care enough, // you would provide one rng = rand.New(rand.NewSource(time.Now().UnixNano())) } return &randomJitter{ jitterFactor: jitterFactor, rng: rng, } } func (j *randomJitter) apply(interval float64) float64 { jitterDelta := interval * j.jitterFactor jitterMin := interval - jitterDelta jitterMax := interval + jitterDelta // Get a random value from the range [minInterval, maxInterval]. // The formula used below has a +1 because if the minInterval is 1 and the maxInterval is 3 then // we want a 33% chance for selecting either 1, 2 or 3. // // see also: https://github.com/cenkalti/backoff/blob/c2975ffa541a1caeca5f76c396cb8c3e7b3bb5f8/exponential.go#L154-L157 return jitterMin + j.rng.Float64()*(jitterMax-jitterMin+1) } golang-github-lestrrat-go-backoff-2.0.8/null.go000066400000000000000000000017071464733622300214220ustar00rootroot00000000000000package backoff import ( "context" "sync" ) // NullPolicy does not do any backoff. It allows the caller // to execute the desired code once, and no more type NullPolicy struct{} func NewNull() *NullPolicy { return &NullPolicy{} } func (p *NullPolicy) Start(ctx context.Context) Controller { return newNullController(ctx) } type nullController struct { mu *sync.RWMutex ctx context.Context next chan struct{} } func newNullController(ctx context.Context) *nullController { cctx, cancel := context.WithCancel(ctx) c := &nullController{ mu: &sync.RWMutex{}, ctx: cctx, next: make(chan struct{}), // NO BUFFER } go func(ch chan struct{}, cancel func()) { ch <- struct{}{} close(ch) cancel() }(c.next, cancel) return c } func (c *nullController) Done() <-chan struct{} { c.mu.RLock() defer c.mu.RUnlock() return c.ctx.Done() } func (c *nullController) Next() <-chan struct{} { c.mu.RLock() defer c.mu.RUnlock() return c.next } golang-github-lestrrat-go-backoff-2.0.8/options.go000066400000000000000000000074551464733622300221510ustar00rootroot00000000000000package backoff import ( "time" "github.com/lestrrat-go/option" ) type identInterval struct{} type identJitterFactor struct{} type identMaxInterval struct{} type identMaxRetries struct{} type identMinInterval struct{} type identMultiplier struct{} type identRNG struct{} // ControllerOption is an option that may be passed to Policy objects, // but are ultimately passed down to the Controller objects. // (Normally you do not have to care about the distinction) type ControllerOption interface { ConstantOption ExponentialOption CommonOption controllerOption() } type controllerOption struct { Option } func (*controllerOption) exponentialOption() {} func (*controllerOption) controllerOption() {} func (*controllerOption) constantOption() {} // ConstantOption is an option that is used by the Constant policy. type ConstantOption interface { Option constantOption() } type constantOption struct { Option } func (*constantOption) constantOption() {} // ExponentialOption is an option that is used by the Exponential policy. type ExponentialOption interface { Option exponentialOption() } type exponentialOption struct { Option } func (*exponentialOption) exponentialOption() {} // CommonOption is an option that can be passed to any of the backoff policies. type CommonOption interface { ExponentialOption ConstantOption } type commonOption struct { Option } func (*commonOption) constantOption() {} func (*commonOption) exponentialOption() {} // WithMaxRetries specifies the maximum number of attempts that can be made // by the backoff policies. By default each policy tries up to 10 times. // // If you would like to retry forever, specify "0" and pass to the constructor // of each policy. // // This option can be passed to all policy constructors except for NullPolicy func WithMaxRetries(v int) ControllerOption { return &controllerOption{option.New(identMaxRetries{}, v)} } // WithInterval specifies the constant interval used in ConstantPolicy and // ConstantInterval. // The default value is 1 minute. func WithInterval(v time.Duration) ConstantOption { return &constantOption{option.New(identInterval{}, v)} } // WithMaxInterval specifies the maximum duration used in exponential backoff // The default value is 1 minute. func WithMaxInterval(v time.Duration) ExponentialOption { return &exponentialOption{option.New(identMaxInterval{}, v)} } // WithMinInterval specifies the minimum duration used in exponential backoff. // The default value is 500ms. func WithMinInterval(v time.Duration) ExponentialOption { return &exponentialOption{option.New(identMinInterval{}, v)} } // WithMultiplier specifies the factor in which the backoff intervals are // increased. By default this value is set to 1.5, which means that for // every iteration a 50% increase in the interval for every iteration // (up to the value specified by WithMaxInterval). this value must be greater // than 1.0. If the value is less than equal to 1.0, the default value // of 1.5 is used. func WithMultiplier(v float64) ExponentialOption { return &exponentialOption{option.New(identMultiplier{}, v)} } // WithJitterFactor enables some randomness (jittering) in the computation of // the backoff intervals. This value must be between 0.0 < v < 1.0. If a // value outside of this range is specified, the value will be silently // ignored and jittering is disabled. // // This option can be passed to ExponentialPolicy or ConstantPolicy constructor func WithJitterFactor(v float64) CommonOption { return &commonOption{option.New(identJitterFactor{}, v)} } // WithRNG specifies the random number generator used for jittering. // If not provided one will be created, but if you want a truly random // jittering, make sure to provide one that you explicitly initialized func WithRNG(v Random) CommonOption { return &commonOption{option.New(identRNG{}, v)} }