retriable 3.4.1 → 3.8.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.
data/spec/config_spec.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "stringio"
4
+
3
5
  describe Retriable::Config do
4
6
  let(:default_config) { described_class.new }
5
7
 
@@ -56,4 +58,189 @@ describe Retriable::Config do
56
58
  it "raises errors on invalid configuration" do
57
59
  expect { described_class.new(does_not_exist: 123) }.to raise_error(ArgumentError, /not a valid option/)
58
60
  end
61
+
62
+ it "raises errors on invalid timing configuration" do
63
+ expect { described_class.new(rand_factor: 1.1) }.to raise_error(ArgumentError, /rand_factor/)
64
+ expect do
65
+ expect { described_class.new(timeout: -1) }.to raise_error(ArgumentError, /timeout/)
66
+ end.to output(/timeout.*deprecated.*Retriable 4\.0/i).to_stderr
67
+ end
68
+
69
+ context "timeout deprecation" do
70
+ it "warns when timeout is configured" do
71
+ expect do
72
+ described_class.new(timeout: 5)
73
+ end.to output(/timeout.*deprecated.*Retriable 4\.0/i).to_stderr
74
+ end
75
+
76
+ it "warns when timeout is set before validation" do
77
+ config = described_class.new
78
+ config.timeout = 5
79
+
80
+ expect do
81
+ config.validate!
82
+ end.to output(/timeout.*deprecated.*Retriable 4\.0/i).to_stderr
83
+ end
84
+
85
+ it "does not warn when timeout is nil" do
86
+ expect do
87
+ described_class.new(timeout: nil)
88
+ end.not_to output.to_stderr
89
+ end
90
+
91
+ it "does not warn when timeout is omitted" do
92
+ expect do
93
+ described_class.new
94
+ end.not_to output.to_stderr
95
+ end
96
+
97
+ it "warns at most once per process" do
98
+ original_stderr = $stderr
99
+ stderr = StringIO.new
100
+ begin
101
+ $stderr = stderr
102
+
103
+ described_class.new(timeout: 5)
104
+ described_class.new(timeout: 5)
105
+
106
+ config = described_class.new
107
+ config.timeout = 5
108
+ config.validate!
109
+ ensure
110
+ $stderr = original_stderr
111
+ end
112
+
113
+ expect(stderr.string.scan("timeout:` option is deprecated").size).to eq(1)
114
+ end
115
+
116
+ it "emits the warning under the :deprecated category when supported", if: WARN_CATEGORY_SUPPORTED do
117
+ captured = []
118
+ allow(Warning).to receive(:warn) do |message, category: nil|
119
+ captured << [message, category]
120
+ end
121
+
122
+ described_class.new(timeout: 5)
123
+
124
+ expect(captured.size).to eq(1)
125
+ message, category = captured.first
126
+ expect(message).to match(/timeout.*deprecated.*Retriable 4\.0/i)
127
+ expect(category).to eq(:deprecated)
128
+ end
129
+
130
+ it "is silenced by Warning[:deprecated] = false", if: WARN_CATEGORY_SUPPORTED do
131
+ original = Warning[:deprecated]
132
+ begin
133
+ Warning[:deprecated] = false
134
+ expect do
135
+ described_class.new(timeout: 5)
136
+ end.not_to output.to_stderr
137
+ ensure
138
+ Warning[:deprecated] = original
139
+ end
140
+ end
141
+
142
+ it "remains armed when silenced via Warning[:deprecated]", if: WARN_CATEGORY_SUPPORTED do
143
+ original = Warning[:deprecated]
144
+ begin
145
+ Warning[:deprecated] = false
146
+ described_class.new(timeout: 5)
147
+ ensure
148
+ Warning[:deprecated] = original
149
+ end
150
+
151
+ expect do
152
+ described_class.new(timeout: 5)
153
+ end.to output(/timeout.*deprecated.*Retriable 4\.0/i).to_stderr
154
+ end
155
+ end
156
+
157
+ it "raises errors when intervals is not an array" do
158
+ expect { described_class.new(intervals: "1") }.to raise_error(ArgumentError, /intervals must be an Array/)
159
+ end
160
+
161
+ it "requires a finite max_elapsed_time when tries is Float::INFINITY" do
162
+ expect { described_class.new(tries: Float::INFINITY, max_elapsed_time: nil) }
163
+ .to raise_error(ArgumentError, /max_elapsed_time must be a finite number/)
164
+ end
165
+
166
+ it "rejects intervals combined with tries: Float::INFINITY" do
167
+ expect do
168
+ described_class.new(
169
+ tries: Float::INFINITY,
170
+ max_elapsed_time: 60,
171
+ intervals: [0.1, 0.2],
172
+ )
173
+ end.to raise_error(ArgumentError, /intervals cannot be used with tries: Float::INFINITY/)
174
+ end
175
+
176
+ it "accepts tries: Float::INFINITY with a finite max_elapsed_time" do
177
+ expect { described_class.new(tries: Float::INFINITY, max_elapsed_time: 60) }
178
+ .not_to raise_error
179
+ end
180
+
181
+ context "on: option validation" do
182
+ it "accepts a single Exception subclass" do
183
+ expect { described_class.new(on: StandardError) }.not_to raise_error
184
+ end
185
+
186
+ it "accepts Exception itself" do
187
+ expect { described_class.new(on: Exception) }.not_to raise_error
188
+ end
189
+
190
+ it "accepts an array of Exception subclasses" do
191
+ expect { described_class.new(on: [StandardError, RuntimeError]) }.not_to raise_error
192
+ end
193
+
194
+ it "accepts a hash with nil pattern values" do
195
+ expect { described_class.new(on: { StandardError => nil }) }.not_to raise_error
196
+ end
197
+
198
+ it "accepts a hash with Regexp pattern values" do
199
+ expect { described_class.new(on: { StandardError => /boom/ }) }.not_to raise_error
200
+ end
201
+
202
+ it "accepts a hash with Array-of-Regexp pattern values" do
203
+ expect { described_class.new(on: { StandardError => [/a/, /b/] }) }.not_to raise_error
204
+ end
205
+
206
+ it "rejects Object as on:" do
207
+ expect { described_class.new(on: Object) }
208
+ .to raise_error(ArgumentError, /on must be an Exception class/)
209
+ end
210
+
211
+ it "rejects Kernel as on:" do
212
+ expect { described_class.new(on: Kernel) }
213
+ .to raise_error(ArgumentError, /on must be an Exception class/)
214
+ end
215
+
216
+ it "rejects an array containing a non-Exception class" do
217
+ expect { described_class.new(on: [StandardError, Kernel]) }
218
+ .to raise_error(ArgumentError, /on must be an Exception class/)
219
+ end
220
+
221
+ it "rejects a hash key that is not an Exception class" do
222
+ expect { described_class.new(on: { Kernel => nil }) }
223
+ .to raise_error(ArgumentError, /on must be an Exception class/)
224
+ end
225
+
226
+ it "rejects a hash value that is a String" do
227
+ expect { described_class.new(on: { StandardError => "boom" }) }
228
+ .to raise_error(ArgumentError, /on\[StandardError\] must be nil, a Regexp, or an Array of Regexps/)
229
+ end
230
+
231
+ it "rejects a hash value that is an Array containing a non-Regexp" do
232
+ expect { described_class.new(on: { StandardError => [/a/, "b"] }) }
233
+ .to raise_error(ArgumentError, /on\[StandardError\] must be nil, a Regexp, or an Array of Regexps/)
234
+ end
235
+
236
+ it "rejects a string passed as on:" do
237
+ expect { described_class.new(on: "StandardError") }
238
+ .to raise_error(ArgumentError, /on must be an Exception class/)
239
+ end
240
+
241
+ it "validates on: even when intervals is provided" do
242
+ expect { described_class.new(intervals: [0.1], on: Object) }
243
+ .to raise_error(ArgumentError, /on must be an Exception class/)
244
+ end
245
+ end
59
246
  end
@@ -22,17 +22,19 @@ describe Retriable::ExponentialBackoff do
22
22
  end
23
23
 
24
24
  it "generates 10 randomized intervals" do
25
- expect(described_class.new(tries: 9).intervals).to eq([
26
- 0.5244067512211441,
27
- 0.9113920238761231,
28
- 1.2406087918999114,
29
- 1.7632403621664823,
30
- 2.338001204738311,
31
- 4.350816718580626,
32
- 5.339852157217869,
33
- 11.889873261212443,
34
- 18.756037881636484,
35
- ])
25
+ expect(described_class.new(tries: 9).intervals).to eq(
26
+ [
27
+ 0.5244067512211441,
28
+ 0.9113920238761231,
29
+ 1.2406087918999114,
30
+ 1.7632403621664823,
31
+ 2.338001204738311,
32
+ 4.350816718580626,
33
+ 5.339852157217869,
34
+ 11.889873261212443,
35
+ 18.756037881636484,
36
+ ],
37
+ )
36
38
  end
37
39
 
38
40
  it "generates defined number of intervals" do
@@ -40,19 +42,23 @@ describe Retriable::ExponentialBackoff do
40
42
  end
41
43
 
42
44
  it "generates intervals with a defined base interval" do
43
- expect(described_class.new(base_interval: 1).intervals).to eq([
44
- 1.0488135024422882,
45
- 1.8227840477522461,
46
- 2.4812175837998227,
47
- ])
45
+ expect(described_class.new(base_interval: 1).intervals).to eq(
46
+ [
47
+ 1.0488135024422882,
48
+ 1.8227840477522461,
49
+ 2.4812175837998227,
50
+ ],
51
+ )
48
52
  end
49
53
 
50
54
  it "generates intervals with a defined multiplier" do
51
- expect(described_class.new(multiplier: 1).intervals).to eq([
52
- 0.5244067512211441,
53
- 0.607594682584082,
54
- 0.5513816852888495,
55
- ])
55
+ expect(described_class.new(multiplier: 1).intervals).to eq(
56
+ [
57
+ 0.5244067512211441,
58
+ 0.607594682584082,
59
+ 0.5513816852888495,
60
+ ],
61
+ )
56
62
  end
57
63
 
58
64
  it "generates intervals with a defined max interval" do
@@ -60,15 +66,28 @@ describe Retriable::ExponentialBackoff do
60
66
  end
61
67
 
62
68
  it "generates intervals with a defined rand_factor" do
63
- expect(described_class.new(rand_factor: 0.2).intervals).to eq([
64
- 0.5097627004884576,
65
- 0.8145568095504492,
66
- 1.1712435167599646,
67
- ])
69
+ expect(described_class.new(rand_factor: 0.2).intervals).to eq(
70
+ [
71
+ 0.5097627004884576,
72
+ 0.8145568095504492,
73
+ 1.1712435167599646,
74
+ ],
75
+ )
68
76
  end
69
77
 
70
78
  it "generates 10 non-randomized intervals" do
71
79
  non_random_intervals = 9.times.inject([0.5]) { |memo, _i| memo + [memo.last * 1.5] }
72
80
  expect(described_class.new(tries: 10, rand_factor: 0.0).intervals).to eq(non_random_intervals)
73
81
  end
82
+
83
+ it "provides capped intervals lazily" do
84
+ interval_for = described_class.new(
85
+ base_interval: 1.0,
86
+ multiplier: 2.0,
87
+ max_interval: 4.0,
88
+ rand_factor: 0.0,
89
+ ).interval_provider
90
+
91
+ expect(Array.new(5) { |index| interval_for.call(index) }).to eq([1.0, 2.0, 4.0, 4.0, 4.0])
92
+ end
74
93
  end