├── .travis.yml ├── .github └── workflows │ └── semgrep.yml ├── LICENSE ├── README.md ├── backoff_test.go └── backoff.go /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | go: 4 | - 1.6 5 | - 1.7 6 | - tip 7 | 8 | before_script: 9 | - go get github.com/GeertJohan/fgt 10 | - go get github.com/golang/lint/golint 11 | - go get golang.org/x/tools/cmd/goimports 12 | - go get honnef.co/go/staticcheck/cmd/staticcheck 13 | 14 | script: 15 | - find . -name \*.go | xargs fgt goimports -l 16 | - fgt go vet ./... 17 | - fgt golint ./... 18 | - fgt staticcheck ./... 19 | - go test ./... 20 | 21 | notifications: 22 | email: 23 | recipients: 24 | - kyle@cloudflare.com 25 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | 2 | on: 3 | pull_request: {} 4 | workflow_dispatch: {} 5 | push: 6 | branches: 7 | - main 8 | - master 9 | schedule: 10 | - cron: '0 0 * * *' 11 | name: Semgrep config 12 | jobs: 13 | semgrep: 14 | name: semgrep/ci 15 | runs-on: ubuntu-20.04 16 | env: 17 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 18 | SEMGREP_URL: https://cloudflare.semgrep.dev 19 | SEMGREP_APP_URL: https://cloudflare.semgrep.dev 20 | SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version 21 | container: 22 | image: returntocorp/semgrep 23 | steps: 24 | - uses: actions/checkout@v3 25 | - run: semgrep ci 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 CloudFlare Inc. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 15 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 16 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 17 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 18 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 19 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 21 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 22 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 23 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # backoff 2 | ## Go implementation of "Exponential Backoff And Jitter" 3 | 4 | This package implements the backoff strategy described in the AWS 5 | Architecture Blog article 6 | ["Exponential Backoff And Jitter"](http://www.awsarchitectureblog.com/2015/03/backoff.html). Essentially, 7 | the backoff has an interval `time.Duration`; the *nth* call 8 | to backoff will return an a `time.Duration` that is *2 n * 9 | interval*. If jitter is enabled (which is the default behaviour), the 10 | duration is a random value between 0 and *2 n * interval*. 11 | The backoff is configured with a maximum duration that will not be 12 | exceeded; e.g., by default, the longest duration returned is 13 | `backoff.DefaultMaxDuration`. 14 | 15 | ## Usage 16 | 17 | A `Backoff` is initialised with a call to `New`. Using zero values 18 | causes it to use `DefaultMaxDuration` and `DefaultInterval` as the 19 | maximum duration and interval. 20 | 21 | ``` 22 | package something 23 | 24 | import "github.com/cloudflare/backoff" 25 | 26 | func retryable() { 27 | b := backoff.New(0, 0) 28 | for { 29 | err := someOperation() 30 | if err == nil { 31 | break 32 | } 33 | 34 | log.Printf("error in someOperation: %v", err) 35 | <-time.After(b.Duration()) 36 | } 37 | 38 | log.Printf("succeeded after %d tries", b.Tries()+1) 39 | b.Reset() 40 | } 41 | ``` 42 | 43 | It can also be used to rate limit code that should retry infinitely, but which does not 44 | use `Backoff` itself. 45 | 46 | ``` 47 | package something 48 | 49 | import ( 50 | "time" 51 | 52 | "github.com/cloudflare/backoff" 53 | ) 54 | 55 | func retryable() { 56 | b := backoff.New(0, 0) 57 | b.SetDecay(30 * time.Second) 58 | 59 | for { 60 | // b will reset if someOperation returns later than 61 | // the last call to b.Duration() + 30s. 62 | err := someOperation() 63 | if err == nil { 64 | break 65 | } 66 | 67 | log.Printf("error in someOperation: %v", err) 68 | <-time.After(b.Duration()) 69 | } 70 | } 71 | ``` 72 | 73 | ## Tunables 74 | 75 | * `NewWithoutJitter` creates a Backoff that doesn't use jitter. 76 | 77 | The default behaviour is controlled by two variables: 78 | 79 | * `DefaultInterval` sets the base interval for backoffs created with 80 | the zero `time.Duration` value in the `Interval` field. 81 | * `DefaultMaxDuration` sets the maximum duration for backoffs created 82 | with the zero `time.Duration` value in the `MaxDuration` field. 83 | 84 | -------------------------------------------------------------------------------- /backoff_test.go: -------------------------------------------------------------------------------- 1 | package backoff 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | // If given New with 0's and no jitter, ensure that certain invariants are met: 11 | // 12 | // - the default max duration and interval should be used 13 | // - noJitter should be true 14 | // - the RNG should not be initialised 15 | // - the first duration should be equal to the default interval 16 | func TestDefaults(t *testing.T) { 17 | b := NewWithoutJitter(0, 0) 18 | 19 | if b.maxDuration != DefaultMaxDuration { 20 | t.Fatalf("expected new backoff to use the default max duration (%s), but have %s", DefaultMaxDuration, b.maxDuration) 21 | } 22 | 23 | if b.interval != DefaultInterval { 24 | t.Fatalf("exepcted new backoff to use the default interval (%s), but have %s", DefaultInterval, b.interval) 25 | } 26 | 27 | if b.noJitter != true { 28 | t.Fatal("backoff should have been initialised without jitter") 29 | } 30 | 31 | dur := b.Duration() 32 | if dur != DefaultInterval { 33 | t.Fatalf("expected first duration to be %s, have %s", DefaultInterval, dur) 34 | } 35 | } 36 | 37 | // Given a zero-value initialised Backoff, it should be transparently 38 | // setup. 39 | func TestSetup(t *testing.T) { 40 | b := new(Backoff) 41 | dur := b.Duration() 42 | if dur < 0 || dur > (5*time.Minute) { 43 | t.Fatalf("want duration between 0 and 5 minutes, have %s", dur) 44 | } 45 | } 46 | 47 | // Ensure that tries incremenets as expected. 48 | func TestTries(t *testing.T) { 49 | b := NewWithoutJitter(5, 1) 50 | 51 | for i := uint64(0); i < 3; i++ { 52 | if b.n != i { 53 | t.Fatalf("want tries=%d, have tries=%d", i, b.n) 54 | } 55 | 56 | pow := 1 << i 57 | expected := time.Duration(pow) 58 | dur := b.Duration() 59 | if dur != expected { 60 | t.Fatalf("want duration=%d, have duration=%d at i=%d", expected, dur, i) 61 | } 62 | } 63 | 64 | for i := uint(3); i < 5; i++ { 65 | dur := b.Duration() 66 | if dur != 5 { 67 | t.Fatalf("want duration=5, have %d at i=%d", dur, i) 68 | } 69 | } 70 | } 71 | 72 | // Ensure that a call to Reset will actually reset the Backoff. 73 | func TestReset(t *testing.T) { 74 | const iter = 10 75 | b := New(1000, 1) 76 | for i := 0; i < iter; i++ { 77 | _ = b.Duration() 78 | } 79 | 80 | if b.n != iter { 81 | t.Fatalf("expected tries=%d, have tries=%d", iter, b.n) 82 | } 83 | 84 | b.Reset() 85 | if b.n != 0 { 86 | t.Fatalf("expected tries=0 after reset, have tries=%d", b.n) 87 | } 88 | } 89 | 90 | const decay = 5 * time.Millisecond 91 | const max = 10 * time.Millisecond 92 | const interval = time.Millisecond 93 | 94 | func TestDecay(t *testing.T) { 95 | const iter = 10 96 | 97 | b := NewWithoutJitter(max, 1) 98 | b.SetDecay(decay) 99 | 100 | var backoff time.Duration 101 | for i := 0; i < iter; i++ { 102 | backoff = b.Duration() 103 | } 104 | 105 | if b.n != iter { 106 | t.Fatalf("expected tries=%d, have tries=%d", iter, b.n) 107 | } 108 | 109 | // Don't decay below backoff 110 | b.lastTry = time.Now().Add(-backoff + 1) 111 | backoff = b.Duration() 112 | if b.n != iter+1 { 113 | t.Fatalf("expected tries=%d, have tries=%d", iter+1, b.n) 114 | } 115 | 116 | // Reset after backoff + decay 117 | b.lastTry = time.Now().Add(-backoff - decay) 118 | b.Duration() 119 | if b.n != 1 { 120 | t.Fatalf("expected tries=%d, have tries=%d", 1, b.n) 121 | } 122 | } 123 | 124 | // Ensure that decay works even if the retry counter is saturated. 125 | func TestDecaySaturation(t *testing.T) { 126 | b := NewWithoutJitter(1<<2, 1) 127 | b.SetDecay(decay) 128 | 129 | var duration time.Duration 130 | for i := 0; i <= 2; i++ { 131 | duration = b.Duration() 132 | } 133 | 134 | if duration != 1<<2 { 135 | t.Fatalf("expected duration=%v, have duration=%v", 1<<2, duration) 136 | } 137 | 138 | b.lastTry = time.Now().Add(-duration - decay) 139 | b.n = math.MaxUint64 140 | 141 | duration = b.Duration() 142 | if duration != 1 { 143 | t.Errorf("expected duration=%v, have duration=%v", 1, duration) 144 | } 145 | } 146 | 147 | func ExampleBackoff_SetDecay() { 148 | b := NewWithoutJitter(max, interval) 149 | b.SetDecay(decay) 150 | 151 | // try 0 152 | fmt.Println(b.Duration()) 153 | 154 | // try 1 155 | fmt.Println(b.Duration()) 156 | 157 | // try 2 158 | duration := b.Duration() 159 | fmt.Println(duration) 160 | 161 | // try 3, below decay 162 | time.Sleep(duration) 163 | duration = b.Duration() 164 | fmt.Println(duration) 165 | 166 | // try 4, resets 167 | time.Sleep(duration + decay) 168 | fmt.Println(b.Duration()) 169 | 170 | // Output: 1ms 171 | // 2ms 172 | // 4ms 173 | // 8ms 174 | // 1ms 175 | } 176 | -------------------------------------------------------------------------------- /backoff.go: -------------------------------------------------------------------------------- 1 | // Package backoff contains an implementation of an intelligent backoff 2 | // strategy. It is based on the approach in the AWS architecture blog 3 | // article titled "Exponential Backoff And Jitter", which is found at 4 | // http://www.awsarchitectureblog.com/2015/03/backoff.html. 5 | // 6 | // Essentially, the backoff has an interval `time.Duration`; the nth 7 | // call to backoff will return a `time.Duration` that is 2^n * 8 | // interval. If jitter is enabled (which is the default behaviour), 9 | // the duration is a random value between 0 and 2^n * interval. The 10 | // backoff is configured with a maximum duration that will not be 11 | // exceeded. 12 | // 13 | // The `New` function will attempt to use the system's cryptographic 14 | // random number generator to seed a Go math/rand random number 15 | // source. If this fails, the package will panic on startup. 16 | package backoff 17 | 18 | import ( 19 | "crypto/rand" 20 | "encoding/binary" 21 | "io" 22 | "math" 23 | mrand "math/rand" 24 | "sync" 25 | "time" 26 | ) 27 | 28 | var prngMu sync.Mutex 29 | var prng *mrand.Rand 30 | 31 | // DefaultInterval is used when a Backoff is initialised with a 32 | // zero-value Interval. 33 | var DefaultInterval = 5 * time.Minute 34 | 35 | // DefaultMaxDuration is maximum amount of time that the backoff will 36 | // delay for. 37 | var DefaultMaxDuration = 6 * time.Hour 38 | 39 | // A Backoff contains the information needed to intelligently backoff 40 | // and retry operations using an exponential backoff algorithm. It should 41 | // be initialised with a call to `New`. 42 | // 43 | // Only use a Backoff from a single goroutine, it is not safe for concurrent 44 | // access. 45 | type Backoff struct { 46 | // maxDuration is the largest possible duration that can be 47 | // returned from a call to Duration. 48 | maxDuration time.Duration 49 | 50 | // interval controls the time step for backing off. 51 | interval time.Duration 52 | 53 | // noJitter controls whether to use the "Full Jitter" 54 | // improvement to attempt to smooth out spikes in a high 55 | // contention scenario. If noJitter is set to true, no 56 | // jitter will be introduced. 57 | noJitter bool 58 | 59 | // decay controls the decay of n. If it is non-zero, n is 60 | // reset if more than the last backoff + decay has elapsed since 61 | // the last try. 62 | decay time.Duration 63 | 64 | n uint64 65 | lastTry time.Time 66 | } 67 | 68 | // New creates a new backoff with the specified max duration and 69 | // interval. Zero values may be used to use the default values. 70 | // 71 | // Panics if either max or interval is negative. 72 | func New(max time.Duration, interval time.Duration) *Backoff { 73 | if max < 0 || interval < 0 { 74 | panic("backoff: max or interval is negative") 75 | } 76 | 77 | b := &Backoff{ 78 | maxDuration: max, 79 | interval: interval, 80 | } 81 | b.setup() 82 | return b 83 | } 84 | 85 | // NewWithoutJitter works similarly to New, except that the created 86 | // Backoff will not use jitter. 87 | func NewWithoutJitter(max time.Duration, interval time.Duration) *Backoff { 88 | b := New(max, interval) 89 | b.noJitter = true 90 | return b 91 | } 92 | 93 | func init() { 94 | var buf [8]byte 95 | var n int64 96 | 97 | _, err := io.ReadFull(rand.Reader, buf[:]) 98 | if err != nil { 99 | panic(err.Error()) 100 | } 101 | 102 | n = int64(binary.LittleEndian.Uint64(buf[:])) 103 | 104 | src := mrand.NewSource(n) 105 | prng = mrand.New(src) 106 | } 107 | 108 | func (b *Backoff) setup() { 109 | if b.interval == 0 { 110 | b.interval = DefaultInterval 111 | } 112 | 113 | if b.maxDuration == 0 { 114 | b.maxDuration = DefaultMaxDuration 115 | } 116 | } 117 | 118 | // Duration returns a time.Duration appropriate for the backoff, 119 | // incrementing the attempt counter. 120 | func (b *Backoff) Duration() time.Duration { 121 | b.setup() 122 | 123 | b.decayN() 124 | 125 | t := b.duration(b.n) 126 | 127 | if b.n < math.MaxUint64 { 128 | b.n++ 129 | } 130 | 131 | if !b.noJitter { 132 | prngMu.Lock() 133 | t = time.Duration(prng.Int63n(int64(t))) 134 | prngMu.Unlock() 135 | } 136 | 137 | return t 138 | } 139 | 140 | // requires b to be locked. 141 | func (b *Backoff) duration(n uint64) (t time.Duration) { 142 | // Saturate pow 143 | pow := time.Duration(math.MaxInt64) 144 | if n < 63 { 145 | pow = 1 << n 146 | } 147 | 148 | t = b.interval * pow 149 | if t/pow != b.interval || t > b.maxDuration { 150 | t = b.maxDuration 151 | } 152 | 153 | return 154 | } 155 | 156 | // Reset resets the attempt counter of a backoff. 157 | // 158 | // It should be called when the rate-limited action succeeds. 159 | func (b *Backoff) Reset() { 160 | b.lastTry = time.Time{} 161 | b.n = 0 162 | } 163 | 164 | // SetDecay sets the duration after which the try counter will be reset. 165 | // Panics if decay is smaller than 0. 166 | // 167 | // The decay only kicks in if at least the last backoff + decay has elapsed 168 | // since the last try. 169 | func (b *Backoff) SetDecay(decay time.Duration) { 170 | if decay < 0 { 171 | panic("backoff: decay < 0") 172 | } 173 | 174 | b.decay = decay 175 | } 176 | 177 | // requires b to be locked 178 | func (b *Backoff) decayN() { 179 | if b.decay == 0 { 180 | return 181 | } 182 | 183 | if b.lastTry.IsZero() { 184 | b.lastTry = time.Now() 185 | return 186 | } 187 | 188 | lastDuration := b.duration(b.n - 1) 189 | decayed := time.Since(b.lastTry) > lastDuration+b.decay 190 | b.lastTry = time.Now() 191 | 192 | if !decayed { 193 | return 194 | } 195 | 196 | b.n = 0 197 | } 198 | --------------------------------------------------------------------------------