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