retriable 3.1.1 → 3.1.2
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 +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
|