take2 0.0.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,4 +2,13 @@
2
2
  #
3
3
  * Add class validations for class helper methods
4
4
  * Add basic documentation
5
- * Rename the public api to :call_with_retry
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "http://rubygems.org"
2
4
  gemspec
3
5
 
@@ -10,4 +12,5 @@ end
10
12
  group :development, :test do
11
13
  gem 'pry'
12
14
  gem 'pry-nav'
13
- end
15
+ gem 'rubocop'
16
+ end
@@ -1,21 +1,28 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- take2 (0.0.3)
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
- rake (12.3.1)
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.16.4
63
+ 1.17.3
data/README.md CHANGED
@@ -3,9 +3,9 @@
3
3
  ![Gem](https://img.shields.io/gem/dt/take2.svg)
4
4
  ![GitHub last commit](https://img.shields.io/github/last-commit/restaurant-cheetah/take2.svg)
5
5
  ![Gem](https://img.shields.io/gem/v/take2.svg)
6
- Define rules for retrying behavior.
7
- Yield block of code into the public api of the take2.
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 KratosService
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, Net::HTTPServerError
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 "#{self.name} - Retrying.. #{tries} of #{self.retriable_configuration[:retries]} (#{error})" }
32
-
33
- sleep_before_retry 3.3
34
-
35
- def call_boy
36
- call_api_with_retry do
37
- # Some logic that might raise..
38
- # If it will raise retriable, magic happens.
39
- # If not the original error re raised
40
-
41
- raise Net::HTTPRetriableError.new 'Release the Kraken...many times!!', nil
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
- # Pass custom options per method call
46
- # The class defaults will not be overwritten
47
- def kill_baldur
48
- call_api_with_retry(retries: 2, retriable: [IOError], retry_proc: proc {}, retry_condition_proc: proc {}, time_to_sleep: 1.11) do
49
- # Some logic that might raise..
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
- KratosService.new.call_boy
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
- KratosService.retriable_configuration
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.time_to_sleep = nil
91
- config.retry_proc = proc {Rails.logger.info "Retry message"}
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
  ```
@@ -1,38 +1,40 @@
1
- require 'net/http'
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 ClassMethods
10
- base.send :set_defaults
11
- base.send :include, InstanceMethods
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
- def self.configuration
19
- @configuration ||= Configuration.new
20
- end
17
+ def config
18
+ @configuration ||= Configuration.new
19
+ end
21
20
 
22
- def self.reset(options = {})
23
- @configuration = Configuration.new(options)
24
- end
21
+ def reset(options = {})
22
+ @configuration = Configuration.new(options)
23
+ end
25
24
 
26
- def self.local_defaults(options)
27
- configuration.validate_options(options)
28
- end
25
+ def local_defaults(options)
26
+ configuration.validate!(options)
27
+ end
29
28
 
30
- def self.configure
31
- yield(configuration) if block_given?
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, Net::HTTPServerError
46
+ # retriable_errors Net::HTTPRetriableError
45
47
  # retriable_condition proc { |error| response_status(error.response) < 500 }
46
- # on_retry proc { |error, tries| puts "#{self.name} - Retrying.. #{tries} of #{self.retriable_configuration[:retries]} (#{error})" }
47
- # sleep_before_retry 3
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
- # call_api_with_retry do
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
- config = self.class.retriable_configuration
60
- config.merge! Take2.local_defaults(options) unless options.empty?
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
- sleep(config[:time_to_sleep]) if config[:time_to_sleep]
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
- module ClassMethods
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 retiable errors
113
+ # errors: List of retriable errors
103
114
  def retriable_errors(*errors)
104
- raise ArgumentError, 'All retriable errors must be StandardError decendants' unless errors.all? { |e| e <= StandardError }
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 number of seconds to sleep before next retry.
149
+ # Sets the backoff strategy
138
150
  #
139
151
  # Example:
140
152
  # class PizzaService
141
153
  # include Take2
142
- # sleep_before_retry 1.5
154
+ # backoff_strategy type: :exponential, start: 3
143
155
  # end
144
156
  # Arguments:
145
- # seconds: number
146
- def sleep_before_retry(seconds)
147
- raise ArgumentError, 'Must be positive numer' unless (seconds.is_a?(Integer) || seconds.is_a?(Float)) && seconds.positive?
148
- self.time_to_sleep = seconds
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
- config = Take2.configuration.to_hash
164
- Take2::Configuration::CONFIG_ATTRS.each do |attr|
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? :status
171
- response.status_code if response.respond_to? :status_code
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
- end
186
+ def rest(config, tries)
187
+ seconds = next_interval(config[:backoff_intervals], config[:retries], tries)
188
+ sleep(seconds)
189
+ end
175
190
 
176
- end
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