retriable 3.8.0 → 4.2.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 +38 -9
- data/.hound.yml +1 -1
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +89 -0
- data/Gemfile +4 -1
- data/README.md +105 -50
- data/lib/retriable/config.rb +25 -57
- data/lib/retriable/core_ext/kernel.rb +6 -4
- data/lib/retriable/exponential_backoff.rb +13 -5
- data/lib/retriable/validation.rb +11 -7
- data/lib/retriable/version.rb +1 -1
- data/lib/retriable.rb +55 -31
- data/retriable.gemspec +2 -7
- data/sig/retriable.rbs +29 -1
- data/spec/config_spec.rb +63 -97
- data/spec/retriable_spec.rb +345 -95
- data/spec/spec_helper.rb +3 -14
- metadata +7 -53
|
@@ -14,14 +14,22 @@ module Retriable
|
|
|
14
14
|
rand_factor
|
|
15
15
|
].freeze
|
|
16
16
|
|
|
17
|
+
DEFAULTS = {
|
|
18
|
+
tries: 3,
|
|
19
|
+
base_interval: 0.5,
|
|
20
|
+
max_interval: 60,
|
|
21
|
+
rand_factor: 0.5,
|
|
22
|
+
multiplier: 1.5
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
17
25
|
attr_accessor(*ATTRIBUTES)
|
|
18
26
|
|
|
19
27
|
def initialize(opts = {})
|
|
20
|
-
@tries =
|
|
21
|
-
@base_interval =
|
|
22
|
-
@max_interval =
|
|
23
|
-
@rand_factor =
|
|
24
|
-
@multiplier =
|
|
28
|
+
@tries = DEFAULTS[:tries]
|
|
29
|
+
@base_interval = DEFAULTS[:base_interval]
|
|
30
|
+
@max_interval = DEFAULTS[:max_interval]
|
|
31
|
+
@rand_factor = DEFAULTS[:rand_factor]
|
|
32
|
+
@multiplier = DEFAULTS[:multiplier]
|
|
25
33
|
|
|
26
34
|
opts.each do |k, v|
|
|
27
35
|
raise ArgumentError, "#{k} is not a valid option" if !ATTRIBUTES.include?(k)
|
data/lib/retriable/validation.rb
CHANGED
|
@@ -28,6 +28,13 @@ module Retriable
|
|
|
28
28
|
validate_non_negative_number(name, value)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
def validate_callable(name, value)
|
|
32
|
+
return unless value # nil/false disable the callback
|
|
33
|
+
return if value.respond_to?(:call)
|
|
34
|
+
|
|
35
|
+
raise ArgumentError, "#{name} must respond to #call or be nil"
|
|
36
|
+
end
|
|
37
|
+
|
|
31
38
|
def validate_rand_factor
|
|
32
39
|
return if finite_number?(rand_factor) && rand_factor >= 0 && rand_factor <= 1
|
|
33
40
|
|
|
@@ -46,7 +53,7 @@ module Retriable
|
|
|
46
53
|
|
|
47
54
|
# Validates an `on:` value. Acceptable shapes:
|
|
48
55
|
# - a Class that descends from Exception
|
|
49
|
-
# - an Array whose elements are Classes that descend from Exception
|
|
56
|
+
# - an Array or Set whose elements are Classes that descend from Exception
|
|
50
57
|
# - a Hash whose keys are such Classes and whose values are nil,
|
|
51
58
|
# a Regexp, or an Array of Regexps
|
|
52
59
|
#
|
|
@@ -58,12 +65,12 @@ module Retriable
|
|
|
58
65
|
# Regexp values explicitly.
|
|
59
66
|
def validate_on(value)
|
|
60
67
|
case value
|
|
61
|
-
|
|
68
|
+
in Hash
|
|
62
69
|
value.each do |klass, pattern|
|
|
63
70
|
validate_on_class(klass)
|
|
64
71
|
validate_on_hash_value(klass, pattern)
|
|
65
72
|
end
|
|
66
|
-
|
|
73
|
+
in Array | Set
|
|
67
74
|
value.each { |klass| validate_on_class(klass) }
|
|
68
75
|
else
|
|
69
76
|
validate_on_class(value)
|
|
@@ -79,10 +86,7 @@ module Retriable
|
|
|
79
86
|
def validate_on_hash_value(klass, pattern)
|
|
80
87
|
return if pattern.nil?
|
|
81
88
|
return if pattern.is_a?(Regexp)
|
|
82
|
-
|
|
83
|
-
# rubocop:disable Style/PredicateWithKind
|
|
84
|
-
return if pattern.is_a?(Array) && pattern.all? { |p| p.is_a?(Regexp) }
|
|
85
|
-
# rubocop:enable Style/PredicateWithKind
|
|
89
|
+
return if pattern.is_a?(Array) && pattern.all?(Regexp)
|
|
86
90
|
|
|
87
91
|
raise ArgumentError,
|
|
88
92
|
"on[#{klass}] must be nil, a Regexp, or an Array of Regexps, got #{pattern.inspect}"
|
data/lib/retriable/version.rb
CHANGED
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,9 @@ module Retriable
|
|
|
41
40
|
end
|
|
42
41
|
end
|
|
43
42
|
|
|
44
|
-
def with_context(context_key, options = {}, &
|
|
43
|
+
def with_context(context_key, options = {}, &)
|
|
44
|
+
raise ArgumentError, "with_context requires a block" unless block_given?
|
|
45
|
+
|
|
45
46
|
contexts = available_contexts
|
|
46
47
|
|
|
47
48
|
if !contexts.key?(context_key)
|
|
@@ -49,27 +50,25 @@ module Retriable
|
|
|
49
50
|
"#{context_key} not found in Retriable contexts (including overrides). Available contexts: #{contexts.keys}"
|
|
50
51
|
end
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
retriable(context_options_for(context_key, options), &block)
|
|
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
|
|
61
60
|
else
|
|
62
|
-
Config.new(apply_override_options(config.to_h
|
|
61
|
+
Config.new(apply_override_options(merge_layer(config.to_h, opts), override_config))
|
|
63
62
|
end
|
|
64
63
|
|
|
65
64
|
# Config is mutable through `configure`, so validate again immediately before use.
|
|
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,30 +78,39 @@ 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
|
|
|
100
|
+
# On the final attempt `interval_for` returns nil (no next retry), and
|
|
101
|
+
# `on_retry` intentionally fires before the give-up check below, so it
|
|
102
|
+
# receives `interval: nil`. See the on_retry/on_give_up README contract.
|
|
101
103
|
interval = interval_for.call(try - 1)
|
|
102
104
|
call_on_retry(on_retry, e, try, elapsed_time.call, interval)
|
|
103
105
|
|
|
104
106
|
elapsed_interval = sleep_disabled == true ? 0 : interval
|
|
105
|
-
|
|
107
|
+
# Snapshot elapsed_time once so the stop check and on_give_up see the same value.
|
|
108
|
+
current_elapsed_time = elapsed_time.call
|
|
109
|
+
stop_reason = retry_stop_reason(try, max_tries, current_elapsed_time, elapsed_interval, max_elapsed_time)
|
|
110
|
+
if stop_reason
|
|
111
|
+
call_on_give_up(on_give_up, e, try, current_elapsed_time, interval, stop_reason)
|
|
112
|
+
raise
|
|
113
|
+
end
|
|
106
114
|
|
|
107
115
|
sleep interval if sleep_disabled != true
|
|
108
116
|
end
|
|
@@ -132,23 +140,30 @@ module Retriable
|
|
|
132
140
|
).interval_provider
|
|
133
141
|
end
|
|
134
142
|
|
|
135
|
-
def call_with_timeout(timeout, try)
|
|
136
|
-
return Timeout.timeout(timeout) { yield(try) } if timeout
|
|
137
|
-
|
|
138
|
-
yield(try)
|
|
139
|
-
end
|
|
140
|
-
|
|
141
143
|
def call_on_retry(on_retry, exception, try, elapsed_time, interval)
|
|
142
144
|
return unless on_retry
|
|
143
145
|
|
|
144
146
|
on_retry.call(exception, try, elapsed_time, interval)
|
|
145
147
|
end
|
|
146
148
|
|
|
147
|
-
def
|
|
148
|
-
|
|
149
|
-
|
|
149
|
+
def call_on_give_up( # rubocop:disable Metrics/ParameterLists
|
|
150
|
+
on_give_up, exception, try, elapsed_time, interval, reason
|
|
151
|
+
)
|
|
152
|
+
return unless on_give_up
|
|
150
153
|
|
|
151
|
-
(elapsed_time
|
|
154
|
+
on_give_up.call(exception, try, elapsed_time, interval, reason)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# `:tries_exhausted` is checked first, but the two conditions can't both hold
|
|
158
|
+
# on the same try in practice: `retry_plan` returns a nil interval whenever
|
|
159
|
+
# `try >= max_tries`, so `(elapsed_time + interval) > max_elapsed_time` is not
|
|
160
|
+
# evaluable on the exhausted-tries try. The early return guards against that
|
|
161
|
+
# nil and also pins precedence in case the plan ever changes.
|
|
162
|
+
def retry_stop_reason(try, max_tries, elapsed_time, interval, max_elapsed_time)
|
|
163
|
+
return :tries_exhausted if max_tries && try >= max_tries
|
|
164
|
+
return nil if max_elapsed_time.nil?
|
|
165
|
+
|
|
166
|
+
:max_elapsed_time if (elapsed_time + interval) > max_elapsed_time
|
|
152
167
|
end
|
|
153
168
|
|
|
154
169
|
# When `on` is a Hash, we need to verify the exception matches a pattern.
|
|
@@ -204,9 +219,17 @@ module Retriable
|
|
|
204
219
|
def apply_override_options(options, overrides)
|
|
205
220
|
return options unless overrides
|
|
206
221
|
|
|
207
|
-
options
|
|
208
|
-
|
|
209
|
-
|
|
222
|
+
merge_layer(options, overrides)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Merge a higher-precedence option layer onto a base layer. A higher layer
|
|
226
|
+
# that sets `tries` without `intervals` clears the base layer's inherited
|
|
227
|
+
# `intervals`, so a caller's `tries:` is never silently ignored. When the
|
|
228
|
+
# higher layer supplies its own `intervals`, those win (same-call override).
|
|
229
|
+
def merge_layer(base, higher)
|
|
230
|
+
merged = base.merge(higher)
|
|
231
|
+
merged[:intervals] = nil if higher.key?(:tries) && !higher.key?(:intervals)
|
|
232
|
+
merged
|
|
210
233
|
end
|
|
211
234
|
|
|
212
235
|
def available_contexts
|
|
@@ -216,7 +239,7 @@ module Retriable
|
|
|
216
239
|
def context_options_for(context_key, options)
|
|
217
240
|
context_options = config_contexts.fetch(context_key, {})
|
|
218
241
|
context_options = {} unless context_options.is_a?(Hash)
|
|
219
|
-
context_options = context_options
|
|
242
|
+
context_options = merge_layer(context_options, options)
|
|
220
243
|
|
|
221
244
|
override_context_options = override_contexts[context_key]
|
|
222
245
|
return context_options unless override_context_options.is_a?(Hash)
|
|
@@ -244,12 +267,13 @@ module Retriable
|
|
|
244
267
|
:execute_tries,
|
|
245
268
|
:retry_plan,
|
|
246
269
|
:interval_provider,
|
|
247
|
-
:call_with_timeout,
|
|
248
270
|
:call_on_retry,
|
|
249
|
-
:
|
|
271
|
+
:call_on_give_up,
|
|
272
|
+
:retry_stop_reason,
|
|
250
273
|
:retriable_exception?,
|
|
251
274
|
:hash_exception_match?,
|
|
252
275
|
:apply_override_options,
|
|
276
|
+
:merge_layer,
|
|
253
277
|
:available_contexts,
|
|
254
278
|
:context_options_for,
|
|
255
279
|
:config_contexts,
|
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/sig/retriable.rbs
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
module Retriable
|
|
2
2
|
VERSION: String
|
|
3
|
-
|
|
3
|
+
OVERRIDE_THREAD_KEY: Symbol
|
|
4
|
+
|
|
5
|
+
def self.configure: () { (Config) -> void } -> void
|
|
6
|
+
def self.config: () -> Config
|
|
7
|
+
def self.with_override: (Hash[Symbol, untyped] options) { () -> untyped } -> untyped
|
|
8
|
+
def self.with_context: (Symbol context_key, ?Hash[Symbol, untyped] options) { (Integer) -> untyped } -> untyped
|
|
9
|
+
def self.retriable: (?Hash[Symbol, untyped] options) { (Integer) -> untyped } -> untyped
|
|
10
|
+
|
|
11
|
+
class Config
|
|
12
|
+
ATTRIBUTES: Array[Symbol]
|
|
13
|
+
|
|
14
|
+
attr_accessor tries: Numeric
|
|
15
|
+
attr_accessor base_interval: Numeric
|
|
16
|
+
attr_accessor max_interval: Numeric
|
|
17
|
+
attr_accessor rand_factor: Numeric
|
|
18
|
+
attr_accessor multiplier: Numeric
|
|
19
|
+
attr_accessor sleep_disabled: bool
|
|
20
|
+
attr_accessor max_elapsed_time: Numeric?
|
|
21
|
+
attr_accessor intervals: Array[Numeric]?
|
|
22
|
+
attr_accessor on: untyped
|
|
23
|
+
attr_accessor retry_if: untyped
|
|
24
|
+
attr_accessor on_retry: untyped
|
|
25
|
+
attr_accessor on_give_up: untyped
|
|
26
|
+
attr_accessor contexts: Hash[Symbol, untyped]
|
|
27
|
+
|
|
28
|
+
def initialize: (?Hash[Symbol, untyped] opts) -> void
|
|
29
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
30
|
+
def validate!: () -> void
|
|
31
|
+
end
|
|
4
32
|
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
|
|
@@ -243,4 +163,50 @@ describe Retriable::Config do
|
|
|
243
163
|
.to raise_error(ArgumentError, /on must be an Exception class/)
|
|
244
164
|
end
|
|
245
165
|
end
|
|
166
|
+
|
|
167
|
+
context "callable option validation" do
|
|
168
|
+
%i[retry_if on_retry on_give_up].each do |opt|
|
|
169
|
+
it "accepts a callable for #{opt}" do
|
|
170
|
+
expect { described_class.new(opt => ->(*) {}) }.not_to raise_error
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it "accepts nil and false for #{opt}" do
|
|
174
|
+
expect { described_class.new(opt => nil) }.not_to raise_error
|
|
175
|
+
expect { described_class.new(opt => false) }.not_to raise_error
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it "rejects a non-callable truthy value for #{opt}" do
|
|
179
|
+
expect { described_class.new(opt => 5) }.to raise_error(ArgumentError, /#{opt}.*#call/)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
context "context structure validation" do
|
|
185
|
+
it "rejects a context whose options contain a nested :contexts key" do
|
|
186
|
+
expect { described_class.new(contexts: { api: { contexts: {} } }) }
|
|
187
|
+
.to raise_error(ArgumentError, /contexts is not a valid option/)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
it "rejects a context with an unknown option key" do
|
|
191
|
+
expect { described_class.new(contexts: { api: { does_not_exist: 1 } }) }
|
|
192
|
+
.to raise_error(ArgumentError, /does_not_exist is not a valid option/)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it "validates context structure even when intervals is provided" do
|
|
196
|
+
expect { described_class.new(intervals: [0.1], contexts: { api: { contexts: {} } }) }
|
|
197
|
+
.to raise_error(ArgumentError, /contexts is not a valid option/)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it "accepts a non-Hash context value (treated as empty options)" do
|
|
201
|
+
expect { described_class.new(contexts: { broken: nil }) }.not_to raise_error
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
it "accepts nil contexts" do
|
|
205
|
+
expect { described_class.new(contexts: nil) }.not_to raise_error
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
it "accepts a valid context" do
|
|
209
|
+
expect { described_class.new(contexts: { api: { tries: 3, base_interval: 1.0 } }) }.not_to raise_error
|
|
210
|
+
end
|
|
211
|
+
end
|
|
246
212
|
end
|