Skip to content

Commit f8f3c7c

Browse files
committed
fixes #15 - added tons of new documentation, godoc, and small cosmetic changes to code
- updated readme - edited and improved godoc comments throughout the code, made them much clearer - function usage in godoc was moved to examples *_test.go files. they will be shows in the same adjacent to the function in the godoc. - some cosmetic changes as well, to comply with gofmt, go vet, golint - fixed indentation problems - added function examples and an advanced HTTP GET example - added new Operation, Notify types to make it easier to understand the how the package works
1 parent 6c45d6b commit f8f3c7c

9 files changed

+303
-105
lines changed

README.md

+70-23
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,80 @@
1-
# backoff
1+
# Exponential Backoff [![GoDoc][godoc image]][godoc] [![Build Status][travis image]][travis]
22

3-
[![GoDoc](https://godoc.org/github.com/cenkalti/backoff?status.png)](https://godoc.org/github.com/cenkalti/backoff)
4-
[![Build Status](https://travis-ci.org/cenkalti/backoff.png)](https://travis-ci.org/cenkalti/backoff)
3+
This is a Go port of the exponential backoff algorithm from [Google's HTTP Client Library for Java][google-http-java-client].
54

6-
This is a Go port of the exponential backoff algorithm from
7-
[google-http-java-client](https://code.google.com/p/google-http-java-client/wiki/ExponentialBackoff).
8-
9-
[Exponential backoff](http://en.wikipedia.org/wiki/Exponential_backoff)
5+
[Exponential backoff][exponential backoff wiki]
106
is an algorithm that uses feedback to multiplicatively decrease the rate of some process,
117
in order to gradually find an acceptable rate.
128
The retries exponentially increase and stop increasing when a certain threshold is met.
139

10+
## How To
1411

12+
We define two functions, `Retry()` and `RetryNotify()`.
13+
They receive an `Operation` to execute, a `BackOff` algorithm,
14+
and an optional `Notify` error handler.
1515

16+
The operation will be executed, and will be retried on failure with delay
17+
as given by the backoff algorithm. The backoff algorithm can also decide when to stop
18+
retrying.
19+
In addition, the notify error handler will be called after each failed attempt,
20+
except for the last time, whose error should be handled by the caller.
1621

17-
## Install
18-
19-
```bash
20-
go get github.com/cenkalti/backoff
22+
```go
23+
// An Operation is executing by Retry() or RetryNotify().
24+
// The operation will be retried using a backoff policy if it returns an error.
25+
type Operation func() error
26+
27+
// Notify is a notify-on-error function. It receives an operation error and
28+
// backoff delay if the operation failed (with an error).
29+
//
30+
// NOTE that if the backoff policy stated to stop retrying,
31+
// the notify function isn't called.
32+
type Notify func(error, time.Duration)
33+
34+
func Retry(Operation, BackOff) error
35+
func RetryNotify(Operation, BackOff, Notify)
2136
```
2237

23-
## Example
38+
## Examples
2439

25-
Simple retry helper that uses exponential back-off algorithm:
40+
See more advanced examples in the [godoc][advanced example].
41+
42+
### Retry
43+
44+
Simple retry helper that uses the default exponential backoff algorithm:
2645

2746
```go
2847
operation := func() error {
29-
// An operation that might fail
48+
// An operation that might fail.
49+
return nil // or return errors.New("some error")
3050
}
3151

32-
err := backoff.Retry(operation, backoff.NewExponentialBackOff())
52+
err := Retry(operation, NewExponentialBackOff())
3353
if err != nil {
34-
// handle error
54+
// Handle error.
55+
return err
3556
}
3657

37-
// operation is successfull
58+
// Operation is successful.
59+
return nil
3860
```
3961

40-
Ticker example:
62+
### Ticker
4163

4264
```go
4365
operation := func() error {
44-
// An operation that may fail
66+
// An operation that might fail
67+
return nil // or return errors.New("some error")
4568
}
4669

47-
b := backoff.NewExponentialBackOff()
48-
ticker := backoff.NewTicker(b)
70+
b := NewExponentialBackOff()
71+
ticker := NewTicker(b)
4972

5073
var err error
5174

5275
// Ticks will continue to arrive when the previous operation is still running,
5376
// so operations that take a while to fail could run in quick succession.
54-
for t = range ticker.C {
77+
for range ticker.C {
5578
if err = operation(); err != nil {
5679
log.Println(err, "will retry...")
5780
continue
@@ -63,7 +86,31 @@ for t = range ticker.C {
6386

6487
if err != nil {
6588
// Operation has failed.
89+
return err
6690
}
6791

68-
// Operation is successfull.
92+
// Operation is successful.
93+
return nil
6994
```
95+
96+
## Getting Started
97+
98+
```bash
99+
# install
100+
$ go get github.com/cenkalti/backoff
101+
102+
# test
103+
$ cd $GOPATH/src/github.com/cenkalti/backoff
104+
$ go get -t ./...
105+
$ go test -v -cover
106+
```
107+
108+
[godoc]: https://godoc.org/github.com/cenkalti/backoff
109+
[godoc image]: https://godoc.org/github.com/cenkalti/backoff?status.png
110+
[travis]: https://travis-ci.org/cenkalti/backoff
111+
[travis image]: https://travis-ci.org/cenkalti/backoff.png
112+
113+
[google-http-java-client]: https://github.com/google/google-http-java-client
114+
[exponential backoff wiki]: http://en.wikipedia.org/wiki/Exponential_backoff
115+
116+
[advanced example]: https://godoc.org/github.com/cenkalti/backoff#example_

adv_example_test.go

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package backoff
2+
3+
import (
4+
"io/ioutil"
5+
"log"
6+
"net/http"
7+
"time"
8+
)
9+
10+
// This is an example that demonstrates how this package could be used
11+
// to perform various advanced operations.
12+
//
13+
// It executes an HTTP GET request with exponential backoff,
14+
// while errors are logged and failed responses are closed, as required by net/http package.
15+
//
16+
// Note we define a condition function which is used inside the operation to
17+
// determine whether the operation succeeded or failed.
18+
func Example() error {
19+
res, err := GetWithRetry(
20+
"http://localhost:9999",
21+
ErrorIfStatusCodeIsNot(http.StatusOK),
22+
NewExponentialBackOff())
23+
24+
if err != nil {
25+
// Close response body of last (failed) attempt.
26+
// The Last attempt isn't handled by the notify-on-error function,
27+
// which closes the body of all the previous attempts.
28+
if e := res.Body.Close(); e != nil {
29+
log.Printf("error closing last attempt's response body: %s", e)
30+
}
31+
log.Printf("too many failed request attempts: %s", err)
32+
return err
33+
}
34+
defer res.Body.Close() // The response's Body must be closed.
35+
36+
// Read body
37+
_, _ = ioutil.ReadAll(res.Body)
38+
39+
// Do more stuff
40+
return nil
41+
}
42+
43+
// GetWithRetry is a helper function that performs an HTTP GET request
44+
// to the given URL, and retries with the given backoff using the given condition function.
45+
//
46+
// It also uses a notify-on-error function which logs
47+
// and closes the response body of the failed request.
48+
func GetWithRetry(url string, condition Condition, bck BackOff) (*http.Response, error) {
49+
var res *http.Response
50+
err := RetryNotify(
51+
func() error {
52+
var err error
53+
res, err = http.Get(url)
54+
if err != nil {
55+
return err
56+
}
57+
return condition(res)
58+
},
59+
bck,
60+
LogAndClose())
61+
62+
return res, err
63+
}
64+
65+
// Condition is a retry condition function.
66+
// It receives a response, and returns an error
67+
// if the response failed the condition.
68+
type Condition func(*http.Response) error
69+
70+
// ErrorIfStatusCodeIsNot returns a retry condition function.
71+
// The condition returns an error
72+
// if the given response's status code is not the given HTTP status code.
73+
func ErrorIfStatusCodeIsNot(status int) Condition {
74+
return func(res *http.Response) error {
75+
if res.StatusCode != status {
76+
return NewError(res)
77+
}
78+
return nil
79+
}
80+
}
81+
82+
// Error is returned on ErrorIfX() condition functions throughout this package.
83+
type Error struct {
84+
Response *http.Response
85+
}
86+
87+
func NewError(res *http.Response) *Error {
88+
// Sanity check
89+
if res == nil {
90+
panic("response object is nil")
91+
}
92+
return &Error{Response: res}
93+
}
94+
func (err *Error) Error() string { return "request failed" }
95+
96+
// LogAndClose is a notify-on-error function.
97+
// It logs the error and closes the response body.
98+
func LogAndClose() Notify {
99+
return func(err error, wait time.Duration) {
100+
switch e := err.(type) {
101+
case *Error:
102+
defer e.Response.Body.Close()
103+
104+
b, err := ioutil.ReadAll(e.Response.Body)
105+
var body string
106+
if err != nil {
107+
body = "can't read body"
108+
} else {
109+
body = string(b)
110+
}
111+
112+
log.Printf("%s: %s", e.Response.Status, body)
113+
default:
114+
log.Println(err)
115+
}
116+
}
117+
}

backoff.go

+12-9
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@ package backoff
55

66
import "time"
77

8-
// Back-off policy when retrying an operation.
8+
// BackOff is a backoff policy for retrying an operation.
99
type BackOff interface {
10-
// Gets the duration to wait before retrying the operation or
11-
// backoff.Stop to indicate that no retries should be made.
10+
// NextBackOff returns the duration to wait before retrying the operation,
11+
// or backoff.Stop to indicate that no more retries should be made.
1212
//
1313
// Example usage:
1414
//
1515
// duration := backoff.NextBackOff();
1616
// if (duration == backoff.Stop) {
17-
// // do not retry operation
17+
// // Do not retry operation.
1818
// } else {
19-
// // sleep for duration and retry operation
19+
// // Sleep for duration and retry operation.
2020
// }
2121
//
2222
NextBackOff() time.Duration
@@ -28,22 +28,25 @@ type BackOff interface {
2828
// Indicates that no more retries should be made for use in NextBackOff().
2929
const Stop time.Duration = -1
3030

31-
// ZeroBackOff is a fixed back-off policy whose back-off time is always zero,
32-
// meaning that the operation is retried immediately without waiting.
31+
// ZeroBackOff is a fixed backoff policy whose backoff time is always zero,
32+
// meaning that the operation is retried immediately without waiting, indefinitely.
3333
type ZeroBackOff struct{}
3434

3535
func (b *ZeroBackOff) Reset() {}
3636

3737
func (b *ZeroBackOff) NextBackOff() time.Duration { return 0 }
3838

39-
// StopBackOff is a fixed back-off policy that always returns backoff.Stop for
40-
// NextBackOff(), meaning that the operation should not be retried.
39+
// StopBackOff is a fixed backoff policy that always returns backoff.Stop for
40+
// NextBackOff(), meaning that the operation should never be retried.
4141
type StopBackOff struct{}
4242

4343
func (b *StopBackOff) Reset() {}
4444

4545
func (b *StopBackOff) NextBackOff() time.Duration { return Stop }
4646

47+
// ConstantBackOff is a backoff policy that always returns the same backoff delay.
48+
// This is in contrast to an exponential backoff policy,
49+
// which returns a delay that grows longer as you call NextBackOff() over and over again.
4750
type ConstantBackOff struct {
4851
Interval time.Duration
4952
}

backoff_test.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
package backoff
22

33
import (
4-
"time"
5-
64
"testing"
5+
"time"
76
)
87

98
func TestNextBackOffMillis(t *testing.T) {

example_test.go

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package backoff
2+
3+
import "log"
4+
5+
func ExampleRetry() error {
6+
operation := func() error {
7+
// An operation that might fail.
8+
return nil // or return errors.New("some error")
9+
}
10+
11+
err := Retry(operation, NewExponentialBackOff())
12+
if err != nil {
13+
// Handle error.
14+
return err
15+
}
16+
17+
// Operation is successful.
18+
return nil
19+
}
20+
21+
func ExampleTicker() error {
22+
operation := func() error {
23+
// An operation that might fail
24+
return nil // or return errors.New("some error")
25+
}
26+
27+
b := NewExponentialBackOff()
28+
ticker := NewTicker(b)
29+
30+
var err error
31+
32+
// Ticks will continue to arrive when the previous operation is still running,
33+
// so operations that take a while to fail could run in quick succession.
34+
for range ticker.C {
35+
if err = operation(); err != nil {
36+
log.Println(err, "will retry...")
37+
continue
38+
}
39+
40+
ticker.Stop()
41+
break
42+
}
43+
44+
if err != nil {
45+
// Operation has failed.
46+
return err
47+
}
48+
49+
// Operation is successful.
50+
return nil
51+
}

0 commit comments

Comments
 (0)