pester 0.1.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f5c9151d53d657939d6506baf8a27e5da2744b3b
4
+ data.tar.gz: 251a6215cfe9cd275f37bf7df458561116cb7401
5
+ SHA512:
6
+ metadata.gz: dd350670630f082f40af4ab8290e7eb5f8a75a2137ca61cacc63c72445c28baad2eda3bc595aec14bb2782bc3ac5778a38c5678661aaec77ab75fe9f1fcb7e9f
7
+ data.tar.gz: 4bd76629e817aca9981340564ec5426ab8e6c6619bbe222b6b7b0d33143e587f21c9b49a31d2dbfb87d9a06ac1863dcf192d550860cf7686ba56448dc38041e0
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,14 @@
1
+ Metrics/AbcSize:
2
+ Max: 20
3
+
4
+ Metrics/LineLength:
5
+ Max: 140
6
+
7
+ Style/Documentation:
8
+ Enabled: false
9
+
10
+ Style/RaiseArgs:
11
+ Enabled: false
12
+
13
+ Style/SignalException:
14
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in pester.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Marc Bollinger
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # Pester
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'pester'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install pester
20
+
21
+ ## Usage
22
+
23
+ TODO: Write usage instructions here
24
+
25
+ ## Contributing
26
+
27
+ 1. Fork it ( https://github.com/[my-github-username]/pester/fork )
28
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
29
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
30
+ 4. Push to the branch (`git push origin my-new-feature`)
31
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rspec/core'
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec) do |spec|
6
+ spec.pattern = FileList['spec/**/*_spec.rb']
7
+ end
8
+
9
+ require 'rubocop/rake_task'
10
+ RuboCop::RakeTask.new
11
+
12
+ task prep: %w(spec rubocop)
13
+
14
+ task default: :spec
@@ -0,0 +1,9 @@
1
+ module Pester
2
+ module Behaviors
3
+ module Sleep
4
+ Linear = ->(attempt_num, delay_interval) { sleep(attempt_num * delay_interval) }
5
+
6
+ Exponential = ->(attempt_num, delay_interval) { sleep((2**attempt_num - 1) * delay_interval) }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ module Pester
2
+ module Behaviors
3
+ WarnAndReraise = lambda do |logger, max_attempts, e|
4
+ logger.warn("Max # of retriable exceptions (#{max_attempts}) exceeded, re-raising. Context: #{e}. Trace: #{e.backtrace}")
5
+ raise
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,16 @@
1
+ module Pester
2
+ class Config
3
+ class << self
4
+ attr_writer :logger
5
+
6
+ def configure
7
+ yield self
8
+ end
9
+
10
+ def logger
11
+ require 'logger' unless defined? Logger
12
+ @logger ||= Logger.new(STDOUT)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ module Pester
2
+ VERSION = '0.1.0'
3
+ end
data/lib/pester.rb ADDED
@@ -0,0 +1,92 @@
1
+ require 'pester/behaviors'
2
+ require 'pester/behaviors/sleep'
3
+ require 'pester/config'
4
+ require 'pester/version'
5
+
6
+ module Pester
7
+ def self.configure(&block)
8
+ Config.configure(&block)
9
+ end
10
+
11
+ def self.retry(options = {}, &block)
12
+ retry_action(options.merge(on_retry: ->(_, delay_interval) { sleep(delay_interval) }), &block)
13
+ end
14
+
15
+ def self.retry_with_backoff(options = {}, &block)
16
+ retry_action(options.merge(on_retry: Behaviors::Sleep::Linear), &block)
17
+ end
18
+
19
+ def self.retry_with_exponential_backoff(options = {}, &block)
20
+ retry_action({ delay_interval: 1 }.merge(options).merge(on_retry: Behaviors::Sleep::Exponential), &block)
21
+ end
22
+
23
+ # This function executes a block and retries the block depending on
24
+ # which errors were thrown. Retries 4 times by default.
25
+ #
26
+ # Options:
27
+ # retry_error_classes - A single or array of exceptions to retry on. Thrown exceptions not in this list
28
+ # (including parent/sub-classes) will be reraised
29
+ # reraise_error_classes - A single or array of exceptions to always re-raiseon. Thrown exceptions not in
30
+ # this list (including parent/sub-classes) will be retried
31
+ # max_attempts - Max number of attempts to retry
32
+ # delay_interval - Second interval by which successive attempts will be incremented. A value of 2
33
+ # passed to retry_with_backoff will retry first after 2 seconds, then 4, then 6, et al.
34
+ # on_retry - A Proc to be called on each successive failure, before the next retry
35
+ # on_max_attempts_exceeded - A Proc to be called when attempt_num >= max_attempts - 1
36
+ # message - String or regex to look for in thrown exception messages. Matches will trigger retry
37
+ # logic, non-matches will cause the exception to be reraised
38
+ #
39
+ # Usage:
40
+ # retry_action do
41
+ # puts 'trying to remove a directory'
42
+ # FileUtils.rm_r(directory)
43
+ # end
44
+ #
45
+ # retryable(error_classes: Mysql2::Error, message: /^Lost connection to MySQL server/, max_attempts: 2) do
46
+ # ActiveRecord::Base.connection.execute("LONG MYSQL STATEMENT")
47
+ # end
48
+ def self.retry_action(opts = {}, &block)
49
+ merge_defaults(opts)
50
+ if opts[:retry_error_classes] && opts[:reraise_error_classes]
51
+ fail 'You can only have one of retry_error_classes or reraise_error_classes'
52
+ end
53
+
54
+ opts[:max_attempts].times do |attempt_num|
55
+ begin
56
+ result = yield block
57
+ return result
58
+ rescue => e
59
+ class_reraise = opts[:retry_error_classes] && !opts[:retry_error_classes].include?(e.class)
60
+ reraise_error = opts[:reraise_error_classes] && opts[:reraise_error_classes].include?(e.class)
61
+ message_reraise = opts[:message] && !e.message[opts[:message]]
62
+
63
+ if class_reraise || message_reraise || reraise_error
64
+ match_type = class_reraise ? 'class' : 'message'
65
+ opts[:logger].warn("Reraising exception from inside retry_action because provided #{match_type} was not matched.")
66
+ raise
67
+ end
68
+
69
+ if opts[:max_attempts] - 1 > attempt_num
70
+ attempts_left = opts[:max_attempts] - attempt_num - 1
71
+ trace = e.backtrace
72
+ opts[:logger].warn("Failure encountered: #{e}, backing off and trying again #{attempts_left} more times. Trace: #{trace}")
73
+ opts[:on_retry].call(attempt_num, opts[:delay_interval])
74
+ else
75
+ return opts[:on_max_attempts_exceeded].call(opts[:logger], opts[:max_attempts], e)
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ def self.merge_defaults(opts)
84
+ opts[:retry_error_classes] = opts[:retry_error_classes] ? Array(opts[:retry_error_classes]) : nil
85
+ opts[:reraise_error_classes] = opts[:reraise_error_classes] ? Array(opts[:reraise_error_classes]) : nil
86
+ opts[:max_attempts] ||= 4
87
+ opts[:delay_interval] ||= 30
88
+ opts[:on_retry] ||= ->(_, _) {}
89
+ opts[:on_max_attempts_exceeded] ||= Behaviors::WarnAndReraise
90
+ opts[:logger] ||= Config.logger
91
+ end
92
+ end
data/pester.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'pester/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'pester'
8
+ spec.version = Pester::VERSION
9
+ spec.authors = ['Marc Bollinger']
10
+ spec.email = ['marc@lumoslabs.com']
11
+ spec.summary = 'Common block-based retry for external calls.'
12
+ spec.description = <<-EOD
13
+ |We found ourselves constantly wrapping network-facing calls with all kinds of bespoke,
14
+ | copied, and rewritten retry logic. This gem is an attempt to unify common behaviors,
15
+ | like simple retry, retry with linear backoff, and retry with exponential backoff.
16
+ EOD
17
+ spec.homepage = 'https://github.com/lumoslabs/pester'
18
+ spec.license = 'MIT'
19
+
20
+ spec.files = `git ls-files -z`.split("\x0")
21
+ spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
22
+ spec.test_files = spec.files.grep(/^(test|spec|features)\//)
23
+ spec.require_paths = ['lib']
24
+
25
+ spec.add_development_dependency 'bundler', '~> 1.7'
26
+ spec.add_development_dependency 'rake', '~> 10.0'
27
+ spec.add_development_dependency 'rspec', '~> 3.2'
28
+ end
@@ -0,0 +1,9 @@
1
+ require 'logger' unless defined? Logger
2
+
3
+ class NullLogger < Logger
4
+ def initialize(*_args)
5
+ end
6
+
7
+ def add(*_args, &_block)
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ class ScriptedFailer
2
+ attr_accessor :fails, :result, :successes
3
+
4
+ def initialize(num_fails = 2, intended_result = 2)
5
+ @fails = num_fails
6
+ @result = intended_result
7
+ @successes = 0
8
+ end
9
+
10
+ def fail(error_class, msg)
11
+ if @fails > 0
12
+ @fails -= 1
13
+ raise error_class.new(msg)
14
+ end
15
+ @successes += 1
16
+ @result
17
+ end
18
+ end
@@ -0,0 +1,204 @@
1
+ require 'logger'
2
+ require 'spec_helper'
3
+
4
+ class MatchedError < RuntimeError; end
5
+ class UnmatchedError < RuntimeError; end
6
+
7
+ shared_examples 'raises an error' do
8
+ it 'raises an error and succeeds 0 times' do
9
+ expect { Pester.retry_action(options) { action } }.to raise_error
10
+ expect(failer.successes).to eq(0)
11
+ end
12
+ end
13
+
14
+ shared_examples "doesn't raise an error" do
15
+ it "doesn't raise an error" do
16
+ expect { Pester.retry_action(options) { action } }.to_not raise_error
17
+ end
18
+ end
19
+
20
+ shared_examples 'returns and succeeds' do
21
+ it 'returns the intended result' do
22
+ expect(Pester.retry_action(options) { action }).to eq(intended_result)
23
+ end
24
+
25
+ it 'succeeds exactly once' do
26
+ Pester.retry_action(options) { action }
27
+ expect(failer.successes).to eq(1)
28
+ end
29
+ end
30
+
31
+ shared_examples 'raises an error only in the correct cases with a retry class' do
32
+ context 'when neither the class is in the retry list, nor is the message matched' do
33
+ let(:actual_error_class) { non_matching_error_class }
34
+ let(:actual_error_message) { non_matching_error_message }
35
+
36
+ it_has_behavior 'raises an error'
37
+ end
38
+
39
+ context 'when the class is in the retry list, but the message is not matched' do
40
+ let(:actual_error_class) { matching_error_class }
41
+ let(:actual_error_message) { non_matching_error_message }
42
+
43
+ it_has_behavior 'raises an error'
44
+ end
45
+
46
+ context 'when the class is not in the retry list, but the message is matched' do
47
+ let(:actual_error_class) { non_matching_error_class }
48
+ let(:actual_error_message) { matching_error_message }
49
+
50
+ it_has_behavior 'raises an error'
51
+ end
52
+
53
+ context 'when the class is in the list, and the message is matched' do
54
+ let(:actual_error_class) { matching_error_class }
55
+ let(:actual_error_message) { matching_error_message }
56
+
57
+ it_has_behavior "doesn't raise an error"
58
+
59
+ it_has_behavior 'returns and succeeds'
60
+ end
61
+ end
62
+
63
+ shared_examples 'raises an error only in the correct cases with a reraise class' do
64
+ context 'when the class is not in the reraise list' do
65
+ let(:actual_error_class) { non_matching_error_class }
66
+ let(:actual_error_message) { non_matching_error_message }
67
+
68
+ it_has_behavior "doesn't raise an error"
69
+
70
+ it_has_behavior 'returns and succeeds'
71
+ end
72
+
73
+ context 'when the class is in the reraise list' do
74
+ let(:actual_error_class) { matching_error_class }
75
+ let(:actual_error_message) { matching_error_message }
76
+
77
+ it_has_behavior 'raises an error'
78
+ end
79
+ end
80
+
81
+ describe 'retry_action' do
82
+ let(:intended_result) { 1000 }
83
+ let(:action) { failer.fail(UnmatchedError, 'Dying') }
84
+ let(:null_logger) { NullLogger.new }
85
+
86
+ context 'for non-failing block' do
87
+ let(:failer) { ScriptedFailer.new(0, intended_result) }
88
+ let(:options) { { delay_interval: 0, logger: null_logger } }
89
+
90
+ it_has_behavior "doesn't raise an error"
91
+
92
+ it_has_behavior 'returns and succeeds'
93
+ end
94
+
95
+ context 'for block that fails less than threshold' do
96
+ let(:failer) { ScriptedFailer.new(2, intended_result) }
97
+ let(:options) { { max_attempts: 3, logger: null_logger } }
98
+
99
+ it_has_behavior "doesn't raise an error"
100
+
101
+ it_has_behavior 'returns and succeeds'
102
+ end
103
+
104
+ context 'for block that fails more than threshold' do
105
+ let(:failer) { ScriptedFailer.new(6) }
106
+ let(:max_attempts) { 1 }
107
+
108
+ context 'without on_max_attempts_exceeded specified' do
109
+ let(:options) { { max_attempts: max_attempts, logger: null_logger } }
110
+
111
+ it_has_behavior 'raises an error'
112
+ end
113
+
114
+ context 'with on_max_attempts_exceeded specified (which does not raise)' do
115
+ let(:do_nothing_proc) { proc {} }
116
+ let(:options) do
117
+ {
118
+ max_attempts: max_attempts,
119
+ on_max_attempts_exceeded: do_nothing_proc,
120
+ logger: null_logger
121
+ }
122
+ end
123
+
124
+ it_has_behavior "doesn't raise an error"
125
+ end
126
+ end
127
+
128
+ context 'for retry_action calls with provided retry classes and message strings' do
129
+ let(:intended_result) { 1000 }
130
+ let(:failer) { ScriptedFailer.new(2, intended_result) }
131
+ let(:action) { failer.fail(actual_error_class, actual_error_message) }
132
+ let(:matching_error_class) { MatchedError }
133
+ let(:non_matching_error_class) { UnmatchedError }
134
+ let(:matching_error_message) { 'Lost connection to MySQL server' }
135
+ let(:non_matching_error_message) { 'You have an error in your SQL syntax' }
136
+
137
+ context 'Using retry error classes' do
138
+ let(:options) do
139
+ {
140
+ retry_error_classes: expected_error_classes,
141
+ message: /^Lost connection to MySQL server/,
142
+ max_attempts: 10,
143
+ logger: null_logger
144
+ }
145
+ end
146
+
147
+ context 'when error_classes is a single error class' do
148
+ let(:expected_error_classes) { matching_error_class }
149
+
150
+ it_has_behavior 'raises an error only in the correct cases with a retry class'
151
+ end
152
+
153
+ context 'when error_classes is a list of error classes' do
154
+ let(:expected_error_classes) { [ArgumentError, matching_error_class] }
155
+
156
+ it_has_behavior 'raises an error only in the correct cases with a retry class'
157
+ end
158
+ end
159
+
160
+ context 'Using reraise error classes' do
161
+ let(:options) do
162
+ {
163
+ reraise_error_classes: expected_error_classes,
164
+ max_attempts: 10,
165
+ logger: null_logger
166
+ }
167
+ end
168
+
169
+ context 'when error_classes is a single error class' do
170
+ let(:expected_error_classes) { matching_error_class }
171
+
172
+ it_has_behavior 'raises an error only in the correct cases with a reraise class'
173
+ end
174
+
175
+ context 'when error_classes is a list of error classes' do
176
+ let(:expected_error_classes) { [ArgumentError, matching_error_class] }
177
+
178
+ it_has_behavior 'raises an error only in the correct cases with a reraise class'
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ describe 'logger' do
185
+ context 'when not otherwise configured' do
186
+ it 'defaults to the ruby logger' do
187
+ Pester.configure do |config|
188
+ config.logger = nil
189
+ end
190
+ expect(Pester::Config.logger).to_not be_nil
191
+ expect(Pester::Config.logger).to be_kind_of(Logger)
192
+ end
193
+ end
194
+
195
+ context 'when configured to use a particular class' do
196
+ it 'users that class' do
197
+ Pester.configure do |config|
198
+ config.logger = NullLogger.new
199
+ end
200
+ expect(Pester::Config.logger).to_not be_nil
201
+ expect(Pester::Config.logger).to be_kind_of(NullLogger)
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,8 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')))
2
+ require 'pester'
3
+
4
+ Dir.glob('./spec/helpers/**/*.rb').each { |f| require f }
5
+
6
+ RSpec.configure do |c|
7
+ c.alias_it_should_behave_like_to :it_has_behavior, 'has behavior:'
8
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pester
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Marc Bollinger
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-03-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.2'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.2'
55
+ description: |2
56
+ |We found ourselves constantly wrapping network-facing calls with all kinds of bespoke,
57
+ | copied, and rewritten retry logic. This gem is an attempt to unify common behaviors,
58
+ | like simple retry, retry with linear backoff, and retry with exponential backoff.
59
+ email:
60
+ - marc@lumoslabs.com
61
+ executables: []
62
+ extensions: []
63
+ extra_rdoc_files: []
64
+ files:
65
+ - ".gitignore"
66
+ - ".rspec"
67
+ - ".rubocop.yml"
68
+ - Gemfile
69
+ - LICENSE.txt
70
+ - README.md
71
+ - Rakefile
72
+ - lib/pester.rb
73
+ - lib/pester/behaviors.rb
74
+ - lib/pester/behaviors/sleep.rb
75
+ - lib/pester/config.rb
76
+ - lib/pester/version.rb
77
+ - pester.gemspec
78
+ - spec/helpers/null_logger.rb
79
+ - spec/helpers/scripted_failer.rb
80
+ - spec/pester_spec.rb
81
+ - spec/spec_helper.rb
82
+ homepage: https://github.com/lumoslabs/pester
83
+ licenses:
84
+ - MIT
85
+ metadata: {}
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubyforge_project:
102
+ rubygems_version: 2.2.2
103
+ signing_key:
104
+ specification_version: 4
105
+ summary: Common block-based retry for external calls.
106
+ test_files:
107
+ - spec/helpers/null_logger.rb
108
+ - spec/helpers/scripted_failer.rb
109
+ - spec/pester_spec.rb
110
+ - spec/spec_helper.rb
111
+ has_rdoc: