retriable 3.5.0 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +38 -9
- data/.hound.yml +1 -1
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +72 -0
- data/Gemfile +4 -1
- data/README.md +187 -110
- data/docs/testing.md +212 -0
- data/lib/retriable/config.rb +56 -4
- data/lib/retriable/core_ext/kernel.rb +4 -4
- data/lib/retriable/exponential_backoff.rb +31 -5
- data/lib/retriable/validation.rb +95 -0
- data/lib/retriable/version.rb +1 -1
- data/lib/retriable.rb +116 -55
- data/retriable.gemspec +2 -7
- data/sig/retriable.rbs +29 -1
- data/spec/config_spec.rb +128 -4
- data/spec/exponential_backoff_spec.rb +45 -26
- data/spec/retriable_spec.rb +713 -85
- data/spec/spec_helper.rb +3 -1
- metadata +8 -52
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5961397b426eeaf2d2df8631f0a5dfe05adf52d5b9070ddbdf7b7825fd88f144
|
|
4
|
+
data.tar.gz: c982809ef09eb85ddedfcee95be53741846f6e4438af27e403c9ad4b020214c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 30d8bfeeff407ab15fa9be7c2af2993142dce86d8406318e220afdb111c728b0c8c7cd2d6b105312ed9623074a5b20c88a9c19a6c33c3169d673d2276e0b52c4
|
|
7
|
+
data.tar.gz: f333fbf26eff94629f85f5d58688245025be3f39830bcad59da7f42ab52ad1506b155c03e1d96020a728b489ed016cb8ee1fcd5cde0a7a0ce3cfe59ad078c4d8
|
data/.github/workflows/main.yml
CHANGED
|
@@ -14,26 +14,22 @@ 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",
|
|
32
30
|
"4.0",
|
|
33
31
|
jruby,
|
|
34
32
|
]
|
|
35
|
-
env:
|
|
36
|
-
CC_TEST_REPORTER_ID: 20a1139ef1830b4f813a10a03d90e8aa179b5226f75e75c5a949b25756ebf558
|
|
37
33
|
|
|
38
34
|
steps:
|
|
39
35
|
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
|
@@ -50,3 +46,36 @@ jobs:
|
|
|
50
46
|
|
|
51
47
|
- name: Run rspec
|
|
52
48
|
run: bundle exec rspec
|
|
49
|
+
|
|
50
|
+
lint:
|
|
51
|
+
runs-on: ubuntu-24.04
|
|
52
|
+
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
55
|
+
|
|
56
|
+
- name: Setup ruby
|
|
57
|
+
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
|
|
58
|
+
with:
|
|
59
|
+
ruby-version: "3.3"
|
|
60
|
+
bundler-cache: true
|
|
61
|
+
|
|
62
|
+
- name: Run rubocop
|
|
63
|
+
run: bundle exec rubocop
|
|
64
|
+
|
|
65
|
+
- name: Validate RBS
|
|
66
|
+
run: bundle exec rbs -I sig validate
|
|
67
|
+
|
|
68
|
+
audit:
|
|
69
|
+
runs-on: ubuntu-24.04
|
|
70
|
+
|
|
71
|
+
steps:
|
|
72
|
+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
73
|
+
|
|
74
|
+
- name: Setup ruby
|
|
75
|
+
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
|
|
76
|
+
with:
|
|
77
|
+
ruby-version: "3.3"
|
|
78
|
+
bundler-cache: true
|
|
79
|
+
|
|
80
|
+
- name: Run bundler-audit
|
|
81
|
+
run: bundle exec bundle-audit check --update
|
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,77 @@
|
|
|
1
1
|
# HEAD
|
|
2
2
|
|
|
3
|
+
## 4.1.1
|
|
4
|
+
|
|
5
|
+
### Bug fixes
|
|
6
|
+
|
|
7
|
+
- `retry_if`, `on_retry`, and `on_give_up` are now validated to be callable
|
|
8
|
+
(respond to `#call`) or falsy. A non-callable truthy value raises
|
|
9
|
+
`ArgumentError` at configuration time instead of a later `NoMethodError` on a
|
|
10
|
+
retry path. ([#140](https://github.com/kamui/retriable/pull/140))
|
|
11
|
+
|
|
12
|
+
### Internal
|
|
13
|
+
|
|
14
|
+
- Add RBS type signatures for the public API (`Retriable.configure`, `config`,
|
|
15
|
+
`retriable`, `with_override`, `with_context`, and `Retriable::Config`) and
|
|
16
|
+
validate them in CI with `rbs validate`.
|
|
17
|
+
([#142](https://github.com/kamui/retriable/pull/142))
|
|
18
|
+
- Enforce a minimum test coverage floor and add a `bundler-audit` dependency
|
|
19
|
+
audit job to CI. ([#143](https://github.com/kamui/retriable/pull/143))
|
|
20
|
+
- Remove an unused `CC_TEST_REPORTER_ID` from the CI workflow.
|
|
21
|
+
([#141](https://github.com/kamui/retriable/pull/141))
|
|
22
|
+
|
|
23
|
+
## 4.1.0
|
|
24
|
+
|
|
25
|
+
### Bug fixes
|
|
26
|
+
|
|
27
|
+
- A per-call or `with_context` `tries:` now clears an inherited `intervals:` from
|
|
28
|
+
global config or a context, matching the documented precedence. Previously
|
|
29
|
+
`Retriable.retriable(tries: 1)` was silently ignored when `intervals` was
|
|
30
|
+
configured, running `intervals.size + 1` times. Passing both `intervals:` and
|
|
31
|
+
`tries:` in the same call still lets `intervals:` win.
|
|
32
|
+
|
|
33
|
+
## 4.0.0
|
|
34
|
+
|
|
35
|
+
**This is a major release with breaking changes. Please read carefully before upgrading.**
|
|
36
|
+
|
|
37
|
+
### Breaking changes
|
|
38
|
+
|
|
39
|
+
- 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.
|
|
40
|
+
- 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.
|
|
41
|
+
|
|
42
|
+
### Features
|
|
43
|
+
|
|
44
|
+
- 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.
|
|
45
|
+
- 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`.
|
|
46
|
+
|
|
47
|
+
### Internal
|
|
48
|
+
|
|
49
|
+
- Switched `Retriable.retriable`, `Retriable.with_context`, and the `Kernel` extension methods to Ruby 3.1+ anonymous block forwarding. No user-visible behavior change.
|
|
50
|
+
|
|
51
|
+
## 3.8.0
|
|
52
|
+
|
|
53
|
+
### Deprecations
|
|
54
|
+
|
|
55
|
+
- 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.
|
|
56
|
+
|
|
57
|
+
## 3.7.0
|
|
58
|
+
|
|
59
|
+
- 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!`.
|
|
60
|
+
|
|
61
|
+
## 3.6.1
|
|
62
|
+
|
|
63
|
+
- Fix: Validate the `on:` option before retrying. Previously, passing a non-`Exception` value such as `Object`, `Kernel`, or a plain `Module` (which appear in every `Exception`'s ancestor chain) would silently retry process-critical exceptions like `SystemExit` and `Interrupt`. The `on:` option now requires an `Exception` subclass, an array of them, or a hash whose keys are such classes and whose values are `nil`, a `Regexp`, or an array of `Regexp`s. Invalid shapes raise `ArgumentError` before the block runs.
|
|
64
|
+
- Fix: Validate `with_override(contexts:)` shape before applying overrides. `contexts` may be `nil` or a hash, and each per-context override must be a hash.
|
|
65
|
+
- Docs: Document that `on_retry: false` disables a callback set in `Retriable.configure` for a single call.
|
|
66
|
+
|
|
67
|
+
## 3.6.0
|
|
68
|
+
|
|
69
|
+
- Breaking: `Retriable.override` and `Retriable.reset_override` are removed and replaced by block-scoped `Retriable.with_override(opts) { ... }`. The new API requires a block, restores the previous override (or absence of override) when the block exits via `ensure`, and is thread-local — overrides set in one thread do not affect other threads, and child threads do not inherit them. Fibers within a thread still share the thread's active override. Nested `with_override` calls correctly restore the outer override on inner exit. See the README and `docs/testing.md` for migration and testing patterns. This replaces the override API introduced in 3.5.0.
|
|
70
|
+
|
|
71
|
+
## 3.5.1
|
|
72
|
+
|
|
73
|
+
- Fix: Validate retry timing and count options before use to reject invalid retry configurations. `tries` must now be a positive integer unless a custom `intervals` array is provided.
|
|
74
|
+
|
|
3
75
|
## 3.5.0
|
|
4
76
|
|
|
5
77
|
- Fix: Do not count skipped sleep intervals against `max_elapsed_time` when `sleep_disabled` is true.
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -1,19 +1,78 @@
|
|
|
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
|
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Requirements](#requirements)
|
|
10
|
+
- [Migration from 3.x to 4.0](#migration-from-3x-to-40)
|
|
11
|
+
- [Installation](#installation)
|
|
12
|
+
- [Usage](#usage)
|
|
13
|
+
- [Defaults](#defaults)
|
|
14
|
+
- [Options](#options)
|
|
15
|
+
- [Configuring Which Options to Retry With :on](#configuring-which-options-to-retry-with-on)
|
|
16
|
+
- [Advanced Retry Matching With :retry_if](#advanced-retry-matching-with-retry_if)
|
|
17
|
+
- [Configuration](#configuration)
|
|
18
|
+
- [Override](#override)
|
|
19
|
+
- [Example Usage](#example-usage)
|
|
20
|
+
- [Custom Interval Array](#custom-interval-array)
|
|
21
|
+
- [Unbounded Retries (Opt-in)](#unbounded-retries-opt-in)
|
|
22
|
+
- [Turn off Exponential Backoff](#turn-off-exponential-backoff)
|
|
23
|
+
- [Callbacks](#callbacks)
|
|
24
|
+
- [Disabling a Configured Callback Per Call](#disabling-a-configured-callback-per-call)
|
|
25
|
+
- [Ensure/Else](#ensureelse)
|
|
26
|
+
- [Contexts](#contexts)
|
|
27
|
+
- [Kernel Extension](#kernel-extension)
|
|
28
|
+
- [Testing](#testing)
|
|
29
|
+
- [Credits](#credits)
|
|
30
|
+
- [Development](#development)
|
|
31
|
+
- [Running Specs](#running-specs)
|
|
32
|
+
|
|
8
33
|
## Requirements
|
|
9
34
|
|
|
10
|
-
Ruby
|
|
35
|
+
Ruby 3.2+
|
|
36
|
+
|
|
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.
|
|
38
|
+
|
|
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.
|
|
40
|
+
|
|
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:
|
|
11
56
|
|
|
12
|
-
|
|
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`.
|
|
13
58
|
|
|
14
|
-
|
|
59
|
+
2. **Manage the timeout yourself inside the block** if no native option exists:
|
|
15
60
|
|
|
16
|
-
|
|
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`.
|
|
17
76
|
|
|
18
77
|
## Installation
|
|
19
78
|
|
|
@@ -32,7 +91,7 @@ require 'retriable'
|
|
|
32
91
|
In your Gemfile:
|
|
33
92
|
|
|
34
93
|
```ruby
|
|
35
|
-
gem 'retriable', '~>
|
|
94
|
+
gem 'retriable', '~> 4.0'
|
|
36
95
|
```
|
|
37
96
|
|
|
38
97
|
## Usage
|
|
@@ -79,27 +138,29 @@ The default interval table with 10 tries looks like this (in seconds, rounded to
|
|
|
79
138
|
|
|
80
139
|
Here are the available options, in some vague order of relevance to most common use patterns:
|
|
81
140
|
|
|
82
|
-
| Option | Default | Definition
|
|
83
|
-
| ---------------------- | ----------------- |
|
|
84
|
-
| **`tries`** | `3` | Number of attempts to make at running your code block (includes initial attempt).
|
|
85
|
-
| **`on`** | `[StandardError]` | Type of exceptions to retry. [Read more](#configuring-which-options-to-retry-with-on).
|
|
86
|
-
| **`retry_if`** | `nil` | Callable (for example a `Proc` or lambda) that receives the rescued exception and returns true/false to decide whether to retry. [Read more](#advanced-retry-matching-with-retry_if).
|
|
87
|
-
| **`on_retry`** | `nil` | `Proc` to call after each try is rescued. [Read more](#callbacks).
|
|
88
|
-
| **`
|
|
89
|
-
| **`
|
|
90
|
-
| **`
|
|
91
|
-
| **`
|
|
92
|
-
| **`
|
|
93
|
-
| **`
|
|
94
|
-
| **`
|
|
95
|
-
| **`
|
|
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`.
|
|
96
157
|
|
|
97
158
|
#### Configuring Which Options to Retry With :on
|
|
98
159
|
|
|
99
160
|
**`:on`** Can take the form:
|
|
100
161
|
|
|
101
162
|
- An `Exception` class (retry every exception of this type, including subclasses)
|
|
102
|
-
- 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)
|
|
103
164
|
- A `Hash` where the keys are `Exception` classes and the values are one of:
|
|
104
165
|
- `nil` (retry every exception of the key's type, including subclasses)
|
|
105
166
|
- A single `Regexp` pattern (retries exceptions ONLY if their `message` matches the pattern)
|
|
@@ -145,39 +206,61 @@ end
|
|
|
145
206
|
`#configure` sets defaults only. Per-call options passed to `Retriable.retriable` and
|
|
146
207
|
`Retriable.with_context` still take precedence.
|
|
147
208
|
|
|
209
|
+
When a higher-precedence layer sets `tries:` without `intervals:`, it clears any
|
|
210
|
+
`intervals:` inherited from a lower layer (so `retriable(tries: 1)` runs once even
|
|
211
|
+
if `intervals` was configured). Within a single call, passing `intervals:` still
|
|
212
|
+
overrides `tries:`.
|
|
213
|
+
|
|
148
214
|
### Override
|
|
149
215
|
|
|
150
|
-
|
|
151
|
-
`#
|
|
216
|
+
`#with_override` is a block-scoped API for forcing retry options that should
|
|
217
|
+
take precedence over both `#configure` defaults and per-call options. It is
|
|
218
|
+
primarily intended for tests — it lets a test force values like `tries: 1` or
|
|
219
|
+
`base_interval: 0` so the suite runs quickly and predictably, regardless of
|
|
220
|
+
the application's `#configure` defaults. In application code, prefer
|
|
221
|
+
`#configure` for app-level defaults and per-call options for caller-specific
|
|
222
|
+
values.
|
|
152
223
|
|
|
153
224
|
```ruby
|
|
154
|
-
Retriable.
|
|
225
|
+
Retriable.with_override(tries: 1, base_interval: 0) do
|
|
226
|
+
Retriable.retriable do
|
|
227
|
+
# code here...
|
|
228
|
+
end
|
|
229
|
+
end
|
|
155
230
|
```
|
|
156
231
|
|
|
157
|
-
|
|
232
|
+
Precedence inside the block:
|
|
158
233
|
|
|
159
234
|
```
|
|
160
|
-
|
|
235
|
+
with_override > local options > configure defaults
|
|
161
236
|
```
|
|
162
237
|
|
|
163
|
-
`#
|
|
164
|
-
|
|
165
|
-
|
|
238
|
+
`#with_override` requires a block and raises `ArgumentError` if called without
|
|
239
|
+
one. The override is active only while the block is executing, and is
|
|
240
|
+
automatically restored to its previous value when the block returns or raises.
|
|
241
|
+
Nested `#with_override` calls work as expected: the inner block temporarily
|
|
242
|
+
replaces the active override and the outer override is restored when the
|
|
243
|
+
inner block exits.
|
|
166
244
|
|
|
167
|
-
`#
|
|
168
|
-
|
|
245
|
+
`#with_override` is scoped to the **current thread**. The active override
|
|
246
|
+
does not affect any other thread, and child threads spawned inside the block
|
|
247
|
+
do not inherit it. This makes `#with_override` safe to use in parallel test
|
|
248
|
+
runners. Fibers running inside the same thread share the thread's active
|
|
249
|
+
override.
|
|
169
250
|
|
|
170
|
-
|
|
251
|
+
`#with_override` stores the provided options directly. Do not mutate the
|
|
252
|
+
options hash or nested values for the duration of the block.
|
|
171
253
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
```
|
|
254
|
+
For test-integration patterns (RSpec `around`, helper methods, Minitest, etc.),
|
|
255
|
+
see [docs/testing.md](docs/testing.md).
|
|
175
256
|
|
|
176
257
|
### Example Usage
|
|
177
258
|
|
|
178
259
|
This example will only retry on a `Timeout::Error`, retry 3 times and sleep for a full second before each try.
|
|
179
260
|
|
|
180
261
|
```ruby
|
|
262
|
+
require "timeout"
|
|
263
|
+
|
|
181
264
|
Retriable.retriable(on: Timeout::Error, tries: 3, base_interval: 1) do
|
|
182
265
|
# code here...
|
|
183
266
|
end
|
|
@@ -186,6 +269,8 @@ end
|
|
|
186
269
|
You can also specify multiple errors to retry on by passing an array of exceptions.
|
|
187
270
|
|
|
188
271
|
```ruby
|
|
272
|
+
require "timeout"
|
|
273
|
+
|
|
189
274
|
Retriable.retriable(on: [Timeout::Error, Errno::ECONNRESET]) do
|
|
190
275
|
# code here...
|
|
191
276
|
end
|
|
@@ -203,35 +288,40 @@ Retriable.retriable(on: {
|
|
|
203
288
|
end
|
|
204
289
|
```
|
|
205
290
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
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.
|
|
291
|
+
If you need millisecond units of time for the sleep interval:
|
|
209
292
|
|
|
210
293
|
```ruby
|
|
211
|
-
Retriable.retriable(
|
|
294
|
+
Retriable.retriable(base_interval: (200 / 1000.0)) do
|
|
212
295
|
# code here...
|
|
213
296
|
end
|
|
214
297
|
```
|
|
215
298
|
|
|
216
|
-
|
|
299
|
+
### Custom Interval Array
|
|
300
|
+
|
|
301
|
+
You can also bypass the built-in interval generation and provide your own array of intervals. Supplying your own intervals overrides the `tries`, `base_interval`, `max_interval`, `rand_factor`, and `multiplier` parameters.
|
|
217
302
|
|
|
218
303
|
```ruby
|
|
219
|
-
Retriable.retriable(
|
|
304
|
+
Retriable.retriable(intervals: [0.5, 1.0, 2.0, 2.5]) do
|
|
220
305
|
# code here...
|
|
221
306
|
end
|
|
222
307
|
```
|
|
223
308
|
|
|
224
|
-
|
|
309
|
+
This example makes 5 total attempts. If the first attempt fails, the 2nd attempt occurs 0.5 seconds later.
|
|
225
310
|
|
|
226
|
-
|
|
311
|
+
### Unbounded Retries (Opt-in)
|
|
312
|
+
|
|
313
|
+
You can opt in to unbounded retries with `tries: Float::INFINITY`. This is useful for long-running worker processes where retrying should continue indefinitely, but it must be used with care.
|
|
227
314
|
|
|
228
315
|
```ruby
|
|
229
|
-
Retriable.retriable(
|
|
316
|
+
Retriable.retriable(tries: Float::INFINITY, max_elapsed_time: 300) do
|
|
230
317
|
# code here...
|
|
231
318
|
end
|
|
232
319
|
```
|
|
233
320
|
|
|
234
|
-
|
|
321
|
+
When `tries: Float::INFINITY` is set:
|
|
322
|
+
|
|
323
|
+
- `max_elapsed_time` must be a finite number. Retriable raises `ArgumentError` if it is `nil` or `Float::INFINITY`. This is a safety bound that prevents accidentally unbounded loops.
|
|
324
|
+
- Custom `intervals:` cannot be combined with `Float::INFINITY` and raises `ArgumentError`. Use the exponential backoff settings (`base_interval`, `multiplier`, `max_interval`, `rand_factor`) instead.
|
|
235
325
|
|
|
236
326
|
### Turn off Exponential Backoff
|
|
237
327
|
|
|
@@ -277,6 +367,48 @@ Retriable.retriable(on_retry: do_this_on_each_retry) do
|
|
|
277
367
|
end
|
|
278
368
|
```
|
|
279
369
|
|
|
370
|
+
#### Disabling a Configured Callback Per Call
|
|
371
|
+
|
|
372
|
+
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`.
|
|
373
|
+
|
|
374
|
+
```ruby
|
|
375
|
+
Retriable.configure do |c|
|
|
376
|
+
c.on_retry = ->(exception, try, elapsed_time, next_interval) { log(...) }
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Most calls use the configured callback.
|
|
380
|
+
Retriable.retriable do
|
|
381
|
+
# ...
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# This specific call opts out of the configured callback.
|
|
385
|
+
Retriable.retriable(on_retry: false) do
|
|
386
|
+
# ...
|
|
387
|
+
end
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
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`.
|
|
391
|
+
|
|
392
|
+
```ruby
|
|
393
|
+
do_this_when_retries_stop = Proc.new do |exception, try, elapsed_time, next_interval, reason|
|
|
394
|
+
log "#{exception.class}: '#{exception.message}' - gave up after #{try} tries because #{reason}."
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
Retriable.retriable(on_give_up: do_this_when_retries_stop) do
|
|
398
|
+
# code here...
|
|
399
|
+
end
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
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.
|
|
403
|
+
|
|
404
|
+
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.
|
|
405
|
+
|
|
406
|
+
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".
|
|
407
|
+
|
|
408
|
+
`: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.
|
|
409
|
+
|
|
410
|
+
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.
|
|
411
|
+
|
|
280
412
|
### Ensure/Else
|
|
281
413
|
|
|
282
414
|
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:
|
|
@@ -304,7 +436,8 @@ Retriable.configure do |c|
|
|
|
304
436
|
c.contexts[:aws] = {
|
|
305
437
|
tries: 3,
|
|
306
438
|
base_interval: 5,
|
|
307
|
-
on_retry: Proc.new { puts 'Curse you, AWS!' }
|
|
439
|
+
on_retry: Proc.new { puts 'Curse you, AWS!' },
|
|
440
|
+
on_give_up: Proc.new { |_e, _try, _elapsed, _interval, reason| puts "Gave up on AWS: #{reason}" }
|
|
308
441
|
}
|
|
309
442
|
c.contexts[:mysql] = {
|
|
310
443
|
tries: 10,
|
|
@@ -367,72 +500,16 @@ retriable_with_context(:api) do
|
|
|
367
500
|
end
|
|
368
501
|
```
|
|
369
502
|
|
|
370
|
-
##
|
|
371
|
-
|
|
372
|
-
When you are running tests for your app it often takes a long time to retry blocks that fail. This is because Retriable will default to 3 tries with exponential backoff. Ideally your tests will run as quickly as possible.
|
|
503
|
+
## Testing
|
|
373
504
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
```ruby
|
|
379
|
-
# config/initializers/retriable.rb
|
|
380
|
-
Retriable.configure do |c|
|
|
381
|
-
c.tries = 3
|
|
382
|
-
c.base_interval = 0.5
|
|
383
|
-
c.rand_factor = 0.5
|
|
384
|
-
end
|
|
505
|
+
`Retriable.with_override` is designed to short-circuit retries in your test
|
|
506
|
+
suite so failing blocks do not slow tests down. The simplest pattern is an
|
|
507
|
+
RSpec `around(:each)` hook (or your test framework's equivalent) that wraps
|
|
508
|
+
every example in `with_override(tries: 1, base_interval: 0)`.
|
|
385
509
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
If you need to run a specific test with normal retry behavior, call `Retriable.reset_override` for that example and then reapply your test override afterward.
|
|
392
|
-
|
|
393
|
-
Alternately, if you are using RSpec, you could override the Retriable configuration in your `spec_helper`.
|
|
394
|
-
|
|
395
|
-
```ruby
|
|
396
|
-
# spec/spec_helper.rb
|
|
397
|
-
Retriable.override(tries: 1, base_interval: 0, rand_factor: 0)
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
If you have defined contexts for your configuration, top-level override values (such as `tries: 1`) already take precedence over context-specific values. However, if you need to override context-specific options (for example, clearing a context's `:intervals` array or changing its `:on` exception list), pass `:contexts` to `Retriable.override`:
|
|
401
|
-
|
|
402
|
-
For example assuming you have configured a `google_api` context:
|
|
403
|
-
|
|
404
|
-
```ruby
|
|
405
|
-
# config/initializers/retriable.rb
|
|
406
|
-
Retriable.configure do |c|
|
|
407
|
-
c.contexts[:google_api] = {
|
|
408
|
-
tries: 5,
|
|
409
|
-
base_interval: 3,
|
|
410
|
-
on: [
|
|
411
|
-
Net::ReadTimeout,
|
|
412
|
-
Signet::AuthorizationError,
|
|
413
|
-
Errno::ECONNRESET,
|
|
414
|
-
OpenSSL::SSL::SSLError
|
|
415
|
-
]
|
|
416
|
-
}
|
|
417
|
-
end
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
Then in your test environment, you can override both top-level defaults and per-context options:
|
|
421
|
-
|
|
422
|
-
```ruby
|
|
423
|
-
# Build context overrides from existing configured context keys
|
|
424
|
-
context_overrides = {}
|
|
425
|
-
Retriable.config.contexts.each_key do |key|
|
|
426
|
-
context_overrides[key] = { tries: 1, base_interval: 0 }
|
|
427
|
-
end
|
|
428
|
-
|
|
429
|
-
Retriable.override(
|
|
430
|
-
multiplier: 1.0,
|
|
431
|
-
rand_factor: 0.0,
|
|
432
|
-
base_interval: 0,
|
|
433
|
-
contexts: context_overrides,
|
|
434
|
-
)
|
|
435
|
-
```
|
|
510
|
+
For Rails integration, opting out of the override for specific tests, and
|
|
511
|
+
overriding configured contexts in tests, see
|
|
512
|
+
[docs/testing.md](docs/testing.md).
|
|
436
513
|
|
|
437
514
|
## Credits
|
|
438
515
|
|