retriable 3.2.1 → 3.3.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.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +2 -2
- data/.gitignore +2 -0
- data/.rubocop.yml +4 -6
- data/CHANGELOG.md +7 -1
- data/lib/retriable/exponential_backoff.rb +7 -6
- data/lib/retriable/version.rb +1 -1
- data/lib/retriable.rb +86 -34
- data/retriable.gemspec +5 -2
- data/spec/retriable_spec.rb +49 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c0e8971cb9e6833e2795f2537ee69950714f7eb85989b497d15fad7f2e2552b6
|
|
4
|
+
data.tar.gz: 222c0e1664bf327dfb22cf161c42a3f3bec07a54ea3787af24a86ebd86abcd50
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b768d200c1dd25f9a5218096e5bba26b0ee815833fb87ed6a004d90d7f94208f4520df0fdb1172fc6fdea55209cf1eeb15d6efc39a52bd2f32c0222b5aecca5a
|
|
7
|
+
data.tar.gz: 0a095f55fcdbe49adf71bb06682c062ed98a4328ef0e4648926e6782b93152c337cd78b2f30a2bf5a79affc37c31c3f2b39730600c3c63b5dcb41d5c09b59380
|
data/.github/workflows/main.yml
CHANGED
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
NewCops: enable
|
|
3
|
+
TargetRubyVersion: 2.3
|
|
4
|
+
|
|
1
5
|
Style/StringLiterals:
|
|
2
6
|
EnforcedStyle: double_quotes
|
|
3
7
|
|
|
@@ -13,12 +17,6 @@ Style/TrailingCommaInArguments:
|
|
|
13
17
|
Lint/InheritException:
|
|
14
18
|
Enabled: false
|
|
15
19
|
|
|
16
|
-
Layout/FirstArrayElementIndentation:
|
|
17
|
-
Enabled: false
|
|
18
|
-
|
|
19
|
-
Layout/FirstHashElementIndentation:
|
|
20
|
-
Enabled: false
|
|
21
|
-
|
|
22
20
|
Style/NegatedIf:
|
|
23
21
|
Enabled: false
|
|
24
22
|
|
data/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
# HEAD
|
|
2
2
|
|
|
3
|
+
## 3.3.0
|
|
4
|
+
|
|
5
|
+
- Refactor `Retriable.retriable` internals into focused private helpers to improve readability while preserving behavior.
|
|
6
|
+
- Modernize `.rubocop.yml` with explicit modern defaults to enable new cops while preserving existing project style policies.
|
|
7
|
+
|
|
3
8
|
## 3.2.1
|
|
4
|
-
|
|
9
|
+
|
|
10
|
+
- Remove executables from gemspec as it was polluting the path for some users. Thanks @hsbt.
|
|
5
11
|
|
|
6
12
|
## 3.2.0
|
|
7
13
|
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module Retriable
|
|
4
4
|
class ExponentialBackoff
|
|
5
|
-
ATTRIBUTES = [
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
ATTRIBUTES = %i[
|
|
6
|
+
tries
|
|
7
|
+
base_interval
|
|
8
|
+
multiplier
|
|
9
|
+
max_interval
|
|
10
|
+
rand_factor
|
|
11
11
|
].freeze
|
|
12
12
|
|
|
13
13
|
attr_accessor(*ATTRIBUTES)
|
|
@@ -21,6 +21,7 @@ module Retriable
|
|
|
21
21
|
|
|
22
22
|
opts.each do |k, v|
|
|
23
23
|
raise ArgumentError, "#{k} is not a valid option" if !ATTRIBUTES.include?(k)
|
|
24
|
+
|
|
24
25
|
instance_variable_set(:"@#{k}", v)
|
|
25
26
|
end
|
|
26
27
|
end
|
data/lib/retriable/version.rb
CHANGED
data/lib/retriable.rb
CHANGED
|
@@ -21,61 +21,113 @@ module Retriable
|
|
|
21
21
|
raise ArgumentError, "#{context_key} not found in Retriable.config.contexts. Available contexts: #{config.contexts.keys}"
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
return unless block_given?
|
|
25
|
+
|
|
26
|
+
retriable(config.contexts[context_key].merge(options), &block)
|
|
25
27
|
end
|
|
26
28
|
|
|
27
|
-
def retriable(opts = {})
|
|
29
|
+
def retriable(opts = {}, &block)
|
|
28
30
|
local_config = opts.empty? ? config : Config.new(config.to_h.merge(opts))
|
|
29
31
|
|
|
30
|
-
tries
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
timeout = local_config.timeout
|
|
38
|
-
on = local_config.on
|
|
39
|
-
on_retry = local_config.on_retry
|
|
40
|
-
sleep_disabled = local_config.sleep_disabled
|
|
32
|
+
tries = local_config.tries
|
|
33
|
+
intervals = build_intervals(local_config, tries)
|
|
34
|
+
timeout = local_config.timeout
|
|
35
|
+
on = local_config.on
|
|
36
|
+
on_retry = local_config.on_retry
|
|
37
|
+
sleep_disabled = local_config.sleep_disabled
|
|
38
|
+
max_elapsed_time = local_config.max_elapsed_time
|
|
41
39
|
|
|
42
40
|
exception_list = on.is_a?(Hash) ? on.keys : on
|
|
41
|
+
exception_list = [*exception_list]
|
|
43
42
|
start_time = Time.now
|
|
44
43
|
elapsed_time = -> { Time.now - start_time }
|
|
45
44
|
|
|
46
|
-
if !intervals
|
|
47
|
-
intervals = ExponentialBackoff.new(
|
|
48
|
-
tries: tries - 1,
|
|
49
|
-
base_interval: base_interval,
|
|
50
|
-
multiplier: multiplier,
|
|
51
|
-
max_interval: max_interval,
|
|
52
|
-
rand_factor: rand_factor
|
|
53
|
-
).intervals
|
|
54
|
-
end
|
|
55
|
-
|
|
56
45
|
tries = intervals.size + 1
|
|
57
46
|
|
|
47
|
+
execute_tries(
|
|
48
|
+
tries: tries, intervals: intervals, timeout: timeout,
|
|
49
|
+
exception_list: exception_list, on: on, on_retry: on_retry,
|
|
50
|
+
elapsed_time: elapsed_time, max_elapsed_time: max_elapsed_time,
|
|
51
|
+
sleep_disabled: sleep_disabled, &block
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def execute_tries( # rubocop:disable Metrics/ParameterLists
|
|
56
|
+
tries:, intervals:, timeout:, exception_list:,
|
|
57
|
+
on:, on_retry:, elapsed_time:, max_elapsed_time:, sleep_disabled:, &block
|
|
58
|
+
)
|
|
58
59
|
tries.times do |index|
|
|
59
60
|
try = index + 1
|
|
60
61
|
|
|
61
62
|
begin
|
|
62
|
-
return
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
rescue *[*exception_list] => exception
|
|
66
|
-
if on.is_a?(Hash)
|
|
67
|
-
raise unless exception_list.any? do |e|
|
|
68
|
-
exception.is_a?(e) && ([*on[e]].empty? || [*on[e]].any? { |pattern| exception.message =~ pattern })
|
|
69
|
-
end
|
|
70
|
-
end
|
|
63
|
+
return call_with_timeout(timeout, try, &block)
|
|
64
|
+
rescue *exception_list => e
|
|
65
|
+
raise unless retriable_exception?(e, on, exception_list)
|
|
71
66
|
|
|
72
67
|
interval = intervals[index]
|
|
73
|
-
on_retry
|
|
68
|
+
call_on_retry(on_retry, e, try, elapsed_time.call, interval)
|
|
74
69
|
|
|
75
|
-
raise
|
|
70
|
+
raise unless can_retry?(try, tries, elapsed_time.call, interval, max_elapsed_time)
|
|
76
71
|
|
|
77
72
|
sleep interval if sleep_disabled != true
|
|
78
73
|
end
|
|
79
74
|
end
|
|
80
75
|
end
|
|
76
|
+
|
|
77
|
+
def build_intervals(local_config, tries)
|
|
78
|
+
return local_config.intervals if local_config.intervals
|
|
79
|
+
|
|
80
|
+
ExponentialBackoff.new(
|
|
81
|
+
tries: tries - 1,
|
|
82
|
+
base_interval: local_config.base_interval,
|
|
83
|
+
multiplier: local_config.multiplier,
|
|
84
|
+
max_interval: local_config.max_interval,
|
|
85
|
+
rand_factor: local_config.rand_factor,
|
|
86
|
+
).intervals
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def call_with_timeout(timeout, try)
|
|
90
|
+
return Timeout.timeout(timeout) { yield(try) } if timeout
|
|
91
|
+
|
|
92
|
+
yield(try)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def call_on_retry(on_retry, exception, try, elapsed_time, interval)
|
|
96
|
+
return unless on_retry
|
|
97
|
+
|
|
98
|
+
on_retry.call(exception, try, elapsed_time, interval)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def can_retry?(try, tries, elapsed_time, interval, max_elapsed_time)
|
|
102
|
+
try < tries && (elapsed_time + interval) <= max_elapsed_time
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# When `on` is a Hash, we need to verify the exception matches a pattern.
|
|
106
|
+
# For any non-Hash `on` value (e.g., Array of classes, single Exception class,
|
|
107
|
+
# or Module), the `rescue *exception_list` clause already guarantees the
|
|
108
|
+
# exception is retriable, so we return true unconditionally.
|
|
109
|
+
def retriable_exception?(exception, on, exception_list)
|
|
110
|
+
return false if on.is_a?(Hash) && !hash_exception_match?(exception, on, exception_list)
|
|
111
|
+
|
|
112
|
+
true
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def hash_exception_match?(exception, on, exception_list)
|
|
116
|
+
exception_list.any? do |error_class|
|
|
117
|
+
next false unless exception.is_a?(error_class)
|
|
118
|
+
|
|
119
|
+
patterns = [*on[error_class]]
|
|
120
|
+
patterns.empty? || patterns.any? { |pattern| exception.message =~ pattern }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private_class_method(
|
|
125
|
+
:execute_tries,
|
|
126
|
+
:build_intervals,
|
|
127
|
+
:call_with_timeout,
|
|
128
|
+
:call_on_retry,
|
|
129
|
+
:can_retry?,
|
|
130
|
+
:retriable_exception?,
|
|
131
|
+
:hash_exception_match?,
|
|
132
|
+
)
|
|
81
133
|
end
|
data/retriable.gemspec
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
5
|
require "retriable/version"
|
|
5
6
|
|
|
@@ -9,7 +10,9 @@ Gem::Specification.new do |spec|
|
|
|
9
10
|
spec.authors = ["Jack Chu"]
|
|
10
11
|
spec.email = ["jack@jackchu.com"]
|
|
11
12
|
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
|
|
13
|
+
spec.description = "Retriable is a simple DSL to retry failed code blocks with randomized " \
|
|
14
|
+
"exponential backoff. This is especially useful when interacting with external " \
|
|
15
|
+
"APIs/services or file system calls."
|
|
13
16
|
spec.homepage = "https://github.com/kamui/retriable"
|
|
14
17
|
spec.license = "MIT"
|
|
15
18
|
|
data/spec/retriable_spec.rb
CHANGED
|
@@ -96,6 +96,28 @@ describe Retriable do
|
|
|
96
96
|
expect(@tries).to eq(10)
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
+
it "does not call on_retry when explicitly set to false" do
|
|
100
|
+
callback_called = false
|
|
101
|
+
original_on_retry = described_class.config.on_retry
|
|
102
|
+
|
|
103
|
+
begin
|
|
104
|
+
described_class.configure do |c|
|
|
105
|
+
c.on_retry = proc { |_exception, _try, _elapsed_time, _next_interval| callback_called = true }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
expect do
|
|
109
|
+
described_class.retriable(on_retry: false, tries: 3) { increment_tries_with_exception }
|
|
110
|
+
end.to raise_error(StandardError)
|
|
111
|
+
|
|
112
|
+
expect(@tries).to eq(3)
|
|
113
|
+
expect(callback_called).to be(false)
|
|
114
|
+
ensure
|
|
115
|
+
described_class.configure do |c|
|
|
116
|
+
c.on_retry = original_on_retry
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
99
121
|
context "with rand_factor 0.0 and an on_retry handler" do
|
|
100
122
|
let(:tries) { 6 }
|
|
101
123
|
let(:no_rand_timetable) { { 1 => 0.5, 2 => 0.75, 3 => 1.125 } }
|
|
@@ -255,6 +277,17 @@ describe Retriable do
|
|
|
255
277
|
end
|
|
256
278
|
|
|
257
279
|
context "#configure" do
|
|
280
|
+
it "exposes only the intended public API" do
|
|
281
|
+
public_api_methods = %i[
|
|
282
|
+
retriable
|
|
283
|
+
with_context
|
|
284
|
+
configure
|
|
285
|
+
config
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
expect(described_class.singleton_methods(false)).to match_array(public_api_methods)
|
|
289
|
+
end
|
|
290
|
+
|
|
258
291
|
it "raises NoMethodError on invalid configuration" do
|
|
259
292
|
expect { described_class.configure { |c| c.does_not_exist = 123 } }.to raise_error(NoMethodError)
|
|
260
293
|
end
|
|
@@ -275,6 +308,22 @@ describe Retriable do
|
|
|
275
308
|
expect(@tries).to eq(1)
|
|
276
309
|
end
|
|
277
310
|
|
|
311
|
+
it "returns nil when called without a block" do
|
|
312
|
+
expect(described_class.with_context(:sql)).to be_nil
|
|
313
|
+
expect(@tries).to eq(0)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
it "passes try count through to the context block" do
|
|
317
|
+
seen_tries = []
|
|
318
|
+
|
|
319
|
+
described_class.with_context(:api) do |try|
|
|
320
|
+
seen_tries << try
|
|
321
|
+
raise StandardError if try < 3
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
expect(seen_tries).to eq([1, 2, 3])
|
|
325
|
+
end
|
|
326
|
+
|
|
278
327
|
it "respects the context options" do
|
|
279
328
|
expect { described_class.with_context(:api) { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
280
329
|
expect(@tries).to eq(api_tries)
|
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.
|
|
4
|
+
version: 3.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jack Chu
|
|
@@ -52,7 +52,7 @@ dependencies:
|
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '3.1'
|
|
54
54
|
description: Retriable is a simple DSL to retry failed code blocks with randomized
|
|
55
|
-
exponential backoff. This is especially useful when interacting external
|
|
55
|
+
exponential backoff. This is especially useful when interacting with external APIs/services
|
|
56
56
|
or file system calls.
|
|
57
57
|
email:
|
|
58
58
|
- jack@jackchu.com
|