Skip to content

Instantly share code, notes, and snippets.

@mumoshu
Last active August 29, 2015 14:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mumoshu/a8c68d42b5f927dda640 to your computer and use it in GitHub Desktop.
Save mumoshu/a8c68d42b5f927dda640 to your computer and use it in GitHub Desktop.
module Reliable
def retries(method, up_to:, on:, delay: Tryer.delay, before_retry: Tryer.before_retry, after_call: Tryer.after_call)
original_method = :"#{method}_without_reliable"
alias_method original_method, method
tryer = Tryer.new(up_to: up_to, on: on, delay: delay, before_retry: before_retry, after_call: after_call)
define_method method do |*args|
tryer.reliably do
__send__ original_method, *args
end
end
end
class Tryer
class << self
def delay=(delay)
@delay = delay
end
def delay
@delay ||= -> x { x ** 2 }
end
def before_retry=(before_retry)
@before_retry = before_retry
end
def before_retry
@before_retry ||= -> e:, tries: {
warn "Retrying on error: #{e.class.name}: #{e}"
}
end
def after_call=(after_call)
@after_call = after_call
end
def after_call
@after_call ||= -> value:, error:, tries: {}
end
end
def initialize(up_to:, on:, delay: Tryer.delay, before_retry: Tryer.before_retry, after_call: Tryer.after_call)
@enumerable = up_to.respond_to?(:each) ? up_to : up_to.times
@before_retry = before_retry
@after_call = after_call
@delay = delay
@retried_errors = [on].flatten
end
def reliably(&block)
tries = 0
last_value = nil
last_error = nil
@enumerable.each do
tries += 1
begin
if tries > 1
@before_retry[e: last_error, tries: tries]
sleep @delay[tries-1]
end
last_error = nil
last_value = block.call
return last_value
rescue *@retried_errors => e
last_error = e
ensure
@after_call[value: last_value, error: last_error, tries: tries]
end
end
raise last_error
end
end
end
Reliable::Tryer.after_call = -> value:, error:, tries: {
puts "#{tries}: value=#{value}, error=#{error.class.name}(#{error})"
}
class Test
extend Reliable
class FooError < StandardError; end
def foo
raise FooError, 'message' if rand < 0.8
"successful result"
end
retries :foo, up_to: 3.times, on: FooError
end
# Test.new.foo
#1: value=, error=Test::FooError(message)
#Retrying on error: Test::FooError: message
#2: value=, error=Test::FooError(message)
#Retrying on error: Test::FooError: message
#3: value=successful result, error=NilClass()
#=> "successful result"
# @usage with_retries(3) { randomly_failing_method }
# @usage with_retries(3, delay: -> x { x ** 2 }) { randomly_failing_method }
def with_retries(retries=3, delay: -> x { x }, &block)
result = retries.times.lazy.map do |trial|
begin
if block.arity == 1
block.call(trial)
else
block.call
end
rescue => e
trial < retries - 1 ? (sleep delay[trial]; nil) : e
end
end.find(&:itself)
if result.is_a? StandardError
raise result
else
result
end
end
# An imperative variant of with_retries
def with_retries2(retries=3, delay: -> x { x }, &block)
trial = 0
while trial < retries
begin
result =
if block.arity == 1
block.call(trial)
else
block.call
end
return result
rescue => e
raise e if trial >= retries - 1
trial += 1
end
end
end
def with_retrying(tries: 1, on: StandardError, delay: -> x { x }, before_retry: -> e:, tries:, max_tries: { warn "Retrying on error: #{tries}/#{max_tries}: #{e.class.name}: #{e}" }, &block)
max_tries = tries
tries = 1
retried_errors = [on].flatten
begin
yield
rescue *retried_errors => e
if tries < max_tries
before_retry[e: e, tries: tries, max_tries: max_tries]
tries += 1
retry
end
raise e
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment