attempt_this 0.8.1 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 99adc9216b884599d6cdf76e1232bd76ac60f58f
4
+ data.tar.gz: f284433eefff3989b31ffefe6438264c19b7c6ee
5
+ SHA512:
6
+ metadata.gz: 5b26edd0f899dd2d1f37ca0ea35a888b4a246e565b190a8e970cff6c85bc78ab81177c5f5f1687e8fcf7f776c58c5944a800e639d81e21a387aefd08f3cc7827
7
+ data.tar.gz: 8938c34e49c4ea02ed9123c655900e62914b72895a84d75d24bc656268340be9ca66e5ebf9430984870e050f1ad9178b05aca70aa12cf593d5a1a4972e121dd1
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ .idea/*
2
+ attempt_this-*.gem
3
+ coverage/*
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gem 'rspec'
4
+
5
+ group :test do
6
+ gem 'simplecov', require: false
7
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,25 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ diff-lcs (1.2.4)
5
+ multi_json (1.7.7)
6
+ rspec (2.13.0)
7
+ rspec-core (~> 2.13.0)
8
+ rspec-expectations (~> 2.13.0)
9
+ rspec-mocks (~> 2.13.0)
10
+ rspec-core (2.13.1)
11
+ rspec-expectations (2.13.0)
12
+ diff-lcs (>= 1.1.3, < 2.0)
13
+ rspec-mocks (2.13.1)
14
+ simplecov (0.7.1)
15
+ multi_json (~> 1.0)
16
+ simplecov-html (~> 0.7.1)
17
+ simplecov-html (0.7.1)
18
+
19
+ PLATFORMS
20
+ ruby
21
+ x86-mingw32
22
+
23
+ DEPENDENCIES
24
+ rspec
25
+ simplecov
data/README.md ADDED
@@ -0,0 +1,84 @@
1
+ attempt_this
2
+ ============
3
+
4
+ Exception-based retry policy mix-in for Ruby project. Its purpose it to retry a code block given number of times if it throws an exception.
5
+ ```ruby
6
+
7
+ attempt(3.times) do
8
+ # Do something
9
+ end
10
+ ```
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.
12
+ If you don't like that behavior, you can specify a function to be called after all attempts have failed.
13
+ ```ruby
14
+ is_failed = false
15
+ attempt(3.times)
16
+ .and_default_to(->{is_failed = true}) do
17
+ # Do something
18
+ end
19
+ ```
20
+
21
+ You may want to retry on specific exception types:
22
+ ```ruby
23
+ attempt(3.times)
24
+ .with_filter(RecoverableError1, RecoverableError2) do
25
+ # Do something
26
+ end
27
+ ```
28
+
29
+ You may chose how to reset the environment between failed attempts. This is useful for transactions:
30
+ ```ruby
31
+ attempt(3.times)
32
+ .with_reset(->{rollback}) do
33
+ start_transaction
34
+ # Do something
35
+ commit_transaction
36
+ end
37
+ ```
38
+ You can specify delay between failed attempts:
39
+
40
+ ```ruby
41
+
42
+ # Wait for 5 seconds between failures.
43
+ attempt(3.times)
44
+ .with_delay(5) do
45
+ # Do something
46
+ end
47
+
48
+ # Random delay between 30 and 60 seconds.
49
+ attempt(3.times)
50
+ .with_delay([30..60]) do
51
+ # Do something
52
+ end
53
+
54
+ # Start with 10 seconds delay and double it after each failed attempt.
55
+ attempt(5.times)
56
+ .with_binary_backoff(10) do
57
+ # Do something
58
+ end
59
+ ```
60
+
61
+ Of course, you can combine multiple options together:
62
+ ```ruby
63
+ attempt(3.times)
64
+ .with_delay(30)
65
+ .with_reset(->{rollback})
66
+ .and_default_to(->{is_failed = true}) do
67
+ # Do something
68
+ end
69
+ ```
70
+
71
+ Finally, you can store various configurations as scenarios and use them by name. This is useful when you need different approaches for specific cases (handling HTTP failures, for example). But remember that you should register your scenarios before using them:
72
+
73
+ ```ruby
74
+ # Run this code when the application starts
75
+ AttemptThis.attempt(5.times).with_filter(*RECOVERABLE_HTTP_ERRORS).scenario(:http)
76
+
77
+
78
+ # And run this from your method:
79
+ attempt(:http) do
80
+ # Make an HTTP call
81
+ end
82
+ ```
83
+
84
+ Enjoy! And feel free to contribute; just make sure you haven't broken any tests by running 'rake' from project's root.
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ RSpec::Core::RakeTask.new(:spec)
4
+
5
+ task :default => :spec
@@ -0,0 +1,15 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'attempt_this'
3
+ s.version = '0.9.0'
4
+ s.date = '2013-05-05'
5
+ s.summary = 'Retry policy mix-in'
6
+ s.description = <<EOM
7
+ Retry policy mix-in with configurable number of attempts, delays, exception filters, and fall back strategies.
8
+
9
+ See project's home page for usage examples and more information.
10
+ EOM
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
+ end
@@ -5,6 +5,19 @@ module AttemptThis
5
5
  # Retry policy implementation.
6
6
  # This class is internal and is not supposed to be used outside of the module.
7
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
+
8
21
  # Initializes object with enumerator.
9
22
  def initialize(enumerator)
10
23
  @enumerator = enumerator
@@ -20,6 +33,7 @@ module AttemptThis
20
33
  @reset_method = ->{} unless @reset_method
21
34
  @exception_filter = ExceptionTypeFilter.new([StandardError]) unless @exception_filter
22
35
 
36
+ @enumerator.rewind
23
37
  @enumerator.each do
24
38
  @delay_policy.call unless first_time
25
39
  last_exception = nil
@@ -108,5 +122,13 @@ module AttemptThis
108
122
  @exception_filter = ExceptionTypeFilter.new(exceptions)
109
123
  attempt(block)
110
124
  end
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
111
133
  end
112
134
  end
@@ -0,0 +1,18 @@
1
+ require 'attempt_this/attempt_object.rb'
2
+
3
+ module AttemptThis
4
+ extend self
5
+
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
+
10
+ impl = AttemptObject::get_object(enumerator)
11
+ impl.attempt(block)
12
+ end
13
+
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
+ end
data/lib/attempt_this.rb CHANGED
@@ -1,11 +1,2 @@
1
- require 'attempt_this/attempt_object.rb'
1
+ require 'attempt_this/attempt_this'
2
2
 
3
- module AttemptThis
4
- # Attempts code block until it doesn't throw an exception or the end of enumerator has been reached.
5
- def attempt(enumerator, &block)
6
- raise(ArgumentError, 'Nil enumerator!') if enumerator.nil?
7
-
8
- impl = AttemptObject.new(enumerator)
9
- impl.attempt(block)
10
- end
11
- end
@@ -0,0 +1,291 @@
1
+ require 'spec_helper.rb'
2
+
3
+ describe AttemptThis do
4
+ include AttemptThis
5
+
6
+ context 'attempt' do
7
+ it 'should reject nil enumerator' do
8
+ ->{attempt(nil)}.should raise_error(ArgumentError)
9
+ end
10
+
11
+ it 'should allow execution without a code block' do
12
+ ->{attempt(3.times)}.should_not raise_error
13
+ end
14
+
15
+ it 'should execute code block' do
16
+ was_called = false
17
+ attempt(1.times) {was_called = true}
18
+ was_called.should be_true
19
+ end
20
+
21
+ it 'should execute the code block only once' do
22
+ call_count = 0
23
+ attempt(3.times) { call_count += 1 }
24
+ call_count.should eql(1)
25
+ end
26
+
27
+ it 'should not execute the code' do
28
+ call_count = 0
29
+ attempt(0.times) { call_count += 1 }
30
+
31
+ call_count.should eql(0)
32
+ end
33
+
34
+ it 'should re-throw the original exception' do
35
+ ->{attempt(2.times){raise 'Test'}}.should raise_error('Test')
36
+ end
37
+
38
+ it 'should attempt 3 times' do
39
+ call_count = 0
40
+ ->{attempt(3.times) { call_count += 1; raise 'Test'}}.should raise_error('Test')
41
+ call_count.should eql(3)
42
+ end
43
+
44
+ it 'should stop trying after a successful attempt' do
45
+ attempt_count = 0
46
+ attempt(3.times) do
47
+ attempt_count += 1
48
+ raise 'Test' if attempt_count < 2
49
+ end
50
+
51
+ attempt_count.should eql(2)
52
+ end
53
+ end
54
+
55
+ context 'with_delay' do
56
+ it 'should reject nil delay' do
57
+ ->{attempt(3.times).with_delay(nil)}.should raise_error(ArgumentError)
58
+ end
59
+
60
+ it 'should reject negative delay' do
61
+ ->{attempt(3.times).with_delay(-1)}.should raise_error(ArgumentError)
62
+ end
63
+
64
+ it 'should reject non-number delay' do
65
+ ->{attempt(3.times).with_delay('foo')}.should raise_error(ArgumentError)
66
+ end
67
+
68
+ it 'should accept floating point delay' do
69
+ ->{attempt(3.times).with_delay(1.5)}.should_not raise_error
70
+ end
71
+
72
+ it 'should accept zero delay' do
73
+ ->{attempt(3.times).with_delay(0)}.should_not raise_error
74
+ end
75
+
76
+ it 'should accept calls without a code block' do
77
+ ->{attempt(3.times).with_delay(3)}.should_not raise_error
78
+ end
79
+
80
+ it 'should call the code block' do
81
+ was_called = false
82
+ attempt(3.times).with_delay(1) do
83
+ was_called = true
84
+ end
85
+ was_called.should be_true
86
+ end
87
+
88
+ it 'should not sleep on success' do
89
+ Kernel.should_not_receive(:sleep)
90
+ attempt(3.times).with_delay(3) {}
91
+ end
92
+
93
+ it 'should sleep for given number of seconds between failed attempts' do
94
+ Kernel.should_receive(:sleep).with(5).exactly(2).times
95
+ ->{attempt(3.times).with_delay(5) {raise 'Test'}}.should raise_error('Test')
96
+ end
97
+
98
+ it 'should not fail on zero delay' do
99
+ ->{attempt(3.times).with_delay(0) { raise 'Test' }}.should raise_error('Test')
100
+ end
101
+
102
+ it 'should reject negative start' do
103
+ ->{attempt(3.times).with_delay(-1..1)}.should raise_error(ArgumentError)
104
+ end
105
+
106
+ it 'should reject negative end' do
107
+ ->{attempt(3.times).with_delay(1..-1)}.should raise_error(ArgumentError)
108
+ end
109
+
110
+ it 'should reject non-number range' do
111
+ ->{attempt(3.times).with_delay('x'..'y')}.should raise_error(ArgumentError)
112
+ end
113
+
114
+ it 'should accept floating point range' do
115
+ ->{attempt(3.times).with_delay(1.5..3)}.should_not raise_error
116
+ end
117
+
118
+ it 'should reject inverse range' do
119
+ ->{attempt(2.times).with_delay(3..1)}.should raise_error(ArgumentError)
120
+ end
121
+
122
+ it 'should accept zero seconds interval' do
123
+ ->{attempt(3.times).with_delay(0..0)}.should_not raise_error
124
+ end
125
+
126
+ it 'should wait for specified number of seconds' do
127
+ Kernel.should_receive(:sleep).with(5).exactly(2).times
128
+ ->{attempt(3.times).with_delay(5..5){raise 'Test'}}.should raise_error('Test')
129
+ end
130
+
131
+ it 'should reject multiple delay policies' do
132
+ ->{attempt(3.times).with_delay(1).with_delay(1)}.should raise_error(ArgumentError)
133
+ end
134
+ end
135
+
136
+ context 'with_reset' do
137
+ it 'should reject nil reset proc' do
138
+ ->{attempt(3.times).with_reset(nil)}.should raise_error(ArgumentError)
139
+ end
140
+
141
+ it 'should accept calls without a code block' do
142
+ ->{attempt(3.times).with_reset(->{})}.should_not raise_error
143
+ end
144
+
145
+ it 'should call the code block' do
146
+ was_called = false
147
+ attempt(1.times).with_reset(->{}) { was_called = true }
148
+
149
+ was_called.should be_true
150
+ end
151
+
152
+ it 'should reject multiple reset procs' do
153
+ ->{attempt(3.times).with_reset(->{}).with_reset(->{})}.should raise_error(ArgumentError)
154
+ end
155
+
156
+ it 'should not be called on successful calls' do
157
+ was_called = false
158
+
159
+ attempt(1.times).with_reset(->{ was_called = true }) {}
160
+ was_called.should be_false
161
+ end
162
+
163
+ it 'should be called on each failure' do
164
+ reset_count = 0
165
+
166
+ ->{attempt(3.times).with_reset(->{ reset_count += 1 }) { raise 'Test' }}.should raise_error('Test')
167
+ reset_count.should eql(3)
168
+ end
169
+ end
170
+
171
+ context 'and_default_to' do
172
+ it 'should reject nil default method' do
173
+ ->{attempt(3.times).and_default_to(nil)}.should raise_error(ArgumentError)
174
+ end
175
+
176
+ it 'should reject duplicate default methods' do
177
+ ->{attempt(3.times).and_default_to(->{}).and_default_to(->{})}.should raise_error(ArgumentError)
178
+ end
179
+
180
+ it 'should allow calls without a code block' do
181
+ ->{attempt(3.times).and_default_to(->{})}.should_not raise_error
182
+ end
183
+
184
+ it 'should call the code block' do
185
+ was_called = false
186
+ attempt(3.times).and_default_to(->{}){ was_called = true }
187
+
188
+ was_called.should be_true
189
+ end
190
+
191
+ it 'should not be called on success' do
192
+ was_called = false
193
+ attempt(3.times).and_default_to(->{ was_called = true }) {}
194
+ was_called.should be_false
195
+ end
196
+
197
+ it 'should be called once on the failure' do
198
+ call_count = 0
199
+ attempt(3.times).and_default_to(->{ call_count += 1 }){ raise 'Test'}
200
+
201
+ call_count.should eql(1)
202
+ end
203
+
204
+ it 'should not be called if code block stopped failing' do
205
+ call_count = 0
206
+ was_called = false
207
+
208
+ attempt(3.times).and_default_to(->{ was_called = true }) { call_count += 1; raise 'Test' if call_count < 2 }
209
+ was_called.should be_false
210
+ end
211
+ end
212
+
213
+ context 'with_binary_backoff' do
214
+ it 'should reject nil initial delay' do
215
+ ->{attempt(3.times).with_binary_backoff(nil)}.should raise_error(ArgumentError)
216
+ end
217
+
218
+ it 'should reject non-integer initial delay' do
219
+ ->{attempt(3.times).with_binary_backoff('foo')}.should raise_error(ArgumentError)
220
+ end
221
+
222
+ it 'should reject zero initial delay' do
223
+ ->{attempt(3.times).with_binary_backoff(0)}.should raise_error(ArgumentError)
224
+ end
225
+
226
+ it 'should reject negative initial delay' do
227
+ ->{attempt(3.times).with_binary_backoff(-1)}.should raise_error(ArgumentError)
228
+ end
229
+
230
+ it 'should reject multiple policies' do
231
+ ->{attempt(3.times).with_binary_backoff(1).with_binary_backoff(2)}.should raise_error(ArgumentError)
232
+ end
233
+
234
+ it 'should accept calls without a code block' do
235
+ ->{attempt(3.times).with_binary_backoff(1)}.should_not raise_error
236
+ end
237
+
238
+ it 'should call the code block' do
239
+ was_called = false
240
+
241
+ attempt(3.times).with_binary_backoff(1) { was_called = true }
242
+ was_called.should be_true
243
+ end
244
+
245
+ it 'should double delay on each failure' do
246
+ Kernel.should_receive(:sleep).ordered.with(1)
247
+ Kernel.should_receive(:sleep).ordered.with(2)
248
+ Kernel.should_receive(:sleep).ordered.with(4)
249
+
250
+ attempt(4.times).with_binary_backoff(1).and_default_to(->{}) { raise 'Test' }
251
+ end
252
+ end
253
+
254
+ context 'with_filter' do
255
+ it 'should reject empty exceptions list' do
256
+ ->{attempt.with_filter}.should raise_error(ArgumentError)
257
+ end
258
+
259
+ it 'should reject non-exceptions' do
260
+ ->{attempt.with_filter(1)}.should raise_error(ArgumentError)
261
+ end
262
+
263
+ it 'should accept calls without a block' do
264
+ ->{attempt(2.times).with_filter(Exception)}.should_not raise_error
265
+ end
266
+
267
+ it 'should call code within the block' do
268
+ was_called = false
269
+ attempt(2.times).with_filter(Exception){ was_called = true }
270
+ was_called.should be_true
271
+ end
272
+
273
+ it 'should ignore other exceptions' do
274
+ count = 0
275
+ ->{attempt(3.times).with_filter(StandardError){ count += 1; raise(Exception, 'Test')}}.should raise_error(Exception)
276
+ count.should eql(1)
277
+ end
278
+
279
+ it 'should not ignore specified exceptions' do
280
+ count = 0
281
+ ->{attempt(3.times).with_filter(RuntimeError){ count += 1; raise 'Test'}}.should raise_error(RuntimeError)
282
+ count.should eql(3)
283
+ end
284
+
285
+ it 'should not ignore derived exceptions' do
286
+ count = 0
287
+ ->{attempt(3.times).with_filter(Exception){ count += 1; raise(StandardError, 'Test')}}.should raise_error(StandardError)
288
+ count.should eql(3)
289
+ end
290
+ end
291
+ end
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe AttemptThis do
4
+ include AttemptThis
5
+
6
+ before(:each) do
7
+ AttemptThis.reset
8
+ end
9
+
10
+ context 'validation' do
11
+ it 'should reject nil scenario id' do
12
+ ->{AttemptThis.attempt(3.times).scenario(nil)}.should raise_error(ArgumentError)
13
+ end
14
+
15
+ it 'should reject empty scenario id' do
16
+ ->{AttemptThis.attempt(3.times).scenario('')}.should raise_error(ArgumentError)
17
+ end
18
+
19
+ it 'should accept string ids' do
20
+ ->{AttemptThis.attempt(3.times).scenario('uploads')}.should_not raise_error
21
+ end
22
+
23
+ it 'should accept symbol ids' do
24
+ ->{AttemptThis.attempt(3.times).scenario(:uploads)}.should_not raise_error
25
+ end
26
+
27
+ it 'should reject duplicate names' do
28
+ AttemptThis.attempt(3.times).scenario(:uploads)
29
+ ->{AttemptThis.attempt(3.times).scenario(:uploads)}.should raise_error(ArgumentError)
30
+ end
31
+ end
32
+
33
+ context 'operation' do
34
+ it 'should attempt given number of times' do
35
+ AttemptThis.attempt(3.times).scenario(:test)
36
+ count = 0
37
+
38
+ ->{attempt(:test) { count += 1; raise 'Test' }}.should raise_error('Test')
39
+ count.should eql(3)
40
+ end
41
+
42
+ it 'should reuse scenario' do
43
+ AttemptThis.attempt(3.times).scenario(:test)
44
+ ->{attempt(:test) { raise 'Test'}}.should raise_error('Test')
45
+
46
+ count = 0
47
+ ->{attempt(:test) {count += 1; raise 'Test'}}.should raise_error('Test')
48
+ count.should eql(3)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,4 @@
1
+ require 'simplecov'
2
+ SimpleCov.start
3
+
4
+ require_relative '../lib/attempt_this.rb'
metadata CHANGED
@@ -1,8 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: attempt_this
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.1
5
- prerelease:
4
+ version: 0.9.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - Aliaksei Baturytski
@@ -11,44 +10,50 @@ bindir: bin
11
10
  cert_chain: []
12
11
  date: 2013-05-05 00:00:00.000000000 Z
13
12
  dependencies: []
14
- description: ! 'Retry policy mix-in with configurable number of attempts, delays,
15
- exception filters, and fall back strategies.
13
+ description: |
14
+ Retry policy mix-in with configurable number of attempts, delays, exception filters, and fall back strategies.
16
15
 
17
-
18
- See project''s home page for usage examples and more information.
19
-
20
- '
16
+ See project's home page for usage examples and more information.
21
17
  email: abaturytski@gmail.com
22
18
  executables: []
23
19
  extensions: []
24
20
  extra_rdoc_files: []
25
21
  files:
22
+ - .gitignore
23
+ - Gemfile
24
+ - Gemfile.lock
25
+ - README.md
26
+ - Rakefile
27
+ - attempt_this.gemspec
26
28
  - lib/attempt_this.rb
27
29
  - lib/attempt_this/attempt_object.rb
30
+ - lib/attempt_this/attempt_this.rb
28
31
  - lib/attempt_this/binary_backoff_policy.rb
29
32
  - lib/attempt_this/exception_type_filter.rb
33
+ - spec/attempt_spec.rb
34
+ - spec/scenarios_spec.rb
35
+ - spec/spec_helper.rb
30
36
  homepage: https://github.com/aliakb/attempt_this
31
37
  licenses: []
38
+ metadata: {}
32
39
  post_install_message:
33
40
  rdoc_options: []
34
41
  require_paths:
35
42
  - lib
36
43
  required_ruby_version: !ruby/object:Gem::Requirement
37
- none: false
38
44
  requirements:
39
- - - ! '>='
45
+ - - '>='
40
46
  - !ruby/object:Gem::Version
41
47
  version: '0'
42
48
  required_rubygems_version: !ruby/object:Gem::Requirement
43
- none: false
44
49
  requirements:
45
- - - ! '>='
50
+ - - '>='
46
51
  - !ruby/object:Gem::Version
47
52
  version: '0'
48
53
  requirements: []
49
54
  rubyforge_project:
50
- rubygems_version: 1.8.24
55
+ rubygems_version: 2.0.3
51
56
  signing_key:
52
- specification_version: 3
57
+ specification_version: 4
53
58
  summary: Retry policy mix-in
54
59
  test_files: []