retriable 3.5.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 +113 -0
- data/Gemfile +4 -1
- data/README.md +197 -110
- data/docs/testing.md +212 -0
- data/lib/retriable/config.rb +81 -10
- data/lib/retriable/core_ext/kernel.rb +6 -4
- data/lib/retriable/exponential_backoff.rb +44 -10
- data/lib/retriable/validation.rb +95 -0
- data/lib/retriable/version.rb +1 -1
- data/lib/retriable.rb +121 -57
- data/retriable.gemspec +2 -7
- data/sig/retriable.rbs +29 -1
- data/spec/config_spec.rb +157 -4
- data/spec/exponential_backoff_spec.rb +45 -26
- data/spec/retriable_spec.rb +739 -87
- data/spec/spec_helper.rb +3 -1
- metadata +9 -53
data/spec/retriable_spec.rb
CHANGED
|
@@ -9,7 +9,7 @@ describe Retriable do
|
|
|
9
9
|
|
|
10
10
|
before(:each) do
|
|
11
11
|
described_class.instance_variable_set(:@config, nil)
|
|
12
|
-
|
|
12
|
+
Thread.current.thread_variable_set(Retriable::OVERRIDE_THREAD_KEY, nil)
|
|
13
13
|
described_class.configure { |c| c.sleep_disabled = true }
|
|
14
14
|
@tries = 0
|
|
15
15
|
@next_interval_table = {}
|
|
@@ -38,6 +38,49 @@ describe Retriable do
|
|
|
38
38
|
expect { retriable { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
39
39
|
expect(@tries).to eq(3)
|
|
40
40
|
end
|
|
41
|
+
|
|
42
|
+
it "passes on_give_up through the kernel extension" do
|
|
43
|
+
require_relative "../lib/retriable/core_ext/kernel"
|
|
44
|
+
received_reason = nil
|
|
45
|
+
handler = proc { |_e, _try, _elapsed, _interval, reason| received_reason = reason }
|
|
46
|
+
|
|
47
|
+
expect { retriable(tries: 1, on_give_up: handler) { increment_tries_with_exception } }
|
|
48
|
+
.to raise_error(StandardError)
|
|
49
|
+
|
|
50
|
+
expect(received_reason).to eq(:tries_exhausted)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# These two specs lock in the anonymous block forwarding (`&`) semantics
|
|
54
|
+
# across both delegation layers: Kernel#retriable_with_context ->
|
|
55
|
+
# Retriable.with_context. If the `&` is dropped at either layer, the block
|
|
56
|
+
# is not forwarded and the `block_given?` guard in with_context raises
|
|
57
|
+
# ArgumentError instead of running the block.
|
|
58
|
+
it "forwards a block through Kernel#retriable_with_context" do
|
|
59
|
+
require_relative "../lib/retriable/core_ext/kernel"
|
|
60
|
+
Retriable.configure { |c| c.contexts[:sql] = { tries: 1 } }
|
|
61
|
+
|
|
62
|
+
retriable_with_context(:sql) { increment_tries }
|
|
63
|
+
|
|
64
|
+
expect(@tries).to eq(1)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "raises an ArgumentError when Kernel#retriable_with_context is called without a block" do
|
|
68
|
+
require_relative "../lib/retriable/core_ext/kernel"
|
|
69
|
+
Retriable.configure { |c| c.contexts[:sql] = { tries: 1 } }
|
|
70
|
+
|
|
71
|
+
expect { retriable_with_context(:sql) }
|
|
72
|
+
.to raise_error(ArgumentError, /with_context requires a block/)
|
|
73
|
+
expect(@tries).to eq(0)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "is not callable with an explicit receiver" do
|
|
77
|
+
require_relative "../lib/retriable/core_ext/kernel"
|
|
78
|
+
|
|
79
|
+
expect { "foo".retriable { increment_tries } }
|
|
80
|
+
.to raise_error(NoMethodError, /private method/)
|
|
81
|
+
expect { "foo".retriable_with_context(:sql) { increment_tries } }
|
|
82
|
+
.to raise_error(NoMethodError, /private method/)
|
|
83
|
+
end
|
|
41
84
|
end
|
|
42
85
|
|
|
43
86
|
context "#retriable" do
|
|
@@ -50,7 +93,6 @@ describe Retriable do
|
|
|
50
93
|
|
|
51
94
|
it "raises a LocalJumpError if not given a block" do
|
|
52
95
|
expect { described_class.retriable }.to raise_error(LocalJumpError)
|
|
53
|
-
expect { described_class.retriable(timeout: 2) }.to raise_error(LocalJumpError)
|
|
54
96
|
end
|
|
55
97
|
|
|
56
98
|
it "stops at first try if the block does not raise an exception" do
|
|
@@ -84,8 +126,92 @@ describe Retriable do
|
|
|
84
126
|
expect(@tries).to eq(10)
|
|
85
127
|
end
|
|
86
128
|
|
|
87
|
-
it "
|
|
88
|
-
|
|
129
|
+
it "does not prebuild generated intervals before the first successful try" do
|
|
130
|
+
interval_for = ->(_index) { raise "interval should not be used" }
|
|
131
|
+
backoff = instance_double(Retriable::ExponentialBackoff, interval_provider: interval_for)
|
|
132
|
+
allow(Retriable::ExponentialBackoff).to receive(:new).and_call_original
|
|
133
|
+
allow(Retriable::ExponentialBackoff).to receive(:new).with(
|
|
134
|
+
hash_including(:base_interval, :multiplier, :max_interval, :rand_factor),
|
|
135
|
+
).and_return(backoff)
|
|
136
|
+
|
|
137
|
+
described_class.retriable(tries: 1_000_000) { increment_tries }
|
|
138
|
+
|
|
139
|
+
expect(@tries).to eq(1)
|
|
140
|
+
expect(backoff).to have_received(:interval_provider)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it "supports unbounded retries until the block succeeds" do
|
|
144
|
+
described_class.retriable(tries: Float::INFINITY, max_elapsed_time: 60) do
|
|
145
|
+
increment_tries
|
|
146
|
+
raise StandardError if @tries < 5
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
expect(@tries).to eq(5)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it "stops unbounded retries at max_elapsed_time" do
|
|
153
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
154
|
+
timeline = [
|
|
155
|
+
start_time,
|
|
156
|
+
start_time,
|
|
157
|
+
start_time,
|
|
158
|
+
start_time + 0.01,
|
|
159
|
+
start_time + 0.01,
|
|
160
|
+
start_time + 0.02,
|
|
161
|
+
start_time + 0.02,
|
|
162
|
+
]
|
|
163
|
+
allow(Process).to receive(:clock_gettime).with(Process::CLOCK_MONOTONIC) { timeline.shift || timeline.last }
|
|
164
|
+
|
|
165
|
+
expect do
|
|
166
|
+
described_class.retriable(
|
|
167
|
+
tries: Float::INFINITY,
|
|
168
|
+
base_interval: 0.01,
|
|
169
|
+
multiplier: 1.0,
|
|
170
|
+
rand_factor: 0.0,
|
|
171
|
+
sleep_disabled: true,
|
|
172
|
+
max_elapsed_time: 0.015,
|
|
173
|
+
) do
|
|
174
|
+
increment_tries_with_exception
|
|
175
|
+
end
|
|
176
|
+
end.to raise_error(StandardError)
|
|
177
|
+
|
|
178
|
+
expect(@tries).to eq(3)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
it "raises ArgumentError when tries is Float::INFINITY without a finite max_elapsed_time" do
|
|
182
|
+
expect do
|
|
183
|
+
described_class.retriable(tries: Float::INFINITY, max_elapsed_time: nil) { increment_tries }
|
|
184
|
+
end.to raise_error(ArgumentError, /max_elapsed_time must be a finite number/)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
it "raises ArgumentError when tries is Float::INFINITY with infinite max_elapsed_time" do
|
|
188
|
+
expect do
|
|
189
|
+
described_class.retriable(tries: Float::INFINITY, max_elapsed_time: Float::INFINITY) { increment_tries }
|
|
190
|
+
end.to raise_error(ArgumentError, /max_elapsed_time must be a finite number/)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it "raises ArgumentError when tries is Float::INFINITY with custom intervals" do
|
|
194
|
+
expect do
|
|
195
|
+
described_class.retriable(tries: Float::INFINITY, intervals: [0.1, 0.2], max_elapsed_time: 60) do
|
|
196
|
+
increment_tries_with_exception
|
|
197
|
+
end
|
|
198
|
+
end.to raise_error(ArgumentError, /intervals cannot be used with tries: Float::INFINITY/)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it "raises ArgumentError when tries is Float::NAN" do
|
|
202
|
+
expect do
|
|
203
|
+
described_class.retriable(tries: Float::NAN) { increment_tries }
|
|
204
|
+
end.to raise_error(ArgumentError, /tries/)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it "raises ArgumentError when tries is negative infinity" do
|
|
208
|
+
expect do
|
|
209
|
+
described_class.retriable(tries: -Float::INFINITY) { increment_tries }
|
|
210
|
+
end.to raise_error(ArgumentError, /tries/)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
it "rejects timeout as an unknown option" do
|
|
214
|
+
expect { described_class.retriable(timeout: 1) { :noop } }.to raise_error(ArgumentError, /not a valid option/)
|
|
89
215
|
end
|
|
90
216
|
|
|
91
217
|
it "applies a randomized exponential backoff to each try" do
|
|
@@ -131,6 +257,187 @@ describe Retriable do
|
|
|
131
257
|
end
|
|
132
258
|
end
|
|
133
259
|
|
|
260
|
+
it "does not call on_retry when explicitly set to nil" do
|
|
261
|
+
callback_called = false
|
|
262
|
+
original_on_retry = described_class.config.on_retry
|
|
263
|
+
|
|
264
|
+
begin
|
|
265
|
+
described_class.configure do |c|
|
|
266
|
+
c.on_retry = proc { |_exception, _try, _elapsed_time, _next_interval| callback_called = true }
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
expect do
|
|
270
|
+
described_class.retriable(on_retry: nil, tries: 3) { increment_tries_with_exception }
|
|
271
|
+
end.to raise_error(StandardError)
|
|
272
|
+
|
|
273
|
+
expect(@tries).to eq(3)
|
|
274
|
+
expect(callback_called).to be(false)
|
|
275
|
+
ensure
|
|
276
|
+
described_class.configure do |c|
|
|
277
|
+
c.on_retry = original_on_retry
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
it "calls on_give_up with max elapsed time details before re-raising" do
|
|
283
|
+
described_class.configure { |c| c.sleep_disabled = false }
|
|
284
|
+
give_up_calls = []
|
|
285
|
+
on_give_up = proc do |exception, try, elapsed_time, next_interval, reason|
|
|
286
|
+
give_up_calls << [exception, try, elapsed_time, next_interval, reason]
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
expect do
|
|
290
|
+
described_class.retriable(
|
|
291
|
+
intervals: [1.0, 1.0],
|
|
292
|
+
max_elapsed_time: 0.5,
|
|
293
|
+
on_give_up: on_give_up,
|
|
294
|
+
) do
|
|
295
|
+
increment_tries_with_exception
|
|
296
|
+
end
|
|
297
|
+
end.to raise_error(StandardError)
|
|
298
|
+
|
|
299
|
+
exception, try, elapsed_time, next_interval, reason = give_up_calls.fetch(0)
|
|
300
|
+
expect(give_up_calls.size).to eq(1)
|
|
301
|
+
expect(exception).to be_a(StandardError)
|
|
302
|
+
expect(exception.message).to eq("StandardError occurred")
|
|
303
|
+
expect(try).to eq(1)
|
|
304
|
+
expect(elapsed_time).to be >= 0
|
|
305
|
+
expect(next_interval).to eq(1.0)
|
|
306
|
+
expect(reason).to eq(:max_elapsed_time)
|
|
307
|
+
expect(@tries).to eq(1)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
it "calls on_give_up with tries exhausted details before re-raising" do
|
|
311
|
+
give_up_calls = []
|
|
312
|
+
on_give_up = proc do |exception, try, elapsed_time, next_interval, reason|
|
|
313
|
+
give_up_calls << [exception, try, elapsed_time, next_interval, reason]
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
expect do
|
|
317
|
+
described_class.retriable(tries: 2, on_give_up: on_give_up) { increment_tries_with_exception }
|
|
318
|
+
end.to raise_error(StandardError)
|
|
319
|
+
|
|
320
|
+
exception, try, elapsed_time, next_interval, reason = give_up_calls.fetch(0)
|
|
321
|
+
expect(give_up_calls.size).to eq(1)
|
|
322
|
+
expect(exception).to be_a(StandardError)
|
|
323
|
+
expect(exception.message).to eq("StandardError occurred")
|
|
324
|
+
expect(try).to eq(2)
|
|
325
|
+
expect(elapsed_time).to be >= 0
|
|
326
|
+
expect(next_interval).to be_nil
|
|
327
|
+
expect(reason).to eq(:tries_exhausted)
|
|
328
|
+
expect(@tries).to eq(2)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
it "does not call on_give_up when the block eventually succeeds" do
|
|
332
|
+
callback_called = false
|
|
333
|
+
|
|
334
|
+
described_class.retriable(tries: 3, on_give_up: proc { callback_called = true }) do
|
|
335
|
+
increment_tries
|
|
336
|
+
raise StandardError if @tries < 2
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
expect(callback_called).to be(false)
|
|
340
|
+
expect(@tries).to eq(2)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
it "does not call on_give_up for non-retriable exception types" do
|
|
344
|
+
callback_called = false
|
|
345
|
+
|
|
346
|
+
expect do
|
|
347
|
+
described_class.retriable(on_give_up: proc { callback_called = true }) do
|
|
348
|
+
increment_tries_with_exception(NonStandardError)
|
|
349
|
+
end
|
|
350
|
+
end.to raise_error(NonStandardError)
|
|
351
|
+
|
|
352
|
+
expect(callback_called).to be(false)
|
|
353
|
+
expect(@tries).to eq(1)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
it "does not call on_give_up when retry_if rejects the exception" do
|
|
357
|
+
callback_called = false
|
|
358
|
+
|
|
359
|
+
expect do
|
|
360
|
+
described_class.retriable(
|
|
361
|
+
tries: 3,
|
|
362
|
+
retry_if: ->(_exception) { false },
|
|
363
|
+
on_give_up: proc { callback_called = true },
|
|
364
|
+
) do
|
|
365
|
+
increment_tries_with_exception
|
|
366
|
+
end
|
|
367
|
+
end.to raise_error(StandardError)
|
|
368
|
+
|
|
369
|
+
expect(callback_called).to be(false)
|
|
370
|
+
expect(@tries).to eq(1)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
it "does not call on_give_up when explicitly set to false" do
|
|
374
|
+
callback_called = false
|
|
375
|
+
original_on_give_up = described_class.config.on_give_up
|
|
376
|
+
|
|
377
|
+
begin
|
|
378
|
+
described_class.configure do |c|
|
|
379
|
+
c.on_give_up = proc { callback_called = true }
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
expect do
|
|
383
|
+
described_class.retriable(on_give_up: false, tries: 1) { increment_tries_with_exception }
|
|
384
|
+
end.to raise_error(StandardError)
|
|
385
|
+
|
|
386
|
+
expect(callback_called).to be(false)
|
|
387
|
+
ensure
|
|
388
|
+
described_class.configure do |c|
|
|
389
|
+
c.on_give_up = original_on_give_up
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
it "does not call on_give_up when explicitly set to nil" do
|
|
395
|
+
callback_called = false
|
|
396
|
+
original_on_give_up = described_class.config.on_give_up
|
|
397
|
+
|
|
398
|
+
begin
|
|
399
|
+
described_class.configure do |c|
|
|
400
|
+
c.on_give_up = proc { callback_called = true }
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
expect do
|
|
404
|
+
described_class.retriable(on_give_up: nil, tries: 1) { increment_tries_with_exception }
|
|
405
|
+
end.to raise_error(StandardError)
|
|
406
|
+
|
|
407
|
+
expect(callback_called).to be(false)
|
|
408
|
+
ensure
|
|
409
|
+
described_class.configure do |c|
|
|
410
|
+
c.on_give_up = original_on_give_up
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
it "calls on_retry before on_give_up when giving up" do
|
|
416
|
+
events = []
|
|
417
|
+
|
|
418
|
+
expect do
|
|
419
|
+
described_class.retriable(
|
|
420
|
+
tries: 1,
|
|
421
|
+
on_retry: proc { events << :on_retry },
|
|
422
|
+
on_give_up: proc { events << :on_give_up },
|
|
423
|
+
) do
|
|
424
|
+
increment_tries_with_exception
|
|
425
|
+
end
|
|
426
|
+
end.to raise_error(StandardError)
|
|
427
|
+
|
|
428
|
+
expect(events).to eq(%i[on_retry on_give_up])
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
it "propagates exceptions raised inside on_give_up, replacing the original exception" do
|
|
432
|
+
handler = proc { raise "handler exploded" }
|
|
433
|
+
|
|
434
|
+
expect do
|
|
435
|
+
described_class.retriable(tries: 1, on_give_up: handler) { increment_tries_with_exception }
|
|
436
|
+
end.to raise_error(RuntimeError, "handler exploded")
|
|
437
|
+
|
|
438
|
+
expect(@tries).to eq(1)
|
|
439
|
+
end
|
|
440
|
+
|
|
134
441
|
context "with rand_factor 0.0 and an on_retry handler" do
|
|
135
442
|
let(:tries) { 6 }
|
|
136
443
|
let(:no_rand_timetable) { { 1 => 0.5, 2 => 0.75, 3 => 1.125 } }
|
|
@@ -210,6 +517,19 @@ describe Retriable do
|
|
|
210
517
|
end
|
|
211
518
|
end
|
|
212
519
|
|
|
520
|
+
context "with a Set :on parameter" do
|
|
521
|
+
it "retries each exception class in the Set" do
|
|
522
|
+
described_class.retriable(on: Set[StandardError, NonStandardError]) do
|
|
523
|
+
increment_tries
|
|
524
|
+
|
|
525
|
+
raise StandardError if @tries == 1
|
|
526
|
+
raise NonStandardError if @tries == 2
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
expect(@tries).to eq(3)
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
|
|
213
533
|
context "with a hash :on parameter" do
|
|
214
534
|
let(:on_hash) { { NonStandardError => /NonStandardError occurred/ } }
|
|
215
535
|
|
|
@@ -242,6 +562,20 @@ describe Retriable do
|
|
|
242
562
|
expect(@tries).to eq(1)
|
|
243
563
|
end
|
|
244
564
|
|
|
565
|
+
it "does not call on_give_up when exception class matches but message does not" do
|
|
566
|
+
callback_called = false
|
|
567
|
+
|
|
568
|
+
expect do
|
|
569
|
+
described_class.retriable(on: on_hash, on_give_up: proc { callback_called = true }) do
|
|
570
|
+
increment_tries
|
|
571
|
+
raise SecondNonStandardError, "not a match"
|
|
572
|
+
end
|
|
573
|
+
end.to raise_error(SecondNonStandardError, /not a match/)
|
|
574
|
+
|
|
575
|
+
expect(callback_called).to be(false)
|
|
576
|
+
expect(@tries).to eq(1)
|
|
577
|
+
end
|
|
578
|
+
|
|
245
579
|
it "successfully retries when the values are arrays of exception message patterns" do
|
|
246
580
|
exceptions = []
|
|
247
581
|
handler = ->(exception, try, _elapsed_time, _next_interval) { exceptions[try] = exception }
|
|
@@ -375,6 +709,47 @@ describe Retriable do
|
|
|
375
709
|
it "raises ArgumentError on invalid options" do
|
|
376
710
|
expect { described_class.retriable(does_not_exist: 123) { increment_tries } }.to raise_error(ArgumentError)
|
|
377
711
|
end
|
|
712
|
+
|
|
713
|
+
it "raises ArgumentError when tries is not a positive integer" do
|
|
714
|
+
expect { described_class.retriable(tries: 1.5) { increment_tries } }
|
|
715
|
+
.to raise_error(ArgumentError, /tries/)
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
it "raises ArgumentError when an interval is negative" do
|
|
719
|
+
expect { described_class.retriable(intervals: [-1]) { increment_tries } }
|
|
720
|
+
.to raise_error(ArgumentError, /intervals/)
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
it "raises ArgumentError when configured timing options become invalid" do
|
|
724
|
+
described_class.configure { |config| config.tries = 0 }
|
|
725
|
+
|
|
726
|
+
expect { described_class.retriable { increment_tries } }
|
|
727
|
+
.to raise_error(ArgumentError, /tries/)
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
it "does not validate generated backoff options when intervals are provided" do
|
|
731
|
+
described_class.retriable(intervals: [0], tries: 0, rand_factor: 1.1) { increment_tries }
|
|
732
|
+
|
|
733
|
+
expect(@tries).to eq(1)
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
it "allows an empty interval array as one attempt" do
|
|
737
|
+
expect do
|
|
738
|
+
described_class.retriable(intervals: []) { increment_tries_with_exception }
|
|
739
|
+
end.to raise_error(StandardError)
|
|
740
|
+
|
|
741
|
+
expect(@tries).to eq(1)
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
it "rejects on: Object before invoking the block" do
|
|
745
|
+
block_invoked = false
|
|
746
|
+
|
|
747
|
+
expect do
|
|
748
|
+
described_class.retriable(on: Object) { block_invoked = true }
|
|
749
|
+
end.to raise_error(ArgumentError, /on must be an Exception class/)
|
|
750
|
+
|
|
751
|
+
expect(block_invoked).to be(false)
|
|
752
|
+
end
|
|
378
753
|
end
|
|
379
754
|
|
|
380
755
|
context "#configure" do
|
|
@@ -384,8 +759,7 @@ describe Retriable do
|
|
|
384
759
|
with_context
|
|
385
760
|
configure
|
|
386
761
|
config
|
|
387
|
-
|
|
388
|
-
reset_override
|
|
762
|
+
with_override
|
|
389
763
|
]
|
|
390
764
|
|
|
391
765
|
expect(described_class.singleton_methods(false)).to match_array(public_api_methods)
|
|
@@ -396,25 +770,55 @@ describe Retriable do
|
|
|
396
770
|
end
|
|
397
771
|
end
|
|
398
772
|
|
|
399
|
-
context "#
|
|
400
|
-
|
|
401
|
-
described_class.
|
|
773
|
+
context "#retriable tries/intervals precedence" do
|
|
774
|
+
it "lets a per-call tries clear globally configured intervals" do
|
|
775
|
+
described_class.configure { |c| c.intervals = [0.5, 1.0] }
|
|
776
|
+
|
|
777
|
+
expect do
|
|
778
|
+
described_class.retriable(tries: 1) { increment_tries_with_exception }
|
|
779
|
+
end.to raise_error(StandardError)
|
|
780
|
+
|
|
781
|
+
expect(@tries).to eq(1)
|
|
402
782
|
end
|
|
403
783
|
|
|
784
|
+
it "still lets per-call intervals win when both intervals and tries are given" do
|
|
785
|
+
expect do
|
|
786
|
+
described_class.retriable(intervals: [0.5, 1.0], tries: 1) { increment_tries_with_exception }
|
|
787
|
+
end.to raise_error(StandardError)
|
|
788
|
+
|
|
789
|
+
expect(@tries).to eq(3) # intervals.size + 1
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
it "lets a with_context tries clear context intervals" do
|
|
793
|
+
described_class.configure do |c|
|
|
794
|
+
c.contexts[:api] = { intervals: [0.5, 1.0] }
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
expect do
|
|
798
|
+
described_class.with_context(:api, tries: 1) { increment_tries_with_exception }
|
|
799
|
+
end.to raise_error(StandardError)
|
|
800
|
+
|
|
801
|
+
expect(@tries).to eq(1)
|
|
802
|
+
end
|
|
803
|
+
end
|
|
804
|
+
|
|
805
|
+
context "#with_override" do
|
|
404
806
|
it "takes precedence over both global config and local options" do
|
|
405
807
|
described_class.configure { |c| c.tries = 2 }
|
|
406
|
-
described_class.override(tries: 1)
|
|
407
808
|
|
|
408
|
-
|
|
809
|
+
described_class.with_override(tries: 1) do
|
|
810
|
+
expect { described_class.retriable(tries: 10) { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
811
|
+
end
|
|
812
|
+
|
|
409
813
|
expect(@tries).to eq(1)
|
|
410
814
|
end
|
|
411
815
|
|
|
412
816
|
it "lets override tries take precedence over local intervals" do
|
|
413
|
-
described_class.
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
end
|
|
817
|
+
described_class.with_override(tries: 1) do
|
|
818
|
+
expect do
|
|
819
|
+
described_class.retriable(intervals: [0.5, 1.0]) { increment_tries_with_exception }
|
|
820
|
+
end.to raise_error(StandardError)
|
|
821
|
+
end
|
|
418
822
|
|
|
419
823
|
expect(@tries).to eq(1)
|
|
420
824
|
end
|
|
@@ -423,9 +827,11 @@ describe Retriable do
|
|
|
423
827
|
described_class.configure do |c|
|
|
424
828
|
c.contexts[:api] = { intervals: [0.5, 1.0] }
|
|
425
829
|
end
|
|
426
|
-
described_class.override(tries: 1)
|
|
427
830
|
|
|
428
|
-
|
|
831
|
+
described_class.with_override(tries: 1) do
|
|
832
|
+
expect { described_class.with_context(:api) { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
833
|
+
end
|
|
834
|
+
|
|
429
835
|
expect(@tries).to eq(1)
|
|
430
836
|
end
|
|
431
837
|
|
|
@@ -433,31 +839,34 @@ describe Retriable do
|
|
|
433
839
|
described_class.configure do |c|
|
|
434
840
|
c.contexts[:api] = { intervals: [0.5, 1.0] }
|
|
435
841
|
end
|
|
436
|
-
described_class.override(contexts: { api: { tries: 1 } })
|
|
437
842
|
|
|
438
|
-
|
|
843
|
+
described_class.with_override(contexts: { api: { tries: 1 } }) do
|
|
844
|
+
expect { described_class.with_context(:api) { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
845
|
+
end
|
|
846
|
+
|
|
439
847
|
expect(@tries).to eq(1)
|
|
440
848
|
end
|
|
441
849
|
|
|
442
850
|
it "replaces hash-valued options instead of deep-merging them" do
|
|
443
|
-
described_class.
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
end
|
|
851
|
+
described_class.with_override(on: { NonStandardError => nil }) do
|
|
852
|
+
expect do
|
|
853
|
+
described_class.retriable(on: { StandardError => nil }, tries: 2) { increment_tries_with_exception }
|
|
854
|
+
end.to raise_error(StandardError)
|
|
855
|
+
end
|
|
448
856
|
|
|
449
857
|
expect(@tries).to eq(1)
|
|
450
858
|
end
|
|
451
859
|
|
|
452
860
|
it "can override local intervals with nil to use configured backoff" do
|
|
453
861
|
described_class.configure { |c| c.tries = 3 }
|
|
454
|
-
described_class.override(intervals: nil)
|
|
455
862
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
863
|
+
described_class.with_override(intervals: nil) do
|
|
864
|
+
expect do
|
|
865
|
+
described_class.retriable(intervals: [0.5, 1.0], on_retry: time_table_handler) do
|
|
866
|
+
increment_tries_with_exception
|
|
867
|
+
end
|
|
868
|
+
end.to raise_error(StandardError)
|
|
869
|
+
end
|
|
461
870
|
|
|
462
871
|
expect(@tries).to eq(3)
|
|
463
872
|
expect(@next_interval_table[1]).to be_between(0.0, 1.0)
|
|
@@ -468,23 +877,26 @@ describe Retriable do
|
|
|
468
877
|
c.contexts[:api] = { tries: 3, base_interval: 1.0 }
|
|
469
878
|
end
|
|
470
879
|
|
|
471
|
-
described_class.
|
|
880
|
+
described_class.with_override(contexts: { api: { tries: 1 } }) do
|
|
881
|
+
described_class.with_context(:api, tries: 10) { increment_tries }
|
|
882
|
+
end
|
|
472
883
|
|
|
473
|
-
described_class.with_context(:api, tries: 10) { increment_tries }
|
|
474
884
|
expect(@tries).to eq(1)
|
|
475
885
|
end
|
|
476
886
|
|
|
477
887
|
it "can define a context only in override config" do
|
|
478
|
-
described_class.
|
|
888
|
+
described_class.with_override(contexts: { test_only: { tries: 1 } }) do
|
|
889
|
+
described_class.with_context(:test_only) { increment_tries }
|
|
890
|
+
end
|
|
479
891
|
|
|
480
|
-
described_class.with_context(:test_only) { increment_tries }
|
|
481
892
|
expect(@tries).to eq(1)
|
|
482
893
|
end
|
|
483
894
|
|
|
484
895
|
it "does not apply context-only overrides to plain retriable calls" do
|
|
485
|
-
described_class.
|
|
896
|
+
described_class.with_override(contexts: { api: { tries: 1 } }) do
|
|
897
|
+
expect { described_class.retriable(tries: 3) { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
898
|
+
end
|
|
486
899
|
|
|
487
|
-
expect { described_class.retriable(tries: 3) { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
488
900
|
expect(@tries).to eq(3)
|
|
489
901
|
end
|
|
490
902
|
|
|
@@ -492,21 +904,24 @@ describe Retriable do
|
|
|
492
904
|
described_class.configure do |c|
|
|
493
905
|
c.contexts[:api] = { tries: 3, on: NonStandardError }
|
|
494
906
|
end
|
|
495
|
-
described_class.override(tries: 1)
|
|
496
907
|
|
|
497
|
-
|
|
498
|
-
.
|
|
908
|
+
described_class.with_override(tries: 1) do
|
|
909
|
+
expect { described_class.with_context(:api) { increment_tries_with_exception(NonStandardError) } }
|
|
910
|
+
.to raise_error(NonStandardError)
|
|
911
|
+
end
|
|
912
|
+
|
|
499
913
|
expect(@tries).to eq(1)
|
|
500
914
|
end
|
|
501
915
|
|
|
502
916
|
it "combines local options with override-only contexts" do
|
|
503
|
-
described_class.
|
|
917
|
+
described_class.with_override(contexts: { api: { tries: 1 } }) do
|
|
918
|
+
expect do
|
|
919
|
+
described_class.with_context(:api, on: NonStandardError) do
|
|
920
|
+
increment_tries_with_exception(NonStandardError)
|
|
921
|
+
end
|
|
922
|
+
end.to raise_error(NonStandardError)
|
|
923
|
+
end
|
|
504
924
|
|
|
505
|
-
expect do
|
|
506
|
-
described_class.with_context(:api, on: NonStandardError) do
|
|
507
|
-
increment_tries_with_exception(NonStandardError)
|
|
508
|
-
end
|
|
509
|
-
end.to raise_error(NonStandardError)
|
|
510
925
|
expect(@tries).to eq(1)
|
|
511
926
|
end
|
|
512
927
|
|
|
@@ -515,23 +930,23 @@ describe Retriable do
|
|
|
515
930
|
c.contexts[:api] = { tries: 1 }
|
|
516
931
|
end
|
|
517
932
|
|
|
518
|
-
described_class.
|
|
933
|
+
described_class.with_override(tries: 1) do
|
|
934
|
+
described_class.with_context(:api) { increment_tries }
|
|
935
|
+
end
|
|
519
936
|
|
|
520
|
-
described_class.with_context(:api) { increment_tries }
|
|
521
937
|
expect(@tries).to eq(1)
|
|
522
938
|
end
|
|
523
939
|
|
|
524
940
|
it "treats non-hash configured contexts as empty when override contexts are hash" do
|
|
525
|
-
|
|
526
|
-
described_class.configure { |c| c.contexts = nil }
|
|
527
|
-
|
|
528
|
-
described_class.override(contexts: { api: { tries: 1 } })
|
|
941
|
+
described_class.configure { |c| c.contexts = nil }
|
|
529
942
|
|
|
943
|
+
described_class.with_override(contexts: { api: { tries: 1 } }) do
|
|
530
944
|
described_class.with_context(:api) { increment_tries }
|
|
531
|
-
expect(@tries).to eq(1)
|
|
532
|
-
ensure
|
|
533
|
-
described_class.configure { |c| c.contexts = {} }
|
|
534
945
|
end
|
|
946
|
+
|
|
947
|
+
expect(@tries).to eq(1)
|
|
948
|
+
ensure
|
|
949
|
+
described_class.configure { |c| c.contexts = {} }
|
|
535
950
|
end
|
|
536
951
|
|
|
537
952
|
it "ignores nil override contexts values in with_context" do
|
|
@@ -539,31 +954,54 @@ describe Retriable do
|
|
|
539
954
|
c.contexts[:api] = { tries: 1 }
|
|
540
955
|
end
|
|
541
956
|
|
|
542
|
-
described_class.
|
|
957
|
+
described_class.with_override(contexts: nil) do
|
|
958
|
+
described_class.with_context(:api) { increment_tries }
|
|
959
|
+
end
|
|
543
960
|
|
|
544
|
-
described_class.with_context(:api) { increment_tries }
|
|
545
961
|
expect(@tries).to eq(1)
|
|
546
962
|
end
|
|
547
963
|
|
|
548
|
-
it "
|
|
549
|
-
|
|
550
|
-
c.contexts[:api] = { tries: 1 }
|
|
551
|
-
end
|
|
964
|
+
it "raises ArgumentError on non-hash override contexts values" do
|
|
965
|
+
block_called = false
|
|
552
966
|
|
|
553
|
-
described_class.
|
|
967
|
+
expect { described_class.with_override(contexts: 123) { block_called = true } }
|
|
968
|
+
.to raise_error(ArgumentError, /contexts must be a Hash or nil/)
|
|
969
|
+
expect(block_called).to be(false)
|
|
970
|
+
end
|
|
554
971
|
|
|
555
|
-
|
|
556
|
-
|
|
972
|
+
it "raises ArgumentError on non-hash per-context override values" do
|
|
973
|
+
block_called = false
|
|
974
|
+
|
|
975
|
+
expect { described_class.with_override(contexts: { api: 123 }) { block_called = true } }
|
|
976
|
+
.to raise_error(ArgumentError, /contexts\[:api\] must be a Hash/)
|
|
977
|
+
expect(block_called).to be(false)
|
|
978
|
+
end
|
|
979
|
+
|
|
980
|
+
it "preserves outer override after rejected nested override contexts values" do
|
|
981
|
+
described_class.with_override(tries: 2) do
|
|
982
|
+
expect { described_class.with_override(tries: 1, contexts: 123) { :noop } }
|
|
983
|
+
.to raise_error(ArgumentError, /contexts must be a Hash or nil/)
|
|
984
|
+
|
|
985
|
+
expect { described_class.retriable(tries: 10) { increment_tries_with_exception } }
|
|
986
|
+
.to raise_error(StandardError)
|
|
987
|
+
end
|
|
988
|
+
|
|
989
|
+
expect(@tries).to eq(2)
|
|
557
990
|
end
|
|
558
991
|
|
|
559
|
-
it "
|
|
992
|
+
it "preserves outer context override after rejected nested per-context values" do
|
|
560
993
|
described_class.configure do |c|
|
|
561
|
-
c.contexts[:api] = { tries:
|
|
994
|
+
c.contexts[:api] = { tries: 10 }
|
|
562
995
|
end
|
|
563
996
|
|
|
564
|
-
described_class.
|
|
997
|
+
described_class.with_override(contexts: { api: { tries: 2 } }) do
|
|
998
|
+
expect { described_class.with_override(contexts: { api: 123 }) { :noop } }
|
|
999
|
+
.to raise_error(ArgumentError, /contexts\[:api\] must be a Hash/)
|
|
1000
|
+
|
|
1001
|
+
expect { described_class.with_context(:api) { increment_tries_with_exception } }
|
|
1002
|
+
.to raise_error(StandardError)
|
|
1003
|
+
end
|
|
565
1004
|
|
|
566
|
-
expect { described_class.with_context(:api) { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
567
1005
|
expect(@tries).to eq(2)
|
|
568
1006
|
end
|
|
569
1007
|
|
|
@@ -572,10 +1010,10 @@ describe Retriable do
|
|
|
572
1010
|
c.contexts[:configured] = { tries: 2 }
|
|
573
1011
|
end
|
|
574
1012
|
|
|
575
|
-
described_class.
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
1013
|
+
described_class.with_override(contexts: { override_only: { tries: 1 } }) do
|
|
1014
|
+
expect { described_class.with_context(:missing) { increment_tries } }
|
|
1015
|
+
.to raise_error(ArgumentError, /override_only/)
|
|
1016
|
+
end
|
|
579
1017
|
end
|
|
580
1018
|
|
|
581
1019
|
it "does not snapshot configured contexts when adding override-only contexts" do
|
|
@@ -583,37 +1021,225 @@ describe Retriable do
|
|
|
583
1021
|
c.contexts[:api] = { tries: 2 }
|
|
584
1022
|
end
|
|
585
1023
|
|
|
586
|
-
described_class.
|
|
1024
|
+
described_class.with_override(contexts: { test_only: { tries: 1 } }) do
|
|
1025
|
+
described_class.configure do |c|
|
|
1026
|
+
c.contexts[:api] = { tries: 5 }
|
|
1027
|
+
end
|
|
587
1028
|
|
|
588
|
-
|
|
589
|
-
c.contexts[:api] = { tries: 5 }
|
|
1029
|
+
expect { described_class.with_context(:api) { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
590
1030
|
end
|
|
591
1031
|
|
|
592
|
-
expect { described_class.with_context(:api) { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
593
1032
|
expect(@tries).to eq(5)
|
|
594
1033
|
end
|
|
595
1034
|
|
|
596
1035
|
it "raises ArgumentError on invalid override options" do
|
|
597
|
-
expect { described_class.
|
|
1036
|
+
expect { described_class.with_override(does_not_exist: 123) { :noop } }.to raise_error(ArgumentError)
|
|
598
1037
|
end
|
|
599
1038
|
|
|
600
1039
|
it "raises ArgumentError on empty override options" do
|
|
601
|
-
expect { described_class.
|
|
1040
|
+
expect { described_class.with_override({}) { :noop } }.to raise_error(ArgumentError, /empty override/)
|
|
1041
|
+
end
|
|
1042
|
+
|
|
1043
|
+
it "raises ArgumentError when called without a block" do
|
|
1044
|
+
expect { described_class.with_override(tries: 1) }.to raise_error(ArgumentError, /requires a block/)
|
|
602
1045
|
end
|
|
603
1046
|
|
|
604
1047
|
it "raises ArgumentError on invalid context override options" do
|
|
605
|
-
expect { described_class.
|
|
1048
|
+
expect { described_class.with_override(contexts: { api: { does_not_exist: 123 } }) { :noop } }
|
|
606
1049
|
.to raise_error(ArgumentError, /does_not_exist is not a valid option/)
|
|
607
1050
|
end
|
|
608
1051
|
|
|
609
|
-
it "
|
|
610
|
-
|
|
611
|
-
|
|
1052
|
+
it "clears the override after the block returns" do
|
|
1053
|
+
described_class.with_override(tries: 1) do
|
|
1054
|
+
# active here
|
|
1055
|
+
end
|
|
612
1056
|
|
|
613
|
-
|
|
1057
|
+
expect { described_class.retriable(tries: 3) { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
1058
|
+
expect(@tries).to eq(3)
|
|
1059
|
+
end
|
|
614
1060
|
|
|
615
|
-
|
|
616
|
-
expect
|
|
1061
|
+
it "clears the override when the block raises" do
|
|
1062
|
+
expect do
|
|
1063
|
+
described_class.with_override(tries: 1) { raise "boom" }
|
|
1064
|
+
end.to raise_error(RuntimeError, "boom")
|
|
1065
|
+
|
|
1066
|
+
expect { described_class.retriable(tries: 3) { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
1067
|
+
expect(@tries).to eq(3)
|
|
1068
|
+
end
|
|
1069
|
+
|
|
1070
|
+
it "returns the block's return value" do
|
|
1071
|
+
result = described_class.with_override(tries: 1) { :return_value }
|
|
1072
|
+
expect(result).to eq(:return_value)
|
|
1073
|
+
end
|
|
1074
|
+
|
|
1075
|
+
it "restores the outer override when nested blocks exit" do
|
|
1076
|
+
tries_seen = []
|
|
1077
|
+
handler = ->(_exception, try, _elapsed, _next) { tries_seen << [Thread.current.object_id, try] }
|
|
1078
|
+
|
|
1079
|
+
described_class.with_override(tries: 2, on_retry: handler) do
|
|
1080
|
+
described_class.with_override(tries: 4, on_retry: handler) do
|
|
1081
|
+
expect { described_class.retriable { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
1082
|
+
end
|
|
1083
|
+
|
|
1084
|
+
# After the inner block exits, the outer tries: 2 override is restored.
|
|
1085
|
+
@tries = 0
|
|
1086
|
+
expect { described_class.retriable { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
1087
|
+
expect(@tries).to eq(2)
|
|
1088
|
+
end
|
|
1089
|
+
end
|
|
1090
|
+
end
|
|
1091
|
+
|
|
1092
|
+
context "#with_override thread safety" do
|
|
1093
|
+
# Coordinate threads with queues rather than sleep so tests are deterministic.
|
|
1094
|
+
# sleep_disabled is already set to true in the top-level before(:each), so
|
|
1095
|
+
# retriable calls do not actually sleep between attempts.
|
|
1096
|
+
|
|
1097
|
+
it "isolates overrides between threads" do
|
|
1098
|
+
ready = Queue.new
|
|
1099
|
+
proceed = Queue.new
|
|
1100
|
+
results = {}
|
|
1101
|
+
mutex = Mutex.new
|
|
1102
|
+
|
|
1103
|
+
threads = [1, 2].map do |id|
|
|
1104
|
+
Thread.new do
|
|
1105
|
+
described_class.with_override(tries: id) do
|
|
1106
|
+
ready << true
|
|
1107
|
+
proceed.pop
|
|
1108
|
+
tries = 0
|
|
1109
|
+
begin
|
|
1110
|
+
described_class.retriable do
|
|
1111
|
+
tries += 1
|
|
1112
|
+
raise StandardError
|
|
1113
|
+
end
|
|
1114
|
+
rescue StandardError
|
|
1115
|
+
mutex.synchronize { results[id] = tries }
|
|
1116
|
+
end
|
|
1117
|
+
end
|
|
1118
|
+
end
|
|
1119
|
+
end
|
|
1120
|
+
|
|
1121
|
+
2.times { ready.pop }
|
|
1122
|
+
2.times { proceed << true }
|
|
1123
|
+
threads.each(&:join)
|
|
1124
|
+
|
|
1125
|
+
expect(results).to eq(1 => 1, 2 => 2)
|
|
1126
|
+
end
|
|
1127
|
+
|
|
1128
|
+
it "does not leak an active override into a sibling thread" do
|
|
1129
|
+
override_active = Queue.new
|
|
1130
|
+
sibling_done = Queue.new
|
|
1131
|
+
sibling_tries = nil
|
|
1132
|
+
|
|
1133
|
+
setter = Thread.new do
|
|
1134
|
+
described_class.with_override(tries: 1) do
|
|
1135
|
+
override_active << true
|
|
1136
|
+
sibling_done.pop
|
|
1137
|
+
end
|
|
1138
|
+
end
|
|
1139
|
+
|
|
1140
|
+
sibling = Thread.new do
|
|
1141
|
+
override_active.pop
|
|
1142
|
+
tries = 0
|
|
1143
|
+
begin
|
|
1144
|
+
described_class.retriable(tries: 3) do
|
|
1145
|
+
tries += 1
|
|
1146
|
+
raise StandardError
|
|
1147
|
+
end
|
|
1148
|
+
rescue StandardError
|
|
1149
|
+
sibling_tries = tries
|
|
1150
|
+
end
|
|
1151
|
+
sibling_done << true
|
|
1152
|
+
end
|
|
1153
|
+
|
|
1154
|
+
[setter, sibling].each(&:join)
|
|
1155
|
+
expect(sibling_tries).to eq(3)
|
|
1156
|
+
end
|
|
1157
|
+
|
|
1158
|
+
it "does not propagate an active override to a child thread" do
|
|
1159
|
+
child_tries = nil
|
|
1160
|
+
|
|
1161
|
+
described_class.with_override(tries: 1) do
|
|
1162
|
+
Thread.new do
|
|
1163
|
+
tries = 0
|
|
1164
|
+
begin
|
|
1165
|
+
described_class.retriable(tries: 3) do
|
|
1166
|
+
tries += 1
|
|
1167
|
+
raise StandardError
|
|
1168
|
+
end
|
|
1169
|
+
rescue StandardError
|
|
1170
|
+
child_tries = tries
|
|
1171
|
+
end
|
|
1172
|
+
end.join
|
|
1173
|
+
end
|
|
1174
|
+
|
|
1175
|
+
expect(child_tries).to eq(3)
|
|
1176
|
+
end
|
|
1177
|
+
|
|
1178
|
+
it "shares the active override with fibers in the same thread" do
|
|
1179
|
+
fiber_tries = nil
|
|
1180
|
+
|
|
1181
|
+
Thread.new do
|
|
1182
|
+
described_class.with_override(tries: 1) do
|
|
1183
|
+
Fiber.new do
|
|
1184
|
+
tries = 0
|
|
1185
|
+
begin
|
|
1186
|
+
described_class.retriable(tries: 10) do
|
|
1187
|
+
tries += 1
|
|
1188
|
+
raise StandardError
|
|
1189
|
+
end
|
|
1190
|
+
rescue StandardError
|
|
1191
|
+
fiber_tries = tries
|
|
1192
|
+
end
|
|
1193
|
+
end.resume
|
|
1194
|
+
end
|
|
1195
|
+
end.join
|
|
1196
|
+
|
|
1197
|
+
expect(fiber_tries).to eq(1)
|
|
1198
|
+
end
|
|
1199
|
+
|
|
1200
|
+
it "does not treat a main-thread override as a global default for other threads" do
|
|
1201
|
+
other_thread_tries = nil
|
|
1202
|
+
|
|
1203
|
+
described_class.with_override(tries: 1) do
|
|
1204
|
+
Thread.new do
|
|
1205
|
+
tries = 0
|
|
1206
|
+
begin
|
|
1207
|
+
described_class.retriable(tries: 3) do
|
|
1208
|
+
tries += 1
|
|
1209
|
+
raise StandardError
|
|
1210
|
+
end
|
|
1211
|
+
rescue StandardError
|
|
1212
|
+
other_thread_tries = tries
|
|
1213
|
+
end
|
|
1214
|
+
end.join
|
|
1215
|
+
end
|
|
1216
|
+
|
|
1217
|
+
expect(other_thread_tries).to eq(3)
|
|
1218
|
+
end
|
|
1219
|
+
|
|
1220
|
+
it "applies overridden on_give_up handlers" do
|
|
1221
|
+
callback_called = false
|
|
1222
|
+
|
|
1223
|
+
expect do
|
|
1224
|
+
described_class.with_override(on_give_up: proc { callback_called = true }) do
|
|
1225
|
+
described_class.retriable(tries: 1) { increment_tries_with_exception }
|
|
1226
|
+
end
|
|
1227
|
+
end.to raise_error(StandardError)
|
|
1228
|
+
|
|
1229
|
+
expect(callback_called).to be(true)
|
|
1230
|
+
end
|
|
1231
|
+
|
|
1232
|
+
it "applies on_give_up handlers configured via per-context overrides" do
|
|
1233
|
+
received_reason = nil
|
|
1234
|
+
handler = proc { |_e, _try, _elapsed, _interval, reason| received_reason = reason }
|
|
1235
|
+
|
|
1236
|
+
expect do
|
|
1237
|
+
described_class.with_override(contexts: { api: { tries: 1, on_give_up: handler } }) do
|
|
1238
|
+
described_class.with_context(:api) { increment_tries_with_exception }
|
|
1239
|
+
end
|
|
1240
|
+
end.to raise_error(StandardError)
|
|
1241
|
+
|
|
1242
|
+
expect(received_reason).to eq(:tries_exhausted)
|
|
617
1243
|
end
|
|
618
1244
|
end
|
|
619
1245
|
|
|
@@ -632,8 +1258,15 @@ describe Retriable do
|
|
|
632
1258
|
expect(@tries).to eq(1)
|
|
633
1259
|
end
|
|
634
1260
|
|
|
635
|
-
it "
|
|
636
|
-
expect
|
|
1261
|
+
it "raises an ArgumentError when called without a block" do
|
|
1262
|
+
expect { described_class.with_context(:sql) }
|
|
1263
|
+
.to raise_error(ArgumentError, /with_context requires a block/)
|
|
1264
|
+
expect(@tries).to eq(0)
|
|
1265
|
+
end
|
|
1266
|
+
|
|
1267
|
+
it "checks for a block before looking up the context" do
|
|
1268
|
+
expect { described_class.with_context(:missing) }
|
|
1269
|
+
.to raise_error(ArgumentError, /with_context requires a block/)
|
|
637
1270
|
expect(@tries).to eq(0)
|
|
638
1271
|
end
|
|
639
1272
|
|
|
@@ -673,5 +1306,24 @@ describe Retriable do
|
|
|
673
1306
|
described_class.with_context(:broken) { increment_tries }
|
|
674
1307
|
expect(@tries).to eq(1)
|
|
675
1308
|
end
|
|
1309
|
+
|
|
1310
|
+
it "surfaces an invalid context on any retriable call before that context is used" do
|
|
1311
|
+
described_class.configure { |c| c.contexts[:unused] = { contexts: {} } }
|
|
1312
|
+
|
|
1313
|
+
expect { described_class.retriable { :ok } }
|
|
1314
|
+
.to raise_error(ArgumentError, /contexts is not a valid option/)
|
|
1315
|
+
end
|
|
1316
|
+
|
|
1317
|
+
it "invokes on_give_up configured on a context" do
|
|
1318
|
+
callback_called = false
|
|
1319
|
+
described_class.configure do |c|
|
|
1320
|
+
c.contexts[:flaky] = { tries: 1, on_give_up: proc { callback_called = true } }
|
|
1321
|
+
end
|
|
1322
|
+
|
|
1323
|
+
expect { described_class.with_context(:flaky) { increment_tries_with_exception } }
|
|
1324
|
+
.to raise_error(StandardError)
|
|
1325
|
+
|
|
1326
|
+
expect(callback_called).to be(true)
|
|
1327
|
+
end
|
|
676
1328
|
end
|
|
677
1329
|
end
|