diff --git a/README.md b/README.md index 2e9052b..1fbe9a4 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,19 @@ DoubleDelay provides a simple function that doubles the duration passed in. This can then be easily used as the `BackoffFunc` in the `CallArgs` structure. +## func ExpBackoff +``` go +func ExpBackoff(minDelay, maxDelay time.Duration, exp float64, applyJitter bool) func(time.Duration, int) time.Duration { +``` +ExpBackoff returns a function a which generates time.Duration values using an +exponential back-off algorithm with the specified parameters. The returned value +can then be easily used as the `BackoffFunc` in the `CallArgs` structure. + +The next delay value is calculated using the following formula: + `newDelay = min(minDelay * exp^attempt, maxDelay)` + +If `applyJitter` is set to `true`, the function will randomly select and return +back a value in the `[minDelay, newDelay]` range. ## func IsAttemptsExceeded ``` go @@ -261,4 +274,4 @@ and Clock have been specified. - - - -Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md) \ No newline at end of file +Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..427905f --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module github.com/juju/retry + +go 1.15 + +require ( + github.com/juju/clock v0.0.0-20180524022203-d293bb356ca4 + github.com/juju/errors v0.0.0-20150916125642-1b5e39b83d18 + github.com/juju/loggo v0.0.0-20170605014607-8232ab8918d9 // indirect + github.com/juju/testing v0.0.0-20180807044555-c84dd6ba038a + github.com/juju/utils v0.0.0-20180424094159-2000ea4ff043 // indirect + github.com/juju/version v0.0.0-20161031051906-1f41e27e54f2 // indirect + golang.org/x/crypto v0.0.0-20180214000028-650f4a345ab4 // indirect + golang.org/x/net v0.0.0-20180406214816-61147c48b25b // indirect + gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2 + gopkg.in/mgo.v2 v2.0.0-20160818015218-f2b6f6c918c4 // indirect + gopkg.in/yaml.v2 v2.0.0-20170712054546-1be3d31502d6 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f6a21cc --- /dev/null +++ b/go.sum @@ -0,0 +1,22 @@ +github.com/juju/clock v0.0.0-20180524022203-d293bb356ca4 h1:v4AMWbdtZyIX8Ohv+FEpSwaCtho9uTtGbwjZab+rDuw= +github.com/juju/clock v0.0.0-20180524022203-d293bb356ca4/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA= +github.com/juju/errors v0.0.0-20150916125642-1b5e39b83d18 h1:Sem5Flzxj8ZdAgY2wfHBUlOYyP4wrpIfM8IZgANNGh8= +github.com/juju/errors v0.0.0-20150916125642-1b5e39b83d18/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= +github.com/juju/loggo v0.0.0-20170605014607-8232ab8918d9 h1:Y+lzErDTURqeXqlqYi4YBYbDd7ycU74gW1ADt57/bgY= +github.com/juju/loggo v0.0.0-20170605014607-8232ab8918d9/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= +github.com/juju/testing v0.0.0-20180807044555-c84dd6ba038a h1:dhnWDfRjO/h2XFBj/n3qm+wGjHm8/UqLSOk9GhZc+P0= +github.com/juju/testing v0.0.0-20180807044555-c84dd6ba038a/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= +github.com/juju/utils v0.0.0-20180424094159-2000ea4ff043 h1:kjdsJcIYzmK2k4X2yVCi5Nip6sGoAuc7CLbp+qQnQUM= +github.com/juju/utils v0.0.0-20180424094159-2000ea4ff043/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= +github.com/juju/version v0.0.0-20161031051906-1f41e27e54f2 h1:loQDi5MyxxNm7Q42mBGuPD6X+F6zw8j5S9yexLgn/BE= +github.com/juju/version v0.0.0-20161031051906-1f41e27e54f2/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= +golang.org/x/crypto v0.0.0-20180214000028-650f4a345ab4 h1:OfaUle5HH9Y0obNU74mlOZ/Igdtwi3eGOKcljJsTnbw= +golang.org/x/crypto v0.0.0-20180214000028-650f4a345ab4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/net v0.0.0-20180406214816-61147c48b25b h1:7rskAFQwNXGW6AD8E/6y0LDHW5mT9rsLD7ViLVFfh5w= +golang.org/x/net v0.0.0-20180406214816-61147c48b25b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2 h1:+j1SppRob9bAgoYmsdW9NNBdKZfgYuWpqnYHv78Qt8w= +gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/mgo.v2 v2.0.0-20160818015218-f2b6f6c918c4 h1:hILp2hNrRnYjZpmIbx70psAHbBSEcQ1NIzDcUbJ1b6g= +gopkg.in/mgo.v2 v2.0.0-20160818015218-f2b6f6c918c4/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/yaml.v2 v2.0.0-20170712054546-1be3d31502d6 h1:CvAnnm1XvMjfib69SZzDwgWfOk+PxYz0hA0HBupilBA= +gopkg.in/yaml.v2 v2.0.0-20170712054546-1be3d31502d6/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= diff --git a/retry.go b/retry.go index 777b5dc..1c20953 100644 --- a/retry.go +++ b/retry.go @@ -5,6 +5,8 @@ package retry import ( "fmt" + "math" + "math/rand" "time" "github.com/juju/errors" @@ -223,3 +225,34 @@ func DoubleDelay(delay time.Duration, attempt int) time.Duration { } return delay * 2 } + +// ExpBackoff returns a function a which generates time.Duration values using +// an exponential back-off algorithm with the specified parameters. The +// returned value can then be easily used as the BackoffFunc in the CallArgs +// structure. +// +// The next delay value is calculated using the following formula: +// newDelay = min(minDelay * exp^attempt, maxDelay) +// +// If applyJitter is set to true, the function will randomly select and return +// back a value in the [minDelay, newDelay] range. +func ExpBackoff(minDelay, maxDelay time.Duration, exp float64, applyJitter bool) func(time.Duration, int) time.Duration { + minDelayF := float64(minDelay) + maxDelayF := float64(maxDelay) + return func(_ time.Duration, attempt int) time.Duration { + newDelay := minDelayF * math.Pow(exp, float64(attempt)) + if newDelay > maxDelayF { + newDelay = maxDelayF + } + + // Return a random value in the [minDelay, newDelay) range. + if applyJitter { + newDelay = rand.Float64() * newDelay + if newDelay < minDelayF { + newDelay = minDelayF + } + } + + return time.Duration(newDelay).Round(time.Millisecond) + } +} diff --git a/retry_test.go b/retry_test.go index c7ac3a6..4b8f62f 100644 --- a/retry_test.go +++ b/retry_test.go @@ -312,3 +312,43 @@ func (*retrySuite) TestMissingClockNotValid(c *gc.C) { c.Check(err, jc.Satisfies, errors.IsNotValid) c.Check(err, gc.ErrorMatches, `missing Clock not valid`) } + +type expBackoffSuite struct { + testing.LoggingSuite +} + +var _ = gc.Suite(&expBackoffSuite{}) + +func (*expBackoffSuite) TestExpBackoffWithoutJitter(c *gc.C) { + backoffFunc := retry.ExpBackoff(200*time.Millisecond, 2*time.Second, 2.0, false) + expDurations := []time.Duration{ + 200 * time.Millisecond, + 400 * time.Millisecond, + 800 * time.Millisecond, + 1600 * time.Millisecond, + 2000 * time.Millisecond, // capped to maxDelay + } + + for attempt, expDuration := range expDurations { + got := backoffFunc(0, attempt) + c.Assert(got, gc.Equals, expDuration, gc.Commentf("unexpected duration for attempt %d", attempt)) + } +} + +func (*expBackoffSuite) TestExpBackoffWithtJitter(c *gc.C) { + minDelay := 200 * time.Millisecond + backoffFunc := retry.ExpBackoff(minDelay, 2*time.Second, 2.0, true) + maxDurations := []time.Duration{ + 200 * time.Millisecond, + 400 * time.Millisecond, + 800 * time.Millisecond, + 1600 * time.Millisecond, + 2000 * time.Millisecond, // capped to maxDelay + } + + for attempt, maxDuration := range maxDurations { + got := backoffFunc(0, attempt) + c.Assert(got, jc.GreaterThan, minDelay-1, gc.Commentf("expected jittered duration for attempt %d to be in the [%s, %s] range; got %s", attempt, minDelay, maxDuration, got)) + c.Assert(got, jc.LessThan, maxDuration+1, gc.Commentf("expected jittered duration for attempt %d to be in the [%s, %s] range; got %s", attempt, minDelay, maxDuration, got)) + } +}