retriable 3.5.1 → 3.6.1
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 +10 -0
- data/README.md +82 -80
- data/docs/superpowers/specs/2026-05-26-on-give-up-callback-followups-design.md +116 -0
- data/docs/testing.md +212 -0
- data/lib/retriable/config.rb +1 -0
- data/lib/retriable/validation.rb +41 -0
- data/lib/retriable/version.rb +1 -1
- data/lib/retriable.rb +39 -14
- data/spec/config_spec.rb +66 -0
- data/spec/retriable_spec.rb +285 -79
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9120a536b473da5668754cf8d0021abc06200c558bc916091b871efb24c60f4c
|
|
4
|
+
data.tar.gz: eb4eaf3b7509cf88c57609a70b83b8fdc75da2727c05a7a0cd5daadf36b601d4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0123eb6bbcdb8d392bb5b4b7eab3c59db53970a8bd48b2a320c2d5b085e9196dc2918fd9934f054755069d7cbc7f3d434cac87d2878f2b4ae767d138e5b03d16
|
|
7
|
+
data.tar.gz: 8d09ecc45ce61a5c76e0afd319b6ddbdb37ae99477f9ec0b6faf81caf6bfbd678abb1336b4254a15350efb2c2bede124625722707241729ed74169d8dfae5a9d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# HEAD
|
|
2
2
|
|
|
3
|
+
## 3.6.1
|
|
4
|
+
|
|
5
|
+
- Fix: Validate the `on:` option before retrying. Previously, passing a non-`Exception` value such as `Object`, `Kernel`, or a plain `Module` (which appear in every `Exception`'s ancestor chain) would silently retry process-critical exceptions like `SystemExit` and `Interrupt`. The `on:` option now requires an `Exception` subclass, an array of them, or a hash whose keys are such classes and whose values are `nil`, a `Regexp`, or an array of `Regexp`s. Invalid shapes raise `ArgumentError` before the block runs.
|
|
6
|
+
- Fix: Validate `with_override(contexts:)` shape before applying overrides. `contexts` may be `nil` or a hash, and each per-context override must be a hash.
|
|
7
|
+
- Docs: Document that `on_retry: false` disables a callback set in `Retriable.configure` for a single call.
|
|
8
|
+
|
|
9
|
+
## 3.6.0
|
|
10
|
+
|
|
11
|
+
- 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.
|
|
12
|
+
|
|
3
13
|
## 3.5.1
|
|
4
14
|
|
|
5
15
|
- 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
|
@@ -5,6 +5,29 @@
|
|
|
5
5
|
|
|
6
6
|
Retriable is a simple DSL to retry failed code blocks with randomized [exponential backoff](http://en.wikipedia.org/wiki/Exponential_backoff) time intervals. This is especially useful when interacting external APIs, remote services, or file system calls.
|
|
7
7
|
|
|
8
|
+
## Table of Contents
|
|
9
|
+
|
|
10
|
+
- [Requirements](#requirements)
|
|
11
|
+
- [Installation](#installation)
|
|
12
|
+
- [Usage](#usage)
|
|
13
|
+
- [Defaults](#defaults)
|
|
14
|
+
- [Options](#options)
|
|
15
|
+
- [Configuring Which Options to Retry With :on](#configuring-which-options-to-retry-with-on)
|
|
16
|
+
- [Advanced Retry Matching With :retry_if](#advanced-retry-matching-with-retry_if)
|
|
17
|
+
- [Configuration](#configuration)
|
|
18
|
+
- [Override](#override)
|
|
19
|
+
- [Example Usage](#example-usage)
|
|
20
|
+
- [Custom Interval Array](#custom-interval-array)
|
|
21
|
+
- [Turn off Exponential Backoff](#turn-off-exponential-backoff)
|
|
22
|
+
- [Callbacks](#callbacks)
|
|
23
|
+
- [Ensure/Else](#ensureelse)
|
|
24
|
+
- [Contexts](#contexts)
|
|
25
|
+
- [Kernel Extension](#kernel-extension)
|
|
26
|
+
- [Testing](#testing)
|
|
27
|
+
- [Credits](#credits)
|
|
28
|
+
- [Development](#development)
|
|
29
|
+
- [Running Specs](#running-specs)
|
|
30
|
+
|
|
8
31
|
## Requirements
|
|
9
32
|
|
|
10
33
|
Ruby 2.3.0+
|
|
@@ -32,7 +55,7 @@ require 'retriable'
|
|
|
32
55
|
In your Gemfile:
|
|
33
56
|
|
|
34
57
|
```ruby
|
|
35
|
-
gem 'retriable', '~> 3.
|
|
58
|
+
gem 'retriable', '~> 3.6'
|
|
36
59
|
```
|
|
37
60
|
|
|
38
61
|
## Usage
|
|
@@ -84,7 +107,7 @@ Here are the available options, in some vague order of relevance to most common
|
|
|
84
107
|
| **`tries`** | `3` | Number of attempts to make at running your code block (includes initial attempt). |
|
|
85
108
|
| **`on`** | `[StandardError]` | Type of exceptions to retry. [Read more](#configuring-which-options-to-retry-with-on). |
|
|
86
109
|
| **`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). |
|
|
87
|
-
| **`on_retry`** | `nil` | `Proc` to call after each try is rescued. [Read more](#callbacks).
|
|
110
|
+
| **`on_retry`** | `nil` | `Proc` to call after each try is rescued. Pass `false` to disable a callback set in `#configure` for a single call. [Read more](#callbacks). |
|
|
88
111
|
| **`sleep_disabled`** | `false` | When true, disable exponential backoff and attempt retries immediately. |
|
|
89
112
|
| **`base_interval`** | `0.5` | The initial interval in seconds between tries. |
|
|
90
113
|
| **`max_elapsed_time`** | `900` (15 min) | The maximum amount of total time in seconds that code is allowed to keep being retried. Set to `nil` to disable the time limit and retry based solely on `tries`. |
|
|
@@ -149,31 +172,46 @@ end
|
|
|
149
172
|
|
|
150
173
|
### Override
|
|
151
174
|
|
|
152
|
-
|
|
153
|
-
`#
|
|
175
|
+
`#with_override` is a block-scoped API for forcing retry options that should
|
|
176
|
+
take precedence over both `#configure` defaults and per-call options. It is
|
|
177
|
+
primarily intended for tests — it lets a test force values like `tries: 1` or
|
|
178
|
+
`base_interval: 0` so the suite runs quickly and predictably, regardless of
|
|
179
|
+
the application's `#configure` defaults. In application code, prefer
|
|
180
|
+
`#configure` for app-level defaults and per-call options for caller-specific
|
|
181
|
+
values.
|
|
154
182
|
|
|
155
183
|
```ruby
|
|
156
|
-
Retriable.
|
|
184
|
+
Retriable.with_override(tries: 1, base_interval: 0) do
|
|
185
|
+
Retriable.retriable do
|
|
186
|
+
# code here...
|
|
187
|
+
end
|
|
188
|
+
end
|
|
157
189
|
```
|
|
158
190
|
|
|
159
|
-
|
|
191
|
+
Precedence inside the block:
|
|
160
192
|
|
|
161
193
|
```
|
|
162
|
-
|
|
194
|
+
with_override > local options > configure defaults
|
|
163
195
|
```
|
|
164
196
|
|
|
165
|
-
`#
|
|
166
|
-
|
|
167
|
-
|
|
197
|
+
`#with_override` requires a block and raises `ArgumentError` if called without
|
|
198
|
+
one. The override is active only while the block is executing, and is
|
|
199
|
+
automatically restored to its previous value when the block returns or raises.
|
|
200
|
+
Nested `#with_override` calls work as expected: the inner block temporarily
|
|
201
|
+
replaces the active override and the outer override is restored when the
|
|
202
|
+
inner block exits.
|
|
168
203
|
|
|
169
|
-
`#
|
|
170
|
-
|
|
204
|
+
`#with_override` is scoped to the **current thread**. The active override
|
|
205
|
+
does not affect any other thread, and child threads spawned inside the block
|
|
206
|
+
do not inherit it. This makes `#with_override` safe to use in parallel test
|
|
207
|
+
runners. Fibers running inside the same thread share the thread's active
|
|
208
|
+
override.
|
|
171
209
|
|
|
172
|
-
|
|
210
|
+
`#with_override` stores the provided options directly. Do not mutate the
|
|
211
|
+
options hash or nested values for the duration of the block.
|
|
173
212
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
```
|
|
213
|
+
For test-integration patterns (RSpec `around`, helper methods, Minitest, etc.),
|
|
214
|
+
see [docs/testing.md](docs/testing.md).
|
|
177
215
|
|
|
178
216
|
### Example Usage
|
|
179
217
|
|
|
@@ -279,6 +317,26 @@ Retriable.retriable(on_retry: do_this_on_each_retry) do
|
|
|
279
317
|
end
|
|
280
318
|
```
|
|
281
319
|
|
|
320
|
+
#### Disabling a Configured Callback Per Call
|
|
321
|
+
|
|
322
|
+
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`. Passing `nil` does not work for this purpose because per-call options are merged over configured defaults; `false` is the explicit "disabled" sentinel.
|
|
323
|
+
|
|
324
|
+
```ruby
|
|
325
|
+
Retriable.configure do |c|
|
|
326
|
+
c.on_retry = ->(exception, try, elapsed_time, next_interval) { log(...) }
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Most calls use the configured callback.
|
|
330
|
+
Retriable.retriable do
|
|
331
|
+
# ...
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# This specific call opts out of the configured callback.
|
|
335
|
+
Retriable.retriable(on_retry: false) do
|
|
336
|
+
# ...
|
|
337
|
+
end
|
|
338
|
+
```
|
|
339
|
+
|
|
282
340
|
### Ensure/Else
|
|
283
341
|
|
|
284
342
|
What if I want to execute a code block at the end, whether or not an exception was rescued ([ensure](http://ruby-doc.org/docs/keywords/1.9/Object.html#method-i-ensure))? Or what if I want to execute a code block if no exception is raised ([else](http://ruby-doc.org/docs/keywords/1.9/Object.html#method-i-else))? Instead of providing more callbacks, I recommend you just wrap retriable in a begin/retry/else/ensure block:
|
|
@@ -369,72 +427,16 @@ retriable_with_context(:api) do
|
|
|
369
427
|
end
|
|
370
428
|
```
|
|
371
429
|
|
|
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.
|
|
394
|
-
|
|
395
|
-
Alternately, if you are using RSpec, you could override the Retriable configuration in your `spec_helper`.
|
|
396
|
-
|
|
397
|
-
```ruby
|
|
398
|
-
# spec/spec_helper.rb
|
|
399
|
-
Retriable.override(tries: 1, base_interval: 0, rand_factor: 0)
|
|
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:
|
|
430
|
+
## Testing
|
|
423
431
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
context_overrides[key] = { tries: 1, base_interval: 0 }
|
|
429
|
-
end
|
|
432
|
+
`Retriable.with_override` is designed to short-circuit retries in your test
|
|
433
|
+
suite so failing blocks do not slow tests down. The simplest pattern is an
|
|
434
|
+
RSpec `around(:each)` hook (or your test framework's equivalent) that wraps
|
|
435
|
+
every example in `with_override(tries: 1, base_interval: 0)`.
|
|
430
436
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
base_interval: 0,
|
|
435
|
-
contexts: context_overrides,
|
|
436
|
-
)
|
|
437
|
-
```
|
|
437
|
+
For Rails integration, opting out of the override for specific tests, and
|
|
438
|
+
overriding configured contexts in tests, see
|
|
439
|
+
[docs/testing.md](docs/testing.md).
|
|
438
440
|
|
|
439
441
|
## Credits
|
|
440
442
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Design: `on_give_up` callback follow-ups (issue #72, PR #127)
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
- Issue [#72](https://github.com/kamui/retriable/issues/72) requests a callback that fires only after all retries are exhausted.
|
|
6
|
+
- Draft PR [#127](https://github.com/kamui/retriable/pull/127) (branch `feat/on-give-up-callback`, authored by maintainer @kamui) implements this as `on_give_up`. It is largely production-ready.
|
|
7
|
+
|
|
8
|
+
This spec defines a small set of follow-up additions on top of `feat/on-give-up-callback`. It does **not** revisit the design decisions already settled in #127 (naming, signature, reason symbols, opt-out behavior).
|
|
9
|
+
|
|
10
|
+
## Settled decisions inherited from #127
|
|
11
|
+
|
|
12
|
+
| Decision | Resolution |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| Callback name | `on_give_up` |
|
|
15
|
+
| Signature | `(exception, try, elapsed_time, next_interval, reason)` |
|
|
16
|
+
| Reason values | `:tries_exhausted`, `:max_elapsed_time` |
|
|
17
|
+
| `next_interval` when `:tries_exhausted` | `nil` |
|
|
18
|
+
| `next_interval` when `:max_elapsed_time` | The interval that would have been slept before the next try |
|
|
19
|
+
| Opt-out | `on_give_up: false` (or `nil`) disables a configured handler |
|
|
20
|
+
| Order vs `on_retry` | `on_retry` runs first; `on_give_up` runs just before re-raise |
|
|
21
|
+
| Non-retriable exception types | `on_give_up` does **not** fire |
|
|
22
|
+
| `retry_if` rejection | `on_give_up` does **not** fire |
|
|
23
|
+
| `elapsed_time` for the give-up decision | Re-read after `on_retry` returns so handler time counts toward `max_elapsed_time` |
|
|
24
|
+
| Threading through `Config::ATTRIBUTES` | Already enables `with_context`, `override`, and `configure` automatically |
|
|
25
|
+
|
|
26
|
+
## Gaps to fill
|
|
27
|
+
|
|
28
|
+
PR #127 has the mechanics right. The follow-up work below closes documentation and test-coverage gaps and locks in undocumented-but-implied semantics.
|
|
29
|
+
|
|
30
|
+
### 1. Document non-firing cases in README
|
|
31
|
+
|
|
32
|
+
PR #127 covers the firing cases. The README should explicitly state when the callback does **not** fire, because users wiring up paging/metrics need to know.
|
|
33
|
+
|
|
34
|
+
Add one paragraph at the end of the new `on_give_up` subsection in `README.md`:
|
|
35
|
+
|
|
36
|
+
> `on_give_up` is invoked only when Retriable rescued an exception that matched the retry rules and then decided to stop. It does **not** fire when the block raises an exception that is not in `on`, nor when `retry_if` returns false. Both of those cases are immediate re-raises, not retry exhaustion, and should be handled with normal Ruby `rescue` blocks around the `Retriable.retriable` call.
|
|
37
|
+
|
|
38
|
+
### 2. Document handler-raised-error policy in README
|
|
39
|
+
|
|
40
|
+
Current `on_retry` documentation does not state what happens if the handler itself raises. PR #127 silently inherits the same behavior: an exception inside `on_give_up` propagates, replacing the original. Make this explicit.
|
|
41
|
+
|
|
42
|
+
Add one sentence to the same subsection:
|
|
43
|
+
|
|
44
|
+
> If `on_give_up` itself raises, that exception propagates to the caller and replaces the original retried exception. Keep the handler defensive (rescue inside it) if you need the original exception to surface.
|
|
45
|
+
|
|
46
|
+
### 3. Mention `on_give_up` in the Contexts example
|
|
47
|
+
|
|
48
|
+
`README.md` already has a Contexts example at `README.md:306`. Extend the `:aws` context to demonstrate `on_give_up`:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
Retriable.configure do |c|
|
|
52
|
+
c.contexts[:aws] = {
|
|
53
|
+
tries: 3,
|
|
54
|
+
base_interval: 5,
|
|
55
|
+
on_retry: Proc.new { puts 'Curse you, AWS!' },
|
|
56
|
+
on_give_up: Proc.new { |_e, _try, _elapsed, _interval, reason|
|
|
57
|
+
puts "Gave up on AWS: #{reason}"
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 4. Test: per-context `override` accepts and dispatches `on_give_up`
|
|
64
|
+
|
|
65
|
+
PR #127 adds a positive `with_context` spec and an `override` spec for top-level overrides, but no spec for the call shape `Retriable.override(contexts: { key: { on_give_up: ... } })`, which is validated by `validate_context_override_options` and applied by `context_options_for`. Add one spec under the existing `#override` context that:
|
|
66
|
+
|
|
67
|
+
1. Calls `Retriable.override(contexts: { api: { on_give_up: handler, tries: 1 } })`.
|
|
68
|
+
2. Invokes `Retriable.with_context(:api) { raise StandardError }`.
|
|
69
|
+
3. Asserts the handler was invoked exactly once with `reason == :tries_exhausted`.
|
|
70
|
+
|
|
71
|
+
### 5. Test: kernel extension passes `on_give_up` through
|
|
72
|
+
|
|
73
|
+
PR #127 does not exercise the kernel extension (`Kernel#retriable` and `Kernel#retriable_with_context`). The delegation is trivial, but a regression guard is cheap. Add one spec inside the existing `context "global scope extension"` block that requires `retriable/core_ext/kernel`, invokes `retriable(tries: 1, on_give_up: handler) { raise }`, and asserts the handler ran with `reason == :tries_exhausted`. A second `retriable_with_context` spec is not needed because item 4 already covers the context-dispatch path.
|
|
74
|
+
|
|
75
|
+
### 6. Test: handler that raises propagates and replaces the original
|
|
76
|
+
|
|
77
|
+
Lock in the policy from item 2 with a spec: handler raises `RuntimeError`, caller observes `RuntimeError`, not the original `StandardError`.
|
|
78
|
+
|
|
79
|
+
### 7. CHANGELOG entry: include signature and reasons
|
|
80
|
+
|
|
81
|
+
PR #127's CHANGELOG line is:
|
|
82
|
+
|
|
83
|
+
> - Add `on_give_up` callback to observe when retries stop because tries are exhausted or the next retry would exceed `max_elapsed_time`.
|
|
84
|
+
|
|
85
|
+
Rewrite to:
|
|
86
|
+
|
|
87
|
+
> - Add `on_give_up` callback that runs when Retriable stops retrying after a rescued retriable exception. Receives `(exception, try, elapsed_time, next_interval, reason)`, where `reason` is `:tries_exhausted` or `:max_elapsed_time`. Does not fire for non-retriable exceptions or `retry_if` rejections. Pass `on_give_up: false` to suppress a configured handler for a single call.
|
|
88
|
+
|
|
89
|
+
## Out of scope
|
|
90
|
+
|
|
91
|
+
- Renaming `on_give_up`. The maintainer authored the draft with this name.
|
|
92
|
+
- Changing the callback signature (e.g., removing `next_interval`).
|
|
93
|
+
- Firing for `retry_if` rejection. That decision was made deliberately in #127.
|
|
94
|
+
- Version bump. Deferred to the maintainer's release commit.
|
|
95
|
+
- Touching the pre-existing rubocop offenses noted in PR #127's description (`retriable.gemspec`, `spec/exponential_backoff_spec.rb`).
|
|
96
|
+
|
|
97
|
+
## Files touched
|
|
98
|
+
|
|
99
|
+
- `README.md` — items 1, 2, 3.
|
|
100
|
+
- `spec/retriable_spec.rb` — items 4, 5, 6.
|
|
101
|
+
- `CHANGELOG.md` — item 7.
|
|
102
|
+
|
|
103
|
+
No changes to `lib/retriable.rb` or `lib/retriable/config.rb`; PR #127's implementation already satisfies the behavior.
|
|
104
|
+
|
|
105
|
+
## Verification
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
bundle exec rspec
|
|
109
|
+
bundle exec rubocop lib spec
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Both must pass. Pre-existing rubocop offenses in `retriable.gemspec` and `spec/exponential_backoff_spec.rb` are intentionally left untouched (see Out of scope).
|
|
113
|
+
|
|
114
|
+
## Delivery
|
|
115
|
+
|
|
116
|
+
Push as additional commits on the existing `feat/on-give-up-callback` branch (PR #127). If we lack push access to the maintainer's branch, open a PR targeting `feat/on-give-up-callback` with these follow-ups, or post the diff as a review comment on #127.
|
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/config.rb
CHANGED
data/lib/retriable/validation.rb
CHANGED
|
@@ -37,5 +37,46 @@ module Retriable
|
|
|
37
37
|
def finite_number?(value)
|
|
38
38
|
value.is_a?(Numeric) && value.to_f.finite?
|
|
39
39
|
end
|
|
40
|
+
|
|
41
|
+
# Validates an `on:` value. Acceptable shapes:
|
|
42
|
+
# - a Class that descends from Exception
|
|
43
|
+
# - an Array whose elements are Classes that descend from Exception
|
|
44
|
+
# - a Hash whose keys are such Classes and whose values are nil,
|
|
45
|
+
# a Regexp, or an Array of Regexps
|
|
46
|
+
#
|
|
47
|
+
# Without this validation, callers can pass values like `Object` or
|
|
48
|
+
# `Kernel` and silently retry process-critical exceptions such as
|
|
49
|
+
# SystemExit and Interrupt, because every Exception's ancestor chain
|
|
50
|
+
# includes both. Hash values that are not Regexps (e.g. plain Strings)
|
|
51
|
+
# also silently fail to match in #hash_exception_match?, so we require
|
|
52
|
+
# Regexp values explicitly.
|
|
53
|
+
def validate_on(value)
|
|
54
|
+
case value
|
|
55
|
+
when Hash
|
|
56
|
+
value.each do |klass, pattern|
|
|
57
|
+
validate_on_class(klass)
|
|
58
|
+
validate_on_hash_value(klass, pattern)
|
|
59
|
+
end
|
|
60
|
+
when Array
|
|
61
|
+
value.each { |klass| validate_on_class(klass) }
|
|
62
|
+
else
|
|
63
|
+
validate_on_class(value)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def validate_on_class(klass)
|
|
68
|
+
return if klass.is_a?(Class) && klass <= Exception
|
|
69
|
+
|
|
70
|
+
raise ArgumentError, "on must be an Exception class or a collection of Exception classes, got #{klass.inspect}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def validate_on_hash_value(klass, pattern)
|
|
74
|
+
return if pattern.nil?
|
|
75
|
+
return if pattern.is_a?(Regexp)
|
|
76
|
+
return if pattern.is_a?(Array) && pattern.all? { |p| p.is_a?(Regexp) }
|
|
77
|
+
|
|
78
|
+
raise ArgumentError,
|
|
79
|
+
"on[#{klass}] must be nil, a Regexp, or an Array of Regexps, got #{pattern.inspect}"
|
|
80
|
+
end
|
|
40
81
|
end
|
|
41
82
|
end
|
data/lib/retriable/version.rb
CHANGED