retriable 3.8.0 → 5.0.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/workflows/main.yml +38 -9
- data/.hound.yml +1 -1
- data/.rubocop.yml +4 -1
- data/AGENTS.md +15 -0
- data/CHANGELOG.md +159 -0
- data/Gemfile +6 -1
- data/README.md +131 -50
- data/benchmark/config_publication.rb +79 -0
- data/docs/adr/0001-copy-on-write-config-publication.md +124 -0
- data/docs/agents/domain.md +38 -0
- data/docs/agents/issue-tracker.md +45 -0
- data/docs/agents/triage-labels.md +17 -0
- data/docs/migration.md +84 -0
- data/lib/retriable/config.rb +123 -51
- 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 +155 -45
- data/retriable.gemspec +2 -7
- data/sig/retriable.rbs +29 -1
- data/spec/config_spec.rb +212 -97
- data/spec/retriable_spec.rb +666 -104
- data/spec/spec_helper.rb +3 -14
- metadata +14 -53
data/spec/retriable_spec.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "rbconfig"
|
|
4
|
-
require "stringio"
|
|
5
4
|
|
|
6
5
|
describe Retriable do
|
|
7
6
|
let(:time_table_handler) do
|
|
@@ -9,7 +8,10 @@ describe Retriable do
|
|
|
9
8
|
end
|
|
10
9
|
|
|
11
10
|
before(:each) do
|
|
12
|
-
|
|
11
|
+
# Reset to a pristine published snapshot, matching what load-time init
|
|
12
|
+
# publishes. Resetting to nil instead would force #config to carry a
|
|
13
|
+
# nil-guard that only the suite can reach.
|
|
14
|
+
described_class.instance_variable_set(:@config, Retriable::Config.new.freeze)
|
|
13
15
|
Thread.current.thread_variable_set(Retriable::OVERRIDE_THREAD_KEY, nil)
|
|
14
16
|
described_class.configure { |c| c.sleep_disabled = true }
|
|
15
17
|
@tries = 0
|
|
@@ -26,6 +28,31 @@ describe Retriable do
|
|
|
26
28
|
raise exception_class, "#{exception_class} occurred"
|
|
27
29
|
end
|
|
28
30
|
|
|
31
|
+
# Wall clock appears in the concurrency specs only as a deadlock backstop,
|
|
32
|
+
# never as a timing assertion. Every worker they start finishes in
|
|
33
|
+
# milliseconds, so this budget is approached only when a regression leaves a
|
|
34
|
+
# thread genuinely stuck.
|
|
35
|
+
let(:deadlock_backstop_seconds) { 10 }
|
|
36
|
+
|
|
37
|
+
# Joins each thread within the backstop, failing the example instead of hanging
|
|
38
|
+
# the suite when one never finishes. A bare Thread#join turns a serialization
|
|
39
|
+
# regression into a run that never terminates, which CI reports as a timeout
|
|
40
|
+
# with no failing example to point at.
|
|
41
|
+
#
|
|
42
|
+
# The kill matters for the examples that follow: Ruby releases a Mutex held by
|
|
43
|
+
# a killed thread, so one stuck worker cannot wedge every later `configure`.
|
|
44
|
+
# Kill is asynchronous, so wait briefly for it to land before failing —
|
|
45
|
+
# otherwise the mutex is still held when the next example starts.
|
|
46
|
+
def join_without_deadlock(*threads)
|
|
47
|
+
threads.flatten.compact.each do |thread|
|
|
48
|
+
next if thread.join(deadlock_backstop_seconds)
|
|
49
|
+
|
|
50
|
+
thread.kill
|
|
51
|
+
thread.join(1)
|
|
52
|
+
raise "a worker thread did not finish within #{deadlock_backstop_seconds}s; suspect a deadlock"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
29
56
|
context "global scope extension" do
|
|
30
57
|
it "cannot be called in the global scope without requiring the core_ext/kernel" do
|
|
31
58
|
script = "require 'retriable'; begin; retriable {}; rescue NoMethodError; exit 0; end; exit 1"
|
|
@@ -39,6 +66,49 @@ describe Retriable do
|
|
|
39
66
|
expect { retriable { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
40
67
|
expect(@tries).to eq(3)
|
|
41
68
|
end
|
|
69
|
+
|
|
70
|
+
it "passes on_give_up through the kernel extension" do
|
|
71
|
+
require_relative "../lib/retriable/core_ext/kernel"
|
|
72
|
+
received_reason = nil
|
|
73
|
+
handler = proc { |_e, _try, _elapsed, _interval, reason| received_reason = reason }
|
|
74
|
+
|
|
75
|
+
expect { retriable(tries: 1, on_give_up: handler) { increment_tries_with_exception } }
|
|
76
|
+
.to raise_error(StandardError)
|
|
77
|
+
|
|
78
|
+
expect(received_reason).to eq(:tries_exhausted)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# These two specs lock in the anonymous block forwarding (`&`) semantics
|
|
82
|
+
# across both delegation layers: Kernel#retriable_with_context ->
|
|
83
|
+
# Retriable.with_context. If the `&` is dropped at either layer, the block
|
|
84
|
+
# is not forwarded and the `block_given?` guard in with_context raises
|
|
85
|
+
# ArgumentError instead of running the block.
|
|
86
|
+
it "forwards a block through Kernel#retriable_with_context" do
|
|
87
|
+
require_relative "../lib/retriable/core_ext/kernel"
|
|
88
|
+
Retriable.configure { |c| c.contexts[:sql] = { tries: 1 } }
|
|
89
|
+
|
|
90
|
+
retriable_with_context(:sql) { increment_tries }
|
|
91
|
+
|
|
92
|
+
expect(@tries).to eq(1)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "raises an ArgumentError when Kernel#retriable_with_context is called without a block" do
|
|
96
|
+
require_relative "../lib/retriable/core_ext/kernel"
|
|
97
|
+
Retriable.configure { |c| c.contexts[:sql] = { tries: 1 } }
|
|
98
|
+
|
|
99
|
+
expect { retriable_with_context(:sql) }
|
|
100
|
+
.to raise_error(ArgumentError, /with_context requires a block/)
|
|
101
|
+
expect(@tries).to eq(0)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "is not callable with an explicit receiver" do
|
|
105
|
+
require_relative "../lib/retriable/core_ext/kernel"
|
|
106
|
+
|
|
107
|
+
expect { "foo".retriable { increment_tries } }
|
|
108
|
+
.to raise_error(NoMethodError, /private method/)
|
|
109
|
+
expect { "foo".retriable_with_context(:sql) { increment_tries } }
|
|
110
|
+
.to raise_error(NoMethodError, /private method/)
|
|
111
|
+
end
|
|
42
112
|
end
|
|
43
113
|
|
|
44
114
|
context "#retriable" do
|
|
@@ -51,9 +121,6 @@ describe Retriable do
|
|
|
51
121
|
|
|
52
122
|
it "raises a LocalJumpError if not given a block" do
|
|
53
123
|
expect { described_class.retriable }.to raise_error(LocalJumpError)
|
|
54
|
-
expect do
|
|
55
|
-
expect { described_class.retriable(timeout: 2) }.to raise_error(LocalJumpError)
|
|
56
|
-
end.to output(/timeout.*deprecated.*Retriable 4\.0/i).to_stderr
|
|
57
124
|
end
|
|
58
125
|
|
|
59
126
|
it "stops at first try if the block does not raise an exception" do
|
|
@@ -66,6 +133,19 @@ describe Retriable do
|
|
|
66
133
|
expect(@tries).to eq(3)
|
|
67
134
|
end
|
|
68
135
|
|
|
136
|
+
it "re-raises StopIteration after the configured tries" do
|
|
137
|
+
failure = StopIteration.new("end of input")
|
|
138
|
+
|
|
139
|
+
expect do
|
|
140
|
+
described_class.retriable(tries: 2) do
|
|
141
|
+
increment_tries
|
|
142
|
+
raise failure
|
|
143
|
+
end
|
|
144
|
+
end.to(raise_error { |error| expect(error).to equal(failure) })
|
|
145
|
+
|
|
146
|
+
expect(@tries).to eq(2)
|
|
147
|
+
end
|
|
148
|
+
|
|
69
149
|
it "makes only 1 try when exception raised is not descendent of StandardError" do
|
|
70
150
|
expect do
|
|
71
151
|
described_class.retriable { increment_tries_with_exception(NonStandardError) }
|
|
@@ -171,86 +251,8 @@ describe Retriable do
|
|
|
171
251
|
end.to raise_error(ArgumentError, /tries/)
|
|
172
252
|
end
|
|
173
253
|
|
|
174
|
-
it "
|
|
175
|
-
expect
|
|
176
|
-
expect { described_class.retriable(timeout: 1) { sleep(1.1) } }.to raise_error(Timeout::Error)
|
|
177
|
-
end.to output(/timeout.*deprecated.*Retriable 4\.0/i).to_stderr
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
context "timeout: deprecation" do
|
|
181
|
-
it "warns at most once per process across repeated retriable calls" do
|
|
182
|
-
original_stderr = $stderr
|
|
183
|
-
stderr = StringIO.new
|
|
184
|
-
begin
|
|
185
|
-
$stderr = stderr
|
|
186
|
-
|
|
187
|
-
described_class.retriable(timeout: 5) { :noop }
|
|
188
|
-
described_class.retriable(timeout: 5) { :noop }
|
|
189
|
-
described_class.retriable(timeout: 5) { :noop }
|
|
190
|
-
|
|
191
|
-
expect(stderr.string.scan("timeout:` option is deprecated").size).to eq(1)
|
|
192
|
-
ensure
|
|
193
|
-
$stderr = original_stderr
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
it "warns when timeout is passed to retriable" do
|
|
198
|
-
expect do
|
|
199
|
-
described_class.retriable(timeout: 5) { :noop }
|
|
200
|
-
end.to output(/timeout.*deprecated.*Retriable 4\.0/i).to_stderr
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
it "keeps applying timeout while deprecated" do
|
|
204
|
-
original_stderr = $stderr
|
|
205
|
-
begin
|
|
206
|
-
$stderr = StringIO.new
|
|
207
|
-
expect do
|
|
208
|
-
described_class.retriable(timeout: 0.05, tries: 1) { sleep(0.5) }
|
|
209
|
-
end.to raise_error(Timeout::Error)
|
|
210
|
-
ensure
|
|
211
|
-
$stderr = original_stderr
|
|
212
|
-
end
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
it "warns when timeout is supplied through with_override" do
|
|
216
|
-
expect do
|
|
217
|
-
described_class.with_override(timeout: 5) do
|
|
218
|
-
described_class.retriable { :noop }
|
|
219
|
-
end
|
|
220
|
-
end.to output(/timeout.*deprecated.*Retriable 4\.0/i).to_stderr
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
it "warns when timeout is supplied through configure" do
|
|
224
|
-
original_config = described_class.config
|
|
225
|
-
begin
|
|
226
|
-
expect do
|
|
227
|
-
described_class.configure { |config| config.timeout = 5 }
|
|
228
|
-
described_class.retriable { :noop }
|
|
229
|
-
end.to output(/timeout.*deprecated.*Retriable 4\.0/i).to_stderr
|
|
230
|
-
ensure
|
|
231
|
-
described_class.configure do |config|
|
|
232
|
-
original_config.to_h.each { |key, value| config.public_send("#{key}=", value) }
|
|
233
|
-
end
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
it "is silenced by Warning[:deprecated] = false", if: WARN_CATEGORY_SUPPORTED do
|
|
238
|
-
original = Warning[:deprecated]
|
|
239
|
-
begin
|
|
240
|
-
Warning[:deprecated] = false
|
|
241
|
-
expect do
|
|
242
|
-
described_class.retriable(timeout: 5) { :noop }
|
|
243
|
-
end.not_to output.to_stderr
|
|
244
|
-
ensure
|
|
245
|
-
Warning[:deprecated] = original
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
it "does not warn when timeout is absent" do
|
|
250
|
-
expect do
|
|
251
|
-
described_class.retriable { :noop }
|
|
252
|
-
end.not_to output.to_stderr
|
|
253
|
-
end
|
|
254
|
+
it "rejects timeout as an unknown option" do
|
|
255
|
+
expect { described_class.retriable(timeout: 1) { :noop } }.to raise_error(ArgumentError, /not a valid option/)
|
|
254
256
|
end
|
|
255
257
|
|
|
256
258
|
it "applies a randomized exponential backoff to each try" do
|
|
@@ -296,6 +298,187 @@ describe Retriable do
|
|
|
296
298
|
end
|
|
297
299
|
end
|
|
298
300
|
|
|
301
|
+
it "does not call on_retry when explicitly set to nil" do
|
|
302
|
+
callback_called = false
|
|
303
|
+
original_on_retry = described_class.config.on_retry
|
|
304
|
+
|
|
305
|
+
begin
|
|
306
|
+
described_class.configure do |c|
|
|
307
|
+
c.on_retry = proc { |_exception, _try, _elapsed_time, _next_interval| callback_called = true }
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
expect do
|
|
311
|
+
described_class.retriable(on_retry: nil, tries: 3) { increment_tries_with_exception }
|
|
312
|
+
end.to raise_error(StandardError)
|
|
313
|
+
|
|
314
|
+
expect(@tries).to eq(3)
|
|
315
|
+
expect(callback_called).to be(false)
|
|
316
|
+
ensure
|
|
317
|
+
described_class.configure do |c|
|
|
318
|
+
c.on_retry = original_on_retry
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
it "calls on_give_up with max elapsed time details before re-raising" do
|
|
324
|
+
described_class.configure { |c| c.sleep_disabled = false }
|
|
325
|
+
give_up_calls = []
|
|
326
|
+
on_give_up = proc do |exception, try, elapsed_time, next_interval, reason|
|
|
327
|
+
give_up_calls << [exception, try, elapsed_time, next_interval, reason]
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
expect do
|
|
331
|
+
described_class.retriable(
|
|
332
|
+
intervals: [1.0, 1.0],
|
|
333
|
+
max_elapsed_time: 0.5,
|
|
334
|
+
on_give_up: on_give_up,
|
|
335
|
+
) do
|
|
336
|
+
increment_tries_with_exception
|
|
337
|
+
end
|
|
338
|
+
end.to raise_error(StandardError)
|
|
339
|
+
|
|
340
|
+
exception, try, elapsed_time, next_interval, reason = give_up_calls.fetch(0)
|
|
341
|
+
expect(give_up_calls.size).to eq(1)
|
|
342
|
+
expect(exception).to be_a(StandardError)
|
|
343
|
+
expect(exception.message).to eq("StandardError occurred")
|
|
344
|
+
expect(try).to eq(1)
|
|
345
|
+
expect(elapsed_time).to be >= 0
|
|
346
|
+
expect(next_interval).to eq(1.0)
|
|
347
|
+
expect(reason).to eq(:max_elapsed_time)
|
|
348
|
+
expect(@tries).to eq(1)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
it "calls on_give_up with tries exhausted details before re-raising" do
|
|
352
|
+
give_up_calls = []
|
|
353
|
+
on_give_up = proc do |exception, try, elapsed_time, next_interval, reason|
|
|
354
|
+
give_up_calls << [exception, try, elapsed_time, next_interval, reason]
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
expect do
|
|
358
|
+
described_class.retriable(tries: 2, on_give_up: on_give_up) { increment_tries_with_exception }
|
|
359
|
+
end.to raise_error(StandardError)
|
|
360
|
+
|
|
361
|
+
exception, try, elapsed_time, next_interval, reason = give_up_calls.fetch(0)
|
|
362
|
+
expect(give_up_calls.size).to eq(1)
|
|
363
|
+
expect(exception).to be_a(StandardError)
|
|
364
|
+
expect(exception.message).to eq("StandardError occurred")
|
|
365
|
+
expect(try).to eq(2)
|
|
366
|
+
expect(elapsed_time).to be >= 0
|
|
367
|
+
expect(next_interval).to be_nil
|
|
368
|
+
expect(reason).to eq(:tries_exhausted)
|
|
369
|
+
expect(@tries).to eq(2)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
it "does not call on_give_up when the block eventually succeeds" do
|
|
373
|
+
callback_called = false
|
|
374
|
+
|
|
375
|
+
described_class.retriable(tries: 3, on_give_up: proc { callback_called = true }) do
|
|
376
|
+
increment_tries
|
|
377
|
+
raise StandardError if @tries < 2
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
expect(callback_called).to be(false)
|
|
381
|
+
expect(@tries).to eq(2)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
it "does not call on_give_up for non-retriable exception types" do
|
|
385
|
+
callback_called = false
|
|
386
|
+
|
|
387
|
+
expect do
|
|
388
|
+
described_class.retriable(on_give_up: proc { callback_called = true }) do
|
|
389
|
+
increment_tries_with_exception(NonStandardError)
|
|
390
|
+
end
|
|
391
|
+
end.to raise_error(NonStandardError)
|
|
392
|
+
|
|
393
|
+
expect(callback_called).to be(false)
|
|
394
|
+
expect(@tries).to eq(1)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
it "does not call on_give_up when retry_if rejects the exception" do
|
|
398
|
+
callback_called = false
|
|
399
|
+
|
|
400
|
+
expect do
|
|
401
|
+
described_class.retriable(
|
|
402
|
+
tries: 3,
|
|
403
|
+
retry_if: ->(_exception) { false },
|
|
404
|
+
on_give_up: proc { callback_called = true },
|
|
405
|
+
) do
|
|
406
|
+
increment_tries_with_exception
|
|
407
|
+
end
|
|
408
|
+
end.to raise_error(StandardError)
|
|
409
|
+
|
|
410
|
+
expect(callback_called).to be(false)
|
|
411
|
+
expect(@tries).to eq(1)
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
it "does not call on_give_up when explicitly set to false" do
|
|
415
|
+
callback_called = false
|
|
416
|
+
original_on_give_up = described_class.config.on_give_up
|
|
417
|
+
|
|
418
|
+
begin
|
|
419
|
+
described_class.configure do |c|
|
|
420
|
+
c.on_give_up = proc { callback_called = true }
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
expect do
|
|
424
|
+
described_class.retriable(on_give_up: false, tries: 1) { increment_tries_with_exception }
|
|
425
|
+
end.to raise_error(StandardError)
|
|
426
|
+
|
|
427
|
+
expect(callback_called).to be(false)
|
|
428
|
+
ensure
|
|
429
|
+
described_class.configure do |c|
|
|
430
|
+
c.on_give_up = original_on_give_up
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
it "does not call on_give_up when explicitly set to nil" do
|
|
436
|
+
callback_called = false
|
|
437
|
+
original_on_give_up = described_class.config.on_give_up
|
|
438
|
+
|
|
439
|
+
begin
|
|
440
|
+
described_class.configure do |c|
|
|
441
|
+
c.on_give_up = proc { callback_called = true }
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
expect do
|
|
445
|
+
described_class.retriable(on_give_up: nil, tries: 1) { increment_tries_with_exception }
|
|
446
|
+
end.to raise_error(StandardError)
|
|
447
|
+
|
|
448
|
+
expect(callback_called).to be(false)
|
|
449
|
+
ensure
|
|
450
|
+
described_class.configure do |c|
|
|
451
|
+
c.on_give_up = original_on_give_up
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
it "calls on_retry before on_give_up when giving up" do
|
|
457
|
+
events = []
|
|
458
|
+
|
|
459
|
+
expect do
|
|
460
|
+
described_class.retriable(
|
|
461
|
+
tries: 1,
|
|
462
|
+
on_retry: proc { events << :on_retry },
|
|
463
|
+
on_give_up: proc { events << :on_give_up },
|
|
464
|
+
) do
|
|
465
|
+
increment_tries_with_exception
|
|
466
|
+
end
|
|
467
|
+
end.to raise_error(StandardError)
|
|
468
|
+
|
|
469
|
+
expect(events).to eq(%i[on_retry on_give_up])
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
it "propagates exceptions raised inside on_give_up, replacing the original exception" do
|
|
473
|
+
handler = proc { raise "handler exploded" }
|
|
474
|
+
|
|
475
|
+
expect do
|
|
476
|
+
described_class.retriable(tries: 1, on_give_up: handler) { increment_tries_with_exception }
|
|
477
|
+
end.to raise_error(RuntimeError, "handler exploded")
|
|
478
|
+
|
|
479
|
+
expect(@tries).to eq(1)
|
|
480
|
+
end
|
|
481
|
+
|
|
299
482
|
context "with rand_factor 0.0 and an on_retry handler" do
|
|
300
483
|
let(:tries) { 6 }
|
|
301
484
|
let(:no_rand_timetable) { { 1 => 0.5, 2 => 0.75, 3 => 1.125 } }
|
|
@@ -375,6 +558,19 @@ describe Retriable do
|
|
|
375
558
|
end
|
|
376
559
|
end
|
|
377
560
|
|
|
561
|
+
context "with a Set :on parameter" do
|
|
562
|
+
it "retries each exception class in the Set" do
|
|
563
|
+
described_class.retriable(on: Set[StandardError, NonStandardError]) do
|
|
564
|
+
increment_tries
|
|
565
|
+
|
|
566
|
+
raise StandardError if @tries == 1
|
|
567
|
+
raise NonStandardError if @tries == 2
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
expect(@tries).to eq(3)
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
|
|
378
574
|
context "with a hash :on parameter" do
|
|
379
575
|
let(:on_hash) { { NonStandardError => /NonStandardError occurred/ } }
|
|
380
576
|
|
|
@@ -407,6 +603,20 @@ describe Retriable do
|
|
|
407
603
|
expect(@tries).to eq(1)
|
|
408
604
|
end
|
|
409
605
|
|
|
606
|
+
it "does not call on_give_up when exception class matches but message does not" do
|
|
607
|
+
callback_called = false
|
|
608
|
+
|
|
609
|
+
expect do
|
|
610
|
+
described_class.retriable(on: on_hash, on_give_up: proc { callback_called = true }) do
|
|
611
|
+
increment_tries
|
|
612
|
+
raise SecondNonStandardError, "not a match"
|
|
613
|
+
end
|
|
614
|
+
end.to raise_error(SecondNonStandardError, /not a match/)
|
|
615
|
+
|
|
616
|
+
expect(callback_called).to be(false)
|
|
617
|
+
expect(@tries).to eq(1)
|
|
618
|
+
end
|
|
619
|
+
|
|
410
620
|
it "successfully retries when the values are arrays of exception message patterns" do
|
|
411
621
|
exceptions = []
|
|
412
622
|
handler = ->(exception, try, _elapsed_time, _next_interval) { exceptions[try] = exception }
|
|
@@ -599,6 +809,274 @@ describe Retriable do
|
|
|
599
809
|
it "raises NoMethodError on invalid configuration" do
|
|
600
810
|
expect { described_class.configure { |c| c.does_not_exist = 123 } }.to raise_error(NoMethodError)
|
|
601
811
|
end
|
|
812
|
+
|
|
813
|
+
it "returns the configure block's result" do
|
|
814
|
+
result = described_class.configure do |c|
|
|
815
|
+
c.tries = 7
|
|
816
|
+
:configured
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
expect(result).to eq(:configured)
|
|
820
|
+
expect(described_class.config.tries).to eq(7)
|
|
821
|
+
end
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
context "#configure thread safety (copy-on-write)" do
|
|
825
|
+
it "eagerly initializes @config at load time, before any configure/config call" do
|
|
826
|
+
script = "require 'retriable'; " \
|
|
827
|
+
"exit(Retriable.instance_variable_get(:@config).is_a?(Retriable::Config) ? 0 : 1)"
|
|
828
|
+
expect(system(RbConfig.ruby, "-Ilib", "-e", script)).to be(true)
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
it "publishes a new Config object on configure instead of mutating in place" do
|
|
832
|
+
before = described_class.config
|
|
833
|
+
described_class.configure { |c| c.tries = 7 }
|
|
834
|
+
after = described_class.config
|
|
835
|
+
|
|
836
|
+
expect(after).not_to equal(before)
|
|
837
|
+
expect(after.tries).to eq(7)
|
|
838
|
+
expect(before.tries).not_to eq(7)
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
it "keeps an already-captured snapshot stable across later configures" do
|
|
842
|
+
described_class.configure { |c| c.contexts[:sql] = { tries: 1 } }
|
|
843
|
+
snapshot = described_class.config
|
|
844
|
+
|
|
845
|
+
described_class.configure { |c| c.contexts[:http] = { tries: 2 } }
|
|
846
|
+
described_class.configure { |c| c.contexts[:sql][:tries] = 99 }
|
|
847
|
+
|
|
848
|
+
expect(snapshot.contexts).to eq(sql: { tries: 1 })
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
it "publishes a frozen snapshot, so direct mutation fails loudly" do
|
|
852
|
+
described_class.configure { |c| c.contexts[:api] = { tries: 1 } }
|
|
853
|
+
|
|
854
|
+
expect { described_class.config.tries = 99 }.to raise_error(FrozenError)
|
|
855
|
+
expect { described_class.config.contexts[:api][:tries] = 99 }.to raise_error(FrozenError)
|
|
856
|
+
expect(described_class.config.contexts[:api]).to eq(tries: 1)
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
it "still hands the configure block a mutable candidate" do
|
|
860
|
+
expect do
|
|
861
|
+
described_class.configure do |c|
|
|
862
|
+
c.contexts[:api] = { tries: 1 }
|
|
863
|
+
c.contexts[:api][:tries] = 2
|
|
864
|
+
c.on = [StandardError]
|
|
865
|
+
c.on << ArgumentError
|
|
866
|
+
end
|
|
867
|
+
end.not_to raise_error
|
|
868
|
+
|
|
869
|
+
expect(described_class.config.contexts[:api]).to eq(tries: 2)
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
it "does not freeze collections the caller still owns" do
|
|
873
|
+
caller_owned = [StandardError]
|
|
874
|
+
|
|
875
|
+
described_class.configure { |c| c.on = caller_owned }
|
|
876
|
+
|
|
877
|
+
expect(caller_owned).not_to be_frozen
|
|
878
|
+
expect(described_class.config.on).to be_frozen
|
|
879
|
+
expect(described_class.config.on).not_to equal(caller_owned)
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
it "owns and freezes a copy of a mutable Hash default" do
|
|
883
|
+
fallback = []
|
|
884
|
+
contexts = Hash.new(fallback)
|
|
885
|
+
contexts[:api] = { tries: 1 }
|
|
886
|
+
|
|
887
|
+
described_class.configure { |c| c.contexts = contexts }
|
|
888
|
+
published_default = described_class.config.contexts.default
|
|
889
|
+
|
|
890
|
+
expect(published_default).not_to equal(fallback)
|
|
891
|
+
expect(published_default).to be_frozen
|
|
892
|
+
expect(fallback).not_to be_frozen
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
it "does not let a published default leak mutations into the caller's object" do
|
|
896
|
+
fallback = []
|
|
897
|
+
described_class.configure { |c| c.contexts = Hash.new(fallback) }
|
|
898
|
+
|
|
899
|
+
expect { described_class.config.contexts[:absent] << :leaked }.to raise_error(FrozenError)
|
|
900
|
+
expect(fallback).to be_empty
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
it "does not drop updates when configured concurrently from many threads" do
|
|
904
|
+
keys = (0...50).map { |i| :"ctx_#{i}" }
|
|
905
|
+
release = Queue.new
|
|
906
|
+
|
|
907
|
+
threads = keys.map do |key|
|
|
908
|
+
Thread.new do
|
|
909
|
+
release.pop
|
|
910
|
+
described_class.configure { |c| c.contexts[key] = { tries: 1 } }
|
|
911
|
+
end
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
keys.size.times { release << true }
|
|
915
|
+
join_without_deadlock(threads)
|
|
916
|
+
|
|
917
|
+
expect(described_class.config.contexts.keys).to match_array(keys)
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
it "lets nested configure calls join the outer transaction" do
|
|
921
|
+
inner_candidate = nil
|
|
922
|
+
|
|
923
|
+
result = described_class.configure do |outer|
|
|
924
|
+
outer.tries = 7
|
|
925
|
+
nested_result = described_class.configure do |inner|
|
|
926
|
+
inner_candidate = inner
|
|
927
|
+
inner.base_interval = 1
|
|
928
|
+
:nested_result
|
|
929
|
+
end
|
|
930
|
+
|
|
931
|
+
expect(nested_result).to eq(:nested_result)
|
|
932
|
+
expect(inner_candidate).to equal(outer)
|
|
933
|
+
:outer_result
|
|
934
|
+
end
|
|
935
|
+
|
|
936
|
+
expect(result).to eq(:outer_result)
|
|
937
|
+
expect(described_class.config.tries).to eq(7)
|
|
938
|
+
expect(described_class.config.base_interval).to eq(1)
|
|
939
|
+
end
|
|
940
|
+
|
|
941
|
+
it "rolls back nested changes when the outer configure raises" do
|
|
942
|
+
described_class.configure { |c| c.tries = 4 }
|
|
943
|
+
|
|
944
|
+
expect do
|
|
945
|
+
described_class.configure do |outer|
|
|
946
|
+
outer.tries = 7
|
|
947
|
+
described_class.configure { |inner| inner.base_interval = 1 }
|
|
948
|
+
raise "outer failed"
|
|
949
|
+
end
|
|
950
|
+
end.to raise_error(RuntimeError, "outer failed")
|
|
951
|
+
|
|
952
|
+
expect(described_class.config.tries).to eq(4)
|
|
953
|
+
expect(described_class.config.base_interval).to eq(0.5)
|
|
954
|
+
end
|
|
955
|
+
|
|
956
|
+
it "lets configure calls from a fiber join the outer transaction" do
|
|
957
|
+
described_class.configure do |outer|
|
|
958
|
+
fiber_candidate = Fiber.new do
|
|
959
|
+
described_class.configure do |inner|
|
|
960
|
+
inner.tries = 9
|
|
961
|
+
inner
|
|
962
|
+
end
|
|
963
|
+
end.resume
|
|
964
|
+
|
|
965
|
+
expect(fiber_candidate).to equal(outer)
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
expect(described_class.config.tries).to eq(9)
|
|
969
|
+
end
|
|
970
|
+
|
|
971
|
+
it "exposes the in-progress config to the configuring thread" do
|
|
972
|
+
described_class.configure do |c|
|
|
973
|
+
c.tries = 7
|
|
974
|
+
expect(described_class.config.tries).to eq(7)
|
|
975
|
+
end
|
|
976
|
+
|
|
977
|
+
expect(described_class.config.tries).to eq(7)
|
|
978
|
+
end
|
|
979
|
+
|
|
980
|
+
it "uses the in-progress config for retriable" do
|
|
981
|
+
attempts = 0
|
|
982
|
+
|
|
983
|
+
described_class.configure do |c|
|
|
984
|
+
c.tries = 1
|
|
985
|
+
c.sleep_disabled = true
|
|
986
|
+
|
|
987
|
+
expect do
|
|
988
|
+
described_class.retriable do
|
|
989
|
+
attempts += 1
|
|
990
|
+
raise StandardError
|
|
991
|
+
end
|
|
992
|
+
end.to raise_error(StandardError)
|
|
993
|
+
end
|
|
994
|
+
|
|
995
|
+
expect(attempts).to eq(1)
|
|
996
|
+
end
|
|
997
|
+
|
|
998
|
+
it "uses candidate-only contexts for with_context" do
|
|
999
|
+
described_class.configure do |c|
|
|
1000
|
+
c.contexts[:candidate] = { tries: 1 }
|
|
1001
|
+
|
|
1002
|
+
expect(described_class.with_context(:candidate) { :found }).to eq(:found)
|
|
1003
|
+
end
|
|
1004
|
+
end
|
|
1005
|
+
|
|
1006
|
+
it "shares the in-progress config with fibers in the configuring thread" do
|
|
1007
|
+
described_class.configure do |c|
|
|
1008
|
+
c.tries = 7
|
|
1009
|
+
|
|
1010
|
+
expect(Fiber.new { described_class.config.tries }.resume).to eq(7)
|
|
1011
|
+
end
|
|
1012
|
+
end
|
|
1013
|
+
|
|
1014
|
+
it "does not block readers while configure is in progress" do
|
|
1015
|
+
published_tries = described_class.config.tries
|
|
1016
|
+
published_base_interval = described_class.config.base_interval
|
|
1017
|
+
configuring = Queue.new
|
|
1018
|
+
release = Queue.new
|
|
1019
|
+
writer = Thread.new do
|
|
1020
|
+
described_class.configure do |c|
|
|
1021
|
+
c.tries = published_tries + 1
|
|
1022
|
+
described_class.configure { |inner| inner.base_interval = published_base_interval + 1 }
|
|
1023
|
+
configuring << true
|
|
1024
|
+
release.pop
|
|
1025
|
+
end
|
|
1026
|
+
end
|
|
1027
|
+
reader = nil
|
|
1028
|
+
|
|
1029
|
+
begin
|
|
1030
|
+
configuring.pop
|
|
1031
|
+
reader = Thread.new do
|
|
1032
|
+
snapshot = described_class.config
|
|
1033
|
+
[snapshot.tries, snapshot.base_interval]
|
|
1034
|
+
end
|
|
1035
|
+
# The timeout is a deadlock backstop, not the assertion. A reader that
|
|
1036
|
+
# is not blocked returns immediately, so this budget is only ever
|
|
1037
|
+
# approached if #config starts waiting on the in-progress #configure.
|
|
1038
|
+
expect(reader.join(deadlock_backstop_seconds)).to be(reader),
|
|
1039
|
+
"reader blocked while configure was in progress"
|
|
1040
|
+
# Thread#value re-raises anything the reader raised, so a broken reader
|
|
1041
|
+
# fails loudly instead of hanging on an empty queue.
|
|
1042
|
+
expect(reader.value).to eq([published_tries, published_base_interval])
|
|
1043
|
+
ensure
|
|
1044
|
+
release << true
|
|
1045
|
+
join_without_deadlock(writer, reader)
|
|
1046
|
+
end
|
|
1047
|
+
end
|
|
1048
|
+
end
|
|
1049
|
+
|
|
1050
|
+
context "#retriable tries/intervals precedence" do
|
|
1051
|
+
it "lets a per-call tries clear globally configured intervals" do
|
|
1052
|
+
described_class.configure { |c| c.intervals = [0.5, 1.0] }
|
|
1053
|
+
|
|
1054
|
+
expect do
|
|
1055
|
+
described_class.retriable(tries: 1) { increment_tries_with_exception }
|
|
1056
|
+
end.to raise_error(StandardError)
|
|
1057
|
+
|
|
1058
|
+
expect(@tries).to eq(1)
|
|
1059
|
+
end
|
|
1060
|
+
|
|
1061
|
+
it "still lets per-call intervals win when both intervals and tries are given" do
|
|
1062
|
+
expect do
|
|
1063
|
+
described_class.retriable(intervals: [0.5, 1.0], tries: 1) { increment_tries_with_exception }
|
|
1064
|
+
end.to raise_error(StandardError)
|
|
1065
|
+
|
|
1066
|
+
expect(@tries).to eq(3) # intervals.size + 1
|
|
1067
|
+
end
|
|
1068
|
+
|
|
1069
|
+
it "lets a with_context tries clear context intervals" do
|
|
1070
|
+
described_class.configure do |c|
|
|
1071
|
+
c.contexts[:api] = { intervals: [0.5, 1.0] }
|
|
1072
|
+
end
|
|
1073
|
+
|
|
1074
|
+
expect do
|
|
1075
|
+
described_class.with_context(:api, tries: 1) { increment_tries_with_exception }
|
|
1076
|
+
end.to raise_error(StandardError)
|
|
1077
|
+
|
|
1078
|
+
expect(@tries).to eq(1)
|
|
1079
|
+
end
|
|
602
1080
|
end
|
|
603
1081
|
|
|
604
1082
|
context "#with_override" do
|
|
@@ -737,17 +1215,15 @@ describe Retriable do
|
|
|
737
1215
|
end
|
|
738
1216
|
|
|
739
1217
|
it "treats non-hash configured contexts as empty when override contexts are hash" do
|
|
740
|
-
|
|
741
|
-
described_class.configure { |c| c.contexts = nil }
|
|
1218
|
+
described_class.configure { |c| c.contexts = nil }
|
|
742
1219
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
end
|
|
746
|
-
|
|
747
|
-
expect(@tries).to eq(1)
|
|
748
|
-
ensure
|
|
749
|
-
described_class.configure { |c| c.contexts = {} }
|
|
1220
|
+
described_class.with_override(contexts: { api: { tries: 1 } }) do
|
|
1221
|
+
described_class.with_context(:api) { increment_tries }
|
|
750
1222
|
end
|
|
1223
|
+
|
|
1224
|
+
expect(@tries).to eq(1)
|
|
1225
|
+
ensure
|
|
1226
|
+
described_class.configure { |c| c.contexts = {} }
|
|
751
1227
|
end
|
|
752
1228
|
|
|
753
1229
|
it "ignores nil override contexts values in with_context" do
|
|
@@ -921,7 +1397,7 @@ describe Retriable do
|
|
|
921
1397
|
|
|
922
1398
|
2.times { ready.pop }
|
|
923
1399
|
2.times { proceed << true }
|
|
924
|
-
threads
|
|
1400
|
+
join_without_deadlock(threads)
|
|
925
1401
|
|
|
926
1402
|
expect(results).to eq(1 => 1, 2 => 2)
|
|
927
1403
|
end
|
|
@@ -952,7 +1428,7 @@ describe Retriable do
|
|
|
952
1428
|
sibling_done << true
|
|
953
1429
|
end
|
|
954
1430
|
|
|
955
|
-
|
|
1431
|
+
join_without_deadlock(setter, sibling)
|
|
956
1432
|
expect(sibling_tries).to eq(3)
|
|
957
1433
|
end
|
|
958
1434
|
|
|
@@ -960,7 +1436,7 @@ describe Retriable do
|
|
|
960
1436
|
child_tries = nil
|
|
961
1437
|
|
|
962
1438
|
described_class.with_override(tries: 1) do
|
|
963
|
-
Thread.new do
|
|
1439
|
+
child = Thread.new do
|
|
964
1440
|
tries = 0
|
|
965
1441
|
begin
|
|
966
1442
|
described_class.retriable(tries: 3) do
|
|
@@ -970,7 +1446,8 @@ describe Retriable do
|
|
|
970
1446
|
rescue StandardError
|
|
971
1447
|
child_tries = tries
|
|
972
1448
|
end
|
|
973
|
-
end
|
|
1449
|
+
end
|
|
1450
|
+
join_without_deadlock(child)
|
|
974
1451
|
end
|
|
975
1452
|
|
|
976
1453
|
expect(child_tries).to eq(3)
|
|
@@ -979,7 +1456,7 @@ describe Retriable do
|
|
|
979
1456
|
it "shares the active override with fibers in the same thread" do
|
|
980
1457
|
fiber_tries = nil
|
|
981
1458
|
|
|
982
|
-
Thread.new do
|
|
1459
|
+
worker = Thread.new do
|
|
983
1460
|
described_class.with_override(tries: 1) do
|
|
984
1461
|
Fiber.new do
|
|
985
1462
|
tries = 0
|
|
@@ -993,7 +1470,8 @@ describe Retriable do
|
|
|
993
1470
|
end
|
|
994
1471
|
end.resume
|
|
995
1472
|
end
|
|
996
|
-
end
|
|
1473
|
+
end
|
|
1474
|
+
join_without_deadlock(worker)
|
|
997
1475
|
|
|
998
1476
|
expect(fiber_tries).to eq(1)
|
|
999
1477
|
end
|
|
@@ -1002,7 +1480,7 @@ describe Retriable do
|
|
|
1002
1480
|
other_thread_tries = nil
|
|
1003
1481
|
|
|
1004
1482
|
described_class.with_override(tries: 1) do
|
|
1005
|
-
Thread.new do
|
|
1483
|
+
other = Thread.new do
|
|
1006
1484
|
tries = 0
|
|
1007
1485
|
begin
|
|
1008
1486
|
described_class.retriable(tries: 3) do
|
|
@@ -1012,11 +1490,37 @@ describe Retriable do
|
|
|
1012
1490
|
rescue StandardError
|
|
1013
1491
|
other_thread_tries = tries
|
|
1014
1492
|
end
|
|
1015
|
-
end
|
|
1493
|
+
end
|
|
1494
|
+
join_without_deadlock(other)
|
|
1016
1495
|
end
|
|
1017
1496
|
|
|
1018
1497
|
expect(other_thread_tries).to eq(3)
|
|
1019
1498
|
end
|
|
1499
|
+
|
|
1500
|
+
it "applies overridden on_give_up handlers" do
|
|
1501
|
+
callback_called = false
|
|
1502
|
+
|
|
1503
|
+
expect do
|
|
1504
|
+
described_class.with_override(on_give_up: proc { callback_called = true }) do
|
|
1505
|
+
described_class.retriable(tries: 1) { increment_tries_with_exception }
|
|
1506
|
+
end
|
|
1507
|
+
end.to raise_error(StandardError)
|
|
1508
|
+
|
|
1509
|
+
expect(callback_called).to be(true)
|
|
1510
|
+
end
|
|
1511
|
+
|
|
1512
|
+
it "applies on_give_up handlers configured via per-context overrides" do
|
|
1513
|
+
received_reason = nil
|
|
1514
|
+
handler = proc { |_e, _try, _elapsed, _interval, reason| received_reason = reason }
|
|
1515
|
+
|
|
1516
|
+
expect do
|
|
1517
|
+
described_class.with_override(contexts: { api: { tries: 1, on_give_up: handler } }) do
|
|
1518
|
+
described_class.with_context(:api) { increment_tries_with_exception }
|
|
1519
|
+
end
|
|
1520
|
+
end.to raise_error(StandardError)
|
|
1521
|
+
|
|
1522
|
+
expect(received_reason).to eq(:tries_exhausted)
|
|
1523
|
+
end
|
|
1020
1524
|
end
|
|
1021
1525
|
|
|
1022
1526
|
context "#with_context" do
|
|
@@ -1034,8 +1538,15 @@ describe Retriable do
|
|
|
1034
1538
|
expect(@tries).to eq(1)
|
|
1035
1539
|
end
|
|
1036
1540
|
|
|
1037
|
-
it "
|
|
1038
|
-
expect
|
|
1541
|
+
it "raises an ArgumentError when called without a block" do
|
|
1542
|
+
expect { described_class.with_context(:sql) }
|
|
1543
|
+
.to raise_error(ArgumentError, /with_context requires a block/)
|
|
1544
|
+
expect(@tries).to eq(0)
|
|
1545
|
+
end
|
|
1546
|
+
|
|
1547
|
+
it "checks for a block before looking up the context" do
|
|
1548
|
+
expect { described_class.with_context(:missing) }
|
|
1549
|
+
.to raise_error(ArgumentError, /with_context requires a block/)
|
|
1039
1550
|
expect(@tries).to eq(0)
|
|
1040
1551
|
end
|
|
1041
1552
|
|
|
@@ -1075,5 +1586,56 @@ describe Retriable do
|
|
|
1075
1586
|
described_class.with_context(:broken) { increment_tries }
|
|
1076
1587
|
expect(@tries).to eq(1)
|
|
1077
1588
|
end
|
|
1589
|
+
|
|
1590
|
+
it "surfaces an invalid context on any retriable call before that context is used" do
|
|
1591
|
+
described_class.configure { |c| c.contexts[:unused] = { contexts: {} } }
|
|
1592
|
+
|
|
1593
|
+
expect { described_class.retriable { :ok } }
|
|
1594
|
+
.to raise_error(ArgumentError, /contexts is not a valid option/)
|
|
1595
|
+
end
|
|
1596
|
+
|
|
1597
|
+
it "invokes on_give_up configured on a context" do
|
|
1598
|
+
callback_called = false
|
|
1599
|
+
described_class.configure do |c|
|
|
1600
|
+
c.contexts[:flaky] = { tries: 1, on_give_up: proc { callback_called = true } }
|
|
1601
|
+
end
|
|
1602
|
+
|
|
1603
|
+
expect { described_class.with_context(:flaky) { increment_tries_with_exception } }
|
|
1604
|
+
.to raise_error(StandardError)
|
|
1605
|
+
|
|
1606
|
+
expect(callback_called).to be(true)
|
|
1607
|
+
end
|
|
1608
|
+
|
|
1609
|
+
it "resolves the context against a single config snapshot" do
|
|
1610
|
+
with_ctx = Retriable::Config.new(sleep_disabled: true, contexts: { api: { tries: 1 } })
|
|
1611
|
+
without_ctx = Retriable::Config.new(sleep_disabled: true)
|
|
1612
|
+
|
|
1613
|
+
# Simulate a concurrent #configure publishing a new config between
|
|
1614
|
+
# with_context's existence check and its option resolution: the first
|
|
1615
|
+
# config read sees the context, later reads do not. with_context must
|
|
1616
|
+
# read config once so the context options are never silently dropped.
|
|
1617
|
+
allow(described_class).to receive(:config).and_return(with_ctx, without_ctx)
|
|
1618
|
+
|
|
1619
|
+
expect { described_class.with_context(:api) { increment_tries_with_exception } }
|
|
1620
|
+
.to raise_error(StandardError)
|
|
1621
|
+
|
|
1622
|
+
expect(@tries).to eq(1)
|
|
1623
|
+
end
|
|
1624
|
+
|
|
1625
|
+
it "resolves global options against the same snapshot used for the context" do
|
|
1626
|
+
special_error = Class.new(StandardError)
|
|
1627
|
+
with_ctx = Retriable::Config.new(sleep_disabled: true, on: [special_error], contexts: { api: { tries: 2 } })
|
|
1628
|
+
swapped = Retriable::Config.new(sleep_disabled: true, on: [ArgumentError])
|
|
1629
|
+
|
|
1630
|
+
# A concurrent #configure swaps the global config (here, the retriable
|
|
1631
|
+
# `on` list) after with_context has captured its snapshot. The whole
|
|
1632
|
+
# context execution must use the captured snapshot, not the swapped one.
|
|
1633
|
+
allow(described_class).to receive(:config).and_return(with_ctx, swapped)
|
|
1634
|
+
|
|
1635
|
+
expect { described_class.with_context(:api) { increment_tries_with_exception(special_error) } }
|
|
1636
|
+
.to raise_error(special_error)
|
|
1637
|
+
|
|
1638
|
+
expect(@tries).to eq(2)
|
|
1639
|
+
end
|
|
1078
1640
|
end
|
|
1079
1641
|
end
|