+++ /dev/null
-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
-
- 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)
- 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
-
- def retry_if
- self[:retry_if] ||= DEFAULT_CHECK
- 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])
- # 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)
- 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 => exception
- if retries > 0 && retry_request?(env, exception)
- 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
-
- private
-
- def retry_request?(env, exception)
- IDEMPOTENT_METHODS.include?(env[:method]) || @options.retry_if.call(env, exception)
- end
-
- end
-end