Coder Social home page Coder Social logo

avast / retry-go Goto Github PK

View Code? Open in Web Editor NEW
2.2K 33.0 154.0 184 KB

Simple golang library for retry mechanism

Home Page: http://godoc.org/github.com/avast/retry-go

License: MIT License

Go 92.95% Makefile 7.05%
golang go retry retry-library hacktoberfest

retry-go's Introduction

retry

Release Software License GitHub Actions Go Report Card Go Reference codecov.io Sourcegraph

Simple library for retry mechanism

Slightly inspired by Try::Tiny::Retry

SYNOPSIS

HTTP GET with retry:

url := "http://example.com"
var body []byte

err := retry.Do(
	func() error {
		resp, err := http.Get(url)
		if err != nil {
			return err
		}
		defer resp.Body.Close()
		body, err = ioutil.ReadAll(resp.Body)
		if err != nil {
			return err
		}
		return nil
	},
)

if err != nil {
	// handle error
}

fmt.Println(string(body))

HTTP GET with retry with data:

url := "http://example.com"

body, err := retry.DoWithData(
	func() ([]byte, error) {
		resp, err := http.Get(url)
		if err != nil {
			return nil, err
		}
		defer resp.Body.Close()
		body, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			return nil, err
		}

		return body, nil
	},
)

if err != nil {
	// handle error
}

fmt.Println(string(body))

More examples

SEE ALSO

  • giantswarm/retry-go - slightly complicated interface.

  • sethgrid/pester - only http retry for http calls with retries and backoff

  • cenkalti/backoff - Go port of the exponential backoff algorithm from Google's HTTP Client Library for Java. Really complicated interface.

  • rafaeljesus/retry-go - looks good, slightly similar as this package, don't have 'simple' Retry method

  • matryer/try - very popular package, nonintuitive interface (for me)

BREAKING CHANGES

  • 4.0.0

    • infinity retry is possible by set Attempts(0) by PR #49
  • 3.0.0

  • 1.0.2 -> 2.0.0

  • 0.3.0 -> 1.0.0

    • retry.Retry function are changed to retry.Do function
    • retry.RetryCustom (OnRetry) and retry.RetryCustomWithOpts functions are now implement via functions produces Options (aka retry.OnRetry)

Usage

func BackOffDelay

func BackOffDelay(n uint, _ error, config *Config) time.Duration

BackOffDelay is a DelayType which increases delay between consecutive retries

func Do

func Do(retryableFunc RetryableFunc, opts ...Option) error

func DoWithData

func DoWithData[T any](retryableFunc RetryableFuncWithData[T], opts ...Option) (T, error)

func FixedDelay

func FixedDelay(_ uint, _ error, config *Config) time.Duration

FixedDelay is a DelayType which keeps delay the same through all iterations

func IsRecoverable

func IsRecoverable(err error) bool

IsRecoverable checks if error is an instance of unrecoverableError

func RandomDelay

func RandomDelay(_ uint, _ error, config *Config) time.Duration

RandomDelay is a DelayType which picks a random delay up to config.maxJitter

func Unrecoverable

func Unrecoverable(err error) error

Unrecoverable wraps an error in unrecoverableError struct

type Config

type Config struct {
}

type DelayTypeFunc

type DelayTypeFunc func(n uint, err error, config *Config) time.Duration

DelayTypeFunc is called to return the next delay to wait after the retriable function fails on err after n attempts.

func CombineDelay

func CombineDelay(delays ...DelayTypeFunc) DelayTypeFunc

CombineDelay is a DelayType the combines all of the specified delays into a new DelayTypeFunc

type Error

type Error []error

Error type represents list of errors in retry

func (Error) As

func (e Error) As(target interface{}) bool

func (Error) Error

func (e Error) Error() string

Error method return string representation of Error It is an implementation of error interface

func (Error) Is

func (e Error) Is(target error) bool

func (Error) Unwrap

func (e Error) Unwrap() error

Unwrap the last error for compatibility with errors.Unwrap(). When you need to unwrap all errors, you should use WrappedErrors() instead.

err := Do(
	func() error {
		return errors.New("original error")
	},
	Attempts(1),
)

fmt.Println(errors.Unwrap(err)) # "original error" is printed

Added in version 4.2.0.

func (Error) WrappedErrors

func (e Error) WrappedErrors() []error

WrappedErrors returns the list of errors that this Error is wrapping. It is an implementation of the errwrap.Wrapper interface in package errwrap so that retry.Error can be used with that library.

type OnRetryFunc

type OnRetryFunc func(attempt uint, err error)

Function signature of OnRetry function

type Option

type Option func(*Config)

Option represents an option for retry.

func Attempts

func Attempts(attempts uint) Option

Attempts set count of retry. Setting to 0 will retry until the retried function succeeds. default is 10

func AttemptsForError

func AttemptsForError(attempts uint, err error) Option

AttemptsForError sets count of retry in case execution results in given err Retries for the given err are also counted against total retries. The retry will stop if any of given retries is exhausted.

added in 4.3.0

func Context

func Context(ctx context.Context) Option

Context allow to set context of retry default are Background context

example of immediately cancellation (maybe it isn't the best example, but it describes behavior enough; I hope)

ctx, cancel := context.WithCancel(context.Background())
cancel()

retry.Do(
	func() error {
		...
	},
	retry.Context(ctx),
)

func Delay

func Delay(delay time.Duration) Option

Delay set delay between retry default is 100ms

func DelayType

func DelayType(delayType DelayTypeFunc) Option

DelayType set type of the delay between retries default is BackOff

func LastErrorOnly

func LastErrorOnly(lastErrorOnly bool) Option

return the direct last error that came from the retried function default is false (return wrapped errors with everything)

func MaxDelay

func MaxDelay(maxDelay time.Duration) Option

MaxDelay set maximum delay between retry does not apply by default

func MaxJitter

func MaxJitter(maxJitter time.Duration) Option

MaxJitter sets the maximum random Jitter between retries for RandomDelay

func OnRetry

func OnRetry(onRetry OnRetryFunc) Option

OnRetry function callback are called each retry

log each retry example:

retry.Do(
	func() error {
		return errors.New("some error")
	},
	retry.OnRetry(func(n uint, err error) {
		log.Printf("#%d: %s\n", n, err)
	}),
)

func RetryIf

func RetryIf(retryIf RetryIfFunc) Option

RetryIf controls whether a retry should be attempted after an error (assuming there are any retry attempts remaining)

skip retry if special error example:

retry.Do(
	func() error {
		return errors.New("special error")
	},
	retry.RetryIf(func(err error) bool {
		if err.Error() == "special error" {
			return false
		}
		return true
	})
)

By default RetryIf stops execution if the error is wrapped using retry.Unrecoverable, so above example may also be shortened to:

retry.Do(
	func() error {
		return retry.Unrecoverable(errors.New("special error"))
	}
)

func UntilSucceeded

func UntilSucceeded() Option

UntilSucceeded will retry until the retried function succeeds. Equivalent to setting Attempts(0).

func WithTimer

func WithTimer(t Timer) Option

WithTimer provides a way to swap out timer module implementations. This primarily is useful for mocking/testing, where you may not want to explicitly wait for a set duration for retries.

example of augmenting time.After with a print statement

type struct MyTimer {}

func (t *MyTimer) After(d time.Duration) <- chan time.Time {
    fmt.Print("Timer called!")
    return time.After(d)
}

retry.Do(
    func() error { ... },
	   retry.WithTimer(&MyTimer{})
)

func WrapContextErrorWithLastError

func WrapContextErrorWithLastError(wrapContextErrorWithLastError bool) Option

WrapContextErrorWithLastError allows the context error to be returned wrapped with the last error that the retried function returned. This is only applicable when Attempts is set to 0 to retry indefinitly and when using a context to cancel / timeout

default is false

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

retry.Do(
	func() error {
		...
	},
	retry.Context(ctx),
	retry.Attempts(0),
	retry.WrapContextErrorWithLastError(true),
)

type RetryIfFunc

type RetryIfFunc func(error) bool

Function signature of retry if function

type RetryableFunc

type RetryableFunc func() error

Function signature of retryable function

type RetryableFuncWithData

type RetryableFuncWithData[T any] func() (T, error)

Function signature of retryable function with data

type Timer

type Timer interface {
	After(time.Duration) <-chan time.Time
}

Timer represents the timer used to track time for a retry.

Contributing

Contributions are very much welcome.

Makefile

Makefile provides several handy rules, like README.md generator , setup for prepare build/dev environment, test, cover, etc...

Try make help for more information.

Before pull request

maybe you need make setup in order to setup environment

please try:

  • run tests (make test)
  • run linter (make lint)
  • if your IDE don't automaticaly do go fmt, run go fmt (make fmt)

README

README.md are generate from template .godocdown.tmpl and code documentation via godocdown.

Never edit README.md direct, because your change will be lost.

retry-go's People

Contributors

ajeetdsouza avatar alex-bechanko avatar amukherj avatar avinassh avatar c1moore avatar craigpangea avatar dependabot[bot] avatar dillonstreator avatar edigaryev avatar farmerchillax avatar fengtingyang avatar fetmar avatar fishy avatar hrily avatar jalaziz avatar jamieedge avatar jasei avatar keiichihirobe avatar lebauce avatar lizhiquan avatar mrtc0 avatar natenho avatar nickchenyx avatar odedva avatar ovaltzer avatar songkeys avatar spacewander avatar sranka avatar subvillion avatar willdot avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

retry-go's Issues

The number of passed in OnRetry is inconsistent

I also found a problem. When attempts is equal to 0, the number of times passed in onRetry is inconsistent with when attempts is not equal to 0.
Here is a minimal example๏ผš

  • when set Attempts != 0
func main() {
	retry.Do(
		func() error {
			return errors.New("some error")
		},
		retry.Attempts(3),
		retry.OnRetry(func(n uint, err error) {
			fmt.Println("retry OnRetry:", n, err)
		}),
	)
}

outputs:

retry OnRetry: 0 some error
retry OnRetry: 1 some error
retry OnRetry: 2 some error
  • when set Attempts = 0
func main() {
	retry.Do(
		func() error {
			return errors.New("some error")
		},
		retry.Attempts(0),
		retry.OnRetry(func(n uint, err error) {
			fmt.Println("retry OnRetry:", n, err)
		}),
	)
}
retry OnRetry: 1 some error
retry OnRetry: 2 some error
retry OnRetry: 3 some error
retry OnRetry: 4 some error
retry OnRetry: 5 some error
^Csignal: interrupt

Proposal: make default unit nanosecond

Its somewhat non-intuitive when setting a delay which takes a time.Duration that the units are in microseconds. Its easy to write code like this and think its valid.

       err = retry.Do(func() error {
               return foo()
       }, retry.Attempts(3), retry.Delay(time.Second))

failing test (timeout) on windows 1.20

gotestcover  -covermode=atomic -coverprofile=coverage.txt $(go list ./... | grep -v /vendor/) -run . -timeout=2m
ok  	github.com/avast/retry-go/v4/examples	5.933s	coverage: [no statements]
command [go test -cover -covermode atomic -coverprofile C:\Users\RUNNER~1\AppData\Local\Temp\gotestcover-175[9](https://github.com/avast/retry-go/actions/runs/5841605985/job/15841924098#step:6:10)463031 github.com/avast/retry-go/v4 -run . -timeout=2m]: exit status 1
--- FAIL: TestMaxDelay (0.25s)
    retry_test.go:279: 
        	Error Trace:	D:/a/retry-go/retry-go/retry_test.go:279
        	Error:      	"250.5436ms" is not less than "250ms"
        	Test:       	TestMaxDelay
        	Messages:   	5 times with maximum delay retry is longer than 250ms
1
2
FAIL
	github.com/avast/retry-go/v4	coverage: 96.3% of statements
FAIL	github.com/avast/retry-go/v4	[16](https://github.com/avast/retry-go/actions/runs/5841605985/job/15841924098#step:6:17).229s
FAIL
test failed
mingw32-make: *** [makefile:31: test_and_cover_report] Error 1

Provide a sensible way to retry indefinitely

Imagine a Go server that is dependent on some bootstrap data and there's little sense in continuing the execution unless that data is retrieved.

With all the perks this package gives (precise delay control, exponential backoff, context support) it would be nice to use it for this case, however, it's currently not possible to do this cleanly:

  • specifying retry.Attempts(0) does not work as one might expect: #41
  • specifying retry.Attempts(arbitrarily large number) is not semantically sound as we don't know the exact number of attempts in advance
  • specifying retry.Attempts(math.MaxUint32) is a bad idea due to:
    errorLog = make(Error, config.attempts)

It would be nice if retry.Attempts(0) could be used for this case, as retry.Attempts(0) is kinda useless in the current implementation: it always results in All attempts fail: ... error.

dynamic delay time

how to make a dynamic delay time for example for he first retry delay is 5 second and for second time delay be 10. I mean delay can increase after a retry

Proposal: pluggable backoff strategies

Exponential backoff is frequently useful, but sometimes you just want a fixed delay or a delay +/- some random jitter. Seems like you could just take an extra option like retry.Strategy(retry.Exponential) or retry.Jitter or retry.Constant or something.

Custom retry function?

I'd like to implement a custom retry function, one that will cap the delay time at some value vs. having it increase forever, as I'm currently trying to retry forever in a particular service. It doesn't look like that is possible as it would require being able to create a function that takes a parameter of type retry.config.

Is this not in the current model? I'm new to this package so I could easily be missing something.

Weird behavior for retry.Attempt(0)

When using retry.Attempt(0) for unlimited retries, it does not support RetryIf and AttemptsForError.

Also if the context is canceled, it doesn't return any of the errors that were reported during retries. I think it would be better if it at least reported the last error (and even when attempts is finite, it would be nice if it added the context cancelled error, so the user can know the context was cancelled).

It looks like this is done because the errorLog is preallocated, and there is no way to allocate an infinite array.

Retry a struct function

type Getter struct {
}

func (g *Getter) Get(url string) error {
	response, err := http.Get(url)
    if err != nil {
		fmt.Println("error")
		return err
    }
    defer response.Body.Close()

	if response.StatusCode != 200 {
		fmt.Println("error ! 200")
		return fmt.Errorf("not 200 got, response: %v\n", response)
	}

	fmt.Println("success")
	return nil
}

I want to retry Get(url string).

Please suggest if it is possible using this library.

Proposal: Exported default variables should be constants

When defining default values, it normally makes no sense to make them mutable. Global default values should either be defined as constants, or be well documented, if there are use cases to globally change the default behavior.

Maybe adding a function in config.go, that creates a new default instance and removing the const block in rety.go would be a better approach here?

func NewWithDefaults() *Config {
	return &Config{
		attempts:      uint(10),
		delay:         100 * time.Millisecond,
		maxJitter:     100 * time.Millisecond,
		onRetry:       func(n uint, err error) {},
		retryIf:       IsRecoverable,
		delayType:     CombineDelay(BackOffDelay, RandomDelay),
		lastErrorOnly: false,
		context:       context.Background(),
	}
}

unrecoverableError cannot be unwrapped with `errors.Is()`

See example (https://play.golang.org/p/SFwoFoqUi_h):

package main

import (
	"fmt"
	retry "github.com/avast/retry-go/v3"
	"github.com/pkg/errors"
)

var ErrTest = errors.New("test")

func main() {
	err := retry.Unrecoverable(ErrTest)
	err2 := fmt.Errorf("wrapping: %w",ErrTest)
	
	fmt.Println(errors.Is(err,ErrTest))
	fmt.Println(errors.Is(err2,ErrTest))
}

Outputs:

false
true

I think this just means that the errUnrecoverable needs to implement Unwrap(), which should be a pretty simple change of adding (see https://play.golang.org/p/LpiCrGbpEOD)

func (u unrecoverableError) Unwrap() error {
	return u.error
}

Shift to modules

The modules setup is now production ready. Do you have this in pipeline?

I will be happy to open a PR.

configuration for getting last error instead of all

Hi there,
I would find it beneficial if I could directly get the last error that was returned from the retry-able function - i will work on a pr of that, just wanted to here you out if this something that make sense to you...
implemented here:
#14

Getting the last retry error when cancelled by context

I have a lot of usages for retry-go (still 3.0.0) along the lines of "retry this until it succeeds or the context is done, returning the last encountered error if it didn't succeed."

I've seen the new features added in 4.x. Unfortunately, they don't match my specific use case, of course ๐Ÿ˜ƒ.

I have a workaround that looks like this:

var lastErr error
retryErr := retry.Do(
	func() error {
		lastErr = someRetriableStuff()
		return lastErr
	},
	retry.Context(ctx),
	retry.Attempts(math.MaxUint), // could probably be retry.Attempts(0) in 4.x?
	retry.LastErrorOnly(true),
)

// Prefer lastErr over retryErr: In case the context was cancelled or timed
// out, retryErr will just be the err of the context, but not the last
// encountered err of the retried function.
if retryErr != nil && lastErr != nil {
	return lastErr
}

return retryErr

I'm wondering if this is something that could be supported natively by retry-go, although I'm not sure how the API could look like, given that there are already quite some interactions between Context, Attempts and LastErrorOnly.

A very simple thing could be PreferContextErrors(false) with its default being true. This would be only effective if both Context and LastErrorOnly(true) are also given. Not a glorious solution, but kinda acceptable. WDYT?

OnRetry terminology is ambiguous

Hi and first thanks for this useful contribution for go ๐Ÿ™

From the OnRetry terminology, I expect OnRetry to happen between an failed try and the subsequent try. I would not expect it to happen just after the last try.
I would expect the current behavior from a function OnFailure or OnTryFailure (to distinguish from a failure), and I'm by the way basing on retry.Do return to know if the operation has not succeeded finally (ie exceeded).

I would understand if you want to keep the existing behavior to avoid breaking changes but I would at least add a boolean parameter like lastTry or Exceeded to be able to distinguish if a next try is really going to happen.
Without it, we have to use a wrapper or implement this behavior in our calling code, which should be agnostic to the "retry business" to my mind.

I may provide a PR for any of the suggested changes.

Thanks in advance!

retry.Attempts(0) causes retry to not return error on context timeout

Problem

When using retry.Option retry.Attempts(0), this changes retry's error handling behavior for a timed-out context. retry.Do returns a nil response instead of an error.

Tested versions with this issue:

  • v4.3.1
  • v4.3.2

Expected

When a context times out, retry.Do should terminate and return an error.

Actual

When using retry.Attempts(0) and a context times out, retry.Do returns with no error, implying success. When retry.Attempts is not set or is set to another value, retry.Do returns an error.

Workaround

Set retry.Attempts to a large number, e.g. retry.Attempts(99999999)

Test

func TestRetry_timeoutWithAttempts0(t *testing.T) {
	g := NewGomegaWithT(t)

	// Test 1, default retry.Attempts (=10)
	// Passes

	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()

	err := retry.Do(func() error {
		return errors.New("simulated error")
	}, retry.Context(ctx))
	g.Expect(err).To(HaveOccurred())

	ctx, cancel = context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()

	// Test 2, retry.Attempts(0)
	// Fails

	err = retry.Do(func() error {
		return errors.New("simulated error")
	}, retry.Context(ctx), retry.Attempts(0))
	g.Expect(err).To(HaveOccurred())
}

Delay does not seems to be tacken into account

Hi,
I have the following code:

var eventPublisher *eventpublisher.EventPublisher
	err = retry.Do(func() error {
		eventPublisher, err = eventpublisher.NewEventPublisher(amqpURL, *amqpEventsExchange)
		if err != nil {
			return err
		}
		return nil
	},
		retry.Delay(5000*time.Second),
		retry.Attempts(30),
	)

I can indeed see 30 retries in the log but they all happen in the same second. Any idea?

Thks,
Alex

How to get unwrapped error from retry?

In retry func, if I have error, i return errors.Wrapf(someErr, "someWrap text", some wrap values).
And retry func return me this type for error:
image

But I need extract someErr, from returned error.
Usually I do this by this code:

for errors.Unwrap(err) != nil {
		err = errors.Unwrap(err)
}

But with retry this method doesn't work.

Full code:

err = retry.Do(
		func() error {
			defer func() { sentCount++ }()
			responseBody, statusCode, err = h.Request(ctx, args.RequestParams)
			if err != nil {
				return errors.Wrapf(err, "fhttp.RequestWithRetry(args: %s)", utils.GetStructJSON(args)) // if last retry this need to unwrap err
			}

			if statusCode != args.ExpectedStatusCode {
				if statusCode == fasthttp.StatusUnauthorized {
					return ErrStatusUnauthorized
				}
				return errors.Wrapf(
					ErrNotExpectedStatusCode,
					"fhttp.RequestWithRetry, expeced %d, got %d statusCode; requestArgs: %s, responseBody: %s",
					args.ExpectedStatusCode,
					statusCode,
					utils.GetStructJSON(args),
					responseBody,
				)  // if last retry in this part of code need possibility unwrap to ErrNotExpectedStatusCode
			}
			return nil
		},
		retry.Delay(args.RetryDelay*time.Millisecond),
		retry.RetryIf(func(err error) bool {
			if ((errors.Is(err, fasthttp.ErrTimeout) ||
				errors.Is(err, fasthttp.ErrDialTimeout) ||
				errors.Is(err, fasthttp.ErrTLSHandshakeTimeout)) && sentCount >= 2) ||
				errors.Is(err, ErrStatusUnauthorized) {
				return false
			}
			return true
		}),
		retry.Attempts(args.RetryAttempts),
	)
        for errors.Unwrap(err) != nil { // do nothing 
		err = errors.Unwrap(err)
	}

Unable to depend on v3.1.0

Following the release of v3.1.0 you are unable to import the project when using Go modules.

Previously in the v3.0.0 release, the addition of +incompatible within the go.mod would allow importing as this project was not a go module.

However, now that the project contains a go.mod you are unable to use the +incompatible functionality to import the project.

See releasing-modules-v2-or-higher for the options on how to handle this.

Limit backoff delay

I think it will be nice to limit the backoff delay to a specific n in the sequence. For instance, when delay time is 30 minutes and n is 5, the delay time sequence (in hours) will be 0.5, 1, 2, 4, 8, 8, 8, 8, 8.... This way, when (as in the example with testing connection to a remote server) the remote server is down only for a short period of time, it could be detected rather quickly with the first few retries, but when the downtime of the server is long, we'd want to test the server every 8 hours but not more than that because if the server is down for a whole week for instance the delay could get way way too high. Hitting the server at constant intervals like 2 hours doesn't make sense either for when the downtime is short. Having a constant interval shorter than 2 hours will cause too many retry attempts with a server of long downtimes.

Edit: sure it's possible to call the retry twice, one with backoff delay and if fails to call it again with a constant delay of 8 hours. But it's more elegant just to limit that value instead of calling the retry function twice.

Edit 2: A closure function to set it is fine.

func delaySetter(base time.Duration, max uint) func(n uint, config *retry.Config) time.Duration {
  return func(n uint, config *retry.Config) time.Duration {
    if n < max {
      return base * (1 << n)
    }
    return base * (1 << max)
  }
}

calling it as retry.DelayType(delaySetter(30*time.Minute, 4))

Wrapping unrecoverable error makes it impossible to detect

When wrapping error already wrapped with retry.Unrevocerable() it is not going to be properly detected by retry.IsRecoverable().

Example:

package main

import (
	"errors"
	"fmt"

	"github.com/avast/retry-go/v4"
)

func main() {
     err := retry.Unrecoverable(errors.New("unrecoverable err"))
     errWr := fmt.Errorf("wrapping: %w", err)

     // This is going to be printed.
     if !retry.IsRecoverable(err) {
          fmt.Println("unrecoverable")
     }

     // This is not going to be printed.
     if !retry.IsRecoverable(errWr) {
          fmt.Println("wrapped unrecoverable")
     }
}

Suggestion - while checking if error is recoverable, continue to unwrap error if possible:

// IsRecoverable checks if error is an instance of `unrecoverableError`
func IsRecoverable(err error) bool {
	for {
		if _, isUnrecoverable := err.(unrecoverableError); isUnrecoverable {
			return false
		}

		if err = errors.Unwrap(err); err == nil {
			return true
		}
	}
}

Feedback: nice, but missing context and delay based on error

I like the way of the retry strategy design that allows easy composition out of the built-in primitives. I cannot, however, use this library for two reasons:

  1. It does not accept a standard go context, the retriable operation has to wait even though the surrounding context might be already canceled.
  2. It does not allow me to compute the next retry delay out of the operation error itself. I use a cloud database (InfluxDB) that is accessible over HTTP and my app is a client to the database. HTTP 429 is a standard mechanism that HTTP servers use to signalize to the clients the next possible time for the operation to be retried. I can't use it

As a solution with this library, I could possibly use a retry strategy from this library and combine it with another implementation from your links that supports go context.

How to solve the problem that http.request is closed after reading when using retry-go

When I use retry-go in http post, I will encounter "http: ContentLength=825 with Body length 0"
The query shows that it is because http.request will be closed after being read once, and it will be empty when the same request is used next time.

func (c *Client) PostJson() (*Response, error) {
	c.ContentType = CONTENT_TYPE_JSON

	req, err := c.NewRequest("POST")
	if err != nil {
		return nil, err
	}

	return c.Do(req)
}

func (c *Client) Do(req *http.Request) (*Response, error) {
	var (
		resp *http.Response
		err  error
	)
	if len(c.RetryOpt) > 0 {
		err = retry.Do(
			func() error {
				resp, err = c.Client.Do(req)
				if err != nil {
					return err
				}
				if resp.StatusCode != http.StatusOK {
					return fmt.Errorf("%d", resp.StatusCode)
				}
				return nil
			}, c.RetryOpt...)
	} else {
		resp, err = c.Client.Do(req)
	}

	if err != nil {
		return nil, err
	}
	return &Response{resp}, nil
}

Length of Error returned by Do does not reflect number of attempts

If an unrecoverable error is encountered before the final attempt, the length of the error returned by Do is equal to the maximum number of attempts rather than the actual number. This behaviour is demonstrated by the following program.

package main

import (
	"errors"
	"fmt"

	"github.com/avast/retry-go/v4"
)

func main() {
	i := 0

	err := retry.Do(
		func() error {
			err := fmt.Errorf("error %d", i)

			if i > 1 {
				err = retry.Unrecoverable(err)
			}

			i++
			return err
		},
	)

	fmt.Println(err)

	var e retry.Error
	if errors.As(err, &e) {
		fmt.Println(len(e))
	}
}

The following output is observed when using v4.3.1.

All attempts fail:
#1: error 0
#2: error 1
#3: error 2
10

The following output is expected instead, where the length of the slice is equal to the number of attempts.

All attempts fail:
#1: error 0
#2: error 1
#3: error 2
3

Add RetryIfValue option to retry on given values

Sometimes want to retry if the computed values does not satisfy certain conditions. For example, you are waiting for some change to be applied to a persisted entity, so you want to retry until the entity read from persistence satisfy certain condition.

Example:

func (h *Handler) waitForApproval(ctx context.Context, id ids.Id) (*Entity, error) {
	return retry.DoWithData(func() (*Entity, error) {
		return h.reader.FindById(ctx, id)
        }, retry.RetryIfData(func (e *Entity) bool {
		return e.Status != StatusApproved // retry if the Status is not approved
       })
}

New type and func:

type RetryIfDataFunc[T any] func(T) bool

func RetryIfData[T any](f RetryIfDataFunc[T]) Option { // Option[T] ??
    // ...
}

It is challenging to handle type for Options to ensure consistency.

If you consider this to be useful (for me it is), I can provide a PR.

Implement `Unwrap() []error` for Go 1.20 multi-error support

Go 1.20 released recently, and includes support for wrapping multiple errors.

If implemented, this could probably replace WrappedErrors(), but it would be a breaking change to return a different type from Unwrap() and would also change the result of errors.Unwrap() on a retry error (as mentioned in #65). I'm not sure what the best approach for compatibility is here but it would be nice to have an API that matches up with the standard library's approach to multi-error unwrapping.

WaitAndRetry

Super useful library,
I could not find a way to use wait and retry, I tried CombineDelay but no success.
is it possible to achieve similar functionality using retry-go?

Policy
  .Handle<SomeExceptionType>()
  .WaitAndRetry(new[]
  {
    TimeSpan.FromSeconds(1),
    TimeSpan.FromSeconds(2),
    TimeSpan.FromSeconds(3)
  }, (exception, timeSpan) => {
    // Add logic to be executed before each retry, such as logging    
  });

This means, you can configure delays after each attempt, first wait 1 second, then 2 second and etc...

Any plans for generic retry from 1.18?

I can implement it if you have any plan idea for it. This may reduce the amount of closure.

type retryableFunc[TRes any] func() (TRes, error)

val, res := retry.Do(......

Error.Unwrap unexpectedly returns nil when all attempts are not performed

If an unrecoverable error is encountered before the final attempt, the unwrapped value of the error returned by Do is nil. This behaviour is demonstrated by the following program.

package main

import (
	"errors"
	"fmt"

	"github.com/avast/retry-go/v4"
)

func main() {
	err := retry.Do(
		func() error {
			return retry.Unrecoverable(errors.New("some error"))
		},
	)

	fmt.Println(err)
	fmt.Println(errors.Unwrap(err))
}

The following output is observed when using v4.3.1.

All attempts fail:
#1: some error
<nil>

The following output is expected instead, where the last encountered error is returned.

All attempts fail:
#1: some error
some error

This is caused by Error.Unwrap not checking for nil values.

retry-go/retry.go

Lines 245 to 247 in affbf8f

func (e Error) Unwrap() error {
return e[len(e)-1]
}

`retry.Context` suppresses error attempt log

When using retry.Context that hits the deadline, the only error returned is context deadline exceeded, suppressing all errors from the attempts made.

Example

func main() {
	ctx, cancelCtx := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancelCtx()
	err := retry.Do(
		func() error {
			return errors.New("error")
		},
		retry.Context(ctx),
	)
	fmt.Println(err)
}

Actual Output

context deadline exceeded

Expected Output

All attempts fail:
#1: context deadline exceeded
#2: error
#3: error
#4: error

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.