119 lines
3.8 KiB
Ruby
119 lines
3.8 KiB
Ruby
module Faraday
|
|
# Catches exceptions and retries each request a limited number of times.
|
|
#
|
|
# By default, it retries 2 times and handles only timeout exceptions. It can
|
|
# be configured with an arbitrary number of retries, a list of exceptions to
|
|
# handle, a retry interval, a percentage of randomness to add to the retry
|
|
# interval, and a backoff factor.
|
|
#
|
|
# Examples
|
|
#
|
|
# Faraday.new do |conn|
|
|
# conn.request :retry, max: 2, interval: 0.05,
|
|
# interval_randomness: 0.5, backoff_factor: 2
|
|
# exceptions: [CustomException, 'Timeout::Error']
|
|
# conn.adapter ...
|
|
# end
|
|
#
|
|
# This example will result in a first interval that is random between 0.05 and 0.075 and a second
|
|
# interval that is random between 0.1 and 0.15
|
|
#
|
|
class Request::Retry < Faraday::Middleware
|
|
class Options < Faraday::Options.new(:max, :interval, :interval_randomness, :backoff_factor, :exceptions)
|
|
def self.from(value)
|
|
if Fixnum === value
|
|
new(value)
|
|
else
|
|
super(value)
|
|
end
|
|
end
|
|
|
|
def max
|
|
(self[:max] ||= 2).to_i
|
|
end
|
|
|
|
def interval
|
|
(self[:interval] ||= 0).to_f
|
|
end
|
|
|
|
def interval_randomness
|
|
(self[:interval_randomness] ||= 0).to_i
|
|
end
|
|
|
|
def backoff_factor
|
|
(self[:backoff_factor] ||= 1).to_f
|
|
end
|
|
|
|
def exceptions
|
|
Array(self[:exceptions] ||= [Errno::ETIMEDOUT, 'Timeout::Error',
|
|
Error::TimeoutError])
|
|
end
|
|
|
|
end
|
|
|
|
# Public: Initialize middleware
|
|
#
|
|
# Options:
|
|
# max - Maximum number of retries (default: 2)
|
|
# interval - Pause in seconds between retries (default: 0)
|
|
# interval_randomness - The maximum random interval amount expressed
|
|
# as a float between 0 and 1 to use in addition to the
|
|
# interval. (default: 0)
|
|
# backoff_factor - The amount to multiple each successive retry's
|
|
# interval amount by in order to provide backoff
|
|
# (default: 1)
|
|
# exceptions - The list of exceptions to handle. Exceptions can be
|
|
# given as Class, Module, or String. (default:
|
|
# [Errno::ETIMEDOUT, Timeout::Error,
|
|
# Error::TimeoutError])
|
|
def initialize(app, options = nil)
|
|
super(app)
|
|
@options = Options.from(options)
|
|
@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)
|
|
random_interval = rand * @options.interval_randomness.to_f * @options.interval
|
|
current_interval + random_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)
|
|
rescue @errmatch
|
|
if retries > 0
|
|
retries -= 1
|
|
sleep sleep_amount(retries + 1)
|
|
retry
|
|
end
|
|
raise
|
|
end
|
|
end
|
|
|
|
# Private: construct an exception matcher object.
|
|
#
|
|
# An exception matcher for the rescue clause can usually be any object that
|
|
# responds to `===`, but for Ruby 1.8 it has to be a Class or Module.
|
|
def build_exception_matcher(exceptions)
|
|
matcher = Module.new
|
|
(class << matcher; self; end).class_eval do
|
|
define_method(:===) do |error|
|
|
exceptions.any? do |ex|
|
|
if ex.is_a? Module
|
|
error.is_a? ex
|
|
else
|
|
error.class.to_s == ex.to_s
|
|
end
|
|
end
|
|
end
|
|
end
|
|
matcher
|
|
end
|
|
end
|
|
end
|