Merge pull request #340 from mauricio/issue-298

Retry only HTTP methods known to be idempotent

User can optionally configure other methods.

Fixes #298
This commit is contained in:
Mislav Marohnić 2014-02-11 14:38:35 +01:00
commit 90faa57440
2 changed files with 74 additions and 14 deletions

View File

@ -19,7 +19,12 @@ module Faraday
# interval that is random between 0.1 and 0.15 # interval that is random between 0.1 and 0.15
# #
class Request::Retry < Faraday::Middleware class Request::Retry < Faraday::Middleware
class Options < Faraday::Options.new(:max, :interval, :interval_randomness, :backoff_factor, :exceptions)
IDEMPOTENT_METHODS = [:delete, :get, :head, :options, :put]
class Options < Faraday::Options.new(:max, :interval, :interval_randomness, :backoff_factor, :exceptions, :retry_if)
DEFAULT_CHECK = lambda { |env,exception| false }
def self.from(value) def self.from(value)
if Fixnum === value if Fixnum === value
new(value) new(value)
@ -49,6 +54,10 @@ module Faraday
Error::TimeoutError]) Error::TimeoutError])
end end
def retry_if
self[:retry_if] ||= DEFAULT_CHECK
end
end end
# Public: Initialize middleware # Public: Initialize middleware
@ -66,6 +75,12 @@ module Faraday
# given as Class, Module, or String. (default: # given as Class, Module, or String. (default:
# [Errno::ETIMEDOUT, Timeout::Error, # [Errno::ETIMEDOUT, Timeout::Error,
# Error::TimeoutError]) # Error::TimeoutError])
# retry_if - block that will receive the env object and the exception raised
# and should decide if the code should retry still the action or
# not independent of the retry count. This would be useful
# if the exception produced is non-recoverable or if the
# the HTTP method called is not idempotent.
# (defaults to return false)
def initialize(app, options = nil) def initialize(app, options = nil)
super(app) super(app)
@options = Options.from(options) @options = Options.from(options)
@ -85,8 +100,8 @@ module Faraday
begin begin
env[:body] = request_body # after failure env[:body] is set to the response body env[:body] = request_body # after failure env[:body] is set to the response body
@app.call(env) @app.call(env)
rescue @errmatch rescue @errmatch => exception
if retries > 0 if retries > 0 && retry_request?(env, exception)
retries -= 1 retries -= 1
sleep sleep_amount(retries + 1) sleep sleep_amount(retries + 1)
retry retry
@ -114,5 +129,12 @@ module Faraday
end end
matcher matcher
end end
private
def retry_request?(env, exception)
IDEMPOTENT_METHODS.include?(env[:method]) || @options.retry_if.call(env, exception)
end
end end
end end

View File

@ -10,35 +10,37 @@ module Middleware
Faraday.new do |b| Faraday.new do |b|
b.request :retry, *retry_args b.request :retry, *retry_args
b.adapter :test do |stub| b.adapter :test do |stub|
stub.post('/unstable') { ['get', 'post'].each do |method|
stub.send(method, '/unstable') {
@times_called += 1 @times_called += 1
@explode.call @times_called @explode.call @times_called
} }
end end
end end
end end
end
def test_unhandled_error def test_unhandled_error
@explode = lambda {|n| raise "boom!" } @explode = lambda {|n| raise "boom!" }
assert_raises(RuntimeError) { conn.post("/unstable") } assert_raises(RuntimeError) { conn.get("/unstable") }
assert_equal 1, @times_called assert_equal 1, @times_called
end end
def test_handled_error def test_handled_error
@explode = lambda {|n| raise Errno::ETIMEDOUT } @explode = lambda {|n| raise Errno::ETIMEDOUT }
assert_raises(Errno::ETIMEDOUT) { conn.post("/unstable") } assert_raises(Errno::ETIMEDOUT) { conn.get("/unstable") }
assert_equal 3, @times_called assert_equal 3, @times_called
end end
def test_legacy_max_retries def test_legacy_max_retries
@explode = lambda {|n| raise Errno::ETIMEDOUT } @explode = lambda {|n| raise Errno::ETIMEDOUT }
assert_raises(Errno::ETIMEDOUT) { conn(1).post("/unstable") } assert_raises(Errno::ETIMEDOUT) { conn(1).get("/unstable") }
assert_equal 2, @times_called assert_equal 2, @times_called
end end
def test_new_max_retries def test_new_max_retries
@explode = lambda {|n| raise Errno::ETIMEDOUT } @explode = lambda {|n| raise Errno::ETIMEDOUT }
assert_raises(Errno::ETIMEDOUT) { conn(:max => 3).post("/unstable") } assert_raises(Errno::ETIMEDOUT) { conn(:max => 3).get("/unstable") }
assert_equal 4, @times_called assert_equal 4, @times_called
end end
@ -46,7 +48,7 @@ module Middleware
@explode = lambda {|n| raise Errno::ETIMEDOUT } @explode = lambda {|n| raise Errno::ETIMEDOUT }
started = Time.now started = Time.now
assert_raises(Errno::ETIMEDOUT) { assert_raises(Errno::ETIMEDOUT) {
conn(:max => 2, :interval => 0.1).post("/unstable") conn(:max => 2, :interval => 0.1).get("/unstable")
} }
assert_in_delta 0.2, Time.now - started, 0.04 assert_in_delta 0.2, Time.now - started, 0.04
end end
@ -70,7 +72,7 @@ module Middleware
retry_middleware.sleep_amount_retries = [2, 1] retry_middleware.sleep_amount_retries = [2, 1]
assert_raises(Errno::ETIMEDOUT) { assert_raises(Errno::ETIMEDOUT) {
retry_middleware.call({}) retry_middleware.call({:method => :get})
} }
assert_empty retry_middleware.sleep_amount_retries assert_empty retry_middleware.sleep_amount_retries
@ -101,9 +103,45 @@ module Middleware
def test_custom_exceptions def test_custom_exceptions
@explode = lambda {|n| raise "boom!" } @explode = lambda {|n| raise "boom!" }
assert_raises(RuntimeError) { assert_raises(RuntimeError) {
conn(:exceptions => StandardError).post("/unstable") conn(:exceptions => StandardError).get("/unstable")
} }
assert_equal 3, @times_called assert_equal 3, @times_called
end end
def test_should_stop_retrying_if_block_returns_false_checking_env
@explode = lambda {|n| raise Errno::ETIMEDOUT }
check = lambda { |env,exception| env[:method] != :post }
assert_raises(Errno::ETIMEDOUT) {
conn(:retry_if => check).post("/unstable")
}
assert_equal 1, @times_called
end
def test_should_stop_retrying_if_block_returns_false_checking_exception
@explode = lambda {|n| raise Errno::ETIMEDOUT }
check = lambda { |env,exception| !exception.kind_of?(Errno::ETIMEDOUT) }
assert_raises(Errno::ETIMEDOUT) {
conn(:retry_if => check).post("/unstable")
}
assert_equal 1, @times_called
end
def test_should_not_call_retry_if_for_idempotent_methods
@explode = lambda {|n| raise Errno::ETIMEDOUT }
check = lambda { |env,exception| raise "this should have never been called" }
assert_raises(Errno::ETIMEDOUT) {
conn(:retry_if => check).get("/unstable")
}
assert_equal 3, @times_called
end
def test_should_not_retry_for_non_idempotent_method
@explode = lambda {|n| raise Errno::ETIMEDOUT }
assert_raises(Errno::ETIMEDOUT) {
conn.post("/unstable")
}
assert_equal 1, @times_called
end
end end
end end