├── .gitignore ├── spec ├── spec_helper.rb └── lib │ └── retryable_spec.rb ├── lib └── reretryable.rb ├── reretryable.gemspec ├── README.markdown └── Rakefile /.gitignore: -------------------------------------------------------------------------------- 1 | /pkg/ -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require 'spec' 3 | rescue LoadError 4 | require 'rubygems' 5 | gem 'rspec' 6 | require 'spec' 7 | end 8 | 9 | require File.join(File.dirname(__FILE__), '..', 'lib', 'reretryable') -------------------------------------------------------------------------------- /lib/reretryable.rb: -------------------------------------------------------------------------------- 1 | module Retryable 2 | def retryable(options = {}, &block) 3 | opts = { :tries => 1, 4 | :on => StandardError, 5 | :sleep => 0, 6 | :matching => /.*/ }.merge(options) 7 | 8 | return nil if opts[:tries] == 0 9 | 10 | retry_exception = [opts[:on]].flatten 11 | tries = opts[:tries] 12 | message_pattern = opts[:matching] 13 | sleep_time = opts[:sleep] 14 | 15 | begin 16 | return yield 17 | rescue *retry_exception => exception 18 | raise unless exception.message =~ message_pattern 19 | 20 | if (tries -= 1) > 0 21 | sleep sleep_time 22 | retry 23 | end 24 | end 25 | 26 | yield 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /reretryable.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | Gem::Specification.new do |s| 4 | s.name = %q{retryable} 5 | s.version = "0.1.0" 6 | 7 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 8 | s.authors = ["Craig 'The Craif' Mackenzie and Niko Felger"] 9 | s.date = %q{2010-04-07} 10 | s.email = %q{developers@songkick.com} 11 | s.extra_rdoc_files = ["README.markdown"] 12 | s.files = ["README.markdown", "retryable.gemspec", "spec", "lib/retryable.rb"] 13 | s.homepage = %q{http://www.songkick.com} 14 | s.rdoc_options = ["--main", "README.markdown"] 15 | s.require_paths = ["lib"] 16 | s.rubygems_version = %q{1.3.5} 17 | s.summary = %q{Retrying code blocks on specific errors} 18 | 19 | if s.respond_to? :specification_version then 20 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 21 | s.specification_version = 3 22 | 23 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then 24 | s.add_development_dependency(%q, [">= 0"]) 25 | else 26 | s.add_dependency(%q, [">= 0"]) 27 | end 28 | else 29 | s.add_dependency(%q, [">= 0"]) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | This fork is available as a gem under the name 'reretryable'. 2 | 3 | # Retryable#retryable 4 | 5 | ## Description 6 | 7 | Runs a code block, and retries it when an exception occurs. It's great when 8 | working with flakey webservices (for example). 9 | 10 | It's configured using four optional parameters `:tries`, `:on`, `:matching` and `:sleep`, and 11 | runs the passed block. Should an exception occur, it'll retry for (tries-1) times. 12 | 13 | Should the number of retries be reached without success, the last exception 14 | will be raised. 15 | 16 | 17 | ## Examples 18 | 19 | Open an URL, retry up to two times when an `OpenURI::HTTPError` occurs. 20 | 21 | require "retryable" 22 | require "open-uri" 23 | 24 | include Retryable 25 | 26 | retryable( :tries => 3, :on => OpenURI::HTTPError ) do 27 | xml = open( "http://example.com/test.xml" ).read 28 | end 29 | 30 | Do _something_, retry up to four times for either `ArgumentError` or 31 | `TimeoutError` exceptions. 32 | 33 | require "retryable" 34 | include Retryable 35 | 36 | retryable( :tries => 5, :on => [ ArgumentError, TimeoutError ] ) do 37 | # some crazy code 38 | end 39 | 40 | 41 | 42 | Do _something_, retry up to three times for `ArgumentError` exceptions 43 | which smell like "Bacon", but have a nap between tries. 44 | 45 | require "retryable" 46 | include Retryable 47 | 48 | retryable( :tries => 3, 49 | :on => ArgumentError, 50 | :matching => /Bacon/, 51 | :sleep => 3) do 52 | 53 | # some crazy code about bacon 54 | end 55 | 56 | 57 | 58 | 59 | ## Defaults 60 | 61 | :tries => 1, :on => Exception, :matching => /.*/, :sleep => 0 62 | 63 | 64 | ## Thanks 65 | 66 | Many thanks to [Chu Yeow for this nifty piece of code](http://blog.codefront.net/2008/01/14/retrying-code-blocks-in-ruby-on-exceptions-whatever/). Look, I liked it 67 | enough to enhance it a little bit and build a gem from it! :) 68 | 69 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "rake/gempackagetask" 3 | require "rake/rdoctask" 4 | 5 | require "spec" 6 | require "spec/rake/spectask" 7 | Spec::Rake::SpecTask.new do |t| 8 | t.spec_opts = %w(--format specdoc --colour) 9 | t.libs = ["spec"] 10 | end 11 | 12 | 13 | task :default => ["spec"] 14 | 15 | # This builds the actual gem. For details of what all these options 16 | # mean, and other ones you can add, check the documentation here: 17 | # 18 | # http://rubygems.org/read/chapter/20 19 | # 20 | spec = Gem::Specification.new do |s| 21 | 22 | # Change these as appropriate 23 | s.name = "reretryable" 24 | s.version = "0.1.1" 25 | s.summary = "Runs a code block, and retries it when an exception occurs. Simple as'at." 26 | s.author = "Craig 'The Craif' Mackenzie and Niko Felger (pushed to rubygems by Brenton Fletcher)" 27 | s.email = "developers@songkick.com" 28 | s.homepage = "http://github.com/bloopletech/retryable" 29 | 30 | s.has_rdoc = true 31 | s.extra_rdoc_files = %w(README.markdown) 32 | s.rdoc_options = %w(--main README.markdown) 33 | 34 | # Add any extra files to include in the gem 35 | s.files = %w(README.markdown reretryable.gemspec) + Dir.glob("{spec,lib/**/*}") 36 | s.require_paths = ["lib"] 37 | 38 | # If you want to depend on other gems, add them here, along with any 39 | # relevant versions 40 | # s.add_dependency("some_other_gem", "~> 0.1.0") 41 | 42 | # If your tests use any gems, include them here 43 | s.add_development_dependency("rspec") 44 | end 45 | 46 | # This task actually builds the gem. We also regenerate a static 47 | # .gemspec file, which is useful if something (i.e. GitHub) will 48 | # be automatically building a gem for this project. If you're not 49 | # using GitHub, edit as appropriate. 50 | # 51 | # To publish your gem online, install the 'gemcutter' gem; Read more 52 | # about that here: http://gemcutter.org/pages/gem_docs 53 | Rake::GemPackageTask.new(spec) do |pkg| 54 | pkg.gem_spec = spec 55 | end 56 | 57 | desc "Build the gemspec file #{spec.name}.gemspec" 58 | task :gemspec do 59 | file = File.dirname(__FILE__) + "/#{spec.name}.gemspec" 60 | File.open(file, "w") {|f| f << spec.to_ruby } 61 | end 62 | 63 | task :package => :gemspec 64 | 65 | # Generate documentation 66 | Rake::RDocTask.new do |rd| 67 | rd.main = "README.markdown" 68 | rd.rdoc_files.include("README.markdown", "lib/**/*.rb") 69 | rd.rdoc_dir = "rdoc" 70 | end 71 | 72 | desc 'Clear out RDoc and generated packages' 73 | task :clean => [:clobber_rdoc, :clobber_package] do 74 | rm "#{spec.name}.gemspec" 75 | end 76 | -------------------------------------------------------------------------------- /spec/lib/retryable_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe "Retryable#retryable" do 4 | include Retryable 5 | 6 | def do_retry(opts = {}) 7 | @num_calls = 0 8 | return retryable(@retryable_opts) do 9 | @num_calls += 1 10 | if exception = opts[:raising] 11 | raise exception if opts[:when].nil? || opts[:when].call 12 | end 13 | opts[:returning] 14 | end 15 | end 16 | 17 | describe "with default options" do 18 | before(:each) do 19 | @retryable_opts = {} 20 | end 21 | 22 | it "should not affect the return value of the block given" do 23 | retryable { 'foo' }.should == 'foo' 24 | end 25 | 26 | it "should not affect the return value of the block given when there is a retry" do 27 | do_retry(:returning => 'foo', :raising => StandardError, :when => lambda { @num_calls == 1 } ).should == 'foo' 28 | @num_calls.should == 2 29 | end 30 | 31 | it "uses default options of :tries => 1 and :on => StandardError when none is given" do 32 | lambda {do_retry(:raising => StandardError)}.should raise_error(StandardError) 33 | @num_calls.should == 2 34 | end 35 | 36 | it "should not catch Exceptions by default" do 37 | lambda {do_retry(:raising => Exception)}.should raise_error(Exception) 38 | @num_calls.should == 1 39 | end 40 | 41 | it "does not retry if none of the retry conditions occur" do 42 | do_retry 43 | @num_calls.should == 1 44 | end 45 | end 46 | 47 | describe "with the :tries option set" do 48 | before(:each) do 49 | @retryable_opts = {:tries => 3} 50 | end 51 | 52 | it "uses retries :tries times when the exception to retry on occurs every time" do 53 | lambda {do_retry(:raising => StandardError)}.should raise_error(StandardError) 54 | @num_calls.should == 4 55 | end 56 | end 57 | 58 | describe "with the :on option set" do 59 | before(:each) do 60 | @retryable_opts = {:on => StandardError} 61 | end 62 | 63 | it "should catch any subclass exceptions" do 64 | do_retry(:raising => IOError, :when => lambda {@num_calls == 1}) 65 | @num_calls.should == 2 66 | end 67 | 68 | it "should not catch any superclass exceptions" do 69 | lambda {do_retry(:raising => Exception, :when => lambda {@num_calls == 1})}.should raise_error(Exception) 70 | @num_calls.should == 1 71 | end 72 | end 73 | 74 | describe "with the :matching option set" do 75 | before(:each) do 76 | @retryable_opts = {:matching => /IO timeout/} 77 | end 78 | 79 | it "should catch an exception that matches the regexp" do 80 | lambda {do_retry(:raising => "there was like an IO timeout and stuffs", :when => lambda {@num_calls == 1})}.should_not raise_error(RuntimeError) 81 | @num_calls.should == 2 82 | end 83 | 84 | it "should not catch an exception that doesn't match the regexp" do 85 | lambda {do_retry(:raising => "ERRROR of sorts", :when => lambda {@num_calls == 1})}.should raise_error(RuntimeError) 86 | @num_calls.should == 1 87 | end 88 | end 89 | 90 | describe "with all the options set" do 91 | before(:each) do 92 | @retryable_opts = { :tries => 3, 93 | :on => RuntimeError, 94 | :sleep => 0.3, 95 | :matching => /IO timeout/ } 96 | end 97 | 98 | it "should still work as expected" do 99 | lambda {do_retry(:raising => "my IO timeout", :when => lambda {@num_calls < 4})}.should_not raise_error 100 | @num_calls.should == 4 101 | end 102 | end 103 | end --------------------------------------------------------------------------------