retriable 3.7.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 +24 -0
- data/Gemfile +2 -1
- data/README.md +90 -37
- data/lib/retriable/config.rb +3 -6
- 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 -5
- data/spec/retriable_spec.rb +287 -12
- 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,29 @@
|
|
|
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
|
+
|
|
21
|
+
## 3.8.0
|
|
22
|
+
|
|
23
|
+
### Deprecations
|
|
24
|
+
|
|
25
|
+
- 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.
|
|
26
|
+
|
|
3
27
|
## 3.7.0
|
|
4
28
|
|
|
5
29
|
- 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!`.
|
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)
|
|
@@ -18,8 +18,10 @@ Retriable is a simple DSL to retry failed code blocks with randomized [exponenti
|
|
|
18
18
|
- [Override](#override)
|
|
19
19
|
- [Example Usage](#example-usage)
|
|
20
20
|
- [Custom Interval Array](#custom-interval-array)
|
|
21
|
+
- [Unbounded Retries (Opt-in)](#unbounded-retries-opt-in)
|
|
21
22
|
- [Turn off Exponential Backoff](#turn-off-exponential-backoff)
|
|
22
23
|
- [Callbacks](#callbacks)
|
|
24
|
+
- [Disabling a Configured Callback Per Call](#disabling-a-configured-callback-per-call)
|
|
23
25
|
- [Ensure/Else](#ensureelse)
|
|
24
26
|
- [Contexts](#contexts)
|
|
25
27
|
- [Kernel Extension](#kernel-extension)
|
|
@@ -30,13 +32,47 @@ Retriable is a simple DSL to retry failed code blocks with randomized [exponenti
|
|
|
30
32
|
|
|
31
33
|
## Requirements
|
|
32
34
|
|
|
33
|
-
Ruby
|
|
35
|
+
Ruby 3.2+
|
|
34
36
|
|
|
35
|
-
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.
|
|
36
38
|
|
|
37
|
-
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.
|
|
38
40
|
|
|
39
|
-
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`.
|
|
40
76
|
|
|
41
77
|
## Installation
|
|
42
78
|
|
|
@@ -55,7 +91,7 @@ require 'retriable'
|
|
|
55
91
|
In your Gemfile:
|
|
56
92
|
|
|
57
93
|
```ruby
|
|
58
|
-
gem 'retriable', '~>
|
|
94
|
+
gem 'retriable', '~> 4.0'
|
|
59
95
|
```
|
|
60
96
|
|
|
61
97
|
## Usage
|
|
@@ -102,29 +138,29 @@ The default interval table with 10 tries looks like this (in seconds, rounded to
|
|
|
102
138
|
|
|
103
139
|
Here are the available options, in some vague order of relevance to most common use patterns:
|
|
104
140
|
|
|
105
|
-
| Option | Default | Definition
|
|
106
|
-
| ---------------------- | ----------------- |
|
|
107
|
-
| **`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.
|
|
108
|
-
| **`on`** | `[StandardError]` | Type of exceptions to retry. [Read more](#configuring-which-options-to-retry-with-on).
|
|
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).
|
|
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).
|
|
111
|
-
| **`
|
|
112
|
-
| **`
|
|
113
|
-
| **`
|
|
114
|
-
| **`
|
|
115
|
-
| **`
|
|
116
|
-
| **`
|
|
117
|
-
| **`
|
|
118
|
-
| **`
|
|
119
|
-
|
|
120
|
-
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`.
|
|
121
157
|
|
|
122
158
|
#### Configuring Which Options to Retry With :on
|
|
123
159
|
|
|
124
160
|
**`:on`** Can take the form:
|
|
125
161
|
|
|
126
162
|
- An `Exception` class (retry every exception of this type, including subclasses)
|
|
127
|
-
- 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)
|
|
128
164
|
- A `Hash` where the keys are `Exception` classes and the values are one of:
|
|
129
165
|
- `nil` (retry every exception of the key's type, including subclasses)
|
|
130
166
|
- A single `Regexp` pattern (retries exceptions ONLY if their `message` matches the pattern)
|
|
@@ -218,6 +254,8 @@ see [docs/testing.md](docs/testing.md).
|
|
|
218
254
|
This example will only retry on a `Timeout::Error`, retry 3 times and sleep for a full second before each try.
|
|
219
255
|
|
|
220
256
|
```ruby
|
|
257
|
+
require "timeout"
|
|
258
|
+
|
|
221
259
|
Retriable.retriable(on: Timeout::Error, tries: 3, base_interval: 1) do
|
|
222
260
|
# code here...
|
|
223
261
|
end
|
|
@@ -226,6 +264,8 @@ end
|
|
|
226
264
|
You can also specify multiple errors to retry on by passing an array of exceptions.
|
|
227
265
|
|
|
228
266
|
```ruby
|
|
267
|
+
require "timeout"
|
|
268
|
+
|
|
229
269
|
Retriable.retriable(on: [Timeout::Error, Errno::ECONNRESET]) do
|
|
230
270
|
# code here...
|
|
231
271
|
end
|
|
@@ -243,20 +283,10 @@ Retriable.retriable(on: {
|
|
|
243
283
|
end
|
|
244
284
|
```
|
|
245
285
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
The implementation uses `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/). You can use this option, but you need to be very careful because the code in the block, including libraries or other code it calls, could be interrupted by the timeout at any line. You must ensure you have the right rescue logic and guards in place ([Thread.handle_interrupt](https://www.rubydoc.info/stdlib/core/Thread.handle_interrupt)) to handle that possible behavior. If that's not possible, the recommendation is that you're better off impelenting your own timeout methods depending on what your code is doing than use this feature.
|
|
249
|
-
|
|
250
|
-
```ruby
|
|
251
|
-
Retriable.retriable(timeout: 60) do
|
|
252
|
-
# code here...
|
|
253
|
-
end
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
If you need millisecond units of time for the sleep or the timeout:
|
|
286
|
+
If you need millisecond units of time for the sleep interval:
|
|
257
287
|
|
|
258
288
|
```ruby
|
|
259
|
-
Retriable.retriable(base_interval: (200 / 1000.0)
|
|
289
|
+
Retriable.retriable(base_interval: (200 / 1000.0)) do
|
|
260
290
|
# code here...
|
|
261
291
|
end
|
|
262
292
|
```
|
|
@@ -334,7 +364,7 @@ end
|
|
|
334
364
|
|
|
335
365
|
#### Disabling a Configured Callback Per Call
|
|
336
366
|
|
|
337
|
-
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`.
|
|
338
368
|
|
|
339
369
|
```ruby
|
|
340
370
|
Retriable.configure do |c|
|
|
@@ -352,6 +382,28 @@ Retriable.retriable(on_retry: false) do
|
|
|
352
382
|
end
|
|
353
383
|
```
|
|
354
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
|
+
|
|
355
407
|
### Ensure/Else
|
|
356
408
|
|
|
357
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:
|
|
@@ -379,7 +431,8 @@ Retriable.configure do |c|
|
|
|
379
431
|
c.contexts[:aws] = {
|
|
380
432
|
tries: 3,
|
|
381
433
|
base_interval: 5,
|
|
382
|
-
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}" }
|
|
383
436
|
}
|
|
384
437
|
c.contexts[:mysql] = {
|
|
385
438
|
tries: 10,
|
data/lib/retriable/config.rb
CHANGED
|
@@ -11,10 +11,10 @@ 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
|
|
|
@@ -31,10 +31,10 @@ module Retriable
|
|
|
31
31
|
@sleep_disabled = false
|
|
32
32
|
@max_elapsed_time = 900 # 15 min
|
|
33
33
|
@intervals = nil
|
|
34
|
-
@timeout = nil
|
|
35
34
|
@on = [StandardError]
|
|
36
35
|
@retry_if = nil
|
|
37
36
|
@on_retry = nil
|
|
37
|
+
@on_give_up = nil
|
|
38
38
|
@contexts = {}
|
|
39
39
|
|
|
40
40
|
opts.each do |k, v|
|
|
@@ -47,13 +47,10 @@ module Retriable
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def to_h
|
|
50
|
-
ATTRIBUTES.
|
|
51
|
-
hash[key] = public_send(key)
|
|
52
|
-
end
|
|
50
|
+
ATTRIBUTES.to_h { |key| [key, public_send(key)] }
|
|
53
51
|
end
|
|
54
52
|
|
|
55
53
|
def validate!
|
|
56
|
-
validate_optional_non_negative_number(:timeout, timeout)
|
|
57
54
|
validate_on(on)
|
|
58
55
|
validate_intervals
|
|
59
56
|
if unbounded_tries?(tries)
|
|
@@ -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
data/lib/retriable.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "timeout"
|
|
4
3
|
require_relative "retriable/config"
|
|
5
4
|
require_relative "retriable/exponential_backoff"
|
|
6
5
|
require_relative "retriable/version"
|
|
@@ -41,7 +40,7 @@ module Retriable
|
|
|
41
40
|
end
|
|
42
41
|
end
|
|
43
42
|
|
|
44
|
-
def with_context(context_key, options = {}, &
|
|
43
|
+
def with_context(context_key, options = {}, &)
|
|
45
44
|
contexts = available_contexts
|
|
46
45
|
|
|
47
46
|
if !contexts.key?(context_key)
|
|
@@ -51,10 +50,10 @@ module Retriable
|
|
|
51
50
|
|
|
52
51
|
return unless block_given?
|
|
53
52
|
|
|
54
|
-
retriable(context_options_for(context_key, options), &
|
|
53
|
+
retriable(context_options_for(context_key, options), &)
|
|
55
54
|
end
|
|
56
55
|
|
|
57
|
-
def retriable(opts = {}, &
|
|
56
|
+
def retriable(opts = {}, &)
|
|
58
57
|
override_config = current_override
|
|
59
58
|
local_config = if opts.empty? && !override_config
|
|
60
59
|
config
|
|
@@ -66,10 +65,10 @@ module Retriable
|
|
|
66
65
|
local_config.validate!
|
|
67
66
|
|
|
68
67
|
plan = retry_plan(local_config)
|
|
69
|
-
timeout = local_config.timeout
|
|
70
68
|
on = local_config.on
|
|
71
69
|
retry_if = local_config.retry_if
|
|
72
70
|
on_retry = local_config.on_retry
|
|
71
|
+
on_give_up = local_config.on_give_up
|
|
73
72
|
sleep_disabled = local_config.sleep_disabled
|
|
74
73
|
max_elapsed_time = local_config.max_elapsed_time
|
|
75
74
|
|
|
@@ -79,22 +78,22 @@ module Retriable
|
|
|
79
78
|
elapsed_time = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time }
|
|
80
79
|
|
|
81
80
|
execute_tries(
|
|
82
|
-
max_tries: plan.max_tries, interval_for: plan.interval_for,
|
|
81
|
+
max_tries: plan.max_tries, interval_for: plan.interval_for,
|
|
83
82
|
exception_list: exception_list, on: on, retry_if: retry_if, on_retry: on_retry,
|
|
84
|
-
elapsed_time: elapsed_time, max_elapsed_time: max_elapsed_time,
|
|
85
|
-
sleep_disabled: sleep_disabled, &
|
|
83
|
+
on_give_up: on_give_up, elapsed_time: elapsed_time, max_elapsed_time: max_elapsed_time,
|
|
84
|
+
sleep_disabled: sleep_disabled, &
|
|
86
85
|
)
|
|
87
86
|
end
|
|
88
87
|
|
|
89
88
|
def execute_tries( # rubocop:disable Metrics/ParameterLists
|
|
90
|
-
max_tries:, interval_for:,
|
|
91
|
-
on:, retry_if:, on_retry:, elapsed_time:, max_elapsed_time:, sleep_disabled
|
|
89
|
+
max_tries:, interval_for:, exception_list:,
|
|
90
|
+
on:, retry_if:, on_retry:, on_give_up:, elapsed_time:, max_elapsed_time:, sleep_disabled:
|
|
92
91
|
)
|
|
93
92
|
try = 0
|
|
94
93
|
loop do
|
|
95
94
|
try += 1
|
|
96
95
|
begin
|
|
97
|
-
return
|
|
96
|
+
return yield(try)
|
|
98
97
|
rescue *exception_list => e
|
|
99
98
|
raise unless retriable_exception?(e, on, exception_list, retry_if)
|
|
100
99
|
|
|
@@ -102,7 +101,13 @@ module Retriable
|
|
|
102
101
|
call_on_retry(on_retry, e, try, elapsed_time.call, interval)
|
|
103
102
|
|
|
104
103
|
elapsed_interval = sleep_disabled == true ? 0 : interval
|
|
105
|
-
|
|
104
|
+
# Snapshot elapsed_time once so the stop check and on_give_up see the same value.
|
|
105
|
+
current_elapsed_time = elapsed_time.call
|
|
106
|
+
stop_reason = retry_stop_reason(try, max_tries, current_elapsed_time, elapsed_interval, max_elapsed_time)
|
|
107
|
+
if stop_reason
|
|
108
|
+
call_on_give_up(on_give_up, e, try, current_elapsed_time, interval, stop_reason)
|
|
109
|
+
raise
|
|
110
|
+
end
|
|
106
111
|
|
|
107
112
|
sleep interval if sleep_disabled != true
|
|
108
113
|
end
|
|
@@ -132,23 +137,30 @@ module Retriable
|
|
|
132
137
|
).interval_provider
|
|
133
138
|
end
|
|
134
139
|
|
|
135
|
-
def call_with_timeout(timeout, try)
|
|
136
|
-
return Timeout.timeout(timeout) { yield(try) } if timeout
|
|
137
|
-
|
|
138
|
-
yield(try)
|
|
139
|
-
end
|
|
140
|
-
|
|
141
140
|
def call_on_retry(on_retry, exception, try, elapsed_time, interval)
|
|
142
141
|
return unless on_retry
|
|
143
142
|
|
|
144
143
|
on_retry.call(exception, try, elapsed_time, interval)
|
|
145
144
|
end
|
|
146
145
|
|
|
147
|
-
def
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
def call_on_give_up( # rubocop:disable Metrics/ParameterLists
|
|
147
|
+
on_give_up, exception, try, elapsed_time, interval, reason
|
|
148
|
+
)
|
|
149
|
+
return unless on_give_up
|
|
150
|
+
|
|
151
|
+
on_give_up.call(exception, try, elapsed_time, interval, reason)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# `:tries_exhausted` is checked first, but the two conditions can't both hold
|
|
155
|
+
# on the same try in practice: `retry_plan` returns a nil interval whenever
|
|
156
|
+
# `try >= max_tries`, so `(elapsed_time + interval) > max_elapsed_time` is not
|
|
157
|
+
# evaluable on the exhausted-tries try. The early return guards against that
|
|
158
|
+
# nil and also pins precedence in case the plan ever changes.
|
|
159
|
+
def retry_stop_reason(try, max_tries, elapsed_time, interval, max_elapsed_time)
|
|
160
|
+
return :tries_exhausted if max_tries && try >= max_tries
|
|
161
|
+
return nil if max_elapsed_time.nil?
|
|
150
162
|
|
|
151
|
-
(elapsed_time + interval)
|
|
163
|
+
:max_elapsed_time if (elapsed_time + interval) > max_elapsed_time
|
|
152
164
|
end
|
|
153
165
|
|
|
154
166
|
# When `on` is a Hash, we need to verify the exception matches a pattern.
|
|
@@ -244,9 +256,9 @@ module Retriable
|
|
|
244
256
|
:execute_tries,
|
|
245
257
|
:retry_plan,
|
|
246
258
|
:interval_provider,
|
|
247
|
-
:call_with_timeout,
|
|
248
259
|
:call_on_retry,
|
|
249
|
-
:
|
|
260
|
+
:call_on_give_up,
|
|
261
|
+
:retry_stop_reason,
|
|
250
262
|
:retriable_exception?,
|
|
251
263
|
:hash_exception_match?,
|
|
252
264
|
:apply_override_options,
|
data/retriable.gemspec
CHANGED
|
@@ -15,15 +15,10 @@ Gem::Specification.new do |spec|
|
|
|
15
15
|
"APIs/services or file system calls."
|
|
16
16
|
spec.homepage = "https://github.com/kamui/retriable"
|
|
17
17
|
spec.license = "MIT"
|
|
18
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
18
19
|
|
|
19
20
|
spec.files = `git ls-files -z`.split("\x0")
|
|
20
|
-
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
21
21
|
spec.require_paths = ["lib"]
|
|
22
22
|
|
|
23
|
-
spec.required_ruby_version = ">=
|
|
24
|
-
|
|
25
|
-
spec.add_development_dependency "bundler"
|
|
26
|
-
spec.add_development_dependency "rspec", "~> 3"
|
|
27
|
-
|
|
28
|
-
spec.add_development_dependency "listen", "~> 3.1"
|
|
23
|
+
spec.required_ruby_version = ">= 3.2"
|
|
29
24
|
end
|
data/spec/config_spec.rb
CHANGED
|
@@ -32,10 +32,6 @@ describe Retriable::Config do
|
|
|
32
32
|
expect(default_config.intervals).to be_nil
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
it "timeout defaults to nil" do
|
|
36
|
-
expect(default_config.timeout).to be_nil
|
|
37
|
-
end
|
|
38
|
-
|
|
39
35
|
it "on defaults to [StandardError]" do
|
|
40
36
|
expect(default_config.on).to eq([StandardError])
|
|
41
37
|
end
|
|
@@ -48,6 +44,10 @@ describe Retriable::Config do
|
|
|
48
44
|
expect(default_config.on_retry).to be_nil
|
|
49
45
|
end
|
|
50
46
|
|
|
47
|
+
it "on_give_up handler defaults to nil" do
|
|
48
|
+
expect(default_config.on_give_up).to be_nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
51
|
it "contexts defaults to {}" do
|
|
52
52
|
expect(default_config.contexts).to eq({})
|
|
53
53
|
end
|
|
@@ -57,9 +57,12 @@ describe Retriable::Config do
|
|
|
57
57
|
expect { described_class.new(does_not_exist: 123) }.to raise_error(ArgumentError, /not a valid option/)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
+
it "rejects timeout as an unknown option" do
|
|
61
|
+
expect { described_class.new(timeout: 5) }.to raise_error(ArgumentError, /not a valid option/)
|
|
62
|
+
end
|
|
63
|
+
|
|
60
64
|
it "raises errors on invalid timing configuration" do
|
|
61
65
|
expect { described_class.new(rand_factor: 1.1) }.to raise_error(ArgumentError, /rand_factor/)
|
|
62
|
-
expect { described_class.new(timeout: -1) }.to raise_error(ArgumentError, /timeout/)
|
|
63
66
|
end
|
|
64
67
|
|
|
65
68
|
it "raises errors when intervals is not an array" do
|
|
@@ -99,6 +102,15 @@ describe Retriable::Config do
|
|
|
99
102
|
expect { described_class.new(on: [StandardError, RuntimeError]) }.not_to raise_error
|
|
100
103
|
end
|
|
101
104
|
|
|
105
|
+
it "accepts a Set of Exception subclasses" do
|
|
106
|
+
expect { described_class.new(on: Set[StandardError, RuntimeError]) }.not_to raise_error
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "rejects a Set containing a non-Exception class" do
|
|
110
|
+
expect { described_class.new(on: Set[StandardError, Kernel]) }
|
|
111
|
+
.to raise_error(ArgumentError, /on must be an Exception class/)
|
|
112
|
+
end
|
|
113
|
+
|
|
102
114
|
it "accepts a hash with nil pattern values" do
|
|
103
115
|
expect { described_class.new(on: { StandardError => nil }) }.not_to raise_error
|
|
104
116
|
end
|
data/spec/retriable_spec.rb
CHANGED
|
@@ -38,6 +38,39 @@ describe Retriable do
|
|
|
38
38
|
expect { retriable { increment_tries_with_exception } }.to raise_error(StandardError)
|
|
39
39
|
expect(@tries).to eq(3)
|
|
40
40
|
end
|
|
41
|
+
|
|
42
|
+
it "passes on_give_up through the kernel extension" do
|
|
43
|
+
require_relative "../lib/retriable/core_ext/kernel"
|
|
44
|
+
received_reason = nil
|
|
45
|
+
handler = proc { |_e, _try, _elapsed, _interval, reason| received_reason = reason }
|
|
46
|
+
|
|
47
|
+
expect { retriable(tries: 1, on_give_up: handler) { increment_tries_with_exception } }
|
|
48
|
+
.to raise_error(StandardError)
|
|
49
|
+
|
|
50
|
+
expect(received_reason).to eq(:tries_exhausted)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# These two specs lock in the anonymous block forwarding (`&`) semantics
|
|
54
|
+
# across both delegation layers: Kernel#retriable_with_context ->
|
|
55
|
+
# Retriable.with_context. If the `&` is dropped at either layer, the
|
|
56
|
+
# block is not forwarded and the inner `block_given?` check at
|
|
57
|
+
# lib/retriable.rb:51 short-circuits, causing the block to never run.
|
|
58
|
+
it "forwards a block through Kernel#retriable_with_context" do
|
|
59
|
+
require_relative "../lib/retriable/core_ext/kernel"
|
|
60
|
+
Retriable.configure { |c| c.contexts[:sql] = { tries: 1 } }
|
|
61
|
+
|
|
62
|
+
retriable_with_context(:sql) { increment_tries }
|
|
63
|
+
|
|
64
|
+
expect(@tries).to eq(1)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "returns nil when Kernel#retriable_with_context is called without a block" do
|
|
68
|
+
require_relative "../lib/retriable/core_ext/kernel"
|
|
69
|
+
Retriable.configure { |c| c.contexts[:sql] = { tries: 1 } }
|
|
70
|
+
|
|
71
|
+
expect(retriable_with_context(:sql)).to be_nil
|
|
72
|
+
expect(@tries).to eq(0)
|
|
73
|
+
end
|
|
41
74
|
end
|
|
42
75
|
|
|
43
76
|
context "#retriable" do
|
|
@@ -50,7 +83,6 @@ describe Retriable do
|
|
|
50
83
|
|
|
51
84
|
it "raises a LocalJumpError if not given a block" do
|
|
52
85
|
expect { described_class.retriable }.to raise_error(LocalJumpError)
|
|
53
|
-
expect { described_class.retriable(timeout: 2) }.to raise_error(LocalJumpError)
|
|
54
86
|
end
|
|
55
87
|
|
|
56
88
|
it "stops at first try if the block does not raise an exception" do
|
|
@@ -168,8 +200,8 @@ describe Retriable do
|
|
|
168
200
|
end.to raise_error(ArgumentError, /tries/)
|
|
169
201
|
end
|
|
170
202
|
|
|
171
|
-
it "
|
|
172
|
-
expect { described_class.retriable(timeout: 1) {
|
|
203
|
+
it "rejects timeout as an unknown option" do
|
|
204
|
+
expect { described_class.retriable(timeout: 1) { :noop } }.to raise_error(ArgumentError, /not a valid option/)
|
|
173
205
|
end
|
|
174
206
|
|
|
175
207
|
it "applies a randomized exponential backoff to each try" do
|
|
@@ -215,6 +247,187 @@ describe Retriable do
|
|
|
215
247
|
end
|
|
216
248
|
end
|
|
217
249
|
|
|
250
|
+
it "does not call on_retry when explicitly set to nil" do
|
|
251
|
+
callback_called = false
|
|
252
|
+
original_on_retry = described_class.config.on_retry
|
|
253
|
+
|
|
254
|
+
begin
|
|
255
|
+
described_class.configure do |c|
|
|
256
|
+
c.on_retry = proc { |_exception, _try, _elapsed_time, _next_interval| callback_called = true }
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
expect do
|
|
260
|
+
described_class.retriable(on_retry: nil, tries: 3) { increment_tries_with_exception }
|
|
261
|
+
end.to raise_error(StandardError)
|
|
262
|
+
|
|
263
|
+
expect(@tries).to eq(3)
|
|
264
|
+
expect(callback_called).to be(false)
|
|
265
|
+
ensure
|
|
266
|
+
described_class.configure do |c|
|
|
267
|
+
c.on_retry = original_on_retry
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
it "calls on_give_up with max elapsed time details before re-raising" do
|
|
273
|
+
described_class.configure { |c| c.sleep_disabled = false }
|
|
274
|
+
give_up_calls = []
|
|
275
|
+
on_give_up = proc do |exception, try, elapsed_time, next_interval, reason|
|
|
276
|
+
give_up_calls << [exception, try, elapsed_time, next_interval, reason]
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
expect do
|
|
280
|
+
described_class.retriable(
|
|
281
|
+
intervals: [1.0, 1.0],
|
|
282
|
+
max_elapsed_time: 0.5,
|
|
283
|
+
on_give_up: on_give_up,
|
|
284
|
+
) do
|
|
285
|
+
increment_tries_with_exception
|
|
286
|
+
end
|
|
287
|
+
end.to raise_error(StandardError)
|
|
288
|
+
|
|
289
|
+
exception, try, elapsed_time, next_interval, reason = give_up_calls.fetch(0)
|
|
290
|
+
expect(give_up_calls.size).to eq(1)
|
|
291
|
+
expect(exception).to be_a(StandardError)
|
|
292
|
+
expect(exception.message).to eq("StandardError occurred")
|
|
293
|
+
expect(try).to eq(1)
|
|
294
|
+
expect(elapsed_time).to be >= 0
|
|
295
|
+
expect(next_interval).to eq(1.0)
|
|
296
|
+
expect(reason).to eq(:max_elapsed_time)
|
|
297
|
+
expect(@tries).to eq(1)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
it "calls on_give_up with tries exhausted details before re-raising" do
|
|
301
|
+
give_up_calls = []
|
|
302
|
+
on_give_up = proc do |exception, try, elapsed_time, next_interval, reason|
|
|
303
|
+
give_up_calls << [exception, try, elapsed_time, next_interval, reason]
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
expect do
|
|
307
|
+
described_class.retriable(tries: 2, on_give_up: on_give_up) { increment_tries_with_exception }
|
|
308
|
+
end.to raise_error(StandardError)
|
|
309
|
+
|
|
310
|
+
exception, try, elapsed_time, next_interval, reason = give_up_calls.fetch(0)
|
|
311
|
+
expect(give_up_calls.size).to eq(1)
|
|
312
|
+
expect(exception).to be_a(StandardError)
|
|
313
|
+
expect(exception.message).to eq("StandardError occurred")
|
|
314
|
+
expect(try).to eq(2)
|
|
315
|
+
expect(elapsed_time).to be >= 0
|
|
316
|
+
expect(next_interval).to be_nil
|
|
317
|
+
expect(reason).to eq(:tries_exhausted)
|
|
318
|
+
expect(@tries).to eq(2)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
it "does not call on_give_up when the block eventually succeeds" do
|
|
322
|
+
callback_called = false
|
|
323
|
+
|
|
324
|
+
described_class.retriable(tries: 3, on_give_up: proc { callback_called = true }) do
|
|
325
|
+
increment_tries
|
|
326
|
+
raise StandardError if @tries < 2
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
expect(callback_called).to be(false)
|
|
330
|
+
expect(@tries).to eq(2)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
it "does not call on_give_up for non-retriable exception types" do
|
|
334
|
+
callback_called = false
|
|
335
|
+
|
|
336
|
+
expect do
|
|
337
|
+
described_class.retriable(on_give_up: proc { callback_called = true }) do
|
|
338
|
+
increment_tries_with_exception(NonStandardError)
|
|
339
|
+
end
|
|
340
|
+
end.to raise_error(NonStandardError)
|
|
341
|
+
|
|
342
|
+
expect(callback_called).to be(false)
|
|
343
|
+
expect(@tries).to eq(1)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
it "does not call on_give_up when retry_if rejects the exception" do
|
|
347
|
+
callback_called = false
|
|
348
|
+
|
|
349
|
+
expect do
|
|
350
|
+
described_class.retriable(
|
|
351
|
+
tries: 3,
|
|
352
|
+
retry_if: ->(_exception) { false },
|
|
353
|
+
on_give_up: proc { callback_called = true },
|
|
354
|
+
) do
|
|
355
|
+
increment_tries_with_exception
|
|
356
|
+
end
|
|
357
|
+
end.to raise_error(StandardError)
|
|
358
|
+
|
|
359
|
+
expect(callback_called).to be(false)
|
|
360
|
+
expect(@tries).to eq(1)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
it "does not call on_give_up when explicitly set to false" do
|
|
364
|
+
callback_called = false
|
|
365
|
+
original_on_give_up = described_class.config.on_give_up
|
|
366
|
+
|
|
367
|
+
begin
|
|
368
|
+
described_class.configure do |c|
|
|
369
|
+
c.on_give_up = proc { callback_called = true }
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
expect do
|
|
373
|
+
described_class.retriable(on_give_up: false, tries: 1) { increment_tries_with_exception }
|
|
374
|
+
end.to raise_error(StandardError)
|
|
375
|
+
|
|
376
|
+
expect(callback_called).to be(false)
|
|
377
|
+
ensure
|
|
378
|
+
described_class.configure do |c|
|
|
379
|
+
c.on_give_up = original_on_give_up
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
it "does not call on_give_up when explicitly set to nil" do
|
|
385
|
+
callback_called = false
|
|
386
|
+
original_on_give_up = described_class.config.on_give_up
|
|
387
|
+
|
|
388
|
+
begin
|
|
389
|
+
described_class.configure do |c|
|
|
390
|
+
c.on_give_up = proc { callback_called = true }
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
expect do
|
|
394
|
+
described_class.retriable(on_give_up: nil, tries: 1) { increment_tries_with_exception }
|
|
395
|
+
end.to raise_error(StandardError)
|
|
396
|
+
|
|
397
|
+
expect(callback_called).to be(false)
|
|
398
|
+
ensure
|
|
399
|
+
described_class.configure do |c|
|
|
400
|
+
c.on_give_up = original_on_give_up
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
it "calls on_retry before on_give_up when giving up" do
|
|
406
|
+
events = []
|
|
407
|
+
|
|
408
|
+
expect do
|
|
409
|
+
described_class.retriable(
|
|
410
|
+
tries: 1,
|
|
411
|
+
on_retry: proc { events << :on_retry },
|
|
412
|
+
on_give_up: proc { events << :on_give_up },
|
|
413
|
+
) do
|
|
414
|
+
increment_tries_with_exception
|
|
415
|
+
end
|
|
416
|
+
end.to raise_error(StandardError)
|
|
417
|
+
|
|
418
|
+
expect(events).to eq(%i[on_retry on_give_up])
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
it "propagates exceptions raised inside on_give_up, replacing the original exception" do
|
|
422
|
+
handler = proc { raise "handler exploded" }
|
|
423
|
+
|
|
424
|
+
expect do
|
|
425
|
+
described_class.retriable(tries: 1, on_give_up: handler) { increment_tries_with_exception }
|
|
426
|
+
end.to raise_error(RuntimeError, "handler exploded")
|
|
427
|
+
|
|
428
|
+
expect(@tries).to eq(1)
|
|
429
|
+
end
|
|
430
|
+
|
|
218
431
|
context "with rand_factor 0.0 and an on_retry handler" do
|
|
219
432
|
let(:tries) { 6 }
|
|
220
433
|
let(:no_rand_timetable) { { 1 => 0.5, 2 => 0.75, 3 => 1.125 } }
|
|
@@ -294,6 +507,19 @@ describe Retriable do
|
|
|
294
507
|
end
|
|
295
508
|
end
|
|
296
509
|
|
|
510
|
+
context "with a Set :on parameter" do
|
|
511
|
+
it "retries each exception class in the Set" do
|
|
512
|
+
described_class.retriable(on: Set[StandardError, NonStandardError]) do
|
|
513
|
+
increment_tries
|
|
514
|
+
|
|
515
|
+
raise StandardError if @tries == 1
|
|
516
|
+
raise NonStandardError if @tries == 2
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
expect(@tries).to eq(3)
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
|
|
297
523
|
context "with a hash :on parameter" do
|
|
298
524
|
let(:on_hash) { { NonStandardError => /NonStandardError occurred/ } }
|
|
299
525
|
|
|
@@ -326,6 +552,20 @@ describe Retriable do
|
|
|
326
552
|
expect(@tries).to eq(1)
|
|
327
553
|
end
|
|
328
554
|
|
|
555
|
+
it "does not call on_give_up when exception class matches but message does not" do
|
|
556
|
+
callback_called = false
|
|
557
|
+
|
|
558
|
+
expect do
|
|
559
|
+
described_class.retriable(on: on_hash, on_give_up: proc { callback_called = true }) do
|
|
560
|
+
increment_tries
|
|
561
|
+
raise SecondNonStandardError, "not a match"
|
|
562
|
+
end
|
|
563
|
+
end.to raise_error(SecondNonStandardError, /not a match/)
|
|
564
|
+
|
|
565
|
+
expect(callback_called).to be(false)
|
|
566
|
+
expect(@tries).to eq(1)
|
|
567
|
+
end
|
|
568
|
+
|
|
329
569
|
it "successfully retries when the values are arrays of exception message patterns" do
|
|
330
570
|
exceptions = []
|
|
331
571
|
handler = ->(exception, try, _elapsed_time, _next_interval) { exceptions[try] = exception }
|
|
@@ -656,17 +896,15 @@ describe Retriable do
|
|
|
656
896
|
end
|
|
657
897
|
|
|
658
898
|
it "treats non-hash configured contexts as empty when override contexts are hash" do
|
|
659
|
-
|
|
660
|
-
described_class.configure { |c| c.contexts = nil }
|
|
661
|
-
|
|
662
|
-
described_class.with_override(contexts: { api: { tries: 1 } }) do
|
|
663
|
-
described_class.with_context(:api) { increment_tries }
|
|
664
|
-
end
|
|
899
|
+
described_class.configure { |c| c.contexts = nil }
|
|
665
900
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
described_class.configure { |c| c.contexts = {} }
|
|
901
|
+
described_class.with_override(contexts: { api: { tries: 1 } }) do
|
|
902
|
+
described_class.with_context(:api) { increment_tries }
|
|
669
903
|
end
|
|
904
|
+
|
|
905
|
+
expect(@tries).to eq(1)
|
|
906
|
+
ensure
|
|
907
|
+
described_class.configure { |c| c.contexts = {} }
|
|
670
908
|
end
|
|
671
909
|
|
|
672
910
|
it "ignores nil override contexts values in with_context" do
|
|
@@ -936,6 +1174,31 @@ describe Retriable do
|
|
|
936
1174
|
|
|
937
1175
|
expect(other_thread_tries).to eq(3)
|
|
938
1176
|
end
|
|
1177
|
+
|
|
1178
|
+
it "applies overridden on_give_up handlers" do
|
|
1179
|
+
callback_called = false
|
|
1180
|
+
|
|
1181
|
+
expect do
|
|
1182
|
+
described_class.with_override(on_give_up: proc { callback_called = true }) do
|
|
1183
|
+
described_class.retriable(tries: 1) { increment_tries_with_exception }
|
|
1184
|
+
end
|
|
1185
|
+
end.to raise_error(StandardError)
|
|
1186
|
+
|
|
1187
|
+
expect(callback_called).to be(true)
|
|
1188
|
+
end
|
|
1189
|
+
|
|
1190
|
+
it "applies on_give_up handlers configured via per-context overrides" do
|
|
1191
|
+
received_reason = nil
|
|
1192
|
+
handler = proc { |_e, _try, _elapsed, _interval, reason| received_reason = reason }
|
|
1193
|
+
|
|
1194
|
+
expect do
|
|
1195
|
+
described_class.with_override(contexts: { api: { tries: 1, on_give_up: handler } }) do
|
|
1196
|
+
described_class.with_context(:api) { increment_tries_with_exception }
|
|
1197
|
+
end
|
|
1198
|
+
end.to raise_error(StandardError)
|
|
1199
|
+
|
|
1200
|
+
expect(received_reason).to eq(:tries_exhausted)
|
|
1201
|
+
end
|
|
939
1202
|
end
|
|
940
1203
|
|
|
941
1204
|
context "#with_context" do
|
|
@@ -994,5 +1257,17 @@ describe Retriable do
|
|
|
994
1257
|
described_class.with_context(:broken) { increment_tries }
|
|
995
1258
|
expect(@tries).to eq(1)
|
|
996
1259
|
end
|
|
1260
|
+
|
|
1261
|
+
it "invokes on_give_up configured on a context" do
|
|
1262
|
+
callback_called = false
|
|
1263
|
+
described_class.configure do |c|
|
|
1264
|
+
c.contexts[:flaky] = { tries: 1, on_give_up: proc { callback_called = true } }
|
|
1265
|
+
end
|
|
1266
|
+
|
|
1267
|
+
expect { described_class.with_context(:flaky) { increment_tries_with_exception } }
|
|
1268
|
+
.to raise_error(StandardError)
|
|
1269
|
+
|
|
1270
|
+
expect(callback_called).to be(true)
|
|
1271
|
+
end
|
|
997
1272
|
end
|
|
998
1273
|
end
|
metadata
CHANGED
|
@@ -1,56 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: retriable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 4.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jack Chu
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
12
|
-
- !ruby/object:Gem::Dependency
|
|
13
|
-
name: bundler
|
|
14
|
-
requirement: !ruby/object:Gem::Requirement
|
|
15
|
-
requirements:
|
|
16
|
-
- - ">="
|
|
17
|
-
- !ruby/object:Gem::Version
|
|
18
|
-
version: '0'
|
|
19
|
-
type: :development
|
|
20
|
-
prerelease: false
|
|
21
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
-
requirements:
|
|
23
|
-
- - ">="
|
|
24
|
-
- !ruby/object:Gem::Version
|
|
25
|
-
version: '0'
|
|
26
|
-
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: rspec
|
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
|
29
|
-
requirements:
|
|
30
|
-
- - "~>"
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '3'
|
|
33
|
-
type: :development
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - "~>"
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '3'
|
|
40
|
-
- !ruby/object:Gem::Dependency
|
|
41
|
-
name: listen
|
|
42
|
-
requirement: !ruby/object:Gem::Requirement
|
|
43
|
-
requirements:
|
|
44
|
-
- - "~>"
|
|
45
|
-
- !ruby/object:Gem::Version
|
|
46
|
-
version: '3.1'
|
|
47
|
-
type: :development
|
|
48
|
-
prerelease: false
|
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
-
requirements:
|
|
51
|
-
- - "~>"
|
|
52
|
-
- !ruby/object:Gem::Version
|
|
53
|
-
version: '3.1'
|
|
11
|
+
dependencies: []
|
|
54
12
|
description: Retriable is a simple DSL to retry failed code blocks with randomized
|
|
55
13
|
exponential backoff. This is especially useful when interacting with external APIs/services
|
|
56
14
|
or file system calls.
|
|
@@ -91,7 +49,8 @@ files:
|
|
|
91
49
|
homepage: https://github.com/kamui/retriable
|
|
92
50
|
licenses:
|
|
93
51
|
- MIT
|
|
94
|
-
metadata:
|
|
52
|
+
metadata:
|
|
53
|
+
rubygems_mfa_required: 'true'
|
|
95
54
|
rdoc_options: []
|
|
96
55
|
require_paths:
|
|
97
56
|
- lib
|
|
@@ -99,7 +58,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
99
58
|
requirements:
|
|
100
59
|
- - ">="
|
|
101
60
|
- !ruby/object:Gem::Version
|
|
102
|
-
version:
|
|
61
|
+
version: '3.2'
|
|
103
62
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
104
63
|
requirements:
|
|
105
64
|
- - ">="
|
|
@@ -110,9 +69,4 @@ rubygems_version: 4.0.3
|
|
|
110
69
|
specification_version: 4
|
|
111
70
|
summary: Retriable is a simple DSL to retry failed code blocks with randomized exponential
|
|
112
71
|
backoff
|
|
113
|
-
test_files:
|
|
114
|
-
- spec/config_spec.rb
|
|
115
|
-
- spec/exponential_backoff_spec.rb
|
|
116
|
-
- spec/retriable_spec.rb
|
|
117
|
-
- spec/spec_helper.rb
|
|
118
|
-
- spec/support/exceptions.rb
|
|
72
|
+
test_files: []
|