Skip to content

Commit 804b278

Browse files
authored
Retry should be limited by the max backoff if no maximum number of tries is specified (#2102)
* fix retry bug, use default retry for invocation db update * add tests, implement floor for max retries * table-based tests
1 parent 67a224c commit 804b278

File tree

4 files changed

+128
-14
lines changed

4 files changed

+128
-14
lines changed

server/backends/invocationdb/invocationdb.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,15 +111,14 @@ func (d *InvocationDB) CreateInvocation(ctx context.Context, ti *tables.Invocati
111111
func (d *InvocationDB) UpdateInvocation(ctx context.Context, ti *tables.Invocation) (bool, error) {
112112
updated := false
113113
var err error
114-
retryOptions := &retry.Options{InitialBackoff: time.Second, Multiplier: 2.0, MaxRetries: 5}
115-
for i, r := 1, retry.New(ctx, retryOptions); r.Next(); i++ {
114+
for r := retry.DefaultWithContext(ctx); r.Next(); {
116115
err = d.h.TransactionWithOptions(ctx, db.Opts().WithQueryName("update_invocation"), func(tx *db.DB) error {
117116
result := tx.Where("`invocation_id` = ? AND `attempt` = ?", ti.InvocationID, ti.Attempt).Updates(ti)
118117
updated = result.RowsAffected > 0
119118
return result.Error
120119
})
121120
if d.h.IsDeadlockError(err) {
122-
log.Warningf("Encountered deadlock when attempting to update invocation table for invocation %s, attempt %d of %d", ti.InvocationID, i, retryOptions.MaxRetries)
121+
log.Warningf("Encountered deadlock when attempting to update invocation table for invocation %s, attempt %d of %d", ti.InvocationID, r.AttemptNumber(), r.MaxAttempts())
123122
continue
124123
}
125124
break

server/util/retry/BUILD

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
1-
load("@io_bazel_rules_go//go:def.bzl", "go_library")
1+
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
22

33
go_library(
44
name = "retry",
55
srcs = ["retry.go"],
66
importpath = "github.com/buildbuddy-io/buildbuddy/server/util/retry",
77
visibility = ["//visibility:public"],
88
)
9+
10+
go_test(
11+
name = "retry_test",
12+
srcs = ["retry_test.go"],
13+
deps = [
14+
":retry",
15+
"@com_github_stretchr_testify//assert",
16+
],
17+
)

server/util/retry/retry.go

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,36 @@ type Options struct {
1212
InitialBackoff time.Duration // How long to wait after the first request
1313
MaxBackoff time.Duration // Max amount of time to wait for a single request
1414
Multiplier float64 // Next backoff is this * previous backoff
15-
MaxRetries int // Max number of retries; 0 is inf
15+
MaxRetries int // Max number of retries; 0 is based on time
1616
}
1717

1818
type Retry struct {
1919
opts *Options
2020
ctx context.Context
2121

2222
currentAttempt int
23+
maxAttempts int
2324
isReset bool
2425
}
2526

2627
func New(ctx context.Context, opts *Options) *Retry {
28+
maxAttempts := opts.MaxRetries
29+
if maxAttempts <= 0 {
30+
// always try at least once
31+
maxAttempts = 1
32+
if opts.Multiplier > 1 && opts.MaxBackoff > opts.InitialBackoff {
33+
maxAttempts = 1 + int(math.Ceil(
34+
math.Log(
35+
float64(opts.MaxBackoff)/float64(opts.InitialBackoff),
36+
)/math.Log(opts.Multiplier),
37+
))
38+
}
39+
}
40+
2741
r := &Retry{
28-
ctx: ctx,
29-
opts: opts,
42+
ctx: ctx,
43+
opts: opts,
44+
maxAttempts: maxAttempts,
3045
}
3146
r.Reset()
3247
return r
@@ -40,17 +55,15 @@ func New(ctx context.Context, opts *Options) *Retry {
4055
// doSomething()
4156
// }
4257
func DefaultWithContext(ctx context.Context) *Retry {
43-
r := &Retry{
44-
ctx: ctx,
45-
opts: &Options{
58+
return New(
59+
ctx,
60+
&Options{
4661
InitialBackoff: 50 * time.Millisecond,
4762
MaxBackoff: 3 * time.Second,
4863
Multiplier: 2,
4964
MaxRetries: 0, // unlimited, time based max by default.
5065
},
51-
}
52-
r.Reset()
53-
return r
66+
)
5467
}
5568

5669
func (r *Retry) Reset() {
@@ -74,7 +87,7 @@ func (r *Retry) Next() bool {
7487
}
7588

7689
// If we're out of retries, exit.
77-
if r.opts.MaxRetries > 0 && r.currentAttempt == r.opts.MaxRetries {
90+
if r.currentAttempt >= r.maxAttempts {
7891
return false
7992
}
8093

@@ -86,3 +99,11 @@ func (r *Retry) Next() bool {
8699
return false
87100
}
88101
}
102+
103+
func (r *Retry) AttemptNumber() int {
104+
return r.currentAttempt + 1
105+
}
106+
107+
func (r *Retry) MaxAttempts() int {
108+
return r.maxAttempts
109+
}

server/util/retry/retry_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package retry_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/buildbuddy-io/buildbuddy/server/util/retry"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestMaxRetryFromBackoff(t *testing.T) {
12+
tests := map[string]struct {
13+
inputOptions *retry.Options
14+
expectedMaxAttempts int
15+
}{
16+
"exact backoff boundary": {
17+
inputOptions: &retry.Options{
18+
InitialBackoff: 3,
19+
MaxBackoff: 24,
20+
Multiplier: 2,
21+
MaxRetries: 0,
22+
},
23+
expectedMaxAttempts: 4,
24+
},
25+
"above exact backoff boundary": {
26+
inputOptions: &retry.Options{
27+
InitialBackoff: 3,
28+
MaxBackoff: 25,
29+
Multiplier: 2,
30+
MaxRetries: 0,
31+
},
32+
expectedMaxAttempts: 5,
33+
},
34+
"below exact backoff boundary": {
35+
inputOptions: &retry.Options{
36+
InitialBackoff: 3,
37+
MaxBackoff: 23,
38+
Multiplier: 2,
39+
MaxRetries: 0,
40+
},
41+
expectedMaxAttempts: 4,
42+
},
43+
"manually set maximum above calculated backoff": {
44+
inputOptions: &retry.Options{
45+
InitialBackoff: 3,
46+
MaxBackoff: 23,
47+
Multiplier: 2,
48+
MaxRetries: 7,
49+
},
50+
expectedMaxAttempts: 7,
51+
},
52+
"manually set maximum below calculated backoff": {
53+
inputOptions: &retry.Options{
54+
InitialBackoff: 3,
55+
MaxBackoff: 23,
56+
Multiplier: 2,
57+
MaxRetries: 2,
58+
},
59+
expectedMaxAttempts: 2,
60+
},
61+
"initial backoff greater than maximum": {
62+
inputOptions: &retry.Options{
63+
InitialBackoff: 3,
64+
MaxBackoff: 1,
65+
Multiplier: 2,
66+
MaxRetries: 0,
67+
},
68+
expectedMaxAttempts: 1,
69+
},
70+
"multiplier less than or equal to one": {
71+
inputOptions: &retry.Options{
72+
InitialBackoff: 3,
73+
MaxBackoff: 7,
74+
Multiplier: 0.5,
75+
MaxRetries: 0,
76+
},
77+
expectedMaxAttempts: 1,
78+
},
79+
}
80+
for name, tc := range tests {
81+
t.Run(name, func(t *testing.T) {
82+
assert.Equal(t, tc.expectedMaxAttempts, retry.New(context.Background(), tc.inputOptions).MaxAttempts())
83+
})
84+
}
85+
}

0 commit comments

Comments
 (0)