retryable 2.0.4 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d2cf59f84731d2def08398714a4366b4b92d15eb
4
+ data.tar.gz: 391c2797ed522cc3a95c2d767335eb3bbb50a105
5
+ SHA512:
6
+ metadata.gz: 7c5df44eb01cc472a8e4f1dc41469b02af8f062fa11290f4cf00481621cdd0e66e5caf93eeef46a2504d9e9fe858c3905d4d1ca479676785b99590668c4dc848
7
+ data.tar.gz: 3c60ad3b26e1af54e5c411d525d3edc271cfb2bf5302f67d2bdc043af649f519cc31d97c1374cdee188d1db7b83eef3db85a4e4b76bafe383b53c3d667240990
@@ -1,8 +1,18 @@
1
+ ## Retryable 3.0.0 ##
2
+ NOTE: this version is backwards compatible with 2.0.4 version unless you're running it against Ruby 1.8 version.
3
+
4
+ * retryable can now also be configured via stored contexts.
5
+ * Ruby 1.8 support has been dropped.
6
+
7
+ Thanks @chubchenko for refactoring and various improvements.
8
+
1
9
  ## Retryable 2.0.4 ##
2
10
 
3
11
  * :infinite value is now available as :tries paramater. Use it for retrying your blocks infinitely until it stops failing.
4
12
  * :sleep_method parameter has been added. This can be very useful when you are working with Celluloid which implements its own version of the method sleep.
5
- Use `:sleep_method => Celluloid.method(:sleep)` in such cases.
13
+ Use `:sleep_method: Celluloid.method(:sleep)` in such cases.
14
+
15
+ Thanks @alexcastano
6
16
 
7
17
  ## Retryable 2.0.3 ##
8
18
 
data/Gemfile ADDED
@@ -0,0 +1,25 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gem 'rake', '~> 10.4'
4
+ gem 'yard'
5
+
6
+ group :development do
7
+ gem 'fasterer'
8
+ gem 'overcommit'
9
+ gem 'pry', '= 0.9.12.6'
10
+ gem 'rubocop'
11
+ gem 'rubocop-rspec'
12
+ end
13
+
14
+ group :test do
15
+ gem 'codeclimate-test-reporter'
16
+ gem 'rspec', '~> 3.1'
17
+ gem 'simplecov', require: false
18
+ end
19
+
20
+ platforms :rbx do
21
+ gem 'racc'
22
+ gem 'rubysl', '~> 2.0'
23
+ end
24
+
25
+ gemspec
data/README.md CHANGED
@@ -1,8 +1,12 @@
1
- retryable gem
2
- =====
1
+ # Retryable
3
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/retryable.svg)](https://badge.fury.io/rb/retryable)
4
4
  [![Build Status](https://travis-ci.org/nfedyashev/retryable.png?branch=master)](https://travis-ci.org/nfedyashev/retryable)
5
5
  [![Dependency Status](https://www.versioneye.com/ruby/retryable/badge.svg)](https://www.versioneye.com/ruby/retryable)
6
+ [![Code Climate](https://codeclimate.com/github/nfedyashev/retryable/badges/gpa.svg)](https://codeclimate.com/github/nfedyashev/retryable)
7
+ [![Test Coverage](https://codeclimate.com/github/nfedyashev/retryable/badges/coverage.svg)](https://codeclimate.com/github/nfedyashev/retryable/coverage)
8
+
9
+ [![Inline docs](http://inch-ci.org/github/nfedyashev/retryable.svg?branch=master)](http://inch-ci.org/github/nfedyashev/retryable)
6
10
 
7
11
  Description
8
12
  --------
@@ -16,6 +20,21 @@ runs the passed block. Should an exception occur, it'll retry for (n-1) times.
16
20
  Should the number of retries be reached without success, the last exception
17
21
  will be raised.
18
22
 
23
+ Installation
24
+ -------
25
+
26
+ Install the gem:
27
+
28
+ ``` bash
29
+ $ gem install retryable
30
+ ```
31
+
32
+ Add it to your Gemfile:
33
+
34
+ ``` ruby
35
+ gem 'retryable'
36
+ ```
37
+
19
38
 
20
39
  Examples
21
40
  --------
@@ -25,24 +44,24 @@ Open an URL, retry up to two times when an `OpenURI::HTTPError` occurs.
25
44
  ``` ruby
26
45
  require "open-uri"
27
46
 
28
- Retryable.retryable(:tries => 3, :on => OpenURI::HTTPError) do
47
+ Retryable.retryable(tries: 3, on: OpenURI::HTTPError) do
29
48
  xml = open("http://example.com/test.xml").read
30
49
  end
31
50
  ```
32
51
 
33
52
  Try the block forever.
34
53
  ```ruby
35
- Retryable.retryable(:tries => :infinite) do
36
- # some code
54
+ Retryable.retryable(tries: :infinite) do
55
+ # code here
37
56
  end
38
57
  ```
39
58
 
40
- Do _something_, retry up to four times for either `ArgumentError` or
59
+ Do something, retry up to four times for either `ArgumentError` or
41
60
  `TimeoutError` exceptions.
42
61
 
43
62
  ``` ruby
44
- Retryable.retryable(:tries => 5, :on => [ArgumentError, TimeoutError]) do
45
- # some crazy code
63
+ Retryable.retryable(tries: 5, on: [ArgumentError, TimeoutError]) do
64
+ # code here
46
65
  end
47
66
  ```
48
67
 
@@ -51,33 +70,42 @@ Ensure that block of code is executed, regardless of whether an exception was ra
51
70
  ``` ruby
52
71
  f = File.open("testfile")
53
72
 
54
- ensure_cb = Proc.new do |retries|
73
+ ensure_cb = proc do |retries|
55
74
  puts "total retry attempts: #{retries}"
56
75
 
57
76
  f.close
58
77
  end
59
78
 
60
- Retryable.retryable(:ensure => ensure_cb) do
79
+ Retryable.retryable(ensure: ensure_cb) do
61
80
  # process file
62
81
  end
63
82
  ```
64
83
 
65
84
  ## Defaults
66
85
 
67
- :tries => 2, :on => StandardError, :sleep => 1, :matching => /.*/, :ensure => Proc.new { }, :exception_cb => Proc.new { }, :not => [], :sleep_method => lambda { |n| Kernel.sleep(n) }
86
+ tries: 2,
87
+ on: StandardError,
88
+ sleep: 1,
89
+ matching : /.*/,
90
+ ensure: proc { },
91
+ exception_cb: proc { },
92
+ not: [],
93
+ sleep_method: lambda { |n| Kernel.sleep(n) },
94
+ contexts = {}
68
95
 
69
96
  Retryable also could be configured globally to change those defaults:
70
97
 
71
- ```
98
+ ```ruby
72
99
  Retryable.configure do |config|
73
- config.ensure = Proc.new {}
74
- config.exception_cb = Proc.new {}
100
+ config.ensure = proc {}
101
+ config.exception_cb = proc {}
75
102
  config.matching = /.*/
76
103
  config.on = StandardError
77
104
  config.sleep = 1
78
105
  config.tries = 2
79
106
  config.not = []
80
107
  config.sleep_method = Celluloid.method(:sleep)
108
+ config.contexts = {}
81
109
  end
82
110
  ```
83
111
 
@@ -86,19 +114,19 @@ Sleeping
86
114
  --------
87
115
  By default Retryable waits for one second between retries. You can change this and even provide your own exponential backoff scheme.
88
116
 
89
- ```
90
- Retryable.retryable(:sleep => 0) { } # don't pause at all between retries
91
- Retryable.retryable(:sleep => 10) { } # sleep ten seconds between retries
92
- Retryable.retryable(:sleep => lambda { |n| 4**n }) { } # sleep 1, 4, 16, etc. each try
117
+ ```ruby
118
+ Retryable.retryable(sleep: 0) { } # don't pause at all between retries
119
+ Retryable.retryable(sleep: 10) { } # sleep ten seconds between retries
120
+ Retryable.retryable(sleep: lambda { |n| 4**n }) { } # sleep 1, 4, 16, etc. each try
93
121
  ```
94
122
 
95
123
  Matching error messages
96
124
  --------
97
125
  You can also retry based on the exception message:
98
126
 
99
- ```
100
- Retryable.retryable(:matching => /IO timeout/) do |retries, exception|
101
- raise "yo, IO timeout!" if retries == 0
127
+ ```ruby
128
+ Retryable.retryable(matching: /IO timeout/) do |retries, exception|
129
+ raise "oops IO timeout!" if retries == 0
102
130
  end
103
131
  ```
104
132
 
@@ -106,31 +134,61 @@ Block Parameters
106
134
  --------
107
135
  Your block is called with two optional parameters: the number of tries until now, and the most recent exception.
108
136
 
109
- ```
137
+ ```ruby
110
138
  Retryable.retryable do |retries, exception|
111
139
  puts "try #{retries} failed with exception: #{exception}" if retries > 0
112
- pick_up_soap
140
+ # code here
113
141
  end
114
142
  ```
115
143
 
116
144
  Callback to run after an exception is rescued
117
145
  --------
118
146
 
119
- ```
120
- exception_cb = Proc.new do |exception|
147
+ ```ruby
148
+ exception_cb = proc do |exception|
121
149
  # http://smartinez87.github.io/exception_notification
122
- ExceptionNotifier.notify_exception(exception, :data => {:message => "it failed"})
150
+ ExceptionNotifier.notify_exception(exception, data: {message: "it failed"})
123
151
  end
124
152
 
125
- Retryable.retryable(:exception_cb => exception_cb) do
126
- # perform risky operation
153
+ Retryable.retryable(exception_cb: exception_cb) do
154
+ # code here
127
155
  end
128
156
  ```
129
157
 
130
- You can temporary disable retryable blocks
158
+ Contexts
131
159
  --------
132
160
 
161
+ Contexts allow you to extract common `Retryable.retryable` calling options for reuse or readability purposes.
162
+
163
+ ```ruby
164
+ Retryable.configure do |config|
165
+ config.contexts[:faulty_service] = {
166
+ :on: [FaultyServiceTimeoutError],
167
+ :sleep: 10,
168
+ :tries: 5
169
+ }
170
+ end
171
+
172
+
173
+ Retryable.with_context(:faulty_service) {
174
+ # code here
175
+ }
176
+ ```
177
+
178
+ You may also override options defined in your contexts:
179
+
180
+ ```ruby
181
+ # :on & sleep defined in the context earlier are still effective
182
+ Retryable.with_context(:faulty_service, tries: 999) {
183
+ # code here
184
+ }
133
185
  ```
186
+
187
+
188
+ You can temporary disable retryable blocks
189
+ --------
190
+
191
+ ```ruby
134
192
  Retryable.enabled?
135
193
  => true
136
194
 
@@ -145,10 +203,10 @@ Specify exceptions where a retry should NOT be performed
145
203
  No more tries will be made if an exception listed in `:not` is raised.
146
204
  Takes precedence over `:on`.
147
205
 
148
- ```
206
+ ```ruby
149
207
  class MyError < StandardError; end
150
208
 
151
- Retryable.retryable(:tries => 5, :on => [StandardError], :not => [MyError]) do
209
+ Retryable.retryable(tries: 5, on: [StandardError], not: [MyError]) do
152
210
  raise MyError "No retries!"
153
211
  end
154
212
 
@@ -159,9 +217,9 @@ Specify the sleep method to use
159
217
  This can be very useful when you are working with [Celluloid](https://github.com/celluloid/celluloid)
160
218
  which implements its own version of the method sleep.
161
219
 
162
- ```
163
- Retryable.retryable(:sleep_method => Celluloid.method(:sleep)) do
164
- retrieve_url
220
+ ```ruby
221
+ Retryable.retryable(sleep_method: Celluloid.method(:sleep)) do
222
+ # code here
165
223
  end
166
224
  ```
167
225
 
@@ -171,12 +229,15 @@ Supported Ruby Versions
171
229
  This library aims to support and is [tested against][travis] the following Ruby
172
230
  versions:
173
231
 
174
- * Ruby 1.8.7
175
- * Ruby 1.9.2
176
232
  * Ruby 1.9.3
177
233
  * Ruby 2.0.0
178
234
  * Ruby 2.1.2
179
235
  * Ruby 2.2.0
236
+ * Ruby 2.3.1
237
+ * Ruby 2.4.0
238
+ * Ruby 2.5.0
239
+
240
+ *NOTE: if you need `retryable` to be running on Ruby 1.8 use gem versions prior to 3.0.0 release*
180
241
 
181
242
  If something doesn't work on one of these versions, it's a bug.
182
243
 
@@ -185,27 +246,3 @@ however support will only be provided for the versions listed above.
185
246
 
186
247
  If you would like this library to support another Ruby version or
187
248
  implementation, you may volunteer to be a maintainer.
188
-
189
-
190
- Installation
191
- -------
192
-
193
- Install the gem:
194
-
195
- ``` bash
196
- $ gem install retryable
197
- ```
198
-
199
- Add it to your Gemfile:
200
-
201
- ``` ruby
202
- gem 'retryable'
203
- ```
204
-
205
- ## Thanks
206
-
207
- [Chu Yeow for this nifty piece of code](http://blog.codefront.net/2008/01/14/retrying-code-blocks-in-ruby-on-exceptions-whatever/)
208
-
209
- [Scott Bronson](https://github.com/bronson/retryable)
210
-
211
- [travis]: http://travis-ci.org/nfedyashev/retryable
data/Rakefile CHANGED
@@ -4,8 +4,8 @@ Bundler::GemHelper.install_tasks
4
4
  require 'rspec/core/rake_task'
5
5
  RSpec::Core::RakeTask.new(:spec)
6
6
 
7
- task :test => :spec
8
- task :default => :spec
7
+ task test: :spec
8
+ task default: :spec
9
9
 
10
10
  require 'yard'
11
11
  YARD::Rake::YardocTask.new
@@ -11,13 +11,15 @@ module Retryable
11
11
  #
12
12
  # @example
13
13
  # Retryable.configure do |config|
14
- # config.ensure = Proc.new {}
15
- # config.exception_cb = Proc.new {}
14
+ # config.contexts = {}
15
+ # config.ensure = proc {}
16
+ # config.exception_cb = proc {}
16
17
  # config.matching = /.*/
17
18
  # config.on = StandardError
18
19
  # config.sleep = 1
19
20
  # config.tries = 2
20
21
  # config.not = []
22
+ # config.sleep_method = ->(seconds) { Kernel.sleep(seconds) }
21
23
  # end
22
24
  def configure
23
25
  yield(configuration)
@@ -29,6 +31,15 @@ module Retryable
29
31
  @configuration ||= Configuration.new
30
32
  end
31
33
 
34
+ def with_context(context_key, options = {}, &block)
35
+ unless configuration.contexts.keys.include? context_key
36
+ raise ArgumentError, "#{context_key} not found in Retryable.configuration.contexts. Available contexts: #{configuration.contexts.keys}"
37
+ end
38
+ retryable(configuration.contexts[context_key].merge(options), &block) if block
39
+ end
40
+
41
+ alias retryable_with_context with_context
42
+
32
43
  def enabled?
33
44
  configuration.enabled?
34
45
  end
@@ -41,25 +52,16 @@ module Retryable
41
52
  configuration.disable
42
53
  end
43
54
 
44
- def retryable(options = {}, &block)
45
- opts = {
46
- :tries => self.configuration.tries,
47
- :sleep => self.configuration.sleep,
48
- :on => self.configuration.on,
49
- :matching => self.configuration.matching,
50
- :ensure => self.configuration.ensure,
51
- :exception_cb => self.configuration.exception_cb,
52
- :not => self.configuration.not,
53
- :sleep_method => self.configuration.sleep_method
54
- }
55
+ def retryable(options = {})
56
+ opts = configuration.to_hash
55
57
 
56
58
  check_for_invalid_options(options, opts)
57
59
  opts.merge!(options)
58
60
 
59
61
  return if opts[:tries] == 0
60
62
 
61
- on_exception = [ opts[:on] ].flatten
62
- not_exception = [ opts[:not] ].flatten
63
+ on_exception = [opts[:on]].flatten
64
+ not_exception = [opts[:not]].flatten
63
65
  tries = opts[:tries]
64
66
  retries = 0
65
67
  retry_exception = nil
@@ -71,7 +73,7 @@ module Retryable
71
73
  rescue *on_exception => exception
72
74
  raise unless configuration.enabled?
73
75
  raise unless exception.message =~ opts[:matching]
74
- raise if tries != :infinite && retries+1 >= tries
76
+ raise if tries != :infinite && retries + 1 >= tries
75
77
 
76
78
  # Interrupt Exception could be raised while sleeping
77
79
  begin
@@ -95,8 +97,8 @@ module Retryable
95
97
 
96
98
  def check_for_invalid_options(custom_options, default_options)
97
99
  invalid_options = default_options.merge(custom_options).keys - default_options.keys
98
-
99
- raise ArgumentError.new("[Retryable] Invalid options: #{invalid_options.join(", ")}") unless invalid_options.empty?
100
+ return if invalid_options.empty?
101
+ raise ArgumentError, "[Retryable] Invalid options: #{invalid_options.join(', ')}"
100
102
  end
101
103
  end
102
104
  end
@@ -1,45 +1,39 @@
1
1
  module Retryable
2
2
  # Used to set up and modify settings for the retryable.
3
3
  class Configuration
4
- OPTIONS = [
4
+ VALID_OPTION_KEYS = [
5
+ :contexts,
5
6
  :ensure,
6
7
  :exception_cb,
7
8
  :matching,
9
+ :not,
8
10
  :on,
9
11
  :sleep,
10
- :tries,
11
- :not,
12
- :sleep_method
12
+ :sleep_method,
13
+ :tries
13
14
  ].freeze
14
15
 
15
- attr_accessor :ensure
16
- attr_accessor :exception_cb
17
- attr_accessor :matching
18
- attr_accessor :on
19
- attr_accessor :sleep
20
- attr_accessor :tries
21
- attr_accessor :not
22
- attr_accessor :sleep_method
16
+ attr_accessor(*VALID_OPTION_KEYS)
23
17
 
24
18
  attr_accessor :enabled
25
19
 
26
- alias_method :enabled?, :enabled
27
-
28
20
  def initialize
29
- @ensure = Proc.new {}
30
- @exception_cb = Proc.new {}
21
+ @contexts = {}
22
+ @ensure = proc {}
23
+ @exception_cb = proc {}
31
24
  @matching = /.*/
32
25
  @on = StandardError
33
26
  @sleep = 1
34
27
  @tries = 2
35
28
  @not = []
36
- @sleep_method = lambda do |seconds| Kernel.sleep(seconds) end
37
- @enabled = true
29
+ @sleep_method = ->(seconds) { Kernel.sleep(seconds) }
30
+ @enabled = true
38
31
  end
39
32
 
40
33
  def enable
41
34
  @enabled = true
42
35
  end
36
+ alias enabled? enabled
43
37
 
44
38
  def disable
45
39
  @enabled = false
@@ -54,9 +48,8 @@ module Retryable
54
48
 
55
49
  # Returns a hash of all configurable options
56
50
  def to_hash
57
- OPTIONS.inject({}) do |hash, option|
58
- hash[option.to_sym] = self.send(option)
59
- hash
51
+ VALID_OPTION_KEYS.each_with_object({}) do |key, memo|
52
+ memo[key] = instance_variable_get("@#{key}")
60
53
  end
61
54
  end
62
55
 
@@ -1,16 +1,40 @@
1
1
  module Retryable
2
- class Version
3
- MAJOR = 2 unless defined? Retryable::Version::MAJOR
4
- MINOR = 0 unless defined? Retryable::Version::MINOR
5
- PATCH = 4 unless defined? Retryable::Version::PATCH
2
+ # This module holds the Retryable version information.
3
+ module Version
4
+ module_function
6
5
 
7
- class << self
6
+ # @return [Integer]
7
+ def major
8
+ 3
9
+ end
10
+
11
+ # @return [Integer]
12
+ def minor
13
+ 0
14
+ end
15
+
16
+ # @return [Integer]
17
+ def patch
18
+ 0
19
+ end
20
+
21
+ # @return [Hash]
22
+ def to_h
23
+ {
24
+ major: major,
25
+ minor: minor,
26
+ patch: patch
27
+ }
28
+ end
8
29
 
9
- # @return [String]
10
- def to_s
11
- [MAJOR, MINOR, PATCH].compact.join('.')
12
- end
30
+ # @return [Hash]
31
+ def to_a
32
+ [major, minor, patch].compact
33
+ end
13
34
 
35
+ # @return [String]
36
+ def to_s
37
+ to_a.join('.')
14
38
  end
15
39
  end
16
40
  end
@@ -1,20 +1,24 @@
1
- # -*- encoding: utf-8 -*-
2
- require File.expand_path('../lib/retryable/version', __FILE__)
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'retryable/version'
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
+ Gem::Specification.new do |spec|
6
+ spec.name = 'retryable'
7
+ spec.version = Retryable::Version
8
+ spec.authors = ['Nikita Fedyashev', 'Carlo Zottmann', 'Chu Yeow']
9
+ spec.email = ['nfedyashev@gmail.com']
10
+
11
+ spec.summary = 'Retrying code blocks in Ruby'
12
+ spec.description = spec.summary
13
+ spec.homepage = 'http://github.com/nfedyashev/retryable'
14
+ spec.licenses = ['MIT']
15
+
16
+ spec.require_paths = ['lib']
17
+ spec.files = Dir['{config,lib,spec}/**/*', '*.md', '*.gemspec', 'Gemfile', 'Rakefile']
18
+ spec.test_files = spec.files.grep(%r{^spec/})
19
+
20
+ spec.required_ruby_version = Gem::Requirement.new('>= 1.9.3')
21
+ spec.required_rubygems_version = Gem::Requirement.new('>= 1.3.6')
22
+
23
+ spec.add_development_dependency 'bundler', '~> 1.0'
20
24
  end
@@ -2,22 +2,22 @@ require 'spec_helper'
2
2
 
3
3
  RSpec.describe Retryable do
4
4
  it 'is enabled by default' do
5
- expect(Retryable).to be_enabled
5
+ expect(described_class).to be_enabled
6
6
  end
7
7
 
8
8
  it 'could be disabled' do
9
- Retryable.disable
10
- expect(Retryable).not_to be_enabled
9
+ described_class.disable
10
+ expect(described_class).not_to be_enabled
11
11
  end
12
12
 
13
13
  context 'when disabled' do
14
14
  before do
15
- Retryable.disable
15
+ described_class.disable
16
16
  end
17
17
 
18
18
  it 'could be re-enabled' do
19
- Retryable.enable
20
- expect(Retryable).to be_enabled
19
+ described_class.enable
20
+ expect(described_class).to be_enabled
21
21
  end
22
22
  end
23
23
 
@@ -25,15 +25,15 @@ RSpec.describe Retryable do
25
25
  it 'passes retry count and exception on retry' do
26
26
  expect(Kernel).to receive(:sleep).once.with(3)
27
27
 
28
- Retryable.configure do |config|
28
+ described_class.configure do |config|
29
29
  config.sleep = 3
30
30
  end
31
31
 
32
- count_retryable(:tries => 2) do |tries, ex|
32
+ counter(tries: 2) do |tries, ex|
33
33
  expect(ex.class).to eq(StandardError) if tries > 0
34
34
  raise StandardError if tries < 1
35
35
  end
36
- expect(@try_count).to eq(2)
36
+ expect(counter.count).to eq(2)
37
37
  end
38
38
  end
39
39
  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,152 @@
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 try on unexpected exception' do
61
+ allow(Kernel).to receive(:sleep)
62
+ expect do
63
+ counter(on: RuntimeError) { |tries| raise StandardError if tries < 1 }
64
+ end.to raise_error StandardError
65
+ expect(counter.count).to eq(1)
66
+ end
67
+
68
+ it 'retries three times' do
69
+ allow(Kernel).to receive(:sleep)
70
+ counter(tries: 3) { |tries| raise StandardError if tries < 2 }
71
+ expect(counter.count).to eq(3)
72
+ end
73
+
74
+ it 'retries infinitely' do
75
+ expect do
76
+ Timeout.timeout(3) do
77
+ counter(tries: :infinite, sleep: 0.1) { raise StandardError }
78
+ end
79
+ end.to raise_error Timeout::Error
80
+
81
+ expect(counter.count).to be > 10
82
+ end
83
+
84
+ it 'executes exponential backoff scheme for :sleep option' do
85
+ [1, 4, 16, 64].each { |i| expect(Kernel).to receive(:sleep).once.ordered.with(i) }
86
+ expect do
87
+ described_class.retryable(tries: 5, sleep: ->(n) { 4**n }) { raise RangeError }
88
+ end.to raise_error RangeError
89
+ end
90
+
91
+ it 'calls :sleep_method option' do
92
+ sleep_method = double
93
+ expect(sleep_method).to receive(:call).twice
94
+ expect do
95
+ described_class.retryable(tries: 3, sleep_method: sleep_method) { |tries| raise RangeError if tries < 9 }
96
+ end.to raise_error RangeError
97
+ end
98
+
99
+ it 'does not retry any exception if :on is empty list' do
100
+ expect do
101
+ counter(on: []) { raise }
102
+ end.to raise_error RuntimeError
103
+ expect(counter.count).to eq(1)
104
+ end
105
+
106
+ it 'catches an exception that matches the regex' do
107
+ expect(Kernel).to receive(:sleep).once.with(1)
108
+ counter(matching: /IO timeout/) { |c, _e| raise 'yo, IO timeout!' if c == 0 }
109
+ expect(counter.count).to eq(2)
110
+ end
111
+
112
+ it 'does not catch an exception that does not match the regex' do
113
+ expect(Kernel).not_to receive(:sleep)
114
+ expect do
115
+ counter(matching: /TimeError/) { raise 'yo, IO timeout!' }
116
+ end.to raise_error RuntimeError
117
+ expect(counter.count).to eq(1)
118
+ end
119
+
120
+ it 'does not allow invalid options' do
121
+ expect do
122
+ described_class.retryable(bad_option: 2) { raise 'this is bad' }
123
+ end.to raise_error ArgumentError, '[Retryable] Invalid options: bad_option'
124
+ end
125
+
126
+ it 'accepts a callback to run after an exception is rescued' do
127
+ expect do
128
+ described_class.retryable(sleep: 0, exception_cb: proc { |e| @raised = e.to_s }) do |tries|
129
+ raise StandardError, 'this is fun!' if tries < 1
130
+ end
131
+ end.not_to raise_error
132
+
133
+ expect(@raised).to eq('this is fun!')
134
+ end
135
+
136
+ it 'does not retry on :not exception' do
137
+ expect do
138
+ counter(not: RuntimeError) { |tries| raise RuntimeError if tries < 1 }
139
+ end.to raise_error RuntimeError
140
+ expect(counter.count).to eq(1)
141
+ end
142
+
143
+ it 'gives precidence for :not over :on' do
144
+ expect do
145
+ counter(sleep: 0, tries: 3, on: StandardError, not: IndexError) do |tries|
146
+ raise tries >= 1 ? IndexError : StandardError
147
+ end
148
+ end.to raise_error IndexError
149
+ expect(counter.count).to eq(2)
150
+ end
151
+ end
152
+ 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
@@ -1,25 +1,16 @@
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
+ Dir.glob(File.expand_path('../support/**/*.rb', __FILE__), &method(:require))
7
5
 
8
- config.before(:each) do
9
- reset_config
10
- end
6
+ SimpleCov.start
11
7
 
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
8
+ RSpec.configure do |config|
9
+ config.disable_monkey_patching!
19
10
 
20
- private
11
+ config.include(Counter)
21
12
 
22
- def reset_config
13
+ config.before do
23
14
  Retryable.configuration = nil
24
15
  end
25
16
  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,81 @@
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.0
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: 2018-01-09 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: '1.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: '1.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_spec.rb
48
+ - spec/retryable_with_context_spec.rb
57
49
  - spec/spec_helper.rb
58
- has_rdoc: true
50
+ - spec/support/counter.rb
59
51
  homepage: http://github.com/nfedyashev/retryable
60
- licenses:
52
+ licenses:
61
53
  - MIT
54
+ metadata: {}
62
55
  post_install_message:
63
56
  rdoc_options: []
64
-
65
- require_paths:
57
+ require_paths:
66
58
  - lib
67
- required_ruby_version: !ruby/object:Gem::Requirement
68
- none: false
69
- requirements:
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
70
61
  - - ">="
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:
62
+ - !ruby/object:Gem::Version
63
+ version: 1.9.3
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
79
66
  - - ">="
80
- - !ruby/object:Gem::Version
81
- hash: 23
82
- segments:
83
- - 1
84
- - 3
85
- - 6
67
+ - !ruby/object:Gem::Version
86
68
  version: 1.3.6
87
69
  requirements: []
88
-
89
70
  rubyforge_project:
90
- rubygems_version: 1.6.2
71
+ rubygems_version: 2.4.5.2
91
72
  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
73
+ specification_version: 4
74
+ summary: Retrying code blocks in Ruby
75
+ test_files:
76
+ - spec/retryable/configuration_spec.rb
77
+ - spec/retryable/version_spec.rb
78
+ - spec/retryable_spec.rb
79
+ - spec/retryable_with_context_spec.rb
97
80
  - spec/spec_helper.rb
81
+ - spec/support/counter.rb
@@ -1,151 +0,0 @@
1
- require 'spec_helper'
2
- require 'timeout'
3
-
4
- RSpec.describe 'Retryable.retryable' do
5
- before(:each) do
6
- Retryable.enable
7
- @attempt = 0
8
- end
9
-
10
- it 'catch StandardError only by default' do
11
- expect do
12
- count_retryable(:tries => 2) { |tries, ex| raise Exception if tries < 1 }
13
- end.to raise_error Exception
14
- expect(@try_count).to eq(1)
15
- end
16
-
17
- it 'retries on default exception' do
18
- expect(Kernel).to receive(:sleep).once.with(1)
19
-
20
- count_retryable(:tries => 2) { |tries, ex| raise StandardError if tries < 1 }
21
- expect(@try_count).to eq(2)
22
- end
23
-
24
- it 'does not retry if disabled' do
25
- Retryable.disable
26
-
27
- expect do
28
- count_retryable(:tries => 2) { raise }
29
- end.to raise_error RuntimeError
30
- expect(@try_count).to eq(1)
31
- end
32
-
33
- it 'executes *ensure* clause' do
34
- ensure_cb = Proc.new do |retries|
35
- expect(retries).to eq(0)
36
- end
37
-
38
- Retryable.retryable(:ensure => ensure_cb) { }
39
- end
40
-
41
- it 'passes retry count and exception on retry' do
42
- expect(Kernel).to receive(:sleep).once.with(1)
43
-
44
- count_retryable(:tries => 2) do |tries, ex|
45
- expect(ex.class).to eq(StandardError) if tries > 0
46
- raise StandardError if tries < 1
47
- end
48
- expect(@try_count).to eq(2)
49
- end
50
-
51
- it 'makes another try if exception is covered by :on' do
52
- allow(Kernel).to receive(:sleep)
53
- count_retryable(:on => [StandardError, ArgumentError, RuntimeError] ) { |tries, ex| raise ArgumentError if tries < 1 }
54
- expect(@try_count).to eq(2)
55
- end
56
-
57
- it 'does not try on unexpected exception' do
58
- allow(Kernel).to receive(:sleep)
59
- expect do
60
- count_retryable(:on => RuntimeError ) { |tries, ex| raise StandardError if tries < 1 }
61
- end.to raise_error StandardError
62
- expect(@try_count).to eq(1)
63
- end
64
-
65
- it 'retries three times' do
66
- allow(Kernel).to receive(:sleep)
67
- count_retryable(:tries => 3) { |tries, ex| raise StandardError if tries < 2 }
68
- expect(@try_count).to eq(3)
69
- end
70
-
71
- it 'retries infinitely' do
72
- expect do
73
- Timeout::timeout(3) do
74
- count_retryable(:tries => :infinite, :sleep => 0.1) { |tries, ex| raise StandardError }
75
- end
76
- end.to raise_error Timeout::Error
77
-
78
- expect(@try_count).to be > 10
79
- end
80
-
81
- it 'retries on default exception' do
82
- expect(Kernel).to receive(:sleep).once.with(1)
83
-
84
- count_retryable(:tries => 2) { |tries, ex| raise StandardError if tries < 1 }
85
- expect(@try_count).to eq(2)
86
- end
87
-
88
- it 'executes exponential backoff scheme for :sleep option' do
89
- [1, 4, 16, 64].each { |i| expect(Kernel).to receive(:sleep).once.ordered.with(i) }
90
- expect do
91
- Retryable.retryable(:tries => 5, :sleep => lambda { |n| 4**n }) { raise RangeError }
92
- end.to raise_error RangeError
93
- end
94
-
95
- it 'calls :sleep_method option' do
96
- sleep_method = double
97
- expect(sleep_method).to receive(:call).twice
98
- expect do
99
- Retryable.retryable(:tries => 3, :sleep_method => sleep_method) { |tries, ex| raise RangeError if tries < 9}
100
- end.to raise_error RangeError
101
- end
102
-
103
- it 'does not retry any exception if :on is empty list' do
104
- expect do
105
- count_retryable(:on => []) { raise }
106
- end.to raise_error RuntimeError
107
- expect(@try_count).to eq(1)
108
- end
109
-
110
- it 'catches an exception that matches the regex' do
111
- expect(Kernel).to receive(:sleep).once.with(1)
112
- count_retryable(:matching => /IO timeout/) { |c,e| raise "yo, IO timeout!" if c == 0 }
113
- expect(@try_count).to eq(2)
114
- end
115
-
116
- it 'does not catch an exception that does not match the regex' do
117
- expect(Kernel).not_to receive(:sleep)
118
- expect do
119
- count_retryable(:matching => /TimeError/) { raise "yo, IO timeout!" }
120
- end.to raise_error RuntimeError
121
- expect(@try_count).to eq(1)
122
- end
123
-
124
- it 'does not allow invalid options' do
125
- expect do
126
- Retryable.retryable(:bad_option => 2) { raise "this is bad" }
127
- end.to raise_error ArgumentError, '[Retryable] Invalid options: bad_option'
128
- end
129
-
130
- it 'accepts a callback to run after an exception is rescued' do
131
- expect do
132
- Retryable.retryable(:sleep => 0, :exception_cb => Proc.new {|e| @raised = e.to_s }) {|tries, ex| raise StandardError.new("this is fun!") if tries < 1 }
133
- end.not_to raise_error
134
-
135
- expect(@raised).to eq("this is fun!")
136
- end
137
-
138
- it 'does not retry on :not exception' do
139
- expect do
140
- count_retryable(:not => RuntimeError ) { |tries, ex| raise RuntimeError if tries < 1 }
141
- end.to raise_error RuntimeError
142
- expect(@try_count).to eq(1)
143
- end
144
-
145
- it 'gives precidence for :not over :on' do
146
- expect do
147
- count_retryable(:sleep => 0, :tries => 3, :on => StandardError, :not => IndexError ) { |tries, ex| raise tries >= 1 ? IndexError : StandardError }
148
- end.to raise_error IndexError
149
- expect(@try_count).to eq(2)
150
- end
151
- end