retryable 2.0.4 → 3.0.5

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.
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