take2 0.0.3 → 1.0.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 +5 -5
- data/.circleci/config.yml +1 -1
- data/.gitignore +3 -0
- data/.hound.yml +3 -0
- data/.rubocop.yml +1201 -0
- data/CHANGELOG.md +10 -1
- data/Gemfile +4 -1
- data/Gemfile.lock +21 -3
- data/README.md +35 -39
- data/lib/take2.rb +67 -48
- data/lib/take2/backoff.rb +48 -0
- data/lib/take2/configuration.rb +35 -30
- data/lib/take2/version.rb +4 -2
- data/spec/spec_helper.rb +3 -1
- data/spec/take2/configuration_spec.rb +34 -63
- data/spec/take2_spec.rb +146 -152
- data/take2.gemspec +9 -7
- metadata +8 -6
data/CHANGELOG.md
CHANGED
@@ -2,4 +2,13 @@
|
|
2
2
|
#
|
3
3
|
* Add class validations for class helper methods
|
4
4
|
* Add basic documentation
|
5
|
-
|
5
|
+
#
|
6
|
+
0.0.2
|
7
|
+
#
|
8
|
+
* Add more tests
|
9
|
+
#
|
10
|
+
v0.0.3
|
11
|
+
* Fix configurations bug (#4)
|
12
|
+
* Ability to set custom configurations on method call (#1)
|
13
|
+
* More tests (#3)
|
14
|
+
* Update docs and badges
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,21 +1,28 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
take2 (0.0
|
4
|
+
take2 (1.0.0)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: http://rubygems.org/
|
8
8
|
specs:
|
9
|
+
ast (2.4.0)
|
9
10
|
coderay (1.1.2)
|
10
11
|
diff-lcs (1.3)
|
12
|
+
jaro_winkler (1.5.2)
|
11
13
|
method_source (0.8.2)
|
14
|
+
parallel (1.12.1)
|
15
|
+
parser (2.5.3.0)
|
16
|
+
ast (~> 2.4.0)
|
17
|
+
powerpack (0.1.2)
|
12
18
|
pry (0.10.4)
|
13
19
|
coderay (~> 1.1.0)
|
14
20
|
method_source (~> 0.8.1)
|
15
21
|
slop (~> 3.4)
|
16
22
|
pry-nav (0.2.4)
|
17
23
|
pry (>= 0.9.10, < 0.11.0)
|
18
|
-
|
24
|
+
rainbow (3.0.0)
|
25
|
+
rake (13.0.1)
|
19
26
|
rspec (3.8.0)
|
20
27
|
rspec-core (~> 3.8.0)
|
21
28
|
rspec-expectations (~> 3.8.0)
|
@@ -29,7 +36,17 @@ GEM
|
|
29
36
|
diff-lcs (>= 1.2.0, < 2.0)
|
30
37
|
rspec-support (~> 3.8.0)
|
31
38
|
rspec-support (3.8.0)
|
39
|
+
rubocop (0.62.0)
|
40
|
+
jaro_winkler (~> 1.5.1)
|
41
|
+
parallel (~> 1.10)
|
42
|
+
parser (>= 2.5, != 2.5.1.1)
|
43
|
+
powerpack (~> 0.1)
|
44
|
+
rainbow (>= 2.2.2, < 4.0)
|
45
|
+
ruby-progressbar (~> 1.7)
|
46
|
+
unicode-display_width (~> 1.4.0)
|
47
|
+
ruby-progressbar (1.10.0)
|
32
48
|
slop (3.6.0)
|
49
|
+
unicode-display_width (1.4.1)
|
33
50
|
|
34
51
|
PLATFORMS
|
35
52
|
ruby
|
@@ -39,7 +56,8 @@ DEPENDENCIES
|
|
39
56
|
pry-nav
|
40
57
|
rake
|
41
58
|
rspec (= 3.8.0)
|
59
|
+
rubocop
|
42
60
|
take2!
|
43
61
|
|
44
62
|
BUNDLED WITH
|
45
|
-
1.
|
63
|
+
1.17.3
|
data/README.md
CHANGED
@@ -3,9 +3,9 @@
|
|
3
3
|

|
4
4
|

|
5
5
|

|
6
|
-
Define rules for retrying behavior.
|
7
|
-
Yield block of code into the
|
8
|
-
Things getting take two :)
|
6
|
+
1. Define rules for retrying behavior.
|
7
|
+
2. Yield block of code into the with_retry method.
|
8
|
+
3. Things getting take two :)
|
9
9
|
|
10
10
|
## Install
|
11
11
|
|
@@ -15,59 +15,57 @@ gem install take2
|
|
15
15
|
## Examples
|
16
16
|
|
17
17
|
```ruby
|
18
|
-
class
|
18
|
+
class Service
|
19
19
|
include Take2
|
20
20
|
|
21
|
-
|
22
21
|
number_of_retries 3
|
23
|
-
|
22
|
+
|
24
23
|
# Could be configured globally or on class level.
|
25
|
-
retriable_errors Net::HTTPRetriableError,
|
24
|
+
retriable_errors Net::HTTPRetriableError, Errno::ECONNRESET
|
26
25
|
|
27
26
|
# Retry unless the response status is 5xx. The implementation is dependent of the http lib in use.
|
28
27
|
retriable_condition proc { |error| error.response.code < 500 }
|
29
28
|
|
30
29
|
# Defines callable code to run before next retry. Could be an out put to some logger.
|
31
|
-
on_retry proc { |error, tries| puts "#{
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
30
|
+
on_retry proc { |error, tries| puts "#{name} - Retrying.. #{tries} of #{retriable_configuration[:retries]} (#{error})" }
|
31
|
+
|
32
|
+
# The available strategies are:
|
33
|
+
# type :constant, start: 2 => [2, 2, 2, 2 ... ]
|
34
|
+
# type :linear, start: 3, factor: 2 => [3, 6, 12, 24 ... ]
|
35
|
+
# type :fibonacci, start: 2 => [2, 3, 5, 8, 13 ... ]
|
36
|
+
# type :exponential, start: 3 => [3, 7, 12, 28, 47 ... ]
|
37
|
+
backoff_strategy type: :fibonacci, start: 3
|
38
|
+
|
39
|
+
class << self
|
40
|
+
def call
|
41
|
+
with_retry do
|
42
|
+
# Some logic that might raise..
|
43
|
+
# If it will raise retriable, magic happens.
|
44
|
+
# If not the original error re raised
|
45
|
+
|
46
|
+
raise Net::HTTPRetriableError.new('Release the Kraken...many times!!', nil)
|
47
|
+
end
|
42
48
|
end
|
43
|
-
end
|
44
49
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
+
# Pass custom options per method call
|
51
|
+
# The class defaults will not be overwritten
|
52
|
+
def read(file)
|
53
|
+
with_retry(retries: 2, retriable: [IOError], retry_proc: proc {}, retry_condition_proc: proc {}) do
|
54
|
+
# Some logic that might raise..
|
55
|
+
end
|
50
56
|
end
|
51
57
|
end
|
52
|
-
|
53
58
|
end
|
54
59
|
|
55
|
-
|
60
|
+
Service.call
|
56
61
|
#=> KratosService - Retrying.. 3 of 3 (Release the Kraken...many times!!)
|
57
62
|
#=> KratosService - Retrying.. 2 of 3 (Release the Kraken...many times!!)
|
58
63
|
#=> KratosService - Retrying.. 1 of 3 (Release the Kraken...many times!!)
|
59
64
|
# After the retrying is done, original error re-raised
|
60
65
|
#=> Net::HTTPRetriableError: Release the Kraken...many times!!
|
61
66
|
|
62
|
-
# Not wrapping with method
|
63
|
-
KratosService.new.call_api_with_retry { 1 / 0 }
|
64
|
-
|
65
|
-
# Or..
|
66
|
-
Class.new { include Take2 }.new.call_api_with_retry { 1 / 0 }
|
67
|
-
|
68
|
-
|
69
67
|
# Current configuration hash
|
70
|
-
|
68
|
+
Service.retriable_configuration
|
71
69
|
|
72
70
|
```
|
73
71
|
|
@@ -80,14 +78,12 @@ KratosService.retriable_configuration
|
|
80
78
|
Take2.configure do |config|
|
81
79
|
config.retries = 3
|
82
80
|
config.retriable = [
|
83
|
-
Net::HTTPServerError,
|
84
|
-
Net::HTTPServerException,
|
85
81
|
Net::HTTPRetriableError,
|
86
82
|
Errno::ECONNRESET,
|
87
83
|
IOError
|
88
84
|
].freeze
|
89
|
-
config.retry_condition_proc = proc {false}
|
90
|
-
config.
|
91
|
-
config.
|
85
|
+
config.retry_condition_proc = proc { false }
|
86
|
+
config.retry_proc = proc { Rails.logger.info "Retry message" }
|
87
|
+
config.backoff_intervals = Take2::Backoff.new(:linear, 1).intervals
|
92
88
|
end
|
93
89
|
```
|
data/lib/take2.rb
CHANGED
@@ -1,38 +1,40 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'net/http'
|
3
4
|
require 'take2/version'
|
4
5
|
require 'take2/configuration'
|
5
6
|
|
6
7
|
module Take2
|
7
|
-
|
8
8
|
def self.included(base)
|
9
|
-
base.extend
|
10
|
-
base.send
|
11
|
-
base.send
|
9
|
+
base.extend(ClassMethods)
|
10
|
+
base.send(:set_defaults)
|
11
|
+
base.send(:include, InstanceMethods)
|
12
12
|
end
|
13
13
|
|
14
14
|
class << self
|
15
15
|
attr_accessor :configuration
|
16
|
-
end
|
17
16
|
|
18
|
-
|
19
|
-
|
20
|
-
|
17
|
+
def config
|
18
|
+
@configuration ||= Configuration.new
|
19
|
+
end
|
21
20
|
|
22
|
-
|
23
|
-
|
24
|
-
|
21
|
+
def reset(options = {})
|
22
|
+
@configuration = Configuration.new(options)
|
23
|
+
end
|
25
24
|
|
26
|
-
|
27
|
-
|
28
|
-
|
25
|
+
def local_defaults(options)
|
26
|
+
configuration.validate!(options)
|
27
|
+
end
|
29
28
|
|
30
|
-
|
31
|
-
|
29
|
+
def configure
|
30
|
+
if block_given?
|
31
|
+
yield(config)
|
32
|
+
config.validate!(config.to_hash)
|
33
|
+
end
|
34
|
+
end
|
32
35
|
end
|
33
36
|
|
34
37
|
module InstanceMethods
|
35
|
-
|
36
38
|
# Yields a block and retries on retriable errors n times.
|
37
39
|
# The raised error could be the defined retriable or it child.
|
38
40
|
#
|
@@ -41,13 +43,15 @@ module Take2
|
|
41
43
|
# include Take2
|
42
44
|
#
|
43
45
|
# number_of_retries 3
|
44
|
-
# retriable_errors Net::HTTPRetriableError
|
46
|
+
# retriable_errors Net::HTTPRetriableError
|
45
47
|
# retriable_condition proc { |error| response_status(error.response) < 500 }
|
46
|
-
# on_retry proc { |error, tries|
|
47
|
-
#
|
48
|
+
# on_retry proc { |error, tries|
|
49
|
+
# puts "#{self.name} - Retrying.. #{tries} of #{self.retriable_configuration[:retries]} (#{error})"
|
50
|
+
# }
|
51
|
+
# backoff_strategy type: :exponential, start: 3
|
48
52
|
#
|
49
53
|
# def give_me_food
|
50
|
-
#
|
54
|
+
# with_retry do
|
51
55
|
# # Some logic that might raise..
|
52
56
|
# # If it will raise retriable, magic happens.
|
53
57
|
# # If not the original error re raised
|
@@ -55,28 +59,35 @@ module Take2
|
|
55
59
|
# end
|
56
60
|
#
|
57
61
|
# end
|
58
|
-
def call_api_with_retry(options = {})
|
59
|
-
|
60
|
-
|
62
|
+
def call_api_with_retry(options = {}, &block)
|
63
|
+
self.class.call_api_with_retry(options, &block)
|
64
|
+
end
|
65
|
+
|
66
|
+
alias_method :with_retry, :call_api_with_retry
|
67
|
+
end
|
68
|
+
|
69
|
+
module ClassMethods
|
70
|
+
def call_api_with_retry(options = {})
|
71
|
+
config = retriable_configuration
|
72
|
+
config.merge!(Take2.local_defaults(options)) unless options.empty?
|
61
73
|
tries ||= config[:retries]
|
62
74
|
begin
|
63
75
|
yield
|
64
76
|
rescue => e
|
65
|
-
if config[:retriable].map {|klass| e.class <= klass }.any?
|
77
|
+
if config[:retriable].map { |klass| e.class <= klass }.any?
|
66
78
|
unless tries.zero? || config[:retry_condition_proc]&.call(e)
|
67
79
|
config[:retry_proc]&.call(e, tries)
|
68
|
-
|
80
|
+
rest(config, tries)
|
69
81
|
tries -= 1
|
70
82
|
retry
|
71
83
|
end
|
72
|
-
end
|
84
|
+
end
|
73
85
|
raise e
|
74
86
|
end
|
75
87
|
end
|
76
|
-
|
77
|
-
end
|
78
88
|
|
79
|
-
|
89
|
+
alias_method :with_retry, :call_api_with_retry
|
90
|
+
|
80
91
|
# Sets number of retries.
|
81
92
|
#
|
82
93
|
# Example:
|
@@ -99,13 +110,14 @@ module Take2
|
|
99
110
|
# retriable_errors Net::HTTPRetriableError, Errno::ECONNRESET
|
100
111
|
# end
|
101
112
|
# Arguments:
|
102
|
-
# errors: List of
|
113
|
+
# errors: List of retriable errors
|
103
114
|
def retriable_errors(*errors)
|
104
|
-
|
115
|
+
message = 'All retriable errors must be StandardError descendants'
|
116
|
+
raise ArgumentError, message unless errors.all? { |e| e <= StandardError }
|
105
117
|
self.retriable = errors
|
106
118
|
end
|
107
119
|
|
108
|
-
# Sets condition for retry attempt.
|
120
|
+
# Sets condition for retry attempt.
|
109
121
|
# If set, it MUST result to +false+ with number left retries greater that zero in order to retry.
|
110
122
|
#
|
111
123
|
# Example:
|
@@ -120,7 +132,7 @@ module Take2
|
|
120
132
|
self.retry_condition_proc = proc
|
121
133
|
end
|
122
134
|
|
123
|
-
# Defines a proc that is called *before* retry attempt.
|
135
|
+
# Defines a proc that is called *before* retry attempt.
|
124
136
|
#
|
125
137
|
# Example:
|
126
138
|
# class PizzaService
|
@@ -134,18 +146,19 @@ module Take2
|
|
134
146
|
self.retry_proc = proc
|
135
147
|
end
|
136
148
|
|
137
|
-
# Sets
|
149
|
+
# Sets the backoff strategy
|
138
150
|
#
|
139
151
|
# Example:
|
140
152
|
# class PizzaService
|
141
153
|
# include Take2
|
142
|
-
#
|
154
|
+
# backoff_strategy type: :exponential, start: 3
|
143
155
|
# end
|
144
156
|
# Arguments:
|
145
|
-
#
|
146
|
-
def
|
147
|
-
|
148
|
-
|
157
|
+
# hash: object
|
158
|
+
def backoff_strategy(options)
|
159
|
+
available_types = [:constant, :linear, :fibonacci, :exponential]
|
160
|
+
raise ArgumentError, 'Incorrect backoff type' unless available_types.include?(options[:type])
|
161
|
+
self.backoff_intervals = Backoff.new(options[:type], options[:start]).intervals
|
149
162
|
end
|
150
163
|
|
151
164
|
# Exposes current class configuration
|
@@ -160,17 +173,23 @@ module Take2
|
|
160
173
|
attr_accessor(*Take2::Configuration::CONFIG_ATTRS)
|
161
174
|
|
162
175
|
def set_defaults
|
163
|
-
|
164
|
-
|
165
|
-
instance_variable_set("@#{attr}", config[attr])
|
176
|
+
Take2.config.to_hash.each do |k, v|
|
177
|
+
instance_variable_set("@#{k}", v)
|
166
178
|
end
|
167
179
|
end
|
168
180
|
|
169
181
|
def response_status(response)
|
170
|
-
return response.status if response.respond_to?
|
171
|
-
response.status_code if response.respond_to?
|
182
|
+
return response.status if response.respond_to?(:status)
|
183
|
+
response.status_code if response.respond_to?(:status_code)
|
172
184
|
end
|
173
185
|
|
174
|
-
|
186
|
+
def rest(config, tries)
|
187
|
+
seconds = next_interval(config[:backoff_intervals], config[:retries], tries)
|
188
|
+
sleep(seconds)
|
189
|
+
end
|
175
190
|
|
176
|
-
|
191
|
+
def next_interval(intervals, retries, current)
|
192
|
+
intervals[retries - current]
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Take2
|
4
|
+
class Backoff
|
5
|
+
attr_reader :type, :start, :retries, :factor, :intervals
|
6
|
+
|
7
|
+
def initialize(type, start = 1, factor = 1, retries = 10)
|
8
|
+
@type = type
|
9
|
+
@start = start.to_i
|
10
|
+
@retries = retries
|
11
|
+
@factor = factor
|
12
|
+
@intervals = intervals_table
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def intervals_table
|
18
|
+
send(type)
|
19
|
+
end
|
20
|
+
|
21
|
+
def constant
|
22
|
+
Array.new(retries, start)
|
23
|
+
end
|
24
|
+
|
25
|
+
def linear
|
26
|
+
(start...(retries + start)).map { |i| i * factor }
|
27
|
+
end
|
28
|
+
|
29
|
+
def fibonacci
|
30
|
+
(1..20).map { |i| fibo(i) }.partition { |x| x >= start }.first.take(retries)
|
31
|
+
end
|
32
|
+
|
33
|
+
def exponential
|
34
|
+
(1..20).each_with_index.inject([]) do |memo, (el, ix)|
|
35
|
+
memo << if ix == 0
|
36
|
+
start
|
37
|
+
else
|
38
|
+
(2**el - 1) + rand(1..2**el)
|
39
|
+
end
|
40
|
+
end.take(retries)
|
41
|
+
end
|
42
|
+
|
43
|
+
def fibo(n, memo = {})
|
44
|
+
return n if n < 2
|
45
|
+
memo[n] ||= fibo(n - 1, memo) + fibo(n - 2, memo)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|