take2 0.0.4 → 0.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/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,20 +1,27 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- take2 (0.0.3)
4
+ take2 (0.0.5)
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)
24
+ rainbow (3.0.0)
18
25
  rake (12.3.1)
19
26
  rspec (3.8.0)
20
27
  rspec-core (~> 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.16.6
@@ -1,21 +1,21 @@
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
16
  end
17
17
 
18
- def self.configuration
18
+ def self.config
19
19
  @configuration ||= Configuration.new
20
20
  end
21
21
 
@@ -28,11 +28,10 @@ module Take2
28
28
  end
29
29
 
30
30
  def self.configure
31
- yield(configuration) if block_given?
31
+ yield(config) if block_given?
32
32
  end
33
33
 
34
34
  module InstanceMethods
35
-
36
35
  # Yields a block and retries on retriable errors n times.
37
36
  # The raised error could be the defined retriable or it child.
38
37
  #
@@ -43,8 +42,10 @@ module Take2
43
42
  # number_of_retries 3
44
43
  # retriable_errors Net::HTTPRetriableError
45
44
  # 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
45
+ # on_retry proc { |error, tries|
46
+ # puts "#{self.name} - Retrying.. #{tries} of #{self.retriable_configuration[:retries]} (#{error})"
47
+ # }
48
+ # backoff_strategy type: :exponential, start: 3
48
49
  #
49
50
  # def give_me_food
50
51
  # call_api_with_retry do
@@ -55,25 +56,40 @@ module Take2
55
56
  # end
56
57
  #
57
58
  # end
58
- def call_api_with_retry(options = {})
59
+ def call_api_with_retry(options = {})
59
60
  config = self.class.retriable_configuration
60
- config.merge! Take2.local_defaults(options) unless options.empty?
61
+ config.merge!(Take2.local_defaults(options)) unless options.empty?
61
62
  tries ||= config[:retries]
62
63
  begin
63
64
  yield
64
65
  rescue => e
65
- if config[:retriable].map {|klass| e.class <= klass }.any?
66
+ if config[:retriable].map { |klass| e.class <= klass }.any?
66
67
  unless tries.zero? || config[:retry_condition_proc]&.call(e)
67
68
  config[:retry_proc]&.call(e, tries)
68
- sleep(config[:time_to_sleep]) if config[:time_to_sleep]
69
+ rest(config, tries)
69
70
  tries -= 1
70
71
  retry
71
72
  end
72
- end
73
+ end
73
74
  raise e
74
75
  end
75
76
  end
76
-
77
+ alias_method :with_retry, :call_api_with_retry
78
+
79
+ private
80
+
81
+ def rest(config, tries)
82
+ seconds = if config[:time_to_sleep].to_f > 0
83
+ config[:time_to_sleep].to_f
84
+ else
85
+ next_interval(config[:backoff_intervals], config[:retries], tries)
86
+ end
87
+ sleep(seconds)
88
+ end
89
+
90
+ def next_interval(intervals, retries, current)
91
+ intervals[retries - current]
92
+ end
77
93
  end
78
94
 
79
95
  module ClassMethods
@@ -101,11 +117,12 @@ module Take2
101
117
  # Arguments:
102
118
  # errors: List of retiable errors
103
119
  def retriable_errors(*errors)
104
- raise ArgumentError, 'All retriable errors must be StandardError decendants' unless errors.all? { |e| e <= StandardError }
120
+ message = 'All retriable errors must be StandardError decendants'
121
+ raise ArgumentError, message unless errors.all? { |e| e <= StandardError }
105
122
  self.retriable = errors
106
123
  end
107
124
 
108
- # Sets condition for retry attempt.
125
+ # Sets condition for retry attempt.
109
126
  # If set, it MUST result to +false+ with number left retries greater that zero in order to retry.
110
127
  #
111
128
  # Example:
@@ -120,7 +137,7 @@ module Take2
120
137
  self.retry_condition_proc = proc
121
138
  end
122
139
 
123
- # Defines a proc that is called *before* retry attempt.
140
+ # Defines a proc that is called *before* retry attempt.
124
141
  #
125
142
  # Example:
126
143
  # class PizzaService
@@ -134,18 +151,28 @@ module Take2
134
151
  self.retry_proc = proc
135
152
  end
136
153
 
137
- # Sets number of seconds to sleep before next retry.
154
+ def sleep_before_retry(seconds)
155
+ unless (seconds.is_a?(Integer) || seconds.is_a?(Float)) && seconds.positive?
156
+ raise ArgumentError, 'Must be positive numer'
157
+ end
158
+ puts "DEPRECATION MESSAGE - The sleep_before_retry method is softly deprecated in favor of backoff_stategy \r
159
+ where the time to sleep is a starting point on the backoff intervals. Please implement it instead."
160
+ self.time_to_sleep = seconds
161
+ end
162
+
163
+ # Sets the backoff strategy
138
164
  #
139
165
  # Example:
140
166
  # class PizzaService
141
167
  # include Take2
142
- # sleep_before_retry 1.5
168
+ # backoff_strategy type: :exponential, start: 3
143
169
  # end
144
170
  # 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
171
+ # hash: object
172
+ def backoff_strategy(options)
173
+ available_types = [:constant, :linear, :fibonacci, :exponential]
174
+ raise ArgumentError, 'Incorrect backoff type' unless available_types.include?(options[:type])
175
+ self.backoff_intervals = Backoff.new(options[:type], options[:start]).intervals
149
176
  end
150
177
 
151
178
  # Exposes current class configuration
@@ -167,10 +194,8 @@ module Take2
167
194
  end
168
195
 
169
196
  def response_status(response)
170
- return response.status if response.respond_to? :status
171
- response.status_code if response.respond_to? :status_code
197
+ return response.status if response.respond_to?(:status)
198
+ response.status_code if response.respond_to?(:status_code)
172
199
  end
173
-
174
200
  end
175
-
176
- end
201
+ 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
@@ -1,6 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'take2/backoff'
4
+
1
5
  module Take2
2
6
  class Configuration
3
- CONFIG_ATTRS = [:retries, :retriable, :retry_proc, :retry_condition_proc, :time_to_sleep].freeze
7
+ CONFIG_ATTRS = [:retries,
8
+ :retriable,
9
+ :retry_proc,
10
+ :retry_condition_proc,
11
+ :time_to_sleep,
12
+ :backoff_setup,
13
+ :backoff_intervals].freeze
14
+
4
15
  attr_accessor(*CONFIG_ATTRS)
5
16
 
6
17
  def initialize(options = {})
@@ -8,13 +19,15 @@ module Take2
8
19
  @retries = 3
9
20
  @retriable = [
10
21
  Net::HTTPServerException,
11
- Net::HTTPRetriableError,
22
+ Net::HTTPRetriableError,
12
23
  Errno::ECONNRESET,
13
24
  IOError,
14
- ].freeze
25
+ ].freeze
15
26
  @retry_proc = proc {}
16
27
  @retry_condition_proc = proc { false }
17
- @time_to_sleep = 3
28
+ @time_to_sleep = 0 # TODO: Soft deprecate time to sleep
29
+ @backoff_setup = { type: :constant, start: 3 }
30
+ @backoff_intervals = Backoff.new(*@backoff_setup.values).intervals
18
31
  # Overwriting the defaults
19
32
  validate_options(options, &setter)
20
33
  end
@@ -26,29 +39,41 @@ module Take2
26
39
  end
27
40
 
28
41
  def [](value)
29
- self.public_send(value)
42
+ public_send(value)
30
43
  end
31
44
 
32
- def validate_options(options, &setter)
45
+ def validate_options(options)
33
46
  options.each do |k, v|
34
- raise ArgumentError, "#{k} is not a valid configuration" unless CONFIG_ATTRS.include?(k)
47
+ raise ArgumentError, "#{k} is not a valid configuration" unless CONFIG_ATTRS.include?(k)
35
48
  case k
36
- when :retries
37
- raise ArgumentError, "#{k} must be positive integer" unless v.is_a?(Integer) && v.positive?
38
- when :time_to_sleep
39
- raise ArgumentError, "#{k} must be positive number" unless (v.is_a?(Integer) || v.is_a?(Float)) && v >= 0
40
- when :retriable
41
- raise ArgumentError, "#{k} must be array of retriable errors" unless v.is_a?(Array)
42
- when :retry_proc, :retry_condition_proc
43
- raise ArgumentError, "#{k} must be Proc" unless v.is_a?(Proc)
49
+ when :retries
50
+ raise ArgumentError, "#{k} must be positive integer" unless v.is_a?(Integer) && v.positive?
51
+ when :time_to_sleep
52
+ raise ArgumentError, "#{k} must be positive number" unless (v.is_a?(Integer) || v.is_a?(Float)) && v >= 0
53
+ when :retriable
54
+ raise ArgumentError, "#{k} must be array of retriable errors" unless v.is_a?(Array)
55
+ when :retry_proc, :retry_condition_proc
56
+ raise ArgumentError, "#{k} must be Proc" unless v.is_a?(Proc)
57
+ when :backoff_setup
58
+ available_types = [:constant, :linear, :fibonacci, :exponential]
59
+ raise ArgumentError, 'Incorrect backoff type' unless available_types.include?(v[:type])
44
60
  end
45
- setter.call(k, v) if block_given?
46
- end
61
+ yield(k, v) if block_given?
62
+ end
47
63
  end
48
64
 
49
65
  def setter
50
- proc { |key, value| instance_variable_set(:"@#{key}", value) }
66
+ ->(key, value) {
67
+ if key == :backoff_setup
68
+ assign_backoff_intervals(value)
69
+ else
70
+ public_send("#{key}=", value)
71
+ end
72
+ }
51
73
  end
52
74
 
75
+ def assign_backoff_intervals(backoff_setup)
76
+ @backoff_intervals = Backoff.new(backoff_setup[:type], backoff_setup[:start]).intervals
77
+ end
53
78
  end
54
- end
79
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Take2
2
- VERSION = "0.0.4"
3
- end
4
+ VERSION = "0.0.5"
5
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rspec'
2
4
  require 'take2'
3
- require 'pry'
5
+ require 'pry'
@@ -1,124 +1,114 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
2
 
3
- RSpec.describe Take2::Configuration do
3
+ require 'spec_helper'
4
4
 
5
+ RSpec.describe(Take2::Configuration) do
5
6
  describe 'default configurations' do
6
-
7
7
  let(:default) { described_class.new }
8
8
 
9
9
  it 'has correct default value for retries' do
10
- expect(default.retries).to eql 3
10
+ expect(default.retries).to(eql(3))
11
11
  end
12
12
 
13
13
  it 'has correct default retriable errors array' do
14
- expect(default.retriable).to eql [
14
+ expect(default.retriable).to(eql([
15
15
  Net::HTTPServerException,
16
- Net::HTTPRetriableError,
16
+ Net::HTTPRetriableError,
17
17
  Errno::ECONNRESET,
18
18
  IOError,
19
- ].freeze
19
+ ].freeze))
20
20
  end
21
21
 
22
22
  it 'has default proc for retry_proc' do
23
23
  p = proc {}
24
- expect(default.retry_proc.call).to eql p.call
24
+ expect(default.retry_proc.call).to(eql(p.call))
25
25
  end
26
26
 
27
27
  it 'has default proc for retry_condition_proc' do
28
- p = proc {false}
29
- expect(default.retry_condition_proc.call).to eql p.call
28
+ p = proc { false }
29
+ expect(default.retry_condition_proc.call).to(eql(p.call))
30
30
  end
31
31
 
32
32
  it 'has correct default value for time_to_sleep' do
33
- expect(default.time_to_sleep).to eql 3
33
+ expect(default.time_to_sleep).to(eql(0))
34
34
  end
35
35
 
36
+ it 'has correct default value for backoff_intervals' do
37
+ expect(default.backoff_intervals).to eql Array.new(10, 3)
38
+ end
36
39
  end
37
40
 
38
41
  describe 'overwriting the default configurations' do
39
-
40
42
  context 'with valid hash' do
41
-
42
- let!(:new_configs_hash) {
43
+ let!(:new_configs_hash) do
43
44
  {
44
45
  retries: 2,
45
46
  retriable: [Net::HTTPRetriableError],
46
47
  retry_condition_proc: proc { true },
47
- retry_proc: proc { 2*2 },
48
- time_to_sleep: 0
48
+ retry_proc: proc { 2 * 2 },
49
+ time_to_sleep: 0,
50
+ backoff_setup: { type: :linear, start: 3 }
49
51
  }
50
- }
52
+ end
51
53
 
52
54
  let!(:new_configuration) { described_class.new(new_configs_hash).to_hash }
53
55
 
54
56
  [:retries, :retriable, :retry_proc, :retry_condition_proc, :time_to_sleep].each do |key|
55
57
  it "sets the #{key} key" do
56
58
  if new_configs_hash[key].respond_to?(:call)
57
- expect(new_configuration[key].call).to eql new_configs_hash[key].call
58
- else
59
- expect(new_configuration[key]).to eql new_configs_hash[key]
59
+ expect(new_configuration[key].call).to(eql(new_configs_hash[key].call))
60
+ else
61
+ expect(new_configuration[key]).to(eql(new_configs_hash[key]))
60
62
  end
61
63
  end
62
64
  end
63
65
 
66
+ it 'sets the backoff_intervals correctly' do
67
+ expect(new_configuration[:backoff_intervals])
68
+ .to eql(Take2::Backoff.new(
69
+ new_configs_hash[:backoff_setup][:type],
70
+ new_configs_hash[:backoff_setup][:start]
71
+ ).intervals)
72
+ end
64
73
  end
65
74
 
66
75
  context 'with invalid hash' do
67
-
68
76
  context 'when retries set to invalid value' do
69
-
70
77
  it 'raises ArgumentError' do
71
-
72
- expect { described_class.new(retries: -1) }.to raise_error ArgumentError
73
- expect { described_class.new(retries: 0) }.to raise_error ArgumentError
74
-
78
+ expect { described_class.new(retries: -1) }.to(raise_error(ArgumentError))
79
+ expect { described_class.new(retries: 0) }.to(raise_error(ArgumentError))
75
80
  end
76
-
77
81
  end
78
82
 
79
83
  context 'when time_to_sleep set to invalid value' do
80
-
81
84
  it 'raises ArgumentError' do
82
-
83
- expect { described_class.new(time_to_sleep: -1) }.to raise_error ArgumentError
84
-
85
+ expect { described_class.new(time_to_sleep: -1) }.to(raise_error(ArgumentError))
85
86
  end
86
-
87
87
  end
88
88
 
89
89
  context 'when retriable set to invalid value' do
90
-
91
90
  it 'raises ArgumentError' do
92
-
93
- expect { described_class.new(retriable: StandardError) }.to raise_error ArgumentError
94
-
91
+ expect { described_class.new(retriable: StandardError) }.to(raise_error(ArgumentError))
95
92
  end
96
-
97
93
  end
98
94
 
99
95
  context 'when retry_proc set to invalid value' do
100
-
101
96
  it 'raises ArgumentError' do
102
-
103
- expect { described_class.new(retry_proc: {}) }.to raise_error ArgumentError
104
-
97
+ expect { described_class.new(retry_proc: {}) }.to(raise_error(ArgumentError))
105
98
  end
106
-
107
99
  end
108
100
 
109
101
  context 'when retry_condition_proc set to invalid value' do
110
-
111
102
  it 'raises ArgumentError' do
112
-
113
- expect { described_class.new(retry_condition_proc: {}) }.to raise_error ArgumentError
114
-
103
+ expect { described_class.new(retry_condition_proc: {}) }.to(raise_error(ArgumentError))
115
104
  end
105
+ end
116
106
 
107
+ context 'when backoff_setup has incorrect type' do
108
+ it 'raises ArgumentError' do
109
+ expect { described_class.new(backoff_setup: { type: :log }) }.to(raise_error(ArgumentError))
110
+ end
117
111
  end
118
-
119
112
  end
120
-
121
113
  end
122
-
123
-
124
- end
114
+ end