retriable 3.1.2 → 3.4.1
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 +4 -4
- data/.github/dependabot.yml +8 -0
- data/.github/workflows/main.yml +49 -0
- data/.gitignore +2 -0
- data/.rspec +2 -1
- data/.rubocop.yml +11 -7
- data/CHANGELOG.md +104 -66
- data/CODE_OF_CONDUCT.md +10 -0
- data/Gemfile +4 -2
- data/README.md +77 -48
- data/Rakefile +12 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/lib/retriable/config.rb +14 -9
- data/lib/retriable/core_ext/kernel.rb +2 -0
- data/lib/retriable/exponential_backoff.rb +11 -8
- data/lib/retriable/version.rb +3 -1
- data/lib/retriable.rb +102 -38
- data/retriable.gemspec +9 -12
- data/sig/retriable.rbs +4 -0
- data/spec/config_spec.rb +6 -0
- data/spec/exponential_backoff_spec.rb +2 -0
- data/spec/retriable_spec.rb +155 -0
- data/spec/spec_helper.rb +3 -7
- data/spec/support/exceptions.rb +2 -0
- metadata +13 -11
- data/.travis.yml +0 -30
data/Rakefile
ADDED
data/bin/console
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "bundler/setup"
|
|
5
|
+
require "retriable"
|
|
6
|
+
|
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
9
|
+
|
|
10
|
+
require "irb"
|
|
11
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/lib/retriable/config.rb
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative "exponential_backoff"
|
|
2
4
|
|
|
3
5
|
module Retriable
|
|
4
6
|
class Config
|
|
5
|
-
ATTRIBUTES = ExponentialBackoff::ATTRIBUTES + [
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
ATTRIBUTES = (ExponentialBackoff::ATTRIBUTES + %i[
|
|
8
|
+
sleep_disabled
|
|
9
|
+
max_elapsed_time
|
|
10
|
+
intervals
|
|
11
|
+
timeout
|
|
12
|
+
on
|
|
13
|
+
retry_if
|
|
14
|
+
on_retry
|
|
15
|
+
contexts
|
|
16
|
+
]).freeze
|
|
14
17
|
|
|
15
18
|
attr_accessor(*ATTRIBUTES)
|
|
16
19
|
|
|
@@ -27,11 +30,13 @@ module Retriable
|
|
|
27
30
|
@intervals = nil
|
|
28
31
|
@timeout = nil
|
|
29
32
|
@on = [StandardError]
|
|
33
|
+
@retry_if = nil
|
|
30
34
|
@on_retry = nil
|
|
31
35
|
@contexts = {}
|
|
32
36
|
|
|
33
37
|
opts.each do |k, v|
|
|
34
38
|
raise ArgumentError, "#{k} is not a valid option" if !ATTRIBUTES.include?(k)
|
|
39
|
+
|
|
35
40
|
instance_variable_set(:"@#{k}", v)
|
|
36
41
|
end
|
|
37
42
|
end
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Retriable
|
|
2
4
|
class ExponentialBackoff
|
|
3
|
-
ATTRIBUTES = [
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
ATTRIBUTES = %i[
|
|
6
|
+
tries
|
|
7
|
+
base_interval
|
|
8
|
+
multiplier
|
|
9
|
+
max_interval
|
|
10
|
+
rand_factor
|
|
9
11
|
].freeze
|
|
10
12
|
|
|
11
13
|
attr_accessor(*ATTRIBUTES)
|
|
@@ -19,13 +21,14 @@ module Retriable
|
|
|
19
21
|
|
|
20
22
|
opts.each do |k, v|
|
|
21
23
|
raise ArgumentError, "#{k} is not a valid option" if !ATTRIBUTES.include?(k)
|
|
24
|
+
|
|
22
25
|
instance_variable_set(:"@#{k}", v)
|
|
23
26
|
end
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
def intervals
|
|
27
30
|
intervals = Array.new(tries) do |iteration|
|
|
28
|
-
[base_interval * multiplier**iteration, max_interval].min
|
|
31
|
+
[base_interval * (multiplier**iteration), max_interval].min
|
|
29
32
|
end
|
|
30
33
|
|
|
31
34
|
return intervals if rand_factor.zero?
|
|
@@ -36,7 +39,7 @@ module Retriable
|
|
|
36
39
|
private
|
|
37
40
|
|
|
38
41
|
def randomize(interval)
|
|
39
|
-
delta = rand_factor * interval
|
|
42
|
+
delta = rand_factor * interval.to_f
|
|
40
43
|
min = interval - delta
|
|
41
44
|
max = interval + delta
|
|
42
45
|
rand(min..max)
|
data/lib/retriable/version.rb
CHANGED
data/lib/retriable.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "timeout"
|
|
2
4
|
require_relative "retriable/config"
|
|
3
5
|
require_relative "retriable/exponential_backoff"
|
|
@@ -16,61 +18,123 @@ module Retriable
|
|
|
16
18
|
|
|
17
19
|
def with_context(context_key, options = {}, &block)
|
|
18
20
|
if !config.contexts.key?(context_key)
|
|
19
|
-
raise ArgumentError,
|
|
21
|
+
raise ArgumentError,
|
|
22
|
+
"#{context_key} not found in Retriable.config.contexts. Available contexts: #{config.contexts.keys}"
|
|
20
23
|
end
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
return unless block_given?
|
|
26
|
+
|
|
27
|
+
retriable(config.contexts[context_key].merge(options), &block)
|
|
23
28
|
end
|
|
24
29
|
|
|
25
|
-
def retriable(opts = {})
|
|
30
|
+
def retriable(opts = {}, &block)
|
|
26
31
|
local_config = opts.empty? ? config : Config.new(config.to_h.merge(opts))
|
|
27
32
|
|
|
28
|
-
tries
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
on = local_config.on
|
|
37
|
-
on_retry = local_config.on_retry
|
|
38
|
-
sleep_disabled = local_config.sleep_disabled
|
|
33
|
+
tries = local_config.tries
|
|
34
|
+
intervals = build_intervals(local_config, tries)
|
|
35
|
+
timeout = local_config.timeout
|
|
36
|
+
on = local_config.on
|
|
37
|
+
retry_if = local_config.retry_if
|
|
38
|
+
on_retry = local_config.on_retry
|
|
39
|
+
sleep_disabled = local_config.sleep_disabled
|
|
40
|
+
max_elapsed_time = local_config.max_elapsed_time
|
|
39
41
|
|
|
40
42
|
exception_list = on.is_a?(Hash) ? on.keys : on
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
else
|
|
47
|
-
intervals = ExponentialBackoff.new(
|
|
48
|
-
tries: tries - 1,
|
|
49
|
-
base_interval: base_interval,
|
|
50
|
-
multiplier: multiplier,
|
|
51
|
-
max_interval: max_interval,
|
|
52
|
-
rand_factor: rand_factor
|
|
53
|
-
).intervals
|
|
54
|
-
end
|
|
43
|
+
exception_list = [*exception_list]
|
|
44
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
45
|
+
elapsed_time = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time }
|
|
46
|
+
|
|
47
|
+
tries = intervals.size + 1
|
|
55
48
|
|
|
49
|
+
execute_tries(
|
|
50
|
+
tries: tries, intervals: intervals, timeout: timeout,
|
|
51
|
+
exception_list: exception_list, on: on, retry_if: retry_if, on_retry: on_retry,
|
|
52
|
+
elapsed_time: elapsed_time, max_elapsed_time: max_elapsed_time,
|
|
53
|
+
sleep_disabled: sleep_disabled, &block
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def execute_tries( # rubocop:disable Metrics/ParameterLists
|
|
58
|
+
tries:, intervals:, timeout:, exception_list:,
|
|
59
|
+
on:, retry_if:, on_retry:, elapsed_time:, max_elapsed_time:, sleep_disabled:, &block
|
|
60
|
+
)
|
|
56
61
|
tries.times do |index|
|
|
57
62
|
try = index + 1
|
|
58
63
|
|
|
59
64
|
begin
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if on.is_a?(Hash)
|
|
64
|
-
raise unless exception_list.any? do |e|
|
|
65
|
-
exception.is_a?(e) && ([*on[e]].empty? || [*on[e]].any? { |pattern| exception.message =~ pattern })
|
|
66
|
-
end
|
|
67
|
-
end
|
|
65
|
+
return call_with_timeout(timeout, try, &block)
|
|
66
|
+
rescue *exception_list => e
|
|
67
|
+
raise unless retriable_exception?(e, on, exception_list, retry_if)
|
|
68
68
|
|
|
69
69
|
interval = intervals[index]
|
|
70
|
-
on_retry
|
|
71
|
-
|
|
70
|
+
call_on_retry(on_retry, e, try, elapsed_time.call, interval)
|
|
71
|
+
|
|
72
|
+
raise unless can_retry?(try, tries, elapsed_time.call, interval, max_elapsed_time)
|
|
73
|
+
|
|
72
74
|
sleep interval if sleep_disabled != true
|
|
73
75
|
end
|
|
74
76
|
end
|
|
75
77
|
end
|
|
78
|
+
|
|
79
|
+
def build_intervals(local_config, tries)
|
|
80
|
+
return local_config.intervals if local_config.intervals
|
|
81
|
+
|
|
82
|
+
ExponentialBackoff.new(
|
|
83
|
+
tries: tries - 1,
|
|
84
|
+
base_interval: local_config.base_interval,
|
|
85
|
+
multiplier: local_config.multiplier,
|
|
86
|
+
max_interval: local_config.max_interval,
|
|
87
|
+
rand_factor: local_config.rand_factor,
|
|
88
|
+
).intervals
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def call_with_timeout(timeout, try)
|
|
92
|
+
return Timeout.timeout(timeout) { yield(try) } if timeout
|
|
93
|
+
|
|
94
|
+
yield(try)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def call_on_retry(on_retry, exception, try, elapsed_time, interval)
|
|
98
|
+
return unless on_retry
|
|
99
|
+
|
|
100
|
+
on_retry.call(exception, try, elapsed_time, interval)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def can_retry?(try, tries, elapsed_time, interval, max_elapsed_time)
|
|
104
|
+
return false unless try < tries
|
|
105
|
+
return true if max_elapsed_time.nil?
|
|
106
|
+
|
|
107
|
+
(elapsed_time + interval) <= max_elapsed_time
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# When `on` is a Hash, we need to verify the exception matches a pattern.
|
|
111
|
+
# For any non-Hash `on` value (e.g., Array of classes, single Exception class,
|
|
112
|
+
# or Module), the `rescue *exception_list` clause already guarantees the
|
|
113
|
+
# exception is retriable with respect to `on`; `retry_if`, if provided, is an
|
|
114
|
+
# additional gate that can still cause this method to return false.
|
|
115
|
+
def retriable_exception?(exception, on, exception_list, retry_if)
|
|
116
|
+
return false if on.is_a?(Hash) && !hash_exception_match?(exception, on, exception_list)
|
|
117
|
+
return false if retry_if && !retry_if.call(exception)
|
|
118
|
+
|
|
119
|
+
true
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def hash_exception_match?(exception, on, exception_list)
|
|
123
|
+
exception_list.any? do |error_class|
|
|
124
|
+
next false unless exception.is_a?(error_class)
|
|
125
|
+
|
|
126
|
+
patterns = [*on[error_class]]
|
|
127
|
+
patterns.empty? || patterns.any? { |pattern| exception.message =~ pattern }
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private_class_method(
|
|
132
|
+
:execute_tries,
|
|
133
|
+
:build_intervals,
|
|
134
|
+
:call_with_timeout,
|
|
135
|
+
:call_on_retry,
|
|
136
|
+
:can_retry?,
|
|
137
|
+
:retriable_exception?,
|
|
138
|
+
:hash_exception_match?,
|
|
139
|
+
)
|
|
76
140
|
end
|
data/retriable.gemspec
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
5
|
require "retriable/version"
|
|
5
6
|
|
|
@@ -9,24 +10,20 @@ Gem::Specification.new do |spec|
|
|
|
9
10
|
spec.authors = ["Jack Chu"]
|
|
10
11
|
spec.email = ["jack@jackchu.com"]
|
|
11
12
|
spec.summary = "Retriable is a simple DSL to retry failed code blocks with randomized exponential backoff"
|
|
12
|
-
spec.description = "Retriable is a simple DSL to retry failed code blocks with randomized
|
|
13
|
-
|
|
13
|
+
spec.description = "Retriable is a simple DSL to retry failed code blocks with randomized " \
|
|
14
|
+
"exponential backoff. This is especially useful when interacting with external " \
|
|
15
|
+
"APIs/services or file system calls."
|
|
16
|
+
spec.homepage = "https://github.com/kamui/retriable"
|
|
14
17
|
spec.license = "MIT"
|
|
15
18
|
|
|
16
19
|
spec.files = `git ls-files -z`.split("\x0")
|
|
17
|
-
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
18
20
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
19
21
|
spec.require_paths = ["lib"]
|
|
20
22
|
|
|
21
|
-
spec.required_ruby_version = ">= 2.
|
|
23
|
+
spec.required_ruby_version = ">= 2.3.0"
|
|
22
24
|
|
|
23
25
|
spec.add_development_dependency "bundler"
|
|
24
26
|
spec.add_development_dependency "rspec", "~> 3"
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
spec.add_development_dependency "ruby_dep", "~> 1.3.1"
|
|
28
|
-
spec.add_development_dependency "listen", "~> 3.0.8"
|
|
29
|
-
else
|
|
30
|
-
spec.add_development_dependency "listen", "~> 3.1"
|
|
31
|
-
end
|
|
28
|
+
spec.add_development_dependency "listen", "~> 3.1"
|
|
32
29
|
end
|
data/sig/retriable.rbs
ADDED
data/spec/config_spec.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
describe Retriable::Config do
|
|
2
4
|
let(:default_config) { described_class.new }
|
|
3
5
|
|
|
@@ -38,6 +40,10 @@ describe Retriable::Config do
|
|
|
38
40
|
expect(default_config.on).to eq([StandardError])
|
|
39
41
|
end
|
|
40
42
|
|
|
43
|
+
it "retry_if defaults to nil" do
|
|
44
|
+
expect(default_config.retry_if).to be_nil
|
|
45
|
+
end
|
|
46
|
+
|
|
41
47
|
it "on_retry handler defaults to nil" do
|
|
42
48
|
expect(default_config.on_retry).to be_nil
|
|
43
49
|
end
|
data/spec/retriable_spec.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
describe Retriable do
|
|
2
4
|
let(:time_table_handler) do
|
|
3
5
|
->(_exception, try, _elapsed_time, next_interval) { @next_interval_table[try] = next_interval }
|
|
@@ -94,6 +96,28 @@ describe Retriable do
|
|
|
94
96
|
expect(@tries).to eq(10)
|
|
95
97
|
end
|
|
96
98
|
|
|
99
|
+
it "does not call on_retry when explicitly set to false" do
|
|
100
|
+
callback_called = false
|
|
101
|
+
original_on_retry = described_class.config.on_retry
|
|
102
|
+
|
|
103
|
+
begin
|
|
104
|
+
described_class.configure do |c|
|
|
105
|
+
c.on_retry = proc { |_exception, _try, _elapsed_time, _next_interval| callback_called = true }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
expect do
|
|
109
|
+
described_class.retriable(on_retry: false, tries: 3) { increment_tries_with_exception }
|
|
110
|
+
end.to raise_error(StandardError)
|
|
111
|
+
|
|
112
|
+
expect(@tries).to eq(3)
|
|
113
|
+
expect(callback_called).to be(false)
|
|
114
|
+
ensure
|
|
115
|
+
described_class.configure do |c|
|
|
116
|
+
c.on_retry = original_on_retry
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
97
121
|
context "with rand_factor 0.0 and an on_retry handler" do
|
|
98
122
|
let(:tries) { 6 }
|
|
99
123
|
let(:no_rand_timetable) { { 1 => 0.5, 2 => 0.75, 3 => 1.125 } }
|
|
@@ -130,6 +154,34 @@ describe Retriable do
|
|
|
130
154
|
expect(@next_interval_table).to eq(interval_hash)
|
|
131
155
|
expect(@tries).to eq(intervals.size + 1)
|
|
132
156
|
end
|
|
157
|
+
|
|
158
|
+
it "intervals option overrides tries, base_interval, max_interval, rand_factor, and multiplier" do
|
|
159
|
+
# Even though we specify tries: 10, base_interval: 1.0, max_interval: 100.0,
|
|
160
|
+
# rand_factor: 0.8, and multiplier: 2.0, the explicit intervals should take precedence
|
|
161
|
+
custom_intervals = [0.1, 0.2, 0.3]
|
|
162
|
+
|
|
163
|
+
expect do
|
|
164
|
+
described_class.retriable(
|
|
165
|
+
intervals: custom_intervals,
|
|
166
|
+
tries: 10,
|
|
167
|
+
base_interval: 1.0,
|
|
168
|
+
max_interval: 100.0,
|
|
169
|
+
rand_factor: 0.8,
|
|
170
|
+
multiplier: 2.0,
|
|
171
|
+
on_retry: time_table_handler,
|
|
172
|
+
) do
|
|
173
|
+
increment_tries_with_exception
|
|
174
|
+
end
|
|
175
|
+
end.to raise_error(StandardError)
|
|
176
|
+
|
|
177
|
+
# Should have 4 tries (3 intervals + 1), not 10
|
|
178
|
+
expect(@tries).to eq(4)
|
|
179
|
+
# Should use the exact intervals provided, not generate them
|
|
180
|
+
expect(@next_interval_table[1]).to eq(0.1)
|
|
181
|
+
expect(@next_interval_table[2]).to eq(0.2)
|
|
182
|
+
expect(@next_interval_table[3]).to eq(0.3)
|
|
183
|
+
expect(@next_interval_table[4]).to be_nil
|
|
184
|
+
end
|
|
133
185
|
end
|
|
134
186
|
|
|
135
187
|
context "with an array :on parameter" do
|
|
@@ -207,6 +259,50 @@ describe Retriable do
|
|
|
207
259
|
end
|
|
208
260
|
end
|
|
209
261
|
|
|
262
|
+
context "with a :retry_if parameter" do
|
|
263
|
+
it "retries only when retry_if returns true" do
|
|
264
|
+
described_class.retriable(tries: 3, retry_if: ->(_exception) { @tries < 3 }) do
|
|
265
|
+
increment_tries
|
|
266
|
+
raise StandardError, "StandardError occurred" if @tries < 3
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
expect(@tries).to eq(3)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
it "does not retry when retry_if returns false" do
|
|
273
|
+
expect do
|
|
274
|
+
described_class.retriable(tries: 3, retry_if: ->(_exception) { false }) do
|
|
275
|
+
increment_tries_with_exception
|
|
276
|
+
end
|
|
277
|
+
end.to raise_error(StandardError)
|
|
278
|
+
|
|
279
|
+
expect(@tries).to eq(1)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
it "can retry based on the wrapped exception cause" do
|
|
283
|
+
root_cause_class = Class.new(StandardError)
|
|
284
|
+
wrapper_class = Class.new(StandardError)
|
|
285
|
+
|
|
286
|
+
described_class.retriable(
|
|
287
|
+
on: [wrapper_class],
|
|
288
|
+
tries: 3,
|
|
289
|
+
retry_if: ->(exception) { exception.cause.is_a?(root_cause_class) },
|
|
290
|
+
) do
|
|
291
|
+
increment_tries
|
|
292
|
+
|
|
293
|
+
if @tries < 3
|
|
294
|
+
begin
|
|
295
|
+
raise root_cause_class, "root cause"
|
|
296
|
+
rescue root_cause_class
|
|
297
|
+
raise wrapper_class, "wrapped"
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
expect(@tries).to eq(3)
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
210
306
|
it "runs for a max elapsed time of 2 seconds" do
|
|
211
307
|
described_class.configure { |c| c.sleep_disabled = false }
|
|
212
308
|
|
|
@@ -219,12 +315,55 @@ describe Retriable do
|
|
|
219
315
|
expect(@tries).to eq(2)
|
|
220
316
|
end
|
|
221
317
|
|
|
318
|
+
it "retries up to tries limit when max_elapsed_time is nil" do
|
|
319
|
+
expect do
|
|
320
|
+
described_class.retriable(tries: 4, max_elapsed_time: nil) { increment_tries_with_exception }
|
|
321
|
+
end.to raise_error(StandardError)
|
|
322
|
+
|
|
323
|
+
expect(@tries).to eq(4)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
it "uses monotonic clock for elapsed time tracking" do
|
|
327
|
+
# Stub Process.clock_gettime to return controlled values so we can
|
|
328
|
+
# verify elapsed_time passed to on_retry is derived from the monotonic clock.
|
|
329
|
+
clock_calls = 0
|
|
330
|
+
allow(Process).to receive(:clock_gettime).with(Process::CLOCK_MONOTONIC) do
|
|
331
|
+
value = clock_calls.to_f
|
|
332
|
+
clock_calls += 1
|
|
333
|
+
value
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
elapsed_times = []
|
|
337
|
+
on_retry = ->(_exception, _try, elapsed_time, _next_interval) { elapsed_times << elapsed_time }
|
|
338
|
+
|
|
339
|
+
expect do
|
|
340
|
+
described_class.retriable(tries: 3, on_retry: on_retry) { increment_tries_with_exception }
|
|
341
|
+
end.to raise_error(StandardError)
|
|
342
|
+
|
|
343
|
+
# start_time (call 0) + at least one elapsed_time computation per retry
|
|
344
|
+
expect(clock_calls).to be >= 3
|
|
345
|
+
# elapsed_time values should be positive and non-decreasing
|
|
346
|
+
expect(elapsed_times).to all(be > 0)
|
|
347
|
+
expect(elapsed_times).to eq(elapsed_times.sort)
|
|
348
|
+
end
|
|
349
|
+
|
|
222
350
|
it "raises ArgumentError on invalid options" do
|
|
223
351
|
expect { described_class.retriable(does_not_exist: 123) { increment_tries } }.to raise_error(ArgumentError)
|
|
224
352
|
end
|
|
225
353
|
end
|
|
226
354
|
|
|
227
355
|
context "#configure" do
|
|
356
|
+
it "exposes only the intended public API" do
|
|
357
|
+
public_api_methods = %i[
|
|
358
|
+
retriable
|
|
359
|
+
with_context
|
|
360
|
+
configure
|
|
361
|
+
config
|
|
362
|
+
]
|
|
363
|
+
|
|
364
|
+
expect(described_class.singleton_methods(false)).to match_array(public_api_methods)
|
|
365
|
+
end
|
|
366
|
+
|
|
228
367
|
it "raises NoMethodError on invalid configuration" do
|
|
229
368
|
expect { described_class.configure { |c| c.does_not_exist = 123 } }.to raise_error(NoMethodError)
|
|
230
369
|
end
|
|
@@ -245,6 +384,22 @@ describe Retriable do
|
|
|
245
384
|
expect(@tries).to eq(1)
|
|
246
385
|
end
|
|
247
386
|
|
|
387
|
+
it "returns nil when called without a block" do
|
|
388
|
+
expect(described_class.with_context(:sql)).to be_nil
|
|
389
|
+
expect(@tries).to eq(0)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
it "passes try count through to the context block" do
|
|
393
|
+
seen_tries = []
|
|
394
|
+
|
|
395
|
+
described_class.with_context(:api) do |try|
|
|
396
|
+
seen_tries << try
|
|
397
|
+
raise StandardError if try < 3
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
expect(seen_tries).to eq([1, 2, 3])
|
|
401
|
+
end
|
|
402
|
+
|
|
248
403
|
it "respects the context options" do
|
|
249
404
|
expect { described_class.with_context(:api) { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
250
405
|
expect(@tries).to eq(api_tries)
|
data/spec/spec_helper.rb
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
require "simplecov"
|
|
3
|
-
|
|
4
|
-
CodeClimate::TestReporter.configure do |config|
|
|
5
|
-
config.logger.level = Logger::WARN
|
|
6
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
7
2
|
|
|
3
|
+
require "simplecov"
|
|
8
4
|
SimpleCov.start
|
|
9
5
|
|
|
10
6
|
require "pry"
|
|
11
7
|
require_relative "../lib/retriable"
|
|
12
|
-
require_relative "support/exceptions
|
|
8
|
+
require_relative "support/exceptions"
|
|
13
9
|
|
|
14
10
|
RSpec.configure do |config|
|
|
15
11
|
config.before(:each) do
|
data/spec/support/exceptions.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: retriable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.1
|
|
4
|
+
version: 3.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jack Chu
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: bundler
|
|
@@ -53,7 +52,7 @@ dependencies:
|
|
|
53
52
|
- !ruby/object:Gem::Version
|
|
54
53
|
version: '3.1'
|
|
55
54
|
description: Retriable is a simple DSL to retry failed code blocks with randomized
|
|
56
|
-
exponential backoff. This is especially useful when interacting external
|
|
55
|
+
exponential backoff. This is especially useful when interacting with external APIs/services
|
|
57
56
|
or file system calls.
|
|
58
57
|
email:
|
|
59
58
|
- jack@jackchu.com
|
|
@@ -61,31 +60,36 @@ executables: []
|
|
|
61
60
|
extensions: []
|
|
62
61
|
extra_rdoc_files: []
|
|
63
62
|
files:
|
|
63
|
+
- ".github/dependabot.yml"
|
|
64
|
+
- ".github/workflows/main.yml"
|
|
64
65
|
- ".gitignore"
|
|
65
66
|
- ".hound.yml"
|
|
66
67
|
- ".rspec"
|
|
67
68
|
- ".rubocop.yml"
|
|
68
|
-
- ".travis.yml"
|
|
69
69
|
- CHANGELOG.md
|
|
70
|
+
- CODE_OF_CONDUCT.md
|
|
70
71
|
- Gemfile
|
|
71
72
|
- LICENSE
|
|
72
73
|
- README.md
|
|
74
|
+
- Rakefile
|
|
75
|
+
- bin/console
|
|
76
|
+
- bin/setup
|
|
73
77
|
- lib/retriable.rb
|
|
74
78
|
- lib/retriable/config.rb
|
|
75
79
|
- lib/retriable/core_ext/kernel.rb
|
|
76
80
|
- lib/retriable/exponential_backoff.rb
|
|
77
81
|
- lib/retriable/version.rb
|
|
78
82
|
- retriable.gemspec
|
|
83
|
+
- sig/retriable.rbs
|
|
79
84
|
- spec/config_spec.rb
|
|
80
85
|
- spec/exponential_backoff_spec.rb
|
|
81
86
|
- spec/retriable_spec.rb
|
|
82
87
|
- spec/spec_helper.rb
|
|
83
88
|
- spec/support/exceptions.rb
|
|
84
|
-
homepage:
|
|
89
|
+
homepage: https://github.com/kamui/retriable
|
|
85
90
|
licenses:
|
|
86
91
|
- MIT
|
|
87
92
|
metadata: {}
|
|
88
|
-
post_install_message:
|
|
89
93
|
rdoc_options: []
|
|
90
94
|
require_paths:
|
|
91
95
|
- lib
|
|
@@ -93,16 +97,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
93
97
|
requirements:
|
|
94
98
|
- - ">="
|
|
95
99
|
- !ruby/object:Gem::Version
|
|
96
|
-
version: 2.
|
|
100
|
+
version: 2.3.0
|
|
97
101
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
98
102
|
requirements:
|
|
99
103
|
- - ">="
|
|
100
104
|
- !ruby/object:Gem::Version
|
|
101
105
|
version: '0'
|
|
102
106
|
requirements: []
|
|
103
|
-
|
|
104
|
-
rubygems_version: 2.7.6
|
|
105
|
-
signing_key:
|
|
107
|
+
rubygems_version: 4.0.3
|
|
106
108
|
specification_version: 4
|
|
107
109
|
summary: Retriable is a simple DSL to retry failed code blocks with randomized exponential
|
|
108
110
|
backoff
|