diff --git a/errors/retrier.go b/errors/retrier.go index 5e4e798..72ddc57 100644 --- a/errors/retrier.go +++ b/errors/retrier.go @@ -124,8 +124,21 @@ func (r Retryer[T, E]) Timeout(timeout time.Duration) Retryer[T, E] { return r } -// Do will execute the provided functions code and automatically retry using the provided retry function. +type Stats struct { + // Number of failed attempts that were retryable + NumAttemptsRetryable uint8 + // Number of failed attempts that were non retryable (but we retried anyway) + NumAttemptsNonRetryable uint8 +} + func (r Retryer[T, E]) Do(ctx context.Context, fn RetryableFn[T, E]) Result[T, E] { + ret, _ := r.DoWithStats(ctx, fn) + return ret +} + +// Do will execute the provided functions code and automatically retry using the provided retry function. +func (r Retryer[T, E]) DoWithStats(ctx context.Context, fn RetryableFn[T, E]) (Result[T, E], Stats) { + stats := Stats{} var attempt int remaining := r.maxAttempts for { @@ -141,7 +154,7 @@ func (r Retryer[T, E]) Do(ctx context.Context, fn RetryableFn[T, E]) Result[T, E err := result.Err() isRetryable := r.isRetryableFn(ctx, err) if !isRetryable && r.isEarlyReturnFn != nil && r.isEarlyReturnFn(ctx, err) { - return result + return result, stats } switch r.maxAttemptsMode { @@ -149,24 +162,33 @@ func (r Retryer[T, E]) Do(ctx context.Context, fn RetryableFn[T, E]) Result[T, E goto RETRY case MaxAttemptsNonRetryableReset: if isRetryable { + stats.NumAttemptsRetryable++ remaining = r.maxAttempts goto RETRY } else if remaining > 0 { + stats.NumAttemptsNonRetryable++ remaining-- } case MaxAttemptsNonRetryable: if isRetryable { + stats.NumAttemptsRetryable++ goto RETRY } else if remaining > 0 { + stats.NumAttemptsNonRetryable++ remaining-- } case MaxAttempts: + if isRetryable { + stats.NumAttemptsRetryable++ + } else { + stats.NumAttemptsNonRetryable++ + } if remaining > 0 { remaining-- } } if remaining == 0 { - return result + return result, stats } RETRY: @@ -174,6 +196,6 @@ func (r Retryer[T, E]) Do(ctx context.Context, fn RetryableFn[T, E]) Result[T, E attempt++ continue } - return result + return result, stats } } diff --git a/go.mod b/go.mod index 1ef3138..a7d684f 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,6 @@ go 1.18 require ( github.com/go-playground/assert/v2 v2.2.0 + github.com/go-playground/errors/v5 v5.4.0 github.com/go-playground/form/v4 v4.2.1 ) diff --git a/go.sum b/go.sum index 479f4d2..7fd3f12 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/errors/v5 v5.4.0 h1:BxBxwlRjuclYbRebE4ddrRrMK705lS2mHzHw7BDoDPA= +github.com/go-playground/errors/v5 v5.4.0/go.mod h1:6aVeVHsT36RNu/m/8AvGdPv8T2J/+KfVv6Su4VvBfpQ= github.com/go-playground/form/v4 v4.2.1 h1:HjdRDKO0fftVMU5epjPW2SOREcZ6/wLUzEobqUGJuPw= github.com/go-playground/form/v4 v4.2.1/go.mod h1:q1a2BY+AQUUzhl6xA/6hBetay6dEIhMHjgvJiGo6K7U= diff --git a/net/http/retrier.go b/net/http/retrier.go index c6913b9..0ec6e7c 100644 --- a/net/http/retrier.go +++ b/net/http/retrier.go @@ -5,12 +5,12 @@ package httpext import ( "context" - "errors" "io" "net/http" "strconv" "time" + "github.com/go-playground/errors/v5" bytesext "github.com/go-playground/pkg/v5/bytes" errorsext "github.com/go-playground/pkg/v5/errors" ioext "github.com/go-playground/pkg/v5/io" @@ -210,13 +210,13 @@ func (r Retryer) Timeout(timeout time.Duration) Retryer { // // NOTE: it is up to the caller to close the response body if a successful request is made. func (r Retryer) DoResponse(ctx context.Context, fn BuildRequestFn2, expectedResponseCodes ...int) Result[*http.Response, error] { - return errorsext.NewRetryer[*http.Response, error](). + result, stats := errorsext.NewRetryer[*http.Response, error](). IsRetryableFn(r.isRetryableFn). MaxAttempts(r.mode, r.maxAttempts). Backoff(r.backoffFn). Timeout(r.timeout). IsEarlyReturnFn(r.isEarlyReturnFn). - Do(ctx, func(ctx context.Context) Result[*http.Response, error] { + DoWithStats(ctx, func(ctx context.Context) Result[*http.Response, error] { req := fn(ctx) if req.IsErr() { return Err[*http.Response, error](req.Err()) @@ -246,6 +246,10 @@ func (r Retryer) DoResponse(ctx context.Context, fn BuildRequestFn2, expectedRes RETURN: return Ok[*http.Response, error](resp) }) + if result.IsErr() { + return Err[*http.Response, error](errors.Wrapf(result.Err(), "failed with %d retryable attempts, and %d non retryable attempts", stats.NumAttemptsRetryable, stats.NumAttemptsNonRetryable)) + } + return result } // Do will execute the provided functions code and automatically retry using the provided retry function decoding