retriable 3.5.0 → 3.8.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: cfd17d793faa48456456036eee71083bf629041cfe6f3f673e3bc92ff0090a88
4
- data.tar.gz: 27946e72249fce2362c2a374c8f352dd27bd8edd4ee3deea61c80e4d0da8f5cd
3
+ metadata.gz: 25948e6545b8d98b0c7b08416bdf23b017ced352eb2a4eb1f2e9fdac45912031
4
+ data.tar.gz: 4a92afb17f79a4ad173b046158ca634b0728fc6ccceedc1b7789c9106342b513
5
5
  SHA512:
6
- metadata.gz: 00424ca023aa864fd2a36016138d0dfd2a6a9030f64c7dd427bb296908b244847db139fcad208527f3bcb5cac075c5e0545fd5dd994f4fef48a31ef55baf2939
7
- data.tar.gz: a414ecfe2931a0bf3fb56f831d654c497a5a1b4d37e4dbe1acf1cbd646083f15bbd85271afcf8f5b9b9f6c484c9f4c5b34ef462b35b2ffd7ec4371b50e7769ab
6
+ metadata.gz: e43731b305a64c806bf57c13d78da410412d8ea4adaec9f255bd434b2d190ac53b6fac29172b72ed6bcc7329a3d5378c4075d72f6dede791f1e0bd16f400e58e
7
+ data.tar.gz: 4259468487738b9ae5bcc817ac992af3341f15bcfe604ab0a61ddf1f8793b444775aa57c06ce5f95561e179186e7360e86a877afd02f3b346b68377ffb3ecaf9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # HEAD
2
2
 
3
+ ## 3.8.0
4
+
5
+ ### Deprecations
6
+
7
+ - 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.
8
+
9
+ ## 3.7.0
10
+
11
+ - 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!`.
12
+
13
+ ## 3.6.1
14
+
15
+ - 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.
16
+ - 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.
17
+ - Docs: Document that `on_retry: false` disables a callback set in `Retriable.configure` for a single call.
18
+
19
+ ## 3.6.0
20
+
21
+ - 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.
22
+
23
+ ## 3.5.1
24
+
25
+ - 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.
26
+
3
27
  ## 3.5.0
4
28
 
5
29
  - Fix: Do not count skipped sleep intervals against `max_elapsed_time` when `sleep_disabled` is true.
data/README.md CHANGED
@@ -5,6 +5,30 @@
5
5
 
6
6
  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
7
 
8
+ ## Table of Contents
9
+
10
+ - [Requirements](#requirements)
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
+ - [Migrating off `timeout:`](#migrating-off-timeout)
21
+ - [Custom Interval Array](#custom-interval-array)
22
+ - [Turn off Exponential Backoff](#turn-off-exponential-backoff)
23
+ - [Callbacks](#callbacks)
24
+ - [Ensure/Else](#ensureelse)
25
+ - [Contexts](#contexts)
26
+ - [Kernel Extension](#kernel-extension)
27
+ - [Testing](#testing)
28
+ - [Credits](#credits)
29
+ - [Development](#development)
30
+ - [Running Specs](#running-specs)
31
+
8
32
  ## Requirements
9
33
 
10
34
  Ruby 2.3.0+
@@ -32,7 +56,7 @@ require 'retriable'
32
56
  In your Gemfile:
33
57
 
34
58
  ```ruby
35
- gem 'retriable', '~> 3.5'
59
+ gem 'retriable', '~> 3.8'
36
60
  ```
37
61
 
38
62
  ## Usage
@@ -79,20 +103,22 @@ The default interval table with 10 tries looks like this (in seconds, rounded to
79
103
 
80
104
  Here are the available options, in some vague order of relevance to most common use patterns:
81
105
 
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
- | **`sleep_disabled`** | `false` | When true, disable exponential backoff and attempt retries immediately. |
89
- | **`base_interval`** | `0.5` | The initial interval in seconds between tries. |
90
- | **`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`. |
91
- | **`max_interval`** | `60` | The maximum interval in seconds that any individual retry can reach. |
92
- | **`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. |
93
- | **`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])` |
94
- | **`intervals`** | `nil` | Skip generated intervals and provide your own array of intervals in seconds. [Read more](#custom-interval-array). |
95
- | **`timeout`** | `nil` | 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. 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/). Proceed with caution. |
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`.
96
122
 
97
123
  #### Configuring Which Options to Retry With :on
98
124
 
@@ -147,31 +173,46 @@ end
147
173
 
148
174
  ### Override
149
175
 
150
- If you need to force values globally (including over per-call options), use
151
- `#override`:
176
+ `#with_override` is a block-scoped API for forcing retry options that should
177
+ take precedence over both `#configure` defaults and per-call options. It is
178
+ primarily intended for tests — it lets a test force values like `tries: 1` or
179
+ `base_interval: 0` so the suite runs quickly and predictably, regardless of
180
+ the application's `#configure` defaults. In application code, prefer
181
+ `#configure` for app-level defaults and per-call options for caller-specific
182
+ values.
152
183
 
153
184
  ```ruby
154
- Retriable.override(tries: 1, base_interval: 0)
185
+ Retriable.with_override(tries: 1, base_interval: 0) do
186
+ Retriable.retriable do
187
+ # code here...
188
+ end
189
+ end
155
190
  ```
156
191
 
157
- `#override` precedence:
192
+ Precedence inside the block:
158
193
 
159
194
  ```
160
- override > local options > configure defaults
195
+ with_override > local options > configure defaults
161
196
  ```
162
197
 
163
- `#override` uses process-global state. Once set, it affects every caller and
164
- thread until `#reset_override` runs. Prefer setting it once at boot (or in test
165
- helpers), and avoid toggling it per request in multi-threaded runtimes.
198
+ `#with_override` requires a block and raises `ArgumentError` if called without
199
+ one. The override is active only while the block is executing, and is
200
+ automatically restored to its previous value when the block returns or raises.
201
+ Nested `#with_override` calls work as expected: the inner block temporarily
202
+ replaces the active override and the outer override is restored when the
203
+ inner block exits.
166
204
 
167
- `#override` stores the provided options directly. Do not mutate the options hash
168
- or nested values after passing them to `#override`.
205
+ `#with_override` is scoped to the **current thread**. The active override
206
+ does not affect any other thread, and child threads spawned inside the block
207
+ do not inherit it. This makes `#with_override` safe to use in parallel test
208
+ runners. Fibers running inside the same thread share the thread's active
209
+ override.
169
210
 
170
- To clear an override:
211
+ `#with_override` stores the provided options directly. Do not mutate the
212
+ options hash or nested values for the duration of the block.
171
213
 
172
- ```ruby
173
- Retriable.reset_override
174
- ```
214
+ For test-integration patterns (RSpec `around`, helper methods, Minitest, etc.),
215
+ see [docs/testing.md](docs/testing.md).
175
216
 
176
217
  ### Example Usage
177
218
 
@@ -203,20 +244,32 @@ Retriable.retriable(on: {
203
244
  end
204
245
  ```
205
246
 
206
- You can also specify a timeout if you want the code block to only try for X amount of seconds. This timeout is per try.
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.
207
252
 
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.
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:
209
254
 
210
255
  ```ruby
211
- Retriable.retriable(timeout: 60) do
212
- # code here...
256
+ require "timeout"
257
+
258
+ Retriable.retriable(on: Timeout::Error, tries: 3) do
259
+ Timeout.timeout(5) do
260
+ # code here...
261
+ end
213
262
  end
214
263
  ```
215
264
 
216
- If you need millisecond units of time for the sleep or the timeout:
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
+ If you need millisecond units of time for the sleep interval:
217
270
 
218
271
  ```ruby
219
- Retriable.retriable(base_interval: (200 / 1000.0), timeout: (500 / 1000.0)) do
272
+ Retriable.retriable(base_interval: (200 / 1000.0)) do
220
273
  # code here...
221
274
  end
222
275
  ```
@@ -233,6 +286,21 @@ end
233
286
 
234
287
  This example makes 5 total attempts. If the first attempt fails, the 2nd attempt occurs 0.5 seconds later.
235
288
 
289
+ ### Unbounded Retries (Opt-in)
290
+
291
+ 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.
292
+
293
+ ```ruby
294
+ Retriable.retriable(tries: Float::INFINITY, max_elapsed_time: 300) do
295
+ # code here...
296
+ end
297
+ ```
298
+
299
+ When `tries: Float::INFINITY` is set:
300
+
301
+ - `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.
302
+ - Custom `intervals:` cannot be combined with `Float::INFINITY` and raises `ArgumentError`. Use the exponential backoff settings (`base_interval`, `multiplier`, `max_interval`, `rand_factor`) instead.
303
+
236
304
  ### Turn off Exponential Backoff
237
305
 
238
306
  Exponential backoff is enabled by default. If you want to simply retry code every second, 5 times maximum, you can do this:
@@ -277,6 +345,26 @@ Retriable.retriable(on_retry: do_this_on_each_retry) do
277
345
  end
278
346
  ```
279
347
 
348
+ #### Disabling a Configured Callback Per Call
349
+
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.
351
+
352
+ ```ruby
353
+ Retriable.configure do |c|
354
+ c.on_retry = ->(exception, try, elapsed_time, next_interval) { log(...) }
355
+ end
356
+
357
+ # Most calls use the configured callback.
358
+ Retriable.retriable do
359
+ # ...
360
+ end
361
+
362
+ # This specific call opts out of the configured callback.
363
+ Retriable.retriable(on_retry: false) do
364
+ # ...
365
+ end
366
+ ```
367
+
280
368
  ### Ensure/Else
281
369
 
282
370
  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:
@@ -367,72 +455,16 @@ retriable_with_context(:api) do
367
455
  end
368
456
  ```
369
457
 
370
- ## Short Circuiting Retriable While Testing Your App
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.
373
-
374
- If you want to short-circuit retries in tests, including calls that pass local options, use `Retriable.override` and set `tries` to `1`.
375
-
376
- Under Rails, keep shared defaults in `Retriable.configure` and apply test-only overrides conditionally:
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
385
-
386
- if Rails.env.test?
387
- Retriable.override(tries: 1, base_interval: 0, rand_factor: 0)
388
- end
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:
458
+ ## Testing
421
459
 
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
460
+ `Retriable.with_override` is designed to short-circuit retries in your test
461
+ suite so failing blocks do not slow tests down. The simplest pattern is an
462
+ RSpec `around(:each)` hook (or your test framework's equivalent) that wraps
463
+ every example in `with_override(tries: 1, base_interval: 0)`.
428
464
 
429
- Retriable.override(
430
- multiplier: 1.0,
431
- rand_factor: 0.0,
432
- base_interval: 0,
433
- contexts: context_overrides,
434
- )
435
- ```
465
+ For Rails integration, opting out of the override for specific tests, and
466
+ overriding configured contexts in tests, see
467
+ [docs/testing.md](docs/testing.md).
436
468
 
437
469
  ## Credits
438
470
 
data/docs/testing.md ADDED
@@ -0,0 +1,212 @@
1
+ # Testing with Retriable
2
+
3
+ `Retriable.with_override` exists primarily for tests. It lets a test force
4
+ retry options like `tries: 1` or `base_interval: 0` so the suite runs quickly
5
+ and predictably, regardless of what the application's `Retriable.configure`
6
+ defaults are.
7
+
8
+ `with_override` is block-scoped: the override is active inside the block and
9
+ restored to its previous value (which is usually "no override") when the block
10
+ exits, even if the block raises. It is also thread-local — overrides set in
11
+ one thread do not affect other threads — so it is safe for parallel test
12
+ runners. See the README for the full API contract.
13
+
14
+ ## RSpec
15
+
16
+ ### Apply an override to every test
17
+
18
+ Use `around(:each)` in `RSpec.configure` so every test in the suite runs inside
19
+ the override. This is the most common pattern:
20
+
21
+ ```ruby
22
+ RSpec.configure do |config|
23
+ config.around(:each) do |example|
24
+ Retriable.with_override(tries: 1, base_interval: 0) do
25
+ example.run
26
+ end
27
+ end
28
+ end
29
+ ```
30
+
31
+ ### Apply an override to a specific context
32
+
33
+ ```ruby
34
+ describe MyClient do
35
+ context "when external calls should not retry" do
36
+ around(:each) do |example|
37
+ Retriable.with_override(tries: 1) { example.run }
38
+ end
39
+
40
+ it "fails fast" do
41
+ # `with_override(tries: 1)` is active here
42
+ end
43
+ end
44
+ end
45
+ ```
46
+
47
+ ### Apply an override to a single test
48
+
49
+ Wrap the test body directly:
50
+
51
+ ```ruby
52
+ it "does the thing without waiting" do
53
+ Retriable.with_override(tries: 1, base_interval: 0) do
54
+ # test body
55
+ end
56
+ end
57
+ ```
58
+
59
+ ### Reusable helper
60
+
61
+ Wrap a common configuration in a helper to keep tests readable:
62
+
63
+ ```ruby
64
+ module RetriableHelpers
65
+ def with_fast_retries(&block)
66
+ Retriable.with_override(tries: 1, base_interval: 0, &block)
67
+ end
68
+ end
69
+
70
+ RSpec.configure do |config|
71
+ config.include RetriableHelpers
72
+ end
73
+
74
+ # In a spec:
75
+ it "does the thing" do
76
+ with_fast_retries do
77
+ # test body
78
+ end
79
+ end
80
+ ```
81
+
82
+ ## Minitest
83
+
84
+ ```ruby
85
+ class MyClientTest < Minitest::Test
86
+ def around
87
+ Retriable.with_override(tries: 1, base_interval: 0) { yield }
88
+ end
89
+
90
+ def test_fails_fast
91
+ # `with_override(tries: 1)` is active here
92
+ end
93
+ end
94
+ ```
95
+
96
+ Older Minitest versions without `around` can wrap the test body directly:
97
+
98
+ ```ruby
99
+ def test_fails_fast
100
+ Retriable.with_override(tries: 1) do
101
+ # test body
102
+ end
103
+ end
104
+ ```
105
+
106
+ ## Short-Circuiting Retriable in Your Test Suite
107
+
108
+ When you are running tests for your app, the default retry behavior (3 tries
109
+ with exponential backoff) makes failing blocks take a long time. To short-circuit
110
+ retries — including calls that pass local options — set `tries: 1` and disable
111
+ backoff using `with_override`.
112
+
113
+ ### Under Rails
114
+
115
+ Keep shared defaults in `Retriable.configure` and apply test-only overrides via
116
+ RSpec's `around` hook (or your test framework's equivalent):
117
+
118
+ ```ruby
119
+ # config/initializers/retriable.rb
120
+ Retriable.configure do |c|
121
+ c.tries = 3
122
+ c.base_interval = 0.5
123
+ c.rand_factor = 0.5
124
+ end
125
+
126
+ # spec/spec_helper.rb (or equivalent)
127
+ RSpec.configure do |config|
128
+ config.around(:each) do |example|
129
+ Retriable.with_override(tries: 1, base_interval: 0, rand_factor: 0) do
130
+ example.run
131
+ end
132
+ end
133
+ end
134
+ ```
135
+
136
+ If a specific test needs normal retry behavior, opt out by running outside the
137
+ `around` hook. The cleanest way is to tag the example and skip the hook for
138
+ tagged examples:
139
+
140
+ ```ruby
141
+ config.around(:each, retriable: :real) { |example| example.run }
142
+ config.around(:each) do |example|
143
+ next example.run if example.metadata[:retriable] == :real
144
+
145
+ Retriable.with_override(tries: 1, base_interval: 0, rand_factor: 0) do
146
+ example.run
147
+ end
148
+ end
149
+
150
+ it "exercises the real retry behavior", retriable: :real do
151
+ # `with_override` is not applied here
152
+ end
153
+ ```
154
+
155
+ ### Overriding Configured Contexts in Tests
156
+
157
+ If you have configured contexts, top-level override values (such as `tries: 1`)
158
+ already take precedence over context-specific values. To override
159
+ context-specific options as well (for example, clearing a context's
160
+ `:intervals` array or shrinking its `:on` exception list), pass `:contexts` to
161
+ `with_override`.
162
+
163
+ Given a configured `google_api` context:
164
+
165
+ ```ruby
166
+ # config/initializers/retriable.rb
167
+ Retriable.configure do |c|
168
+ c.contexts[:google_api] = {
169
+ tries: 5,
170
+ base_interval: 3,
171
+ on: [
172
+ Net::ReadTimeout,
173
+ Signet::AuthorizationError,
174
+ Errno::ECONNRESET,
175
+ OpenSSL::SSL::SSLError,
176
+ ],
177
+ }
178
+ end
179
+ ```
180
+
181
+ You can override both top-level defaults and per-context options in your
182
+ test setup:
183
+
184
+ ```ruby
185
+ RSpec.configure do |config|
186
+ config.around(:each) do |example|
187
+ context_overrides = Retriable.config.contexts.each_key.with_object({}) do |key, h|
188
+ h[key] = { tries: 1, base_interval: 0 }
189
+ end
190
+
191
+ Retriable.with_override(
192
+ multiplier: 1.0,
193
+ rand_factor: 0.0,
194
+ base_interval: 0,
195
+ contexts: context_overrides,
196
+ ) do
197
+ example.run
198
+ end
199
+ end
200
+ end
201
+ ```
202
+
203
+ ## Notes
204
+
205
+ - The override is automatically cleared when the block exits, including when
206
+ the block raises. You do not need to clean up after the block.
207
+ - `with_override` calls nest: an inner block temporarily replaces the active
208
+ override, and the outer override is restored when the inner block exits.
209
+ - Overrides are thread-local. Child threads spawned inside the block do not
210
+ inherit it. If a test spawns background threads that themselves call
211
+ `Retriable.retriable`, wrap each background thread's body in its own
212
+ `with_override` call.