├── .gitignore ├── .travis.yml ├── examples ├── example04 │ └── example04.go ├── example02 │ └── example02.go ├── example03 │ └── example03.go └── example01 │ └── example01.go ├── LICENSE ├── README.md ├── goback_test.go └── goback.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Vim swap files 2 | *.swp 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.2 4 | - 1.3 5 | - 1.4 6 | 7 | install: 8 | - go get github.com/axw/gocov/gocov 9 | - go get github.com/mattn/goveralls 10 | - if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi 11 | - go get github.com/stretchr/testify/assert 12 | script: 13 | - $HOME/gopath/bin/goveralls -service=travis-ci 14 | -------------------------------------------------------------------------------- /examples/example04/example04.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/carlescere/goback" 8 | ) 9 | 10 | func main() { 11 | // Jitter backoff randomises the retry duration to minimise condending clients 12 | b := &goback.JitterBackoff{ 13 | Min: 100 * time.Millisecond, 14 | Max: 60 * time.Second, 15 | Factor: 2, 16 | } 17 | fmt.Println(b.NextAttempt()) 18 | fmt.Println(b.NextAttempt()) 19 | fmt.Println(b.NextAttempt()) 20 | fmt.Println(b.NextAttempt()) 21 | fmt.Println(b.NextAttempt()) 22 | 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Carles Cerezo Guzmán 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 | 23 | -------------------------------------------------------------------------------- /examples/example02/example02.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/carlescere/goback" 9 | ) 10 | 11 | // Creates a function that will fail 6 times before connecting 12 | func retryGenerator() func(chan bool) { 13 | var failedAttempts = 6 14 | return func(done chan bool) { 15 | if failedAttempts > 0 { 16 | failedAttempts-- 17 | return 18 | } 19 | done <- true 20 | } 21 | } 22 | 23 | func connect(retry func(chan bool), b goback.Backoff) { 24 | done := make(chan bool, 1) 25 | for { 26 | retry(done) 27 | select { 28 | case err := <-goback.After(b): 29 | if err != nil { 30 | log.Fatalf("Error connecting: %v", err) 31 | } 32 | log.Printf("Problem connecting") 33 | continue 34 | case <-done: 35 | //conected 36 | b.Reset() 37 | return 38 | } 39 | } 40 | 41 | } 42 | 43 | func main() { 44 | retry := retryGenerator() 45 | b := &goback.SimpleBackoff{ 46 | Min: 100 * time.Millisecond, 47 | Max: 60 * time.Second, 48 | Factor: 2, 49 | } 50 | connect(retry, b) 51 | // Duplicates the time each time from a minimum of 100ms to a maximum of 1 min. 52 | fmt.Println("Connected") 53 | } 54 | -------------------------------------------------------------------------------- /examples/example03/example03.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/carlescere/goback" 9 | ) 10 | 11 | // Creates a function that will fail 6 times before connecting 12 | func retryGenerator() func(chan bool) { 13 | var failedAttempts = 6 14 | return func(done chan bool) { 15 | if failedAttempts > 0 { 16 | failedAttempts-- 17 | return 18 | } 19 | done <- true 20 | } 21 | } 22 | 23 | func connect(retry func(chan bool), b goback.Backoff) { 24 | done := make(chan bool, 1) 25 | for { 26 | retry(done) 27 | select { 28 | case err := <-goback.After(b): 29 | if err != nil { 30 | log.Fatalf("Error connecting: %v", err) 31 | } 32 | log.Printf("Problem connecting") 33 | continue 34 | case <-done: 35 | //conected 36 | b.Reset() 37 | return 38 | } 39 | } 40 | 41 | } 42 | 43 | func main() { 44 | retry := retryGenerator() 45 | b := &goback.SimpleBackoff{ 46 | Min: 100 * time.Millisecond, 47 | Max: 60 * time.Second, 48 | Factor: 2, 49 | MaxAttempts: 4, 50 | } 51 | connect(retry, b) 52 | // Duplicates the time each time from a minimum of 100ms to a maximum of 1 min. 53 | fmt.Println("Connected") 54 | } 55 | -------------------------------------------------------------------------------- /examples/example01/example01.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | "time" 8 | 9 | "github.com/carlescere/goback" 10 | ) 11 | 12 | // Creates a function that will fail 6 times before connecting 13 | func faultyTCPGenerator() func(string, string, string) (net.Listener, error) { 14 | var failedAttempts = 6 15 | return func(protocol, address, port string) (net.Listener, error) { 16 | if failedAttempts > 0 { 17 | failedAttempts-- 18 | return nil, fmt.Errorf("Haha!") // :) 19 | } 20 | l, err := net.Listen(protocol, fmt.Sprintf("%s:%s", address, port)) 21 | return l, err 22 | } 23 | } 24 | 25 | func main() { 26 | tcpConnect := faultyTCPGenerator() 27 | // Duplicates the time each time from a minimum of 100ms to a maximum of 1 min. 28 | b := &goback.SimpleBackoff{ 29 | Min: 100 * time.Millisecond, 30 | Max: 60 * time.Second, 31 | Factor: 2, 32 | } 33 | for { 34 | l, err := tcpConnect("tcp", "localhost", "5000") 35 | if err != nil { // fail to connect 36 | log.Printf("Error connecting: %v", err) 37 | goback.Wait(b) // Exponential backoff 38 | continue 39 | } 40 | defer l.Close() 41 | // connected 42 | log.Printf("Connected!") 43 | b.Reset() // Reset number of attempts. Brings backoff time to the minimum 44 | break // Here be dragons 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # goback 2 | [![GoDoc](https://godoc.org/github.com/carlescere/goback?status.svg)](https://godoc.org/github.com/carlescere/goback) 3 | [![Build Status](https://travis-ci.org/carlescere/goback.svg)](https://travis-ci.org/carlescere/goback) 4 | [![Coverage Status](https://coveralls.io/repos/carlescere/goback/badge.svg)](https://coveralls.io/r/carlescere/goback) 5 | 6 | 7 | Goback implements a simple exponential backoff. 8 | 9 | An exponential backoff approach is typically used when treating with potentially faulty/slow systems. If a system fails quick retries may exacerbate the system specially when the system is dealing with several clients. In this case a backoff provides the faulty system enough room to recover. 10 | 11 | ## How to use 12 | ```go 13 | func main() { 14 | b := &goback.SimpleBackoff( 15 | Min: 100 * time.Millisecond, 16 | Max: 60 * time.Second, 17 | Factor: 2, 18 | ) 19 | goback.Wait(b) // sleeps 100ms 20 | goback.Wait(b) // sleeps 200ms 21 | goback.Wait(b) // sleeps 400ms 22 | fmt.Println(b.NextRun()) // prints 800ms 23 | b.Reset() // resets the backoff 24 | goback.Wait(b) // sleeps 100ms 25 | } 26 | ``` 27 | 28 | Furter examples can be found in the examples folder. 29 | 30 | ## Strategies 31 | At the moment there are two backoff strategies implemented. 32 | 33 | ### Simple Backoff 34 | It starts with a minumum duration and multiplies it by the factor until the maximum waiting time is reached. In that case it will return `Max`. 35 | 36 | The optional `MaxAttempts` will limit the maximum number of retries and will return an error when is exceeded. 37 | 38 | ### Jitter Backoff 39 | The Jitter strategy is based on the simple backoff but adds a light randomisation to minimise collisions between contending clients. 40 | 41 | The result of the 'NextDuration()' method will be a random duration between `[d-min, d+min]` where `d` is the expected duration without jitter and `min` is the minimum duration. 42 | 43 | ### Extensibility 44 | By creating structs that implement the methods of the `Backoff` interface you will be able to use them as a backoff strategy. 45 | 46 | A naive example of this is: 47 | ```go 48 | type NaiveBackoff struct{} 49 | 50 | func (b *NaiveBackoff) NextAttempt() (time.Duration, error) { return time.Second, nil } 51 | func (b *NaiveBackoff) Reset() { } 52 | ``` 53 | This will return always a 1s duration. 54 | 55 | ## Credits 56 | This package is inspired in https://github.com/jpillora/backoff 57 | 58 | ## License 59 | Distributed under MIT license. See `LICENSE` file for more information. 60 | -------------------------------------------------------------------------------- /goback_test.go: -------------------------------------------------------------------------------- 1 | package goback 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestAfter(t *testing.T) { 12 | b := &SimpleBackoff{ 13 | Factor: 2, 14 | Min: 100 * time.Millisecond, 15 | Max: 2 * time.Second, 16 | MaxAttempts: 1, 17 | } 18 | var t1, t2, t3 time.Time 19 | var err1, err2 error 20 | t1 = time.Now() 21 | select { 22 | case err1 = <-After(b): 23 | t2 = time.Now() 24 | case <-time.After(101 * time.Millisecond): 25 | t.Error("Not executed on time") 26 | } 27 | select { 28 | case err2 = <-After(b): 29 | case <-time.After(time.Millisecond): 30 | t.Error("Not executed on time") 31 | } 32 | t3 = time.Now() 33 | assert.WithinDuration(t, t1.Add(100*time.Millisecond), t2, time.Millisecond) 34 | assert.WithinDuration(t, t2, t3, time.Millisecond) 35 | assert.Nil(t, err1) 36 | assert.NotNil(t, err2) 37 | 38 | } 39 | 40 | func TestWait(t *testing.T) { 41 | b := &SimpleBackoff{ 42 | Factor: 2, 43 | Min: 100 * time.Millisecond, 44 | Max: 2 * time.Second, 45 | MaxAttempts: 1, 46 | } 47 | t1 := time.Now() 48 | err1 := Wait(b) 49 | t2 := time.Now() 50 | err2 := Wait(b) 51 | t3 := time.Now() 52 | assert.WithinDuration(t, t1.Add(100*time.Millisecond), t2, time.Millisecond) 53 | assert.WithinDuration(t, t2, t3, time.Millisecond) 54 | assert.Nil(t, err1) 55 | assert.NotNil(t, err2) 56 | 57 | } 58 | 59 | func TestSimple(t *testing.T) { 60 | b := &SimpleBackoff{ 61 | Factor: 2, 62 | Min: 100 * time.Millisecond, 63 | Max: 2 * time.Second, 64 | MaxAttempts: 6, 65 | } 66 | next, err := b.NextAttempt() 67 | assert.Equal(t, 100*time.Millisecond, next) 68 | assert.Nil(t, err) 69 | next, err = b.NextAttempt() 70 | assert.Equal(t, 200*time.Millisecond, next) 71 | assert.Nil(t, err) 72 | next, err = b.NextAttempt() 73 | assert.Equal(t, 400*time.Millisecond, next) 74 | assert.Nil(t, err) 75 | next, err = b.NextAttempt() 76 | assert.Equal(t, 800*time.Millisecond, next) 77 | assert.Nil(t, err) 78 | next, err = b.NextAttempt() 79 | assert.Equal(t, 1600*time.Millisecond, next) 80 | assert.Nil(t, err) 81 | next, err = b.NextAttempt() 82 | assert.Equal(t, 2*time.Second, next) 83 | assert.Nil(t, err) 84 | next, err = b.NextAttempt() 85 | assert.NotNil(t, err) 86 | b.Reset() 87 | next, err = b.NextAttempt() 88 | assert.Equal(t, 100*time.Millisecond, next) 89 | assert.Nil(t, err) 90 | } 91 | 92 | func TestJitter(t *testing.T) { 93 | min := 100 * time.Millisecond 94 | b := &JitterBackoff{ 95 | Factor: 2, 96 | Min: min, 97 | Max: 10 * time.Second, 98 | MaxAttempts: 3, 99 | } 100 | next, err := b.NextAttempt() 101 | between(t, next, 100*time.Millisecond, min) 102 | assert.Nil(t, err) 103 | next, err = b.NextAttempt() 104 | between(t, next, 200*time.Millisecond, min) 105 | assert.Nil(t, err) 106 | next, err = b.NextAttempt() 107 | between(t, next, 400*time.Millisecond, min) 108 | assert.Nil(t, err) 109 | next, err = b.NextAttempt() 110 | assert.NotNil(t, err) 111 | } 112 | 113 | func between(t *testing.T, next, expected, offset time.Duration) { 114 | assert.True(t, next >= expected-offset, fmt.Sprintf("%v %v %v", next, expected, offset)) 115 | assert.True(t, next <= expected+offset, fmt.Sprintf("%v %v %v", next, expected, offset)) 116 | 117 | } 118 | -------------------------------------------------------------------------------- /goback.go: -------------------------------------------------------------------------------- 1 | // Package goback implements a simple exponential backoff 2 | // 3 | // An exponential backoff approach is typically used when treating with potentially 4 | // faulty/slow systems. If a system fails quick retries may exacerbate the system 5 | // specially when the system is dealing with several clients. In this case a backoff 6 | // provides the faulty system enough room to recover. 7 | // 8 | // Simple example: 9 | // func main() { 10 | // b := &goback.SimpleBackoff( 11 | // Min: 100 * time.Millisecond, 12 | // Max: 60 * time.Second, 13 | // Factor: 2, 14 | // ) 15 | // goback.Wait(b) // sleeps 100ms 16 | // goback.Wait(b) // sleeps 200ms 17 | // goback.Wait(b) // sleeps 400ms 18 | // fmt.Println(b.NextRun()) // prints 800ms 19 | // b.Reset() // resets the backoff 20 | // goback.Wait(b) // sleeps 100ms 21 | // } 22 | // 23 | // Furter examples can be found in the examples folder in the repository. 24 | package goback 25 | 26 | import ( 27 | "errors" 28 | "math" 29 | "math/rand" 30 | "time" 31 | ) 32 | 33 | var ( 34 | // ErrMaxAttemptsExceeded indicates that the maximum retries has been 35 | // excedeed. Usually to consider a service unreachable/unavailable. 36 | ErrMaxAttemptsExceeded = errors.New("maximum of attempts exceeded") 37 | ) 38 | 39 | // Backoff is the interface that any Backoff strategy needs to implement. 40 | type Backoff interface { 41 | // NextAttempt returns the duration to wait for the next retry. 42 | NextAttempt() (time.Duration, error) 43 | // Reset clears the number of tries. Next call to NextAttempt will return 44 | // the minimum backoff time (if there is no error). 45 | Reset() 46 | } 47 | 48 | // SimpleBackoff provides a simple strategy to backoff. 49 | type SimpleBackoff struct { 50 | Attempts int 51 | MaxAttempts int 52 | Factor float64 53 | Min time.Duration 54 | Max time.Duration 55 | } 56 | 57 | // NextAttempt returns the duration to wait for the next retry. 58 | func (b *SimpleBackoff) NextAttempt() (time.Duration, error) { 59 | if b.MaxAttempts > 0 && b.Attempts >= b.MaxAttempts { 60 | return 0, ErrMaxAttemptsExceeded 61 | } 62 | next := GetNextDuration(b.Min, b.Max, b.Factor, b.Attempts) 63 | b.Attempts++ 64 | return next, nil 65 | } 66 | 67 | // Reset clears the number of tries. Next call to NextAttempt will return 68 | // the minimum backoff time (if there is no error). 69 | func (b *SimpleBackoff) Reset() { 70 | b.Attempts = 0 71 | } 72 | 73 | // JitterBackoff provides an strategy similar to SimpleBackoff but lightly randomises 74 | // the duration to minimise collisions between contending clients. 75 | type JitterBackoff SimpleBackoff 76 | 77 | // NextAttempt returns the duration to wait for the next retry. 78 | func (b *JitterBackoff) NextAttempt() (time.Duration, error) { 79 | if b.MaxAttempts > 0 && b.Attempts >= b.MaxAttempts { 80 | return 0, ErrMaxAttemptsExceeded 81 | } 82 | next := GetNextDuration(b.Min, b.Max, b.Factor, b.Attempts) 83 | next = addJitter(next, b.Min) 84 | b.Attempts++ 85 | return next, nil 86 | } 87 | 88 | // GetNextDuration returns the duration for the strategies considering the minimum and 89 | // maximum durations, the factor of increase and the number of attemtps tried. 90 | func GetNextDuration(min, max time.Duration, factor float64, attempts int) time.Duration { 91 | d := time.Duration(float64(min) * math.Pow(factor, float64(attempts))) 92 | if d > max { 93 | return max 94 | } 95 | return d 96 | } 97 | 98 | // Wait sleeps for the duration of the time specified by the backoff strategy. 99 | func Wait(b Backoff) error { 100 | next, err := b.NextAttempt() 101 | if err != nil { 102 | return err 103 | } 104 | time.Sleep(next) 105 | return nil 106 | } 107 | 108 | // After returns a channel that will be called after the time specified by the backoff 109 | // strategy or will exit immediately with an error. 110 | func After(b Backoff) <-chan error { 111 | c := make(chan error, 1) 112 | next, err := b.NextAttempt() 113 | if err != nil { 114 | c <- err 115 | return c 116 | } 117 | go func() { 118 | time.Sleep(next) 119 | c <- nil 120 | }() 121 | return c 122 | } 123 | 124 | // addJitter randomises the final duration 125 | func addJitter(next, min time.Duration) time.Duration { 126 | return time.Duration(rand.Float64()*float64(2*min) + float64(next-min)) 127 | } 128 | 129 | func init() { 130 | rand.Seed(time.Now().UTC().UnixNano()) 131 | } 132 | --------------------------------------------------------------------------------