retriable 3.8.0 → 4.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.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +20 -7
- data/.hound.yml +1 -1
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +18 -0
- data/Gemfile +2 -1
- data/README.md +88 -48
- data/lib/retriable/config.rb +3 -57
- data/lib/retriable/core_ext/kernel.rb +4 -4
- data/lib/retriable/validation.rb +4 -7
- data/lib/retriable/version.rb +1 -1
- data/lib/retriable.rb +36 -24
- data/retriable.gemspec +2 -7
- data/spec/config_spec.rb +17 -97
- data/spec/retriable_spec.rb +287 -93
- data/spec/spec_helper.rb +0 -13
- metadata +6 -52
data/lib/retriable.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "timeout"
|
|
4
3
|
require_relative "retriable/config"
|
|
5
4
|
require_relative "retriable/exponential_backoff"
|
|
6
5
|
require_relative "retriable/version"
|
|
@@ -41,7 +40,7 @@ module Retriable
|
|
|
41
40
|
end
|
|
42
41
|
end
|
|
43
42
|
|
|
44
|
-
def with_context(context_key, options = {}, &
|
|
43
|
+
def with_context(context_key, options = {}, &)
|
|
45
44
|
contexts = available_contexts
|
|
46
45
|
|
|
47
46
|
if !contexts.key?(context_key)
|
|
@@ -51,10 +50,10 @@ module Retriable
|
|
|
51
50
|
|
|
52
51
|
return unless block_given?
|
|
53
52
|
|
|
54
|
-
retriable(context_options_for(context_key, options), &
|
|
53
|
+
retriable(context_options_for(context_key, options), &)
|
|
55
54
|
end
|
|
56
55
|
|
|
57
|
-
def retriable(opts = {}, &
|
|
56
|
+
def retriable(opts = {}, &)
|
|
58
57
|
override_config = current_override
|
|
59
58
|
local_config = if opts.empty? && !override_config
|
|
60
59
|
config
|
|
@@ -66,10 +65,10 @@ module Retriable
|
|
|
66
65
|
local_config.validate!
|
|
67
66
|
|
|
68
67
|
plan = retry_plan(local_config)
|
|
69
|
-
timeout = local_config.timeout
|
|
70
68
|
on = local_config.on
|
|
71
69
|
retry_if = local_config.retry_if
|
|
72
70
|
on_retry = local_config.on_retry
|
|
71
|
+
on_give_up = local_config.on_give_up
|
|
73
72
|
sleep_disabled = local_config.sleep_disabled
|
|
74
73
|
max_elapsed_time = local_config.max_elapsed_time
|
|
75
74
|
|
|
@@ -79,22 +78,22 @@ module Retriable
|
|
|
79
78
|
elapsed_time = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time }
|
|
80
79
|
|
|
81
80
|
execute_tries(
|
|
82
|
-
max_tries: plan.max_tries, interval_for: plan.interval_for,
|
|
81
|
+
max_tries: plan.max_tries, interval_for: plan.interval_for,
|
|
83
82
|
exception_list: exception_list, on: on, retry_if: retry_if, on_retry: on_retry,
|
|
84
|
-
elapsed_time: elapsed_time, max_elapsed_time: max_elapsed_time,
|
|
85
|
-
sleep_disabled: sleep_disabled, &
|
|
83
|
+
on_give_up: on_give_up, elapsed_time: elapsed_time, max_elapsed_time: max_elapsed_time,
|
|
84
|
+
sleep_disabled: sleep_disabled, &
|
|
86
85
|
)
|
|
87
86
|
end
|
|
88
87
|
|
|
89
88
|
def execute_tries( # rubocop:disable Metrics/ParameterLists
|
|
90
|
-
max_tries:, interval_for:,
|
|
91
|
-
on:, retry_if:, on_retry:, elapsed_time:, max_elapsed_time:, sleep_disabled
|
|
89
|
+
max_tries:, interval_for:, exception_list:,
|
|
90
|
+
on:, retry_if:, on_retry:, on_give_up:, elapsed_time:, max_elapsed_time:, sleep_disabled:
|
|
92
91
|
)
|
|
93
92
|
try = 0
|
|
94
93
|
loop do
|
|
95
94
|
try += 1
|
|
96
95
|
begin
|
|
97
|
-
return
|
|
96
|
+
return yield(try)
|
|
98
97
|
rescue *exception_list => e
|
|
99
98
|
raise unless retriable_exception?(e, on, exception_list, retry_if)
|
|
100
99
|
|
|
@@ -102,7 +101,13 @@ module Retriable
|
|
|
102
101
|
call_on_retry(on_retry, e, try, elapsed_time.call, interval)
|
|
103
102
|
|
|
104
103
|
elapsed_interval = sleep_disabled == true ? 0 : interval
|
|
105
|
-
|
|
104
|
+
# Snapshot elapsed_time once so the stop check and on_give_up see the same value.
|
|
105
|
+
current_elapsed_time = elapsed_time.call
|
|
106
|
+
stop_reason = retry_stop_reason(try, max_tries, current_elapsed_time, elapsed_interval, max_elapsed_time)
|
|
107
|
+
if stop_reason
|
|
108
|
+
call_on_give_up(on_give_up, e, try, current_elapsed_time, interval, stop_reason)
|
|
109
|
+
raise
|
|
110
|
+
end
|
|
106
111
|
|
|
107
112
|
sleep interval if sleep_disabled != true
|
|
108
113
|
end
|
|
@@ -132,23 +137,30 @@ module Retriable
|
|
|
132
137
|
).interval_provider
|
|
133
138
|
end
|
|
134
139
|
|
|
135
|
-
def call_with_timeout(timeout, try)
|
|
136
|
-
return Timeout.timeout(timeout) { yield(try) } if timeout
|
|
137
|
-
|
|
138
|
-
yield(try)
|
|
139
|
-
end
|
|
140
|
-
|
|
141
140
|
def call_on_retry(on_retry, exception, try, elapsed_time, interval)
|
|
142
141
|
return unless on_retry
|
|
143
142
|
|
|
144
143
|
on_retry.call(exception, try, elapsed_time, interval)
|
|
145
144
|
end
|
|
146
145
|
|
|
147
|
-
def
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
def call_on_give_up( # rubocop:disable Metrics/ParameterLists
|
|
147
|
+
on_give_up, exception, try, elapsed_time, interval, reason
|
|
148
|
+
)
|
|
149
|
+
return unless on_give_up
|
|
150
|
+
|
|
151
|
+
on_give_up.call(exception, try, elapsed_time, interval, reason)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# `:tries_exhausted` is checked first, but the two conditions can't both hold
|
|
155
|
+
# on the same try in practice: `retry_plan` returns a nil interval whenever
|
|
156
|
+
# `try >= max_tries`, so `(elapsed_time + interval) > max_elapsed_time` is not
|
|
157
|
+
# evaluable on the exhausted-tries try. The early return guards against that
|
|
158
|
+
# nil and also pins precedence in case the plan ever changes.
|
|
159
|
+
def retry_stop_reason(try, max_tries, elapsed_time, interval, max_elapsed_time)
|
|
160
|
+
return :tries_exhausted if max_tries && try >= max_tries
|
|
161
|
+
return nil if max_elapsed_time.nil?
|
|
150
162
|
|
|
151
|
-
(elapsed_time + interval)
|
|
163
|
+
:max_elapsed_time if (elapsed_time + interval) > max_elapsed_time
|
|
152
164
|
end
|
|
153
165
|
|
|
154
166
|
# When `on` is a Hash, we need to verify the exception matches a pattern.
|
|
@@ -244,9 +256,9 @@ module Retriable
|
|
|
244
256
|
:execute_tries,
|
|
245
257
|
:retry_plan,
|
|
246
258
|
:interval_provider,
|
|
247
|
-
:call_with_timeout,
|
|
248
259
|
:call_on_retry,
|
|
249
|
-
:
|
|
260
|
+
:call_on_give_up,
|
|
261
|
+
:retry_stop_reason,
|
|
250
262
|
:retriable_exception?,
|
|
251
263
|
:hash_exception_match?,
|
|
252
264
|
:apply_override_options,
|
data/retriable.gemspec
CHANGED
|
@@ -15,15 +15,10 @@ Gem::Specification.new do |spec|
|
|
|
15
15
|
"APIs/services or file system calls."
|
|
16
16
|
spec.homepage = "https://github.com/kamui/retriable"
|
|
17
17
|
spec.license = "MIT"
|
|
18
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
18
19
|
|
|
19
20
|
spec.files = `git ls-files -z`.split("\x0")
|
|
20
|
-
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
21
21
|
spec.require_paths = ["lib"]
|
|
22
22
|
|
|
23
|
-
spec.required_ruby_version = ">=
|
|
24
|
-
|
|
25
|
-
spec.add_development_dependency "bundler"
|
|
26
|
-
spec.add_development_dependency "rspec", "~> 3"
|
|
27
|
-
|
|
28
|
-
spec.add_development_dependency "listen", "~> 3.1"
|
|
23
|
+
spec.required_ruby_version = ">= 3.2"
|
|
29
24
|
end
|
data/spec/config_spec.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "stringio"
|
|
4
|
-
|
|
5
3
|
describe Retriable::Config do
|
|
6
4
|
let(:default_config) { described_class.new }
|
|
7
5
|
|
|
@@ -34,10 +32,6 @@ describe Retriable::Config do
|
|
|
34
32
|
expect(default_config.intervals).to be_nil
|
|
35
33
|
end
|
|
36
34
|
|
|
37
|
-
it "timeout defaults to nil" do
|
|
38
|
-
expect(default_config.timeout).to be_nil
|
|
39
|
-
end
|
|
40
|
-
|
|
41
35
|
it "on defaults to [StandardError]" do
|
|
42
36
|
expect(default_config.on).to eq([StandardError])
|
|
43
37
|
end
|
|
@@ -50,6 +44,10 @@ describe Retriable::Config do
|
|
|
50
44
|
expect(default_config.on_retry).to be_nil
|
|
51
45
|
end
|
|
52
46
|
|
|
47
|
+
it "on_give_up handler defaults to nil" do
|
|
48
|
+
expect(default_config.on_give_up).to be_nil
|
|
49
|
+
end
|
|
50
|
+
|
|
53
51
|
it "contexts defaults to {}" do
|
|
54
52
|
expect(default_config.contexts).to eq({})
|
|
55
53
|
end
|
|
@@ -59,99 +57,12 @@ describe Retriable::Config do
|
|
|
59
57
|
expect { described_class.new(does_not_exist: 123) }.to raise_error(ArgumentError, /not a valid option/)
|
|
60
58
|
end
|
|
61
59
|
|
|
62
|
-
it "
|
|
63
|
-
expect { described_class.new(
|
|
64
|
-
expect do
|
|
65
|
-
expect { described_class.new(timeout: -1) }.to raise_error(ArgumentError, /timeout/)
|
|
66
|
-
end.to output(/timeout.*deprecated.*Retriable 4\.0/i).to_stderr
|
|
60
|
+
it "rejects timeout as an unknown option" do
|
|
61
|
+
expect { described_class.new(timeout: 5) }.to raise_error(ArgumentError, /not a valid option/)
|
|
67
62
|
end
|
|
68
63
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
expect do
|
|
72
|
-
described_class.new(timeout: 5)
|
|
73
|
-
end.to output(/timeout.*deprecated.*Retriable 4\.0/i).to_stderr
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
it "warns when timeout is set before validation" do
|
|
77
|
-
config = described_class.new
|
|
78
|
-
config.timeout = 5
|
|
79
|
-
|
|
80
|
-
expect do
|
|
81
|
-
config.validate!
|
|
82
|
-
end.to output(/timeout.*deprecated.*Retriable 4\.0/i).to_stderr
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
it "does not warn when timeout is nil" do
|
|
86
|
-
expect do
|
|
87
|
-
described_class.new(timeout: nil)
|
|
88
|
-
end.not_to output.to_stderr
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
it "does not warn when timeout is omitted" do
|
|
92
|
-
expect do
|
|
93
|
-
described_class.new
|
|
94
|
-
end.not_to output.to_stderr
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
it "warns at most once per process" do
|
|
98
|
-
original_stderr = $stderr
|
|
99
|
-
stderr = StringIO.new
|
|
100
|
-
begin
|
|
101
|
-
$stderr = stderr
|
|
102
|
-
|
|
103
|
-
described_class.new(timeout: 5)
|
|
104
|
-
described_class.new(timeout: 5)
|
|
105
|
-
|
|
106
|
-
config = described_class.new
|
|
107
|
-
config.timeout = 5
|
|
108
|
-
config.validate!
|
|
109
|
-
ensure
|
|
110
|
-
$stderr = original_stderr
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
expect(stderr.string.scan("timeout:` option is deprecated").size).to eq(1)
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
it "emits the warning under the :deprecated category when supported", if: WARN_CATEGORY_SUPPORTED do
|
|
117
|
-
captured = []
|
|
118
|
-
allow(Warning).to receive(:warn) do |message, category: nil|
|
|
119
|
-
captured << [message, category]
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
described_class.new(timeout: 5)
|
|
123
|
-
|
|
124
|
-
expect(captured.size).to eq(1)
|
|
125
|
-
message, category = captured.first
|
|
126
|
-
expect(message).to match(/timeout.*deprecated.*Retriable 4\.0/i)
|
|
127
|
-
expect(category).to eq(:deprecated)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
it "is silenced by Warning[:deprecated] = false", if: WARN_CATEGORY_SUPPORTED do
|
|
131
|
-
original = Warning[:deprecated]
|
|
132
|
-
begin
|
|
133
|
-
Warning[:deprecated] = false
|
|
134
|
-
expect do
|
|
135
|
-
described_class.new(timeout: 5)
|
|
136
|
-
end.not_to output.to_stderr
|
|
137
|
-
ensure
|
|
138
|
-
Warning[:deprecated] = original
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
it "remains armed when silenced via Warning[:deprecated]", if: WARN_CATEGORY_SUPPORTED do
|
|
143
|
-
original = Warning[:deprecated]
|
|
144
|
-
begin
|
|
145
|
-
Warning[:deprecated] = false
|
|
146
|
-
described_class.new(timeout: 5)
|
|
147
|
-
ensure
|
|
148
|
-
Warning[:deprecated] = original
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
expect do
|
|
152
|
-
described_class.new(timeout: 5)
|
|
153
|
-
end.to output(/timeout.*deprecated.*Retriable 4\.0/i).to_stderr
|
|
154
|
-
end
|
|
64
|
+
it "raises errors on invalid timing configuration" do
|
|
65
|
+
expect { described_class.new(rand_factor: 1.1) }.to raise_error(ArgumentError, /rand_factor/)
|
|
155
66
|
end
|
|
156
67
|
|
|
157
68
|
it "raises errors when intervals is not an array" do
|
|
@@ -191,6 +102,15 @@ describe Retriable::Config do
|
|
|
191
102
|
expect { described_class.new(on: [StandardError, RuntimeError]) }.not_to raise_error
|
|
192
103
|
end
|
|
193
104
|
|
|
105
|
+
it "accepts a Set of Exception subclasses" do
|
|
106
|
+
expect { described_class.new(on: Set[StandardError, RuntimeError]) }.not_to raise_error
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "rejects a Set containing a non-Exception class" do
|
|
110
|
+
expect { described_class.new(on: Set[StandardError, Kernel]) }
|
|
111
|
+
.to raise_error(ArgumentError, /on must be an Exception class/)
|
|
112
|
+
end
|
|
113
|
+
|
|
194
114
|
it "accepts a hash with nil pattern values" do
|
|
195
115
|
expect { described_class.new(on: { StandardError => nil }) }.not_to raise_error
|
|
196
116
|
end
|