retriable 3.5.1 → 3.6.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 +38 -79
- data/docs/testing.md +212 -0
- data/lib/retriable/version.rb +1 -1
- data/lib/retriable.rb +27 -9
- data/spec/retriable_spec.rb +248 -72
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 575fe838c7ddda74e7e26abf2a3b74a9e4b6866bf20d33a86bc59d1b7c45770d
|
|
4
|
+
data.tar.gz: f8d81648c9ea122680f3e8eb2f71b7cbbfffc0049313123da850378bf5b3f77d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bff5a1d9a0efec882841085a306290d11ccd9f20c0622b8a27c733c13981a68c681a562d80eb267c17b866a816a03d514a92b4a17883a1d4180d835f2b76fd02
|
|
7
|
+
data.tar.gz: 9da2cfea5cf807d352ed899926b0fa9e45d825935697788163dfbd5cf7c47afe7b0cfd7b20af5b29ce61f84d5d923181b7a671e21e3cafd709b15f922c68abf8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# HEAD
|
|
2
2
|
|
|
3
|
+
## 3.6.0
|
|
4
|
+
|
|
5
|
+
- Breaking: `Retriable.override` and `Retriable.reset_override` are removed and replaced by block-scoped `Retriable.with_override(opts) { ... }`. The new API requires a block, restores the previous override (or absence of override) when the block exits via `ensure`, and is thread-local — overrides set in one thread do not affect other threads, and child threads do not inherit them. Fibers within a thread still share the thread's active override. Nested `with_override` calls correctly restore the outer override on inner exit. See the README and `docs/testing.md` for migration and testing patterns. This replaces the override API introduced in 3.5.0.
|
|
6
|
+
|
|
3
7
|
## 3.5.1
|
|
4
8
|
|
|
5
9
|
- Fix: Validate retry timing and count options before use to reject invalid retry configurations. `tries` must now be a positive integer unless a custom `intervals` array is provided.
|
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.6'
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
## Usage
|
|
@@ -149,31 +149,46 @@ end
|
|
|
149
149
|
|
|
150
150
|
### Override
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
`#
|
|
152
|
+
`#with_override` is a block-scoped API for forcing retry options that should
|
|
153
|
+
take precedence over both `#configure` defaults and per-call options. It is
|
|
154
|
+
primarily intended for tests — it lets a test force values like `tries: 1` or
|
|
155
|
+
`base_interval: 0` so the suite runs quickly and predictably, regardless of
|
|
156
|
+
the application's `#configure` defaults. In application code, prefer
|
|
157
|
+
`#configure` for app-level defaults and per-call options for caller-specific
|
|
158
|
+
values.
|
|
154
159
|
|
|
155
160
|
```ruby
|
|
156
|
-
Retriable.
|
|
161
|
+
Retriable.with_override(tries: 1, base_interval: 0) do
|
|
162
|
+
Retriable.retriable do
|
|
163
|
+
# code here...
|
|
164
|
+
end
|
|
165
|
+
end
|
|
157
166
|
```
|
|
158
167
|
|
|
159
|
-
|
|
168
|
+
Precedence inside the block:
|
|
160
169
|
|
|
161
170
|
```
|
|
162
|
-
|
|
171
|
+
with_override > local options > configure defaults
|
|
163
172
|
```
|
|
164
173
|
|
|
165
|
-
`#
|
|
166
|
-
|
|
167
|
-
|
|
174
|
+
`#with_override` requires a block and raises `ArgumentError` if called without
|
|
175
|
+
one. The override is active only while the block is executing, and is
|
|
176
|
+
automatically restored to its previous value when the block returns or raises.
|
|
177
|
+
Nested `#with_override` calls work as expected: the inner block temporarily
|
|
178
|
+
replaces the active override and the outer override is restored when the
|
|
179
|
+
inner block exits.
|
|
168
180
|
|
|
169
|
-
`#
|
|
170
|
-
|
|
181
|
+
`#with_override` is scoped to the **current thread**. The active override
|
|
182
|
+
does not affect any other thread, and child threads spawned inside the block
|
|
183
|
+
do not inherit it. This makes `#with_override` safe to use in parallel test
|
|
184
|
+
runners. Fibers running inside the same thread share the thread's active
|
|
185
|
+
override.
|
|
171
186
|
|
|
172
|
-
|
|
187
|
+
`#with_override` stores the provided options directly. Do not mutate the
|
|
188
|
+
options hash or nested values for the duration of the block.
|
|
173
189
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
```
|
|
190
|
+
For test-integration patterns (RSpec `around`, helper methods, Minitest, etc.),
|
|
191
|
+
see [docs/testing.md](docs/testing.md).
|
|
177
192
|
|
|
178
193
|
### Example Usage
|
|
179
194
|
|
|
@@ -369,72 +384,16 @@ retriable_with_context(:api) do
|
|
|
369
384
|
end
|
|
370
385
|
```
|
|
371
386
|
|
|
372
|
-
##
|
|
373
|
-
|
|
374
|
-
When you are running tests for your app it often takes a long time to retry blocks that fail. This is because Retriable will default to 3 tries with exponential backoff. Ideally your tests will run as quickly as possible.
|
|
375
|
-
|
|
376
|
-
If you want to short-circuit retries in tests, including calls that pass local options, use `Retriable.override` and set `tries` to `1`.
|
|
377
|
-
|
|
378
|
-
Under Rails, keep shared defaults in `Retriable.configure` and apply test-only overrides conditionally:
|
|
379
|
-
|
|
380
|
-
```ruby
|
|
381
|
-
# config/initializers/retriable.rb
|
|
382
|
-
Retriable.configure do |c|
|
|
383
|
-
c.tries = 3
|
|
384
|
-
c.base_interval = 0.5
|
|
385
|
-
c.rand_factor = 0.5
|
|
386
|
-
end
|
|
387
|
-
|
|
388
|
-
if Rails.env.test?
|
|
389
|
-
Retriable.override(tries: 1, base_interval: 0, rand_factor: 0)
|
|
390
|
-
end
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
If you need to run a specific test with normal retry behavior, call `Retriable.reset_override` for that example and then reapply your test override afterward.
|
|
387
|
+
## Testing
|
|
394
388
|
|
|
395
|
-
|
|
389
|
+
`Retriable.with_override` is designed to short-circuit retries in your test
|
|
390
|
+
suite so failing blocks do not slow tests down. The simplest pattern is an
|
|
391
|
+
RSpec `around(:each)` hook (or your test framework's equivalent) that wraps
|
|
392
|
+
every example in `with_override(tries: 1, base_interval: 0)`.
|
|
396
393
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
```
|
|
401
|
-
|
|
402
|
-
If you have defined contexts for your configuration, top-level override values (such as `tries: 1`) already take precedence over context-specific values. However, if you need to override context-specific options (for example, clearing a context's `:intervals` array or changing its `:on` exception list), pass `:contexts` to `Retriable.override`:
|
|
403
|
-
|
|
404
|
-
For example assuming you have configured a `google_api` context:
|
|
405
|
-
|
|
406
|
-
```ruby
|
|
407
|
-
# config/initializers/retriable.rb
|
|
408
|
-
Retriable.configure do |c|
|
|
409
|
-
c.contexts[:google_api] = {
|
|
410
|
-
tries: 5,
|
|
411
|
-
base_interval: 3,
|
|
412
|
-
on: [
|
|
413
|
-
Net::ReadTimeout,
|
|
414
|
-
Signet::AuthorizationError,
|
|
415
|
-
Errno::ECONNRESET,
|
|
416
|
-
OpenSSL::SSL::SSLError
|
|
417
|
-
]
|
|
418
|
-
}
|
|
419
|
-
end
|
|
420
|
-
```
|
|
421
|
-
|
|
422
|
-
Then in your test environment, you can override both top-level defaults and per-context options:
|
|
423
|
-
|
|
424
|
-
```ruby
|
|
425
|
-
# Build context overrides from existing configured context keys
|
|
426
|
-
context_overrides = {}
|
|
427
|
-
Retriable.config.contexts.each_key do |key|
|
|
428
|
-
context_overrides[key] = { tries: 1, base_interval: 0 }
|
|
429
|
-
end
|
|
430
|
-
|
|
431
|
-
Retriable.override(
|
|
432
|
-
multiplier: 1.0,
|
|
433
|
-
rand_factor: 0.0,
|
|
434
|
-
base_interval: 0,
|
|
435
|
-
contexts: context_overrides,
|
|
436
|
-
)
|
|
437
|
-
```
|
|
394
|
+
For Rails integration, opting out of the override for specific tests, and
|
|
395
|
+
overriding configured contexts in tests, see
|
|
396
|
+
[docs/testing.md](docs/testing.md).
|
|
438
397
|
|
|
439
398
|
## Credits
|
|
440
399
|
|
data/docs/testing.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# Testing with Retriable
|
|
2
|
+
|
|
3
|
+
`Retriable.with_override` exists primarily for tests. It lets a test force
|
|
4
|
+
retry options like `tries: 1` or `base_interval: 0` so the suite runs quickly
|
|
5
|
+
and predictably, regardless of what the application's `Retriable.configure`
|
|
6
|
+
defaults are.
|
|
7
|
+
|
|
8
|
+
`with_override` is block-scoped: the override is active inside the block and
|
|
9
|
+
restored to its previous value (which is usually "no override") when the block
|
|
10
|
+
exits, even if the block raises. It is also thread-local — overrides set in
|
|
11
|
+
one thread do not affect other threads — so it is safe for parallel test
|
|
12
|
+
runners. See the README for the full API contract.
|
|
13
|
+
|
|
14
|
+
## RSpec
|
|
15
|
+
|
|
16
|
+
### Apply an override to every test
|
|
17
|
+
|
|
18
|
+
Use `around(:each)` in `RSpec.configure` so every test in the suite runs inside
|
|
19
|
+
the override. This is the most common pattern:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
RSpec.configure do |config|
|
|
23
|
+
config.around(:each) do |example|
|
|
24
|
+
Retriable.with_override(tries: 1, base_interval: 0) do
|
|
25
|
+
example.run
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Apply an override to a specific context
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
describe MyClient do
|
|
35
|
+
context "when external calls should not retry" do
|
|
36
|
+
around(:each) do |example|
|
|
37
|
+
Retriable.with_override(tries: 1) { example.run }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "fails fast" do
|
|
41
|
+
# `with_override(tries: 1)` is active here
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Apply an override to a single test
|
|
48
|
+
|
|
49
|
+
Wrap the test body directly:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
it "does the thing without waiting" do
|
|
53
|
+
Retriable.with_override(tries: 1, base_interval: 0) do
|
|
54
|
+
# test body
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Reusable helper
|
|
60
|
+
|
|
61
|
+
Wrap a common configuration in a helper to keep tests readable:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
module RetriableHelpers
|
|
65
|
+
def with_fast_retries(&block)
|
|
66
|
+
Retriable.with_override(tries: 1, base_interval: 0, &block)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
RSpec.configure do |config|
|
|
71
|
+
config.include RetriableHelpers
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# In a spec:
|
|
75
|
+
it "does the thing" do
|
|
76
|
+
with_fast_retries do
|
|
77
|
+
# test body
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Minitest
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
class MyClientTest < Minitest::Test
|
|
86
|
+
def around
|
|
87
|
+
Retriable.with_override(tries: 1, base_interval: 0) { yield }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def test_fails_fast
|
|
91
|
+
# `with_override(tries: 1)` is active here
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Older Minitest versions without `around` can wrap the test body directly:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
def test_fails_fast
|
|
100
|
+
Retriable.with_override(tries: 1) do
|
|
101
|
+
# test body
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Short-Circuiting Retriable in Your Test Suite
|
|
107
|
+
|
|
108
|
+
When you are running tests for your app, the default retry behavior (3 tries
|
|
109
|
+
with exponential backoff) makes failing blocks take a long time. To short-circuit
|
|
110
|
+
retries — including calls that pass local options — set `tries: 1` and disable
|
|
111
|
+
backoff using `with_override`.
|
|
112
|
+
|
|
113
|
+
### Under Rails
|
|
114
|
+
|
|
115
|
+
Keep shared defaults in `Retriable.configure` and apply test-only overrides via
|
|
116
|
+
RSpec's `around` hook (or your test framework's equivalent):
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
# config/initializers/retriable.rb
|
|
120
|
+
Retriable.configure do |c|
|
|
121
|
+
c.tries = 3
|
|
122
|
+
c.base_interval = 0.5
|
|
123
|
+
c.rand_factor = 0.5
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# spec/spec_helper.rb (or equivalent)
|
|
127
|
+
RSpec.configure do |config|
|
|
128
|
+
config.around(:each) do |example|
|
|
129
|
+
Retriable.with_override(tries: 1, base_interval: 0, rand_factor: 0) do
|
|
130
|
+
example.run
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
If a specific test needs normal retry behavior, opt out by running outside the
|
|
137
|
+
`around` hook. The cleanest way is to tag the example and skip the hook for
|
|
138
|
+
tagged examples:
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
config.around(:each, retriable: :real) { |example| example.run }
|
|
142
|
+
config.around(:each) do |example|
|
|
143
|
+
next example.run if example.metadata[:retriable] == :real
|
|
144
|
+
|
|
145
|
+
Retriable.with_override(tries: 1, base_interval: 0, rand_factor: 0) do
|
|
146
|
+
example.run
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it "exercises the real retry behavior", retriable: :real do
|
|
151
|
+
# `with_override` is not applied here
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Overriding Configured Contexts in Tests
|
|
156
|
+
|
|
157
|
+
If you have configured contexts, top-level override values (such as `tries: 1`)
|
|
158
|
+
already take precedence over context-specific values. To override
|
|
159
|
+
context-specific options as well (for example, clearing a context's
|
|
160
|
+
`:intervals` array or shrinking its `:on` exception list), pass `:contexts` to
|
|
161
|
+
`with_override`.
|
|
162
|
+
|
|
163
|
+
Given a configured `google_api` context:
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
# config/initializers/retriable.rb
|
|
167
|
+
Retriable.configure do |c|
|
|
168
|
+
c.contexts[:google_api] = {
|
|
169
|
+
tries: 5,
|
|
170
|
+
base_interval: 3,
|
|
171
|
+
on: [
|
|
172
|
+
Net::ReadTimeout,
|
|
173
|
+
Signet::AuthorizationError,
|
|
174
|
+
Errno::ECONNRESET,
|
|
175
|
+
OpenSSL::SSL::SSLError,
|
|
176
|
+
],
|
|
177
|
+
}
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
You can override both top-level defaults and per-context options in your
|
|
182
|
+
test setup:
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
RSpec.configure do |config|
|
|
186
|
+
config.around(:each) do |example|
|
|
187
|
+
context_overrides = Retriable.config.contexts.each_key.with_object({}) do |key, h|
|
|
188
|
+
h[key] = { tries: 1, base_interval: 0 }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
Retriable.with_override(
|
|
192
|
+
multiplier: 1.0,
|
|
193
|
+
rand_factor: 0.0,
|
|
194
|
+
base_interval: 0,
|
|
195
|
+
contexts: context_overrides,
|
|
196
|
+
) do
|
|
197
|
+
example.run
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Notes
|
|
204
|
+
|
|
205
|
+
- The override is automatically cleared when the block exits, including when
|
|
206
|
+
the block raises. You do not need to clean up after the block.
|
|
207
|
+
- `with_override` calls nest: an inner block temporarily replaces the active
|
|
208
|
+
override, and the outer override is restored when the inner block exits.
|
|
209
|
+
- Overrides are thread-local. Child threads spawned inside the block do not
|
|
210
|
+
inherit it. If a test spawns background threads that themselves call
|
|
211
|
+
`Retriable.retriable`, wrap each background thread's body in its own
|
|
212
|
+
`with_override` call.
|
data/lib/retriable/version.rb
CHANGED
data/lib/retriable.rb
CHANGED
|
@@ -6,6 +6,13 @@ require_relative "retriable/exponential_backoff"
|
|
|
6
6
|
require_relative "retriable/version"
|
|
7
7
|
|
|
8
8
|
module Retriable
|
|
9
|
+
# Thread-local storage key for the active #with_override block.
|
|
10
|
+
# We deliberately use Thread#thread_variable_set/get (true thread-local)
|
|
11
|
+
# rather than Thread.current[] (fiber-local) so that fibers within a thread
|
|
12
|
+
# share the same override. Changing this to Thread.current[] would silently
|
|
13
|
+
# break callers that use fiber-based concurrency.
|
|
14
|
+
OVERRIDE_THREAD_KEY = :retriable_override
|
|
15
|
+
|
|
9
16
|
module_function
|
|
10
17
|
|
|
11
18
|
def configure
|
|
@@ -16,15 +23,19 @@ module Retriable
|
|
|
16
23
|
@config ||= Config.new
|
|
17
24
|
end
|
|
18
25
|
|
|
19
|
-
def
|
|
20
|
-
raise ArgumentError, "empty override options are not allowed
|
|
26
|
+
def with_override(opts = {})
|
|
27
|
+
raise ArgumentError, "empty override options are not allowed" if opts.empty?
|
|
28
|
+
raise ArgumentError, "with_override requires a block" unless block_given?
|
|
21
29
|
|
|
22
30
|
validate_override_options(opts)
|
|
23
|
-
@override_config = opts
|
|
24
|
-
end
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
previous = Thread.current.thread_variable_get(OVERRIDE_THREAD_KEY)
|
|
33
|
+
Thread.current.thread_variable_set(OVERRIDE_THREAD_KEY, opts)
|
|
34
|
+
begin
|
|
35
|
+
yield
|
|
36
|
+
ensure
|
|
37
|
+
Thread.current.thread_variable_set(OVERRIDE_THREAD_KEY, previous)
|
|
38
|
+
end
|
|
28
39
|
end
|
|
29
40
|
|
|
30
41
|
def with_context(context_key, options = {}, &block)
|
|
@@ -41,10 +52,11 @@ module Retriable
|
|
|
41
52
|
end
|
|
42
53
|
|
|
43
54
|
def retriable(opts = {}, &block)
|
|
44
|
-
|
|
55
|
+
override_config = current_override
|
|
56
|
+
local_config = if opts.empty? && !override_config
|
|
45
57
|
config
|
|
46
58
|
else
|
|
47
|
-
Config.new(apply_override_options(config.to_h.merge(opts),
|
|
59
|
+
Config.new(apply_override_options(config.to_h.merge(opts), override_config))
|
|
48
60
|
end
|
|
49
61
|
|
|
50
62
|
# Config is mutable through `configure`, so validate again immediately before use.
|
|
@@ -199,10 +211,15 @@ module Retriable
|
|
|
199
211
|
end
|
|
200
212
|
|
|
201
213
|
def override_contexts
|
|
202
|
-
|
|
214
|
+
override_config = current_override
|
|
215
|
+
contexts = override_config && override_config[:contexts]
|
|
203
216
|
contexts.is_a?(Hash) ? contexts : {}
|
|
204
217
|
end
|
|
205
218
|
|
|
219
|
+
def current_override
|
|
220
|
+
Thread.current.thread_variable_get(OVERRIDE_THREAD_KEY)
|
|
221
|
+
end
|
|
222
|
+
|
|
206
223
|
private_class_method(
|
|
207
224
|
:validate_override_options,
|
|
208
225
|
:validate_context_override_options,
|
|
@@ -218,5 +235,6 @@ module Retriable
|
|
|
218
235
|
:context_options_for,
|
|
219
236
|
:config_contexts,
|
|
220
237
|
:override_contexts,
|
|
238
|
+
:current_override,
|
|
221
239
|
)
|
|
222
240
|
end
|
data/spec/retriable_spec.rb
CHANGED
|
@@ -9,7 +9,7 @@ describe Retriable do
|
|
|
9
9
|
|
|
10
10
|
before(:each) do
|
|
11
11
|
described_class.instance_variable_set(:@config, nil)
|
|
12
|
-
|
|
12
|
+
Thread.current.thread_variable_set(Retriable::OVERRIDE_THREAD_KEY, nil)
|
|
13
13
|
described_class.configure { |c| c.sleep_disabled = true }
|
|
14
14
|
@tries = 0
|
|
15
15
|
@next_interval_table = {}
|
|
@@ -415,8 +415,7 @@ describe Retriable do
|
|
|
415
415
|
with_context
|
|
416
416
|
configure
|
|
417
417
|
config
|
|
418
|
-
|
|
419
|
-
reset_override
|
|
418
|
+
with_override
|
|
420
419
|
]
|
|
421
420
|
|
|
422
421
|
expect(described_class.singleton_methods(false)).to match_array(public_api_methods)
|
|
@@ -427,25 +426,23 @@ describe Retriable do
|
|
|
427
426
|
end
|
|
428
427
|
end
|
|
429
428
|
|
|
430
|
-
context "#
|
|
431
|
-
after(:each) do
|
|
432
|
-
described_class.reset_override
|
|
433
|
-
end
|
|
434
|
-
|
|
429
|
+
context "#with_override" do
|
|
435
430
|
it "takes precedence over both global config and local options" do
|
|
436
431
|
described_class.configure { |c| c.tries = 2 }
|
|
437
|
-
described_class.override(tries: 1)
|
|
438
432
|
|
|
439
|
-
|
|
433
|
+
described_class.with_override(tries: 1) do
|
|
434
|
+
expect { described_class.retriable(tries: 10) { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
435
|
+
end
|
|
436
|
+
|
|
440
437
|
expect(@tries).to eq(1)
|
|
441
438
|
end
|
|
442
439
|
|
|
443
440
|
it "lets override tries take precedence over local intervals" do
|
|
444
|
-
described_class.
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
end
|
|
441
|
+
described_class.with_override(tries: 1) do
|
|
442
|
+
expect do
|
|
443
|
+
described_class.retriable(intervals: [0.5, 1.0]) { increment_tries_with_exception }
|
|
444
|
+
end.to raise_error(StandardError)
|
|
445
|
+
end
|
|
449
446
|
|
|
450
447
|
expect(@tries).to eq(1)
|
|
451
448
|
end
|
|
@@ -454,9 +451,11 @@ describe Retriable do
|
|
|
454
451
|
described_class.configure do |c|
|
|
455
452
|
c.contexts[:api] = { intervals: [0.5, 1.0] }
|
|
456
453
|
end
|
|
457
|
-
described_class.override(tries: 1)
|
|
458
454
|
|
|
459
|
-
|
|
455
|
+
described_class.with_override(tries: 1) do
|
|
456
|
+
expect { described_class.with_context(:api) { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
457
|
+
end
|
|
458
|
+
|
|
460
459
|
expect(@tries).to eq(1)
|
|
461
460
|
end
|
|
462
461
|
|
|
@@ -464,31 +463,34 @@ describe Retriable do
|
|
|
464
463
|
described_class.configure do |c|
|
|
465
464
|
c.contexts[:api] = { intervals: [0.5, 1.0] }
|
|
466
465
|
end
|
|
467
|
-
described_class.override(contexts: { api: { tries: 1 } })
|
|
468
466
|
|
|
469
|
-
|
|
467
|
+
described_class.with_override(contexts: { api: { tries: 1 } }) do
|
|
468
|
+
expect { described_class.with_context(:api) { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
469
|
+
end
|
|
470
|
+
|
|
470
471
|
expect(@tries).to eq(1)
|
|
471
472
|
end
|
|
472
473
|
|
|
473
474
|
it "replaces hash-valued options instead of deep-merging them" do
|
|
474
|
-
described_class.
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
end
|
|
475
|
+
described_class.with_override(on: { NonStandardError => nil }) do
|
|
476
|
+
expect do
|
|
477
|
+
described_class.retriable(on: { StandardError => nil }, tries: 2) { increment_tries_with_exception }
|
|
478
|
+
end.to raise_error(StandardError)
|
|
479
|
+
end
|
|
479
480
|
|
|
480
481
|
expect(@tries).to eq(1)
|
|
481
482
|
end
|
|
482
483
|
|
|
483
484
|
it "can override local intervals with nil to use configured backoff" do
|
|
484
485
|
described_class.configure { |c| c.tries = 3 }
|
|
485
|
-
described_class.override(intervals: nil)
|
|
486
486
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
487
|
+
described_class.with_override(intervals: nil) do
|
|
488
|
+
expect do
|
|
489
|
+
described_class.retriable(intervals: [0.5, 1.0], on_retry: time_table_handler) do
|
|
490
|
+
increment_tries_with_exception
|
|
491
|
+
end
|
|
492
|
+
end.to raise_error(StandardError)
|
|
493
|
+
end
|
|
492
494
|
|
|
493
495
|
expect(@tries).to eq(3)
|
|
494
496
|
expect(@next_interval_table[1]).to be_between(0.0, 1.0)
|
|
@@ -499,23 +501,26 @@ describe Retriable do
|
|
|
499
501
|
c.contexts[:api] = { tries: 3, base_interval: 1.0 }
|
|
500
502
|
end
|
|
501
503
|
|
|
502
|
-
described_class.
|
|
504
|
+
described_class.with_override(contexts: { api: { tries: 1 } }) do
|
|
505
|
+
described_class.with_context(:api, tries: 10) { increment_tries }
|
|
506
|
+
end
|
|
503
507
|
|
|
504
|
-
described_class.with_context(:api, tries: 10) { increment_tries }
|
|
505
508
|
expect(@tries).to eq(1)
|
|
506
509
|
end
|
|
507
510
|
|
|
508
511
|
it "can define a context only in override config" do
|
|
509
|
-
described_class.
|
|
512
|
+
described_class.with_override(contexts: { test_only: { tries: 1 } }) do
|
|
513
|
+
described_class.with_context(:test_only) { increment_tries }
|
|
514
|
+
end
|
|
510
515
|
|
|
511
|
-
described_class.with_context(:test_only) { increment_tries }
|
|
512
516
|
expect(@tries).to eq(1)
|
|
513
517
|
end
|
|
514
518
|
|
|
515
519
|
it "does not apply context-only overrides to plain retriable calls" do
|
|
516
|
-
described_class.
|
|
520
|
+
described_class.with_override(contexts: { api: { tries: 1 } }) do
|
|
521
|
+
expect { described_class.retriable(tries: 3) { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
522
|
+
end
|
|
517
523
|
|
|
518
|
-
expect { described_class.retriable(tries: 3) { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
519
524
|
expect(@tries).to eq(3)
|
|
520
525
|
end
|
|
521
526
|
|
|
@@ -523,21 +528,24 @@ describe Retriable do
|
|
|
523
528
|
described_class.configure do |c|
|
|
524
529
|
c.contexts[:api] = { tries: 3, on: NonStandardError }
|
|
525
530
|
end
|
|
526
|
-
described_class.override(tries: 1)
|
|
527
531
|
|
|
528
|
-
|
|
529
|
-
.
|
|
532
|
+
described_class.with_override(tries: 1) do
|
|
533
|
+
expect { described_class.with_context(:api) { increment_tries_with_exception(NonStandardError) } }
|
|
534
|
+
.to raise_error(NonStandardError)
|
|
535
|
+
end
|
|
536
|
+
|
|
530
537
|
expect(@tries).to eq(1)
|
|
531
538
|
end
|
|
532
539
|
|
|
533
540
|
it "combines local options with override-only contexts" do
|
|
534
|
-
described_class.
|
|
541
|
+
described_class.with_override(contexts: { api: { tries: 1 } }) do
|
|
542
|
+
expect do
|
|
543
|
+
described_class.with_context(:api, on: NonStandardError) do
|
|
544
|
+
increment_tries_with_exception(NonStandardError)
|
|
545
|
+
end
|
|
546
|
+
end.to raise_error(NonStandardError)
|
|
547
|
+
end
|
|
535
548
|
|
|
536
|
-
expect do
|
|
537
|
-
described_class.with_context(:api, on: NonStandardError) do
|
|
538
|
-
increment_tries_with_exception(NonStandardError)
|
|
539
|
-
end
|
|
540
|
-
end.to raise_error(NonStandardError)
|
|
541
549
|
expect(@tries).to eq(1)
|
|
542
550
|
end
|
|
543
551
|
|
|
@@ -546,9 +554,10 @@ describe Retriable do
|
|
|
546
554
|
c.contexts[:api] = { tries: 1 }
|
|
547
555
|
end
|
|
548
556
|
|
|
549
|
-
described_class.
|
|
557
|
+
described_class.with_override(tries: 1) do
|
|
558
|
+
described_class.with_context(:api) { increment_tries }
|
|
559
|
+
end
|
|
550
560
|
|
|
551
|
-
described_class.with_context(:api) { increment_tries }
|
|
552
561
|
expect(@tries).to eq(1)
|
|
553
562
|
end
|
|
554
563
|
|
|
@@ -556,9 +565,10 @@ describe Retriable do
|
|
|
556
565
|
begin
|
|
557
566
|
described_class.configure { |c| c.contexts = nil }
|
|
558
567
|
|
|
559
|
-
described_class.
|
|
568
|
+
described_class.with_override(contexts: { api: { tries: 1 } }) do
|
|
569
|
+
described_class.with_context(:api) { increment_tries }
|
|
570
|
+
end
|
|
560
571
|
|
|
561
|
-
described_class.with_context(:api) { increment_tries }
|
|
562
572
|
expect(@tries).to eq(1)
|
|
563
573
|
ensure
|
|
564
574
|
described_class.configure { |c| c.contexts = {} }
|
|
@@ -570,9 +580,10 @@ describe Retriable do
|
|
|
570
580
|
c.contexts[:api] = { tries: 1 }
|
|
571
581
|
end
|
|
572
582
|
|
|
573
|
-
described_class.
|
|
583
|
+
described_class.with_override(contexts: nil) do
|
|
584
|
+
described_class.with_context(:api) { increment_tries }
|
|
585
|
+
end
|
|
574
586
|
|
|
575
|
-
described_class.with_context(:api) { increment_tries }
|
|
576
587
|
expect(@tries).to eq(1)
|
|
577
588
|
end
|
|
578
589
|
|
|
@@ -581,9 +592,10 @@ describe Retriable do
|
|
|
581
592
|
c.contexts[:api] = { tries: 1 }
|
|
582
593
|
end
|
|
583
594
|
|
|
584
|
-
described_class.
|
|
595
|
+
described_class.with_override(contexts: 123) do
|
|
596
|
+
described_class.with_context(:api) { increment_tries }
|
|
597
|
+
end
|
|
585
598
|
|
|
586
|
-
described_class.with_context(:api) { increment_tries }
|
|
587
599
|
expect(@tries).to eq(1)
|
|
588
600
|
end
|
|
589
601
|
|
|
@@ -592,9 +604,10 @@ describe Retriable do
|
|
|
592
604
|
c.contexts[:api] = { tries: 2 }
|
|
593
605
|
end
|
|
594
606
|
|
|
595
|
-
described_class.
|
|
607
|
+
described_class.with_override(contexts: { api: 123 }) do
|
|
608
|
+
expect { described_class.with_context(:api) { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
609
|
+
end
|
|
596
610
|
|
|
597
|
-
expect { described_class.with_context(:api) { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
598
611
|
expect(@tries).to eq(2)
|
|
599
612
|
end
|
|
600
613
|
|
|
@@ -603,10 +616,10 @@ describe Retriable do
|
|
|
603
616
|
c.contexts[:configured] = { tries: 2 }
|
|
604
617
|
end
|
|
605
618
|
|
|
606
|
-
described_class.
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
619
|
+
described_class.with_override(contexts: { override_only: { tries: 1 } }) do
|
|
620
|
+
expect { described_class.with_context(:missing) { increment_tries } }
|
|
621
|
+
.to raise_error(ArgumentError, /override_only/)
|
|
622
|
+
end
|
|
610
623
|
end
|
|
611
624
|
|
|
612
625
|
it "does not snapshot configured contexts when adding override-only contexts" do
|
|
@@ -614,37 +627,200 @@ describe Retriable do
|
|
|
614
627
|
c.contexts[:api] = { tries: 2 }
|
|
615
628
|
end
|
|
616
629
|
|
|
617
|
-
described_class.
|
|
630
|
+
described_class.with_override(contexts: { test_only: { tries: 1 } }) do
|
|
631
|
+
described_class.configure do |c|
|
|
632
|
+
c.contexts[:api] = { tries: 5 }
|
|
633
|
+
end
|
|
618
634
|
|
|
619
|
-
|
|
620
|
-
c.contexts[:api] = { tries: 5 }
|
|
635
|
+
expect { described_class.with_context(:api) { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
621
636
|
end
|
|
622
637
|
|
|
623
|
-
expect { described_class.with_context(:api) { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
624
638
|
expect(@tries).to eq(5)
|
|
625
639
|
end
|
|
626
640
|
|
|
627
641
|
it "raises ArgumentError on invalid override options" do
|
|
628
|
-
expect { described_class.
|
|
642
|
+
expect { described_class.with_override(does_not_exist: 123) { :noop } }.to raise_error(ArgumentError)
|
|
629
643
|
end
|
|
630
644
|
|
|
631
645
|
it "raises ArgumentError on empty override options" do
|
|
632
|
-
expect { described_class.
|
|
646
|
+
expect { described_class.with_override({}) { :noop } }.to raise_error(ArgumentError, /empty override/)
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
it "raises ArgumentError when called without a block" do
|
|
650
|
+
expect { described_class.with_override(tries: 1) }.to raise_error(ArgumentError, /requires a block/)
|
|
633
651
|
end
|
|
634
652
|
|
|
635
653
|
it "raises ArgumentError on invalid context override options" do
|
|
636
|
-
expect { described_class.
|
|
654
|
+
expect { described_class.with_override(contexts: { api: { does_not_exist: 123 } }) { :noop } }
|
|
637
655
|
.to raise_error(ArgumentError, /does_not_exist is not a valid option/)
|
|
638
656
|
end
|
|
639
657
|
|
|
640
|
-
it "
|
|
641
|
-
|
|
642
|
-
|
|
658
|
+
it "clears the override after the block returns" do
|
|
659
|
+
described_class.with_override(tries: 1) do
|
|
660
|
+
# active here
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
expect { described_class.retriable(tries: 3) { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
664
|
+
expect(@tries).to eq(3)
|
|
665
|
+
end
|
|
643
666
|
|
|
644
|
-
|
|
667
|
+
it "clears the override when the block raises" do
|
|
668
|
+
expect do
|
|
669
|
+
described_class.with_override(tries: 1) { raise "boom" }
|
|
670
|
+
end.to raise_error(RuntimeError, "boom")
|
|
645
671
|
|
|
646
|
-
expect { described_class.retriable(tries:
|
|
647
|
-
expect(@tries).to eq(
|
|
672
|
+
expect { described_class.retriable(tries: 3) { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
673
|
+
expect(@tries).to eq(3)
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
it "returns the block's return value" do
|
|
677
|
+
result = described_class.with_override(tries: 1) { :return_value }
|
|
678
|
+
expect(result).to eq(:return_value)
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
it "restores the outer override when nested blocks exit" do
|
|
682
|
+
tries_seen = []
|
|
683
|
+
handler = ->(_exception, try, _elapsed, _next) { tries_seen << [Thread.current.object_id, try] }
|
|
684
|
+
|
|
685
|
+
described_class.with_override(tries: 2, on_retry: handler) do
|
|
686
|
+
described_class.with_override(tries: 4, on_retry: handler) do
|
|
687
|
+
expect { described_class.retriable { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
# After the inner block exits, the outer tries: 2 override is restored.
|
|
691
|
+
@tries = 0
|
|
692
|
+
expect { described_class.retriable { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
693
|
+
expect(@tries).to eq(2)
|
|
694
|
+
end
|
|
695
|
+
end
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
context "#with_override thread safety" do
|
|
699
|
+
# Coordinate threads with queues rather than sleep so tests are deterministic.
|
|
700
|
+
# sleep_disabled is already set to true in the top-level before(:each), so
|
|
701
|
+
# retriable calls do not actually sleep between attempts.
|
|
702
|
+
|
|
703
|
+
it "isolates overrides between threads" do
|
|
704
|
+
ready = Queue.new
|
|
705
|
+
proceed = Queue.new
|
|
706
|
+
results = {}
|
|
707
|
+
mutex = Mutex.new
|
|
708
|
+
|
|
709
|
+
threads = [1, 2].map do |id|
|
|
710
|
+
Thread.new do
|
|
711
|
+
described_class.with_override(tries: id) do
|
|
712
|
+
ready << true
|
|
713
|
+
proceed.pop
|
|
714
|
+
tries = 0
|
|
715
|
+
begin
|
|
716
|
+
described_class.retriable do
|
|
717
|
+
tries += 1
|
|
718
|
+
raise StandardError
|
|
719
|
+
end
|
|
720
|
+
rescue StandardError
|
|
721
|
+
mutex.synchronize { results[id] = tries }
|
|
722
|
+
end
|
|
723
|
+
end
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
2.times { ready.pop }
|
|
728
|
+
2.times { proceed << true }
|
|
729
|
+
threads.each(&:join)
|
|
730
|
+
|
|
731
|
+
expect(results).to eq(1 => 1, 2 => 2)
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
it "does not leak an active override into a sibling thread" do
|
|
735
|
+
override_active = Queue.new
|
|
736
|
+
sibling_done = Queue.new
|
|
737
|
+
sibling_tries = nil
|
|
738
|
+
|
|
739
|
+
setter = Thread.new do
|
|
740
|
+
described_class.with_override(tries: 1) do
|
|
741
|
+
override_active << true
|
|
742
|
+
sibling_done.pop
|
|
743
|
+
end
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
sibling = Thread.new do
|
|
747
|
+
override_active.pop
|
|
748
|
+
tries = 0
|
|
749
|
+
begin
|
|
750
|
+
described_class.retriable(tries: 3) do
|
|
751
|
+
tries += 1
|
|
752
|
+
raise StandardError
|
|
753
|
+
end
|
|
754
|
+
rescue StandardError
|
|
755
|
+
sibling_tries = tries
|
|
756
|
+
end
|
|
757
|
+
sibling_done << true
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
[setter, sibling].each(&:join)
|
|
761
|
+
expect(sibling_tries).to eq(3)
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
it "does not propagate an active override to a child thread" do
|
|
765
|
+
child_tries = nil
|
|
766
|
+
|
|
767
|
+
described_class.with_override(tries: 1) do
|
|
768
|
+
Thread.new do
|
|
769
|
+
tries = 0
|
|
770
|
+
begin
|
|
771
|
+
described_class.retriable(tries: 3) do
|
|
772
|
+
tries += 1
|
|
773
|
+
raise StandardError
|
|
774
|
+
end
|
|
775
|
+
rescue StandardError
|
|
776
|
+
child_tries = tries
|
|
777
|
+
end
|
|
778
|
+
end.join
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
expect(child_tries).to eq(3)
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
it "shares the active override with fibers in the same thread" do
|
|
785
|
+
fiber_tries = nil
|
|
786
|
+
|
|
787
|
+
Thread.new do
|
|
788
|
+
described_class.with_override(tries: 1) do
|
|
789
|
+
Fiber.new do
|
|
790
|
+
tries = 0
|
|
791
|
+
begin
|
|
792
|
+
described_class.retriable(tries: 10) do
|
|
793
|
+
tries += 1
|
|
794
|
+
raise StandardError
|
|
795
|
+
end
|
|
796
|
+
rescue StandardError
|
|
797
|
+
fiber_tries = tries
|
|
798
|
+
end
|
|
799
|
+
end.resume
|
|
800
|
+
end
|
|
801
|
+
end.join
|
|
802
|
+
|
|
803
|
+
expect(fiber_tries).to eq(1)
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
it "does not treat a main-thread override as a global default for other threads" do
|
|
807
|
+
other_thread_tries = nil
|
|
808
|
+
|
|
809
|
+
described_class.with_override(tries: 1) do
|
|
810
|
+
Thread.new do
|
|
811
|
+
tries = 0
|
|
812
|
+
begin
|
|
813
|
+
described_class.retriable(tries: 3) do
|
|
814
|
+
tries += 1
|
|
815
|
+
raise StandardError
|
|
816
|
+
end
|
|
817
|
+
rescue StandardError
|
|
818
|
+
other_thread_tries = tries
|
|
819
|
+
end
|
|
820
|
+
end.join
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
expect(other_thread_tries).to eq(3)
|
|
648
824
|
end
|
|
649
825
|
end
|
|
650
826
|
|
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.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jack Chu
|
|
@@ -74,6 +74,7 @@ files:
|
|
|
74
74
|
- Rakefile
|
|
75
75
|
- bin/console
|
|
76
76
|
- bin/setup
|
|
77
|
+
- docs/testing.md
|
|
77
78
|
- lib/retriable.rb
|
|
78
79
|
- lib/retriable/config.rb
|
|
79
80
|
- lib/retriable/core_ext/kernel.rb
|