Skip to content

Commit

Permalink
Finalized API adjustments
Browse files Browse the repository at this point in the history
With this commit I am ready to call the API stable. I have been using
this library for a bit and adjusted the API from my experience. The
API changes can be summarized as follows:

I found myself always doing retry.Exponential().Units(time.Second).
It is very unlikely that anyone would use a bare Exponential() strategy,
but there are strong use cases for using milliseconds vs seconds or
minutes. With this in mind, I added a `time.Duration` argument to
Exponential.

There was a lot of overlap in what the `Units` method did and what
the `Scale` method did. In addition, `Scale`'s float64 parameter felt
out of place.

This change was asthetic; retry.Intervals(...) reads nicer than retry.Fixed(...)

I felt that Min and Max were more widely understood than Ceil and
Floor, which are usually associated with floating-point numbers. Before,
I was reserving Min and Max for a possible method that chose the minimum
or maximum of two Strategies, but decided such a feature was unnecessary.

Future changes to this package will be minimal and non-breaking.
The only addition I have in mind is the introduction of a Memoize
method for Strategies, that saves the returned values in an array.
  • Loading branch information
David Arroyo committed Mar 20, 2015
1 parent d67aa52 commit e7eefae
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 112 deletions.
125 changes: 64 additions & 61 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import (
)

func ExampleExponential() {
backoff := retry.Exponential().Units(time.Second)
backoff := retry.Exponential(time.Second)

for tries := 0; tries < 10; tries++ {
fmt.Printf("Connection failed; will try again in %s\n", backoff(tries))
for try := 0; try < 10; try++ {
fmt.Printf("Connection failed; will try again in %s\n", backoff(try))
}

// Output: Connection failed; will try again in 1s
Expand All @@ -30,8 +30,8 @@ func ExampleExponential() {
func ExampleSeconds() {
backoff := retry.Seconds(2, 4, 6, 22, 39, 18)

for tries := 0; tries < 10; tries++ {
fmt.Printf("Connection failed; will try again in %s\n", backoff(tries))
for try := 0; try < 10; try++ {
fmt.Printf("Connection failed; will try again in %s\n", backoff(try))
}
// Output: Connection failed; will try again in 2s
// Connection failed; will try again in 4s
Expand All @@ -48,8 +48,8 @@ func ExampleSeconds() {
func ExampleMilliseconds() {
backoff := retry.Milliseconds(2, 4, 6, 22, 39, 18)

for tries := 0; tries < 8; tries++ {
fmt.Printf("Connection failed; will try again in %s\n", backoff(tries))
for try := 0; try < 8; try++ {
fmt.Printf("Connection failed; will try again in %s\n", backoff(try))
}
// Output: Connection failed; will try again in 2ms
// Connection failed; will try again in 4ms
Expand All @@ -61,31 +61,39 @@ func ExampleMilliseconds() {
// Connection failed; will try again in 18ms
}

func ExampleStrategy_Add() {
backoff := retry.Exponential().Units(time.Second).Add(time.Minute)
func ExampleIntervals() {
backoff := retry.Intervals(time.Minute, time.Hour, time.Hour*2)

for tries := 0; tries < 10; tries++ {
fmt.Println(backoff(tries))
for try := 0; try < 4; try++ {
fmt.Println(backoff(try))
}
// Output: 1m0s
// 1h0m0s
// 2h0m0s
// 2h0m0s
}

func ExampleIntervals_scaled() {
backoff := retry.Intervals(1, 2, 3, 4, 5, 6, 7).Scale(time.Microsecond)

// Output: 1m1s
// 1m2s
// 1m4s
// 1m8s
// 1m16s
// 1m32s
// 2m4s
// 3m8s
// 5m16s
// 9m32s
for try := 0; try < 7; try++ {
fmt.Println(backoff(try))
}
// Output: 1µs
// 2µs
// 3µs
// 4µs
// 5µs
// 6µs
// 7µs
}

func ExampleStrategy_Scale() {
// Sleep for 2ⁿ milliseconds, not seconds
backoff := retry.Exponential().Units(time.Second).Scale(1e-3)
// Sleep for 2ⁿ milliseconds, not nanoseconds
backoff := retry.Exponential(1).Scale(time.Millisecond)

for tries := 0; tries < 10; tries++ {
fmt.Println(backoff(tries))
for try := 0; try < 10; try++ {
fmt.Println(backoff(try))
}

// Output: 1ms
Expand All @@ -101,18 +109,18 @@ func ExampleStrategy_Scale() {
}

func ExampleStrategy_Splay() {
backoff := retry.Exponential().Splay(time.Second / 2)
backoff := retry.Exponential(time.Second).Splay(time.Second / 2)

for tries := 0; tries < 10; tries++ {
fmt.Println(backoff(tries))
for try := 0; try < 10; try++ {
fmt.Println(backoff(try))
}
}

func ExampleStrategy_Fixed() {
backoff := retry.Fixed(time.Minute, time.Hour, time.Hour*2)
func ExampleStrategy_Intervals() {
backoff := retry.Intervals(time.Minute, time.Hour, time.Hour*2)

for tries := 0; tries < 4; tries++ {
fmt.Println(backoff(tries))
for try := 0; try < 4; try++ {
fmt.Println(backoff(try))
}

// Output: 1m0s
Expand All @@ -122,45 +130,42 @@ func ExampleStrategy_Fixed() {
}

func ExampleStrategy_Unshift() {
backoff := retry.Exponential().
Units(time.Second).
Unshift(time.Minute)
backoff := retry.Exponential(time.Minute).
Unshift(time.Hour)

for tries := 0; tries < 10; tries++ {
fmt.Println(backoff(tries))
for try := 0; try < 10; try++ {
fmt.Printf("%d: %s\n", try, backoff(try))
}

// Output: 1m0s
// 1s
// 2s
// 4s
// 8s
// 16s
// 32s
// 1m4s
// 2m8s
// 4m16s
// Output: 0: 1h0m0s
// 1: 1m0s
// 2: 2m0s
// 3: 4m0s
// 4: 8m0s
// 5: 16m0s
// 6: 32m0s
// 7: 1h4m0s
// 8: 2h8m0s
// 9: 4h16m0s
}

func ExampleStrategy_Shift() {
backoff := retry.Seconds(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).
Shift(2)
backoff := retry.Seconds(1, 2, 3, 4, 5, 6, 7).Shift(2)

for tries := 0; tries < 10; tries++ {
fmt.Println(backoff(tries))
for try := 0; try < 10; try++ {
fmt.Println(backoff(try))
}

// Output: 3s
// 4s
// 5s
// 6s
// 7s
// 8s
// 9s
// 10s
// 10s
// 10s

// 7s
// 7s
// 7s
// 7s
// 7s
}

func Example() {
Expand All @@ -174,10 +179,8 @@ func Example() {
// Request a dump from a service every hour. If something goes
// wrong, retry on lengthening intervals until we get a response,
// then go back to per-hour dumps.
backoff := retry.Exponential().
Units(time.Minute).
Shift(1).
Overwrite(time.Hour)
backoff := retry.Exponential(time.Minute).Shift(2).
Unshift(time.Hour)
try := 0

for i := 0; i < 7; i++ {
Expand Down
66 changes: 17 additions & 49 deletions retry.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,31 +39,34 @@ func init() {
type Strategy func(nth int) time.Duration

// Exponential creates an exponential backoff Strategy that returns
// 2ⁿ nanoseconds. The values returned by Exponential will increase
// up to the maximum value of time.Duration and will not overflow.
func Exponential() Strategy {
// 2ⁿ units. The values returned by Exponential will increase up to
// the maximum value of time.Duration and will not overflow.
func Exponential(units time.Duration) Strategy {
return func(retry int) time.Duration {
if retry < 0 {
// Can't return a fraction of a nanosecond
// NOTE(droyo) We could return 2ⁿ here by
// taking the nth root if units is more than
// 1ns, but I don't see such a feature being
// used.
return 0
}
var x int64 = 1
x := units
for i := 0; i < retry; i++ {
if x > math.MaxInt64/2 {
return math.MaxInt64
}
x *= 2
}
return time.Duration(x)
return x
}
}

// Fixed creates a backoff policy that selects the nth duration in the
// Intervals creates a backoff policy that selects the nth duration in the
// argument list. If the retry counter is greater than the number of
// items provided, the final item is returned. If the retry counter
// is less than 0 the first item is returned. If the parameter list
// is empty, the returned strategy will always return 0.
func Fixed(dur ...time.Duration) Strategy {
func Intervals(dur ...time.Duration) Strategy {
if len(dur) == 0 {
return func(int) time.Duration { return 0 }
}
Expand Down Expand Up @@ -138,22 +141,9 @@ func (base Strategy) Splay(d time.Duration) Strategy {
}
}

// Scale scales a backoff policy by a fixed duration. The returned
// Policy will return values from the policy, uniformly multiplied by
// secs.
func (base Strategy) Scale(seconds float64) Strategy {
if base == nil {
panic("Scale called on nil Strategy")
}
return func(retry int) time.Duration {
x := base(retry).Seconds() * seconds
return time.Duration(math.Floor(float64(time.Second) * x))
}
}

// Units multiplies all values returned by a duration by a fixed
// Scale multiplies all values returned by a duration by a fixed
// duration.
func (base Strategy) Units(units time.Duration) Strategy {
func (base Strategy) Scale(units time.Duration) Strategy {
if base == nil {
panic("Units called on nil Strategy")
}
Expand All @@ -162,28 +152,6 @@ func (base Strategy) Units(units time.Duration) Strategy {
}
}

// Add adds a fixed duration to every duration returned by a
// Strategy.
func (base Strategy) Add(dur time.Duration) Strategy {
if base == nil {
panic("Add called on nil Strategy")
}
return func(retry int) time.Duration {
return base(retry) + dur
}
}

// Sub subtracts a fixed duration from every duration returned
// by a Strategy.
func (base Strategy) Sub(dur time.Duration) Strategy {
if base == nil {
panic("Sub called on nil Strategy")
}
return func(retry int) time.Duration {
return base(retry) + dur
}
}

// Unshift displaces the first len(dur) mappings of a Strategy, selecting
// durations from the given parameter list instead. Passing len(dur)
// to the returned strategy is equivalent to passing 0 to the original
Expand Down Expand Up @@ -215,10 +183,10 @@ func (base Strategy) Shift(n int) Strategy {
}
}

// The Floor method imposes a minimum value on the durations returned
// The Min method imposes a minimum value on the durations returned
// by a Strategy. Values returned by the resulting Strategy will always
// be greater than or equal to min.
func (base Strategy) Floor(min time.Duration) Strategy {
func (base Strategy) Min(min time.Duration) Strategy {
if base == nil {
panic("Overwrite called on nil Strategy")
}
Expand All @@ -231,10 +199,10 @@ func (base Strategy) Floor(min time.Duration) Strategy {
}
}

// The Ceil method imposes a maximum value on the durations returned
// The Max method imposes a maximum value on the durations returned
// by a Strategy. Values returned by the resulting Strategy will always
// be less than or equal to max
func (base Strategy) Ceil(max time.Duration) Strategy {
func (base Strategy) Max(max time.Duration) Strategy {
if base == nil {
panic("Ceil called on nil Strategy")
}
Expand Down
4 changes: 2 additions & 2 deletions retry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (

func TestExponentialBackoff(t *testing.T) {
var next, prev time.Duration
var bk = Exponential()
var bk = Exponential(time.Second)

for i := 0; i < 100; i++ {
next = bk(i)
Expand All @@ -23,7 +23,7 @@ func TestExponentialBackoff(t *testing.T) {
}
}

func TestFixedBackoff(t *testing.T) {
func TestIntervalBackoff(t *testing.T) {
x := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
const jitter = 50
backoff := Seconds(x...)
Expand Down

0 comments on commit e7eefae

Please sign in to comment.