retriable 3.3.0 → 3.4.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/CHANGELOG.md +4 -0
- data/README.md +28 -1
- data/lib/retriable/config.rb +10 -8
- data/lib/retriable/version.rb +1 -1
- data/lib/retriable.rb +10 -6
- data/spec/config_spec.rb +4 -0
- data/spec/retriable_spec.rb +44 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1c6c4929adafa9b223f3fbe0ee530ef8a7d1e764cac8f5605be9ce33e3d1b0cc
|
|
4
|
+
data.tar.gz: 5d7013b0d5ce9ab8514a8deab7a32d77b0422cc02ead4aceb5a8c9b5ba28bff1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9a83302cfe426348ae0a35e6410edce33c8985d49289ab06100be7c5d49a586f93a429632814b2129e67b20dde8963eb2f5941bcc59d12f4c1db56ce05fd6784
|
|
7
|
+
data.tar.gz: 792c54b8b8805eb11c93d9ac100671b357b91a465424939a5c3995a2a50a8449033a0e94138bdf2510b2a7f89b4dc35787b0c490c53d63c9a0d2b85f044c80a9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# HEAD
|
|
2
2
|
|
|
3
|
+
## 3.4.0
|
|
4
|
+
|
|
5
|
+
- Add `retry_if` option to support custom retry predicates, including checks against wrapped `exception.cause` values.
|
|
6
|
+
|
|
3
7
|
## 3.3.0
|
|
4
8
|
|
|
5
9
|
- Refactor `Retriable.retriable` internals into focused private helpers to improve readability while preserving behavior.
|
data/README.md
CHANGED
|
@@ -32,7 +32,7 @@ require 'retriable'
|
|
|
32
32
|
In your Gemfile:
|
|
33
33
|
|
|
34
34
|
```ruby
|
|
35
|
-
gem 'retriable', '~> 3.
|
|
35
|
+
gem 'retriable', '~> 3.4'
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
## Usage
|
|
@@ -83,6 +83,7 @@ Here are the available options, in some vague order of relevance to most common
|
|
|
83
83
|
| ---------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
84
84
|
| **`tries`** | `3` | Number of attempts to make at running your code block (includes initial attempt). |
|
|
85
85
|
| **`on`** | `[StandardError]` | Type of exceptions to retry. [Read more](#configuring-which-options-to-retry-with-on). |
|
|
86
|
+
| **`retry_if`** | `nil` | Callable (for example a `Proc` or lambda) that receives the rescued exception and returns true/false to decide whether to retry. [Read more](#advanced-retry-matching-with-retry_if). |
|
|
86
87
|
| **`on_retry`** | `nil` | `Proc` to call after each try is rescued. [Read more](#callbacks). |
|
|
87
88
|
| **`sleep_disabled`** | `false` | When true, disable exponential backoff and attempt retries immediately. |
|
|
88
89
|
| **`base_interval`** | `0.5` | The initial interval in seconds between tries. |
|
|
@@ -104,6 +105,32 @@ Here are the available options, in some vague order of relevance to most common
|
|
|
104
105
|
- A single `Regexp` pattern (retries exceptions ONLY if their `message` matches the pattern)
|
|
105
106
|
- An array of patterns (retries exceptions ONLY if their `message` matches at least one of the patterns)
|
|
106
107
|
|
|
108
|
+
#### Advanced Retry Matching With :retry_if
|
|
109
|
+
|
|
110
|
+
Use **`:retry_if`** when retry logic depends on details that `:on` does not cover. The Proc receives the rescued exception and should return `true` to retry or `false` to re-raise immediately.
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
def caused_by?(error, klass)
|
|
114
|
+
current = error
|
|
115
|
+
while current
|
|
116
|
+
return true if current.is_a?(klass)
|
|
117
|
+
|
|
118
|
+
current = current.cause
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
false
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
Retriable.retriable(
|
|
125
|
+
on: [Faraday::ConnectionFailed],
|
|
126
|
+
retry_if: ->(exception) { caused_by?(exception, Errno::ECONNRESET) }
|
|
127
|
+
) do
|
|
128
|
+
# code here...
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`:retry_if` runs after the exception type has matched `:on`.
|
|
133
|
+
|
|
107
134
|
### Configuration
|
|
108
135
|
|
|
109
136
|
You can change the global defaults with a `#configure` block:
|
data/lib/retriable/config.rb
CHANGED
|
@@ -4,14 +4,15 @@ require_relative "exponential_backoff"
|
|
|
4
4
|
|
|
5
5
|
module Retriable
|
|
6
6
|
class Config
|
|
7
|
-
ATTRIBUTES = (ExponentialBackoff::ATTRIBUTES + [
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
ATTRIBUTES = (ExponentialBackoff::ATTRIBUTES + %i[
|
|
8
|
+
sleep_disabled
|
|
9
|
+
max_elapsed_time
|
|
10
|
+
intervals
|
|
11
|
+
timeout
|
|
12
|
+
on
|
|
13
|
+
retry_if
|
|
14
|
+
on_retry
|
|
15
|
+
contexts
|
|
15
16
|
]).freeze
|
|
16
17
|
|
|
17
18
|
attr_accessor(*ATTRIBUTES)
|
|
@@ -29,6 +30,7 @@ module Retriable
|
|
|
29
30
|
@intervals = nil
|
|
30
31
|
@timeout = nil
|
|
31
32
|
@on = [StandardError]
|
|
33
|
+
@retry_if = nil
|
|
32
34
|
@on_retry = nil
|
|
33
35
|
@contexts = {}
|
|
34
36
|
|
data/lib/retriable/version.rb
CHANGED
data/lib/retriable.rb
CHANGED
|
@@ -18,7 +18,8 @@ module Retriable
|
|
|
18
18
|
|
|
19
19
|
def with_context(context_key, options = {}, &block)
|
|
20
20
|
if !config.contexts.key?(context_key)
|
|
21
|
-
raise ArgumentError,
|
|
21
|
+
raise ArgumentError,
|
|
22
|
+
"#{context_key} not found in Retriable.config.contexts. Available contexts: #{config.contexts.keys}"
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
return unless block_given?
|
|
@@ -33,6 +34,7 @@ module Retriable
|
|
|
33
34
|
intervals = build_intervals(local_config, tries)
|
|
34
35
|
timeout = local_config.timeout
|
|
35
36
|
on = local_config.on
|
|
37
|
+
retry_if = local_config.retry_if
|
|
36
38
|
on_retry = local_config.on_retry
|
|
37
39
|
sleep_disabled = local_config.sleep_disabled
|
|
38
40
|
max_elapsed_time = local_config.max_elapsed_time
|
|
@@ -46,7 +48,7 @@ module Retriable
|
|
|
46
48
|
|
|
47
49
|
execute_tries(
|
|
48
50
|
tries: tries, intervals: intervals, timeout: timeout,
|
|
49
|
-
exception_list: exception_list, on: on, on_retry: on_retry,
|
|
51
|
+
exception_list: exception_list, on: on, retry_if: retry_if, on_retry: on_retry,
|
|
50
52
|
elapsed_time: elapsed_time, max_elapsed_time: max_elapsed_time,
|
|
51
53
|
sleep_disabled: sleep_disabled, &block
|
|
52
54
|
)
|
|
@@ -54,7 +56,7 @@ module Retriable
|
|
|
54
56
|
|
|
55
57
|
def execute_tries( # rubocop:disable Metrics/ParameterLists
|
|
56
58
|
tries:, intervals:, timeout:, exception_list:,
|
|
57
|
-
on:, on_retry:, elapsed_time:, max_elapsed_time:, sleep_disabled:, &block
|
|
59
|
+
on:, retry_if:, on_retry:, elapsed_time:, max_elapsed_time:, sleep_disabled:, &block
|
|
58
60
|
)
|
|
59
61
|
tries.times do |index|
|
|
60
62
|
try = index + 1
|
|
@@ -62,7 +64,7 @@ module Retriable
|
|
|
62
64
|
begin
|
|
63
65
|
return call_with_timeout(timeout, try, &block)
|
|
64
66
|
rescue *exception_list => e
|
|
65
|
-
raise unless retriable_exception?(e, on, exception_list)
|
|
67
|
+
raise unless retriable_exception?(e, on, exception_list, retry_if)
|
|
66
68
|
|
|
67
69
|
interval = intervals[index]
|
|
68
70
|
call_on_retry(on_retry, e, try, elapsed_time.call, interval)
|
|
@@ -105,9 +107,11 @@ module Retriable
|
|
|
105
107
|
# When `on` is a Hash, we need to verify the exception matches a pattern.
|
|
106
108
|
# For any non-Hash `on` value (e.g., Array of classes, single Exception class,
|
|
107
109
|
# or Module), the `rescue *exception_list` clause already guarantees the
|
|
108
|
-
# exception is retriable
|
|
109
|
-
|
|
110
|
+
# exception is retriable with respect to `on`; `retry_if`, if provided, is an
|
|
111
|
+
# additional gate that can still cause this method to return false.
|
|
112
|
+
def retriable_exception?(exception, on, exception_list, retry_if)
|
|
110
113
|
return false if on.is_a?(Hash) && !hash_exception_match?(exception, on, exception_list)
|
|
114
|
+
return false if retry_if && !retry_if.call(exception)
|
|
111
115
|
|
|
112
116
|
true
|
|
113
117
|
end
|
data/spec/config_spec.rb
CHANGED
|
@@ -40,6 +40,10 @@ describe Retriable::Config do
|
|
|
40
40
|
expect(default_config.on).to eq([StandardError])
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
it "retry_if defaults to nil" do
|
|
44
|
+
expect(default_config.retry_if).to be_nil
|
|
45
|
+
end
|
|
46
|
+
|
|
43
47
|
it "on_retry handler defaults to nil" do
|
|
44
48
|
expect(default_config.on_retry).to be_nil
|
|
45
49
|
end
|
data/spec/retriable_spec.rb
CHANGED
|
@@ -259,6 +259,50 @@ describe Retriable do
|
|
|
259
259
|
end
|
|
260
260
|
end
|
|
261
261
|
|
|
262
|
+
context "with a :retry_if parameter" do
|
|
263
|
+
it "retries only when retry_if returns true" do
|
|
264
|
+
described_class.retriable(tries: 3, retry_if: ->(_exception) { @tries < 3 }) do
|
|
265
|
+
increment_tries
|
|
266
|
+
raise StandardError, "StandardError occurred" if @tries < 3
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
expect(@tries).to eq(3)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
it "does not retry when retry_if returns false" do
|
|
273
|
+
expect do
|
|
274
|
+
described_class.retriable(tries: 3, retry_if: ->(_exception) { false }) do
|
|
275
|
+
increment_tries_with_exception
|
|
276
|
+
end
|
|
277
|
+
end.to raise_error(StandardError)
|
|
278
|
+
|
|
279
|
+
expect(@tries).to eq(1)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
it "can retry based on the wrapped exception cause" do
|
|
283
|
+
root_cause_class = Class.new(StandardError)
|
|
284
|
+
wrapper_class = Class.new(StandardError)
|
|
285
|
+
|
|
286
|
+
described_class.retriable(
|
|
287
|
+
on: [wrapper_class],
|
|
288
|
+
tries: 3,
|
|
289
|
+
retry_if: ->(exception) { exception.cause.is_a?(root_cause_class) },
|
|
290
|
+
) do
|
|
291
|
+
increment_tries
|
|
292
|
+
|
|
293
|
+
if @tries < 3
|
|
294
|
+
begin
|
|
295
|
+
raise root_cause_class, "root cause"
|
|
296
|
+
rescue root_cause_class
|
|
297
|
+
raise wrapper_class, "wrapped"
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
expect(@tries).to eq(3)
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
262
306
|
it "runs for a max elapsed time of 2 seconds" do
|
|
263
307
|
described_class.configure { |c| c.sleep_disabled = false }
|
|
264
308
|
|