├── go.mod ├── .github └── workflows │ └── build.yml ├── LICENSE ├── README.md ├── backoff.go └── backoff_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jpillora/backoff 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Set up Go 15 | uses: actions/setup-go@v3 16 | with: 17 | go-version: '>=1.17.0' 18 | - name: Test 19 | run: | 20 | go mod download 21 | go test -v ./... -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Jaime Pillora 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Backoff 2 | 3 | A simple exponential backoff counter in Go (Golang) 4 | 5 | [![GoDoc](https://godoc.org/github.com/jpillora/backoff?status.svg)](https://godoc.org/github.com/jpillora/backoff) 6 | [![Build Status](https://github.com/jpillora/backoff/actions/workflows/build.yml/badge.svg)](https://github.com/jpillora/backoff/actions/workflows/build.yml) 7 | 8 | ### Install 9 | 10 | ``` 11 | $ go get -v github.com/jpillora/backoff 12 | ``` 13 | 14 | ### Usage 15 | 16 | Backoff is a `time.Duration` counter. It starts at `Min`. After every call to `Duration()` it is multiplied by `Factor`. It is capped at `Max`. It returns to `Min` on every call to `Reset()`. `Jitter` adds randomness ([see below](#example-using-jitter)). Used in conjunction with the `time` package. 17 | 18 | --- 19 | 20 | #### Simple example 21 | 22 | ``` go 23 | 24 | b := &backoff.Backoff{ 25 | //These are the defaults 26 | Min: 100 * time.Millisecond, 27 | Max: 10 * time.Second, 28 | Factor: 2, 29 | Jitter: false, 30 | } 31 | 32 | fmt.Printf("%s\n", b.Duration()) 33 | fmt.Printf("%s\n", b.Duration()) 34 | fmt.Printf("%s\n", b.Duration()) 35 | 36 | fmt.Printf("Reset!\n") 37 | b.Reset() 38 | 39 | fmt.Printf("%s\n", b.Duration()) 40 | ``` 41 | 42 | ``` 43 | 100ms 44 | 200ms 45 | 400ms 46 | Reset! 47 | 100ms 48 | ``` 49 | 50 | --- 51 | 52 | #### Example using `net` package 53 | 54 | ``` go 55 | b := &backoff.Backoff{ 56 | Max: 5 * time.Minute, 57 | } 58 | 59 | for { 60 | conn, err := net.Dial("tcp", "example.com:5309") 61 | if err != nil { 62 | d := b.Duration() 63 | fmt.Printf("%s, reconnecting in %s", err, d) 64 | time.Sleep(d) 65 | continue 66 | } 67 | //connected 68 | b.Reset() 69 | conn.Write([]byte("hello world!")) 70 | // ... Read ... Write ... etc 71 | conn.Close() 72 | //disconnected 73 | } 74 | 75 | ``` 76 | 77 | --- 78 | 79 | #### Example using `Jitter` 80 | 81 | Enabling `Jitter` adds some randomization to the backoff durations. [See Amazon's writeup of performance gains using jitter](http://www.awsarchitectureblog.com/2015/03/backoff.html). Seeding is not necessary but doing so gives repeatable results. 82 | 83 | ```go 84 | import "math/rand" 85 | 86 | b := &backoff.Backoff{ 87 | Jitter: true, 88 | } 89 | 90 | rand.Seed(42) 91 | 92 | fmt.Printf("%s\n", b.Duration()) 93 | fmt.Printf("%s\n", b.Duration()) 94 | fmt.Printf("%s\n", b.Duration()) 95 | 96 | fmt.Printf("Reset!\n") 97 | b.Reset() 98 | 99 | fmt.Printf("%s\n", b.Duration()) 100 | fmt.Printf("%s\n", b.Duration()) 101 | fmt.Printf("%s\n", b.Duration()) 102 | ``` 103 | 104 | ``` 105 | 100ms 106 | 106.600049ms 107 | 281.228155ms 108 | Reset! 109 | 100ms 110 | 104.381845ms 111 | 214.957989ms 112 | ``` 113 | 114 | #### Documentation 115 | 116 | https://godoc.org/github.com/jpillora/backoff 117 | 118 | #### Credits 119 | 120 | Forked from [some JavaScript](https://github.com/segmentio/backo) written by [@tj](https://github.com/tj) 121 | -------------------------------------------------------------------------------- /backoff.go: -------------------------------------------------------------------------------- 1 | // Package backoff provides an exponential-backoff implementation. 2 | package backoff 3 | 4 | import ( 5 | "math" 6 | "math/rand" 7 | "sync/atomic" 8 | "time" 9 | ) 10 | 11 | // Backoff is a time.Duration counter, starting at Min. After every call to 12 | // the Duration method the current timing is multiplied by Factor, but it 13 | // never exceeds Max. 14 | // 15 | // Backoff is not generally concurrent-safe, but the ForAttempt method can 16 | // be used concurrently. 17 | type Backoff struct { 18 | attempt uint64 19 | 20 | // Factor is the multiplying factor for each increment step. 21 | // 22 | // Defaults to 2. 23 | Factor float64 24 | 25 | // Jitter eases contention by randomizing backoff steps. 26 | // 27 | // Defaults to false. 28 | Jitter bool 29 | 30 | // Minimum value of the counter. 31 | // 32 | // Defaults to 100 milliseconds. 33 | Min time.Duration 34 | 35 | // Maximum value of the counter. 36 | // 37 | // Defaults to 10 seconds. 38 | Max time.Duration 39 | } 40 | 41 | // Duration returns the duration for the current attempt before incrementing 42 | // the attempt counter. See ForAttempt. 43 | func (b *Backoff) Duration() time.Duration { 44 | d := b.ForAttempt(float64(atomic.AddUint64(&b.attempt, 1) - 1)) 45 | return d 46 | } 47 | 48 | const maxInt64 = float64(math.MaxInt64 - 512) 49 | 50 | // ForAttempt returns the duration for a specific attempt. This is useful if 51 | // you have a large number of independent Backoffs, but don't want use 52 | // unnecessary memory storing the Backoff parameters per Backoff. The first 53 | // attempt should be 0. 54 | // 55 | // ForAttempt is concurrent-safe. 56 | func (b *Backoff) ForAttempt(attempt float64) time.Duration { 57 | // Zero-values are nonsensical, so we use 58 | // them to apply defaults 59 | min := b.Min 60 | if min <= 0 { 61 | min = 100 * time.Millisecond 62 | } 63 | max := b.Max 64 | if max <= 0 { 65 | max = 10 * time.Second 66 | } 67 | if min >= max { 68 | // short-circuit 69 | return max 70 | } 71 | factor := b.Factor 72 | if factor <= 0 { 73 | factor = 2 74 | } 75 | //calculate this duration 76 | minf := float64(min) 77 | durf := minf * math.Pow(factor, attempt) 78 | if b.Jitter { 79 | durf = rand.Float64()*(durf-minf) + minf 80 | } 81 | //ensure float64 wont overflow int64 82 | if durf > maxInt64 { 83 | return max 84 | } 85 | dur := time.Duration(durf) 86 | //keep within bounds 87 | if dur < min { 88 | return min 89 | } 90 | if dur > max { 91 | return max 92 | } 93 | return dur 94 | } 95 | 96 | // Reset restarts the current attempt counter at zero. 97 | func (b *Backoff) Reset() { 98 | atomic.StoreUint64(&b.attempt, 0) 99 | } 100 | 101 | // Attempt returns the current attempt counter value. 102 | func (b *Backoff) Attempt() float64 { 103 | return float64(atomic.LoadUint64(&b.attempt)) 104 | } 105 | 106 | // Copy returns a backoff with equals constraints as the original 107 | func (b *Backoff) Copy() *Backoff { 108 | return &Backoff{ 109 | Factor: b.Factor, 110 | Jitter: b.Jitter, 111 | Min: b.Min, 112 | Max: b.Max, 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /backoff_test.go: -------------------------------------------------------------------------------- 1 | package backoff 2 | 3 | import ( 4 | "reflect" 5 | "sync" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func Test1(t *testing.T) { 11 | 12 | b := &Backoff{ 13 | Min: 100 * time.Millisecond, 14 | Max: 10 * time.Second, 15 | Factor: 2, 16 | } 17 | 18 | equals(t, b.Duration(), 100*time.Millisecond) 19 | equals(t, b.Duration(), 200*time.Millisecond) 20 | equals(t, b.Duration(), 400*time.Millisecond) 21 | b.Reset() 22 | equals(t, b.Duration(), 100*time.Millisecond) 23 | } 24 | 25 | func TestForAttempt(t *testing.T) { 26 | 27 | b := &Backoff{ 28 | Min: 100 * time.Millisecond, 29 | Max: 10 * time.Second, 30 | Factor: 2, 31 | } 32 | 33 | equals(t, b.ForAttempt(0), 100*time.Millisecond) 34 | equals(t, b.ForAttempt(1), 200*time.Millisecond) 35 | equals(t, b.ForAttempt(2), 400*time.Millisecond) 36 | b.Reset() 37 | equals(t, b.ForAttempt(0), 100*time.Millisecond) 38 | } 39 | 40 | func Test2(t *testing.T) { 41 | 42 | b := &Backoff{ 43 | Min: 100 * time.Millisecond, 44 | Max: 10 * time.Second, 45 | Factor: 1.5, 46 | } 47 | 48 | equals(t, b.Duration(), 100*time.Millisecond) 49 | equals(t, b.Duration(), 150*time.Millisecond) 50 | equals(t, b.Duration(), 225*time.Millisecond) 51 | b.Reset() 52 | equals(t, b.Duration(), 100*time.Millisecond) 53 | } 54 | 55 | func Test3(t *testing.T) { 56 | 57 | b := &Backoff{ 58 | Min: 100 * time.Nanosecond, 59 | Max: 10 * time.Second, 60 | Factor: 1.75, 61 | } 62 | 63 | equals(t, b.Duration(), 100*time.Nanosecond) 64 | equals(t, b.Duration(), 175*time.Nanosecond) 65 | equals(t, b.Duration(), 306*time.Nanosecond) 66 | b.Reset() 67 | equals(t, b.Duration(), 100*time.Nanosecond) 68 | } 69 | 70 | func Test4(t *testing.T) { 71 | b := &Backoff{ 72 | Min: 500 * time.Second, 73 | Max: 100 * time.Second, 74 | Factor: 1, 75 | } 76 | 77 | equals(t, b.Duration(), b.Max) 78 | } 79 | 80 | func TestGetAttempt(t *testing.T) { 81 | b := &Backoff{ 82 | Min: 100 * time.Millisecond, 83 | Max: 10 * time.Second, 84 | Factor: 2, 85 | } 86 | equals(t, b.Attempt(), float64(0)) 87 | equals(t, b.Duration(), 100*time.Millisecond) 88 | equals(t, b.Attempt(), float64(1)) 89 | equals(t, b.Duration(), 200*time.Millisecond) 90 | equals(t, b.Attempt(), float64(2)) 91 | equals(t, b.Duration(), 400*time.Millisecond) 92 | equals(t, b.Attempt(), float64(3)) 93 | b.Reset() 94 | equals(t, b.Attempt(), float64(0)) 95 | equals(t, b.Duration(), 100*time.Millisecond) 96 | equals(t, b.Attempt(), float64(1)) 97 | } 98 | 99 | func TestJitter(t *testing.T) { 100 | b := &Backoff{ 101 | Min: 100 * time.Millisecond, 102 | Max: 10 * time.Second, 103 | Factor: 2, 104 | Jitter: true, 105 | } 106 | 107 | equals(t, b.Duration(), 100*time.Millisecond) 108 | between(t, b.Duration(), 100*time.Millisecond, 200*time.Millisecond) 109 | between(t, b.Duration(), 100*time.Millisecond, 400*time.Millisecond) 110 | b.Reset() 111 | equals(t, b.Duration(), 100*time.Millisecond) 112 | } 113 | 114 | func TestCopy(t *testing.T) { 115 | b := &Backoff{ 116 | Min: 100 * time.Millisecond, 117 | Max: 10 * time.Second, 118 | Factor: 2, 119 | } 120 | b2 := b.Copy() 121 | equals(t, b, b2) 122 | } 123 | 124 | func TestConcurrent(t *testing.T) { 125 | b := &Backoff{ 126 | Min: 100 * time.Millisecond, 127 | Max: 10 * time.Second, 128 | Factor: 2, 129 | } 130 | 131 | wg := &sync.WaitGroup{} 132 | 133 | test := func() { 134 | time.Sleep(b.Duration()) 135 | wg.Done() 136 | } 137 | 138 | wg.Add(2) 139 | go test() 140 | go test() 141 | wg.Wait() 142 | } 143 | 144 | func between(t *testing.T, actual, low, high time.Duration) { 145 | t.Helper() 146 | if actual < low { 147 | t.Fatalf("Got %s, Expecting >= %s", actual, low) 148 | } 149 | if actual > high { 150 | t.Fatalf("Got %s, Expecting <= %s", actual, high) 151 | } 152 | } 153 | 154 | func equals(t *testing.T, v1, v2 interface{}) { 155 | t.Helper() 156 | if !reflect.DeepEqual(v1, v2) { 157 | t.Fatalf("Got %v, Expecting %v", v1, v2) 158 | } 159 | } 160 | --------------------------------------------------------------------------------