retriable 3.5.1 → 3.6.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/CHANGELOG.md +10 -0
- data/README.md +82 -80
- data/docs/superpowers/specs/2026-05-26-on-give-up-callback-followups-design.md +116 -0
- data/docs/testing.md +212 -0
- data/lib/retriable/config.rb +1 -0
- data/lib/retriable/validation.rb +41 -0
- data/lib/retriable/version.rb +1 -1
- data/lib/retriable.rb +39 -14
- data/spec/config_spec.rb +66 -0
- data/spec/retriable_spec.rb +285 -79
- metadata +3 -1
data/lib/retriable.rb
CHANGED
|
@@ -6,6 +6,13 @@ require_relative "retriable/exponential_backoff"
|
|
|
6
6
|
require_relative "retriable/version"
|
|
7
7
|
|
|
8
8
|
module Retriable
|
|
9
|
+
# Thread-local storage key for the active #with_override block.
|
|
10
|
+
# We deliberately use Thread#thread_variable_set/get (true thread-local)
|
|
11
|
+
# rather than Thread.current[] (fiber-local) so that fibers within a thread
|
|
12
|
+
# share the same override. Changing this to Thread.current[] would silently
|
|
13
|
+
# break callers that use fiber-based concurrency.
|
|
14
|
+
OVERRIDE_THREAD_KEY = :retriable_override
|
|
15
|
+
|
|
9
16
|
module_function
|
|
10
17
|
|
|
11
18
|
def configure
|
|
@@ -16,15 +23,19 @@ module Retriable
|
|
|
16
23
|
@config ||= Config.new
|
|
17
24
|
end
|
|
18
25
|
|
|
19
|
-
def
|
|
20
|
-
raise ArgumentError, "empty override options are not allowed
|
|
26
|
+
def with_override(opts = {})
|
|
27
|
+
raise ArgumentError, "empty override options are not allowed" if opts.empty?
|
|
28
|
+
raise ArgumentError, "with_override requires a block" unless block_given?
|
|
21
29
|
|
|
22
30
|
validate_override_options(opts)
|
|
23
|
-
@override_config = opts
|
|
24
|
-
end
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
previous = Thread.current.thread_variable_get(OVERRIDE_THREAD_KEY)
|
|
33
|
+
Thread.current.thread_variable_set(OVERRIDE_THREAD_KEY, opts)
|
|
34
|
+
begin
|
|
35
|
+
yield
|
|
36
|
+
ensure
|
|
37
|
+
Thread.current.thread_variable_set(OVERRIDE_THREAD_KEY, previous)
|
|
38
|
+
end
|
|
28
39
|
end
|
|
29
40
|
|
|
30
41
|
def with_context(context_key, options = {}, &block)
|
|
@@ -41,10 +52,11 @@ module Retriable
|
|
|
41
52
|
end
|
|
42
53
|
|
|
43
54
|
def retriable(opts = {}, &block)
|
|
44
|
-
|
|
55
|
+
override_config = current_override
|
|
56
|
+
local_config = if opts.empty? && !override_config
|
|
45
57
|
config
|
|
46
58
|
else
|
|
47
|
-
Config.new(apply_override_options(config.to_h.merge(opts),
|
|
59
|
+
Config.new(apply_override_options(config.to_h.merge(opts), override_config))
|
|
48
60
|
end
|
|
49
61
|
|
|
50
62
|
# Config is mutable through `configure`, so validate again immediately before use.
|
|
@@ -154,16 +166,23 @@ module Retriable
|
|
|
154
166
|
raise ArgumentError, "#{k} is not a valid option" unless Config::ATTRIBUTES.include?(k)
|
|
155
167
|
end
|
|
156
168
|
|
|
169
|
+
return unless opts.key?(:contexts)
|
|
170
|
+
|
|
157
171
|
contexts = opts[:contexts]
|
|
158
|
-
return
|
|
172
|
+
return if contexts.nil?
|
|
173
|
+
|
|
174
|
+
raise ArgumentError, "contexts must be a Hash or nil, got #{contexts.inspect}" unless contexts.is_a?(Hash)
|
|
159
175
|
|
|
160
|
-
contexts.
|
|
161
|
-
validate_context_override_options(context_options)
|
|
176
|
+
contexts.each do |context_key, context_options|
|
|
177
|
+
validate_context_override_options(context_key, context_options)
|
|
162
178
|
end
|
|
163
179
|
end
|
|
164
180
|
|
|
165
|
-
def validate_context_override_options(context_options)
|
|
166
|
-
|
|
181
|
+
def validate_context_override_options(context_key, context_options)
|
|
182
|
+
unless context_options.is_a?(Hash)
|
|
183
|
+
raise ArgumentError,
|
|
184
|
+
"contexts[#{context_key.inspect}] must be a Hash, got #{context_options.inspect}"
|
|
185
|
+
end
|
|
167
186
|
|
|
168
187
|
context_attributes = Config::ATTRIBUTES - [:contexts]
|
|
169
188
|
context_options.each_key do |k|
|
|
@@ -199,10 +218,15 @@ module Retriable
|
|
|
199
218
|
end
|
|
200
219
|
|
|
201
220
|
def override_contexts
|
|
202
|
-
|
|
221
|
+
override_config = current_override
|
|
222
|
+
contexts = override_config && override_config[:contexts]
|
|
203
223
|
contexts.is_a?(Hash) ? contexts : {}
|
|
204
224
|
end
|
|
205
225
|
|
|
226
|
+
def current_override
|
|
227
|
+
Thread.current.thread_variable_get(OVERRIDE_THREAD_KEY)
|
|
228
|
+
end
|
|
229
|
+
|
|
206
230
|
private_class_method(
|
|
207
231
|
:validate_override_options,
|
|
208
232
|
:validate_context_override_options,
|
|
@@ -218,5 +242,6 @@ module Retriable
|
|
|
218
242
|
:context_options_for,
|
|
219
243
|
:config_contexts,
|
|
220
244
|
:override_contexts,
|
|
245
|
+
:current_override,
|
|
221
246
|
)
|
|
222
247
|
end
|
data/spec/config_spec.rb
CHANGED
|
@@ -65,4 +65,70 @@ describe Retriable::Config do
|
|
|
65
65
|
it "raises errors when intervals is not an array" do
|
|
66
66
|
expect { described_class.new(intervals: "1") }.to raise_error(ArgumentError, /intervals must be an Array/)
|
|
67
67
|
end
|
|
68
|
+
|
|
69
|
+
context "on: option validation" do
|
|
70
|
+
it "accepts a single Exception subclass" do
|
|
71
|
+
expect { described_class.new(on: StandardError) }.not_to raise_error
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "accepts Exception itself" do
|
|
75
|
+
expect { described_class.new(on: Exception) }.not_to raise_error
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "accepts an array of Exception subclasses" do
|
|
79
|
+
expect { described_class.new(on: [StandardError, RuntimeError]) }.not_to raise_error
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "accepts a hash with nil pattern values" do
|
|
83
|
+
expect { described_class.new(on: { StandardError => nil }) }.not_to raise_error
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "accepts a hash with Regexp pattern values" do
|
|
87
|
+
expect { described_class.new(on: { StandardError => /boom/ }) }.not_to raise_error
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "accepts a hash with Array-of-Regexp pattern values" do
|
|
91
|
+
expect { described_class.new(on: { StandardError => [/a/, /b/] }) }.not_to raise_error
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "rejects Object as on:" do
|
|
95
|
+
expect { described_class.new(on: Object) }
|
|
96
|
+
.to raise_error(ArgumentError, /on must be an Exception class/)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "rejects Kernel as on:" do
|
|
100
|
+
expect { described_class.new(on: Kernel) }
|
|
101
|
+
.to raise_error(ArgumentError, /on must be an Exception class/)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "rejects an array containing a non-Exception class" do
|
|
105
|
+
expect { described_class.new(on: [StandardError, Kernel]) }
|
|
106
|
+
.to raise_error(ArgumentError, /on must be an Exception class/)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "rejects a hash key that is not an Exception class" do
|
|
110
|
+
expect { described_class.new(on: { Kernel => nil }) }
|
|
111
|
+
.to raise_error(ArgumentError, /on must be an Exception class/)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "rejects a hash value that is a String" do
|
|
115
|
+
expect { described_class.new(on: { StandardError => "boom" }) }
|
|
116
|
+
.to raise_error(ArgumentError, /on\[StandardError\] must be nil, a Regexp, or an Array of Regexps/)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "rejects a hash value that is an Array containing a non-Regexp" do
|
|
120
|
+
expect { described_class.new(on: { StandardError => [/a/, "b"] }) }
|
|
121
|
+
.to raise_error(ArgumentError, /on\[StandardError\] must be nil, a Regexp, or an Array of Regexps/)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it "rejects a string passed as on:" do
|
|
125
|
+
expect { described_class.new(on: "StandardError") }
|
|
126
|
+
.to raise_error(ArgumentError, /on must be an Exception class/)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "validates on: even when intervals is provided" do
|
|
130
|
+
expect { described_class.new(intervals: [0.1], on: Object) }
|
|
131
|
+
.to raise_error(ArgumentError, /on must be an Exception class/)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
68
134
|
end
|