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