take2 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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