2

We have an application that makes hundreds of API calls to external services. Sometimes some calls take too much time to respond.

I am using the rake_timeout gem to find time consuming process, so, Timeout::Error will be thrown whenever some request is taking too long to respond. I am rescuing this error and doing a retry on that method:

def new
 @make_one_external_service_call = exteral_api_fetch1(params[:id])
 @make_second_external_call = exteral_api_fetch1(@make_one_external_service_call)

  #Below code will be repeated in every method

 tries = 0
rescue Timeout::Error => e
  tries += 1
  retry if tries <= 3
  logger.error e.message 
end

This lets the method fully re-run it. This is very verbose and I am repeating it every time.

Is there any way to do this so that, if the Timeout:Error occurrs, it will retry that method automatically three times?

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Paritosh Piplewar
  • 7,982
  • 5
  • 26
  • 41

4 Answers4

3

I have a little module for that:

# in lib/retryable.rb
module Retryable

  # Options:
  # * :tries - Number of tries to perform. Defaults to 1. If you want to retry once you must set tries to 2.
  # * :on - The Exception on which a retry will be performed. Defaults to Exception, which retries on any Exception.
  # * :log - The log level to log the exception. Defaults to nil.
  #
  # If you work with something like ActiveRecord#find_or_create_by_foo, remember to put that call in a uncached { } block. That
  # forces subsequent finds to hit the database again.
  #
  # Example
  # =======
  #   retryable(:tries => 2, :on => OpenURI::HTTPError) do
  #     # your code here
  #   end
  #
  def retryable(options = {}, &block)
    opts = { :tries => 1, :on => Exception }.merge(options)

    retry_exception, retries = opts[:on], opts[:tries]

    begin
      return yield
    rescue retry_exception => e
      logger.send(opts[:log], e.message) if opts[:log]
      retry if (retries -= 1) > 0
    end

    yield
  end

end

and than in your model:

extend Retryable

def new
  retryable(:tries => 3, :on => Timeout::Error, :log =>:error) do
    @make_one_external_service_call = exteral_api_fetch1(params[:id])
    @make_second_external_call = exteral_api_fetch1(@make_one_external_service_call)
  end
  ...
end
spickermann
  • 100,941
  • 9
  • 101
  • 131
  • This is a fantastic start. There are a few issues though. First, it will run `tries` + 1 times. Also, you should never rescue from `Exception` but instead the default should be to rescue from `StandardError` (see http://stackoverflow.com/a/10048406/1084109) Here's an updated version: https://gist.github.com/mcfadden/0130f1a7fe52463b6628 – McFadden Jul 14 '14 at 17:20
2

You could do something like this:

module Foo
  def self.retryable(options = {})
    retry_times   = options[:times] || 10
    try_exception = options[:on]    || Exception

    yield if block_given?
  rescue *try_exception => e
    retry if (retry_times -= 1) > 0
    raise e
  end
end

Foo.retryable(on: Timeout::Error, times: 5) do
  # your code here
end

You can even pass multiple exceptions to "catch":

Foo.retryable(on: [Timeout::Error, StandardError]) do
  # your code here
end
Pierre-Louis Gottfrois
  • 17,561
  • 8
  • 47
  • 71
1

I think what you need is the retryable gem.

With the gem, you can write your method like below

def new
  retryable :on => Timeout::Error, :times => 3 do
   @make_one_external_service_call = exteral_api_fetch1(params[:id])
   @make_second_external_call = exteral_api_fetch1(@make_one_external_service_call)
  end
end

Please read the documentation for more information on how to use the gem and the other options it provides

usha
  • 28,973
  • 5
  • 72
  • 93
0

you could just write a helper-method for that:

class TimeoutHelper
  def call_and_retry(tries=3)
    yield
  rescue Timeout::Error => e
    tries -= 1
    retry if tries > 0
    Rails.logger.error e.message
  end
end

(completely untested) and call it via

TimeoutHelper.call_and_retry { [your code] }
phoet
  • 18,688
  • 4
  • 46
  • 74