Skip to content
This repository has been archived by the owner on Feb 13, 2024. It is now read-only.

Improve Retry Behaviour. #146

Merged
merged 1 commit into from
Jan 15, 2018
Merged

Improve Retry Behaviour. #146

merged 1 commit into from
Jan 15, 2018

Conversation

f2prateek
Copy link
Contributor

@f2prateek f2prateek commented Jan 11, 2018

Previously we were only retrying network errors or a 5xx error on an idempotent request (GET, HEAD, OPTIONS, PUT or DELETE).

The latter doesn't really guard/apply to us since we use POST for uploading messages. Our POST requests are idempotent since we send messages with a unique message ID that is guarded by our dedupe layer (https://segment.com/blog/exactly-once-delivery/).

For us, our retry policy should be (as per https://paper.dropbox.com/doc/analytics-foo-library-guidelines-2trBhLKQD4Soz4VatvuGR#:uid=189560039280582553736180&h2=Handling-Network-Errors):

  1. Retry network errors (socket timed out, etc.)
  2. Retry server errors (HTTP 5xx)
  3. Retry HTTP 429.

@f2prateek
Copy link
Contributor Author

hey @vadimdemedes, @stephenmathieson; wanted to get your thoughts on this. I think the behaviour is functionally correct, but not sure on how to test it. https://github.com/softonic/axios-retry doesn't seem to have tests. My initial idea was to test the custom retry function directly (something simple like t.true(retryCondition({ response: { status: 429 }}))) but wasn't sure how to access access a private function in the test.

index.js Outdated
@@ -249,4 +252,26 @@ class Analytics {
}
}

var retryCondition = function (error) {

Choose a reason for hiding this comment

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

Should be an arrow function + const instead of var.

Copy link
Contributor

Choose a reason for hiding this comment

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

I also might suggest adding this method to the Analytics class so we can more easily write tests for it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

what's an arrow function?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated to be const.

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 also might suggest adding this method to the Analytics class so we can more easily write tests for it.

Why would that be easier? This seems like a stateless function that can be tested without depending on the analytics class.

Also per my understanding, adding it to the analytics class will automatically expose this function to the public API.

Copy link
Contributor

Choose a reason for hiding this comment

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

In order to test it directly, we'd have to export it. Attaching it directly to exports (or module.exports) make it look like part of the public API. If we add it as a method of the class, it will be exposed, but won't look like it's part of the public API.

Consider the following:

// exporting directly
exports.somePrivateMethod = () => {}

// exporting as part of the class
module.exports = class Analytics {
  track () {}
  somePrivateMethod () {}
}

IMO the first looks like it's exposed for consumption by the end user, while the second just looks like a regular class.

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! Makes sense about testing, chatted with Roland and he suggested using _ as a prefix to denote it as a private, so I'll do that.

index.js Outdated
// Retry if rate limited.
if (error.response.status === 429) {
return true
}

Choose a reason for hiding this comment

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

This function should return false at the end.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated.

index.js Outdated
@@ -249,4 +252,28 @@ class Analytics {
}
}

const retryCondition = function (error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

const retryCondition = 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.

thanks! roland just explained what this was to me :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Roland actually suggested not assigning and using it as a function variable at all, which makes sense to me.

function _retryCondition(error) {

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't follow, but either way, we need to expose this logic somehow in order to test it.

Choose a reason for hiding this comment

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

Roland actually suggested not assigning and using it as a function variable at all, which makes sense to me.

Why not?

Copy link

Choose a reason for hiding this comment

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

To me function _retryCondition(error) { looks cleaner than const _retryCondition = error => {. 🤷‍♂️

Copy link
Contributor

Choose a reason for hiding this comment

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

🚲 🏠

@f2prateek
Copy link
Contributor Author

reminder to myself, don't merge without tests!

@f2prateek
Copy link
Contributor Author

f2prateek commented Jan 11, 2018

Ok, should be ready for review now! Added tests and made some changes from the original approach:

  • function moved to Analytics class for testing
  • function named with _ prefix to denote this is not meant to be part of the public API.

@Rowno showed me how I could make the function static too. It wasn't clear if that was fully compatible with what we support today and if it cause issues, so I left it as a simple class function for now.

index.js Outdated
@@ -247,6 +250,30 @@ class Analytics {
done(err)
})
}

_retryCondition (error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Might rename this shouldRetry or isErrorRetryable for clarity.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renamed to isErrorRetryable

Copy link
Contributor

Choose a reason for hiding this comment

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

I think you might have forgotten to push?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops! Pushed now.

test.js Outdated
t.false(client._retryCondition({ code: 'ECONNABORTED' }))

t.true(client._retryCondition({ response: { status: 500 } }))
t.true(client._retryCondition({ response: { status: 429 } }))
Copy link
Contributor

Choose a reason for hiding this comment

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

We should add one last test to verify the default return false behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added one, though it's pretty contrived; we'd never really expect to see and error for a 2xx response.

@f2prateek f2prateek force-pushed the retries branch 2 times, most recently from c48865a to b4dc6ad Compare January 12, 2018 19:32
Previously we were only retrying network errors or a 5xx error on an idempotent request (GET, HEAD, OPTIONS, PUT or DELETE).

The latter doesn't really guard/apply to us since we use POST for uploading messages. Our POST requests are effectively idempotent since we send messages with a unique message ID that is guarded by our dedupe layer (https://segment.com/blog/exactly-once-delivery/).

For us, our retry policy should be (as per https://paper.dropbox.com/doc/analytics-foo-library-guidelines-2trBhLKQD4Soz4VatvuGR#:uid=189560039280582553736180&h2=Handling-Network-Errors):

1. Retry network errors (socket timed out, etc.)
2. Retry server errors (HTTP 5xx)
3. Retry HTTP 429.
Copy link
Contributor

@stephenmathieson stephenmathieson left a comment

Choose a reason for hiding this comment

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

LGTM!

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

Successfully merging this pull request may close these issues.

4 participants