retriable 3.8.0 → 4.0.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.
@@ -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,39 @@ 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
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
42
74
  end
43
75
 
44
76
  context "#retriable" do
@@ -51,9 +83,6 @@ describe Retriable do
51
83
 
52
84
  it "raises a LocalJumpError if not given a block" do
53
85
  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
86
  end
58
87
 
59
88
  it "stops at first try if the block does not raise an exception" do
@@ -171,86 +200,8 @@ describe Retriable do
171
200
  end.to raise_error(ArgumentError, /tries/)
172
201
  end
173
202
 
174
- it "will timeout after 1 second" do
175
- expect do
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
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/)
254
205
  end
255
206
 
256
207
  it "applies a randomized exponential backoff to each try" do
@@ -296,6 +247,187 @@ describe Retriable do
296
247
  end
297
248
  end
298
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
+
299
431
  context "with rand_factor 0.0 and an on_retry handler" do
300
432
  let(:tries) { 6 }
301
433
  let(:no_rand_timetable) { { 1 => 0.5, 2 => 0.75, 3 => 1.125 } }
@@ -375,6 +507,19 @@ describe Retriable do
375
507
  end
376
508
  end
377
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
+
378
523
  context "with a hash :on parameter" do
379
524
  let(:on_hash) { { NonStandardError => /NonStandardError occurred/ } }
380
525
 
@@ -407,6 +552,20 @@ describe Retriable do
407
552
  expect(@tries).to eq(1)
408
553
  end
409
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
+
410
569
  it "successfully retries when the values are arrays of exception message patterns" do
411
570
  exceptions = []
412
571
  handler = ->(exception, try, _elapsed_time, _next_interval) { exceptions[try] = exception }
@@ -737,17 +896,15 @@ describe Retriable do
737
896
  end
738
897
 
739
898
  it "treats non-hash configured contexts as empty when override contexts are hash" do
740
- begin
741
- described_class.configure { |c| c.contexts = nil }
899
+ described_class.configure { |c| c.contexts = nil }
742
900
 
743
- described_class.with_override(contexts: { api: { tries: 1 } }) do
744
- described_class.with_context(:api) { increment_tries }
745
- end
746
-
747
- expect(@tries).to eq(1)
748
- ensure
749
- described_class.configure { |c| c.contexts = {} }
901
+ described_class.with_override(contexts: { api: { tries: 1 } }) do
902
+ described_class.with_context(:api) { increment_tries }
750
903
  end
904
+
905
+ expect(@tries).to eq(1)
906
+ ensure
907
+ described_class.configure { |c| c.contexts = {} }
751
908
  end
752
909
 
753
910
  it "ignores nil override contexts values in with_context" do
@@ -1017,6 +1174,31 @@ describe Retriable do
1017
1174
 
1018
1175
  expect(other_thread_tries).to eq(3)
1019
1176
  end
1177
+
1178
+ it "applies overridden on_give_up handlers" do
1179
+ callback_called = false
1180
+
1181
+ expect do
1182
+ described_class.with_override(on_give_up: proc { callback_called = true }) do
1183
+ described_class.retriable(tries: 1) { increment_tries_with_exception }
1184
+ end
1185
+ end.to raise_error(StandardError)
1186
+
1187
+ expect(callback_called).to be(true)
1188
+ end
1189
+
1190
+ it "applies on_give_up handlers configured via per-context overrides" do
1191
+ received_reason = nil
1192
+ handler = proc { |_e, _try, _elapsed, _interval, reason| received_reason = reason }
1193
+
1194
+ expect do
1195
+ described_class.with_override(contexts: { api: { tries: 1, on_give_up: handler } }) do
1196
+ described_class.with_context(:api) { increment_tries_with_exception }
1197
+ end
1198
+ end.to raise_error(StandardError)
1199
+
1200
+ expect(received_reason).to eq(:tries_exhausted)
1201
+ end
1020
1202
  end
1021
1203
 
1022
1204
  context "#with_context" do
@@ -1075,5 +1257,17 @@ describe Retriable do
1075
1257
  described_class.with_context(:broken) { increment_tries }
1076
1258
  expect(@tries).to eq(1)
1077
1259
  end
1260
+
1261
+ it "invokes on_give_up configured on a context" do
1262
+ callback_called = false
1263
+ described_class.configure do |c|
1264
+ c.contexts[:flaky] = { tries: 1, on_give_up: proc { callback_called = true } }
1265
+ end
1266
+
1267
+ expect { described_class.with_context(:flaky) { increment_tries_with_exception } }
1268
+ .to raise_error(StandardError)
1269
+
1270
+ expect(callback_called).to be(true)
1271
+ end
1078
1272
  end
1079
1273
  end
data/spec/spec_helper.rb CHANGED
@@ -7,21 +7,8 @@ require "pry"
7
7
  require_relative "../lib/retriable"
8
8
  require_relative "support/exceptions"
9
9
 
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
10
  RSpec.configure do |config|
22
11
  config.before(:each) do
23
12
  srand(0)
24
- Retriable::Config.timeout_deprecation_warned = false
25
- Warning[:deprecated] = true if WARNING_DEPRECATION_SUPPORTED
26
13
  end
27
14
  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: 3.8.0
4
+ version: 4.0.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,7 +58,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
99
58
  requirements:
100
59
  - - ">="
101
60
  - !ruby/object:Gem::Version
102
- version: 2.3.0
61
+ version: '3.2'
103
62
  required_rubygems_version: !ruby/object:Gem::Requirement
104
63
  requirements:
105
64
  - - ">="
@@ -110,9 +69,4 @@ rubygems_version: 4.0.3
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: []