attempt_this 0.9.0 → 0.9.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/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
|