# frozen_string_literal: truerequire"date"require"http/retriable/errors"require"http/retriable/delay_calculator"require"openssl"moduleHTTPmoduleRetriable# Request performing watchdog.# @api privateclassPerformer# Exceptions we should retryRETRIABLE_ERRORS=[HTTP::TimeoutError,HTTP::ConnectionError,IO::EAGAINWaitReadable,Errno::ECONNRESET,Errno::ECONNREFUSED,Errno::EHOSTUNREACH,OpenSSL::SSL::SSLError,EOFError,IOError].freeze# @param [Hash] opts# @option opts [#to_i] :tries (5)# @option opts [#call, #to_i] :delay (DELAY_PROC)# @option opts [Array(Exception)] :exceptions (RETRIABLE_ERRORS)# @option opts [Array(#to_i)] :retry_statuses# @option opts [#call] :on_retry# @option opts [#to_f] :max_delay (Float::MAX)# @option opts [#call] :should_retrydefinitialize(opts)@exception_classes=opts.fetch(:exceptions,RETRIABLE_ERRORS)@retry_statuses=opts[:retry_statuses]@tries=opts.fetch(:tries,5).to_i@on_retry=opts.fetch(:on_retry,->(*){})@should_retry_proc=opts[:should_retry]@delay_calculator=DelayCalculator.new(opts)end# Watches request/response execution.## If any of {RETRIABLE_ERRORS} occur or response status is `5xx`, retries# up to `:tries` amount of times. Sleeps for amount of seconds calculated# with `:delay` proc before each retry.## @see #initialize# @api privatedefperform(client,req,&block)1.upto(Float::INFINITY)do|attempt|# infinite loop with indexerr,res=try_request(&block)ifretry_request?(req,err,res,attempt)beginwait_for_retry_or_raise(req,err,res,attempt)ensure# Some servers support Keep-Alive on any response. Thus we should# flush response before retry, to avoid state error (when socket# has pending response data and we try to write new request).# Alternatively, as we don't need response body here at all, we# are going to close client, effectivle closing underlying socket# and resetting client's state.client.closeendelsiferrclient.closeraiseerrelsifresreturnresendendenddefcalculate_delay(iteration,response)@delay_calculator.call(iteration,response)endprivate# rubocop:disable Lint/RescueExceptiondeftry_requesterr,res=nilbeginres=yieldrescueException=>eerr=eend[err,res]end# rubocop:enable Lint/RescueExceptiondefretry_request?(req,err,res,attempt)if@should_retry_proc@should_retry_proc.call(req,err,res,attempt)elsiferrretry_exception?(err)elseretry_response?(res)endenddefretry_exception?(err)@exception_classes.any?{|e|err.is_a?(e)}enddefretry_response?(res)returnfalseunless@retry_statusesresponse_status=res.status.to_iretry_matchers=[@retry_statuses].flattenretry_matchers.any?do|matcher|casematcherwhenRangethenmatcher.cover?(response_status)whenNumericthenmatcher==response_statuselsematcher.call(response_status)endendenddefwait_for_retry_or_raise(req,err,res,attempt)ifattempt<@tries@on_retry.call(req,err,res)sleepcalculate_delay(attempt,res)elseres&.flushraiseout_of_retries_error(req,res,err)endend# Builds OutOfRetriesError## @param request [HTTP::Request]# @param status [HTTP::Response, nil]# @param exception [Exception, nil]defout_of_retries_error(request,response,exception)message="#{request.verb.to_s.upcase} <#{request.uri}> failed"message+=" with #{response.status}"ifresponsemessage+=":#{exception}"ifexceptionHTTP::OutOfRetriesError.new(message).tapdo|ex|ex.cause=exceptionex.response=responseendendendendend