retries 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.yardopts +1 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +98 -0
- data/Rakefile +10 -0
- data/lib/retries.rb +50 -0
- data/lib/retries/version.rb +3 -0
- data/retries.gemspec +25 -0
- data/test/retries_test.rb +73 -0
- metadata +101 -0
data/.gitignore
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--no-private --protected --markup=markdown -- lib/**/*.rb
|
data/Gemfile
ADDED
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.
|
data/README.md
ADDED
@@ -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/).
|
data/Rakefile
ADDED
data/lib/retries.rb
ADDED
@@ -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
|
data/retries.gemspec
ADDED
@@ -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:
|