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 +4 -4
- data/.travis.yml +9 -0
- data/README.md +102 -6
- data/lib/pester.rb +3 -1
- data/lib/pester/behaviors/sleep.rb +2 -0
- data/lib/pester/version.rb +1 -1
- data/pester.gemspec +1 -1
- data/spec/pester_spec.rb +64 -0
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2e359e529e96c33e2c045131b4105c957f4574f1
|
4
|
+
data.tar.gz: 7ecedc1fd852d0ed9ffd87f0e453140a86a586c2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1ee4066ba850950f881d26f79ef5128161732db89f6403a7f0ee6125a92661b4824dce2e9033e80cfe3da4150559a166fefd70e99c1d634b6e41f29fd31487c8
|
7
|
+
data.tar.gz: b6633b146f951025076480d0f236bf50232e27faef95ff0080764435e3e34f972b188d9bb1016648512059e384c8194c7b0cd35abf100fd5a6778f9e6dffcc19
|
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,6 +1,106 @@
|
|
1
|
-
# Pester
|
1
|
+
# Pester - Coordinated retry logic
|
2
2
|
|
3
|
-
|
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 )
|
data/lib/pester.rb
CHANGED
@@ -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:
|
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) }
|
data/lib/pester/version.rb
CHANGED
data/pester.gemspec
CHANGED
@@ -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.
|
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
|
data/spec/pester_spec.rb
CHANGED
@@ -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.
|
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-
|
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.
|
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.
|
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
|