retriable 3.5.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.
data/sig/retriable.rbs CHANGED
@@ -1,4 +1,32 @@
1
1
  module Retriable
2
2
  VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
3
+ OVERRIDE_THREAD_KEY: Symbol
4
+
5
+ def self.configure: () { (Config) -> void } -> void
6
+ def self.config: () -> Config
7
+ def self.with_override: (Hash[Symbol, untyped] options) { () -> untyped } -> untyped
8
+ def self.with_context: (Symbol context_key, ?Hash[Symbol, untyped] options) { (Integer) -> untyped } -> untyped
9
+ def self.retriable: (?Hash[Symbol, untyped] options) { (Integer) -> untyped } -> untyped
10
+
11
+ class Config
12
+ ATTRIBUTES: Array[Symbol]
13
+
14
+ attr_accessor tries: Numeric
15
+ attr_accessor base_interval: Numeric
16
+ attr_accessor max_interval: Numeric
17
+ attr_accessor rand_factor: Numeric
18
+ attr_accessor multiplier: Numeric
19
+ attr_accessor sleep_disabled: bool
20
+ attr_accessor max_elapsed_time: Numeric?
21
+ attr_accessor intervals: Array[Numeric]?
22
+ attr_accessor on: untyped
23
+ attr_accessor retry_if: untyped
24
+ attr_accessor on_retry: untyped
25
+ attr_accessor on_give_up: untyped
26
+ attr_accessor contexts: Hash[Symbol, untyped]
27
+
28
+ def initialize: (?Hash[Symbol, untyped] opts) -> void
29
+ def to_h: () -> Hash[Symbol, untyped]
30
+ def validate!: () -> void
31
+ end
4
32
  end
data/spec/config_spec.rb CHANGED
@@ -32,10 +32,6 @@ describe Retriable::Config do
32
32
  expect(default_config.intervals).to be_nil
33
33
  end
34
34
 
35
- it "timeout defaults to nil" do
36
- expect(default_config.timeout).to be_nil
37
- end
38
-
39
35
  it "on defaults to [StandardError]" do
40
36
  expect(default_config.on).to eq([StandardError])
41
37
  end
@@ -48,6 +44,10 @@ describe Retriable::Config do
48
44
  expect(default_config.on_retry).to be_nil
49
45
  end
50
46
 
47
+ it "on_give_up handler defaults to nil" do
48
+ expect(default_config.on_give_up).to be_nil
49
+ end
50
+
51
51
  it "contexts defaults to {}" do
52
52
  expect(default_config.contexts).to eq({})
53
53
  end
@@ -56,4 +56,157 @@ 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 "rejects timeout as an unknown option" do
61
+ expect { described_class.new(timeout: 5) }.to raise_error(ArgumentError, /not a valid option/)
62
+ end
63
+
64
+ it "raises errors on invalid timing configuration" do
65
+ expect { described_class.new(rand_factor: 1.1) }.to raise_error(ArgumentError, /rand_factor/)
66
+ end
67
+
68
+ it "raises errors when intervals is not an array" do
69
+ expect { described_class.new(intervals: "1") }.to raise_error(ArgumentError, /intervals must be an Array/)
70
+ end
71
+
72
+ it "requires a finite max_elapsed_time when tries is Float::INFINITY" do
73
+ expect { described_class.new(tries: Float::INFINITY, max_elapsed_time: nil) }
74
+ .to raise_error(ArgumentError, /max_elapsed_time must be a finite number/)
75
+ end
76
+
77
+ it "rejects intervals combined with tries: Float::INFINITY" do
78
+ expect do
79
+ described_class.new(
80
+ tries: Float::INFINITY,
81
+ max_elapsed_time: 60,
82
+ intervals: [0.1, 0.2],
83
+ )
84
+ end.to raise_error(ArgumentError, /intervals cannot be used with tries: Float::INFINITY/)
85
+ end
86
+
87
+ it "accepts tries: Float::INFINITY with a finite max_elapsed_time" do
88
+ expect { described_class.new(tries: Float::INFINITY, max_elapsed_time: 60) }
89
+ .not_to raise_error
90
+ end
91
+
92
+ context "on: option validation" do
93
+ it "accepts a single Exception subclass" do
94
+ expect { described_class.new(on: StandardError) }.not_to raise_error
95
+ end
96
+
97
+ it "accepts Exception itself" do
98
+ expect { described_class.new(on: Exception) }.not_to raise_error
99
+ end
100
+
101
+ it "accepts an array of Exception subclasses" do
102
+ expect { described_class.new(on: [StandardError, RuntimeError]) }.not_to raise_error
103
+ end
104
+
105
+ it "accepts a Set of Exception subclasses" do
106
+ expect { described_class.new(on: Set[StandardError, RuntimeError]) }.not_to raise_error
107
+ end
108
+
109
+ it "rejects a Set containing a non-Exception class" do
110
+ expect { described_class.new(on: Set[StandardError, Kernel]) }
111
+ .to raise_error(ArgumentError, /on must be an Exception class/)
112
+ end
113
+
114
+ it "accepts a hash with nil pattern values" do
115
+ expect { described_class.new(on: { StandardError => nil }) }.not_to raise_error
116
+ end
117
+
118
+ it "accepts a hash with Regexp pattern values" do
119
+ expect { described_class.new(on: { StandardError => /boom/ }) }.not_to raise_error
120
+ end
121
+
122
+ it "accepts a hash with Array-of-Regexp pattern values" do
123
+ expect { described_class.new(on: { StandardError => [/a/, /b/] }) }.not_to raise_error
124
+ end
125
+
126
+ it "rejects Object as on:" do
127
+ expect { described_class.new(on: Object) }
128
+ .to raise_error(ArgumentError, /on must be an Exception class/)
129
+ end
130
+
131
+ it "rejects Kernel as on:" do
132
+ expect { described_class.new(on: Kernel) }
133
+ .to raise_error(ArgumentError, /on must be an Exception class/)
134
+ end
135
+
136
+ it "rejects an array containing a non-Exception class" do
137
+ expect { described_class.new(on: [StandardError, Kernel]) }
138
+ .to raise_error(ArgumentError, /on must be an Exception class/)
139
+ end
140
+
141
+ it "rejects a hash key that is not an Exception class" do
142
+ expect { described_class.new(on: { Kernel => nil }) }
143
+ .to raise_error(ArgumentError, /on must be an Exception class/)
144
+ end
145
+
146
+ it "rejects a hash value that is a String" do
147
+ expect { described_class.new(on: { StandardError => "boom" }) }
148
+ .to raise_error(ArgumentError, /on\[StandardError\] must be nil, a Regexp, or an Array of Regexps/)
149
+ end
150
+
151
+ it "rejects a hash value that is an Array containing a non-Regexp" do
152
+ expect { described_class.new(on: { StandardError => [/a/, "b"] }) }
153
+ .to raise_error(ArgumentError, /on\[StandardError\] must be nil, a Regexp, or an Array of Regexps/)
154
+ end
155
+
156
+ it "rejects a string passed as on:" do
157
+ expect { described_class.new(on: "StandardError") }
158
+ .to raise_error(ArgumentError, /on must be an Exception class/)
159
+ end
160
+
161
+ it "validates on: even when intervals is provided" do
162
+ expect { described_class.new(intervals: [0.1], on: Object) }
163
+ .to raise_error(ArgumentError, /on must be an Exception class/)
164
+ end
165
+ end
166
+
167
+ context "callable option validation" do
168
+ %i[retry_if on_retry on_give_up].each do |opt|
169
+ it "accepts a callable for #{opt}" do
170
+ expect { described_class.new(opt => ->(*) {}) }.not_to raise_error
171
+ end
172
+
173
+ it "accepts nil and false for #{opt}" do
174
+ expect { described_class.new(opt => nil) }.not_to raise_error
175
+ expect { described_class.new(opt => false) }.not_to raise_error
176
+ end
177
+
178
+ it "rejects a non-callable truthy value for #{opt}" do
179
+ expect { described_class.new(opt => 5) }.to raise_error(ArgumentError, /#{opt}.*#call/)
180
+ end
181
+ end
182
+ end
183
+
184
+ context "context structure validation" do
185
+ it "rejects a context whose options contain a nested :contexts key" do
186
+ expect { described_class.new(contexts: { api: { contexts: {} } }) }
187
+ .to raise_error(ArgumentError, /contexts is not a valid option/)
188
+ end
189
+
190
+ it "rejects a context with an unknown option key" do
191
+ expect { described_class.new(contexts: { api: { does_not_exist: 1 } }) }
192
+ .to raise_error(ArgumentError, /does_not_exist is not a valid option/)
193
+ end
194
+
195
+ it "validates context structure even when intervals is provided" do
196
+ expect { described_class.new(intervals: [0.1], contexts: { api: { contexts: {} } }) }
197
+ .to raise_error(ArgumentError, /contexts is not a valid option/)
198
+ end
199
+
200
+ it "accepts a non-Hash context value (treated as empty options)" do
201
+ expect { described_class.new(contexts: { broken: nil }) }.not_to raise_error
202
+ end
203
+
204
+ it "accepts nil contexts" do
205
+ expect { described_class.new(contexts: nil) }.not_to raise_error
206
+ end
207
+
208
+ it "accepts a valid context" do
209
+ expect { described_class.new(contexts: { api: { tries: 3, base_interval: 1.0 } }) }.not_to raise_error
210
+ end
211
+ end
59
212
  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