retriable 3.5.0 → 3.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cfd17d793faa48456456036eee71083bf629041cfe6f3f673e3bc92ff0090a88
4
- data.tar.gz: 27946e72249fce2362c2a374c8f352dd27bd8edd4ee3deea61c80e4d0da8f5cd
3
+ metadata.gz: b0cb0bb3751f465d22addceebf5466b165871741e5820c7c3291d2e842f6f48a
4
+ data.tar.gz: 8bef0641e9c3b0e39c04d79ffcbdaf4542790264ce6648f6540db933cfddbc54
5
5
  SHA512:
6
- metadata.gz: 00424ca023aa864fd2a36016138d0dfd2a6a9030f64c7dd427bb296908b244847db139fcad208527f3bcb5cac075c5e0545fd5dd994f4fef48a31ef55baf2939
7
- data.tar.gz: a414ecfe2931a0bf3fb56f831d654c497a5a1b4d37e4dbe1acf1cbd646083f15bbd85271afcf8f5b9b9f6c484c9f4c5b34ef462b35b2ffd7ec4371b50e7769ab
6
+ metadata.gz: 6c4e07023a30aa934d5397717344c480fb6af8dd56349fcbcb9b85b9ba5539a3f16d2b16a30cbb3682e4140a04f3ace9ecbf0602e68bc4a1a5c805230a031de6
7
+ data.tar.gz: 4272451f1213b81537fd8f7cd85ced8a341b8f8dbf228493a42393b74cee3400e90eb9aa466ec3e404b98eef78b2a42ade5600b49c5d8d9ba7dd9e9b4ae1f444
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # HEAD
2
2
 
3
+ ## 3.5.1
4
+
5
+ - Fix: Validate retry timing and count options before use to reject invalid retry configurations. `tries` must now be a positive integer unless a custom `intervals` array is provided.
6
+
3
7
  ## 3.5.0
4
8
 
5
9
  - Fix: Do not count skipped sleep intervals against `max_elapsed_time` when `sleep_disabled` is true.
data/README.md CHANGED
@@ -94,6 +94,8 @@ Here are the available options, in some vague order of relevance to most common
94
94
  | **`intervals`** | `nil` | Skip generated intervals and provide your own array of intervals in seconds. [Read more](#custom-interval-array). |
95
95
  | **`timeout`** | `nil` | Number of seconds to allow the code block to run before raising a `Timeout::Error` inside each try. `nil` means the code block can run forever without raising error. The implementation uses `Timeout::timeout`, which may be [unsafe](https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/) [and](http://blog.headius.com/2008/02/ruby-threadraise-threadkill-timeoutrb.html) [even](https://adamhooper.medium.com/in-ruby-dont-use-timeout-77d9d4e5a001) [dangerous](https://www.mikeperham.com/2015/05/08/timeout-rubys-most-dangerous-api/). Proceed with caution. |
96
96
 
97
+ Timing options are validated before retrying. `tries` must be a positive integer when Retriable generates intervals. `base_interval`, `max_interval`, `multiplier`, `max_elapsed_time`, and `timeout` must be non-negative numbers, with `max_elapsed_time` and `timeout` also accepting `nil`. `rand_factor` must be a number from `0` through `1`. If provided, `intervals` must be an array of non-negative numbers; because it replaces generated intervals, it also overrides `tries`, `base_interval`, `max_interval`, `rand_factor`, and `multiplier` validation.
98
+
97
99
  #### Configuring Which Options to Retry With :on
98
100
 
99
101
  **`:on`** Can take the form:
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "exponential_backoff"
4
+ require_relative "validation"
4
5
 
5
6
  module Retriable
6
7
  class Config
8
+ include Validation
9
+
7
10
  ATTRIBUTES = (ExponentialBackoff::ATTRIBUTES + %i[
8
11
  sleep_disabled
9
12
  max_elapsed_time
@@ -39,6 +42,8 @@ module Retriable
39
42
 
40
43
  instance_variable_set(:"@#{k}", v)
41
44
  end
45
+
46
+ validate!
42
47
  end
43
48
 
44
49
  def to_h
@@ -46,5 +51,28 @@ module Retriable
46
51
  hash[key] = public_send(key)
47
52
  end
48
53
  end
54
+
55
+ def validate!
56
+ validate_optional_non_negative_number(:max_elapsed_time, max_elapsed_time)
57
+ validate_optional_non_negative_number(:timeout, timeout)
58
+ validate_intervals
59
+ return if intervals
60
+
61
+ validate_positive_integer(:tries, tries)
62
+ validate_non_negative_number(:base_interval, base_interval)
63
+ validate_non_negative_number(:multiplier, multiplier)
64
+ validate_non_negative_number(:max_interval, max_interval)
65
+ validate_rand_factor
66
+ end
67
+
68
+ private
69
+
70
+ def validate_intervals
71
+ return if intervals.nil?
72
+ raise ArgumentError, "intervals must be an Array" unless intervals.is_a?(Array)
73
+ return if intervals.all? { |interval| finite_number?(interval) && interval >= 0 }
74
+
75
+ raise ArgumentError, "intervals must contain only non-negative numbers"
76
+ end
49
77
  end
50
78
  end
@@ -1,7 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "validation"
4
+
3
5
  module Retriable
4
6
  class ExponentialBackoff
7
+ include Validation
8
+
5
9
  ATTRIBUTES = %i[
6
10
  tries
7
11
  base_interval
@@ -24,6 +28,8 @@ module Retriable
24
28
 
25
29
  instance_variable_set(:"@#{k}", v)
26
30
  end
31
+
32
+ validate!
27
33
  end
28
34
 
29
35
  def intervals
@@ -38,6 +44,14 @@ module Retriable
38
44
 
39
45
  private
40
46
 
47
+ def validate!
48
+ validate_non_negative_integer(:tries, tries)
49
+ validate_non_negative_number(:base_interval, base_interval)
50
+ validate_non_negative_number(:multiplier, multiplier)
51
+ validate_non_negative_number(:max_interval, max_interval)
52
+ validate_rand_factor
53
+ end
54
+
41
55
  def randomize(interval)
42
56
  delta = rand_factor * interval.to_f
43
57
  min = interval - delta
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Retriable
4
+ module Validation
5
+ private
6
+
7
+ def validate_positive_integer(name, value)
8
+ return if value.is_a?(Integer) && value.positive?
9
+
10
+ raise ArgumentError, "#{name} must be a positive integer"
11
+ end
12
+
13
+ def validate_non_negative_integer(name, value)
14
+ return if value.is_a?(Integer) && value >= 0
15
+
16
+ raise ArgumentError, "#{name} must be a non-negative integer"
17
+ end
18
+
19
+ def validate_non_negative_number(name, value)
20
+ return if finite_number?(value) && value >= 0
21
+
22
+ raise ArgumentError, "#{name} must be a non-negative number"
23
+ end
24
+
25
+ def validate_optional_non_negative_number(name, value)
26
+ return if value.nil?
27
+
28
+ validate_non_negative_number(name, value)
29
+ end
30
+
31
+ def validate_rand_factor
32
+ return if finite_number?(rand_factor) && rand_factor >= 0 && rand_factor <= 1
33
+
34
+ raise ArgumentError, "rand_factor must be between 0 and 1"
35
+ end
36
+
37
+ def finite_number?(value)
38
+ value.is_a?(Numeric) && value.to_f.finite?
39
+ end
40
+ end
41
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Retriable
4
- VERSION = "3.5.0"
4
+ VERSION = "3.5.1"
5
5
  end
data/lib/retriable.rb CHANGED
@@ -47,6 +47,9 @@ module Retriable
47
47
  Config.new(apply_override_options(config.to_h.merge(opts), @override_config))
48
48
  end
49
49
 
50
+ # Config is mutable through `configure`, so validate again immediately before use.
51
+ local_config.validate!
52
+
50
53
  tries = local_config.tries
51
54
  intervals = build_intervals(local_config, tries)
52
55
  timeout = local_config.timeout
data/spec/config_spec.rb CHANGED
@@ -56,4 +56,13 @@ describe Retriable::Config do
56
56
  it "raises errors on invalid configuration" do
57
57
  expect { described_class.new(does_not_exist: 123) }.to raise_error(ArgumentError, /not a valid option/)
58
58
  end
59
+
60
+ it "raises errors on invalid timing configuration" do
61
+ expect { described_class.new(rand_factor: 1.1) }.to raise_error(ArgumentError, /rand_factor/)
62
+ expect { described_class.new(timeout: -1) }.to raise_error(ArgumentError, /timeout/)
63
+ end
64
+
65
+ it "raises errors when intervals is not an array" do
66
+ expect { described_class.new(intervals: "1") }.to raise_error(ArgumentError, /intervals must be an Array/)
67
+ end
59
68
  end
@@ -375,6 +375,37 @@ describe Retriable do
375
375
  it "raises ArgumentError on invalid options" do
376
376
  expect { described_class.retriable(does_not_exist: 123) { increment_tries } }.to raise_error(ArgumentError)
377
377
  end
378
+
379
+ it "raises ArgumentError when tries is not a positive integer" do
380
+ expect { described_class.retriable(tries: 1.5) { increment_tries } }
381
+ .to raise_error(ArgumentError, /tries/)
382
+ end
383
+
384
+ it "raises ArgumentError when an interval is negative" do
385
+ expect { described_class.retriable(intervals: [-1]) { increment_tries } }
386
+ .to raise_error(ArgumentError, /intervals/)
387
+ end
388
+
389
+ it "raises ArgumentError when configured timing options become invalid" do
390
+ described_class.configure { |config| config.tries = 0 }
391
+
392
+ expect { described_class.retriable { increment_tries } }
393
+ .to raise_error(ArgumentError, /tries/)
394
+ end
395
+
396
+ it "does not validate generated backoff options when intervals are provided" do
397
+ described_class.retriable(intervals: [0], tries: 0, rand_factor: 1.1) { increment_tries }
398
+
399
+ expect(@tries).to eq(1)
400
+ end
401
+
402
+ it "allows an empty interval array as one attempt" do
403
+ expect do
404
+ described_class.retriable(intervals: []) { increment_tries_with_exception }
405
+ end.to raise_error(StandardError)
406
+
407
+ expect(@tries).to eq(1)
408
+ end
378
409
  end
379
410
 
380
411
  context "#configure" do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: retriable
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.5.0
4
+ version: 3.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jack Chu
@@ -78,6 +78,7 @@ files:
78
78
  - lib/retriable/config.rb
79
79
  - lib/retriable/core_ext/kernel.rb
80
80
  - lib/retriable/exponential_backoff.rb
81
+ - lib/retriable/validation.rb
81
82
  - lib/retriable/version.rb
82
83
  - retriable.gemspec
83
84
  - sig/retriable.rbs