retries 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ Gemfile.lock
3
+ doc/
4
+ .yardoc/
@@ -0,0 +1 @@
1
+ --no-private --protected --markup=markdown -- lib/**/*.rb
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in retries.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Ooyala, Inc.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,98 @@
1
+ # Retries
2
+
3
+ Retries is a small gem that provides a single function, `with_retries`, to evaluate a block with randomized,
4
+ truncated, exponential backoff.
5
+
6
+ There are similar projects out there (see [here](https://github.com/afazio/retry_block) and
7
+ [here](https://bitbucket.org/amanking/retry_this/wiki/Home)) but these will require you to implement the
8
+ backoff scheme yourself. If you don't need randomized exponential backoff, you should check out those gems.
9
+
10
+ ## Installation
11
+
12
+ You can get the gem with `gem install retries` or simply add `gem "retries"` to your Gemfile if you're using
13
+ bundler.
14
+
15
+ ## Usage
16
+
17
+ Suppose we have some task we are trying to perform: `do_the_thing`. This might be a call to a third-party API
18
+ or a flaky service. Here's how you can try it three times before failing:
19
+
20
+ ``` ruby
21
+ require "retries"
22
+
23
+ with_retries(:max_tries => 3) { do_the_thing }
24
+ ```
25
+
26
+ The block is passed a single parameter, `attempt_number`, which is the number of attempts that have been made
27
+ (starting at 1):
28
+
29
+ ``` ruby
30
+ with_retries(:max_tries => 3) do |attempt_number|
31
+ puts "Trying to do the thing: attempt #{attempt_number}"
32
+ do_the_thing
33
+ end
34
+ ```
35
+
36
+ ### Custom exceptions
37
+
38
+ By default `with_retries` rescues instances of `StandardError`. You'll likely want to make this more specific
39
+ to your use case. You may provide an exception class or an array of classes:
40
+
41
+ ``` ruby
42
+ with_retries(:max_tries => 3, :rescue => RestClient::Exception) { do_the_thing }
43
+ with_retries(:max_tries => 3, :rescue => [RestClient::Unauthorized, RestClient::RequestFailed]) do
44
+ do_the_thing
45
+ end
46
+ ```
47
+
48
+ ### Handlers
49
+
50
+ `with_retries` allows you to pass a custom handler that will be called each time before the block is retried.
51
+ The handler will be called with two arguments: `exception` (the rescued exception) and `attempt_number` (the
52
+ number of attempts that have been made thus far).
53
+
54
+ ``` ruby
55
+ handler = Proc.new do |exception, attempt_number|
56
+ puts "Handler saw a #{exception.class}; retry attempt #{attempt_number}"
57
+ end
58
+ with_retries(:max_tries => 5, :handler => handler, :rescue => [RuntimeError, ZeroDivisionError]) do |attempt|
59
+ (1 / 0) if attempt == 3
60
+ raise "hey!" if attempt < 5
61
+ end
62
+ ```
63
+
64
+ This will print:
65
+
66
+ ```
67
+ Handler saw a RuntimeError; retry attempt 1
68
+ Handler saw a RuntimeError; retry attempt 2
69
+ Handler saw a ZeroDivisionError; retry attempt 3
70
+ Handler saw a RuntimeError; retry attempt 4
71
+ ```
72
+
73
+ ### Delay parameters
74
+
75
+ By default, `with_retries` will wait about a half second between the first and second attempts, and then the
76
+ delay time will increase exponentially between attempts (but stay at no more than 1 second). The delays are
77
+ perturbed randomly. You can control the parameters via the two options `:base_sleep_seconds` and
78
+ `:max_sleep_seconds`. For instance, you can start the delay at 100ms and go up to a maximum of about 2
79
+ seconds:
80
+
81
+ ``` ruby
82
+ with_retries(:max_tries => 10, :base_sleep_seconds => 0.1, :max_sleep_seconds => 2.0) { do_the_thing }
83
+ ```
84
+
85
+ ## Issues
86
+
87
+ File tickets here on Github.
88
+
89
+ ## Development
90
+
91
+ To run the tests: first clone the repo, then
92
+
93
+ $ bundle install
94
+ $ bundle exec rake test
95
+
96
+ ## License
97
+
98
+ Retries is released under the [MIT License](http://opensource.org/licenses/mit-license.php/).
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ task :test => ["test:units"]
5
+
6
+ namespace :test do
7
+ Rake::TestTask.new(:units) do |task|
8
+ task.test_files = FileList["test/**/*.rb"]
9
+ end
10
+ end
@@ -0,0 +1,50 @@
1
+ require "retries/version"
2
+
3
+ module Kernel
4
+ # Runs the supplied code block an retries with an exponential backoff.
5
+ #
6
+ # @param [Hash] options the retry options.
7
+ # @option options [Fixnum] :max_tries (3) The maximum number of times to run the block.
8
+ # @option options [Float] :base_sleep_seconds (0.5) The starting delay between retries.
9
+ # @option options [Float] :max_sleep_seconds (1.0) The maximum to which to expand the delay between retries.
10
+ # @option options [Proc] :handler (nil) If not `nil`, a `Proc` that will be called for each retry. It will be
11
+ # passed two arguments, `exception` (the rescued exception) and `attempt_number`.
12
+ # @option options [Exception, <Exception>] :rescue (StandardError) This may be a specific exception class to
13
+ # rescue or an array of classes.
14
+ # @yield [attempt_number] The (required) block to be executed, which is passed the attempt number as a
15
+ # parameter.
16
+ def with_retries(options = {}, &block)
17
+ # Check the options and set defaults
18
+ options_error_string = "Error with options to with_retries:"
19
+ max_tries = options[:max_tries] || 3
20
+ raise "#{options_error_string} :max_tries must be greater than 0." unless max_tries > 0
21
+ base_sleep_seconds = options[:base_sleep_seconds] || 0.5
22
+ max_sleep_seconds = options[:max_sleep_seconds] || 1.0
23
+ if base_sleep_seconds > max_sleep_seconds
24
+ raise "#{options_error_string} :base_sleep_seconds cannot be greater than :max_sleep_seconds."
25
+ end
26
+ handler = options[:handler]
27
+ exception_types_to_rescue = options[:rescue] || StandardError
28
+ exception_types_to_rescue = [exception_types_to_rescue] unless exception_types_to_rescue.is_a?(Array)
29
+ raise "#{options_error_string} with_retries must be passed a block" unless block_given?
30
+
31
+ # Let's do this thing
32
+ attempts = 0
33
+ begin
34
+ attempts += 1
35
+ return block.call(attempts)
36
+ rescue *exception_types_to_rescue => exception
37
+ raise exception if attempts >= max_tries
38
+ handler.call(exception, attempts) if handler
39
+ # The sleep time is an exponentially-increasing function of base_sleep_seconds. But, it never exceeds
40
+ # max_sleep_seconds.
41
+ sleep_seconds = [base_sleep_seconds * (2 ** (attempts - 1)), max_sleep_seconds].min
42
+ # Randomize to a random value in the range sleep_seconds/2 .. sleep_seconds
43
+ sleep_seconds = sleep_seconds * (0.5 * (1 + rand()))
44
+ # But never sleep less than base_sleep_seconds
45
+ sleep_seconds = [base_sleep_seconds, sleep_seconds].max
46
+ sleep sleep_seconds
47
+ retry
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,3 @@
1
+ module Retries
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/retries/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Caleb Spare"]
6
+ gem.email = ["caleb@ooyala.com"]
7
+ gem.description = %q{Retries is a gem for retrying blocks with randomized exponential backoff.}
8
+ gem.summary = %q{Retries is a gem for retrying blocks with randomized exponential backoff.}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "retries"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Retries::VERSION
17
+
18
+ # For running the tests
19
+ gem.add_development_dependency "rake"
20
+ gem.add_development_dependency "scope"
21
+
22
+ # For generating the documentation
23
+ gem.add_development_dependency "yard"
24
+ gem.add_development_dependency "redcarpet"
25
+ end
@@ -0,0 +1,73 @@
1
+ require "scope"
2
+ require "minitest/autorun"
3
+
4
+ $:.unshift File.join(File.dirname(__FILE__), "../lib")
5
+ require "retries"
6
+
7
+ class CustomErrorA < RuntimeError; end
8
+ class CustomErrorB < RuntimeError; end
9
+
10
+ class RetriesTest < Scope::TestCase
11
+ context "with_retries" do
12
+ should "retry until successful" do
13
+ tries = 0
14
+ result = with_retries(:max_tries => 4, :base_sleep_seconds => 0, :max_sleep_seconds => 0,
15
+ :rescue => CustomErrorA) do |attempt|
16
+ tries += 1
17
+ # Verify that the attempt number passed in is accurate
18
+ assert_equal tries, attempt
19
+ raise CustomErrorA.new if tries < 4
20
+ "done"
21
+ end
22
+ assert_equal "done", result
23
+ assert_equal 4, tries
24
+ end
25
+
26
+ should "re-raise after :max_tries" do
27
+ assert_raises(CustomErrorA) do
28
+ with_retries(:base_sleep_seconds => 0, :max_sleep_seconds => 0, :rescue => CustomErrorA) do
29
+ raise CustomErrorA.new
30
+ end
31
+ end
32
+ end
33
+
34
+ should "immediately raise any exception not specified by :rescue" do
35
+ tries = 0
36
+ assert_raises(CustomErrorA) do
37
+ with_retries(:base_sleep_seconds => 0, :max_sleep_seconds => 0, :rescue => CustomErrorB) do
38
+ tries += 1
39
+ raise CustomErrorA.new
40
+ end
41
+ end
42
+ assert_equal 1, tries
43
+ end
44
+
45
+ should "allow for catching any of an array of exceptions specified by :rescue" do
46
+ result = with_retries(:max_tries => 3, :base_sleep_seconds => 0, :max_sleep_seconds => 0,
47
+ :rescue => [CustomErrorA, CustomErrorB]) do |attempt|
48
+ raise CustomErrorA.new if attempt == 0
49
+ raise CustomErrorB.new if attempt == 1
50
+ "done"
51
+ end
52
+ assert_equal "done", result
53
+ end
54
+
55
+ should "run :handler with the expected arguments upon each handled exception" do
56
+ exception_handler_run_times = 0
57
+ tries = 0
58
+ handler = Proc.new do |exception, attempt_number|
59
+ exception_handler_run_times += 1
60
+ # Check that the handler is passed the proper exception and attempt number
61
+ assert_equal exception_handler_run_times, attempt_number
62
+ assert exception.is_a?(CustomErrorA)
63
+ end
64
+ with_retries(:max_tries => 4, :base_sleep_seconds => 0, :max_sleep_seconds => 0, :handler => handler,
65
+ :rescue => CustomErrorA) do
66
+ tries += 1
67
+ raise CustomErrorA.new if tries < 4
68
+ end
69
+ assert_equal 4, tries
70
+ assert_equal 3, exception_handler_run_times
71
+ end
72
+ end
73
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: retries
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Caleb Spare
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-08-15 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: &19157880 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *19157880
25
+ - !ruby/object:Gem::Dependency
26
+ name: scope
27
+ requirement: &19157420 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *19157420
36
+ - !ruby/object:Gem::Dependency
37
+ name: yard
38
+ requirement: &19156980 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *19156980
47
+ - !ruby/object:Gem::Dependency
48
+ name: redcarpet
49
+ requirement: &19156540 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *19156540
58
+ description: Retries is a gem for retrying blocks with randomized exponential backoff.
59
+ email:
60
+ - caleb@ooyala.com
61
+ executables: []
62
+ extensions: []
63
+ extra_rdoc_files: []
64
+ files:
65
+ - .gitignore
66
+ - .yardopts
67
+ - Gemfile
68
+ - LICENSE
69
+ - README.md
70
+ - Rakefile
71
+ - lib/retries.rb
72
+ - lib/retries/version.rb
73
+ - retries.gemspec
74
+ - test/retries_test.rb
75
+ homepage: ''
76
+ licenses: []
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ! '>='
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubyforge_project:
95
+ rubygems_version: 1.8.10
96
+ signing_key:
97
+ specification_version: 3
98
+ summary: Retries is a gem for retrying blocks with randomized exponential backoff.
99
+ test_files:
100
+ - test/retries_test.rb
101
+ has_rdoc: