attempt_this 0.9.0 → 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +1 -5
- data/Gemfile.lock +22 -5
- data/README.md +13 -10
- data/attempt_this.gemspec +16 -9
- data/lib/attempt_this/attempt_object.rb +121 -129
- data/lib/attempt_this/attempt_this.rb +11 -11
- data/lib/attempt_this/binary_backoff_policy.rb +12 -12
- data/lib/attempt_this/exception_type_filter.rb +11 -11
- data/spec/attempt_spec.rb +292 -287
- data/spec/scenarios_spec.rb +47 -47
- data/spec/spec_helper.rb +1 -0
- metadata +98 -12
- checksums.yaml +0 -7
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,8 +1,17 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
attempt_this (0.9.1)
|
5
|
+
|
1
6
|
GEM
|
2
7
|
remote: http://rubygems.org/
|
3
8
|
specs:
|
4
9
|
diff-lcs (1.2.4)
|
5
|
-
|
10
|
+
docile (1.1.2)
|
11
|
+
macaddr (1.6.1)
|
12
|
+
systemu (~> 2.5.0)
|
13
|
+
multi_json (1.8.4)
|
14
|
+
rake (10.1.1)
|
6
15
|
rspec (2.13.0)
|
7
16
|
rspec-core (~> 2.13.0)
|
8
17
|
rspec-expectations (~> 2.13.0)
|
@@ -11,15 +20,23 @@ GEM
|
|
11
20
|
rspec-expectations (2.13.0)
|
12
21
|
diff-lcs (>= 1.1.3, < 2.0)
|
13
22
|
rspec-mocks (2.13.1)
|
14
|
-
simplecov (0.
|
15
|
-
|
16
|
-
|
17
|
-
|
23
|
+
simplecov (0.8.2)
|
24
|
+
docile (~> 1.1.0)
|
25
|
+
multi_json
|
26
|
+
simplecov-html (~> 0.8.0)
|
27
|
+
simplecov-html (0.8.0)
|
28
|
+
systemu (2.5.2)
|
29
|
+
uuid (2.3.7)
|
30
|
+
macaddr (~> 1.0)
|
18
31
|
|
19
32
|
PLATFORMS
|
20
33
|
ruby
|
21
34
|
x86-mingw32
|
22
35
|
|
23
36
|
DEPENDENCIES
|
37
|
+
attempt_this!
|
38
|
+
bundler
|
39
|
+
rake
|
24
40
|
rspec
|
25
41
|
simplecov
|
42
|
+
uuid
|
data/README.md
CHANGED
@@ -5,7 +5,7 @@ Exception-based retry policy mix-in for Ruby project. Its purpose it to retry a
|
|
5
5
|
```ruby
|
6
6
|
|
7
7
|
attempt(3.times) do
|
8
|
-
|
8
|
+
# Do something
|
9
9
|
end
|
10
10
|
```
|
11
11
|
This will retry the code block up to three times. If the last attempt will result in an exception, that exception will be thrown outside of the attempt block.
|
@@ -14,7 +14,7 @@ If you don't like that behavior, you can specify a function to be called after a
|
|
14
14
|
is_failed = false
|
15
15
|
attempt(3.times)
|
16
16
|
.and_default_to(->{is_failed = true}) do
|
17
|
-
|
17
|
+
# Do something
|
18
18
|
end
|
19
19
|
```
|
20
20
|
|
@@ -22,7 +22,7 @@ You may want to retry on specific exception types:
|
|
22
22
|
```ruby
|
23
23
|
attempt(3.times)
|
24
24
|
.with_filter(RecoverableError1, RecoverableError2) do
|
25
|
-
|
25
|
+
# Do something
|
26
26
|
end
|
27
27
|
```
|
28
28
|
|
@@ -30,9 +30,9 @@ You may chose how to reset the environment between failed attempts. This is usef
|
|
30
30
|
```ruby
|
31
31
|
attempt(3.times)
|
32
32
|
.with_reset(->{rollback}) do
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
start_transaction
|
34
|
+
# Do something
|
35
|
+
commit_transaction
|
36
36
|
end
|
37
37
|
```
|
38
38
|
You can specify delay between failed attempts:
|
@@ -42,19 +42,19 @@ You can specify delay between failed attempts:
|
|
42
42
|
# Wait for 5 seconds between failures.
|
43
43
|
attempt(3.times)
|
44
44
|
.with_delay(5) do
|
45
|
-
|
45
|
+
# Do something
|
46
46
|
end
|
47
47
|
|
48
48
|
# Random delay between 30 and 60 seconds.
|
49
49
|
attempt(3.times)
|
50
50
|
.with_delay([30..60]) do
|
51
|
-
|
51
|
+
# Do something
|
52
52
|
end
|
53
53
|
|
54
54
|
# Start with 10 seconds delay and double it after each failed attempt.
|
55
55
|
attempt(5.times)
|
56
56
|
.with_binary_backoff(10) do
|
57
|
-
|
57
|
+
# Do something
|
58
58
|
end
|
59
59
|
```
|
60
60
|
|
@@ -77,8 +77,11 @@ AttemptThis.attempt(5.times).with_filter(*RECOVERABLE_HTTP_ERRORS).scenario(:htt
|
|
77
77
|
|
78
78
|
# And run this from your method:
|
79
79
|
attempt(:http) do
|
80
|
-
|
80
|
+
# Make an HTTP call
|
81
81
|
end
|
82
82
|
```
|
83
83
|
|
84
|
+
# Return values
|
85
|
+
Calling 'attempt' will return result of the code block if there was no exception; otherwise it will return result of the default handler.
|
86
|
+
|
84
87
|
Enjoy! And feel free to contribute; just make sure you haven't broken any tests by running 'rake' from project's root.
|
data/attempt_this.gemspec
CHANGED
@@ -1,15 +1,22 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
2
|
+
s.name = 'attempt_this'
|
3
|
+
s.version = '0.9.1'
|
4
|
+
s.date = '2014-02-18'
|
5
|
+
s.summary = 'Retry policy mix-in'
|
6
|
+
s.description = <<EOM
|
7
7
|
Retry policy mix-in with configurable number of attempts, delays, exception filters, and fall back strategies.
|
8
8
|
|
9
9
|
See project's home page for usage examples and more information.
|
10
10
|
EOM
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
s.authors = ['Aliaksei Baturytski']
|
12
|
+
s.email = 'abaturytski@gmail.com'
|
13
|
+
s.files = `git ls-files`.split($/)
|
14
|
+
s.homepage = 'https://github.com/aliakb/attempt_this'
|
15
|
+
s.license = 'MIT'
|
16
|
+
|
17
|
+
s.add_development_dependency('bundler')
|
18
|
+
s.add_development_dependency('rake')
|
19
|
+
s.add_development_dependency('rspec')
|
20
|
+
s.add_development_dependency('simplecov')
|
21
|
+
s.add_development_dependency('uuid')
|
15
22
|
end
|
@@ -2,133 +2,125 @@ require_relative 'binary_backoff_policy.rb'
|
|
2
2
|
require_relative 'exception_type_filter.rb'
|
3
3
|
|
4
4
|
module AttemptThis
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
# Creates a scenario with the given id.
|
127
|
-
def scenario(id)
|
128
|
-
raise(ArgumentError, 'Blank id!') if id.nil? || id.empty?
|
129
|
-
raise(ArgumentError, "There is already a scenario with id #{id}") if @@scenarios.has_key?(id)
|
130
|
-
|
131
|
-
@@scenarios[id] = self
|
132
|
-
end
|
133
|
-
end
|
5
|
+
# Retry policy implementation.
|
6
|
+
# This class is internal and is not supposed to be used outside of the module.
|
7
|
+
class AttemptObject
|
8
|
+
@@scenarios = {} # All registered scenarios
|
9
|
+
|
10
|
+
# Resets all static data.
|
11
|
+
def self.reset
|
12
|
+
@@scenarios = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.get_object(id_or_enumerator)
|
16
|
+
impl = @@scenarios[id_or_enumerator]
|
17
|
+
impl ||= AttemptObject.new(id_or_enumerator)
|
18
|
+
impl
|
19
|
+
end
|
20
|
+
|
21
|
+
# Initializes object with enumerator.
|
22
|
+
def initialize(enumerator)
|
23
|
+
@enumerator = enumerator
|
24
|
+
end
|
25
|
+
|
26
|
+
# Executes the code block.
|
27
|
+
def attempt(block)
|
28
|
+
# Returning self will allow chaining calls
|
29
|
+
return self unless block
|
30
|
+
|
31
|
+
last_exception = nil
|
32
|
+
first_time = true
|
33
|
+
|
34
|
+
@delay_policy = ->{} unless @delay_policy
|
35
|
+
@reset_method = ->{} unless @reset_method
|
36
|
+
@exception_filter = ExceptionTypeFilter.new([StandardError]) unless @exception_filter
|
37
|
+
|
38
|
+
@enumerator.rewind
|
39
|
+
@enumerator.with_index do |i|
|
40
|
+
@delay_policy.call unless i == 0
|
41
|
+
last_exception = nil
|
42
|
+
begin
|
43
|
+
return block.call
|
44
|
+
rescue Exception => ex
|
45
|
+
raise unless @exception_filter.include?(ex)
|
46
|
+
@reset_method.call
|
47
|
+
last_exception = ex
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
if (last_exception)
|
52
|
+
return @default_method[] if @default_method
|
53
|
+
raise last_exception
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Specifies delay in seconds between failed attempts.
|
58
|
+
def with_delay(delay, &block)
|
59
|
+
# Delay should be either an integer or a range of integers.
|
60
|
+
if (delay.is_a?(Numeric))
|
61
|
+
raise(ArgumentError, "Delay should be a non-negative number; got #{delay}!") unless delay >= 0
|
62
|
+
delay = delay..delay
|
63
|
+
elsif delay.is_a?(Range)
|
64
|
+
raise(ArgumentError, "Range members should be numbers; got #{delay}!") unless delay.first.is_a?(Numeric) && delay.last.is_a?(Numeric)
|
65
|
+
raise(ArgumentError, "Range members should be non-negative; got #{delay}!") unless delay.first >= 0 && delay.last >= 0
|
66
|
+
raise(ArgumentError, "Range's end should be greater than or equal to range's start; got #{delay}!") unless delay.first <= delay.last
|
67
|
+
else
|
68
|
+
raise(ArgumentError, "Delay should be either an number or a range of numbers; got #{delay}!")
|
69
|
+
end
|
70
|
+
raise(ArgumentError, 'Delay policy has already been specified!') if @delay_policy
|
71
|
+
@delay_policy = ->{Kernel.sleep(rand(delay))}
|
72
|
+
|
73
|
+
attempt(block)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Specifies reset method that will be called after each failed attempt.
|
77
|
+
def with_reset(reset_method, &block)
|
78
|
+
raise(ArgumentError, 'Reset method is nil!') unless reset_method
|
79
|
+
raise(ArgumentError, 'Reset method has already been speicifed!') if @reset_method
|
80
|
+
|
81
|
+
@reset_method = reset_method
|
82
|
+
attempt(block)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Specifies default method that should be called after all attempts have failed.
|
86
|
+
def and_default_to(default_method, &block)
|
87
|
+
raise(ArgumentError, 'Default method is nil!') unless default_method
|
88
|
+
raise(ArgumentError, 'Default method has already been specified!') if @default_method
|
89
|
+
|
90
|
+
@default_method = default_method
|
91
|
+
attempt(block)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Specifies delay which doubles between failed attempts.
|
95
|
+
def with_binary_backoff(initial_delay, &block)
|
96
|
+
raise(ArgumentError, "Delay should be a number; got ${initial_delay}!") unless initial_delay.is_a?(Numeric)
|
97
|
+
raise(ArgumentError, "Delay should be a positive number; got #{initial_delay}!") unless initial_delay > 0
|
98
|
+
raise(ArgumentError, "Delay policy has already been specified!") if @delay_policy
|
99
|
+
|
100
|
+
@delay_policy = BinaryBackoffPolicy.new(initial_delay)
|
101
|
+
attempt(block)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Specifies exceptions
|
105
|
+
def with_filter(*exceptions, &block)
|
106
|
+
raise(ArgumentError, "Empty exceptions list!") unless exceptions.size > 0
|
107
|
+
# Everything must be an exception.
|
108
|
+
exceptions.each do |e|
|
109
|
+
raise(ArgumentError, "Not an exception: #{e}!") unless e <= Exception
|
110
|
+
end
|
111
|
+
|
112
|
+
raise(ArgumentError, "Exception filter has already been specified!") if @exception_filter
|
113
|
+
|
114
|
+
@exception_filter = ExceptionTypeFilter.new(exceptions)
|
115
|
+
attempt(block)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Creates a scenario with the given id.
|
119
|
+
def scenario(id)
|
120
|
+
raise(ArgumentError, 'Blank id!') if id.nil? || id.empty?
|
121
|
+
raise(ArgumentError, "There is already a scenario with id #{id}") if @@scenarios.has_key?(id)
|
122
|
+
|
123
|
+
@@scenarios[id] = self
|
124
|
+
end
|
125
|
+
end
|
134
126
|
end
|
@@ -1,18 +1,18 @@
|
|
1
1
|
require 'attempt_this/attempt_object.rb'
|
2
2
|
|
3
3
|
module AttemptThis
|
4
|
-
|
4
|
+
extend self
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
# Attempts code block until it doesn't throw an exception or the end of enumerator has been reached.
|
7
|
+
def attempt(enumerator, &block)
|
8
|
+
raise(ArgumentError, 'Nil enumerator!') if enumerator.nil?
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
impl = AttemptObject::get_object(enumerator)
|
11
|
+
impl.attempt(block)
|
12
|
+
end
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
# Resets all static data (scenarios). This is intended to use by tests only (to reset scenarios)
|
15
|
+
def self.reset
|
16
|
+
AttemptObject.reset
|
17
|
+
end
|
18
18
|
end
|