-
Notifications
You must be signed in to change notification settings - Fork 976
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
Honor Retry-After header #773
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,7 +23,9 @@ class Request::Retry < Faraday::Middleware | |
IDEMPOTENT_METHODS = [:delete, :get, :head, :options, :put] | ||
|
||
class Options < Faraday::Options.new(:max, :interval, :max_interval, :interval_randomness, | ||
:backoff_factor, :exceptions, :methods, :retry_if, :retry_block) | ||
:backoff_factor, :exceptions, :methods, :retry_if, :retry_block, | ||
:retry_statuses) | ||
|
||
DEFAULT_CHECK = lambda { |env,exception| false } | ||
|
||
def self.from(value) | ||
|
@@ -56,7 +58,8 @@ def backoff_factor | |
|
||
def exceptions | ||
Array(self[:exceptions] ||= [Errno::ETIMEDOUT, 'Timeout::Error', | ||
Error::TimeoutError]) | ||
Error::TimeoutError, | ||
Faraday::Error::RetriableResponse]) | ||
end | ||
|
||
def methods | ||
|
@@ -71,6 +74,9 @@ def retry_block | |
self[:retry_block] ||= Proc.new {} | ||
end | ||
|
||
def retry_statuses | ||
Array(self[:retry_statuses] ||= []) | ||
end | ||
end | ||
|
||
# Public: Initialize middleware | ||
|
@@ -106,29 +112,35 @@ def initialize(app, options = nil) | |
@errmatch = build_exception_matcher(@options.exceptions) | ||
end | ||
|
||
def sleep_amount(retries) | ||
retry_index = @options.max - retries | ||
current_interval = @options.interval * (@options.backoff_factor ** retry_index) | ||
current_interval = [current_interval, @options.max_interval].min | ||
random_interval = rand * @options.interval_randomness.to_f * @options.interval | ||
current_interval + random_interval | ||
def calculate_sleep_amount(retries, env) | ||
retry_after = calculate_retry_after(env) | ||
retry_interval = calculate_retry_interval(retries) | ||
|
||
return if retry_after && retry_after > @options.max_interval | ||
|
||
retry_after && retry_after >= retry_interval ? retry_after : retry_interval | ||
end | ||
|
||
def call(env) | ||
retries = @options.max | ||
request_body = env[:body] | ||
begin | ||
env[:body] = request_body # after failure env[:body] is set to the response body | ||
@app.call(env) | ||
@app.call(env).tap do |resp| | ||
raise Faraday::Error::RetriableResponse.new(nil, resp) if @options.retry_statuses.include?(resp.status) | ||
end | ||
rescue @errmatch => exception | ||
if retries > 0 && retry_request?(env, exception) | ||
retries -= 1 | ||
rewind_files(request_body) | ||
@options.retry_block.call(env, @options, retries, exception) | ||
sleep sleep_amount(retries + 1) | ||
retry | ||
if (sleep_amount = calculate_sleep_amount(retries + 1, env)) | ||
sleep sleep_amount | ||
retry | ||
end | ||
end | ||
raise | ||
|
||
raise unless exception.is_a?(Faraday::Error::RetriableResponse) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bronislav @iMacTia Hey all, I've tried to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @fabiokr I see what you mean. Would you mind to create a pull request for this change? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right @fabiokr, we missed what to do in case we reach this point (too many retries) with a PR with demonstrating test and fix welcome! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
end | ||
end | ||
|
||
|
@@ -167,5 +179,29 @@ def rewind_files(body) | |
end | ||
end | ||
|
||
# MDN spec for Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After | ||
def calculate_retry_after(env) | ||
response_headers = env[:response_headers] | ||
return unless response_headers | ||
|
||
retry_after_value = env[:response_headers]["Retry-After"] | ||
|
||
# Try to parse date from the header value | ||
begin | ||
datetime = DateTime.rfc2822(retry_after_value) | ||
datetime.to_time - Time.now.utc | ||
rescue ArgumentError | ||
retry_after_value.to_f | ||
end | ||
end | ||
|
||
def calculate_retry_interval(retries) | ||
retry_index = @options.max - retries | ||
current_interval = @options.interval * (@options.backoff_factor ** retry_index) | ||
current_interval = [current_interval, @options.max_interval].min | ||
random_interval = rand * @options.interval_randomness.to_f * @options.interval | ||
|
||
current_interval + random_interval | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can see what you're trying to do with
Faraday::Error::RetriableResponse
, however I don't think it's a good idea to introduce this exception and use it this way.The reason is that, after we reach the maximum number of retries, the new exception will be propagated outside and this might be an unexpected behaviour compared to what happens now.
I like the idea of allowing to define a list of statuses that you want to retry, but this should be tested internally without raising any exception.
My suggestion is to add this to
retry_request?
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've tried to do this without raising an exception but this will lead to implementing two places with retry logic which seems a bit overhead. It could be done using
do ... while
loop but it looks hacky for me.And I'll move statuses testing to the better place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've tried to move this line around but it seems that this is the best option.