retriable 3.8.0 → 4.0.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 +20 -7
- data/.hound.yml +1 -1
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +18 -0
- data/Gemfile +2 -1
- data/README.md +88 -48
- data/lib/retriable/config.rb +3 -57
- data/lib/retriable/core_ext/kernel.rb +4 -4
- data/lib/retriable/validation.rb +4 -7
- data/lib/retriable/version.rb +1 -1
- data/lib/retriable.rb +36 -24
- data/retriable.gemspec +2 -7
- data/spec/config_spec.rb +17 -97
- data/spec/retriable_spec.rb +287 -93
- data/spec/spec_helper.rb +0 -13
- metadata +6 -52
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 392e6bc1b69a4c85c286f5df1b90c5991243cd7290cd3b41c028f253ad16dae4
|
|
4
|
+
data.tar.gz: f078e585b65160045cc71e4a9f8746b7b480645cf841ff13a7bbbec78fc9ffeb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 01dc250d04fbc71c89d6af526ab061ec4439d382ec50872776859415df9bad2d1d4c8f2e0cfb53fc263343d72871ce691df5840721d33caf0ccaacee03bf9f6e
|
|
7
|
+
data.tar.gz: f1ae42a3015541313322371fd297f5f677d1a974747b48a524c25e1f676fc7adeb8d309975851198c69632fc124a34e4db5488486783a40beb8758ef12cf6c01
|
data/.github/workflows/main.yml
CHANGED
|
@@ -14,18 +14,16 @@ jobs:
|
|
|
14
14
|
ci:
|
|
15
15
|
# The type of runner that the job will run on
|
|
16
16
|
runs-on: ${{ matrix.os }}
|
|
17
|
+
# Ruby 4.0 is still in preview. Treat its results as informational so a
|
|
18
|
+
# preview-only regression doesn't block merges. Drop this gate (or update
|
|
19
|
+
# the version literal) once Ruby 4.0 is released and we treat it as
|
|
20
|
+
# required.
|
|
21
|
+
continue-on-error: ${{ matrix.ruby == '4.0' }}
|
|
17
22
|
strategy:
|
|
18
23
|
matrix:
|
|
19
24
|
os: [ubuntu-24.04]
|
|
20
25
|
ruby:
|
|
21
26
|
[
|
|
22
|
-
"2.3",
|
|
23
|
-
"2.4",
|
|
24
|
-
"2.5",
|
|
25
|
-
"2.6",
|
|
26
|
-
"2.7",
|
|
27
|
-
"3.0",
|
|
28
|
-
"3.1",
|
|
29
27
|
"3.2",
|
|
30
28
|
"3.3",
|
|
31
29
|
"3.4",
|
|
@@ -50,3 +48,18 @@ jobs:
|
|
|
50
48
|
|
|
51
49
|
- name: Run rspec
|
|
52
50
|
run: bundle exec rspec
|
|
51
|
+
|
|
52
|
+
lint:
|
|
53
|
+
runs-on: ubuntu-24.04
|
|
54
|
+
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
57
|
+
|
|
58
|
+
- name: Setup ruby
|
|
59
|
+
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
|
|
60
|
+
with:
|
|
61
|
+
ruby-version: "3.3"
|
|
62
|
+
bundler-cache: true
|
|
63
|
+
|
|
64
|
+
- name: Run rubocop
|
|
65
|
+
run: bundle exec rubocop
|
data/.hound.yml
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
ruby:
|
|
2
|
-
|
|
2
|
+
enabled: false
|
data/.rubocop.yml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
AllCops:
|
|
2
2
|
NewCops: enable
|
|
3
|
-
TargetRubyVersion: 2
|
|
3
|
+
TargetRubyVersion: 3.2
|
|
4
4
|
|
|
5
5
|
Style/StringLiterals:
|
|
6
6
|
EnforcedStyle: double_quotes
|
|
@@ -40,3 +40,6 @@ Metrics/AbcSize:
|
|
|
40
40
|
|
|
41
41
|
Style/TrailingCommaInArrayLiteral:
|
|
42
42
|
Enabled: false
|
|
43
|
+
|
|
44
|
+
Naming/MethodParameterName:
|
|
45
|
+
MinNameLength: 2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# HEAD
|
|
2
2
|
|
|
3
|
+
## 4.0.0
|
|
4
|
+
|
|
5
|
+
**This is a major release with breaking changes. Please read carefully before upgrading.**
|
|
6
|
+
|
|
7
|
+
### Breaking changes
|
|
8
|
+
|
|
9
|
+
- Removed `timeout:` option. The `timeout:` option has been removed from `Retriable.retriable`, `Retriable.configure`, and `Retriable.with_override`. It was a thin wrapper around Ruby's `Timeout.timeout`, which has well-documented safety issues: it interrupts execution at arbitrary lines and can corrupt internal state in libraries that are not interrupt-safe (mutexes, file handles, network sockets, allocator state). This was first raised against this gem in [#96](https://github.com/kamui/retriable/issues/96) in 2021; Retriable 3.8.0 deprecated the option, and 4.0 removes the footgun entirely. As a side effect, the historical bug where Retriable's own internal `Timeout::Error` was silently retried by default is no longer reachable, since Retriable no longer raises a timeout itself. User-raised `Timeout::Error` (for example, from a `Timeout.timeout` block you write inside the retried block) is still matched by the default `on: [StandardError]` because `Timeout::Error < RuntimeError < StandardError`. Passing `timeout:` to `Retriable.retriable` or `Retriable.with_override` now raises `ArgumentError`; setting `config.timeout` in `Retriable.configure` now raises `NoMethodError` because the configuration attribute has been removed. See the [4.0 migration section in the README](README.md#migration-from-3x-to-40) for replacement patterns.
|
|
10
|
+
- Minimum Ruby version is now 3.2. Support for Ruby 2.x, 3.0, and 3.1 has been dropped in Retriable 4.0. If you need Retriable on Ruby 2.3.0-3.1.x, the 3.8.x line (`~> 3.8`) remains available.
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
- Add [`on_give_up`](README.md#callbacks) 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.
|
|
15
|
+
- Accept a [`Set` of `Exception` classes](README.md#configuring-which-options-to-retry-with-on) as the `on:` option, in addition to a single class, an `Array`, or a `Hash`.
|
|
16
|
+
|
|
17
|
+
### Internal
|
|
18
|
+
|
|
19
|
+
- Switched `Retriable.retriable`, `Retriable.with_context`, and the `Kernel` extension methods to Ruby 3.1+ anonymous block forwarding. No user-visible behavior change.
|
|
20
|
+
|
|
3
21
|
## 3.8.0
|
|
4
22
|
|
|
5
23
|
### Deprecations
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Retriable
|
|
2
2
|
|
|
3
3
|

|
|
4
|
-
[](https://houndci.com)
|
|
5
4
|
|
|
6
5
|
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
6
|
|
|
8
7
|
## Table of Contents
|
|
9
8
|
|
|
10
9
|
- [Requirements](#requirements)
|
|
10
|
+
- [Migration from 3.x to 4.0](#migration-from-3x-to-40)
|
|
11
11
|
- [Installation](#installation)
|
|
12
12
|
- [Usage](#usage)
|
|
13
13
|
- [Defaults](#defaults)
|
|
@@ -17,10 +17,11 @@ Retriable is a simple DSL to retry failed code blocks with randomized [exponenti
|
|
|
17
17
|
- [Configuration](#configuration)
|
|
18
18
|
- [Override](#override)
|
|
19
19
|
- [Example Usage](#example-usage)
|
|
20
|
-
- [Migrating off `timeout:`](#migrating-off-timeout)
|
|
21
20
|
- [Custom Interval Array](#custom-interval-array)
|
|
21
|
+
- [Unbounded Retries (Opt-in)](#unbounded-retries-opt-in)
|
|
22
22
|
- [Turn off Exponential Backoff](#turn-off-exponential-backoff)
|
|
23
23
|
- [Callbacks](#callbacks)
|
|
24
|
+
- [Disabling a Configured Callback Per Call](#disabling-a-configured-callback-per-call)
|
|
24
25
|
- [Ensure/Else](#ensureelse)
|
|
25
26
|
- [Contexts](#contexts)
|
|
26
27
|
- [Kernel Extension](#kernel-extension)
|
|
@@ -31,13 +32,47 @@ Retriable is a simple DSL to retry failed code blocks with randomized [exponenti
|
|
|
31
32
|
|
|
32
33
|
## Requirements
|
|
33
34
|
|
|
34
|
-
Ruby
|
|
35
|
+
Ruby 3.2+
|
|
35
36
|
|
|
36
|
-
If you need
|
|
37
|
+
If you need Ruby 2.3.0-3.1.x support, use the [3.8.x branch](https://github.com/kamui/retriable/tree/3.8.x) by specifying `~> 3.8` in your Gemfile.
|
|
37
38
|
|
|
38
|
-
If you need
|
|
39
|
+
If you need Ruby 2.0.0-2.2.x support, use the [3.1 branch](https://github.com/kamui/retriable/tree/3.1.x) by specifying `~3.1` in your Gemfile.
|
|
39
40
|
|
|
40
|
-
If you need
|
|
41
|
+
If you need Ruby 1.9.3 support, use the [2.x branch](https://github.com/kamui/retriable/tree/2.x) by specifying `~2.1` in your Gemfile.
|
|
42
|
+
|
|
43
|
+
If you need Ruby 1.8.x to 1.9.2 support, use the [1.x branch](https://github.com/kamui/retriable/tree/1.x) by specifying `~1.4` in your Gemfile.
|
|
44
|
+
|
|
45
|
+
## Migration from 3.x to 4.0
|
|
46
|
+
|
|
47
|
+
### Ruby version
|
|
48
|
+
|
|
49
|
+
Retriable 4.0 requires Ruby 3.2 or later. If you run Ruby 2.3.0-3.1.x, or want to stay on the 3.x gem line, use Retriable 3.8.x by specifying `~> 3.8` in your Gemfile.
|
|
50
|
+
|
|
51
|
+
### `timeout:` option removed
|
|
52
|
+
|
|
53
|
+
The `timeout:` option was deprecated in Retriable 3.8.0 and has been removed in Retriable 4.0. It was a thin wrapper around `Timeout.timeout`, which has well-documented safety issues: it interrupts execution at arbitrary lines and can corrupt internal state in libraries that are not interrupt-safe. See [issue #96](https://github.com/kamui/retriable/issues/96) for the original report of this problem.
|
|
54
|
+
|
|
55
|
+
If you previously used `Retriable.retriable(timeout: 5) { ... }`, you have two recommended alternatives:
|
|
56
|
+
|
|
57
|
+
1. **Use your library's native timeout** (preferred). For example, configure `Net::HTTP#read_timeout`, Faraday's `request.timeout`, or your database client's statement timeout. Library-native timeouts do not have the safety issues of `Timeout.timeout`.
|
|
58
|
+
|
|
59
|
+
2. **Manage the timeout yourself inside the block** if no native option exists:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
require "timeout"
|
|
63
|
+
|
|
64
|
+
Retriable.retriable do
|
|
65
|
+
Timeout.timeout(5) do
|
|
66
|
+
# code here...
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Note:** This still uses `Timeout.timeout`, which has the same safety issues that motivated removing the option — interruption can happen at any line, including inside non-interrupt-safe library code (mutexes, file handles, network sockets, allocator state). Prefer option 1 wherever possible. For background, see [why Ruby's `Timeout` is dangerous](https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/), [Headius on Thread#raise and Timeout](http://blog.headius.com/2008/02/ruby-threadraise-threadkill-timeoutrb.html), [In Ruby, don't use `Timeout`](https://adamhooper.medium.com/in-ruby-dont-use-timeout-77d9d4e5a001), and [Timeout: Ruby's most dangerous API](https://www.mikeperham.com/2015/05/08/timeout-rubys-most-dangerous-api/).
|
|
72
|
+
|
|
73
|
+
Like the removed `timeout:` option, `Timeout.timeout(5)` inside the block is per-try — each retry gets a fresh 5-second budget. For an overall cap across all retries, use `max_elapsed_time:` instead.
|
|
74
|
+
|
|
75
|
+
Passing `timeout:` to `Retriable.retriable` or `Retriable.with_override` now raises `ArgumentError`. The `timeout` configuration attribute has also been removed, so `Retriable.configure { |c| c.timeout = 5 }` now raises `NoMethodError`.
|
|
41
76
|
|
|
42
77
|
## Installation
|
|
43
78
|
|
|
@@ -56,7 +91,7 @@ require 'retriable'
|
|
|
56
91
|
In your Gemfile:
|
|
57
92
|
|
|
58
93
|
```ruby
|
|
59
|
-
gem 'retriable', '~>
|
|
94
|
+
gem 'retriable', '~> 4.0'
|
|
60
95
|
```
|
|
61
96
|
|
|
62
97
|
## Usage
|
|
@@ -103,29 +138,29 @@ The default interval table with 10 tries looks like this (in seconds, rounded to
|
|
|
103
138
|
|
|
104
139
|
Here are the available options, in some vague order of relevance to most common use patterns:
|
|
105
140
|
|
|
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
|
-
| **`
|
|
113
|
-
| **`
|
|
114
|
-
| **`
|
|
115
|
-
| **`
|
|
116
|
-
| **`
|
|
117
|
-
| **`
|
|
118
|
-
| **`
|
|
119
|
-
| **`
|
|
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`,
|
|
141
|
+
| Option | Default | Definition |
|
|
142
|
+
| ---------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
143
|
+
| **`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. |
|
|
144
|
+
| **`on`** | `[StandardError]` | Type of exceptions to retry. [Read more](#configuring-which-options-to-retry-with-on). |
|
|
145
|
+
| **`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). |
|
|
146
|
+
| **`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). |
|
|
147
|
+
| **`on_give_up`** | `nil` | `Proc` to call when Retriable stops retrying after a rescued retriable exception. [Read more](#callbacks). |
|
|
148
|
+
| **`sleep_disabled`** | `false` | When true, disable exponential backoff and attempt retries immediately. |
|
|
149
|
+
| **`base_interval`** | `0.5` | The initial interval in seconds between tries. |
|
|
150
|
+
| **`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`. |
|
|
151
|
+
| **`max_interval`** | `60` | The maximum interval in seconds that any individual retry can reach. |
|
|
152
|
+
| **`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. |
|
|
153
|
+
| **`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])` |
|
|
154
|
+
| **`intervals`** | `nil` | Skip generated intervals and provide your own array of intervals in seconds. [Read more](#custom-interval-array). |
|
|
155
|
+
|
|
156
|
+
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`, and `max_elapsed_time` must be non-negative numbers, with `max_elapsed_time` 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`.
|
|
122
157
|
|
|
123
158
|
#### Configuring Which Options to Retry With :on
|
|
124
159
|
|
|
125
160
|
**`:on`** Can take the form:
|
|
126
161
|
|
|
127
162
|
- An `Exception` class (retry every exception of this type, including subclasses)
|
|
128
|
-
- An `Array` of `Exception` classes (retry any exception of one of these types, including subclasses)
|
|
163
|
+
- An `Array` or `Set` of `Exception` classes (retry any exception of one of these types, including subclasses)
|
|
129
164
|
- A `Hash` where the keys are `Exception` classes and the values are one of:
|
|
130
165
|
- `nil` (retry every exception of the key's type, including subclasses)
|
|
131
166
|
- A single `Regexp` pattern (retries exceptions ONLY if their `message` matches the pattern)
|
|
@@ -219,6 +254,8 @@ see [docs/testing.md](docs/testing.md).
|
|
|
219
254
|
This example will only retry on a `Timeout::Error`, retry 3 times and sleep for a full second before each try.
|
|
220
255
|
|
|
221
256
|
```ruby
|
|
257
|
+
require "timeout"
|
|
258
|
+
|
|
222
259
|
Retriable.retriable(on: Timeout::Error, tries: 3, base_interval: 1) do
|
|
223
260
|
# code here...
|
|
224
261
|
end
|
|
@@ -227,6 +264,8 @@ end
|
|
|
227
264
|
You can also specify multiple errors to retry on by passing an array of exceptions.
|
|
228
265
|
|
|
229
266
|
```ruby
|
|
267
|
+
require "timeout"
|
|
268
|
+
|
|
230
269
|
Retriable.retriable(on: [Timeout::Error, Errno::ECONNRESET]) do
|
|
231
270
|
# code here...
|
|
232
271
|
end
|
|
@@ -244,28 +283,6 @@ Retriable.retriable(on: {
|
|
|
244
283
|
end
|
|
245
284
|
```
|
|
246
285
|
|
|
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.
|
|
250
|
-
|
|
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:
|
|
254
|
-
|
|
255
|
-
```ruby
|
|
256
|
-
require "timeout"
|
|
257
|
-
|
|
258
|
-
Retriable.retriable(on: Timeout::Error, tries: 3) do
|
|
259
|
-
Timeout.timeout(5) do
|
|
260
|
-
# code here...
|
|
261
|
-
end
|
|
262
|
-
end
|
|
263
|
-
```
|
|
264
|
-
|
|
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
286
|
If you need millisecond units of time for the sleep interval:
|
|
270
287
|
|
|
271
288
|
```ruby
|
|
@@ -347,7 +364,7 @@ end
|
|
|
347
364
|
|
|
348
365
|
#### Disabling a Configured Callback Per Call
|
|
349
366
|
|
|
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
|
|
367
|
+
If `on_retry` is set in `Retriable.configure`, every call uses it by default. To opt a specific call out — for example, a critical call site that should not log on retry — pass `on_retry: false` or `on_retry: nil`.
|
|
351
368
|
|
|
352
369
|
```ruby
|
|
353
370
|
Retriable.configure do |c|
|
|
@@ -365,6 +382,28 @@ Retriable.retriable(on_retry: false) do
|
|
|
365
382
|
end
|
|
366
383
|
```
|
|
367
384
|
|
|
385
|
+
You can also use `:on_give_up` to run a callback when Retriable stops retrying after a rescued retriable exception. This callback receives the `exception`, the `try_number`, the `elapsed_time` for all tries so far, the `next_interval`, and the `reason` Retriable is giving up. The `reason` is either `:tries_exhausted` or `:max_elapsed_time`.
|
|
386
|
+
|
|
387
|
+
```ruby
|
|
388
|
+
do_this_when_retries_stop = Proc.new do |exception, try, elapsed_time, next_interval, reason|
|
|
389
|
+
log "#{exception.class}: '#{exception.message}' - gave up after #{try} tries because #{reason}."
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
Retriable.retriable(on_give_up: do_this_when_retries_stop) do
|
|
393
|
+
# code here...
|
|
394
|
+
end
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
When the reason is `:tries_exhausted`, `next_interval` is `nil` because there is no next retry. When the reason is `:max_elapsed_time`, `next_interval` is the interval that would have been slept before the next try. This reason means the next retry would exceed `max_elapsed_time`, not necessarily that the elapsed time has already exceeded it.
|
|
398
|
+
|
|
399
|
+
If both `:on_retry` and `:on_give_up` are configured, `:on_retry` still runs first for the final rescued retriable exception. This preserves the existing behavior that `:on_retry` runs whenever Retriable rescues an exception that matches its retry rules.
|
|
400
|
+
|
|
401
|
+
If you configure a default `:on_give_up` callback but want to suppress it for a specific call, pass `on_give_up: false` (or `nil`). Both are treated as "no callback".
|
|
402
|
+
|
|
403
|
+
`: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.
|
|
404
|
+
|
|
405
|
+
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.
|
|
406
|
+
|
|
368
407
|
### Ensure/Else
|
|
369
408
|
|
|
370
409
|
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:
|
|
@@ -392,7 +431,8 @@ Retriable.configure do |c|
|
|
|
392
431
|
c.contexts[:aws] = {
|
|
393
432
|
tries: 3,
|
|
394
433
|
base_interval: 5,
|
|
395
|
-
on_retry: Proc.new { puts 'Curse you, AWS!' }
|
|
434
|
+
on_retry: Proc.new { puts 'Curse you, AWS!' },
|
|
435
|
+
on_give_up: Proc.new { |_e, _try, _elapsed, _interval, reason| puts "Gave up on AWS: #{reason}" }
|
|
396
436
|
}
|
|
397
437
|
c.contexts[:mysql] = {
|
|
398
438
|
tries: 10,
|
data/lib/retriable/config.rb
CHANGED
|
@@ -11,26 +11,13 @@ module Retriable
|
|
|
11
11
|
sleep_disabled
|
|
12
12
|
max_elapsed_time
|
|
13
13
|
intervals
|
|
14
|
-
timeout
|
|
15
14
|
on
|
|
16
15
|
retry_if
|
|
17
16
|
on_retry
|
|
17
|
+
on_give_up
|
|
18
18
|
contexts
|
|
19
19
|
]).freeze
|
|
20
20
|
|
|
21
|
-
TIMEOUT_DEPRECATION_MESSAGE = "NOTE: Retriable's `timeout:` option is deprecated and will be removed in " \
|
|
22
|
-
"Retriable 4.0. It is a thin wrapper around `Timeout.timeout`, which " \
|
|
23
|
-
"can interrupt execution at arbitrary lines and corrupt internal state " \
|
|
24
|
-
"in libraries that are not interrupt-safe. Prefer your library's native " \
|
|
25
|
-
"timeout, or wrap your block in `Timeout.timeout(...)` yourself."
|
|
26
|
-
private_constant :TIMEOUT_DEPRECATION_MESSAGE
|
|
27
|
-
|
|
28
|
-
@timeout_deprecation_warned = false
|
|
29
|
-
|
|
30
|
-
class << self
|
|
31
|
-
attr_accessor :timeout_deprecation_warned
|
|
32
|
-
end
|
|
33
|
-
|
|
34
21
|
attr_accessor(*ATTRIBUTES)
|
|
35
22
|
|
|
36
23
|
def initialize(opts = {})
|
|
@@ -44,10 +31,10 @@ module Retriable
|
|
|
44
31
|
@sleep_disabled = false
|
|
45
32
|
@max_elapsed_time = 900 # 15 min
|
|
46
33
|
@intervals = nil
|
|
47
|
-
@timeout = nil
|
|
48
34
|
@on = [StandardError]
|
|
49
35
|
@retry_if = nil
|
|
50
36
|
@on_retry = nil
|
|
37
|
+
@on_give_up = nil
|
|
51
38
|
@contexts = {}
|
|
52
39
|
|
|
53
40
|
opts.each do |k, v|
|
|
@@ -60,14 +47,10 @@ module Retriable
|
|
|
60
47
|
end
|
|
61
48
|
|
|
62
49
|
def to_h
|
|
63
|
-
ATTRIBUTES.
|
|
64
|
-
hash[key] = public_send(key)
|
|
65
|
-
end
|
|
50
|
+
ATTRIBUTES.to_h { |key| [key, public_send(key)] }
|
|
66
51
|
end
|
|
67
52
|
|
|
68
53
|
def validate!
|
|
69
|
-
warn_timeout_deprecation
|
|
70
|
-
validate_optional_non_negative_number(:timeout, timeout)
|
|
71
54
|
validate_on(on)
|
|
72
55
|
validate_intervals
|
|
73
56
|
if unbounded_tries?(tries)
|
|
@@ -84,43 +67,6 @@ module Retriable
|
|
|
84
67
|
|
|
85
68
|
private
|
|
86
69
|
|
|
87
|
-
# Emits the `timeout:` deprecation notice at most once per process.
|
|
88
|
-
#
|
|
89
|
-
# On Rubies that support `Kernel#warn(category: :deprecated)` (2.7+), the
|
|
90
|
-
# notice is emitted under the `:deprecated` category, so callers can use the
|
|
91
|
-
# standard controls (`Warning[:deprecated] = false`, `-W:no-deprecated`,
|
|
92
|
-
# `Warning.warn` override) to silence it. On older Rubies the kwarg is not
|
|
93
|
-
# available and we fall back to plain `Kernel.warn`.
|
|
94
|
-
#
|
|
95
|
-
# When the warning is suppressed (either because `Warning[:deprecated]` is
|
|
96
|
-
# false or the runtime has otherwise muted the category), we deliberately
|
|
97
|
-
# leave the once-per-process flag unset so a future call with the category
|
|
98
|
-
# re-enabled still surfaces the notice.
|
|
99
|
-
def warn_timeout_deprecation
|
|
100
|
-
return if timeout.nil?
|
|
101
|
-
return if self.class.timeout_deprecation_warned
|
|
102
|
-
|
|
103
|
-
category_supported = deprecated_warning_category_supported?
|
|
104
|
-
return if category_supported && !deprecated_warnings_enabled?
|
|
105
|
-
|
|
106
|
-
self.class.timeout_deprecation_warned = true
|
|
107
|
-
if category_supported
|
|
108
|
-
Kernel.warn(TIMEOUT_DEPRECATION_MESSAGE, category: :deprecated)
|
|
109
|
-
else
|
|
110
|
-
Kernel.warn(TIMEOUT_DEPRECATION_MESSAGE)
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def deprecated_warning_category_supported?
|
|
115
|
-
defined?(Warning) && Kernel.method(:warn).parameters.any? { |type, name| type == :key && name == :category }
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def deprecated_warnings_enabled?
|
|
119
|
-
return true unless defined?(Warning) && Warning.respond_to?(:[])
|
|
120
|
-
|
|
121
|
-
Warning[:deprecated]
|
|
122
|
-
end
|
|
123
|
-
|
|
124
70
|
def validate_backoff_options
|
|
125
71
|
validate_non_negative_number(:base_interval, base_interval)
|
|
126
72
|
validate_non_negative_number(:multiplier, multiplier)
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
require_relative "../../retriable"
|
|
4
4
|
|
|
5
5
|
module Kernel
|
|
6
|
-
def retriable(opts = {}, &
|
|
7
|
-
Retriable.retriable(opts, &
|
|
6
|
+
def retriable(opts = {}, &)
|
|
7
|
+
Retriable.retriable(opts, &)
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
-
def retriable_with_context(context_key, opts = {}, &
|
|
11
|
-
Retriable.with_context(context_key, opts, &
|
|
10
|
+
def retriable_with_context(context_key, opts = {}, &)
|
|
11
|
+
Retriable.with_context(context_key, opts, &)
|
|
12
12
|
end
|
|
13
13
|
end
|
data/lib/retriable/validation.rb
CHANGED
|
@@ -46,7 +46,7 @@ module Retriable
|
|
|
46
46
|
|
|
47
47
|
# Validates an `on:` value. Acceptable shapes:
|
|
48
48
|
# - a Class that descends from Exception
|
|
49
|
-
# - an Array whose elements are Classes that descend from Exception
|
|
49
|
+
# - an Array or Set whose elements are Classes that descend from Exception
|
|
50
50
|
# - a Hash whose keys are such Classes and whose values are nil,
|
|
51
51
|
# a Regexp, or an Array of Regexps
|
|
52
52
|
#
|
|
@@ -58,12 +58,12 @@ module Retriable
|
|
|
58
58
|
# Regexp values explicitly.
|
|
59
59
|
def validate_on(value)
|
|
60
60
|
case value
|
|
61
|
-
|
|
61
|
+
in Hash
|
|
62
62
|
value.each do |klass, pattern|
|
|
63
63
|
validate_on_class(klass)
|
|
64
64
|
validate_on_hash_value(klass, pattern)
|
|
65
65
|
end
|
|
66
|
-
|
|
66
|
+
in Array | Set
|
|
67
67
|
value.each { |klass| validate_on_class(klass) }
|
|
68
68
|
else
|
|
69
69
|
validate_on_class(value)
|
|
@@ -79,10 +79,7 @@ module Retriable
|
|
|
79
79
|
def validate_on_hash_value(klass, pattern)
|
|
80
80
|
return if pattern.nil?
|
|
81
81
|
return if pattern.is_a?(Regexp)
|
|
82
|
-
|
|
83
|
-
# rubocop:disable Style/PredicateWithKind
|
|
84
|
-
return if pattern.is_a?(Array) && pattern.all? { |p| p.is_a?(Regexp) }
|
|
85
|
-
# rubocop:enable Style/PredicateWithKind
|
|
82
|
+
return if pattern.is_a?(Array) && pattern.all?(Regexp)
|
|
86
83
|
|
|
87
84
|
raise ArgumentError,
|
|
88
85
|
"on[#{klass}] must be nil, a Regexp, or an Array of Regexps, got #{pattern.inspect}"
|
data/lib/retriable/version.rb
CHANGED