retryable 2.0.4 → 3.0.5

Sign up to get free protection for your applications and to get access to all the features.
data/retryable.gemspec CHANGED
@@ -1,20 +1,31 @@
1
- # -*- encoding: utf-8 -*-
2
- require File.expand_path('../lib/retryable/version', __FILE__)
1
+ # rubocop:disable Style/ExpandPathArguments
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ # rubocop:enable Style/ExpandPathArguments
3
4
 
4
- Gem::Specification.new do |gem|
5
- gem.add_development_dependency 'bundler', '~> 1.0'
6
- gem.authors = ["Nikita Fedyashev", "Carlo Zottmann", "Chu Yeow"]
7
- gem.description = %q{Retryable#retryable, allow for retrying of code blocks.}
8
- gem.email = %q{nfedyashev@gmail.com}
9
- gem.files = %w(CHANGELOG.md LICENSE.md README.md Rakefile retryable.gemspec)
10
- gem.files += Dir.glob("lib/**/*.rb")
11
- gem.files += Dir.glob("spec/**/*")
12
- gem.homepage = %q{http://github.com/nfedyashev/retryable}
13
- gem.name = 'retryable'
14
- gem.license = 'MIT'
15
- gem.require_paths = ["lib"]
16
- gem.required_rubygems_version = '>= 1.3.6'
17
- gem.summary = gem.description
18
- gem.test_files = Dir.glob("spec/**/*")
19
- gem.version = Retryable::Version
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+ require 'retryable/version'
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = 'retryable'
10
+ spec.version = Retryable::Version
11
+ spec.authors = ['Nikita Fedyashev', 'Carlo Zottmann', 'Chu Yeow']
12
+ spec.email = ['nfedyashev@gmail.com']
13
+
14
+ spec.summary = 'Retrying code blocks in Ruby'
15
+ spec.description = spec.summary
16
+ spec.homepage = 'http://github.com/nfedyashev/retryable'
17
+ spec.metadata = {
18
+ 'changelog_uri' => 'https://github.com/nfedyashev/retryable/blob/master/CHANGELOG.md',
19
+ 'source_code_uri' => 'https://github.com/nfedyashev/retryable/tree/master'
20
+ }
21
+ spec.licenses = ['MIT']
22
+
23
+ spec.require_paths = ['lib']
24
+ spec.files = Dir['{config,lib,spec}/**/*', '*.md', '*.gemspec', 'Gemfile', 'Rakefile']
25
+ spec.test_files = spec.files.grep(%r{^spec/})
26
+
27
+ spec.required_ruby_version = Gem::Requirement.new('>= 1.9.3')
28
+ spec.required_rubygems_version = Gem::Requirement.new('>= 1.3.6')
29
+
30
+ spec.add_development_dependency 'bundler'
20
31
  end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Retryable do
4
+ it 'is enabled by default' do
5
+ expect(described_class).to be_enabled
6
+ end
7
+
8
+ it 'could be disabled' do
9
+ described_class.disable
10
+ expect(described_class).not_to be_enabled
11
+ end
12
+
13
+ context 'when disabled' do
14
+ before do
15
+ described_class.disable
16
+ end
17
+
18
+ it 'could be re-enabled' do
19
+ described_class.enable
20
+ expect(described_class).to be_enabled
21
+ end
22
+ end
23
+
24
+ context 'when configured locally' do
25
+ it 'does not affect the original global config' do
26
+ new_sleep = 2
27
+ original_sleep = described_class.configuration.send(:sleep)
28
+
29
+ expect(original_sleep).not_to eq(new_sleep)
30
+
31
+ counter(tries: 2, sleep: new_sleep) do |tries, ex|
32
+ raise StandardError if tries < 1
33
+ end
34
+
35
+ actual = described_class.configuration.send(:sleep)
36
+ expect(actual).to eq(original_sleep)
37
+ end
38
+ end
39
+
40
+ context 'when configured globally with custom sleep parameter' do
41
+ it 'passes retry count and exception on retry' do
42
+ expect(Kernel).to receive(:sleep).once.with(3)
43
+
44
+ described_class.configure do |config|
45
+ config.sleep = 3
46
+ end
47
+
48
+ counter(tries: 2) do |tries, ex|
49
+ expect(ex.class).to eq(StandardError) if tries > 0
50
+ raise StandardError if tries < 1
51
+ end
52
+ expect(counter.count).to eq(2)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,32 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Retryable::Version do
4
+ before do
5
+ allow(described_class).to receive(:major).and_return(2)
6
+ allow(described_class).to receive(:minor).and_return(0)
7
+ allow(described_class).to receive(:patch).and_return(4)
8
+ end
9
+
10
+ describe '.to_h' do
11
+ it 'returns a hash with the right values' do
12
+ expect(described_class.to_h).to be_a Hash
13
+ expect(described_class.to_h[:major]).to eq(2)
14
+ expect(described_class.to_h[:minor]).to eq(0)
15
+ expect(described_class.to_h[:patch]).to eq(4)
16
+ end
17
+ end
18
+
19
+ describe '.to_a' do
20
+ it 'returns an array with the right values' do
21
+ expect(described_class.to_a).to be_an Array
22
+ expect(described_class.to_a).to eq([2, 0, 4])
23
+ end
24
+ end
25
+
26
+ describe '.to_s' do
27
+ it 'returns a string with the right value' do
28
+ expect(described_class.to_s).to be_a String
29
+ expect(described_class.to_s).to eq('2.0.4')
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+ require 'logger'
3
+
4
+ RSpec.describe Retryable do
5
+ describe '.retryable' do
6
+ before do
7
+ described_class.enable
8
+ expect(Kernel).to receive(:sleep)
9
+ end
10
+
11
+ let(:retryable) do
12
+ -> { Retryable.retryable(tries: 2) { |tries| raise StandardError, "because foo" if tries < 1 } }
13
+ end
14
+
15
+ context 'given default configuration' do
16
+ it 'does not output anything' do
17
+ expect { retryable.call }.not_to output.to_stdout_from_any_process
18
+ end
19
+ end
20
+
21
+ context 'given custom STDOUT logger config option' do
22
+ it 'does not output anything' do
23
+ described_class.configure do |config|
24
+ config.log_method = lambda do |retries, exception|
25
+ Logger.new(STDOUT).debug("[Attempt ##{retries}] Retrying because [#{exception.class} - #{exception.message}]: #{exception.backtrace.first(5).join(' | ')}")
26
+ end
27
+ end
28
+
29
+ expect { retryable.call }.to output(/\[Attempt #1\] Retrying because \[StandardError - because foo\]/).to_stdout_from_any_process
30
+ end
31
+ end
32
+ end
33
+ end
34
+
@@ -0,0 +1,192 @@
1
+ require 'spec_helper'
2
+ require 'timeout'
3
+
4
+ RSpec.describe Retryable do
5
+ describe '.retryable' do
6
+ before do
7
+ described_class.enable
8
+ @attempt = 0
9
+ end
10
+
11
+ it 'catch StandardError only by default' do
12
+ expect do
13
+ counter(tries: 2) { |tries| raise Exception if tries < 1 }
14
+ end.to raise_error Exception
15
+ expect(counter.count).to eq(1)
16
+ end
17
+
18
+ it 'retries on default exception' do
19
+ expect(Kernel).to receive(:sleep).once.with(1)
20
+
21
+ counter(tries: 2) { |tries| raise StandardError if tries < 1 }
22
+ expect(counter.count).to eq(2)
23
+ end
24
+
25
+ it 'does not retry if disabled' do
26
+ described_class.disable
27
+
28
+ expect do
29
+ counter(tries: 2) { raise }
30
+ end.to raise_error RuntimeError
31
+ expect(counter.count).to eq(1)
32
+ end
33
+
34
+ it 'executes *ensure* clause' do
35
+ ensure_cb = proc do |retries|
36
+ expect(retries).to eq(0)
37
+ end
38
+
39
+ described_class.retryable(ensure: ensure_cb) {}
40
+ end
41
+
42
+ it 'passes retry count and exception on retry' do
43
+ expect(Kernel).to receive(:sleep).once.with(1)
44
+
45
+ counter(tries: 2) do |tries, ex|
46
+ expect(ex.class).to eq(StandardError) if tries > 0
47
+ raise StandardError if tries < 1
48
+ end
49
+ expect(counter.count).to eq(2)
50
+ end
51
+
52
+ it 'makes another try if exception is covered by :on' do
53
+ allow(Kernel).to receive(:sleep)
54
+ counter(on: [StandardError, ArgumentError, RuntimeError]) do |tries|
55
+ raise ArgumentError if tries < 1
56
+ end
57
+ expect(counter.count).to eq(2)
58
+ end
59
+
60
+ it 'does not retry on :not exception which is covered by Array' do
61
+ expect do
62
+ counter(not: [RuntimeError, IndexError]) { |tries| raise RuntimeError if tries < 1 }
63
+ end.to raise_error RuntimeError
64
+ expect(counter.count).to eq(1)
65
+ end
66
+
67
+ it 'does not try on unexpected exception' do
68
+ allow(Kernel).to receive(:sleep)
69
+ expect do
70
+ counter(on: RuntimeError) { |tries| raise StandardError if tries < 1 }
71
+ end.to raise_error StandardError
72
+ expect(counter.count).to eq(1)
73
+ end
74
+
75
+ it 'retries three times' do
76
+ allow(Kernel).to receive(:sleep)
77
+ counter(tries: 3) { |tries| raise StandardError if tries < 2 }
78
+ expect(counter.count).to eq(3)
79
+ end
80
+
81
+ context 'infinite retries' do
82
+ example 'with magic constant' do
83
+ expect do
84
+ Timeout.timeout(3) do
85
+ counter(tries: :infinite, sleep: 0.1) { raise StandardError }
86
+ end
87
+ end.to raise_error Timeout::Error
88
+
89
+ expect(counter.count).to be > 10
90
+ end
91
+
92
+ example 'with native infinity data type' do
93
+ expect do
94
+ require 'bigdecimal'
95
+
96
+ tries = [Float::INFINITY, BigDecimal::INFINITY, BigDecimal("1.0") / BigDecimal("0.0")]
97
+ Timeout.timeout(3) do
98
+ counter(tries: tries.sample, sleep: 0.1) { raise StandardError }
99
+ end
100
+ end.to raise_error Timeout::Error
101
+
102
+ expect(counter.count).to be > 10
103
+ end
104
+ end
105
+
106
+ it 'executes exponential backoff scheme for :sleep option' do
107
+ [1, 4, 16, 64].each { |i| expect(Kernel).to receive(:sleep).once.ordered.with(i) }
108
+ expect do
109
+ described_class.retryable(tries: 5, sleep: ->(n) { 4**n }) { raise RangeError }
110
+ end.to raise_error RangeError
111
+ end
112
+
113
+ it 'calls :sleep_method option' do
114
+ sleep_method = double
115
+ expect(sleep_method).to receive(:call).twice
116
+ expect do
117
+ described_class.retryable(tries: 3, sleep_method: sleep_method) { |tries| raise RangeError if tries < 9 }
118
+ end.to raise_error RangeError
119
+ end
120
+
121
+ it 'does not retry any exception if :on is empty list' do
122
+ expect do
123
+ counter(on: []) { raise }
124
+ end.to raise_error RuntimeError
125
+ expect(counter.count).to eq(1)
126
+ end
127
+
128
+ it 'catches an exception that matches the regex' do
129
+ expect(Kernel).to receive(:sleep).once.with(1)
130
+ counter(matching: /IO timeout/) { |c, _e| raise 'yo, IO timeout!' if c == 0 }
131
+ expect(counter.count).to eq(2)
132
+ end
133
+
134
+ it 'does not catch an exception that does not match the regex' do
135
+ expect(Kernel).not_to receive(:sleep)
136
+ expect do
137
+ counter(matching: /TimeError/) { raise 'yo, IO timeout!' }
138
+ end.to raise_error RuntimeError
139
+ expect(counter.count).to eq(1)
140
+ end
141
+
142
+ it 'catches an exception in the list of matches' do
143
+ expect(Kernel).to receive(:sleep).once.with(1)
144
+ counter(matching: [/IO timeout/, 'IO tymeout']) { |c, _e| raise 'yo, IO timeout!' if c == 0 }
145
+ expect(counter.count).to eq(2)
146
+
147
+ expect(Kernel).to receive(:sleep).once.with(1)
148
+ counter(matching: [/IO timeout/, 'IO tymeout']) { |c, _e| raise 'yo, IO tymeout!' if c == 0 }
149
+ expect(counter.count).to eq(4)
150
+ end
151
+
152
+ it 'does not allow invalid type of matching option' do
153
+ expect do
154
+ described_class.retryable(matching: 1) { raise 'this is invaid type of matching iotion' }
155
+ end.to raise_error ArgumentError, ':matching must be a string or regex'
156
+ end
157
+
158
+ it 'does not allow invalid options' do
159
+ expect do
160
+ described_class.retryable(bad_option: 2) { raise 'this is bad' }
161
+ end.to raise_error ArgumentError, '[Retryable] Invalid options: bad_option'
162
+ end
163
+
164
+ # rubocop:disable Rspec/InstanceVariable
165
+ it 'accepts a callback to run after an exception is rescued' do
166
+ expect do
167
+ described_class.retryable(sleep: 0, exception_cb: proc { |e| @raised = e.to_s }) do |tries|
168
+ raise StandardError, 'this is fun!' if tries < 1
169
+ end
170
+ end.not_to raise_error
171
+
172
+ expect(@raised).to eq('this is fun!')
173
+ end
174
+ # rubocop:enable Rspec/InstanceVariable
175
+
176
+ it 'does not retry on :not exception' do
177
+ expect do
178
+ counter(not: RuntimeError) { |tries| raise RuntimeError if tries < 1 }
179
+ end.to raise_error RuntimeError
180
+ expect(counter.count).to eq(1)
181
+ end
182
+
183
+ it 'gives precidence for :not over :on' do
184
+ expect do
185
+ counter(sleep: 0, tries: 3, on: StandardError, not: IndexError) do |tries|
186
+ raise tries >= 1 ? IndexError : StandardError
187
+ end
188
+ end.to raise_error IndexError
189
+ expect(counter.count).to eq(2)
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Retryable do
4
+ describe '.with_context' do
5
+ before do
6
+ described_class.enable
7
+ @attempt = 0
8
+ end
9
+
10
+ it 'properly checks context configuration' do
11
+ expect do
12
+ described_class.with_context(:foo) {}
13
+ end.to raise_error ArgumentError, 'foo not found in Retryable.configuration.contexts. Available contexts: []'
14
+
15
+ expect do
16
+ described_class.retryable_with_context(:bar) {}
17
+ end.to raise_error ArgumentError, 'bar not found in Retryable.configuration.contexts. Available contexts: []'
18
+
19
+ expect do
20
+ described_class.configure do |config|
21
+ config.contexts[:faulty_service] = {
22
+ sleep: 3
23
+ }
24
+ end
25
+
26
+ described_class.retryable_with_context(:baz) {}
27
+ end.to raise_error ArgumentError, 'baz not found in Retryable.configuration.contexts. Available contexts: [:faulty_service]'
28
+ end
29
+
30
+ it 'properly fetches context options' do
31
+ allow(Kernel).to receive(:sleep)
32
+
33
+ described_class.configure do |config|
34
+ config.contexts[:faulty_service] = {
35
+ tries: 3
36
+ }
37
+ end
38
+
39
+ c = counter_with_context(:faulty_service) { |tries| raise StandardError if tries < 2 }
40
+ expect(c.count).to eq(3)
41
+ end
42
+
43
+ it 'properly overrides context options with local arguments' do
44
+ allow(Kernel).to receive(:sleep)
45
+
46
+ described_class.configure do |config|
47
+ config.contexts[:faulty_service] = {
48
+ tries: 1
49
+ }
50
+ end
51
+
52
+ c = counter_with_context(:faulty_service, tries: 3) { |tries| raise StandardError if tries < 2 }
53
+ expect(c.count).to eq(3)
54
+ end
55
+ end
56
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,25 +1,18 @@
1
- require File.dirname(__FILE__) + '/../lib/retryable'
2
- require 'rspec'
3
- require 'pry'
1
+ require 'retryable'
2
+ require 'simplecov'
4
3
 
5
- RSpec.configure do |config|
6
- config.disable_monkey_patching!
4
+ # rubocop:disable Style/ExpandPathArguments
5
+ Dir.glob(File.expand_path('../support/**/*.rb', __FILE__), &method(:require))
6
+ # rubocop:enable Style/ExpandPathArguments
7
7
 
8
- config.before(:each) do
9
- reset_config
10
- end
8
+ SimpleCov.start
11
9
 
12
- def count_retryable(*opts)
13
- @try_count = 0
14
- return Retryable.retryable(*opts) do |*args|
15
- @try_count += 1
16
- yield *args
17
- end
18
- end
10
+ RSpec.configure do |config|
11
+ config.disable_monkey_patching!
19
12
 
20
- private
13
+ config.include(Counter)
21
14
 
22
- def reset_config
15
+ config.before do
23
16
  Retryable.configuration = nil
24
17
  end
25
18
  end
@@ -0,0 +1,58 @@
1
+ module Counter
2
+ class PlainGenerator
3
+ attr_reader :count
4
+
5
+ def initialize(options)
6
+ @options = options
7
+ @count = 0
8
+ end
9
+
10
+ def around
11
+ Retryable.retryable(@options) do |*arguments|
12
+ increment
13
+ yield(*arguments)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def increment
20
+ @count += 1
21
+ end
22
+ end
23
+
24
+ class GeneratorWithContext
25
+ attr_reader :count
26
+
27
+ def initialize(context_key, options)
28
+ @context_key = context_key
29
+ @count = 0
30
+ @options = options
31
+ end
32
+
33
+ def around
34
+ Retryable.with_context(@context_key, @options) do |*arguments|
35
+ increment
36
+ yield(*arguments)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def increment
43
+ @count += 1
44
+ end
45
+ end
46
+
47
+ def counter(options = {}, &block)
48
+ @counter ||= PlainGenerator.new(options)
49
+ @counter.around(&block) if block_given?
50
+ @counter
51
+ end
52
+
53
+ def counter_with_context(context_key, options = {}, &block)
54
+ @counter_with_context ||= GeneratorWithContext.new(context_key, options)
55
+ @counter_with_context.around(&block) if block_given?
56
+ @counter_with_context
57
+ end
58
+ end
metadata CHANGED
@@ -1,97 +1,84 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: retryable
3
- version: !ruby/object:Gem::Version
4
- hash: 7
5
- prerelease:
6
- segments:
7
- - 2
8
- - 0
9
- - 4
10
- version: 2.0.4
3
+ version: !ruby/object:Gem::Version
4
+ version: 3.0.5
11
5
  platform: ruby
12
- authors:
6
+ authors:
13
7
  - Nikita Fedyashev
14
8
  - Carlo Zottmann
15
9
  - Chu Yeow
16
10
  autorequire:
17
11
  bindir: bin
18
12
  cert_chain: []
19
-
20
- date: 2016-07-14 00:00:00 +03:00
21
- default_executable:
22
- dependencies:
23
- - !ruby/object:Gem::Dependency
13
+ date: 2019-11-11 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
24
16
  name: bundler
25
- prerelease: false
26
- requirement: &id001 !ruby/object:Gem::Requirement
27
- none: false
28
- requirements:
29
- - - ~>
30
- - !ruby/object:Gem::Version
31
- hash: 15
32
- segments:
33
- - 1
34
- - 0
35
- version: "1.0"
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
36
22
  type: :development
37
- version_requirements: *id001
38
- description: Retryable#retryable, allow for retrying of code blocks.
39
- email: nfedyashev@gmail.com
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '0'
29
+ description: Retrying code blocks in Ruby
30
+ email:
31
+ - nfedyashev@gmail.com
40
32
  executables: []
41
-
42
33
  extensions: []
43
-
44
34
  extra_rdoc_files: []
45
-
46
- files:
35
+ files:
47
36
  - CHANGELOG.md
37
+ - Gemfile
48
38
  - LICENSE.md
49
39
  - README.md
50
40
  - Rakefile
51
- - retryable.gemspec
41
+ - lib/retryable.rb
52
42
  - lib/retryable/configuration.rb
53
43
  - lib/retryable/version.rb
54
- - lib/retryable.rb
55
- - spec/lib/configuration_spec.rb
56
- - spec/lib/retryable_spec.rb
44
+ - retryable.gemspec
45
+ - spec/retryable/configuration_spec.rb
46
+ - spec/retryable/version_spec.rb
47
+ - spec/retryable_logging_spec.rb
48
+ - spec/retryable_spec.rb
49
+ - spec/retryable_with_context_spec.rb
57
50
  - spec/spec_helper.rb
58
- has_rdoc: true
51
+ - spec/support/counter.rb
59
52
  homepage: http://github.com/nfedyashev/retryable
60
- licenses:
53
+ licenses:
61
54
  - MIT
55
+ metadata:
56
+ changelog_uri: https://github.com/nfedyashev/retryable/blob/master/CHANGELOG.md
57
+ source_code_uri: https://github.com/nfedyashev/retryable/tree/master
62
58
  post_install_message:
63
59
  rdoc_options: []
64
-
65
- require_paths:
60
+ require_paths:
66
61
  - lib
67
- required_ruby_version: !ruby/object:Gem::Requirement
68
- none: false
69
- requirements:
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
70
64
  - - ">="
71
- - !ruby/object:Gem::Version
72
- hash: 3
73
- segments:
74
- - 0
75
- version: "0"
76
- required_rubygems_version: !ruby/object:Gem::Requirement
77
- none: false
78
- requirements:
65
+ - !ruby/object:Gem::Version
66
+ version: 1.9.3
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
79
69
  - - ">="
80
- - !ruby/object:Gem::Version
81
- hash: 23
82
- segments:
83
- - 1
84
- - 3
85
- - 6
70
+ - !ruby/object:Gem::Version
86
71
  version: 1.3.6
87
72
  requirements: []
88
-
89
- rubyforge_project:
90
- rubygems_version: 1.6.2
73
+ rubygems_version: 3.0.3
91
74
  signing_key:
92
- specification_version: 3
93
- summary: Retryable#retryable, allow for retrying of code blocks.
94
- test_files:
95
- - spec/lib/configuration_spec.rb
96
- - spec/lib/retryable_spec.rb
75
+ specification_version: 4
76
+ summary: Retrying code blocks in Ruby
77
+ test_files:
78
+ - spec/retryable/configuration_spec.rb
79
+ - spec/retryable/version_spec.rb
80
+ - spec/retryable_logging_spec.rb
81
+ - spec/retryable_spec.rb
82
+ - spec/retryable_with_context_spec.rb
97
83
  - spec/spec_helper.rb
84
+ - spec/support/counter.rb