# frozen_string_literal: truerequire_relative'retryable'moduleFaradaymoduleRetry# This class provides the main implementation for your middleware.# Your middleware can implement any of the following methods:# * on_request - called when the request is being prepared# * on_complete - called when the response is being processed## Optionally, you can also override the following methods from Faraday::Middleware# * initialize(app, options = {}) - the initializer method# * call(env) - the main middleware invocation method.# This already calls on_request and on_complete, so you normally don't need to override it.# You may need to in case you need to "wrap" the request or need more control# (see "retry" middleware: https://github.com/lostisland/faraday/blob/main/lib/faraday/request/retry.rb#L142).# IMPORTANT: Remember to call `@app.call(env)` or `super` to not interrupt the middleware chain!classMiddleware<Faraday::MiddlewareincludeRetryableDEFAULT_EXCEPTIONS=[Errno::ETIMEDOUT,'Timeout::Error',Faraday::TimeoutError,Faraday::RetriableResponse].freezeIDEMPOTENT_METHODS=%i[delete get head options put].freeze# Options contains the configurable parameters for the Retry middleware.classOptions<Faraday::Options.new(:max,:interval,:max_interval,:interval_randomness,:backoff_factor,:exceptions,:methods,:retry_if,:retry_block,:retry_statuses,:rate_limit_retry_header,:rate_limit_reset_header,:header_parser_block,:exhausted_retries_block)DEFAULT_CHECK=->(_env,_exception){false}defself.from(value)ifvalue.is_a?(Integer)new(value)elsesuper(value)endenddefmax(self[:max]||=2).to_ienddefinterval(self[:interval]||=0).to_fenddefmax_interval(self[:max_interval]||=Float::MAX).to_fenddefinterval_randomness(self[:interval_randomness]||=0).to_fenddefbackoff_factor(self[:backoff_factor]||=1).to_fenddefexceptionsArray(self[:exceptions]||=DEFAULT_EXCEPTIONS)enddefmethodsArray(self[:methods]||=IDEMPOTENT_METHODS)enddefretry_ifself[:retry_if]||=DEFAULT_CHECKenddefretry_blockself[:retry_block]||=proc{}enddefretry_statusesArray(self[:retry_statuses]||=[])enddefexhausted_retries_blockself[:exhausted_retries_block]||=proc{}endend# @param app [#call]# @param options [Hash]# @option options [Integer] :max (2) Maximum number of retries# @option options [Integer] :interval (0) Pause in seconds between retries# @option options [Integer] :interval_randomness (0) The maximum random# interval amount expressed as a float between# 0 and 1 to use in addition to the interval.# @option options [Integer] :max_interval (Float::MAX) An upper limit# for the interval# @option options [Integer] :backoff_factor (1) The amount to multiply# each successive retry's interval amount by in order to provide backoff# @option options [Array] :exceptions ([ Errno::ETIMEDOUT,# 'Timeout::Error', Faraday::TimeoutError, Faraday::RetriableResponse])# The list of exceptions to handle. Exceptions can be given as# Class, Module, or String.# @option options [Array<Symbol>] :methods (the idempotent HTTP methods# in IDEMPOTENT_METHODS) A list of HTTP methods, as symbols, to retry without# calling retry_if. Pass an empty Array to call retry_if# for all exceptions.# @option options [Block] :retry_if (false) 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.# @option options [Block] :retry_block block that is executed before# every retry. The block will be yielded keyword arguments:# * env [Faraday::Env]: Request environment# * options [Faraday::Options]: middleware options# * retry_count [Integer]: how many retries have already occured (starts at 0)# * exception [Exception]: exception that triggered the retry,# will be the synthetic `Faraday::RetriableResponse` if the# retry was triggered by something other than an exception.# * will_retry_in [Float]: retry_block is called *before* the retry# delay, actual retry will happen in will_retry_in number of# seconds.# @option options [Array] :retry_statuses Array of Integer HTTP status# codes or a single Integer value that determines whether to raise# a Faraday::RetriableResponse exception based on the HTTP status code# of an HTTP response.# @option options [Block] :header_parser_block block that will receive# the the value of the retry header and should return the number of# seconds to wait before retrying the request. This is useful if the# value of the header is not a number of seconds or a RFC 2822 formatted date.# @option options [Block] :exhausted_retries_block block will receive# when all attempts are exhausted. The block will be yielded keyword arguments:# * env [Faraday::Env]: Request environment# * exception [Exception]: exception that triggered the retry,# will be the synthetic `Faraday::RetriableResponse` if the# retry was triggered by something other than an exception.# * options [Faraday::Options]: middleware optionsdefinitialize(app,options=nil)super(app)@options=Options.from(options)@errmatch=build_exception_matcher(@options.exceptions)enddefcalculate_sleep_amount(retries,env)retry_after=[calculate_retry_after(env),calculate_rate_limit_reset(env)].compact.maxretry_interval=calculate_retry_interval(retries)returnifretry_after&&retry_after>@options.max_intervalifretry_after&&retry_after>=retry_intervalretry_afterelseretry_intervalendend# @param env [Faraday::Env]defcall(env)retries=@options.maxrequest_body=env[:body]with_retries(env: env,options: @options,retries: retries,body: request_body,errmatch: @errmatch)do# after failure env[:body] is set to the response bodyenv[:body]=request_body@app.call(env).tapdo|resp|raiseFaraday::RetriableResponse.new(nil,resp)if@options.retry_statuses.include?(resp.status)endendend# 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.## @param exceptions [Array]# @api private# @return [Module] an exception matcherdefbuild_exception_matcher(exceptions)matcher=Module.new(class<<matcherselfend).class_evaldodefine_method(:===)do|error|exceptions.any?do|ex|ifex.is_a?Moduleerror.is_a?exelseObject.const_defined?(ex.to_s)&&error.is_a?(Object.const_get(ex.to_s))endendendendmatcherendprivatedefretry_request?(env,exception)@options.methods.include?(env[:method])||@options.retry_if.call(env,exception)enddefrewind_files(body)returnunlessdefined?(Faraday::UploadIO)returnunlessbody.is_a?(Hash)body.eachdo|_,value|value.rewindifvalue.is_a?(Faraday::UploadIO)endend# RFC for RateLimit Header Fields for HTTP:# https://www.ietf.org/archive/id/draft-ietf-httpapi-ratelimit-headers-05.html#name-fields-definitiondefcalculate_rate_limit_reset(env)reset_header=@options.rate_limit_reset_header||'RateLimit-Reset'parse_retry_header(env,reset_header)end# MDN spec for Retry-After header:# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-Afterdefcalculate_retry_after(env)retry_header=@options.rate_limit_retry_header||'Retry-After'parse_retry_header(env,retry_header)enddefcalculate_retry_interval(retries)retry_index=@options.max-retriescurrent_interval=@options.interval*(@options.backoff_factor**retry_index)current_interval=[current_interval,@options.max_interval].minrandom_interval=rand*@options.interval_randomness.to_f*@options.intervalcurrent_interval+random_intervalenddefparse_retry_header(env,header)response_headers=env[:response_headers]returnunlessresponse_headersretry_after_value=env[:response_headers][header]if@options.header_parser_block@options.header_parser_block.call(retry_after_value)else# Try to parse date from the header valuebegindatetime=DateTime.rfc2822(retry_after_value)datetime.to_time-Time.now.utcrescueArgumentErrorretry_after_value.to_fendendendendendend