Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce retry looping. #5

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open

Introduce retry looping. #5

wants to merge 1 commit into from

Conversation

howbazaar
Copy link
Contributor

After listening to users of the retry Call method, it became clear that it isn't always useful to just have a single func to call.

This introduces retry.Loop which provides a mechanism to use inside for loops more naturally.

Copy link
Member

@jameinel jameinel left a comment

Choose a reason for hiding this comment

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

Having loop.Error() solves one of my big concerns with rogpeppe's implemention, as exiting the loop early is clearly flagged as an Error condition (DurationExceeded, etc).
Which means that you know with a
if loop.Error() != nil {
}
outside of the loop whether it exited because it ran out of retries, or it exited because it was successful.
Passing "err" into the Next() function feels weird, but it is the piece that enables this. AFAICT

// Delay specifies how long to wait between retries.
Delay time.Duration

// MaxDelay specifies how longest time to wait between retries. If no
Copy link
Member

Choose a reason for hiding this comment

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

"specifies the longest time"

// `retry.DoubleDelay` can be used that will provide an exponential
// backoff. The first time this function is called attempt is 1, the
// second time, attempt is 2 and so on.
BackoffFunc func(delay time.Duration, attempt int) time.Duration
Copy link
Member

Choose a reason for hiding this comment

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

Can you also comment about what 'delay' is being passed in? From the "BackoffFactor" function, it looks to be the time of the last delay. Else you would do delay * pow(time.Duration(factor), attempt)

But that doesn't seem to be clearly documented here.
I could go with either "base delay" or "last delay" but we should be clear which is used.

// BackoffFactor returns a new LoopSpec with the backoff function set to
// scale the backoff each time by the factor specified. This is an example
// of the syntactic sugar that could be applied to the spec structures.
func (spec LoopSpec) BackoffFactor(factor int) LoopSpec {
Copy link
Member

Choose a reason for hiding this comment

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

I would have expected factor to actually be a float, as then you can do things like "50% longer each cycle" (factor =1.5)

return errors.NotValidf("missing Clock")
}
// One of Attempts or MaxDuration need to be specified
if args.Attempts == 0 && args.MaxDuration == 0 && args.Stop == nil {
Copy link
Member

Choose a reason for hiding this comment

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

Given your earlier wording, isn't Attempts <= 0 considered disabled?

close(stop)

s.spec.Stop = stop
var err error
Copy link
Member

Choose a reason for hiding this comment

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

you should initialize 'err' to something other than 'nil' else 'err = success()' isn't really evaluated. I suppose it is covered by called = true, but it seems odd. You could actually avoid the extra variable with just
err := &ErrNotCalled{}
or something along those lines.

start := args.Clock.Now()
for i := 1; args.Attempts <= 0 || i <= args.Attempts; i++ {

spec := LoopSpec{
Copy link
Member

Choose a reason for hiding this comment

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

I'm very happy to see one form implemented with the other, as then you help ensure they evolve together.

@stolowski
Copy link

My 2 cents as somebody who just recently started using juju/retry in snapd:
I think this is an improvement over the existing implementation, however I'm in favor of rogpeppe's PR, as this implementation still revolves around errors to drive retry mechanism, so it doesn't solve the gripes I have with current implementation.

The main issue is there are cases where e.g. a HTTP request doesn't technically fail (err is nil), but I still want to retry based on http response code, e.g. on 500 or 503. To handle this case in existing implementation or the one from this PR I'm forced to introduce my own error type just to satisfy the needs of retry loop handling.

The other "oddity" is the LastError method which - as is - packs the error I'm most interested into a "unexpected error type: ..." string, removing the ability to inspect actual error type; to avoid this I need to implement NotifyFunc handler just to keep track of my error.

Perhaps this PR and rogpeppe's could be combined?

Copy link

@mjs mjs left a comment

Choose a reason for hiding this comment

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

I think this is quite good but I do wonder (as per other reviewers) whether always tying retries to errors is really the best approach. Certainly this will be a common use case (and there should be helpers to do that easily) but it perhaps shouldn't be the only way.

It might be good to compare to Roger's work and see what can be incorporated from that.

c.Assert(s.clock.delays, gc.HasLen, 3)
}

func (s *loopSuite) TestMulitipleLoops(c *gc.C) {
Copy link

Choose a reason for hiding this comment

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

typo

@@ -163,8 +163,8 @@ func (args *CallArgs) Validate() error {
return errors.NotValidf("missing Clock")
}
// One of Attempts or MaxDuration need to be specified
if args.Attempts == 0 && args.MaxDuration == 0 {
return errors.NotValidf("missing Attempts or MaxDuration")
if args.Attempts == 0 && args.MaxDuration == 0 && args.Stop == nil {
Copy link

Choose a reason for hiding this comment

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

This is nice. It's important that at least one way of stopping the retries is in place.


func (s *loopSuite) TestCalledOnceEvenIfStopped(c *gc.C) {
stop := make(chan struct{})
called := false
Copy link

Choose a reason for hiding this comment

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

move this closer to the for statement

c.Assert(s.clock.delays, gc.HasLen, 0)
}

func (s *loopSuite) TestCalledOnceEvenIfStopped(c *gc.C) {
Copy link

Choose a reason for hiding this comment

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

do you actually want this behaviour? I would have thought that if the stop channel is closed it's game over, you don't want the loop at all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants