├── .gitignore ├── CHANGELOG ├── LICENSE ├── Manifest ├── README.markdown ├── Rakefile ├── lib └── retryable.rb ├── retryable-rb.gemspec └── test └── retryable_test.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.swp 3 | *.swo 4 | pkg 5 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | v1.1.0. Setting default retry sleep period to random. 2 | 3 | v1.0.0. First release. 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Robert Sosinski (http://www.robertsosinski.com) 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Manifest: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | LICENSE 3 | Manifest 4 | README.markdown 5 | Rakefile 6 | lib/retryable.rb 7 | retryable-rb.gemspec 8 | test/retryable_test.rb 9 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | Retryable is an easy to use DSL to retry code if an exception is raised. This is especially useful when interacting with unreliable services that fail randomly. 5 | 6 | Installation 7 | ------------ 8 | 9 | gem install retryable-rb 10 | 11 | # In your ruby application 12 | require 'retryable' 13 | 14 | Using Retryable 15 | --------------- 16 | 17 | Code wrapped in a Retryable block will be retried if a failure occurs. As such, code attempted once, will be retried again for another attempt if it fails to run. 18 | 19 | # Include Retryable into your class 20 | class Api 21 | include Retryable 22 | 23 | # Use it in methods that interact with unreliable services 24 | def get 25 | retryable do 26 | # code here... 27 | end 28 | end 29 | end 30 | 31 | By default, Retryable will rescue any exception inherited from `Exception`, retry once (for a total of two attempts) and sleep for a random amount time (between 0 to 100 milliseconds, in 10 millisecond increments). You can choose additional options by passing them via an options `Hash`. 32 | 33 | retryable :on => Timeout::Error, :times => 3, :sleep => 1 do 34 | # code here... 35 | end 36 | 37 | This example will only retry on a `Timeout::Error`, retry 3 times (for a total of 4 attempts) and sleep for a full second before each retry. You can also specify multiple errors to retry on by passing an array. 38 | 39 | retryable :on => [Timeout::Error, Errno::ECONNRESET] do 40 | # code here... 41 | end 42 | 43 | You can also have Ruby retry immediately after a failure by passing `false` as the sleep option. 44 | 45 | retryable :sleep => false do 46 | # code here... 47 | end 48 | 49 | Retryable also allows for callbacks to be defined, which is useful to log failures for analytics purposes or cleanup after repeated failures. Retryable has three types of callbacks: `then`, `finally`, and `always`. 50 | 51 | `then`: Run every time a failure occurs. 52 | 53 | `finally`: Run when the number of retries runs out. 54 | 55 | `always`: Run when the code wrapped in a Retryable block passes or when the number of retries runs out. 56 | 57 | The `then` and `finally` callbacks pass the exception raised, which can be used for logging or error control. All three callbacks also have a `handler`, which provides an interface to pass data between the code wrapped in the Retryable block and the callbacks defined. 58 | 59 | Furthermore, each callback provides the number of `attempts`, `retries` and `times` that the wrapped code should be retried. As these are specified in a `Proc`, unnecessary variables can be left out of the parameter list. 60 | 61 | then_cb = Proc.new do |exception, handler, attempts, retries, times| 62 | log "#{exception.class}: '#{exception.message}' - #{attempts} attempts, #{retries} out of #{times} retries left."} 63 | end 64 | 65 | finally_cb = Proc.new do |exception, handler| 66 | log "#{exception.class} raised too many times. First attempt at #{handler[:start]} and final attempt at #{Time.now}" 67 | end 68 | 69 | always_cb = Proc.new do |handler, attempts| 70 | log "total time for #{attempts} attempts: #{Time.now - handler[:start]}" 71 | end 72 | 73 | retryable :then => then_cb, :finally => finally_cb, :always => always_cb do |handler| 74 | handler[:start] ||= Time.now 75 | 76 | # code here... 77 | end 78 | 79 | If you are using Retryable once or outside of a class, you can also use it via its module method as well. 80 | 81 | Retryable.retryable do 82 | # code here... 83 | end 84 | 85 | Credits 86 | ------- 87 | 88 | Retryable was inspired by code written by [Michael Celona](http://github.com/mcelona) and later assisted by [David Malin](http://github.com/dmalin). 89 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'echoe' 4 | 5 | Rake::TestTask.new do |t| 6 | t.libs << "test" 7 | end 8 | 9 | Echoe.new("retryable-rb") do |p| 10 | p.author = "Robert Sosinski" 11 | p.email = "email@robertsosinski.com" 12 | p.url = "http://github.com/robertsosinski/retryable" 13 | p.description = p.summary = "Easy to use DSL to retry code if an exception is raised." 14 | p.runtime_dependencies = [] 15 | p.development_dependencies = ["echoe >=4.3.1"] 16 | end -------------------------------------------------------------------------------- /lib/retryable.rb: -------------------------------------------------------------------------------- 1 | module Retryable 2 | extend self 3 | 4 | def retryable(options = {}) 5 | opts = {:on => Exception, :times => 1}.merge(options) 6 | handler = {} 7 | 8 | retry_exception = opts[:on].is_a?(Array) ? opts[:on] : [opts[:on]] 9 | times = retries = opts[:times] 10 | attempts = 0 11 | 12 | begin 13 | attempts += 1 14 | 15 | return yield(handler) 16 | rescue *retry_exception => exception 17 | opts[:then].call(exception, handler, attempts, retries, times) if opts[:then] 18 | 19 | if attempts <= times 20 | sleep(opts[:sleep] || (rand(11) / 100.0)) unless opts[:sleep] == false 21 | retries -= 1 22 | retry 23 | else 24 | opts[:finally].call(exception, handler, attempts, retries, times) if opts[:finally] 25 | raise exception 26 | end 27 | ensure 28 | opts[:always].call(handler, attempts, retries, times) if opts[:always] 29 | end 30 | 31 | yield(handler) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /retryable-rb.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | Gem::Specification.new do |s| 4 | s.name = %q{retryable-rb} 5 | s.version = "1.1.0" 6 | 7 | s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version= 8 | s.authors = ["Robert Sosinski"] 9 | s.date = %q{2011-04-17} 10 | s.description = %q{Easy to use DSL to retry code if an exception is raised.} 11 | s.email = %q{email@robertsosinski.com} 12 | s.extra_rdoc_files = ["CHANGELOG", "LICENSE", "README.markdown", "lib/retryable.rb"] 13 | s.files = ["CHANGELOG", "LICENSE", "Manifest", "README.markdown", "Rakefile", "lib/retryable.rb", "retryable-rb.gemspec", "test/retryable_test.rb"] 14 | s.homepage = %q{http://github.com/robertsosinski/retryable} 15 | s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Retryable-rb", "--main", "README.markdown"] 16 | s.require_paths = ["lib"] 17 | s.rubyforge_project = %q{retryable-rb} 18 | s.rubygems_version = %q{1.3.7} 19 | s.summary = %q{Easy to use DSL to retry code if an exception is raised.} 20 | s.test_files = ["test/retryable_test.rb"] 21 | 22 | if s.respond_to? :specification_version then 23 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 24 | s.specification_version = 3 25 | 26 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 27 | s.add_development_dependency(%q, [">= 4.3.1"]) 28 | else 29 | s.add_dependency(%q, [">= 4.3.1"]) 30 | end 31 | else 32 | s.add_dependency(%q, [">= 4.3.1"]) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/retryable_test.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.dirname(__FILE__) + "../lib") 2 | 3 | require 'test/unit' 4 | require 'retryable' 5 | 6 | class RetryableTest < Test::Unit::TestCase 7 | def test_without_arguments 8 | i = 0 9 | 10 | Retryable.retryable do 11 | i += 1 12 | 13 | raise Exception.new 14 | end 15 | rescue Exception 16 | assert_equal i, 2 17 | end 18 | 19 | def test_with_one_exception_and_two_times 20 | i = 0 21 | 22 | Retryable.retryable :on => EOFError, :times => 2 do 23 | i += 1 24 | 25 | raise EOFError.new 26 | end 27 | 28 | rescue EOFError 29 | assert_equal i, 3 30 | end 31 | 32 | def test_with_arguments_and_handler 33 | i = 0 34 | 35 | then_cb = Proc.new do |e, h, a, r, t| 36 | assert_equal e.class, ArgumentError 37 | assert h[:value] 38 | 39 | assert_equal a, i 40 | assert_equal r, 6 - a 41 | assert_equal t, 5 42 | end 43 | 44 | finally_cb = Proc.new do |e, h, a, r, t| 45 | assert_equal e.class, ArgumentError 46 | assert h[:value] 47 | 48 | assert_equal a, 6 49 | assert_equal r, 0 50 | assert_equal t, 5 51 | end 52 | 53 | always_cb = Proc.new do |h, a, r, t| 54 | assert h[:value] 55 | 56 | assert_equal a, 6 57 | assert_equal r, 0 58 | assert_equal t, 5 59 | end 60 | 61 | Retryable.retryable :on => [EOFError, ArgumentError], :then => then_cb, :finally => finally_cb, :always => always_cb, :times => 5, :sleep => 0.2 do |h| 62 | i += 1 63 | 64 | h[:value] = true 65 | 66 | raise ArgumentError.new 67 | end 68 | 69 | rescue ArgumentError 70 | assert_equal i, 6 71 | end 72 | end --------------------------------------------------------------------------------