How to write RSpec test cases for TimeOutError Exception - ruby-on-rails

I have written retry logic for sending mails and trying to cover test cases for it.
def send_mail(mail,log_obj)
begin
attempts ||= 1
mail.deliver_now
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Net::OpenTimeout, Net::ReadTimeout => e
CommonLogger.input_log(log_obj,"Timeout: #{e} (attempt ##{ attempts })")
if (attempts += 1) <= 4
sleep(1)
Rails.logger.info ("Retrying...")
retry
else
Rails.logger.error("Retry attempts exceeded.")
raise e
end
end
end

You can use and_raise to raise errors
https://relishapp.com/rspec/rspec-mocks/docs/configuring-responses/raising-an-error
allow(mail).to receive(:deliver_now).and_raise(Net::OpenTimeout)
allow_any_instance_of(MailClass).to receive(:deliver_now).and_raise(Net::OpenTimeout)

Related

test coverage for rescue blocks in rspec

I am trying to get coverage on the following sections of code (from begin to end)in my attached spec where
def process_request(data)
agents = agent_emails(data)
begin
ej = EmailerJob.new(agents: agents, data: data)
CommonLogger.input_log(log_obj,"Sending Lead email to #{agents}")
ej.deliver_emails
rescue Timeout::Error => error
CommonLogger.input_log(log_obj,"#{error}","error")
raise error
rescue StandardError => error
Rails.logger.error "StandardError Handling for #{agents}"
CommonLogger.input_log(log_obj,"#{error}","error")
rescue Exception => error
Rails.logger.error "Exception Handling for #{agents}"
CommonLogger.input_log(log_obj,"#{error}","error")
raise error
end
end
coverage report:
Can somebody please help me how can i cover all rescue sections?
Rspec: 3.12.0

Is it possible for begin .. rescue to NOT catch an exception?

I have a process that is within a begin rescue loop, that looks like this:
begin
# do some stuff
rescue Exception => e
Rails.logger.info "#{e.response.message}"
end
Is it possible for this to NOT catch an exception? For some reason my process is running, not throwing errors, but randomly not working.
Just use :
# do some stuff
without any begin/rescue block, and see which Error comes out. Let's say it is NoMethodError. Maybe you have a typo in some of your code, like "abc".spilt.
Correct it. Try again, maybe you get Errno::ECONNRESET.
Try :
begin
# do some stuff
rescue Errno::ECONNRESET => e
Rails.logger.info "#{e.message}"
end
Rinse and repeat, but start from scratch. rescue Exception is just too much.
Maybe you can temporary comment rescue block:
#begin
...
#rescue Exception => e
#Rails.logger.info "#{e.response.message}"
or you can raise raise this exception in rescue:
begin
do some stuff
rescue Exception => e
Rails.logger.info "#{e.response.message}"
raise e
end

Can I automatically re-run a method if a timeout error occurs?

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?
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
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
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
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] }

Inconsistent Timeout::timeout and rescue Timeout::Error behavior

I'm using Timeout::timeout(1) for a process that takes longer than 1 second, though it only occasionally triggers a timeout. When it does, rescue captures it in different ways each time. Here's a sample of my code:
require 'timeout'
...
begin
status = Timeout::timeout(1) {
open(file_url) do |foo|
feed = RSS::Parser.parse(foo)
some_method_call(arg1, arg2)
#other stuff
end
}
rescue Timeout::Error
Rails.logger.debug "Timeout"
return nil
rescue Exception => ex
Rails.logger.debug "EXCEPTION - #{ex.message}"
return nil
end
Here are the three scenarios I encounter with the same input:
Process runs to completion and takes longer than 60 seconds
Process times out and hangs, printing only execution expired in development.log
Process times out, is rescued properly, prints "Timeout" in development.log, and returns nil
Why is this so inconsistent?
UPDATE
After reducing the timeout to 0.0001s, the process is timing out consistently and as expected. It seems that the open(file_url) block was opening faster than 1 second, and despite everything within the block taking more than 1 second, the Timeout was only triggered if the opening itself took longer than 1 second.
This however did not explain the execution expired exception. To test this, I moved the Timeout::timeout(0.0001) to within the open block. The code looks like the following:
require 'timeout'
...
begin
open(file_url) do |foo|
status = Timeout::timeout(0.0001) do
begin
feed = RSS::Parser.parse(foo)
some_method_call(arg1, arg2)
#other stuff
rescue Timeout::Error
Rails.logger.debug "Timeout 2"
rescue Exception => ex
Rails.logger.debug "EXCEPTION 2 - #{ex.message}"
end
end
end
rescue Timeout::Error
Rails.logger.debug "Timeout"
return nil
rescue Exception => ex
Rails.logger.debug "EXCEPTION - #{ex.message}"
return nil
end
Now, I'm consistently receiving the output EXCEPTION 2 - execution expired. Why is it that the Timeout::Error is not being triggered here?
Your inner
rescue Exception => ex
Rails.logger.debug "EXCEPTION 2 - #{ex.message}"
end
keeps the outer timeoutblock from raising Timeout::Error.
Removing that rescuestatement should do the trick.
If you really need to catch any exception, replace it with:
rescue StandardError => ex
Rails.logger.debug "EXCEPTION 2 - #{ex.message}"
end
Internally (within the Timeout block) Timeout does not use Timeout::Error. If it did, then every garden-variety rescue would catch it, and you don't want that. So it creates a new Exception and uses that, so that it hopefully blows through all normal error handling and actually makes the code stop running.
Check out the timeout.rb code in ruby200/lib/ruby/2.0.0. It's quite short, and pretty informative.
In particular, you can pass your own Exception in as the second parameter of Timeout::timeout, and Timeout will use that. So you can catch it inside your code, if you want.
Note that Logger currently traps all exceptions that happen while writing, and doesn't re-raise, so it breaks Timeout. I've filed a bug report.

How do I reschedule a failed Rufus every job?

I have a Rufus "every job" that runs periodically, but sometimes it may fail to perform it's task.
On failure, I would like to reschedule a retry soon rather than wait until the next cycle.
class PollProducts
def initialize()
end
def call(job)
puts "Updating products"
begin
# Do something that might fail
raise if rand(3) == 1
rescue Exception => e
puts "Request failed - recheduling: #{e}"
# job.in("5s") <-- What can I do?
end
end
end
scheduler.every '6h', PollProducts.new, :first_in => '5s', :blocking => true
Is this possible?
Ok this worked for me:
job.scheduler.in '5s', self

Resources