retriable 3.4.1 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +5 -2
- data/CHANGELOG.md +29 -0
- data/README.md +148 -84
- data/docs/testing.md +212 -0
- data/lib/retriable/config.rb +103 -0
- data/lib/retriable/exponential_backoff.rb +31 -5
- data/lib/retriable/validation.rb +91 -0
- data/lib/retriable/version.rb +1 -1
- data/lib/retriable.rb +141 -22
- data/spec/config_spec.rb +187 -0
- data/spec/exponential_backoff_spec.rb +45 -26
- data/spec/retriable_spec.rb +662 -3
- data/spec/spec_helper.rb +13 -0
- 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: 25948e6545b8d98b0c7b08416bdf23b017ced352eb2a4eb1f2e9fdac45912031
|
|
4
|
+
data.tar.gz: 4a92afb17f79a4ad173b046158ca634b0728fc6ccceedc1b7789c9106342b513
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e43731b305a64c806bf57c13d78da410412d8ea4adaec9f255bd434b2d190ac53b6fac29172b72ed6bcc7329a3d5378c4075d72f6dede791f1e0bd16f400e58e
|
|
7
|
+
data.tar.gz: 4259468487738b9ae5bcc817ac992af3341f15bcfe604ab0a61ddf1f8793b444775aa57c06ce5f95561e179186e7360e86a877afd02f3b346b68377ffb3ecaf9
|
data/.github/workflows/main.yml
CHANGED
|
@@ -7,6 +7,9 @@ on:
|
|
|
7
7
|
branches: [main]
|
|
8
8
|
types: [opened, synchronize, reopened]
|
|
9
9
|
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
10
13
|
jobs:
|
|
11
14
|
ci:
|
|
12
15
|
# The type of runner that the job will run on
|
|
@@ -34,10 +37,10 @@ jobs:
|
|
|
34
37
|
|
|
35
38
|
steps:
|
|
36
39
|
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
|
37
|
-
- uses: actions/checkout@v6
|
|
40
|
+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
38
41
|
|
|
39
42
|
- name: Setup ruby
|
|
40
|
-
uses: ruby/setup-ruby@v1
|
|
43
|
+
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
|
|
41
44
|
with:
|
|
42
45
|
ruby-version: ${{ matrix.ruby }}
|
|
43
46
|
bundler-cache: true
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# HEAD
|
|
2
2
|
|
|
3
|
+
## 3.8.0
|
|
4
|
+
|
|
5
|
+
### Deprecations
|
|
6
|
+
|
|
7
|
+
- Deprecated the `timeout:` option ahead of its removal in Retriable 4.0. Non-nil timeout values supplied through `Retriable.configure`, `Retriable.retriable(...)`, or `Retriable.with_override(...)` now emit a deprecation warning while keeping the existing runtime behavior unchanged. On Ruby 2.7+ the warning is emitted via `Kernel.warn(..., category: :deprecated)`, so callers can silence it through the standard Ruby controls (`Warning[:deprecated] = false`, `ruby -W:no-deprecated`, or a custom `Warning.warn`). To keep the notice from drowning busy applications, it is emitted at most once per process; suppression via `Warning[:deprecated]` leaves the warner armed for the next call that re-enables the category. Prefer library-native timeout settings, or wrap the retried block in `Timeout.timeout(...)` directly if you still need that behavior. See the README migration guidance for details.
|
|
8
|
+
|
|
9
|
+
## 3.7.0
|
|
10
|
+
|
|
11
|
+
- Feature: Opt-in unbounded retries via `tries: Float::INFINITY`. Requires a finite `max_elapsed_time` as a safety bound and is incompatible with custom `intervals:`. Both invalid configurations raise `ArgumentError` from `Config#validate!`.
|
|
12
|
+
|
|
13
|
+
## 3.6.1
|
|
14
|
+
|
|
15
|
+
- 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.
|
|
16
|
+
- 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.
|
|
17
|
+
- Docs: Document that `on_retry: false` disables a callback set in `Retriable.configure` for a single call.
|
|
18
|
+
|
|
19
|
+
## 3.6.0
|
|
20
|
+
|
|
21
|
+
- 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.
|
|
22
|
+
|
|
23
|
+
## 3.5.1
|
|
24
|
+
|
|
25
|
+
- 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.
|
|
26
|
+
|
|
27
|
+
## 3.5.0
|
|
28
|
+
|
|
29
|
+
- Fix: Do not count skipped sleep intervals against `max_elapsed_time` when `sleep_disabled` is true.
|
|
30
|
+
- Add `override` and `reset_override` APIs to force retry settings over local call options when needed (for example, test short-circuiting).
|
|
31
|
+
|
|
3
32
|
## 3.4.1
|
|
4
33
|
|
|
5
34
|
- Fix: Use `Process.clock_gettime(CLOCK_MONOTONIC)` for elapsed time tracking so retry timing is immune to wall-clock adjustments (NTP, manual changes).
|
data/README.md
CHANGED
|
@@ -5,6 +5,30 @@
|
|
|
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
|
+
- [Migrating off `timeout:`](#migrating-off-timeout)
|
|
21
|
+
- [Custom Interval Array](#custom-interval-array)
|
|
22
|
+
- [Turn off Exponential Backoff](#turn-off-exponential-backoff)
|
|
23
|
+
- [Callbacks](#callbacks)
|
|
24
|
+
- [Ensure/Else](#ensureelse)
|
|
25
|
+
- [Contexts](#contexts)
|
|
26
|
+
- [Kernel Extension](#kernel-extension)
|
|
27
|
+
- [Testing](#testing)
|
|
28
|
+
- [Credits](#credits)
|
|
29
|
+
- [Development](#development)
|
|
30
|
+
- [Running Specs](#running-specs)
|
|
31
|
+
|
|
8
32
|
## Requirements
|
|
9
33
|
|
|
10
34
|
Ruby 2.3.0+
|
|
@@ -32,7 +56,7 @@ require 'retriable'
|
|
|
32
56
|
In your Gemfile:
|
|
33
57
|
|
|
34
58
|
```ruby
|
|
35
|
-
gem 'retriable', '~> 3.
|
|
59
|
+
gem 'retriable', '~> 3.8'
|
|
36
60
|
```
|
|
37
61
|
|
|
38
62
|
## Usage
|
|
@@ -79,20 +103,22 @@ The default interval table with 10 tries looks like this (in seconds, rounded to
|
|
|
79
103
|
|
|
80
104
|
Here are the available options, in some vague order of relevance to most common use patterns:
|
|
81
105
|
|
|
82
|
-
| Option | Default | Definition
|
|
83
|
-
| ---------------------- | ----------------- |
|
|
84
|
-
| **`tries`** | `3` | Number of attempts to make at running your code block (includes initial attempt).
|
|
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).
|
|
87
|
-
| **`on_retry`** | `nil` | `Proc` to call after each try is rescued. [Read more](#callbacks).
|
|
88
|
-
| **`sleep_disabled`** | `false` | When true, disable exponential backoff and attempt retries immediately.
|
|
89
|
-
| **`base_interval`** | `0.5` | The initial interval in seconds between tries.
|
|
90
|
-
| **`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`.
|
|
91
|
-
| **`max_interval`** | `60` | The maximum interval in seconds that any individual retry can reach.
|
|
92
|
-
| **`multiplier`** | `1.5` | Each successive interval grows by this factor. A multipler of 1.5 means the next interval will be 1.5x the current interval.
|
|
93
|
-
| **`rand_factor`** | `0.5` | The percentage to randomize the next retry interval time. The next interval calculation is `randomized_interval = retry_interval * (random value in range [1 - randomization_factor, 1 + randomization_factor])`
|
|
94
|
-
| **`intervals`** | `nil` | Skip generated intervals and provide your own array of intervals in seconds. [Read more](#custom-interval-array).
|
|
95
|
-
| **`timeout`** | `nil` | Number of seconds to allow the code block to run before raising a `Timeout::Error` inside each try. `nil` means the code block can run forever without raising error.
|
|
106
|
+
| Option | Default | Definition |
|
|
107
|
+
| ---------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
108
|
+
| **`tries`** | `3` | Number of attempts to make at running your code block (includes initial attempt). Pass `Float::INFINITY` to keep retrying until success or until `max_elapsed_time` is reached. |
|
|
109
|
+
| **`on`** | `[StandardError]` | Type of exceptions to retry. [Read more](#configuring-which-options-to-retry-with-on). |
|
|
110
|
+
| **`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). |
|
|
111
|
+
| **`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). |
|
|
112
|
+
| **`sleep_disabled`** | `false` | When true, disable exponential backoff and attempt retries immediately. |
|
|
113
|
+
| **`base_interval`** | `0.5` | The initial interval in seconds between tries. |
|
|
114
|
+
| **`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`. |
|
|
115
|
+
| **`max_interval`** | `60` | The maximum interval in seconds that any individual retry can reach. |
|
|
116
|
+
| **`multiplier`** | `1.5` | Each successive interval grows by this factor. A multipler of 1.5 means the next interval will be 1.5x the current interval. |
|
|
117
|
+
| **`rand_factor`** | `0.5` | The percentage to randomize the next retry interval time. The next interval calculation is `randomized_interval = retry_interval * (random value in range [1 - randomization_factor, 1 + randomization_factor])` |
|
|
118
|
+
| **`intervals`** | `nil` | Skip generated intervals and provide your own array of intervals in seconds. [Read more](#custom-interval-array). |
|
|
119
|
+
| **`timeout`** | `nil` | Deprecated in 3.8.0 and removed in 4.0. Number of seconds to allow the code block to run before raising a `Timeout::Error` inside each try. `nil` means the code block can run forever without raising error. Non-nil values emit a deprecation warning. See [Migrating off `timeout:`](#migrating-off-timeout). |
|
|
120
|
+
|
|
121
|
+
Timing options are validated before retrying. `tries` must be a positive integer when Retriable generates intervals, or `Float::INFINITY` for unbounded retries. `base_interval`, `max_interval`, `multiplier`, `max_elapsed_time`, and `timeout` must be non-negative numbers, with `max_elapsed_time` and `timeout` also accepting `nil`. `rand_factor` must be a number from `0` through `1`. If provided, `intervals` must be an array of non-negative numbers; because it replaces generated intervals, it also overrides `tries`, `base_interval`, `max_interval`, `rand_factor`, and `multiplier` validation. `intervals` cannot be combined with `tries: Float::INFINITY`.
|
|
96
122
|
|
|
97
123
|
#### Configuring Which Options to Retry With :on
|
|
98
124
|
|
|
@@ -142,6 +168,52 @@ Retriable.configure do |c|
|
|
|
142
168
|
end
|
|
143
169
|
```
|
|
144
170
|
|
|
171
|
+
`#configure` sets defaults only. Per-call options passed to `Retriable.retriable` and
|
|
172
|
+
`Retriable.with_context` still take precedence.
|
|
173
|
+
|
|
174
|
+
### Override
|
|
175
|
+
|
|
176
|
+
`#with_override` is a block-scoped API for forcing retry options that should
|
|
177
|
+
take precedence over both `#configure` defaults and per-call options. It is
|
|
178
|
+
primarily intended for tests — it lets a test force values like `tries: 1` or
|
|
179
|
+
`base_interval: 0` so the suite runs quickly and predictably, regardless of
|
|
180
|
+
the application's `#configure` defaults. In application code, prefer
|
|
181
|
+
`#configure` for app-level defaults and per-call options for caller-specific
|
|
182
|
+
values.
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
Retriable.with_override(tries: 1, base_interval: 0) do
|
|
186
|
+
Retriable.retriable do
|
|
187
|
+
# code here...
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Precedence inside the block:
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
with_override > local options > configure defaults
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`#with_override` requires a block and raises `ArgumentError` if called without
|
|
199
|
+
one. The override is active only while the block is executing, and is
|
|
200
|
+
automatically restored to its previous value when the block returns or raises.
|
|
201
|
+
Nested `#with_override` calls work as expected: the inner block temporarily
|
|
202
|
+
replaces the active override and the outer override is restored when the
|
|
203
|
+
inner block exits.
|
|
204
|
+
|
|
205
|
+
`#with_override` is scoped to the **current thread**. The active override
|
|
206
|
+
does not affect any other thread, and child threads spawned inside the block
|
|
207
|
+
do not inherit it. This makes `#with_override` safe to use in parallel test
|
|
208
|
+
runners. Fibers running inside the same thread share the thread's active
|
|
209
|
+
override.
|
|
210
|
+
|
|
211
|
+
`#with_override` stores the provided options directly. Do not mutate the
|
|
212
|
+
options hash or nested values for the duration of the block.
|
|
213
|
+
|
|
214
|
+
For test-integration patterns (RSpec `around`, helper methods, Minitest, etc.),
|
|
215
|
+
see [docs/testing.md](docs/testing.md).
|
|
216
|
+
|
|
145
217
|
### Example Usage
|
|
146
218
|
|
|
147
219
|
This example will only retry on a `Timeout::Error`, retry 3 times and sleep for a full second before each try.
|
|
@@ -172,20 +244,32 @@ Retriable.retriable(on: {
|
|
|
172
244
|
end
|
|
173
245
|
```
|
|
174
246
|
|
|
175
|
-
|
|
247
|
+
#### Migrating off `timeout:`
|
|
248
|
+
|
|
249
|
+
The `timeout:` option is deprecated in Retriable 3.8.0 and will be removed in Retriable 4.0. It still works in 3.x, but any non-nil value supplied through `Retriable.configure`, `Retriable.retriable(...)`, or `Retriable.with_override(...)` emits a deprecation warning. In Retriable 4.0, passing `timeout:` will raise `ArgumentError` because it will no longer be a valid option.
|
|
176
250
|
|
|
177
|
-
|
|
251
|
+
`timeout:` is deprecated because it is a thin wrapper around `Timeout.timeout`, which may be [unsafe](https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/) [and](http://blog.headius.com/2008/02/ruby-threadraise-threadkill-timeoutrb.html) [even](https://adamhooper.medium.com/in-ruby-dont-use-timeout-77d9d4e5a001) [dangerous](https://www.mikeperham.com/2015/05/08/timeout-rubys-most-dangerous-api/). It can interrupt the retried block at any line, including inside libraries that are not interrupt-safe.
|
|
252
|
+
|
|
253
|
+
Prefer timeout settings from the library you are calling, such as `Net::HTTP#read_timeout`, `Net::HTTP#open_timeout`, or Faraday's request timeout options. If you still need `Timeout.timeout`, wrap the retried block explicitly so the risk is visible at the call site:
|
|
178
254
|
|
|
179
255
|
```ruby
|
|
180
|
-
|
|
181
|
-
|
|
256
|
+
require "timeout"
|
|
257
|
+
|
|
258
|
+
Retriable.retriable(on: Timeout::Error, tries: 3) do
|
|
259
|
+
Timeout.timeout(5) do
|
|
260
|
+
# code here...
|
|
261
|
+
end
|
|
182
262
|
end
|
|
183
263
|
```
|
|
184
264
|
|
|
185
|
-
If you
|
|
265
|
+
Like the deprecated `timeout:` option, `Timeout.timeout(5)` inside the block is per-try — each retry gets a fresh 5-second budget. If you want an overall cap across all retries instead, prefer `max_elapsed_time:`.
|
|
266
|
+
|
|
267
|
+
The deprecation warning is emitted under the `:deprecated` warning category and at most once per process. To silence it (for example, in tests), use the standard Ruby controls — set `Warning[:deprecated] = false`, run with `ruby -W:no-deprecated`, or override `Warning.warn` to filter the message.
|
|
268
|
+
|
|
269
|
+
If you need millisecond units of time for the sleep interval:
|
|
186
270
|
|
|
187
271
|
```ruby
|
|
188
|
-
Retriable.retriable(base_interval: (200 / 1000.0)
|
|
272
|
+
Retriable.retriable(base_interval: (200 / 1000.0)) do
|
|
189
273
|
# code here...
|
|
190
274
|
end
|
|
191
275
|
```
|
|
@@ -202,6 +286,21 @@ end
|
|
|
202
286
|
|
|
203
287
|
This example makes 5 total attempts. If the first attempt fails, the 2nd attempt occurs 0.5 seconds later.
|
|
204
288
|
|
|
289
|
+
### Unbounded Retries (Opt-in)
|
|
290
|
+
|
|
291
|
+
You can opt in to unbounded retries with `tries: Float::INFINITY`. This is useful for long-running worker processes where retrying should continue indefinitely, but it must be used with care.
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
Retriable.retriable(tries: Float::INFINITY, max_elapsed_time: 300) do
|
|
295
|
+
# code here...
|
|
296
|
+
end
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
When `tries: Float::INFINITY` is set:
|
|
300
|
+
|
|
301
|
+
- `max_elapsed_time` must be a finite number. Retriable raises `ArgumentError` if it is `nil` or `Float::INFINITY`. This is a safety bound that prevents accidentally unbounded loops.
|
|
302
|
+
- Custom `intervals:` cannot be combined with `Float::INFINITY` and raises `ArgumentError`. Use the exponential backoff settings (`base_interval`, `multiplier`, `max_interval`, `rand_factor`) instead.
|
|
303
|
+
|
|
205
304
|
### Turn off Exponential Backoff
|
|
206
305
|
|
|
207
306
|
Exponential backoff is enabled by default. If you want to simply retry code every second, 5 times maximum, you can do this:
|
|
@@ -246,6 +345,26 @@ Retriable.retriable(on_retry: do_this_on_each_retry) do
|
|
|
246
345
|
end
|
|
247
346
|
```
|
|
248
347
|
|
|
348
|
+
#### Disabling a Configured Callback Per Call
|
|
349
|
+
|
|
350
|
+
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.
|
|
351
|
+
|
|
352
|
+
```ruby
|
|
353
|
+
Retriable.configure do |c|
|
|
354
|
+
c.on_retry = ->(exception, try, elapsed_time, next_interval) { log(...) }
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Most calls use the configured callback.
|
|
358
|
+
Retriable.retriable do
|
|
359
|
+
# ...
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# This specific call opts out of the configured callback.
|
|
363
|
+
Retriable.retriable(on_retry: false) do
|
|
364
|
+
# ...
|
|
365
|
+
end
|
|
366
|
+
```
|
|
367
|
+
|
|
249
368
|
### Ensure/Else
|
|
250
369
|
|
|
251
370
|
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:
|
|
@@ -336,71 +455,16 @@ retriable_with_context(:api) do
|
|
|
336
455
|
end
|
|
337
456
|
```
|
|
338
457
|
|
|
339
|
-
##
|
|
340
|
-
|
|
341
|
-
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.
|
|
342
|
-
|
|
343
|
-
You can disable retrying by setting `tries` to 1 in the test environment. If you want to test that the code is retrying an error, you want to [turn off exponential backoff](#turn-off-exponential-backoff).
|
|
344
|
-
|
|
345
|
-
Under Rails, you could change your initializer to have different options in test, as follows:
|
|
346
|
-
|
|
347
|
-
```ruby
|
|
348
|
-
# config/initializers/retriable.rb
|
|
349
|
-
Retriable.configure do |c|
|
|
350
|
-
# ... default configuration
|
|
351
|
-
|
|
352
|
-
if Rails.env.test?
|
|
353
|
-
c.tries = 1
|
|
354
|
-
end
|
|
355
|
-
end
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
Note: In this and the following examples, `Retriable.configure` sets a default config, it doesn't override the configuration for the `retriable` method calls. Calling `Retriable.retriable` with options will override the default configuration for that call. So if you have `tries` set to 5 in `Retriable.configure`, but then you call `Retriable.retriable(tries: 3)`, that call will use 3 tries instead of 5. The configuration is basically a default set of options that can be overridden by passing options to the `retriable` method or by using contexts.
|
|
359
|
-
|
|
360
|
-
Alternately, if you are using RSpec, you could override the Retriable confguration in your `spec_helper`.
|
|
361
|
-
|
|
362
|
-
```ruby
|
|
363
|
-
# spec/spec_helper.rb
|
|
364
|
-
Retriable.configure do |c|
|
|
365
|
-
c.tries = 1
|
|
366
|
-
end
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
If you have defined contexts for your configuration, you'll need to change values for each context, because those values take precedence over the default configured value.
|
|
458
|
+
## Testing
|
|
370
459
|
|
|
371
|
-
|
|
460
|
+
`Retriable.with_override` is designed to short-circuit retries in your test
|
|
461
|
+
suite so failing blocks do not slow tests down. The simplest pattern is an
|
|
462
|
+
RSpec `around(:each)` hook (or your test framework's equivalent) that wraps
|
|
463
|
+
every example in `with_override(tries: 1, base_interval: 0)`.
|
|
372
464
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
c.contexts[:google_api] = {
|
|
377
|
-
tries: 5,
|
|
378
|
-
base_interval: 3,
|
|
379
|
-
on: [
|
|
380
|
-
Net::ReadTimeout,
|
|
381
|
-
Signet::AuthorizationError,
|
|
382
|
-
Errno::ECONNRESET,
|
|
383
|
-
OpenSSL::SSL::SSLError
|
|
384
|
-
]
|
|
385
|
-
}
|
|
386
|
-
end
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
Then in your test environment, you would need to set each context and the default value:
|
|
390
|
-
|
|
391
|
-
```ruby
|
|
392
|
-
# spec/spec_helper.rb
|
|
393
|
-
Retriable.configure do |c|
|
|
394
|
-
c.multiplier = 1.0
|
|
395
|
-
c.rand_factor = 0.0
|
|
396
|
-
c.base_interval = 0
|
|
397
|
-
|
|
398
|
-
c.contexts.keys.each do |context|
|
|
399
|
-
c.contexts[context][:tries] = 1
|
|
400
|
-
c.contexts[context][:base_interval] = 0
|
|
401
|
-
end
|
|
402
|
-
end
|
|
403
|
-
```
|
|
465
|
+
For Rails integration, opting out of the override for specific tests, and
|
|
466
|
+
overriding configured contexts in tests, see
|
|
467
|
+
[docs/testing.md](docs/testing.md).
|
|
404
468
|
|
|
405
469
|
## Credits
|
|
406
470
|
|
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.
|