retriable 3.8.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +38 -9
- data/.hound.yml +1 -1
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +89 -0
- data/Gemfile +4 -1
- data/README.md +105 -50
- data/lib/retriable/config.rb +25 -57
- data/lib/retriable/core_ext/kernel.rb +6 -4
- data/lib/retriable/exponential_backoff.rb +13 -5
- data/lib/retriable/validation.rb +11 -7
- data/lib/retriable/version.rb +1 -1
- data/lib/retriable.rb +55 -31
- data/retriable.gemspec +2 -7
- data/sig/retriable.rbs +29 -1
- data/spec/config_spec.rb +63 -97
- data/spec/retriable_spec.rb +345 -95
- data/spec/spec_helper.rb +3 -14
- metadata +7 -53
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
|
|
@@ -39,6 +38,49 @@ describe Retriable do
|
|
|
39
38
|
expect { retriable { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
40
39
|
expect(@tries).to eq(3)
|
|
41
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
|
|
42
84
|
end
|
|
43
85
|
|
|
44
86
|
context "#retriable" do
|
|
@@ -51,9 +93,6 @@ describe Retriable do
|
|
|
51
93
|
|
|
52
94
|
it "raises a LocalJumpError if not given a block" do
|
|
53
95
|
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
96
|
end
|
|
58
97
|
|
|
59
98
|
it "stops at first try if the block does not raise an exception" do
|
|
@@ -171,86 +210,8 @@ describe Retriable do
|
|
|
171
210
|
end.to raise_error(ArgumentError, /tries/)
|
|
172
211
|
end
|
|
173
212
|
|
|
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
|
|
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/)
|
|
254
215
|
end
|
|
255
216
|
|
|
256
217
|
it "applies a randomized exponential backoff to each try" do
|
|
@@ -296,6 +257,187 @@ describe Retriable do
|
|
|
296
257
|
end
|
|
297
258
|
end
|
|
298
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
|
+
|
|
299
441
|
context "with rand_factor 0.0 and an on_retry handler" do
|
|
300
442
|
let(:tries) { 6 }
|
|
301
443
|
let(:no_rand_timetable) { { 1 => 0.5, 2 => 0.75, 3 => 1.125 } }
|
|
@@ -375,6 +517,19 @@ describe Retriable do
|
|
|
375
517
|
end
|
|
376
518
|
end
|
|
377
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
|
+
|
|
378
533
|
context "with a hash :on parameter" do
|
|
379
534
|
let(:on_hash) { { NonStandardError => /NonStandardError occurred/ } }
|
|
380
535
|
|
|
@@ -407,6 +562,20 @@ describe Retriable do
|
|
|
407
562
|
expect(@tries).to eq(1)
|
|
408
563
|
end
|
|
409
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
|
+
|
|
410
579
|
it "successfully retries when the values are arrays of exception message patterns" do
|
|
411
580
|
exceptions = []
|
|
412
581
|
handler = ->(exception, try, _elapsed_time, _next_interval) { exceptions[try] = exception }
|
|
@@ -601,6 +770,38 @@ describe Retriable do
|
|
|
601
770
|
end
|
|
602
771
|
end
|
|
603
772
|
|
|
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)
|
|
782
|
+
end
|
|
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
|
+
|
|
604
805
|
context "#with_override" do
|
|
605
806
|
it "takes precedence over both global config and local options" do
|
|
606
807
|
described_class.configure { |c| c.tries = 2 }
|
|
@@ -737,17 +938,15 @@ describe Retriable do
|
|
|
737
938
|
end
|
|
738
939
|
|
|
739
940
|
it "treats non-hash configured contexts as empty when override contexts are hash" do
|
|
740
|
-
|
|
741
|
-
described_class.configure { |c| c.contexts = nil }
|
|
742
|
-
|
|
743
|
-
described_class.with_override(contexts: { api: { tries: 1 } }) do
|
|
744
|
-
described_class.with_context(:api) { increment_tries }
|
|
745
|
-
end
|
|
941
|
+
described_class.configure { |c| c.contexts = nil }
|
|
746
942
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
described_class.configure { |c| c.contexts = {} }
|
|
943
|
+
described_class.with_override(contexts: { api: { tries: 1 } }) do
|
|
944
|
+
described_class.with_context(:api) { increment_tries }
|
|
750
945
|
end
|
|
946
|
+
|
|
947
|
+
expect(@tries).to eq(1)
|
|
948
|
+
ensure
|
|
949
|
+
described_class.configure { |c| c.contexts = {} }
|
|
751
950
|
end
|
|
752
951
|
|
|
753
952
|
it "ignores nil override contexts values in with_context" do
|
|
@@ -1017,6 +1216,31 @@ describe Retriable do
|
|
|
1017
1216
|
|
|
1018
1217
|
expect(other_thread_tries).to eq(3)
|
|
1019
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)
|
|
1243
|
+
end
|
|
1020
1244
|
end
|
|
1021
1245
|
|
|
1022
1246
|
context "#with_context" do
|
|
@@ -1034,8 +1258,15 @@ describe Retriable do
|
|
|
1034
1258
|
expect(@tries).to eq(1)
|
|
1035
1259
|
end
|
|
1036
1260
|
|
|
1037
|
-
it "
|
|
1038
|
-
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/)
|
|
1039
1270
|
expect(@tries).to eq(0)
|
|
1040
1271
|
end
|
|
1041
1272
|
|
|
@@ -1075,5 +1306,24 @@ describe Retriable do
|
|
|
1075
1306
|
described_class.with_context(:broken) { increment_tries }
|
|
1076
1307
|
expect(@tries).to eq(1)
|
|
1077
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
|
|
1078
1328
|
end
|
|
1079
1329
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -1,27 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "simplecov"
|
|
4
|
-
SimpleCov.start
|
|
4
|
+
SimpleCov.start do
|
|
5
|
+
minimum_coverage 95
|
|
6
|
+
end
|
|
5
7
|
|
|
6
8
|
require "pry"
|
|
7
9
|
require_relative "../lib/retriable"
|
|
8
10
|
require_relative "support/exceptions"
|
|
9
11
|
|
|
10
|
-
# Make Retriable's deprecation notices observable to RSpec's
|
|
11
|
-
# `output().to_stderr` matcher. On Ruby 3.0+ the `:deprecated` warning category
|
|
12
|
-
# is suppressed by default, which would hide the notices we want to assert on.
|
|
13
|
-
WARNING_DEPRECATION_SUPPORTED = defined?(Warning) && Warning.respond_to?(:[])
|
|
14
|
-
Warning[:deprecated] = true if WARNING_DEPRECATION_SUPPORTED
|
|
15
|
-
|
|
16
|
-
# Used by deprecation specs that only make sense on Rubies where `Kernel#warn`
|
|
17
|
-
# supports the `category:` keyword (added in Ruby 2.7).
|
|
18
|
-
WARN_CATEGORY_SUPPORTED = WARNING_DEPRECATION_SUPPORTED &&
|
|
19
|
-
Kernel.method(:warn).parameters.include?(%i[key category])
|
|
20
|
-
|
|
21
12
|
RSpec.configure do |config|
|
|
22
13
|
config.before(:each) do
|
|
23
14
|
srand(0)
|
|
24
|
-
Retriable::Config.timeout_deprecation_warned = false
|
|
25
|
-
Warning[:deprecated] = true if WARNING_DEPRECATION_SUPPORTED
|
|
26
15
|
end
|
|
27
16
|
end
|
metadata
CHANGED
|
@@ -1,56 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: retriable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 4.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jack Chu
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
12
|
-
- !ruby/object:Gem::Dependency
|
|
13
|
-
name: bundler
|
|
14
|
-
requirement: !ruby/object:Gem::Requirement
|
|
15
|
-
requirements:
|
|
16
|
-
- - ">="
|
|
17
|
-
- !ruby/object:Gem::Version
|
|
18
|
-
version: '0'
|
|
19
|
-
type: :development
|
|
20
|
-
prerelease: false
|
|
21
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
-
requirements:
|
|
23
|
-
- - ">="
|
|
24
|
-
- !ruby/object:Gem::Version
|
|
25
|
-
version: '0'
|
|
26
|
-
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: rspec
|
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
|
29
|
-
requirements:
|
|
30
|
-
- - "~>"
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '3'
|
|
33
|
-
type: :development
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - "~>"
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '3'
|
|
40
|
-
- !ruby/object:Gem::Dependency
|
|
41
|
-
name: listen
|
|
42
|
-
requirement: !ruby/object:Gem::Requirement
|
|
43
|
-
requirements:
|
|
44
|
-
- - "~>"
|
|
45
|
-
- !ruby/object:Gem::Version
|
|
46
|
-
version: '3.1'
|
|
47
|
-
type: :development
|
|
48
|
-
prerelease: false
|
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
-
requirements:
|
|
51
|
-
- - "~>"
|
|
52
|
-
- !ruby/object:Gem::Version
|
|
53
|
-
version: '3.1'
|
|
11
|
+
dependencies: []
|
|
54
12
|
description: Retriable is a simple DSL to retry failed code blocks with randomized
|
|
55
13
|
exponential backoff. This is especially useful when interacting with external APIs/services
|
|
56
14
|
or file system calls.
|
|
@@ -91,7 +49,8 @@ files:
|
|
|
91
49
|
homepage: https://github.com/kamui/retriable
|
|
92
50
|
licenses:
|
|
93
51
|
- MIT
|
|
94
|
-
metadata:
|
|
52
|
+
metadata:
|
|
53
|
+
rubygems_mfa_required: 'true'
|
|
95
54
|
rdoc_options: []
|
|
96
55
|
require_paths:
|
|
97
56
|
- lib
|
|
@@ -99,20 +58,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
99
58
|
requirements:
|
|
100
59
|
- - ">="
|
|
101
60
|
- !ruby/object:Gem::Version
|
|
102
|
-
version:
|
|
61
|
+
version: '3.2'
|
|
103
62
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
104
63
|
requirements:
|
|
105
64
|
- - ">="
|
|
106
65
|
- !ruby/object:Gem::Version
|
|
107
66
|
version: '0'
|
|
108
67
|
requirements: []
|
|
109
|
-
rubygems_version:
|
|
68
|
+
rubygems_version: 3.6.9
|
|
110
69
|
specification_version: 4
|
|
111
70
|
summary: Retriable is a simple DSL to retry failed code blocks with randomized exponential
|
|
112
71
|
backoff
|
|
113
|
-
test_files:
|
|
114
|
-
- spec/config_spec.rb
|
|
115
|
-
- spec/exponential_backoff_spec.rb
|
|
116
|
-
- spec/retriable_spec.rb
|
|
117
|
-
- spec/spec_helper.rb
|
|
118
|
-
- spec/support/exceptions.rb
|
|
72
|
+
test_files: []
|