Skip to content

Commit ecca9f3

Browse files
authored
Restructure retry mechanism (#108)
* chore: update dependencies * feat: add wrapper over new backoff pkg * feat: modify retryPolicy to use new backoff pkg * test: update tests * feat: use builder pattern for backoff * fix: remove unused code * feat: add IsError method to the response struct
1 parent 42c5d4e commit ecca9f3

File tree

6 files changed

+87
-12
lines changed

6 files changed

+87
-12
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ require (
3232
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
3333
github.com/andybalholm/brotli v1.1.0 // indirect
3434
github.com/beorn7/perks v1.0.1 // indirect
35+
github.com/cenkalti/backoff/v4 v4.3.0
3536
github.com/cespare/xxhash/v2 v2.2.0 // indirect
3637
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
3738
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,8 @@ github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInq
744744
github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
745745
github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
746746
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
747+
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
748+
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
747749
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
748750
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
749751
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=

pkg/zhttpclient/backoff/backoff.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package backoff
2+
3+
import (
4+
"time"
5+
6+
"github.com/cenkalti/backoff/v4"
7+
)
8+
9+
const (
10+
exponentialMultiplier = 2
11+
)
12+
13+
type BackOff struct {
14+
maxAttempts int
15+
maxDuration time.Duration
16+
initialDuration time.Duration
17+
}
18+
19+
func New() *BackOff {
20+
return &BackOff{}
21+
}
22+
23+
func (b *BackOff) WithMaxAttempts(maxAttempts int) *BackOff {
24+
b.maxAttempts = maxAttempts
25+
return b
26+
}
27+
func (b *BackOff) WithMaxDuration(max time.Duration) *BackOff {
28+
b.maxDuration = max
29+
return b
30+
}
31+
func (b *BackOff) WithInitialDuration(initial time.Duration) *BackOff {
32+
b.initialDuration = initial
33+
return b
34+
}
35+
36+
func (b *BackOff) Exponential() backoff.BackOff {
37+
tmp := backoff.NewExponentialBackOff(backoff.WithInitialInterval(b.initialDuration), backoff.WithMaxElapsedTime(b.maxDuration), backoff.WithMultiplier(exponentialMultiplier))
38+
return backoff.WithMaxRetries(tmp, uint64(b.maxAttempts))
39+
}
40+
41+
func (b *BackOff) Linear() backoff.BackOff {
42+
return backoff.WithMaxRetries(backoff.NewConstantBackOff(b.initialDuration), uint64(b.maxAttempts))
43+
}
44+
45+
// Do retries op if it returns an error according to the provided backoff
46+
func Do(op func() error, b backoff.BackOff) error {
47+
return backoff.Retry(op, b)
48+
}

pkg/zhttpclient/client_test.go

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
"errors"
7+
"fmt"
78
"io"
89
"net"
910
"net/http"
@@ -13,6 +14,7 @@ import (
1314
"testing"
1415
"time"
1516

17+
"github.com/cenkalti/backoff/v4"
1618
"github.com/zondax/golem/pkg/zhttpclient"
1719

1820
"github.com/stretchr/testify/assert"
@@ -345,10 +347,10 @@ func TestHTTPClient_Retry(t *testing.T) {
345347
// the request should be retried exponentialy starting from 100ms i.e 100ms * (2 ^ attempt) for a max of 2 times
346348
// the total time of the request should be:
347349
// total = 0ms // attempt 1
348-
// total += 100ms * (2 ^ 1) = 200ms // attempt 1
349-
// total += 100ms * (2 ^ 2) = 400ms // attempt 2
350-
// total = 600ms
351-
// the time between retries ( we will only check the last retry attempt) = 400ms
350+
// total += 100ms * (2 ^ 0) = 100ms // attempt 1
351+
// total += 100ms * (2 ^ 1) = 200ms // attempt 2
352+
// total = 300ms
353+
// the time between retries ( we will only check the last retry attempt) = 200ms
352354
{
353355
name: "exponential retry",
354356
srv: newTestSrv(t, http.StatusInternalServerError, nil, 0),
@@ -361,14 +363,25 @@ func TestHTTPClient_Retry(t *testing.T) {
361363
MaxWaitBeforeRetry: 2 * time.Second,
362364
}
363365
r.WithCodes(http.StatusInternalServerError)
364-
r.SetExponentialBackoff(100 * time.Millisecond)
366+
367+
tmp := backoff.NewExponentialBackOff(backoff.WithInitialInterval(100*time.Millisecond),
368+
backoff.WithMaxElapsedTime(r.MaxWaitBeforeRetry),
369+
backoff.WithMultiplier(2),
370+
)
371+
tmp.RandomizationFactor = 0
372+
b := backoff.WithMaxRetries(tmp, uint64(r.MaxAttempts))
373+
374+
r.SetBackoff(func(_ uint, _ *http.Response, _ error) time.Duration {
375+
return b.NextBackOff()
376+
})
377+
365378
return r
366379
},
367380
wantRetry: true,
368-
wantWaitBetween: 400 * time.Millisecond,
381+
wantWaitBetween: 200 * time.Millisecond,
369382
wantCalled: 3,
370383
wantCode: http.StatusInternalServerError,
371-
wantTotalWait: 600 * time.Millisecond,
384+
wantTotalWait: 300 * time.Millisecond,
372385
wantBody: []byte{},
373386
},
374387
}
@@ -422,6 +435,7 @@ func TestHTTPClient_Retry(t *testing.T) {
422435
assert.Equal(t, tt.wantCalled, tt.srv.called)
423436
if tt.wantRetry {
424437
// ignore minor deviations in millisecond values
438+
fmt.Println(end, start, end-start, tt.wantTotalWait.Milliseconds())
425439
assert.Equal(t, (end-start)/tt.wantTotalWait.Milliseconds(), int64(1))
426440
assert.Equal(t, tt.srv.waitBetweenCalls/tt.wantWaitBetween.Milliseconds(), int64(1))
427441
}

pkg/zhttpclient/request.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ type zRequest struct {
3333
url string
3434
}
3535

36+
// IsError returns true if the statusCode is >= 400
37+
func (r *Response) IsError() bool {
38+
return r.Code > 399
39+
}
40+
3641
func newZRequest(client *zHTTPClient) ZRequest {
3742
// only used to enforce retry policies at the request level
3843
c := New(*client.config).(*zHTTPClient)

pkg/zhttpclient/retry.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package zhttpclient
22

33
import (
4-
"math"
54
"net/http"
65
"time"
6+
7+
"github.com/cenkalti/backoff/v4"
8+
zbackoff "github.com/zondax/golem/pkg/zhttpclient/backoff"
79
)
810

911
// BackoffFn is a function that returns a backoff duration.
@@ -16,6 +18,8 @@ type RetryPolicy struct {
1618
WaitBeforeRetry time.Duration
1719
// MaxWaitBeforeRetry is the maximum cap for the wait before retry
1820
MaxWaitBeforeRetry time.Duration
21+
// b is a wrapped backoff functions provider
22+
b backoff.BackOff
1923
// backoffFn is a function that returns a custom sleep duration before a retry.
2024
// It is capped between WaitBeforeRetry and MaxWaitBeforeRetry
2125
backoffFn BackoffFn
@@ -38,15 +42,16 @@ func (r *RetryPolicy) SetBackoff(fn BackoffFn) {
3842

3943
// SetLinearBackoff sets a constant sleep duration between retries.
4044
func (r *RetryPolicy) SetLinearBackoff(duration time.Duration) {
45+
r.b = zbackoff.New().WithInitialDuration(duration).WithMaxAttempts(r.MaxAttempts).Linear()
4146
r.backoffFn = func(uint, *http.Response, error) time.Duration {
42-
return duration
47+
return r.b.NextBackOff()
4348
}
4449
}
4550

4651
// SetExponentialBackoff sets an exponential base 2 delay ( duration * 2 ^ attempt ) for each attempt.
4752
func (r *RetryPolicy) SetExponentialBackoff(duration time.Duration) {
48-
r.backoffFn = func(attempt uint, _ *http.Response, _ error) time.Duration {
49-
mul := int64(math.Pow(2.0, float64(attempt)))
50-
return time.Millisecond * time.Duration(duration.Milliseconds()*mul)
53+
r.b = zbackoff.New().WithInitialDuration(duration).WithMaxAttempts(r.MaxAttempts).WithMaxDuration(r.MaxWaitBeforeRetry).Exponential()
54+
r.backoffFn = func(_ uint, _ *http.Response, _ error) time.Duration {
55+
return r.b.NextBackOff()
5156
}
5257
}

0 commit comments

Comments
 (0)