Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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

0 comments on commit e7ff949

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