Skip to content

Commit e4968c7

Browse files
authored
Merge pull request #5 from go-waitfor/chore/backoff-v5
Refactor error handling and update options for resource testing
2 parents dcf146c + 132a65c commit e4968c7

File tree

9 files changed

+103
-41
lines changed

9 files changed

+103
-41
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,8 @@ func main() {
535535

536536
- `ErrWait`: Returned when resources are not available after all retry attempts
537537
- `ErrInvalidArgument`: Returned for invalid input parameters
538+
- `ErrResourceNotFound`: Returned when a resource type is not registered
539+
- `ErrResourceAlreadyRegistered`: Returned when trying to register a resource type that already exists
538540

539541
### Error Handling Patterns
540542

errors.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ package waitfor
22

33
import "errors"
44

5-
// ErrWait is returned when resource availability testing fails.
6-
// This error indicates that one or more resources did not become
7-
// available within the configured timeout and retry parameters.
85
var (
9-
ErrWait = errors.New("failed to wait for resource availability")
6+
// ErrWait is returned when resource availability testing fails.
7+
// This error indicates that one or more resources did not become
8+
// available within the configured timeout and retry parameters.
9+
ErrWait = errors.New("failed to wait for resource availability")
1010
// ErrInvalidArgument is returned when invalid arguments are passed
1111
// to functions, such as empty resource URLs or invalid configuration.
1212
ErrInvalidArgument = errors.New("invalid argument")
13+
// ErrResourceAlreadyRegistered is returned when a resource factory is already registered for a scheme.
14+
ErrResourceAlreadyRegistered = errors.New("resource is already registered with a given scheme")
15+
// ErrResourceNotFound is returned when no resource factory is found for a scheme.
16+
ErrResourceNotFound = errors.New("resource with a given scheme is not found")
1317
)

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.23.0
55
toolchain go1.24.6
66

77
require (
8-
github.com/cenkalti/backoff v2.2.1+incompatible
8+
github.com/cenkalti/backoff/v5 v5.0.3
99
github.com/stretchr/testify v1.10.0
1010
)
1111

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
2-
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
1+
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
2+
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
33
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
44
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
55
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

options.go

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,37 @@ import (
55
)
66

77
type (
8-
// Options contains configuration parameters for resource testing behavior.
8+
// options contains configuration parameters for resource testing behavior.
99
// These options control retry intervals, maximum wait times, and the number
1010
// of attempts made when testing resource availability.
11-
Options struct {
12-
interval time.Duration // Initial retry interval between attempts
13-
maxInterval time.Duration // Maximum interval for exponential backoff
14-
attempts uint64 // Maximum number of retry attempts
11+
options struct {
12+
interval time.Duration // Initial retry interval between attempts
13+
maxInterval time.Duration // Maximum interval for exponential backoff
14+
attempts uint64 // Maximum number of retry attempts
15+
multiplier float64 // Multiplier for exponential backoff
16+
randomizationFactor float64 // Randomization factor for backoff intervals
1517
}
1618

17-
// Option is a function type used to configure Options through the functional
19+
// Option is a function type used to configure options through the functional
1820
// options pattern. This allows flexible and extensible configuration of
1921
// resource testing behavior.
20-
Option func(opts *Options)
22+
Option func(opts *options)
2123
)
2224

23-
// newOptions creates a new Options instance with default values and applies
25+
// newOptions creates a new options instance with default values and applies
2426
// the provided option setters. Default values are:
2527
// - interval: 5 seconds
26-
// - maxInterval: 60 seconds
28+
// - maxInterval: 60 seconds
2729
// - attempts: 5.
28-
func newOptions(setters []Option) *Options {
29-
opts := &Options{
30-
interval: time.Duration(5) * time.Second,
31-
maxInterval: time.Duration(60) * time.Second,
32-
attempts: 5,
30+
// - multiplier: 1.5
31+
// - randomizationFactor: 0.5
32+
func newOptions(setters []Option) *options {
33+
opts := &options{
34+
interval: time.Duration(5) * time.Second,
35+
maxInterval: time.Duration(60) * time.Second,
36+
attempts: 5,
37+
multiplier: 1.5,
38+
randomizationFactor: 0.5,
3339
}
3440

3541
for _, setter := range setters {
@@ -47,7 +53,7 @@ func newOptions(setters []Option) *Options {
4753
//
4854
// runner.Test(ctx, resources, waitfor.WithInterval(2)) // Start with 2 second intervals
4955
func WithInterval(interval uint64) Option {
50-
return func(opts *Options) {
56+
return func(opts *options) {
5157
opts.interval = time.Duration(interval) * time.Second
5258
}
5359
}
@@ -60,7 +66,7 @@ func WithInterval(interval uint64) Option {
6066
//
6167
// runner.Test(ctx, resources, waitfor.WithMaxInterval(30)) // Cap at 30 seconds
6268
func WithMaxInterval(interval uint64) Option {
63-
return func(opts *Options) {
69+
return func(opts *options) {
6470
opts.maxInterval = time.Duration(interval) * time.Second
6571
}
6672
}
@@ -73,7 +79,34 @@ func WithMaxInterval(interval uint64) Option {
7379
//
7480
// runner.Test(ctx, resources, waitfor.WithAttempts(10)) // Try up to 10 times
7581
func WithAttempts(attempts uint64) Option {
76-
return func(opts *Options) {
82+
return func(opts *options) {
7783
opts.attempts = attempts
7884
}
7985
}
86+
87+
// WithMultiplier creates an Option that sets the multiplier for exponential backoff.
88+
// This value determines how quickly the retry interval increases after each attempt.
89+
// A higher multiplier results in faster growth of the interval.
90+
//
91+
// Example:
92+
//
93+
// runner.Test(ctx, resources, waitfor.WithMultiplier(2.0)) // Double the interval each time
94+
func WithMultiplier(multiplier float64) Option {
95+
return func(opts *options) {
96+
opts.multiplier = multiplier
97+
}
98+
}
99+
100+
// WithRandomizationFactor creates an Option that sets the randomization factor for
101+
// exponential backoff. This factor introduces jitter to the retry intervals,
102+
// helping to prevent thundering herd problems when multiple clients are retrying
103+
// simultaneously.
104+
//
105+
// Example:
106+
//
107+
// runner.Test(ctx, resources, waitfor.WithRandomizationFactor(0.5)) // 50% jitter
108+
func WithRandomizationFactor(factor float64) Option {
109+
return func(opts *options) {
110+
opts.randomizationFactor = factor
111+
}
112+
}

options_test.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func TestNewOptions_WithSetters(t *testing.T) {
3131

3232
func TestWithInterval(t *testing.T) {
3333
option := WithInterval(30)
34-
opts := &Options{}
34+
opts := &options{}
3535

3636
option(opts)
3737

@@ -40,7 +40,7 @@ func TestWithInterval(t *testing.T) {
4040

4141
func TestWithMaxInterval(t *testing.T) {
4242
option := WithMaxInterval(90)
43-
opts := &Options{}
43+
opts := &options{}
4444

4545
option(opts)
4646

@@ -49,7 +49,7 @@ func TestWithMaxInterval(t *testing.T) {
4949

5050
func TestWithAttempts(t *testing.T) {
5151
option := WithAttempts(20)
52-
opts := &Options{}
52+
opts := &options{}
5353

5454
option(opts)
5555

@@ -67,3 +67,19 @@ func TestCombinedOptions(t *testing.T) {
6767
assert.Equal(t, time.Duration(30)*time.Second, opts.maxInterval)
6868
assert.Equal(t, uint64(8), opts.attempts)
6969
}
70+
71+
func TestWithMultiplier(t *testing.T) {
72+
opts := newOptions([]Option{
73+
WithMultiplier(2.5),
74+
})
75+
76+
assert.Equal(t, 2.5, opts.multiplier)
77+
}
78+
79+
func TestWithRandomizationFactor(t *testing.T) {
80+
opts := newOptions([]Option{
81+
WithRandomizationFactor(0.3),
82+
})
83+
84+
assert.Equal(t, 0.3, opts.randomizationFactor)
85+
}

registry.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package waitfor
22

33
import (
44
"context"
5-
"errors"
5+
"fmt"
66
"net/url"
77
"strings"
88
)
@@ -84,7 +84,7 @@ func (r *Registry) Register(scheme string, factory ResourceFactory) error {
8484
_, exists := r.resources[scheme]
8585

8686
if exists {
87-
return errors.New("resource is already registered with a given scheme:" + scheme)
87+
return fmt.Errorf("%w: %s", ErrResourceAlreadyRegistered, scheme)
8888
}
8989

9090
r.resources[scheme] = factory
@@ -113,7 +113,7 @@ func (r *Registry) Resolve(location string) (Resource, error) {
113113
rf, found := r.resources[u.Scheme]
114114

115115
if !found {
116-
return nil, errors.New("resource with a given scheme is not found:" + u.Scheme)
116+
return nil, fmt.Errorf("%w: %s", ErrResourceNotFound, u.Scheme)
117117
}
118118

119119
return rf(u)

waitfor.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import (
2929
"os/exec"
3030
"sync"
3131

32-
"github.com/cenkalti/backoff"
32+
"github.com/cenkalti/backoff/v5"
3333
)
3434

3535
type (
@@ -109,8 +109,10 @@ func (r *Runner) Run(ctx context.Context, program Program, setters ...Option) ([
109109
//
110110
// The setters parameter allows customization of retry behavior including:
111111
// - Initial retry interval (WithInterval)
112-
// - Maximum retry interval (WithMaxInterval)
112+
// - Maximum retry interval (WithMaxInterval)
113113
// - Number of retry attempts (WithAttempts)
114+
// - Multiplier for exponential backoff (WithMultiplier)
115+
// - Randomization factor for backoff intervals (WithRandomizationFactor)
114116
//
115117
// Example:
116118
//
@@ -142,7 +144,7 @@ func (r *Runner) Test(ctx context.Context, resources []string, setters ...Option
142144
// testAllInternal concurrently tests all provided resources and returns a channel
143145
// of errors. Each resource is tested in its own goroutine with the specified options.
144146
// The channel is closed when all tests complete.
145-
func (r *Runner) testAllInternal(ctx context.Context, resources []string, opts Options) <-chan error {
147+
func (r *Runner) testAllInternal(ctx context.Context, resources []string, opts options) <-chan error {
146148
var wg sync.WaitGroup
147149
wg.Add(len(resources))
148150

@@ -169,7 +171,7 @@ func (r *Runner) testAllInternal(ctx context.Context, resources []string, opts O
169171
// testInternal tests a single resource with retry logic using exponential backoff.
170172
// It resolves the resource from the registry and applies the configured retry
171173
// strategy until the resource test passes or max attempts are reached.
172-
func (r *Runner) testInternal(ctx context.Context, resource string, opts Options) error {
174+
func (r *Runner) testInternal(ctx context.Context, resource string, opts options) error {
173175
rsc, err := r.registry.Resolve(resource)
174176

175177
if err != nil {
@@ -179,8 +181,13 @@ func (r *Runner) testInternal(ctx context.Context, resource string, opts Options
179181
b := backoff.NewExponentialBackOff()
180182
b.InitialInterval = opts.interval
181183
b.MaxInterval = opts.maxInterval
184+
b.Multiplier = opts.multiplier
185+
b.RandomizationFactor = opts.randomizationFactor
182186

183-
return backoff.Retry(func() error {
184-
return rsc.Test(ctx)
185-
}, backoff.WithContext(backoff.WithMaxRetries(b, opts.attempts), ctx))
187+
_, err = backoff.Retry(ctx, func() (bool, error) {
188+
// The return value doesn't matter to us
189+
return false, rsc.Test(ctx)
190+
}, backoff.WithMaxTries(uint(opts.attempts)))
191+
192+
return err
186193
}

waitfor_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ func TestRunner_testInternal_ResolutionError(t *testing.T) {
212212
runner := New() // No resources registered
213213

214214
ctx := context.Background()
215-
opts := Options{
215+
opts := options{
216216
interval: time.Second,
217217
maxInterval: time.Minute,
218218
attempts: 1,
@@ -231,7 +231,7 @@ func TestRunner_testInternal_ResourceTestError(t *testing.T) {
231231
runner := New(config)
232232

233233
ctx := context.Background()
234-
opts := Options{
234+
opts := options{
235235
interval: 1 * time.Millisecond, // Very short for testing
236236
maxInterval: 2 * time.Millisecond,
237237
attempts: 1, // Only one attempt to avoid long test time
@@ -249,7 +249,7 @@ func TestRunner_testAllInternal(t *testing.T) {
249249
runner := New(config)
250250

251251
ctx := context.Background()
252-
opts := Options{
252+
opts := options{
253253
interval: time.Millisecond,
254254
maxInterval: time.Millisecond * 10,
255255
attempts: 1,
@@ -277,7 +277,7 @@ func TestRunner_testAllInternal_WithErrors(t *testing.T) {
277277
runner := New(config)
278278

279279
ctx := context.Background()
280-
opts := Options{
280+
opts := options{
281281
interval: time.Millisecond,
282282
maxInterval: time.Millisecond * 10,
283283
attempts: 1,

0 commit comments

Comments
 (0)