pester 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c726cd4ef970107604a3cabf1d26ddf04fffbc45
4
- data.tar.gz: 3579a4065f03f7791720e9d98d0c6bdc5a3c57f7
3
+ metadata.gz: 2e359e529e96c33e2c045131b4105c957f4574f1
4
+ data.tar.gz: 7ecedc1fd852d0ed9ffd87f0e453140a86a586c2
5
5
  SHA512:
6
- metadata.gz: 3e6af9161e42c15a613748d81845adfa8d786b4a5309b41163b87c5d42945968e984e66e84a95f9ba141b2a713c1c30f1a2d5c6b8b8648e516fc1cb0931b84d0
7
- data.tar.gz: a5719117192311422b5bce432f1b53cef9d98f12ef3b4e1df6b244d237709353ce64af6152c015b75910226b010119abadd890cc23a5f15eeebf34c441b0f450
6
+ metadata.gz: 1ee4066ba850950f881d26f79ef5128161732db89f6403a7f0ee6125a92661b4824dce2e9033e80cfe3da4150559a166fefd70e99c1d634b6e41f29fd31487c8
7
+ data.tar.gz: b6633b146f951025076480d0f236bf50232e27faef95ff0080764435e3e34f972b188d9bb1016648512059e384c8194c7b0cd35abf100fd5a6778f9e6dffcc19
@@ -0,0 +1,9 @@
1
+ ---
2
+ language: ruby
3
+ cache:
4
+ - bundler
5
+ rvm:
6
+ - ruby-2.0.0-p353
7
+ env:
8
+ TRAVIS: true
9
+ script: "bundle exec rspec spec --color"
data/README.md CHANGED
@@ -1,6 +1,106 @@
1
- # Pester
1
+ # Pester - Coordinated retry logic
2
2
 
3
- TODO: Write a gem description
3
+ [![Travis](https://travis-ci.org/lumoslabs/pester.svg?branch=master)](https://travis-ci.org/lumoslabs/pester)
4
+ [![Code Climate](https://codeclimate.com/github/lumoslabs/pester/badges/gpa.svg)](https://codeclimate.com/github/lumoslabs/pester)
5
+
6
+ In a lot of our backend code, we quite often found ourselves repeating some of the same patterns for retry logic. Relying on external services--including internal service endpoints and databases--means things fail intermittently. Many of these operations are idempotent and can be retried, but external services don't especially like being pestered without some pause, so you need to slow your roll.
7
+
8
+ ## Usage
9
+
10
+ From the outset, the goal of Pester is to offer a simple interface. For example:
11
+
12
+ irb(main):001:0> require 'pester'
13
+ => true
14
+ irb(main):002:0> Pester.retry { fail 'derp' }
15
+ W, [2015-04-04T10:37:46.413158 #87600] WARN -- : Failure encountered: derp, backing off and trying again 3 more times. etc etc
16
+
17
+ will retry the block--which always fails--until Pester has exhausted its amount of retries. With no options provided, this will sleep for a constant number of seconds between attempts.
18
+
19
+ Pester's basic retry behaviors are defined by three options:
20
+
21
+ * `delay_interval`
22
+ * `max_attempts`
23
+ * `on_retry`
24
+
25
+ `delay_interval` is the unit, in seconds, that will be delayed between attempts. Normally, this is just the total number of seconds, but it can change with other `Behavior`s. `max_attempts` is the number of tries Pester will make, including the initial one. If this is set to 1, Pester will basically not retry; less than 1, it will not even bother executing the block:
26
+
27
+ irb(main):001:0> Pester.retry(max_attempts: 0) { puts 'Trying...'; fail 'derp' }
28
+ => nil
29
+
30
+ `on_retry` defines the behavior between retries, which can either be a custom block of code, or one of the predefined `Behavior`s, specifically in `Pester::Behaviors::Sleep`. If passed an empty lambda/block, Pester will immediately retry. When writing a custom behavior, `on_retry` expects a block that can be called with two parameters, `attempt_num`, and `delay_interval`, the idea being that these will mostly be used to define a function that determines just how long to sleep between attempts.
31
+
32
+ Three behaviors are provided out-of-the box:
33
+
34
+ * `Constant` is the default, and will simply sleep for `delay_interval` seconds
35
+ * `Linear` simply multiplies `attempt_num` by `delay_interval` and sleeps for that many seconds
36
+ * `Exponential` sleeps for 2<sup>(`attempt_num` - 1)</sup> * `delay_interval` seconds
37
+
38
+ All three are available either by passing the behaviors to `on_retry`, or by calling the increasingly-verbosely-named `retry` (constant), `retry_with_backoff` (linear), or `retry_with_exponential_backoff` (exponential).
39
+
40
+ Pester does log retry attempts (see below), however custom retry behavior that wraps existing `Behavior`s may be appropriate for logging custom information, incrementing statsd counters, etc. Also of note, different loggers can be passed per-call via the `logger` option.
41
+
42
+ Finally, one last behavior is executed once the max number of retries has been exhausted, `on_max_attempts_exceeded`, which is also configurable per-call. By default, this will log an exhaustion message to `warn` and just reraise the called exception, preserving the original stacktrace.
43
+
44
+ ### Choosing what to retry
45
+
46
+ Pester can be configured to be picky about what it chooses to retry and what it lets through. Three options control this behavior:
47
+
48
+ * retry_error_classes
49
+ * reraise_error_classes
50
+ * retry_error_messages
51
+
52
+ The first two are mutually-exclusive whitelist and blacklists, both taking either a single error class or an array. Raising an error not covered by `retry_error_classes` (whitelist) causes it to immediately fail:
53
+
54
+ irb(main):002:0> Pester.retry(retry_error_classes: NotImplementedError) do
55
+ puts 'Trying...'
56
+ fail 'derp'
57
+ end
58
+ Trying...
59
+ RuntimeError: derp
60
+
61
+ Raising an error covered by `reraise_error_classes` (blacklist) causes it to immediately fail:
62
+
63
+ irb(main):002:0> Pester.retry(reraise_error_classes: NotImplementedError) do
64
+ puts 'Trying...'
65
+ raise NotImplementedError.new('derp')
66
+ end
67
+ Trying...
68
+ NotImplementedError: derp
69
+
70
+ `retry_error_messages` also takes a single string or array, and calls `include?` on the error message. If it matches, the error's retried:
71
+
72
+ irb(main):002:0> Pester.retry(retry_error_messages: 'please') do
73
+ puts 'Trying...'
74
+ fail 'retry this, please'
75
+ end
76
+ Trying...
77
+ Trying...
78
+
79
+ Because it calls `include?`, this also works for regexes:
80
+
81
+ irb(main):002:0> Pester.retry(retry_error_messages: /\d/) do
82
+ puts 'Trying...'
83
+ fail 'retry this 2'
84
+ end
85
+ Trying...
86
+ Trying...
87
+
88
+ ### Configuration
89
+
90
+ Pester will write retry and exhaustion information into your logs, by default using a ruby `Logger` to standard out. This can be configured either per-call, or one time per application in your initializer via `Pester#configure`. The following will suppress all logs by using a class that simply does nothing with log data, as found in `spec/`:
91
+
92
+ Pester.configure do |c|
93
+ c.logger = NullLogger.new
94
+ end
95
+
96
+ And thus:
97
+
98
+ irb(main):002:0> Pester.retry(delay_interval: 1) { puts 'Trying...'; fail 'derp' }
99
+ Trying...
100
+ Trying...
101
+ Trying...
102
+ Trying...
103
+ RuntimeError: derp
4
104
 
5
105
  ## Installation
6
106
 
@@ -18,10 +118,6 @@ Or install it yourself as:
18
118
 
19
119
  $ gem install pester
20
120
 
21
- ## Usage
22
-
23
- TODO: Write usage instructions here
24
-
25
121
  ## Contributing
26
122
 
27
123
  1. Fork it ( https://github.com/[my-github-username]/pester/fork )
@@ -9,7 +9,7 @@ module Pester
9
9
  end
10
10
 
11
11
  def self.retry(options = {}, &block)
12
- retry_action(options.merge(on_retry: ->(_, delay_interval) { sleep(delay_interval) }), &block)
12
+ retry_action(options.merge(on_retry: Behaviors::Sleep::Constant), &block)
13
13
  end
14
14
 
15
15
  def self.retry_with_backoff(options = {}, &block)
@@ -71,6 +71,8 @@ module Pester
71
71
  end
72
72
  end
73
73
  end
74
+
75
+ nil
74
76
  end
75
77
 
76
78
  private
@@ -1,6 +1,8 @@
1
1
  module Pester
2
2
  module Behaviors
3
3
  module Sleep
4
+ Constant = ->(_, delay_interval) { sleep(delay_interval) }
5
+
4
6
  Linear = ->(attempt_num, delay_interval) { sleep(attempt_num * delay_interval) }
5
7
 
6
8
  Exponential = ->(attempt_num, delay_interval) { sleep((2**attempt_num - 1) * delay_interval) }
@@ -1,3 +1,3 @@
1
1
  module Pester
2
- VERSION = '0.1.1'
2
+ VERSION = '0.1.2'
3
3
  end
@@ -22,7 +22,7 @@ EOD
22
22
  spec.test_files = spec.files.grep(/^(test|spec|features)\//)
23
23
  spec.require_paths = ['lib']
24
24
 
25
- spec.add_development_dependency 'bundler', '~> 1.7'
25
+ spec.add_development_dependency 'bundler', '~> 1.6'
26
26
  spec.add_development_dependency 'rake', '~> 10.0'
27
27
  spec.add_development_dependency 'rspec', '~> 3.2'
28
28
  end
@@ -28,6 +28,17 @@ shared_examples 'returns and succeeds' do
28
28
  end
29
29
  end
30
30
 
31
+ shared_examples 'has not run' do
32
+ it 'returns the intended result' do
33
+ expect(Pester.retry_action(options) { action }).to eq(nil)
34
+ end
35
+
36
+ it 'succeeds exactly once' do
37
+ Pester.retry_action(options) { action }
38
+ expect(failer.successes).to eq(0)
39
+ end
40
+ end
41
+
31
42
  shared_examples 'raises an error only in the correct cases with a retry class' do
32
43
  context 'when neither the class is in the retry list, nor is the message matched' do
33
44
  let(:actual_error_class) { non_matching_error_class }
@@ -192,6 +203,59 @@ describe 'retry_action' do
192
203
  end
193
204
  end
194
205
  end
206
+
207
+ context 'when max_attempts is set' do
208
+ let(:intended_result) { 42 }
209
+ let(:options) { { delay_interval: 0, logger: null_logger, max_attempts: max_attempts } }
210
+
211
+ shared_examples "doesn't even run" do
212
+ let(:action) { failer.fail(StandardError, 'Dying') }
213
+
214
+ context 'for block does not fail' do
215
+ let(:failer) { ScriptedFailer.new(0, intended_result) }
216
+
217
+ it_has_behavior "doesn't raise an error"
218
+ it_has_behavior 'has not run'
219
+ end
220
+
221
+ context 'for block fails' do
222
+ let(:failer) { ScriptedFailer.new(1, intended_result) }
223
+
224
+ it_has_behavior "doesn't raise an error"
225
+ it_has_behavior 'has not run'
226
+ end
227
+ end
228
+
229
+ context 'to one' do
230
+ let(:max_attempts) { 1 }
231
+ let(:action) { failer.fail(StandardError, 'Dying') }
232
+
233
+ context 'for block does not fail' do
234
+ let(:failer) { ScriptedFailer.new(0, intended_result) }
235
+
236
+ it_has_behavior "doesn't raise an error"
237
+ it_has_behavior 'returns and succeeds'
238
+ end
239
+
240
+ context 'for block fails' do
241
+ let(:failer) { ScriptedFailer.new(1, intended_result) }
242
+
243
+ it_has_behavior 'raises an error'
244
+ end
245
+ end
246
+
247
+ context 'to zero' do
248
+ let(:max_attempts) { 0 }
249
+
250
+ it_has_behavior "doesn't even run"
251
+ end
252
+
253
+ context 'to less than zero' do
254
+ let(:max_attempts) { -1 }
255
+
256
+ it_has_behavior "doesn't even run"
257
+ end
258
+ end
195
259
  end
196
260
 
197
261
  describe 'logger' do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pester
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marc Bollinger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-04-01 00:00:00.000000000 Z
11
+ date: 2015-04-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.7'
19
+ version: '1.6'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.7'
26
+ version: '1.6'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -65,6 +65,7 @@ files:
65
65
  - ".gitignore"
66
66
  - ".rspec"
67
67
  - ".rubocop.yml"
68
+ - ".travis.yml"
68
69
  - Gemfile
69
70
  - LICENSE.txt
70
71
  - README.md