Skip to content

Commit fe83d04

Browse files
committed
Add new backoff package with functional strategies.
1 parent d1d968a commit fe83d04

File tree

4 files changed

+155
-0
lines changed

4 files changed

+155
-0
lines changed

Diff for: backoff/doc.go

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Package backoff provides a system for introducing delays between retries of
2+
// an operation.
3+
package backoff

Diff for: backoff/ginkgo_test.go

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package backoff_test
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
7+
"github.com/onsi/ginkgo"
8+
"github.com/onsi/gomega"
9+
)
10+
11+
func TestSuite(t *testing.T) {
12+
type tag struct{}
13+
gomega.RegisterFailHandler(ginkgo.Fail)
14+
ginkgo.RunSpecs(t, reflect.TypeOf(tag{}).PkgPath())
15+
}

Diff for: backoff/strategy.go

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package backoff
2+
3+
import (
4+
"math"
5+
"time"
6+
7+
"github.com/dogmatiq/linger"
8+
)
9+
10+
// DefaultStrategy is the default strategy used if none is specified.
11+
//
12+
// It is a conservative policy favouring large delay times under the assumption
13+
// that the operation is expensive.
14+
var DefaultStrategy Strategy = WithTransforms(
15+
Exponential(3*time.Second),
16+
linger.FullJitter,
17+
linger.Limiter(0, 1*time.Hour),
18+
)
19+
20+
// Strategy is a function for computing delays between attempts to perform some
21+
// application-defined operation.
22+
//
23+
// n is the number of successive failures since the last success.
24+
//
25+
// err is the error describing the operation's failure, if known. A nil error
26+
// does not indicate a success.
27+
type Strategy func(n int, err error) time.Duration
28+
29+
// Exponential returns a Strategy that uses binary exponential backoff (BEB).
30+
//
31+
// The unit delay is doubled after each successive failure.
32+
func Exponential(unit time.Duration) Strategy {
33+
if unit <= 0 {
34+
panic("the unit duration must be postive")
35+
}
36+
37+
u := unit.Seconds()
38+
39+
return func(n int, _ error) time.Duration {
40+
scale := math.Pow(2, float64(n-1))
41+
seconds := u * scale
42+
43+
return linger.FromSeconds(seconds)
44+
}
45+
}
46+
47+
// Constant returns a Strategy that returns a fixed wait duration.
48+
func Constant(d time.Duration) Strategy {
49+
return func(_ int, _ error) time.Duration {
50+
return d
51+
}
52+
}
53+
54+
// Linear returns a Strategy that increases the wait duration linearly.
55+
//
56+
// The unit delay is multiplied by the number of successive failures.
57+
func Linear(unit time.Duration) Strategy {
58+
return func(n int, _ error) time.Duration {
59+
return time.Duration(n) * unit
60+
}
61+
}
62+
63+
// WithTransforms returns a strategy that transforms the result of s using each
64+
// of the given transforms in order.
65+
func WithTransforms(s Strategy, transforms ...linger.DurationTransform) Strategy {
66+
return func(n int, err error) time.Duration {
67+
d := s(n, err)
68+
69+
for _, x := range transforms {
70+
d = x(d)
71+
}
72+
73+
return d
74+
}
75+
}

Diff for: backoff/strategy_test.go

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package backoff
2+
3+
import (
4+
"time"
5+
6+
"github.com/dogmatiq/linger"
7+
. "github.com/onsi/ginkgo"
8+
. "github.com/onsi/gomega"
9+
)
10+
11+
var _ = Describe("func Exponential()", func() {
12+
It("returns a strategy that backs-off exponentially", func() {
13+
strategy := Exponential(3 * time.Second)
14+
15+
Expect(strategy(5, nil)).To(Equal(48 * time.Second))
16+
Expect(strategy(6, nil)).To(Equal(96 * time.Second))
17+
})
18+
19+
It("panics if the unit is zero", func() {
20+
Expect(func() {
21+
Exponential(0)
22+
}).To(Panic())
23+
})
24+
25+
It("panics if the unit is negative", func() {
26+
Expect(func() {
27+
Exponential(-1)
28+
}).To(Panic())
29+
})
30+
})
31+
32+
var _ = Describe("func Constant()", func() {
33+
It("returns a strategy that returns a fixed duration", func() {
34+
strategy := Constant(3 * time.Second)
35+
36+
Expect(strategy(5, nil)).To(Equal(3 * time.Second))
37+
Expect(strategy(6, nil)).To(Equal(3 * time.Second))
38+
})
39+
})
40+
41+
var _ = Describe("func Linear()", func() {
42+
It("returns a strategy that returns a fixed duration", func() {
43+
strategy := Linear(3 * time.Second)
44+
45+
Expect(strategy(5, nil)).To(Equal(15 * time.Second))
46+
Expect(strategy(6, nil)).To(Equal(18 * time.Second))
47+
})
48+
})
49+
50+
var _ = Describe("func WithTransform()", func() {
51+
It("returns a strategy that that transforms the result of the input strategy", func() {
52+
s := WithTransforms(
53+
Linear(10*time.Second),
54+
linger.Limiter(15*time.Second, linger.MaxDuration),
55+
linger.Limiter(0, 25*time.Second),
56+
)
57+
58+
Expect(s(1, nil)).To(Equal(15 * time.Second))
59+
Expect(s(2, nil)).To(Equal(20 * time.Second))
60+
Expect(s(3, nil)).To(Equal(25 * time.Second))
61+
})
62+
})

0 commit comments

Comments
 (0)