retriable 3.1.1 → 3.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.hound.yml +2 -0
- data/.rspec +2 -0
- data/.rubocop.yml +4 -7
- data/.travis.yml +10 -9
- data/CHANGELOG.md +6 -0
- data/Gemfile +1 -3
- data/README.md +135 -58
- data/lib/retriable.rb +2 -2
- data/lib/retriable/config.rb +1 -1
- data/lib/retriable/exponential_backoff.rb +0 -1
- data/lib/retriable/version.rb +1 -1
- data/retriable.gemspec +3 -7
- data/spec/config_spec.rb +36 -40
- data/spec/exponential_backoff_spec.rb +22 -45
- data/spec/retriable_spec.rb +174 -343
- data/spec/spec_helper.rb +7 -3
- data/spec/support/exceptions.rb +3 -0
- metadata +12 -52
- data/Guardfile +0 -8
- data/Rakefile +0 -12
data/lib/retriable/config.rb
CHANGED
data/lib/retriable/version.rb
CHANGED
data/retriable.gemspec
CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.version = Retriable::VERSION
|
9
9
|
spec.authors = ["Jack Chu"]
|
10
10
|
spec.email = ["jack@jackchu.com"]
|
11
|
-
spec.summary = "Retriable is
|
12
|
-
spec.description = "Retriable is
|
11
|
+
spec.summary = "Retriable is a simple DSL to retry failed code blocks with randomized exponential backoff"
|
12
|
+
spec.description = "Retriable is a simple DSL to retry failed code blocks with randomized exponential backoff. This is especially useful when interacting external api/services or file system calls."
|
13
13
|
spec.homepage = "http://github.com/kamui/retriable"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
@@ -21,11 +21,7 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.required_ruby_version = ">= 2.0.0"
|
22
22
|
|
23
23
|
spec.add_development_dependency "bundler"
|
24
|
-
spec.add_development_dependency "
|
25
|
-
|
26
|
-
spec.add_development_dependency "minitest", "~> 5.10"
|
27
|
-
spec.add_development_dependency "guard"
|
28
|
-
spec.add_development_dependency "guard-minitest"
|
24
|
+
spec.add_development_dependency "rspec", "~> 3"
|
29
25
|
|
30
26
|
if RUBY_VERSION < "2.3"
|
31
27
|
spec.add_development_dependency "ruby_dep", "~> 1.3.1"
|
data/spec/config_spec.rb
CHANGED
@@ -1,57 +1,53 @@
|
|
1
|
-
require_relative "spec_helper"
|
2
|
-
|
3
1
|
describe Retriable::Config do
|
4
|
-
|
5
|
-
Retriable::Config
|
6
|
-
end
|
2
|
+
let(:default_config) { described_class.new }
|
7
3
|
|
8
|
-
|
9
|
-
|
10
|
-
|
4
|
+
context "defaults" do
|
5
|
+
it "sleep defaults to enabled" do
|
6
|
+
expect(default_config.sleep_disabled).to be_falsey
|
7
|
+
end
|
11
8
|
|
12
|
-
|
13
|
-
|
14
|
-
|
9
|
+
it "tries defaults to 3" do
|
10
|
+
expect(default_config.tries).to eq(3)
|
11
|
+
end
|
15
12
|
|
16
|
-
|
17
|
-
|
18
|
-
|
13
|
+
it "max interval defaults to 60" do
|
14
|
+
expect(default_config.max_interval).to eq(60)
|
15
|
+
end
|
19
16
|
|
20
|
-
|
21
|
-
|
22
|
-
|
17
|
+
it "randomization factor defaults to 0.5" do
|
18
|
+
expect(default_config.base_interval).to eq(0.5)
|
19
|
+
end
|
23
20
|
|
24
|
-
|
25
|
-
|
26
|
-
|
21
|
+
it "multiplier defaults to 1.5" do
|
22
|
+
expect(default_config.multiplier).to eq(1.5)
|
23
|
+
end
|
27
24
|
|
28
|
-
|
29
|
-
|
30
|
-
|
25
|
+
it "max elapsed time defaults to 900" do
|
26
|
+
expect(default_config.max_elapsed_time).to eq(900)
|
27
|
+
end
|
31
28
|
|
32
|
-
|
33
|
-
|
34
|
-
|
29
|
+
it "intervals defaults to nil" do
|
30
|
+
expect(default_config.intervals).to be_nil
|
31
|
+
end
|
35
32
|
|
36
|
-
|
37
|
-
|
38
|
-
|
33
|
+
it "timeout defaults to nil" do
|
34
|
+
expect(default_config.timeout).to be_nil
|
35
|
+
end
|
39
36
|
|
40
|
-
|
41
|
-
|
42
|
-
|
37
|
+
it "on defaults to [StandardError]" do
|
38
|
+
expect(default_config.on).to eq([StandardError])
|
39
|
+
end
|
43
40
|
|
44
|
-
|
45
|
-
|
46
|
-
|
41
|
+
it "on_retry handler defaults to nil" do
|
42
|
+
expect(default_config.on_retry).to be_nil
|
43
|
+
end
|
47
44
|
|
48
|
-
|
49
|
-
|
45
|
+
it "contexts defaults to {}" do
|
46
|
+
expect(default_config.contexts).to eq({})
|
47
|
+
end
|
50
48
|
end
|
51
49
|
|
52
50
|
it "raises errors on invalid configuration" do
|
53
|
-
|
54
|
-
subject.new(does_not_exist: 123)
|
55
|
-
end
|
51
|
+
expect { described_class.new(does_not_exist: 123) }.to raise_error(ArgumentError, /not a valid option/)
|
56
52
|
end
|
57
53
|
end
|
@@ -1,32 +1,26 @@
|
|
1
|
-
require_relative "spec_helper"
|
2
|
-
|
3
1
|
describe Retriable::ExponentialBackoff do
|
4
|
-
|
5
|
-
|
6
|
-
end
|
2
|
+
context "defaults" do
|
3
|
+
let(:backoff_config) { described_class.new }
|
7
4
|
|
8
|
-
|
9
|
-
|
10
|
-
|
5
|
+
it "tries defaults to 3" do
|
6
|
+
expect(backoff_config.tries).to eq(3)
|
7
|
+
end
|
11
8
|
|
12
|
-
|
13
|
-
|
14
|
-
|
9
|
+
it "max interval defaults to 60" do
|
10
|
+
expect(backoff_config.max_interval).to eq(60)
|
11
|
+
end
|
15
12
|
|
16
|
-
|
17
|
-
|
18
|
-
|
13
|
+
it "randomization factor defaults to 0.5" do
|
14
|
+
expect(backoff_config.base_interval).to eq(0.5)
|
15
|
+
end
|
19
16
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
it "multiplier defaults to 1.5" do
|
25
|
-
expect(subject.new.multiplier).must_equal 1.5
|
17
|
+
it "multiplier defaults to 1.5" do
|
18
|
+
expect(backoff_config.multiplier).to eq(1.5)
|
19
|
+
end
|
26
20
|
end
|
27
21
|
|
28
22
|
it "generates 10 randomized intervals" do
|
29
|
-
expect(
|
23
|
+
expect(described_class.new(tries: 9).intervals).to eq([
|
30
24
|
0.5244067512211441,
|
31
25
|
0.9113920238761231,
|
32
26
|
1.2406087918999114,
|
@@ -40,11 +34,11 @@ describe Retriable::ExponentialBackoff do
|
|
40
34
|
end
|
41
35
|
|
42
36
|
it "generates defined number of intervals" do
|
43
|
-
expect(
|
37
|
+
expect(described_class.new(tries: 5).intervals.size).to eq(5)
|
44
38
|
end
|
45
39
|
|
46
40
|
it "generates intervals with a defined base interval" do
|
47
|
-
expect(
|
41
|
+
expect(described_class.new(base_interval: 1).intervals).to eq([
|
48
42
|
1.0488135024422882,
|
49
43
|
1.8227840477522461,
|
50
44
|
2.4812175837998227,
|
@@ -52,7 +46,7 @@ describe Retriable::ExponentialBackoff do
|
|
52
46
|
end
|
53
47
|
|
54
48
|
it "generates intervals with a defined multiplier" do
|
55
|
-
expect(
|
49
|
+
expect(described_class.new(multiplier: 1).intervals).to eq([
|
56
50
|
0.5244067512211441,
|
57
51
|
0.607594682584082,
|
58
52
|
0.5513816852888495,
|
@@ -60,15 +54,11 @@ describe Retriable::ExponentialBackoff do
|
|
60
54
|
end
|
61
55
|
|
62
56
|
it "generates intervals with a defined max interval" do
|
63
|
-
expect(
|
64
|
-
0.5,
|
65
|
-
0.75,
|
66
|
-
1.0,
|
67
|
-
])
|
57
|
+
expect(described_class.new(max_interval: 1.0, rand_factor: 0.0).intervals).to eq([0.5, 0.75, 1.0])
|
68
58
|
end
|
69
59
|
|
70
60
|
it "generates intervals with a defined rand_factor" do
|
71
|
-
expect(
|
61
|
+
expect(described_class.new(rand_factor: 0.2).intervals).to eq([
|
72
62
|
0.5097627004884576,
|
73
63
|
0.8145568095504492,
|
74
64
|
1.1712435167599646,
|
@@ -76,20 +66,7 @@ describe Retriable::ExponentialBackoff do
|
|
76
66
|
end
|
77
67
|
|
78
68
|
it "generates 10 non-randomized intervals" do
|
79
|
-
|
80
|
-
|
81
|
-
rand_factor: 0.0,
|
82
|
-
).intervals).must_equal([
|
83
|
-
0.5,
|
84
|
-
0.75,
|
85
|
-
1.125,
|
86
|
-
1.6875,
|
87
|
-
2.53125,
|
88
|
-
3.796875,
|
89
|
-
5.6953125,
|
90
|
-
8.54296875,
|
91
|
-
12.814453125,
|
92
|
-
19.2216796875,
|
93
|
-
])
|
69
|
+
non_random_intervals = 9.times.inject([0.5]) { |memo, _i| memo + [memo.last * 1.5] }
|
70
|
+
expect(described_class.new(tries: 10, rand_factor: 0.0).intervals).to eq(non_random_intervals)
|
94
71
|
end
|
95
72
|
end
|
data/spec/retriable_spec.rb
CHANGED
@@ -1,434 +1,265 @@
|
|
1
|
-
|
1
|
+
describe Retriable do
|
2
|
+
let(:time_table_handler) do
|
3
|
+
->(_exception, try, _elapsed_time, next_interval) { @next_interval_table[try] = next_interval }
|
4
|
+
end
|
2
5
|
|
3
|
-
|
6
|
+
before(:each) do
|
7
|
+
described_class.configure { |c| c.sleep_disabled = true }
|
8
|
+
@tries = 0
|
9
|
+
@next_interval_table = {}
|
10
|
+
end
|
4
11
|
|
5
|
-
|
6
|
-
|
7
|
-
Retriable
|
12
|
+
def increment_tries
|
13
|
+
@tries += 1
|
8
14
|
end
|
9
15
|
|
10
|
-
|
11
|
-
|
16
|
+
def increment_tries_with_exception(exception_class = nil)
|
17
|
+
exception_class ||= StandardError
|
18
|
+
increment_tries
|
19
|
+
raise exception_class, "#{exception_class} occurred"
|
12
20
|
end
|
13
21
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
c.sleep_disabled = true
|
18
|
-
end
|
22
|
+
context "global scope extension" do
|
23
|
+
it "cannot be called in the global scope without requiring the core_ext/kernel" do
|
24
|
+
expect { retriable { puts "should raise NoMethodError" } }.to raise_error(NoMethodError)
|
19
25
|
end
|
20
26
|
|
21
|
-
it "
|
22
|
-
|
23
|
-
subject.retriable do
|
24
|
-
tries += 1
|
25
|
-
end
|
27
|
+
it "can be called once the kernel extension is required" do
|
28
|
+
require_relative "../lib/retriable/core_ext/kernel"
|
26
29
|
|
27
|
-
expect(
|
30
|
+
expect { retriable { increment_tries_with_exception } }.to raise_error(StandardError)
|
31
|
+
expect(@tries).to eq(3)
|
28
32
|
end
|
33
|
+
end
|
29
34
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
35
|
+
context "#retriable" do
|
36
|
+
it "raises a LocalJumpError if not given a block" do
|
37
|
+
expect { described_class.retriable }.to raise_error(LocalJumpError)
|
38
|
+
expect { described_class.retriable(timeout: 2) }.to raise_error(LocalJumpError)
|
39
|
+
end
|
34
40
|
|
35
|
-
|
36
|
-
|
37
|
-
|
41
|
+
it "stops at first try if the block does not raise an exception" do
|
42
|
+
described_class.retriable { increment_tries }
|
43
|
+
expect(@tries).to eq(1)
|
38
44
|
end
|
39
45
|
|
40
46
|
it "makes 3 tries when retrying block of code raising StandardError with no arguments" do
|
41
|
-
|
42
|
-
|
43
|
-
expect do
|
44
|
-
subject.retriable do
|
45
|
-
tries += 1
|
46
|
-
raise StandardError.new, "StandardError occurred"
|
47
|
-
end
|
48
|
-
end.must_raise StandardError
|
49
|
-
|
50
|
-
expect(tries).must_equal 3
|
47
|
+
expect { described_class.retriable { increment_tries_with_exception } }.to raise_error(StandardError)
|
48
|
+
expect(@tries).to eq(3)
|
51
49
|
end
|
52
50
|
|
53
|
-
it "makes only 1 try when exception raised is not
|
54
|
-
tries = 0
|
55
|
-
|
51
|
+
it "makes only 1 try when exception raised is not descendent of StandardError" do
|
56
52
|
expect do
|
57
|
-
|
58
|
-
|
59
|
-
raise TestError.new, "TestError occurred"
|
60
|
-
end
|
61
|
-
end.must_raise TestError
|
53
|
+
described_class.retriable { increment_tries_with_exception(NonStandardError) }
|
54
|
+
end.to raise_error(NonStandardError)
|
62
55
|
|
63
|
-
expect(tries).
|
56
|
+
expect(@tries).to eq(1)
|
64
57
|
end
|
65
58
|
|
66
|
-
it "
|
67
|
-
tries = 0
|
68
|
-
|
59
|
+
it "with custom exception tries 3 times and re-raises the exception" do
|
69
60
|
expect do
|
70
|
-
|
71
|
-
|
72
|
-
raise TestError.new, "TestError occurred"
|
73
|
-
end
|
74
|
-
end.must_raise TestError
|
61
|
+
described_class.retriable(on: NonStandardError) { increment_tries_with_exception(NonStandardError) }
|
62
|
+
end.to raise_error(NonStandardError)
|
75
63
|
|
76
|
-
expect(tries).
|
64
|
+
expect(@tries).to eq(3)
|
77
65
|
end
|
78
66
|
|
79
|
-
it "
|
80
|
-
tries
|
81
|
-
|
82
|
-
expect do
|
83
|
-
subject.retriable(tries: 10) do
|
84
|
-
tries += 1
|
85
|
-
raise StandardError.new, "StandardError occurred"
|
86
|
-
end
|
87
|
-
end.must_raise StandardError
|
88
|
-
|
89
|
-
expect(tries).must_equal 10
|
67
|
+
it "tries 10 times when specified" do
|
68
|
+
expect { described_class.retriable(tries: 10) { increment_tries_with_exception } }.to raise_error(StandardError)
|
69
|
+
expect(@tries).to eq(10)
|
90
70
|
end
|
91
71
|
|
92
|
-
it "
|
93
|
-
expect
|
94
|
-
subject.retriable timeout: 1 do
|
95
|
-
sleep 1.1
|
96
|
-
end
|
97
|
-
end.must_raise Timeout::Error
|
72
|
+
it "will timeout after 1 second" do
|
73
|
+
expect { described_class.retriable(timeout: 1) { sleep(1.1) } }.to raise_error(Timeout::Error)
|
98
74
|
end
|
99
75
|
|
100
76
|
it "applies a randomized exponential backoff to each try" do
|
101
|
-
tries = 0
|
102
|
-
time_table = []
|
103
|
-
|
104
|
-
handler = lambda do |exception, _try, _elapsed_time, next_interval|
|
105
|
-
expect(exception.class).must_equal ArgumentError
|
106
|
-
time_table << next_interval
|
107
|
-
end
|
108
|
-
|
109
77
|
expect do
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
5.339852157217869,
|
128
|
-
11.889873261212443,
|
129
|
-
18.756037881636484,
|
130
|
-
nil,
|
131
|
-
])
|
132
|
-
|
133
|
-
expect(tries).must_equal(10)
|
78
|
+
described_class.retriable(on_retry: time_table_handler, tries: 10) { increment_tries_with_exception }
|
79
|
+
end.to raise_error(StandardError)
|
80
|
+
|
81
|
+
expect(@next_interval_table).to eq(
|
82
|
+
1 => 0.5244067512211441,
|
83
|
+
2 => 0.9113920238761231,
|
84
|
+
3 => 1.2406087918999114,
|
85
|
+
4 => 1.7632403621664823,
|
86
|
+
5 => 2.338001204738311,
|
87
|
+
6 => 4.350816718580626,
|
88
|
+
7 => 5.339852157217869,
|
89
|
+
8 => 11.889873261212443,
|
90
|
+
9 => 18.756037881636484,
|
91
|
+
10 => nil,
|
92
|
+
)
|
93
|
+
|
94
|
+
expect(@tries).to eq(10)
|
134
95
|
end
|
135
96
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
@time_table = {}
|
97
|
+
context "with rand_factor 0.0 and an on_retry handler" do
|
98
|
+
let(:tries) { 6 }
|
99
|
+
let(:no_rand_timetable) { { 1 => 0.5, 2 => 0.75, 3 => 1.125 } }
|
100
|
+
let(:args) { { on_retry: time_table_handler, rand_factor: 0.0, tries: tries } }
|
141
101
|
|
142
|
-
|
143
|
-
|
144
|
-
|
102
|
+
it "applies a non-randomized exponential backoff to each try" do
|
103
|
+
described_class.retriable(args) do
|
104
|
+
increment_tries
|
105
|
+
raise StandardError if @tries < tries
|
145
106
|
end
|
146
107
|
|
147
|
-
|
148
|
-
|
149
|
-
on_retry: handler,
|
150
|
-
rand_factor: 0.0,
|
151
|
-
tries: tries,
|
152
|
-
) do
|
153
|
-
@try_count += 1
|
154
|
-
raise ArgumentError.new, "ArgumentError occurred" if @try_count < tries
|
155
|
-
end
|
108
|
+
expect(@tries).to eq(tries)
|
109
|
+
expect(@next_interval_table).to eq(no_rand_timetable.merge(4 => 1.6875, 5 => 2.53125))
|
156
110
|
end
|
157
111
|
|
158
|
-
it "
|
159
|
-
expect
|
160
|
-
|
112
|
+
it "obeys a max interval of 1.5 seconds" do
|
113
|
+
expect do
|
114
|
+
described_class.retriable(args.merge(max_interval: 1.5)) { increment_tries_with_exception }
|
115
|
+
end.to raise_error(StandardError)
|
161
116
|
|
162
|
-
|
163
|
-
expect(@time_table).must_equal(
|
164
|
-
1 => 0.5,
|
165
|
-
2 => 0.75,
|
166
|
-
3 => 1.125,
|
167
|
-
4 => 1.6875,
|
168
|
-
5 => 2.53125,
|
169
|
-
)
|
117
|
+
expect(@next_interval_table).to eq(no_rand_timetable.merge(4 => 1.5, 5 => 1.5, 6 => nil))
|
170
118
|
end
|
171
|
-
end
|
172
|
-
|
173
|
-
it "#retriable has a max interval of 1.5 seconds" do
|
174
|
-
tries = 0
|
175
|
-
time_table = {}
|
176
119
|
|
177
|
-
|
178
|
-
|
179
|
-
|
120
|
+
it "obeys custom defined intervals" do
|
121
|
+
interval_hash = no_rand_timetable.merge(4 => 1.5, 5 => 1.5, 6 => nil)
|
122
|
+
intervals = interval_hash.values.compact.sort
|
180
123
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
tries: 5,
|
187
|
-
max_interval: 1.5,
|
188
|
-
) do
|
189
|
-
tries += 1
|
190
|
-
raise StandardError.new, "StandardError occurred"
|
191
|
-
end
|
192
|
-
end.must_raise StandardError
|
193
|
-
|
194
|
-
expect(time_table).must_equal(
|
195
|
-
1 => 0.5,
|
196
|
-
2 => 0.75,
|
197
|
-
3 => 1.125,
|
198
|
-
4 => 1.5,
|
199
|
-
5 => nil,
|
200
|
-
)
|
201
|
-
end
|
124
|
+
expect do
|
125
|
+
described_class.retriable(on_retry: time_table_handler, intervals: intervals) do
|
126
|
+
increment_tries_with_exception
|
127
|
+
end
|
128
|
+
end.to raise_error(StandardError)
|
202
129
|
|
203
|
-
|
204
|
-
|
205
|
-
0.5,
|
206
|
-
0.75,
|
207
|
-
1.125,
|
208
|
-
1.5,
|
209
|
-
1.5,
|
210
|
-
]
|
211
|
-
time_table = {}
|
212
|
-
|
213
|
-
handler = lambda do |_exception, try, _elapsed_time, next_interval|
|
214
|
-
time_table[try] = next_interval
|
130
|
+
expect(@next_interval_table).to eq(interval_hash)
|
131
|
+
expect(@tries).to eq(intervals.size + 1)
|
215
132
|
end
|
133
|
+
end
|
216
134
|
|
217
|
-
|
135
|
+
context "with an array :on parameter" do
|
136
|
+
it "handles both kinds of exceptions" do
|
137
|
+
described_class.retriable(on: [StandardError, NonStandardError]) do
|
138
|
+
increment_tries
|
218
139
|
|
219
|
-
|
220
|
-
|
221
|
-
on_retry: handler,
|
222
|
-
intervals: intervals,
|
223
|
-
) do
|
224
|
-
try_count += 1
|
225
|
-
raise StandardError.new, "StandardError occurred"
|
140
|
+
raise StandardError if @tries == 1
|
141
|
+
raise NonStandardError if @tries == 2
|
226
142
|
end
|
227
|
-
end.must_raise StandardError
|
228
|
-
|
229
|
-
expect(time_table).must_equal(
|
230
|
-
1 => 0.5,
|
231
|
-
2 => 0.75,
|
232
|
-
3 => 1.125,
|
233
|
-
4 => 1.5,
|
234
|
-
5 => 1.5,
|
235
|
-
6 => nil,
|
236
|
-
)
|
237
143
|
|
238
|
-
|
144
|
+
expect(@tries).to eq(3)
|
145
|
+
end
|
239
146
|
end
|
240
147
|
|
241
|
-
|
242
|
-
|
243
|
-
subject.retriable on: { TestError => /something went wrong/ } do
|
244
|
-
raise TestError, "something went wrong"
|
245
|
-
end
|
246
|
-
end.must_raise TestError
|
247
|
-
|
248
|
-
expect(e.message).must_equal "something went wrong"
|
249
|
-
end
|
148
|
+
context "with a hash :on parameter" do
|
149
|
+
let(:on_hash) { { NonStandardError => /NonStandardError occurred/ } }
|
250
150
|
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
tries = 0
|
256
|
-
e = expect do
|
257
|
-
subject.retriable on: {
|
258
|
-
DifferentTestError => /should never happen/,
|
259
|
-
TestError => /something went wrong/,
|
260
|
-
DifferentTestError => /also should never happen/,
|
261
|
-
}, tries: 4 do
|
262
|
-
tries += 1
|
263
|
-
raise SecondTestError, "something went wrong"
|
264
|
-
end
|
265
|
-
end.must_raise SecondTestError
|
151
|
+
it "where the value is an exception message pattern" do
|
152
|
+
expect do
|
153
|
+
described_class.retriable(on: on_hash) { increment_tries_with_exception(NonStandardError) }
|
154
|
+
end.to raise_error(NonStandardError, /NonStandardError occurred/)
|
266
155
|
|
267
|
-
|
268
|
-
|
269
|
-
end
|
156
|
+
expect(@tries).to eq(3)
|
157
|
+
end
|
270
158
|
|
271
|
-
|
272
|
-
|
159
|
+
it "matches exception subclasses when message matches pattern" do
|
160
|
+
expect do
|
161
|
+
described_class.retriable(on: on_hash.merge(DifferentError => [/shouldn't happen/, /also not/])) do
|
162
|
+
increment_tries_with_exception(SecondNonStandardError)
|
163
|
+
end
|
164
|
+
end.to raise_error(SecondNonStandardError, /SecondNonStandardError occurred/)
|
273
165
|
|
274
|
-
|
275
|
-
|
276
|
-
subject.retriable on: { TestError => /something went wrong/ }, tries: 4 do
|
277
|
-
tries += 1
|
278
|
-
raise SecondTestError, "not a match"
|
279
|
-
end
|
280
|
-
end.must_raise SecondTestError
|
166
|
+
expect(@tries).to eq(3)
|
167
|
+
end
|
281
168
|
|
282
|
-
|
283
|
-
|
169
|
+
it "does not retry matching exception subclass but not message" do
|
170
|
+
expect do
|
171
|
+
described_class.retriable(on: on_hash) do
|
172
|
+
increment_tries
|
173
|
+
raise SecondNonStandardError, "not a match"
|
174
|
+
end
|
175
|
+
end.to raise_error(SecondNonStandardError, /not a match/)
|
284
176
|
|
285
|
-
|
286
|
-
tries = 0
|
287
|
-
exceptions = []
|
288
|
-
handler = lambda do |exception, try, _elapsed_time, _next_interval|
|
289
|
-
exceptions[try] = exception
|
177
|
+
expect(@tries).to eq(1)
|
290
178
|
end
|
291
179
|
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
180
|
+
it "successfully retries when the values are arrays of exception message patterns" do
|
181
|
+
exceptions = []
|
182
|
+
handler = ->(exception, try, _elapsed_time, _next_interval) { exceptions[try] = exception }
|
183
|
+
on_hash = { StandardError => nil, NonStandardError => [/foo/, /bar/] }
|
184
|
+
|
185
|
+
expect do
|
186
|
+
described_class.retriable(tries: 4, on: on_hash, on_retry: handler) do
|
187
|
+
increment_tries
|
188
|
+
|
189
|
+
case @tries
|
190
|
+
when 1
|
191
|
+
raise NonStandardError, "foo"
|
192
|
+
when 2
|
193
|
+
raise NonStandardError, "bar"
|
194
|
+
when 3
|
195
|
+
raise StandardError
|
196
|
+
else
|
197
|
+
raise NonStandardError, "crash"
|
198
|
+
end
|
304
199
|
end
|
305
|
-
end
|
306
|
-
end.must_raise TestError
|
307
|
-
|
308
|
-
expect(e.message).must_equal "crash"
|
309
|
-
expect(exceptions[1].class).must_equal TestError
|
310
|
-
expect(exceptions[1].message).must_equal "foo"
|
311
|
-
expect(exceptions[2].class).must_equal TestError
|
312
|
-
expect(exceptions[2].message).must_equal "bar"
|
313
|
-
expect(exceptions[3].class).must_equal StandardError
|
314
|
-
end
|
200
|
+
end.to raise_error(NonStandardError, /crash/)
|
315
201
|
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
end
|
322
|
-
|
323
|
-
require_relative "../lib/retriable/core_ext/kernel"
|
202
|
+
expect(exceptions[1]).to be_a(NonStandardError)
|
203
|
+
expect(exceptions[1].message).to eq("foo")
|
204
|
+
expect(exceptions[2]).to be_a(NonStandardError)
|
205
|
+
expect(exceptions[2].message).to eq("bar")
|
206
|
+
expect(exceptions[3]).to be_a(StandardError)
|
207
|
+
end
|
208
|
+
end
|
324
209
|
|
325
|
-
|
210
|
+
it "runs for a max elapsed time of 2 seconds" do
|
211
|
+
described_class.configure { |c| c.sleep_disabled = false }
|
326
212
|
|
327
213
|
expect do
|
328
|
-
retriable do
|
329
|
-
|
330
|
-
raise StandardError
|
214
|
+
described_class.retriable(base_interval: 1.0, multiplier: 1.0, rand_factor: 0.0, max_elapsed_time: 2.0) do
|
215
|
+
increment_tries_with_exception
|
331
216
|
end
|
332
|
-
end.
|
333
|
-
|
334
|
-
expect(tries).must_equal 3
|
335
|
-
end
|
336
|
-
end
|
217
|
+
end.to raise_error(StandardError)
|
337
218
|
|
338
|
-
|
339
|
-
subject.configure do |c|
|
340
|
-
c.sleep_disabled = false
|
219
|
+
expect(@tries).to eq(2)
|
341
220
|
end
|
342
221
|
|
343
|
-
|
344
|
-
|
345
|
-
tries = 0
|
346
|
-
time_table = {}
|
347
|
-
|
348
|
-
handler = lambda do |_exception, try, elapsed_time, _next_interval|
|
349
|
-
time_table[try] = elapsed_time
|
222
|
+
it "raises ArgumentError on invalid options" do
|
223
|
+
expect { described_class.retriable(does_not_exist: 123) { increment_tries } }.to raise_error(ArgumentError)
|
350
224
|
end
|
351
|
-
|
352
|
-
expect do
|
353
|
-
subject.retriable(
|
354
|
-
base_interval: 1.0,
|
355
|
-
multiplier: 1.0,
|
356
|
-
rand_factor: 0.0,
|
357
|
-
max_elapsed_time: 2.0,
|
358
|
-
on_retry: handler,
|
359
|
-
) do
|
360
|
-
tries += 1
|
361
|
-
raise EOFError
|
362
|
-
end
|
363
|
-
end.must_raise EOFError
|
364
|
-
|
365
|
-
expect(tries).must_equal 2
|
366
225
|
end
|
367
226
|
|
368
|
-
|
369
|
-
|
370
|
-
|
227
|
+
context "#configure" do
|
228
|
+
it "raises NoMethodError on invalid configuration" do
|
229
|
+
expect { described_class.configure { |c| c.does_not_exist = 123 } }.to raise_error(NoMethodError)
|
371
230
|
end
|
372
231
|
end
|
373
232
|
|
374
|
-
|
375
|
-
|
376
|
-
Retriable.retriable(does_not_exist: 123)
|
377
|
-
end
|
378
|
-
end
|
233
|
+
context "#with_context" do
|
234
|
+
let(:api_tries) { 4 }
|
379
235
|
|
380
|
-
describe "#with_context" do
|
381
236
|
before do
|
382
|
-
|
383
|
-
c.sleep_disabled = true
|
237
|
+
described_class.configure do |c|
|
384
238
|
c.contexts[:sql] = { tries: 1 }
|
385
|
-
c.contexts[:api] = { tries:
|
239
|
+
c.contexts[:api] = { tries: api_tries }
|
386
240
|
end
|
387
241
|
end
|
388
242
|
|
389
|
-
it "
|
390
|
-
|
391
|
-
|
392
|
-
tries += 1
|
393
|
-
end
|
394
|
-
|
395
|
-
expect(tries).must_equal 1
|
243
|
+
it "stops at first try if the block does not raise an exception" do
|
244
|
+
described_class.with_context(:sql) { increment_tries }
|
245
|
+
expect(@tries).to eq(1)
|
396
246
|
end
|
397
247
|
|
398
|
-
it "
|
399
|
-
|
400
|
-
|
401
|
-
expect do
|
402
|
-
subject.with_context(:api) do
|
403
|
-
tries += 1
|
404
|
-
raise StandardError.new, "StandardError occurred"
|
405
|
-
end
|
406
|
-
end.must_raise StandardError
|
407
|
-
|
408
|
-
expect(tries).must_equal 3
|
248
|
+
it "respects the context options" do
|
249
|
+
expect { described_class.with_context(:api) { increment_tries_with_exception } }.to raise_error(StandardError)
|
250
|
+
expect(@tries).to eq(api_tries)
|
409
251
|
end
|
410
252
|
|
411
|
-
it "
|
412
|
-
tries = 0
|
413
|
-
|
253
|
+
it "allows override options" do
|
414
254
|
expect do
|
415
|
-
|
416
|
-
|
417
|
-
raise StandardError.new, "StandardError occurred"
|
418
|
-
end
|
419
|
-
end.must_raise StandardError
|
255
|
+
described_class.with_context(:sql, tries: 5) { increment_tries_with_exception }
|
256
|
+
end.to raise_error(StandardError)
|
420
257
|
|
421
|
-
expect(tries).
|
258
|
+
expect(@tries).to eq(5)
|
422
259
|
end
|
423
260
|
|
424
261
|
it "raises an ArgumentError when the context isn't found" do
|
425
|
-
|
426
|
-
|
427
|
-
expect do
|
428
|
-
subject.with_context(:wtf) do
|
429
|
-
tries += 1
|
430
|
-
end
|
431
|
-
end.must_raise ArgumentError
|
262
|
+
expect { described_class.with_context(:wtf) { increment_tries } }.to raise_error(ArgumentError, /wtf not found/)
|
432
263
|
end
|
433
264
|
end
|
434
265
|
end
|