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.
- 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:
|