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

add support for HTTP retries #436

Merged
merged 8 commits into from
Apr 6, 2020
Merged

Conversation

lionelvillard
Copy link
Contributor

Add support for retrying HTTP requests when a certain class of error occurs.

There is no tests yet. Opening the PR to get early feedback

@n3wscott

Copy link
Member

@n3wscott n3wscott left a comment

Choose a reason for hiding this comment

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

Yeah I think this is the right track. Nice work!! Can you add some tests maybe in the v2/testing area ? This is pretty hard to test isolated unless you know some tricks.

v2/protocol/http/protocol.go Outdated Show resolved Hide resolved
v2/protocol/http/protocol.go Outdated Show resolved Hide resolved
if e.Retries == 0 {
return e.Result.Error()
}
return fmt.Sprintf("%s (%dx)", e.Result.Error(), e.Retries)
Copy link
Member

Choose a reason for hiding this comment

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

This might do funky things with the ack wrapping so a test for this is needed

if e.Retries == 0 {
return e.Result.Error()
}
return fmt.Sprintf("%s (%dx)", e.Result.Error(), e.Retries)
Copy link
Member

Choose a reason for hiding this comment

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

This might do funky things with the ack wrapping so a test for this is needed

Copy link
Member

@slinkydeveloper slinkydeveloper left a comment

Choose a reason for hiding this comment

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

I like it! Just two points:

  • Since WithRetry is an HTTP specific feature, should we move it into protocol/http? @n3wscott wdyt?
  • I think Backoff should not live in types, since it's the module that contains the cloudevents spec types, not the data structure about the logic of the client. Probably a better place for Backoff is in the same module of WithRetry or in client module

protocol.Result

// Retries is the number of times the request was tried
Retries int
Copy link
Member

Choose a reason for hiding this comment

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

I think we want to return back the total time spent trying too if we do exp backoff, what do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

to make sure we are on the same page, this is the time spent trying excluding the succesful (if any) request, right. I'm pretty sure the answer is yes...

import "time"

// Backoff holds parameters applied to retries
type Backoff struct {
Copy link
Member

Choose a reason for hiding this comment

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

Rather than make a custom type, I would hide that detail from the user with a change to the WithBackoff method, I will comment there.

}

// RetryFrom looks in the given context and returns the backoff parameters if found, otherwise nil
func RetryFrom(ctx context.Context) *types.Backoff {
Copy link
Member

Choose a reason for hiding this comment

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

I know slinky recommended moving this to http but there are settings in other protocols that have similar settings, I feel like it is ok to keep it in the general spot here.

Copy link
Member

Choose a reason for hiding this comment

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

Ok i'm fine with that

var retryKey = retryKeyType{}

// WithRetry returns back a new context with the retry backoff parameters.
func WithRetry(ctx context.Context, backoff types.Backoff) context.Context {
Copy link
Member

Choose a reason for hiding this comment

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

We might want more than one strategy for backoff, this is linear and we might also want exponential backoff.

Also, rather than a custom type (or an internal custom type is ok) I would not expose that to the user. I recommend being explicit about the backoff and retries:

func WithLinearBackoff(ctx context.Context, period time.Duration, maxTries uint) context.Context {

is one way. I think internally we should make a type, it can be defined here in this file:

type BackoffConfig struct {
    Strategy string
    Period time.Duration
    MaxRetries int
}

And WithLinearBackoff makes a BackoffConfig{Strategy:"linear", ....

I suggest this to let the api self documenting and not require the caller to understand another object to make.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sound good. Fixing....

retries++

// Linear backoff.
time.Sleep(backoff.Delay)
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 make this method more of a wrapper for doOnce with a some kind of pattern of:

for { 
  select {
    case <-ctx.Done():
    // canceled.
    case <-timer.Tick():
      strongTypedErr: doOnce(ctx, request)
      ... custom logic to see if we should stop retry
  }
}

That way the logic of producing the protocol.NewReceipt is only one place, and you are still free to collect them in the outer retry method.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried but the reason I didn't call doOnce is because of if retry && !err.(*url.Error).Timeout() {. When doOnce returns an error, there is no way to unwrap the original error.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

which can be easily solved by changing doOnce signature.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ended up trying go 1.13 unwrap API. Not tested yet.

@@ -133,6 +145,64 @@ func (p *Protocol) Request(ctx context.Context, m binding.Message) (binding.Mess
return NewMessage(resp.Header, resp.Body), NewResult(resp.StatusCode, "%w", result)
}

func (p *Protocol) doWithRetry(backoff *types.Backoff) func(*http.Request) (binding.Message, error) {
Copy link
Member

Choose a reason for hiding this comment

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

I think this needs context.Context passed in to allow for the app to cancel in the case of shutdown or term signals.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good point! thanks!

@lionelvillard lionelvillard changed the title WIP: add support for HTTP retries add support for HTTP retries Apr 3, 2020
@lionelvillard
Copy link
Contributor Author

this is ready for a second round of review. I added some tests under v2/protocol/http. I didn't see v2/test. I'm happy to move the tests over there if needed.

@n3wscott @slinkydeveloper

Copy link
Member

@slinkydeveloper slinkydeveloper left a comment

Choose a reason for hiding this comment

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

Can you move Backoff outside types? I think the proper place is content module

@slinkydeveloper slinkydeveloper added this to the SDK v2 milestone Apr 6, 2020
@slinkydeveloper slinkydeveloper linked an issue Apr 6, 2020 that may be closed by this pull request
@lionelvillard
Copy link
Contributor Author

@slinkydeveloper there is no content package. Do you mean context?

@slinkydeveloper
Copy link
Member

yep, context package

Copy link
Member

@slinkydeveloper slinkydeveloper left a comment

Choose a reason for hiding this comment

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

LGTM, waiting for scott review

v2/context/context.go Outdated Show resolved Hide resolved
// Opaque key type used to store linear backoff parameters
type linearBackoffKeyType struct{}

var linearBackoffKey = linearBackoffKeyType{}
Copy link
Member

Choose a reason for hiding this comment

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

this should just be a backoffKey

Copy link
Contributor Author

Choose a reason for hiding this comment

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

using retriesKey.

@@ -118,6 +118,16 @@ func (p *Protocol) Request(ctx context.Context, m binding.Message) (binding.Mess
if err = WriteRequest(ctx, m, req, p.transformers); err != nil {
return nil, err
}

do := p.doOnce
if delay, tries := cecontext.LinearBackoffFrom(ctx); tries > 0 {
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 change this method to BackoffFrom and return the Backoff struct, then have a switch for BackoffStrategyLinear or BackoffStrategyNone

v2/context/context.go Outdated Show resolved Hide resolved
v2/context/backoff.go Outdated Show resolved Hide resolved
Signed-off-by: Lionel Villard <villard@us.ibm.com>
Signed-off-by: Lionel Villard <villard@us.ibm.com>
Signed-off-by: Lionel Villard <villard@us.ibm.com>
Signed-off-by: Lionel Villard <villard@us.ibm.com>
Signed-off-by: Lionel Villard <villard@us.ibm.com>
Signed-off-by: Lionel Villard <villard@us.ibm.com>
Signed-off-by: Lionel Villard <villard@us.ibm.com>
Signed-off-by: Lionel Villard <villard@us.ibm.com>
@n3wscott
Copy link
Member

n3wscott commented Apr 6, 2020

LGTM!!

Thanks, this is gonna be awesome!!

@n3wscott n3wscott merged commit abe3124 into cloudevents:master Apr 6, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support for retries
3 participants