retriable 4.1.1 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -0
- data/Gemfile +1 -1
- data/README.md +12 -2
- data/lib/retriable/config.rb +25 -6
- data/lib/retriable/core_ext/kernel.rb +2 -0
- data/lib/retriable/exponential_backoff.rb +13 -5
- data/lib/retriable/version.rb +1 -1
- data/lib/retriable.rb +5 -2
- data/sig/retriable.rbs +1 -1
- data/spec/config_spec.rb +29 -0
- data/spec/retriable_spec.rb +31 -7
- 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: a551efb6db5acea9e82dde8e48afdea1869f6b93461a3c48c33f8385fbea6392
|
|
4
|
+
data.tar.gz: fbf6bb8c5ade48420e0f2a43532bafff0e4159eb4cf149bce559116ec1c91e50
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 19d5ac72e1614b7fdb3984697db76ad3f6ae2e9b1f60ff1eb470e210738cd67154afc2776b55329fca345fad6263035a41eb0748626f427fd2d05cc0bfea9c89
|
|
7
|
+
data.tar.gz: c1d46594742b7c19985a63037199125b718e6c6395dd8276013719d8c7f9cd64376637471b90fa9249869767da9a2474cafc31afd04f373f73249d8ebdfa0337
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# HEAD
|
|
2
2
|
|
|
3
|
+
## 4.2.0
|
|
4
|
+
|
|
5
|
+
### Bug fixes
|
|
6
|
+
|
|
7
|
+
- The `Kernel` extension methods (`require "retriable/core_ext/kernel"`) are now
|
|
8
|
+
private, matching idiomatic `Kernel` helpers like `puts` and `rand`.
|
|
9
|
+
Previously `retriable` and `retriable_with_context` were public instance
|
|
10
|
+
methods, so they leaked onto every object's public API and could be invoked
|
|
11
|
+
with an explicit receiver (e.g. `"foo".retriable { ... }`). They remain
|
|
12
|
+
callable in the documented receiver-less form.
|
|
13
|
+
([#146](https://github.com/kamui/retriable/pull/146))
|
|
14
|
+
- `Retriable.with_context` (and `Kernel#retriable_with_context`) now raises
|
|
15
|
+
`ArgumentError` when called without a block, matching `with_override`.
|
|
16
|
+
Previously a missing block was silently ignored: the call returned `nil` and
|
|
17
|
+
the intended block never ran, hiding a caller bug. Behavior change: code that
|
|
18
|
+
relied on the silent no-op will now raise.
|
|
19
|
+
- `Config#validate!` now validates the structure of each entry in `contexts`,
|
|
20
|
+
so configured contexts are checked on every `Retriable.retriable`/
|
|
21
|
+
`with_context` call rather than only when a given context is first used. A
|
|
22
|
+
context whose options contain an unknown key (including a nested `contexts`
|
|
23
|
+
key) now raises `ArgumentError, "<key> is not a valid option"`, matching the
|
|
24
|
+
`with_override` path. Non-Hash `contexts` and non-Hash per-context values
|
|
25
|
+
remain leniently treated as empty options (no behavior change). Option
|
|
26
|
+
_values_ are still validated lazily at retry time, unchanged.
|
|
27
|
+
|
|
28
|
+
### Docs
|
|
29
|
+
|
|
30
|
+
- Document that `on_retry` receives `next_interval: nil` on the final rescued
|
|
31
|
+
attempt, when Retriable is about to give up because `tries` are exhausted.
|
|
32
|
+
`on_retry` still fires before `on_give_up` (unchanged behavior); the `nil`
|
|
33
|
+
contract is now called out in the `on_retry` documentation so handlers guard
|
|
34
|
+
arithmetic or logging on `next_interval`.
|
|
35
|
+
|
|
36
|
+
### Performance
|
|
37
|
+
|
|
38
|
+
- `Config#initialize` no longer allocates a throwaway `ExponentialBackoff` (and
|
|
39
|
+
runs its redundant `validate!`) just to read default values. Defaults now live
|
|
40
|
+
in a frozen `ExponentialBackoff::DEFAULTS` constant, removing an allocation and
|
|
41
|
+
redundant validation from the `retriable` hot path.
|
|
42
|
+
([#149](https://github.com/kamui/retriable/pull/149))
|
|
43
|
+
|
|
3
44
|
## 4.1.1
|
|
4
45
|
|
|
5
46
|
### Bug fixes
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -248,8 +248,13 @@ do not inherit it. This makes `#with_override` safe to use in parallel test
|
|
|
248
248
|
runners. Fibers running inside the same thread share the thread's active
|
|
249
249
|
override.
|
|
250
250
|
|
|
251
|
-
`#with_override` stores the provided options
|
|
252
|
-
|
|
251
|
+
`#with_override` stores the provided options hash **by reference** and reads
|
|
252
|
+
from it on every attempt while the block runs. Treat the hash and all of its
|
|
253
|
+
nested values as immutable for the duration of the block: do not mutate them
|
|
254
|
+
from inside the block, and do not mutate them from another thread or fiber that
|
|
255
|
+
shares this thread's active override. Mutating the options mid-block results in
|
|
256
|
+
undefined retry behavior. If options must be computed, build the hash before
|
|
257
|
+
calling `#with_override` and do not retain a reference you will later mutate.
|
|
253
258
|
|
|
254
259
|
For test-integration patterns (RSpec `around`, helper methods, Minitest, etc.),
|
|
255
260
|
see [docs/testing.md](docs/testing.md).
|
|
@@ -367,6 +372,8 @@ Retriable.retriable(on_retry: do_this_on_each_retry) do
|
|
|
367
372
|
end
|
|
368
373
|
```
|
|
369
374
|
|
|
375
|
+
> **Note:** On the final rescued attempt — when Retriable is about to give up because `tries` are exhausted — `on_retry` still fires (before `on_give_up`; see below), but `next_interval` is **`nil`** because there is no next retry. Guard any handler that does arithmetic or formatting on `next_interval` (for example `next_interval&.*(1000)`, or `if next_interval`), and avoid unconditionally logging messages like `"retrying in #{next_interval}s"` since no retry is coming. This mirrors the `nil` contract documented for [`on_give_up`](#callbacks) below.
|
|
376
|
+
|
|
370
377
|
#### Disabling a Configured Callback Per Call
|
|
371
378
|
|
|
372
379
|
If `on_retry` is set in `Retriable.configure`, every call uses it by default. To opt a specific call out — for example, a critical call site that should not log on retry — pass `on_retry: false` or `on_retry: nil`.
|
|
@@ -471,6 +478,9 @@ Retriable.with_context(:mysql, tries: 30) do
|
|
|
471
478
|
end
|
|
472
479
|
```
|
|
473
480
|
|
|
481
|
+
`#with_context` requires a block and raises `ArgumentError` if called without
|
|
482
|
+
one.
|
|
483
|
+
|
|
474
484
|
## Kernel Extension
|
|
475
485
|
|
|
476
486
|
If you want to call `Retriable.retriable` without the `Retriable` module prefix and you don't mind extending `Kernel`,
|
data/lib/retriable/config.rb
CHANGED
|
@@ -18,16 +18,19 @@ module Retriable
|
|
|
18
18
|
contexts
|
|
19
19
|
]).freeze
|
|
20
20
|
|
|
21
|
+
CONTEXT_ATTRIBUTES = (ATTRIBUTES - %i[contexts]).freeze
|
|
22
|
+
private_constant :CONTEXT_ATTRIBUTES
|
|
23
|
+
|
|
21
24
|
attr_accessor(*ATTRIBUTES)
|
|
22
25
|
|
|
23
26
|
def initialize(opts = {})
|
|
24
|
-
|
|
27
|
+
defaults = ExponentialBackoff::DEFAULTS
|
|
25
28
|
|
|
26
|
-
@tries =
|
|
27
|
-
@base_interval =
|
|
28
|
-
@max_interval =
|
|
29
|
-
@rand_factor =
|
|
30
|
-
@multiplier =
|
|
29
|
+
@tries = defaults[:tries]
|
|
30
|
+
@base_interval = defaults[:base_interval]
|
|
31
|
+
@max_interval = defaults[:max_interval]
|
|
32
|
+
@rand_factor = defaults[:rand_factor]
|
|
33
|
+
@multiplier = defaults[:multiplier]
|
|
31
34
|
@sleep_disabled = false
|
|
32
35
|
@max_elapsed_time = 900 # 15 min
|
|
33
36
|
@intervals = nil
|
|
@@ -51,6 +54,7 @@ module Retriable
|
|
|
51
54
|
end
|
|
52
55
|
|
|
53
56
|
def validate!
|
|
57
|
+
validate_contexts
|
|
54
58
|
validate_callable(:retry_if, retry_if)
|
|
55
59
|
validate_callable(:on_retry, on_retry)
|
|
56
60
|
validate_callable(:on_give_up, on_give_up)
|
|
@@ -70,6 +74,21 @@ module Retriable
|
|
|
70
74
|
|
|
71
75
|
private
|
|
72
76
|
|
|
77
|
+
def validate_contexts
|
|
78
|
+
return unless contexts.is_a?(Hash)
|
|
79
|
+
return if contexts.empty?
|
|
80
|
+
|
|
81
|
+
contexts.each_value do |options|
|
|
82
|
+
next unless options.is_a?(Hash)
|
|
83
|
+
|
|
84
|
+
options.each_key do |k|
|
|
85
|
+
next if CONTEXT_ATTRIBUTES.include?(k)
|
|
86
|
+
|
|
87
|
+
raise ArgumentError, "#{k} is not a valid option"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
73
92
|
def validate_backoff_options
|
|
74
93
|
validate_non_negative_number(:base_interval, base_interval)
|
|
75
94
|
validate_non_negative_number(:multiplier, multiplier)
|
|
@@ -14,14 +14,22 @@ module Retriable
|
|
|
14
14
|
rand_factor
|
|
15
15
|
].freeze
|
|
16
16
|
|
|
17
|
+
DEFAULTS = {
|
|
18
|
+
tries: 3,
|
|
19
|
+
base_interval: 0.5,
|
|
20
|
+
max_interval: 60,
|
|
21
|
+
rand_factor: 0.5,
|
|
22
|
+
multiplier: 1.5
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
17
25
|
attr_accessor(*ATTRIBUTES)
|
|
18
26
|
|
|
19
27
|
def initialize(opts = {})
|
|
20
|
-
@tries =
|
|
21
|
-
@base_interval =
|
|
22
|
-
@max_interval =
|
|
23
|
-
@rand_factor =
|
|
24
|
-
@multiplier =
|
|
28
|
+
@tries = DEFAULTS[:tries]
|
|
29
|
+
@base_interval = DEFAULTS[:base_interval]
|
|
30
|
+
@max_interval = DEFAULTS[:max_interval]
|
|
31
|
+
@rand_factor = DEFAULTS[:rand_factor]
|
|
32
|
+
@multiplier = DEFAULTS[:multiplier]
|
|
25
33
|
|
|
26
34
|
opts.each do |k, v|
|
|
27
35
|
raise ArgumentError, "#{k} is not a valid option" if !ATTRIBUTES.include?(k)
|
data/lib/retriable/version.rb
CHANGED
data/lib/retriable.rb
CHANGED
|
@@ -41,6 +41,8 @@ module Retriable
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def with_context(context_key, options = {}, &)
|
|
44
|
+
raise ArgumentError, "with_context requires a block" unless block_given?
|
|
45
|
+
|
|
44
46
|
contexts = available_contexts
|
|
45
47
|
|
|
46
48
|
if !contexts.key?(context_key)
|
|
@@ -48,8 +50,6 @@ module Retriable
|
|
|
48
50
|
"#{context_key} not found in Retriable contexts (including overrides). Available contexts: #{contexts.keys}"
|
|
49
51
|
end
|
|
50
52
|
|
|
51
|
-
return unless block_given?
|
|
52
|
-
|
|
53
53
|
retriable(context_options_for(context_key, options), &)
|
|
54
54
|
end
|
|
55
55
|
|
|
@@ -97,6 +97,9 @@ module Retriable
|
|
|
97
97
|
rescue *exception_list => e
|
|
98
98
|
raise unless retriable_exception?(e, on, exception_list, retry_if)
|
|
99
99
|
|
|
100
|
+
# On the final attempt `interval_for` returns nil (no next retry), and
|
|
101
|
+
# `on_retry` intentionally fires before the give-up check below, so it
|
|
102
|
+
# receives `interval: nil`. See the on_retry/on_give_up README contract.
|
|
100
103
|
interval = interval_for.call(try - 1)
|
|
101
104
|
call_on_retry(on_retry, e, try, elapsed_time.call, interval)
|
|
102
105
|
|
data/sig/retriable.rbs
CHANGED
|
@@ -5,7 +5,7 @@ module Retriable
|
|
|
5
5
|
def self.configure: () { (Config) -> void } -> void
|
|
6
6
|
def self.config: () -> Config
|
|
7
7
|
def self.with_override: (Hash[Symbol, untyped] options) { () -> untyped } -> untyped
|
|
8
|
-
def self.with_context: (Symbol context_key, ?Hash[Symbol, untyped] options)
|
|
8
|
+
def self.with_context: (Symbol context_key, ?Hash[Symbol, untyped] options) { (Integer) -> untyped } -> untyped
|
|
9
9
|
def self.retriable: (?Hash[Symbol, untyped] options) { (Integer) -> untyped } -> untyped
|
|
10
10
|
|
|
11
11
|
class Config
|
data/spec/config_spec.rb
CHANGED
|
@@ -180,4 +180,33 @@ describe Retriable::Config do
|
|
|
180
180
|
end
|
|
181
181
|
end
|
|
182
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
|
|
183
212
|
end
|
data/spec/retriable_spec.rb
CHANGED
|
@@ -52,9 +52,9 @@ describe Retriable do
|
|
|
52
52
|
|
|
53
53
|
# These two specs lock in the anonymous block forwarding (`&`) semantics
|
|
54
54
|
# across both delegation layers: Kernel#retriable_with_context ->
|
|
55
|
-
# Retriable.with_context. If the `&` is dropped at either layer, the
|
|
56
|
-
#
|
|
57
|
-
#
|
|
55
|
+
# Retriable.with_context. If the `&` is dropped at either layer, the block
|
|
56
|
+
# is not forwarded and the `block_given?` guard in with_context raises
|
|
57
|
+
# ArgumentError instead of running the block.
|
|
58
58
|
it "forwards a block through Kernel#retriable_with_context" do
|
|
59
59
|
require_relative "../lib/retriable/core_ext/kernel"
|
|
60
60
|
Retriable.configure { |c| c.contexts[:sql] = { tries: 1 } }
|
|
@@ -64,13 +64,23 @@ describe Retriable do
|
|
|
64
64
|
expect(@tries).to eq(1)
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
it "
|
|
67
|
+
it "raises an ArgumentError when Kernel#retriable_with_context is called without a block" do
|
|
68
68
|
require_relative "../lib/retriable/core_ext/kernel"
|
|
69
69
|
Retriable.configure { |c| c.contexts[:sql] = { tries: 1 } }
|
|
70
70
|
|
|
71
|
-
expect
|
|
71
|
+
expect { retriable_with_context(:sql) }
|
|
72
|
+
.to raise_error(ArgumentError, /with_context requires a block/)
|
|
72
73
|
expect(@tries).to eq(0)
|
|
73
74
|
end
|
|
75
|
+
|
|
76
|
+
it "is not callable with an explicit receiver" do
|
|
77
|
+
require_relative "../lib/retriable/core_ext/kernel"
|
|
78
|
+
|
|
79
|
+
expect { "foo".retriable { increment_tries } }
|
|
80
|
+
.to raise_error(NoMethodError, /private method/)
|
|
81
|
+
expect { "foo".retriable_with_context(:sql) { increment_tries } }
|
|
82
|
+
.to raise_error(NoMethodError, /private method/)
|
|
83
|
+
end
|
|
74
84
|
end
|
|
75
85
|
|
|
76
86
|
context "#retriable" do
|
|
@@ -1248,8 +1258,15 @@ describe Retriable do
|
|
|
1248
1258
|
expect(@tries).to eq(1)
|
|
1249
1259
|
end
|
|
1250
1260
|
|
|
1251
|
-
it "
|
|
1252
|
-
expect
|
|
1261
|
+
it "raises an ArgumentError when called without a block" do
|
|
1262
|
+
expect { described_class.with_context(:sql) }
|
|
1263
|
+
.to raise_error(ArgumentError, /with_context requires a block/)
|
|
1264
|
+
expect(@tries).to eq(0)
|
|
1265
|
+
end
|
|
1266
|
+
|
|
1267
|
+
it "checks for a block before looking up the context" do
|
|
1268
|
+
expect { described_class.with_context(:missing) }
|
|
1269
|
+
.to raise_error(ArgumentError, /with_context requires a block/)
|
|
1253
1270
|
expect(@tries).to eq(0)
|
|
1254
1271
|
end
|
|
1255
1272
|
|
|
@@ -1290,6 +1307,13 @@ describe Retriable do
|
|
|
1290
1307
|
expect(@tries).to eq(1)
|
|
1291
1308
|
end
|
|
1292
1309
|
|
|
1310
|
+
it "surfaces an invalid context on any retriable call before that context is used" do
|
|
1311
|
+
described_class.configure { |c| c.contexts[:unused] = { contexts: {} } }
|
|
1312
|
+
|
|
1313
|
+
expect { described_class.retriable { :ok } }
|
|
1314
|
+
.to raise_error(ArgumentError, /contexts is not a valid option/)
|
|
1315
|
+
end
|
|
1316
|
+
|
|
1293
1317
|
it "invokes on_give_up configured on a context" do
|
|
1294
1318
|
callback_called = false
|
|
1295
1319
|
described_class.configure do |c|
|
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: 4.
|
|
4
|
+
version: 4.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jack Chu
|
|
@@ -65,7 +65,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
65
65
|
- !ruby/object:Gem::Version
|
|
66
66
|
version: '0'
|
|
67
67
|
requirements: []
|
|
68
|
-
rubygems_version:
|
|
68
|
+
rubygems_version: 3.6.9
|
|
69
69
|
specification_version: 4
|
|
70
70
|
summary: Retriable is a simple DSL to retry failed code blocks with randomized exponential
|
|
71
71
|
backoff
|