Skip to content
Browse files

Initial commit for the "rescue_me" gem.

This gem gives us the #rescue_and_retry convenience method which retries a block
of code if it raises a temporary exception.
  • Loading branch information...
0 parents commit e7ff9498eb782f8b5cb825aed3f438cb32f3ba6e @ashirazi ashirazi committed Aug 24, 2010
Showing with 290 additions and 0 deletions.
  1. +5 −0 .document
  2. +21 −0 .gitignore
  3. +26 −0 LICENSE
  4. +55 −0 README.rdoc
  5. +57 −0 Rakefile
  6. +30 −0 lib/rescue_me.rb
  7. +10 −0 test/helper.rb
  8. +86 −0 test/test_rescue_me.rb
5 .document
@@ -0,0 +1,5 @@
+README.rdoc
+lib/**/*.rb
+bin/*
+features/**/*.feature
+LICENSE
21 .gitignore
@@ -0,0 +1,21 @@
+## MAC OS
+.DS_Store
+
+## TEXTMATE
+*.tmproj
+tmtags
+
+## EMACS
+*~
+\#*
+.\#*
+
+## VIM
+*.swp
+
+## PROJECT::GENERAL
+coverage
+rdoc
+pkg
+
+## PROJECT::SPECIFIC
26 LICENSE
@@ -0,0 +1,26 @@
+Copyright (c) 2010 Arild Shirazi. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY ARILD SHIRAZI ``AS IS'' AND ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
+SHALL ARILD SHIRAZI OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+The views and conclusions contained in the software and documentation are those
+of the authors and should not be interpreted as representing official policies,
+either expressed or implied, of Arild Shirazi.
55 README.rdoc
@@ -0,0 +1,55 @@
+= rescue_me
+
+Provides a convenience method to retry blocks of code that might fail due to
+temporary errors, e.g. a network service that becomes temporarily unavailable.
+The retries are timed to back-off exponentially (2^n seconds), hopefully giving
+time for the remote server to recover. These are the default wait times between
+consecutive attempts:
+ 0, 1, 2, 4, 8, 16, 32, 64, 128, ... seconds
+
+Usage:
+ rescue_and_retry(max_attempts, *temporary_exceptions) {
+ # your code
+ }
+
+Example - retry my code up to 7 times (over about a minute) if I see the
+following 2 network errors:
+ rescue_and_retry(7, Net::SMTPServerBusy, IOError) {
+ smtp.send_message(message, from_address, to_address )
+ }
+
+Log output:
+ WARN -- : rescue and retry (attempt 1/5): ./mailer.rb:43, <SMTPServerBusy: 451 4.3.0 Mail server temporarily rejected message.>
+ WARN -- : rescue and retry (attempt 2/5): ./mailer.rb:43, <SMTPServerBusy: 451 4.3.0 Mail server temporarily rejected message.>
+ WARN -- : rescue and retry (attempt 3/5): ./mailer.rb:43, <SMTPServerBusy: 451 4.3.0 Mail server temporarily rejected message.>
+ # No further output or stacktrace. Block succeeded on 4th attempt.
+
+
+== Copyright
+
+Copyright (c) 2010 Arild Shirazi. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY ARILD SHIRAZI ``AS IS'' AND ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
+SHALL ARILD SHIRAZI OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+The views and conclusions contained in the software and documentation are those
+of the authors and should not be interpreted as representing official policies,
+either expressed or implied, of Arild Shirazi.
57 Rakefile
@@ -0,0 +1,57 @@
+require 'rubygems'
+require 'rake'
+
+begin
+ require 'jeweler'
+ Jeweler::Tasks.new do |gem|
+ gem.name = "rescue_me"
+ gem.summary = %Q{Retry a block of code that might fail due to temporary errors.}
+ gem.description = %Q{Provides a convenience method to retry blocks of code \
+that might fail due to temporary errors, e.g. a network service that becomes \
+temporarily unavailable. The retries are timed to back-off exponentially (2^n \
+seconds), hopefully giving time for the remote server to recover.}
+ gem.email = "as4@eshirazi.com"
+ gem.homepage = "http://github.com/ashirazi/rescue_me"
+ gem.authors = ["Arild Shirazi"]
+ # gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
+ gem.add_development_dependency "shoulda", ">= 0"
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
+ end
+ Jeweler::GemcutterTasks.new
+rescue LoadError
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
+end
+
+require 'rake/testtask'
+Rake::TestTask.new(:test) do |test|
+ test.libs << 'lib' << 'test'
+ test.pattern = 'test/**/test_*.rb'
+ test.verbose = true
+end
+
+begin
+ require 'rcov/rcovtask'
+ Rcov::RcovTask.new do |test|
+ test.libs << 'test'
+ test.pattern = 'test/**/test_*.rb'
+ test.verbose = true
+ end
+rescue LoadError
+ task :rcov do
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
+ end
+end
+
+task :test => :check_dependencies
+
+task :default => :test
+
+require 'rake/rdoctask'
+Rake::RDocTask.new do |rdoc|
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
+
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = "rescue_me #{version}"
+ rdoc.rdoc_files.include('README*')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
30 lib/rescue_me.rb
@@ -0,0 +1,30 @@
+module Kernel
+
+ # Reattempts to run code passed in to this block if a temporary exception occurs
+ # (e.g. Net::SMTPServerBusy), using an exponential back-off algorithm
+ # (e.g. retry immediately, then after 1, 2, 4, 8, 16, 32... sec).
+ # max_attempts - the maximum number of attempts to make trying to run the
+ # block successfully before giving up
+ # temporary_exceptions - temporary exceptions that are to be caught in your
+ # code. If no exceptions are provided will capture all Exceptions. You are
+ # strongly encouraged to provide arguments that capture temporary
+ # exceptional conditions that are likely to work upon a retry.
+ def rescue_and_retry(max_attempts=7, *temporary_exceptions)
+ retry_interval = 2 # A good initial start value. Tweak as needed.
+ temporary_exceptions << Exception if temporary_exceptions.empty?
+ begin
+ yield
+ rescue *temporary_exceptions => e
+ attempt = (attempt || 0) + 1
+ message = "rescue and retry (attempt #{attempt}/#{max_attempts}): " +
+ "#{caller.first}, <#{e.class}: #{e.message}>"
+ # TODO AS: Is there a better way to access and use a logger?
+ (defined? logger) ? logger.warn(message) : puts(message)
+ raise(e) if attempt >= max_attempts
+ # Retry immediately before exponential waits (1, 2, 4, 16, ... sec)
+ sleep retry_interval**(attempt - 2) if attempt >= 2
+ retry
+ end
+ end
+
+end
10 test/helper.rb
@@ -0,0 +1,10 @@
+require 'rubygems'
+require 'test/unit'
+require 'shoulda'
+
+$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
+$LOAD_PATH.unshift(File.dirname(__FILE__))
+require 'rescue_me'
+
+class Test::Unit::TestCase
+end
86 test/test_rescue_me.rb
@@ -0,0 +1,86 @@
+require 'helper'
+
+class TestRescueMe < Test::Unit::TestCase
+
+ class ExceptionCounter
+
+ require 'net/smtp'
+ attr_reader :method_called_count
+
+ def initialize
+ @method_called_count = 0;
+ end
+
+ def exception_free
+ @method_called_count += 1
+ "This method does not raise exceptions"
+ end
+
+ def raise_zero_division_error
+ @method_called_count += 1
+ 12/0
+ end
+
+ # Will raise SMTPServerBusy the first x times this method is called,
+ # after which this method does nothing.
+ def raise_smtp_exception_until_call(times)
+ @method_called_count += 1
+ @smtp_exception_count = (@smtp_exception_count ||= 0) + 1
+ raise Net::SMTPServerBusy until times > @smtp_exception_count
+ end
+
+ end # ExceptionCounter
+
+ context "rescue_and_retry of code that might raise temporary exceptions" do
+ setup do
+ @exception_counter = ExceptionCounter.new
+ @previous_call_count = @exception_counter.method_called_count
+ end
+
+ should "run an exception-free block of code once" do
+ assert_nothing_raised do
+ rescue_and_retry {
+ @exception_counter.exception_free
+ }
+ end
+ assert 1, @exception_counter.method_called_count
+ end
+
+ should "attempt to run a block that raises an unexpected exception " +
+ "only once" do
+ assert_raise(ZeroDivisionError) do
+ rescue_and_retry(5, IOError) {
+ @exception_counter.raise_zero_division_error
+ }
+ end
+ assert 1, @exception_counter.method_called_count
+ end
+
+ should "re-run the block of code for exactly max_attempt number of times" do
+ assert_raise(ZeroDivisionError) do
+ rescue_and_retry(3, IOError) {
+ @exception_counter.raise_zero_division_error
+ }
+ end
+ assert 3, @exception_counter.method_called_count - @previous_call_count
+
+ assert_raise(ZeroDivisionError) do
+ rescue_and_retry(1, IOError) {
+ @exception_counter.raise_zero_division_error
+ }
+ end
+ assert 1, @exception_counter.method_called_count - @previous_call_count
+ end
+
+ should "not re-run the block of code after it has run successfully" do
+ assert_nothing_raised do
+ rescue_and_retry(5, Net::SMTPServerBusy) {
+ @exception_counter.raise_smtp_exception_until_call(3)
+ }
+ end
+ assert 3, @exception_counter.method_called_count
+ end
+
+ end
+
+end

0 comments on commit e7ff949

Please sign in to comment.
Something went wrong with that request. Please try again.