Recently I needed to execute a block of code in Ruby which could possibly throw a few types of errors. For a certain subset of errors, I wanted the block to be retried for a given number of times before giving up, while for other errors, I wanted the block to bomb immediately.
This is what I came up with (available on Bitbucket and as a gem):
class Gateway include RetryThis ... def make_request(url) connection = create_connection retry_this(:times => 3, :error_types => [SocketError, Timeout::Error]) do # this is retried at least 3 times if a SocketError or Timeout::Error occurs connection.start do |http_connection| http_connection.request(Net::HTTP::Get.new(url)) end end rescue SocketError, Timeout::Error => e raise Gateway::ConnectionError, "Connection failed: #{e.message}" rescue StandardError => error raise Gateway::UnexpectedError, "Unexpected error: #{error.message}" end ... end
The following is the code for RetryThis:
retry_this.rb
module RetryThis def retry_this(options = {}) number_of_retries = options[:times] || 1 error_types = options[:error_types] || [StandardError] (1 + number_of_retries).times do |attempt_number| begin return yield if block_given? rescue *error_types => e raise e unless attempt_number < number_of_retries end end end end if __FILE__ == $0 require 'test/unit' class RetryThisTest < Test::Unit::TestCase include RetryThis def test_should_retry_block_for_given_number_of_times_for_given_error_types_and_raise_error_if_all_attempts_fail attempts = 0 retry_this(:times => 3, :error_types => [TypeError, RuntimeError]) do attempts += 1 if attempts % 2 == 0 raise 'some runtime error' else 'hi' + 1 # TypeError end end rescue => e assert e.kind_of?(TypeError) || e.kind_of?(RuntimeError) assert_equal(1 + 3, attempts) end def test_should_retry_block_for_default_1_time_for_default_standard_error_and_raise_error_if_all_attempts_fail attempts = 0 retry_this do attempts += 1 raise StandardError, 'some error' end rescue => e assert_equal('some error', e.message) assert_equal(1 + 1, attempts) end def test_should_pass_on_an_error_that_is_not_among_given_errors attempts = 0 retry_this(:error_types => [TypeError]) do attempts += 1 raise StandardError, 'some error' end rescue => e assert_equal('some error', e.message) assert_equal(1, attempts) end def test_should_return_value_of_block_if_any_attempt_leads_to_no_error attempts = 0 value = retry_this do attempts += 1 raise 'some error' if attempts == 1 'hi' end assert_equal('hi', value) assert_equal(1 + 1, attempts) end end end