retriable 3.8.0 → 5.0.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/AGENTS.md +15 -0
- data/CHANGELOG.md +159 -0
- data/Gemfile +6 -1
- data/README.md +131 -50
- data/benchmark/config_publication.rb +79 -0
- data/docs/adr/0001-copy-on-write-config-publication.md +124 -0
- data/docs/agents/domain.md +38 -0
- data/docs/agents/issue-tracker.md +45 -0
- data/docs/agents/triage-labels.md +17 -0
- data/docs/migration.md +84 -0
- data/lib/retriable/config.rb +123 -51
- data/lib/retriable/core_ext/kernel.rb +6 -4
- data/lib/retriable/exponential_backoff.rb +13 -5
- data/lib/retriable/validation.rb +11 -7
- data/lib/retriable/version.rb +1 -1
- data/lib/retriable.rb +155 -45
- data/retriable.gemspec +2 -7
- data/sig/retriable.rbs +29 -1
- data/spec/config_spec.rb +212 -97
- data/spec/retriable_spec.rb +666 -104
- data/spec/spec_helper.rb +3 -14
- metadata +14 -53
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7ec8418b3d0009ce1ddfd7aa839eab270ae479a2a9498660e9cc0da8fa2541c5
|
|
4
|
+
data.tar.gz: efccc4c3e6e29f5ba12426f1ec39503e9c59a9b606389a2c318606a205bb18b7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b7a1f5935366d2b243e508f5da6308ebdd403e61b4d4c9dc05f7f3018c3d93a0bacca1e9a2c9c84a0ebdf0fe685116cd4e3a76bf8dbafeaf6edb25f03a4b31fe
|
|
7
|
+
data.tar.gz: b5bdcef731117fc3c379b65a54e8acfecc9403e188089d269c86f721e6f7bf080f4c2670dc20f15ce09941f69e5ee117a493d593ec94c076c300c86eb02de841
|
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/AGENTS.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Retriable
|
|
2
|
+
|
|
3
|
+
## Agent skills
|
|
4
|
+
|
|
5
|
+
### Issue tracker
|
|
6
|
+
|
|
7
|
+
Issues live in GitHub Issues on `kamui/retriable`, via the `gh` CLI. See `docs/agents/issue-tracker.md`.
|
|
8
|
+
|
|
9
|
+
### Triage labels
|
|
10
|
+
|
|
11
|
+
The five canonical triage roles, using their default label strings. See `docs/agents/triage-labels.md`.
|
|
12
|
+
|
|
13
|
+
### Domain docs
|
|
14
|
+
|
|
15
|
+
Single-context: `CONTEXT.md` and `docs/adr/` at the repo root. See `docs/agents/domain.md`.
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,164 @@
|
|
|
1
1
|
# HEAD
|
|
2
2
|
|
|
3
|
+
## 5.0.1
|
|
4
|
+
|
|
5
|
+
### Bug fixes
|
|
6
|
+
|
|
7
|
+
- `Retriable.retriable` now propagates `StopIteration` instead of reporting it as
|
|
8
|
+
retry exhaustion. The retry loop used `Kernel#loop`, which rescues
|
|
9
|
+
`StopIteration`, so a block or callback (`on_retry`, `on_give_up`) raising it
|
|
10
|
+
was converted into a `Retriable::ExhaustedError` even when retries remained.
|
|
11
|
+
The loop no longer goes through `Kernel#loop`, so `StopIteration` reaches the
|
|
12
|
+
caller as any other exception would.
|
|
13
|
+
([#155](https://github.com/kamui/retriable/pull/155))
|
|
14
|
+
- A reused configuration is now validated once, when it is picked up for an
|
|
15
|
+
attempt, instead of being validated again immediately before every use. The
|
|
16
|
+
validation guarantee is unchanged; this only removes the duplicate work on
|
|
17
|
+
each retry.
|
|
18
|
+
([#156](https://github.com/kamui/retriable/pull/156))
|
|
19
|
+
|
|
20
|
+
## 5.0.0
|
|
21
|
+
|
|
22
|
+
**This is a major release with a breaking change. Please read carefully before
|
|
23
|
+
upgrading.**
|
|
24
|
+
|
|
25
|
+
### Breaking changes
|
|
26
|
+
|
|
27
|
+
Retriable 5.0 makes the thread-safety change below. Because it changes direct
|
|
28
|
+
config mutation, it is released as a major version.
|
|
29
|
+
([#151](https://github.com/kamui/retriable/pull/151))
|
|
30
|
+
|
|
31
|
+
The published config is frozen. Mutating `Retriable.config` directly
|
|
32
|
+
raises `FrozenError`:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
Retriable.config.sleep_disabled = true # => FrozenError
|
|
36
|
+
Retriable.config.contexts[:api] = {} # => FrozenError
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Go through `configure` instead:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
Retriable.configure { |c| c.sleep_disabled = true }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Check your test setup first: `Retriable.config.sleep_disabled = true` in a
|
|
46
|
+
`spec_helper` or `rails_helper` is the likeliest place this bites. Reading
|
|
47
|
+
`Retriable.config` is unaffected.
|
|
48
|
+
|
|
49
|
+
A published config is shared by every thread reading it, so an in-place write
|
|
50
|
+
was a data race that could corrupt another thread's retry behavior with no sign
|
|
51
|
+
anything had gone wrong. Freezing it is what makes the copy-on-write guarantee
|
|
52
|
+
below hold in practice rather than only on paper.
|
|
53
|
+
|
|
54
|
+
The full upgrade guide lives in [docs/migration.md](docs/migration.md#4x-to-5x).
|
|
55
|
+
|
|
56
|
+
### Bug fixes
|
|
57
|
+
|
|
58
|
+
- `Retriable.configure` is now thread-safe. Configuration is copy-on-write: the
|
|
59
|
+
block mutates a duplicate, which is published atomically only if the block
|
|
60
|
+
returns without raising. Concurrent readers therefore see either the whole
|
|
61
|
+
previous config or the whole new one, never a half-applied mix, and a raising
|
|
62
|
+
block leaves the existing config in place. Nested mutable values (`on`,
|
|
63
|
+
`intervals`, `contexts`, and anything inside them) are deep-copied, so a
|
|
64
|
+
mutation inside a `configure` block can no longer reach back into the config
|
|
65
|
+
other threads are reading. `Retriable.with_context` now resolves the context
|
|
66
|
+
lookup and the global options against a single snapshot, closing a race where
|
|
67
|
+
a concurrent `configure` could drop a context's retry options. Nested
|
|
68
|
+
`configure` calls remain supported: they share the outer working copy and
|
|
69
|
+
publish once when the outermost block returns. See **Breaking changes** above
|
|
70
|
+
for the direct-mutation behavior change this required.
|
|
71
|
+
([#151](https://github.com/kamui/retriable/pull/151))
|
|
72
|
+
|
|
73
|
+
## 4.2.0
|
|
74
|
+
|
|
75
|
+
### Bug fixes
|
|
76
|
+
|
|
77
|
+
- The `Kernel` extension methods (`require "retriable/core_ext/kernel"`) are now
|
|
78
|
+
private, matching idiomatic `Kernel` helpers like `puts` and `rand`.
|
|
79
|
+
Previously `retriable` and `retriable_with_context` were public instance
|
|
80
|
+
methods, so they leaked onto every object's public API and could be invoked
|
|
81
|
+
with an explicit receiver (e.g. `"foo".retriable { ... }`). They remain
|
|
82
|
+
callable in the documented receiver-less form.
|
|
83
|
+
([#146](https://github.com/kamui/retriable/pull/146))
|
|
84
|
+
- `Retriable.with_context` (and `Kernel#retriable_with_context`) now raises
|
|
85
|
+
`ArgumentError` when called without a block, matching `with_override`.
|
|
86
|
+
Previously a missing block was silently ignored: the call returned `nil` and
|
|
87
|
+
the intended block never ran, hiding a caller bug. Behavior change: code that
|
|
88
|
+
relied on the silent no-op will now raise.
|
|
89
|
+
- `Config#validate!` now validates the structure of each entry in `contexts`,
|
|
90
|
+
so configured contexts are checked on every `Retriable.retriable`/
|
|
91
|
+
`with_context` call rather than only when a given context is first used. A
|
|
92
|
+
context whose options contain an unknown key (including a nested `contexts`
|
|
93
|
+
key) now raises `ArgumentError, "<key> is not a valid option"`, matching the
|
|
94
|
+
`with_override` path. Non-Hash `contexts` and non-Hash per-context values
|
|
95
|
+
remain leniently treated as empty options (no behavior change). Option
|
|
96
|
+
_values_ are still validated lazily at retry time, unchanged.
|
|
97
|
+
|
|
98
|
+
### Docs
|
|
99
|
+
|
|
100
|
+
- Document that `on_retry` receives `next_interval: nil` on the final rescued
|
|
101
|
+
attempt, when Retriable is about to give up because `tries` are exhausted.
|
|
102
|
+
`on_retry` still fires before `on_give_up` (unchanged behavior); the `nil`
|
|
103
|
+
contract is now called out in the `on_retry` documentation so handlers guard
|
|
104
|
+
arithmetic or logging on `next_interval`.
|
|
105
|
+
|
|
106
|
+
### Performance
|
|
107
|
+
|
|
108
|
+
- `Config#initialize` no longer allocates a throwaway `ExponentialBackoff` (and
|
|
109
|
+
runs its redundant `validate!`) just to read default values. Defaults now live
|
|
110
|
+
in a frozen `ExponentialBackoff::DEFAULTS` constant, removing an allocation and
|
|
111
|
+
redundant validation from the `retriable` hot path.
|
|
112
|
+
([#149](https://github.com/kamui/retriable/pull/149))
|
|
113
|
+
|
|
114
|
+
## 4.1.1
|
|
115
|
+
|
|
116
|
+
### Bug fixes
|
|
117
|
+
|
|
118
|
+
- `retry_if`, `on_retry`, and `on_give_up` are now validated to be callable
|
|
119
|
+
(respond to `#call`) or falsy. A non-callable truthy value raises
|
|
120
|
+
`ArgumentError` at configuration time instead of a later `NoMethodError` on a
|
|
121
|
+
retry path. ([#140](https://github.com/kamui/retriable/pull/140))
|
|
122
|
+
|
|
123
|
+
### Internal
|
|
124
|
+
|
|
125
|
+
- Add RBS type signatures for the public API (`Retriable.configure`, `config`,
|
|
126
|
+
`retriable`, `with_override`, `with_context`, and `Retriable::Config`) and
|
|
127
|
+
validate them in CI with `rbs validate`.
|
|
128
|
+
([#142](https://github.com/kamui/retriable/pull/142))
|
|
129
|
+
- Enforce a minimum test coverage floor and add a `bundler-audit` dependency
|
|
130
|
+
audit job to CI. ([#143](https://github.com/kamui/retriable/pull/143))
|
|
131
|
+
- Remove an unused `CC_TEST_REPORTER_ID` from the CI workflow.
|
|
132
|
+
([#141](https://github.com/kamui/retriable/pull/141))
|
|
133
|
+
|
|
134
|
+
## 4.1.0
|
|
135
|
+
|
|
136
|
+
### Bug fixes
|
|
137
|
+
|
|
138
|
+
- A per-call or `with_context` `tries:` now clears an inherited `intervals:` from
|
|
139
|
+
global config or a context, matching the documented precedence. Previously
|
|
140
|
+
`Retriable.retriable(tries: 1)` was silently ignored when `intervals` was
|
|
141
|
+
configured, running `intervals.size + 1` times. Passing both `intervals:` and
|
|
142
|
+
`tries:` in the same call still lets `intervals:` win.
|
|
143
|
+
|
|
144
|
+
## 4.0.0
|
|
145
|
+
|
|
146
|
+
**This is a major release with breaking changes. Please read carefully before upgrading.**
|
|
147
|
+
|
|
148
|
+
### Breaking changes
|
|
149
|
+
|
|
150
|
+
- 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 guide](docs/migration.md#3x-to-40) for replacement patterns.
|
|
151
|
+
- 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.
|
|
152
|
+
|
|
153
|
+
### Features
|
|
154
|
+
|
|
155
|
+
- 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.
|
|
156
|
+
- 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`.
|
|
157
|
+
|
|
158
|
+
### Internal
|
|
159
|
+
|
|
160
|
+
- Switched `Retriable.retriable`, `Retriable.with_context`, and the `Kernel` extension methods to Ruby 3.1+ anonymous block forwarding. No user-visible behavior change.
|
|
161
|
+
|
|
3
162
|
## 3.8.0
|
|
4
163
|
|
|
5
164
|
### Deprecations
|
data/Gemfile
CHANGED
|
@@ -10,10 +10,15 @@ group :test do
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
group :development do
|
|
13
|
-
gem "
|
|
13
|
+
gem "bundler-audit", "~> 0.9"
|
|
14
|
+
gem "listen", "~> 3.1"
|
|
15
|
+
gem "rbs", "~> 4.0", platforms: :ruby
|
|
16
|
+
gem "rubocop", "~> 1.86"
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
group :development, :test do
|
|
20
|
+
# ostruct left the default gems in Ruby 3.5; pry still requires it.
|
|
21
|
+
gem "ostruct"
|
|
17
22
|
gem "pry"
|
|
18
23
|
gem "rake", "~> 13.0"
|
|
19
24
|
end
|
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
|
+
- [Upgrading](#upgrading)
|
|
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,29 @@ Retriable is a simple DSL to retry failed code blocks with randomized [exponenti
|
|
|
31
32
|
|
|
32
33
|
## Requirements
|
|
33
34
|
|
|
34
|
-
Ruby
|
|
35
|
+
Ruby 3.2+
|
|
35
36
|
|
|
36
|
-
If you need
|
|
37
|
+
If you need Ruby 2.3.0-3.1.x support, use the [3.8.x branch](https://github.com/kamui/retriable/tree/3.8.x) by specifying `~> 3.8` in your Gemfile.
|
|
37
38
|
|
|
38
|
-
If you need
|
|
39
|
+
If you need Ruby 2.0.0-2.2.x support, use the [3.1 branch](https://github.com/kamui/retriable/tree/3.1.x) by specifying `~3.1` in your Gemfile.
|
|
39
40
|
|
|
40
|
-
If you need
|
|
41
|
+
If you need Ruby 1.9.3 support, use the [2.x branch](https://github.com/kamui/retriable/tree/2.x) by specifying `~2.1` in your Gemfile.
|
|
42
|
+
|
|
43
|
+
If you need Ruby 1.8.x to 1.9.2 support, use the [1.x branch](https://github.com/kamui/retriable/tree/1.x) by specifying `~1.4` in your Gemfile.
|
|
44
|
+
|
|
45
|
+
## Upgrading
|
|
46
|
+
|
|
47
|
+
Retriable 5.0 changes one thing for existing users: `Retriable.config` is now a
|
|
48
|
+
frozen snapshot, so direct writes raise `FrozenError`.
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
Retriable.config.sleep_disabled = true # => FrozenError
|
|
52
|
+
Retriable.configure { |c| c.sleep_disabled = true } # do this instead
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Test setup files such as `spec_helper` and `rails_helper` are the likeliest
|
|
56
|
+
place this bites. For the full 4.x to 5.x and 3.x to 4.0 guides, see
|
|
57
|
+
[docs/migration.md](docs/migration.md).
|
|
41
58
|
|
|
42
59
|
## Installation
|
|
43
60
|
|
|
@@ -56,7 +73,7 @@ require 'retriable'
|
|
|
56
73
|
In your Gemfile:
|
|
57
74
|
|
|
58
75
|
```ruby
|
|
59
|
-
gem 'retriable', '~>
|
|
76
|
+
gem 'retriable', '~> 5.0'
|
|
60
77
|
```
|
|
61
78
|
|
|
62
79
|
## Usage
|
|
@@ -103,29 +120,29 @@ The default interval table with 10 tries looks like this (in seconds, rounded to
|
|
|
103
120
|
|
|
104
121
|
Here are the available options, in some vague order of relevance to most common use patterns:
|
|
105
122
|
|
|
106
|
-
| Option | Default | Definition
|
|
107
|
-
| ---------------------- | ----------------- |
|
|
108
|
-
| **`tries`** | `3` | Number of attempts to make at running your code block (includes initial attempt). Pass `Float::INFINITY` to keep retrying until success or until `max_elapsed_time` is reached.
|
|
109
|
-
| **`on`** | `[StandardError]` | Type of exceptions to retry. [Read more](#configuring-which-options-to-retry-with-on).
|
|
110
|
-
| **`retry_if`** | `nil` | Callable (for example a `Proc` or lambda) that receives the rescued exception and returns true/false to decide whether to retry. [Read more](#advanced-retry-matching-with-retry_if).
|
|
111
|
-
| **`on_retry`** | `nil` | `Proc` to call after each try is rescued. Pass `false` to disable a callback set in `#configure` for a single call. [Read more](#callbacks).
|
|
112
|
-
| **`
|
|
113
|
-
| **`
|
|
114
|
-
| **`
|
|
115
|
-
| **`
|
|
116
|
-
| **`
|
|
117
|
-
| **`
|
|
118
|
-
| **`
|
|
119
|
-
| **`
|
|
120
|
-
|
|
121
|
-
Timing options are validated before retrying. `tries` must be a positive integer when Retriable generates intervals, or `Float::INFINITY` for unbounded retries. `base_interval`, `max_interval`, `multiplier`,
|
|
123
|
+
| Option | Default | Definition |
|
|
124
|
+
| ---------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
125
|
+
| **`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. |
|
|
126
|
+
| **`on`** | `[StandardError]` | Type of exceptions to retry. [Read more](#configuring-which-options-to-retry-with-on). |
|
|
127
|
+
| **`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). |
|
|
128
|
+
| **`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). |
|
|
129
|
+
| **`on_give_up`** | `nil` | `Proc` to call when Retriable stops retrying after a rescued retriable exception. [Read more](#callbacks). |
|
|
130
|
+
| **`sleep_disabled`** | `false` | When true, disable exponential backoff and attempt retries immediately. |
|
|
131
|
+
| **`base_interval`** | `0.5` | The initial interval in seconds between tries. |
|
|
132
|
+
| **`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`. |
|
|
133
|
+
| **`max_interval`** | `60` | The maximum interval in seconds that any individual retry can reach. |
|
|
134
|
+
| **`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. |
|
|
135
|
+
| **`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])` |
|
|
136
|
+
| **`intervals`** | `nil` | Skip generated intervals and provide your own array of intervals in seconds. [Read more](#custom-interval-array). |
|
|
137
|
+
|
|
138
|
+
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
139
|
|
|
123
140
|
#### Configuring Which Options to Retry With :on
|
|
124
141
|
|
|
125
142
|
**`:on`** Can take the form:
|
|
126
143
|
|
|
127
144
|
- 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)
|
|
145
|
+
- An `Array` or `Set` of `Exception` classes (retry any exception of one of these types, including subclasses)
|
|
129
146
|
- A `Hash` where the keys are `Exception` classes and the values are one of:
|
|
130
147
|
- `nil` (retry every exception of the key's type, including subclasses)
|
|
131
148
|
- A single `Regexp` pattern (retries exceptions ONLY if their `message` matches the pattern)
|
|
@@ -171,6 +188,55 @@ end
|
|
|
171
188
|
`#configure` sets defaults only. Per-call options passed to `Retriable.retriable` and
|
|
172
189
|
`Retriable.with_context` still take precedence.
|
|
173
190
|
|
|
191
|
+
When a higher-precedence layer sets `tries:` without `intervals:`, it clears any
|
|
192
|
+
`intervals:` inherited from a lower layer (so `retriable(tries: 1)` runs once even
|
|
193
|
+
if `intervals` was configured). Within a single call, passing `intervals:` still
|
|
194
|
+
overrides `tries:`.
|
|
195
|
+
|
|
196
|
+
#### Thread safety
|
|
197
|
+
|
|
198
|
+
`#configure` is the only supported way to change configuration, and it is safe to
|
|
199
|
+
call from multiple threads.
|
|
200
|
+
|
|
201
|
+
Configuration is copy-on-write. `#configure` duplicates the current config, hands
|
|
202
|
+
your block the copy, and publishes it only if the block returns without raising.
|
|
203
|
+
So a reader in another thread always sees either the whole previous config or the
|
|
204
|
+
whole new one, never a half-applied mix, and a block that raises leaves the
|
|
205
|
+
existing config in place.
|
|
206
|
+
|
|
207
|
+
Configuration blocks are serialized. Keep them short, and do not wait inside one
|
|
208
|
+
for work that may call `#configure`, because that work cannot begin until the
|
|
209
|
+
current block returns. Readers are unaffected and continue using the last
|
|
210
|
+
published config while a block runs.
|
|
211
|
+
|
|
212
|
+
The published config is deeply frozen. Reaching around `#configure` to mutate it
|
|
213
|
+
raises `FrozenError`:
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
Retriable.config.tries = 5 # => FrozenError
|
|
217
|
+
Retriable.config.contexts[:api] = {} # => FrozenError
|
|
218
|
+
|
|
219
|
+
Retriable.configure { |c| c.tries = 5 } # this is the supported path
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
That is deliberate. A published config is shared by every thread reading it, so an
|
|
223
|
+
in-place write is a data race that used to corrupt other threads' retry behavior
|
|
224
|
+
silently. `Retriable.config` remains fine to **read**.
|
|
225
|
+
|
|
226
|
+
Two more details:
|
|
227
|
+
|
|
228
|
+
- `#configure` calls can nest. Nested calls on the configuring thread, including
|
|
229
|
+
calls from its fibers, share the outer working copy. Only the outermost call
|
|
230
|
+
publishes. If its block raises, none of the nested changes are published. A
|
|
231
|
+
nested call does not create an independent commit or savepoint.
|
|
232
|
+
- Inside a `#configure` block, every fiber on the configuring thread sees the
|
|
233
|
+
in-progress config. Other threads keep seeing the last published one until the
|
|
234
|
+
block completes.
|
|
235
|
+
|
|
236
|
+
Thread safety covers the config structure. User-supplied callbacks such as
|
|
237
|
+
`retry_if`, `on_retry`, and `on_give_up` can still hold mutable state. The caller
|
|
238
|
+
must synchronize that state if the callback can run from multiple threads.
|
|
239
|
+
|
|
174
240
|
### Override
|
|
175
241
|
|
|
176
242
|
`#with_override` is a block-scoped API for forcing retry options that should
|
|
@@ -208,8 +274,13 @@ do not inherit it. This makes `#with_override` safe to use in parallel test
|
|
|
208
274
|
runners. Fibers running inside the same thread share the thread's active
|
|
209
275
|
override.
|
|
210
276
|
|
|
211
|
-
`#with_override` stores the provided options
|
|
212
|
-
|
|
277
|
+
`#with_override` stores the provided options hash **by reference** and reads
|
|
278
|
+
from it on every attempt while the block runs. Treat the hash and all of its
|
|
279
|
+
nested values as immutable for the duration of the block: do not mutate them
|
|
280
|
+
from inside the block, and do not mutate them from another thread or fiber that
|
|
281
|
+
shares this thread's active override. Mutating the options mid-block results in
|
|
282
|
+
undefined retry behavior. If options must be computed, build the hash before
|
|
283
|
+
calling `#with_override` and do not retain a reference you will later mutate.
|
|
213
284
|
|
|
214
285
|
For test-integration patterns (RSpec `around`, helper methods, Minitest, etc.),
|
|
215
286
|
see [docs/testing.md](docs/testing.md).
|
|
@@ -219,6 +290,8 @@ see [docs/testing.md](docs/testing.md).
|
|
|
219
290
|
This example will only retry on a `Timeout::Error`, retry 3 times and sleep for a full second before each try.
|
|
220
291
|
|
|
221
292
|
```ruby
|
|
293
|
+
require "timeout"
|
|
294
|
+
|
|
222
295
|
Retriable.retriable(on: Timeout::Error, tries: 3, base_interval: 1) do
|
|
223
296
|
# code here...
|
|
224
297
|
end
|
|
@@ -227,6 +300,8 @@ end
|
|
|
227
300
|
You can also specify multiple errors to retry on by passing an array of exceptions.
|
|
228
301
|
|
|
229
302
|
```ruby
|
|
303
|
+
require "timeout"
|
|
304
|
+
|
|
230
305
|
Retriable.retriable(on: [Timeout::Error, Errno::ECONNRESET]) do
|
|
231
306
|
# code here...
|
|
232
307
|
end
|
|
@@ -244,28 +319,6 @@ Retriable.retriable(on: {
|
|
|
244
319
|
end
|
|
245
320
|
```
|
|
246
321
|
|
|
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
322
|
If you need millisecond units of time for the sleep interval:
|
|
270
323
|
|
|
271
324
|
```ruby
|
|
@@ -345,9 +398,11 @@ Retriable.retriable(on_retry: do_this_on_each_retry) do
|
|
|
345
398
|
end
|
|
346
399
|
```
|
|
347
400
|
|
|
401
|
+
> **Note:** On the final rescued attempt — when Retriable is about to give up because `tries` are exhausted — `on_retry` still fires (before `on_give_up`; see below), but `next_interval` is **`nil`** because there is no next retry. Guard any handler that does arithmetic or formatting on `next_interval` (for example `next_interval&.*(1000)`, or `if next_interval`), and avoid unconditionally logging messages like `"retrying in #{next_interval}s"` since no retry is coming. This mirrors the `nil` contract documented for [`on_give_up`](#callbacks) below.
|
|
402
|
+
|
|
348
403
|
#### Disabling a Configured Callback Per Call
|
|
349
404
|
|
|
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
|
|
405
|
+
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
406
|
|
|
352
407
|
```ruby
|
|
353
408
|
Retriable.configure do |c|
|
|
@@ -365,6 +420,28 @@ Retriable.retriable(on_retry: false) do
|
|
|
365
420
|
end
|
|
366
421
|
```
|
|
367
422
|
|
|
423
|
+
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`.
|
|
424
|
+
|
|
425
|
+
```ruby
|
|
426
|
+
do_this_when_retries_stop = Proc.new do |exception, try, elapsed_time, next_interval, reason|
|
|
427
|
+
log "#{exception.class}: '#{exception.message}' - gave up after #{try} tries because #{reason}."
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
Retriable.retriable(on_give_up: do_this_when_retries_stop) do
|
|
431
|
+
# code here...
|
|
432
|
+
end
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
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.
|
|
436
|
+
|
|
437
|
+
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.
|
|
438
|
+
|
|
439
|
+
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".
|
|
440
|
+
|
|
441
|
+
`: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.
|
|
442
|
+
|
|
443
|
+
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.
|
|
444
|
+
|
|
368
445
|
### Ensure/Else
|
|
369
446
|
|
|
370
447
|
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 +469,8 @@ Retriable.configure do |c|
|
|
|
392
469
|
c.contexts[:aws] = {
|
|
393
470
|
tries: 3,
|
|
394
471
|
base_interval: 5,
|
|
395
|
-
on_retry: Proc.new { puts 'Curse you, AWS!' }
|
|
472
|
+
on_retry: Proc.new { puts 'Curse you, AWS!' },
|
|
473
|
+
on_give_up: Proc.new { |_e, _try, _elapsed, _interval, reason| puts "Gave up on AWS: #{reason}" }
|
|
396
474
|
}
|
|
397
475
|
c.contexts[:mysql] = {
|
|
398
476
|
tries: 10,
|
|
@@ -426,6 +504,9 @@ Retriable.with_context(:mysql, tries: 30) do
|
|
|
426
504
|
end
|
|
427
505
|
```
|
|
428
506
|
|
|
507
|
+
`#with_context` requires a block and raises `ArgumentError` if called without
|
|
508
|
+
one.
|
|
509
|
+
|
|
429
510
|
## Kernel Extension
|
|
430
511
|
|
|
431
512
|
If you want to call `Retriable.retriable` without the `Retriable` module prefix and you don't mind extending `Kernel`,
|