Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,8 @@ func main() {

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

### Error Handling Patterns

Expand Down
12 changes: 8 additions & 4 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ package waitfor

import "errors"

// ErrWait is returned when resource availability testing fails.
// This error indicates that one or more resources did not become
// available within the configured timeout and retry parameters.
var (
ErrWait = errors.New("failed to wait for resource availability")
// ErrWait is returned when resource availability testing fails.
// This error indicates that one or more resources did not become
// available within the configured timeout and retry parameters.
ErrWait = errors.New("failed to wait for resource availability")
// ErrInvalidArgument is returned when invalid arguments are passed
// to functions, such as empty resource URLs or invalid configuration.
ErrInvalidArgument = errors.New("invalid argument")
// ErrResourceAlreadyRegistered is returned when a resource factory is already registered for a scheme.
ErrResourceAlreadyRegistered = errors.New("resource is already registered with a given scheme")
// ErrResourceNotFound is returned when no resource factory is found for a scheme.
ErrResourceNotFound = errors.New("resource with a given scheme is not found")
)
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.23.0
toolchain go1.24.6

require (
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/cenkalti/backoff/v5 v5.0.3
github.com/stretchr/testify v1.10.0
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down
67 changes: 50 additions & 17 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,37 @@ import (
)

type (
// Options contains configuration parameters for resource testing behavior.
// options contains configuration parameters for resource testing behavior.
// These options control retry intervals, maximum wait times, and the number
// of attempts made when testing resource availability.
Options struct {
interval time.Duration // Initial retry interval between attempts
maxInterval time.Duration // Maximum interval for exponential backoff
attempts uint64 // Maximum number of retry attempts
options struct {
interval time.Duration // Initial retry interval between attempts
maxInterval time.Duration // Maximum interval for exponential backoff
attempts uint64 // Maximum number of retry attempts
multiplier float64 // Multiplier for exponential backoff
randomizationFactor float64 // Randomization factor for backoff intervals
}

// Option is a function type used to configure Options through the functional
// Option is a function type used to configure options through the functional
// options pattern. This allows flexible and extensible configuration of
// resource testing behavior.
Option func(opts *Options)
Option func(opts *options)
)

// newOptions creates a new Options instance with default values and applies
// newOptions creates a new options instance with default values and applies
// the provided option setters. Default values are:
// - interval: 5 seconds
// - maxInterval: 60 seconds
// - maxInterval: 60 seconds
// - attempts: 5.
func newOptions(setters []Option) *Options {
opts := &Options{
interval: time.Duration(5) * time.Second,
maxInterval: time.Duration(60) * time.Second,
attempts: 5,
// - multiplier: 1.5
// - randomizationFactor: 0.5
func newOptions(setters []Option) *options {
opts := &options{
interval: time.Duration(5) * time.Second,
maxInterval: time.Duration(60) * time.Second,
attempts: 5,
multiplier: 1.5,
randomizationFactor: 0.5,
}

for _, setter := range setters {
Expand All @@ -47,7 +53,7 @@ func newOptions(setters []Option) *Options {
//
// runner.Test(ctx, resources, waitfor.WithInterval(2)) // Start with 2 second intervals
func WithInterval(interval uint64) Option {
return func(opts *Options) {
return func(opts *options) {
opts.interval = time.Duration(interval) * time.Second
}
}
Expand All @@ -60,7 +66,7 @@ func WithInterval(interval uint64) Option {
//
// runner.Test(ctx, resources, waitfor.WithMaxInterval(30)) // Cap at 30 seconds
func WithMaxInterval(interval uint64) Option {
return func(opts *Options) {
return func(opts *options) {
opts.maxInterval = time.Duration(interval) * time.Second
}
}
Expand All @@ -73,7 +79,34 @@ func WithMaxInterval(interval uint64) Option {
//
// runner.Test(ctx, resources, waitfor.WithAttempts(10)) // Try up to 10 times
func WithAttempts(attempts uint64) Option {
return func(opts *Options) {
return func(opts *options) {
opts.attempts = attempts
}
}

// WithMultiplier creates an Option that sets the multiplier for exponential backoff.
// This value determines how quickly the retry interval increases after each attempt.
// A higher multiplier results in faster growth of the interval.
//
// Example:
//
// runner.Test(ctx, resources, waitfor.WithMultiplier(2.0)) // Double the interval each time
func WithMultiplier(multiplier float64) Option {
return func(opts *options) {
opts.multiplier = multiplier
}
}

// WithRandomizationFactor creates an Option that sets the randomization factor for
// exponential backoff. This factor introduces jitter to the retry intervals,
// helping to prevent thundering herd problems when multiple clients are retrying
// simultaneously.
Copy link

Copilot AI Sep 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function documentation is incomplete. It's missing an example usage comment similar to other option functions in this file.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot complete the documentation.

//
// Example:
//
// runner.Test(ctx, resources, waitfor.WithRandomizationFactor(0.5)) // 50% jitter
func WithRandomizationFactor(factor float64) Option {
return func(opts *options) {
opts.randomizationFactor = factor
}
}
22 changes: 19 additions & 3 deletions options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func TestNewOptions_WithSetters(t *testing.T) {

func TestWithInterval(t *testing.T) {
option := WithInterval(30)
opts := &Options{}
opts := &options{}

option(opts)

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

func TestWithMaxInterval(t *testing.T) {
option := WithMaxInterval(90)
opts := &Options{}
opts := &options{}

option(opts)

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

func TestWithAttempts(t *testing.T) {
option := WithAttempts(20)
opts := &Options{}
opts := &options{}

option(opts)

Expand All @@ -67,3 +67,19 @@ func TestCombinedOptions(t *testing.T) {
assert.Equal(t, time.Duration(30)*time.Second, opts.maxInterval)
assert.Equal(t, uint64(8), opts.attempts)
}

func TestWithMultiplier(t *testing.T) {
opts := newOptions([]Option{
WithMultiplier(2.5),
})

assert.Equal(t, 2.5, opts.multiplier)
}

func TestWithRandomizationFactor(t *testing.T) {
opts := newOptions([]Option{
WithRandomizationFactor(0.3),
})

assert.Equal(t, 0.3, opts.randomizationFactor)
}
6 changes: 3 additions & 3 deletions registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package waitfor

import (
"context"
"errors"
"fmt"
"net/url"
"strings"
)
Expand Down Expand Up @@ -84,7 +84,7 @@ func (r *Registry) Register(scheme string, factory ResourceFactory) error {
_, exists := r.resources[scheme]

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

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

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

return rf(u)
Expand Down
21 changes: 14 additions & 7 deletions waitfor.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import (
"os/exec"
"sync"

"github.com/cenkalti/backoff"
"github.com/cenkalti/backoff/v5"
)

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

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

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

return backoff.Retry(func() error {
return rsc.Test(ctx)
}, backoff.WithContext(backoff.WithMaxRetries(b, opts.attempts), ctx))
_, err = backoff.Retry(ctx, func() (bool, error) {
// The return value doesn't matter to us
return false, rsc.Test(ctx)
}, backoff.WithMaxTries(uint(opts.attempts)))

return err
}
8 changes: 4 additions & 4 deletions waitfor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ func TestRunner_testInternal_ResolutionError(t *testing.T) {
runner := New() // No resources registered

ctx := context.Background()
opts := Options{
opts := options{
interval: time.Second,
maxInterval: time.Minute,
attempts: 1,
Expand All @@ -231,7 +231,7 @@ func TestRunner_testInternal_ResourceTestError(t *testing.T) {
runner := New(config)

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

ctx := context.Background()
opts := Options{
opts := options{
interval: time.Millisecond,
maxInterval: time.Millisecond * 10,
attempts: 1,
Expand Down Expand Up @@ -277,7 +277,7 @@ func TestRunner_testAllInternal_WithErrors(t *testing.T) {
runner := New(config)

ctx := context.Background()
opts := Options{
opts := options{
interval: time.Millisecond,
maxInterval: time.Millisecond * 10,
attempts: 1,
Expand Down
Loading