1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
|
# frozen_string_literal: true
require "timeout"
require_relative "retriable/config"
require_relative "retriable/exponential_backoff"
require_relative "retriable/version"
module Retriable
module_function
def configure
yield(config)
end
def config
@config ||= Config.new
end
def with_context(context_key, options = {}, &block)
if !config.contexts.key?(context_key)
raise ArgumentError,
"#{context_key} not found in Retriable.config.contexts. Available contexts: #{config.contexts.keys}"
end
return unless block_given?
retriable(config.contexts[context_key].merge(options), &block)
end
def retriable(opts = {}, &block)
local_config = opts.empty? ? config : Config.new(config.to_h.merge(opts))
tries = local_config.tries
intervals = build_intervals(local_config, tries)
timeout = local_config.timeout
on = local_config.on
retry_if = local_config.retry_if
on_retry = local_config.on_retry
sleep_disabled = local_config.sleep_disabled
max_elapsed_time = local_config.max_elapsed_time
exception_list = on.is_a?(Hash) ? on.keys : on
exception_list = [*exception_list]
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
elapsed_time = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time }
tries = intervals.size + 1
execute_tries(
tries: tries, intervals: intervals, timeout: timeout,
exception_list: exception_list, on: on, retry_if: retry_if, on_retry: on_retry,
elapsed_time: elapsed_time, max_elapsed_time: max_elapsed_time,
sleep_disabled: sleep_disabled, &block
)
end
def execute_tries( # rubocop:disable Metrics/ParameterLists
tries:, intervals:, timeout:, exception_list:,
on:, retry_if:, on_retry:, elapsed_time:, max_elapsed_time:, sleep_disabled:, &block
)
tries.times do |index|
try = index + 1
begin
return call_with_timeout(timeout, try, &block)
rescue *exception_list => e
raise unless retriable_exception?(e, on, exception_list, retry_if)
interval = intervals[index]
call_on_retry(on_retry, e, try, elapsed_time.call, interval)
raise unless can_retry?(try, tries, elapsed_time.call, interval, max_elapsed_time)
sleep interval if sleep_disabled != true
end
end
end
def build_intervals(local_config, tries)
return local_config.intervals if local_config.intervals
ExponentialBackoff.new(
tries: tries - 1,
base_interval: local_config.base_interval,
multiplier: local_config.multiplier,
max_interval: local_config.max_interval,
rand_factor: local_config.rand_factor,
).intervals
end
def call_with_timeout(timeout, try)
return Timeout.timeout(timeout) { yield(try) } if timeout
yield(try)
end
def call_on_retry(on_retry, exception, try, elapsed_time, interval)
return unless on_retry
on_retry.call(exception, try, elapsed_time, interval)
end
def can_retry?(try, tries, elapsed_time, interval, max_elapsed_time)
return false unless try < tries
return true if max_elapsed_time.nil?
(elapsed_time + interval) <= max_elapsed_time
end
# When `on` is a Hash, we need to verify the exception matches a pattern.
# For any non-Hash `on` value (e.g., Array of classes, single Exception class,
# or Module), the `rescue *exception_list` clause already guarantees the
# exception is retriable with respect to `on`; `retry_if`, if provided, is an
# additional gate that can still cause this method to return false.
def retriable_exception?(exception, on, exception_list, retry_if)
return false if on.is_a?(Hash) && !hash_exception_match?(exception, on, exception_list)
return false if retry_if && !retry_if.call(exception)
true
end
def hash_exception_match?(exception, on, exception_list)
exception_list.any? do |error_class|
next false unless exception.is_a?(error_class)
patterns = [*on[error_class]]
patterns.empty? || patterns.any? { |pattern| exception.message =~ pattern }
end
end
private_class_method(
:execute_tries,
:build_intervals,
:call_with_timeout,
:call_on_retry,
:can_retry?,
:retriable_exception?,
:hash_exception_match?,
)
end
|