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

Optionally retry requests #78

Merged
merged 5 commits into from
Dec 13, 2016
Merged

Optionally retry requests #78

merged 5 commits into from
Dec 13, 2016

Conversation

pimterry
Copy link
Contributor

Adds a 'retries' option you can pass to send(), which sets the number of times to retry this request (defaulting to 0).

This is part of the fix for balena-io/balena-sdk#237.

@emirotin
Copy link
Contributor

Please do git rebase against the master as I've merged my PR and published the new version just now.

Copy link
Contributor

@emirotin emirotin left a comment

Choose a reason for hiding this comment

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

Nice job! Please wait for Juan to approve as well

Copy link
Contributor

@jviotti jviotti left a comment

Choose a reason for hiding this comment

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

Very nice! Thanks a lot for the tests :)

@pimterry
Copy link
Contributor Author

Rebased.

Also, whoops, in starting to put this to use against Resin-SDK I've realised it needs to go a little further. There's an extra change here now that lets you set a default retries in the initial getRequest call, so we can inject the option once in the tests, and have it set the default behaviour for all future send calls from that request instance.

@pimterry pimterry requested review from emirotin and jviotti December 13, 2016 15:38
@pimterry pimterry merged commit ef28633 into master Dec 13, 2016
@pimterry pimterry deleted the retry branch December 13, 2016 17:10
@@ -292,10 +293,12 @@ timeoutPromise = (ms, promise) ->
# utils.requestAsync({ url: 'http://example.com' }).then (response) ->
# console.log(response)
###
exports.requestAsync = (options) ->
exports.requestAsync = (options, retriesRemaining = undefined) ->
Copy link
Contributor

Choose a reason for hiding this comment

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

This retriesRemaining = undefined is redundant, it will only be set to undefined if it is already nullish, but then retriesRemaining ?= opts.retries will also set it if it's nullish, making it redundant to explicitly change nulls into undefineds

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, totally agree it doesn't directly do anything. It's there really just to explicitly say upfront that this is an optional argument, and afaik this the easiest way to show that in CS. Personally I quite like it, but it might be more of a TS style; happy to change it if that's not useful/how we do things here.

Copy link
Contributor

Choose a reason for hiding this comment

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

I personally really dislike it as it makes me think there is some special reason it needs to be undefined rather than just nullish, so I'm watching out for that in the code and it adds processing overhead from that. I'd rather just have it in the jsdoc due to that, also for typescript I would write it as retriesRemaining?: number (or retriesRemaining: number | null, I'm not sure which I prefer yet) rather than retriesRemaining: number | null = undefined

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, that makes sense, I'll move it to the JSDoc.

To briefly dive on the TypeScript diversion (with maybe unwarranted enthusiasm): those two options aren't equivalent in TS, so it's not just style.

Firstly, because x?: number has the same type within the function as x: number | undefined, not x: number | null. retriesRemaining: number | null = undefined isn't valid (assuming strictNullChecks)

Secondly, because the ? doesn't just change the type of the argument inside the function, it also makes the argument optional. With x: number | undefined you can explicitly pass undefined, but you can't not pass any argument at all. With x?: number you can pass undefined implicitly, and I think that's probably the idiomatic option.

Playground example

@@ -306,6 +309,12 @@ exports.requestAsync = (options) ->
headers: options.headers
uri: urlLib.parse(url)
return response
.catch (error) ->
Copy link
Contributor

Choose a reason for hiding this comment

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

This can be

.catch (-> retriesRemaining > 0), ->
	exports.requestAsync(options, retriesRemaining - 1)

or

p = p.then ...
if retriesRemaining > 0
	p = p.catch ->
		exports.requestAsync(options, retriesRemaining - 1)
return p

to avoid needing to manually rethrow

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you want to pull it out like that we can just implicitly return a nice ternary, rather than mutating p:

...

if retriesRemaining > 0 then p.catch ->
    exports.requestAsync(options, retriesRemaining - 1)
else p

Thoughts? Otherwise I'd go for your 2nd option myself - the first for me is a little tricky to parse without having read much CS before. I'm still getting to grips with CS style though.

Copy link
Contributor

Choose a reason for hiding this comment

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

I prefer the mutating style in that I read it more easily along the lines of "in this case I add an error handler, and in all cases I return this promise", rather than "in this case I add an error handler and return the promise, and in the other case I return the promise" - albeit they're equivalent in function and I would be ok with the ternary version.

As for the first one I suggested, in that style it's a bit ugly for sure so I'd go with one of the others, especially as it's not needed in this case at all because the predicate result is always known in advace. However the way to make it a nicer would be to separate out the predicate function and have it named instead, ie:

shouldRetry = -> retriesRemaining > 0
...
.catch shouldRetry, ->
	exports.requestAsync(options, retriesRemaining - 1)


beforeEach ->
requestsSeen = 0
fetchMock.get 'https://example.com/initially-failing', ->
Copy link
Contributor

Choose a reason for hiding this comment

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

You could use Promise.method to do:

fetchMock.get 'https://example.com/initially-failing', Promise.method ->
	requestsSeen += 1
	if requestsSeen <= 2
		throw new Error('low-level network error')
	else
		return {
			body: result: 'success'
			headers:
				'Content-Type': 'application/json'
		}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Definitely better, done.

I've done this as tiny patch I've pushed straight to master, referencing this issue. I assume that's the right approach for small tweaks like this when the PRs already merged - let me know there's a different process I should be following.

Copy link
Contributor

Choose a reason for hiding this comment

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

Generally even for smaller tweaks we'll open up a PR, that way it will still get some eyes on it in case some small mistake was added by accident - after all it can happen to any of us, especially as the small fixes also tend to be the ones that get paid the least attention to. Actually for a lot of our repos it's impossible to push directly to master and changes have to go via a PR and won't let you merge until the tests have passed as well, it'd be good to set that up on even more repos imo

@emirotin
Copy link
Contributor

emirotin commented Dec 14, 2016 via email

pimterry added a commit that referenced this pull request Dec 14, 2016
@emirotin
Copy link
Contributor

emirotin commented Dec 14, 2016 via email

@pimterry
Copy link
Contributor Author

Ok, new patch with review markups for this is up: #81

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