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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 25948e6545b8d98b0c7b08416bdf23b017ced352eb2a4eb1f2e9fdac45912031
4
- data.tar.gz: 4a92afb17f79a4ad173b046158ca634b0728fc6ccceedc1b7789c9106342b513
3
+ metadata.gz: 392e6bc1b69a4c85c286f5df1b90c5991243cd7290cd3b41c028f253ad16dae4
4
+ data.tar.gz: f078e585b65160045cc71e4a9f8746b7b480645cf841ff13a7bbbec78fc9ffeb
5
5
  SHA512:
6
- metadata.gz: e43731b305a64c806bf57c13d78da410412d8ea4adaec9f255bd434b2d190ac53b6fac29172b72ed6bcc7329a3d5378c4075d72f6dede791f1e0bd16f400e58e
7
- data.tar.gz: 4259468487738b9ae5bcc817ac992af3341f15bcfe604ab0a61ddf1f8793b444775aa57c06ce5f95561e179186e7360e86a877afd02f3b346b68377ffb3ecaf9
6
+ metadata.gz: 01dc250d04fbc71c89d6af526ab061ec4439d382ec50872776859415df9bad2d1d4c8f2e0cfb53fc263343d72871ce691df5840721d33caf0ccaacee03bf9f6e
7
+ data.tar.gz: f1ae42a3015541313322371fd297f5f677d1a974747b48a524c25e1f676fc7adeb8d309975851198c69632fc124a34e4db5488486783a40beb8758ef12cf6c01
@@ -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
- config_file: .rubocop.yml
2
+ enabled: false
data/.rubocop.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  AllCops:
2
2
  NewCops: enable
3
- TargetRubyVersion: 2.3
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
@@ -10,7 +10,8 @@ group :test do
10
10
  end
11
11
 
12
12
  group :development do
13
- gem "rubocop"
13
+ gem "listen", "~> 3.1"
14
+ gem "rubocop", "~> 1.86"
14
15
  end
15
16
 
16
17
  group :development, :test do
data/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # Retriable
2
2
 
3
3
  ![Build Status](https://github.com/kamui/retriable/actions/workflows/main.yml/badge.svg)
4
- [![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](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 2.3.0+
35
+ Ruby 3.2+
35
36
 
36
- 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.
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 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.
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 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.
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', '~> 3.8'
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
- | **`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`.
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`. Passing `nil` does not work for this purpose because per-call options are merged over configured defaults; `false` is the explicit "disabled" sentinel.
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,
@@ -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.each_with_object({}) do |key, hash|
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 = {}, &block)
7
- Retriable.retriable(opts, &block)
6
+ def retriable(opts = {}, &)
7
+ Retriable.retriable(opts, &)
8
8
  end
9
9
 
10
- def retriable_with_context(context_key, opts = {}, &block)
11
- Retriable.with_context(context_key, opts, &block)
10
+ def retriable_with_context(context_key, opts = {}, &)
11
+ Retriable.with_context(context_key, opts, &)
12
12
  end
13
13
  end
@@ -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
- when Hash
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
- when Array
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
- # Ruby 2.3 does not support Enumerable#all?(pattern).
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}"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Retriable
4
- VERSION = "3.8.0"
4
+ VERSION = "4.0.0"
5
5
  end