retriable 3.5.1 → 3.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b0cb0bb3751f465d22addceebf5466b165871741e5820c7c3291d2e842f6f48a
4
- data.tar.gz: 8bef0641e9c3b0e39c04d79ffcbdaf4542790264ce6648f6540db933cfddbc54
3
+ metadata.gz: 9120a536b473da5668754cf8d0021abc06200c558bc916091b871efb24c60f4c
4
+ data.tar.gz: eb4eaf3b7509cf88c57609a70b83b8fdc75da2727c05a7a0cd5daadf36b601d4
5
5
  SHA512:
6
- metadata.gz: 6c4e07023a30aa934d5397717344c480fb6af8dd56349fcbcb9b85b9ba5539a3f16d2b16a30cbb3682e4140a04f3ace9ecbf0602e68bc4a1a5c805230a031de6
7
- data.tar.gz: 4272451f1213b81537fd8f7cd85ced8a341b8f8dbf228493a42393b74cee3400e90eb9aa466ec3e404b98eef78b2a42ade5600b49c5d8d9ba7dd9e9b4ae1f444
6
+ metadata.gz: 0123eb6bbcdb8d392bb5b4b7eab3c59db53970a8bd48b2a320c2d5b085e9196dc2918fd9934f054755069d7cbc7f3d434cac87d2878f2b4ae767d138e5b03d16
7
+ data.tar.gz: 8d09ecc45ce61a5c76e0afd319b6ddbdb37ae99477f9ec0b6faf81caf6bfbd678abb1336b4254a15350efb2c2bede124625722707241729ed74169d8dfae5a9d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # HEAD
2
2
 
3
+ ## 3.6.1
4
+
5
+ - 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.
6
+ - 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.
7
+ - Docs: Document that `on_retry: false` disables a callback set in `Retriable.configure` for a single call.
8
+
9
+ ## 3.6.0
10
+
11
+ - 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.
12
+
3
13
  ## 3.5.1
4
14
 
5
15
  - 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.
data/README.md CHANGED
@@ -5,6 +5,29 @@
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
+ - [Custom Interval Array](#custom-interval-array)
21
+ - [Turn off Exponential Backoff](#turn-off-exponential-backoff)
22
+ - [Callbacks](#callbacks)
23
+ - [Ensure/Else](#ensureelse)
24
+ - [Contexts](#contexts)
25
+ - [Kernel Extension](#kernel-extension)
26
+ - [Testing](#testing)
27
+ - [Credits](#credits)
28
+ - [Development](#development)
29
+ - [Running Specs](#running-specs)
30
+
8
31
  ## Requirements
9
32
 
10
33
  Ruby 2.3.0+
@@ -32,7 +55,7 @@ require 'retriable'
32
55
  In your Gemfile:
33
56
 
34
57
  ```ruby
35
- gem 'retriable', '~> 3.5'
58
+ gem 'retriable', '~> 3.6'
36
59
  ```
37
60
 
38
61
  ## Usage
@@ -84,7 +107,7 @@ Here are the available options, in some vague order of relevance to most common
84
107
  | **`tries`** | `3` | Number of attempts to make at running your code block (includes initial attempt). |
85
108
  | **`on`** | `[StandardError]` | Type of exceptions to retry. [Read more](#configuring-which-options-to-retry-with-on). |
86
109
  | **`retry_if`** | `nil` | Callable (for example a `Proc` or lambda) that receives the rescued exception and returns true/false to decide whether to retry. [Read more](#advanced-retry-matching-with-retry_if). |
87
- | **`on_retry`** | `nil` | `Proc` to call after each try is rescued. [Read more](#callbacks). |
110
+ | **`on_retry`** | `nil` | `Proc` to call after each try is rescued. Pass `false` to disable a callback set in `#configure` for a single call. [Read more](#callbacks). |
88
111
  | **`sleep_disabled`** | `false` | When true, disable exponential backoff and attempt retries immediately. |
89
112
  | **`base_interval`** | `0.5` | The initial interval in seconds between tries. |
90
113
  | **`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`. |
@@ -149,31 +172,46 @@ end
149
172
 
150
173
  ### Override
151
174
 
152
- If you need to force values globally (including over per-call options), use
153
- `#override`:
175
+ `#with_override` is a block-scoped API for forcing retry options that should
176
+ take precedence over both `#configure` defaults and per-call options. It is
177
+ primarily intended for tests — it lets a test force values like `tries: 1` or
178
+ `base_interval: 0` so the suite runs quickly and predictably, regardless of
179
+ the application's `#configure` defaults. In application code, prefer
180
+ `#configure` for app-level defaults and per-call options for caller-specific
181
+ values.
154
182
 
155
183
  ```ruby
156
- Retriable.override(tries: 1, base_interval: 0)
184
+ Retriable.with_override(tries: 1, base_interval: 0) do
185
+ Retriable.retriable do
186
+ # code here...
187
+ end
188
+ end
157
189
  ```
158
190
 
159
- `#override` precedence:
191
+ Precedence inside the block:
160
192
 
161
193
  ```
162
- override > local options > configure defaults
194
+ with_override > local options > configure defaults
163
195
  ```
164
196
 
165
- `#override` uses process-global state. Once set, it affects every caller and
166
- thread until `#reset_override` runs. Prefer setting it once at boot (or in test
167
- helpers), and avoid toggling it per request in multi-threaded runtimes.
197
+ `#with_override` requires a block and raises `ArgumentError` if called without
198
+ one. The override is active only while the block is executing, and is
199
+ automatically restored to its previous value when the block returns or raises.
200
+ Nested `#with_override` calls work as expected: the inner block temporarily
201
+ replaces the active override and the outer override is restored when the
202
+ inner block exits.
168
203
 
169
- `#override` stores the provided options directly. Do not mutate the options hash
170
- or nested values after passing them to `#override`.
204
+ `#with_override` is scoped to the **current thread**. The active override
205
+ does not affect any other thread, and child threads spawned inside the block
206
+ do not inherit it. This makes `#with_override` safe to use in parallel test
207
+ runners. Fibers running inside the same thread share the thread's active
208
+ override.
171
209
 
172
- To clear an override:
210
+ `#with_override` stores the provided options directly. Do not mutate the
211
+ options hash or nested values for the duration of the block.
173
212
 
174
- ```ruby
175
- Retriable.reset_override
176
- ```
213
+ For test-integration patterns (RSpec `around`, helper methods, Minitest, etc.),
214
+ see [docs/testing.md](docs/testing.md).
177
215
 
178
216
  ### Example Usage
179
217
 
@@ -279,6 +317,26 @@ Retriable.retriable(on_retry: do_this_on_each_retry) do
279
317
  end
280
318
  ```
281
319
 
320
+ #### Disabling a Configured Callback Per Call
321
+
322
+ 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.
323
+
324
+ ```ruby
325
+ Retriable.configure do |c|
326
+ c.on_retry = ->(exception, try, elapsed_time, next_interval) { log(...) }
327
+ end
328
+
329
+ # Most calls use the configured callback.
330
+ Retriable.retriable do
331
+ # ...
332
+ end
333
+
334
+ # This specific call opts out of the configured callback.
335
+ Retriable.retriable(on_retry: false) do
336
+ # ...
337
+ end
338
+ ```
339
+
282
340
  ### Ensure/Else
283
341
 
284
342
  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:
@@ -369,72 +427,16 @@ retriable_with_context(:api) do
369
427
  end
370
428
  ```
371
429
 
372
- ## Short Circuiting Retriable While Testing Your App
373
-
374
- 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.
375
-
376
- If you want to short-circuit retries in tests, including calls that pass local options, use `Retriable.override` and set `tries` to `1`.
377
-
378
- Under Rails, keep shared defaults in `Retriable.configure` and apply test-only overrides conditionally:
379
-
380
- ```ruby
381
- # config/initializers/retriable.rb
382
- Retriable.configure do |c|
383
- c.tries = 3
384
- c.base_interval = 0.5
385
- c.rand_factor = 0.5
386
- end
387
-
388
- if Rails.env.test?
389
- Retriable.override(tries: 1, base_interval: 0, rand_factor: 0)
390
- end
391
- ```
392
-
393
- 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.
394
-
395
- Alternately, if you are using RSpec, you could override the Retriable configuration in your `spec_helper`.
396
-
397
- ```ruby
398
- # spec/spec_helper.rb
399
- Retriable.override(tries: 1, base_interval: 0, rand_factor: 0)
400
- ```
401
-
402
- 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`:
403
-
404
- For example assuming you have configured a `google_api` context:
405
-
406
- ```ruby
407
- # config/initializers/retriable.rb
408
- Retriable.configure do |c|
409
- c.contexts[:google_api] = {
410
- tries: 5,
411
- base_interval: 3,
412
- on: [
413
- Net::ReadTimeout,
414
- Signet::AuthorizationError,
415
- Errno::ECONNRESET,
416
- OpenSSL::SSL::SSLError
417
- ]
418
- }
419
- end
420
- ```
421
-
422
- Then in your test environment, you can override both top-level defaults and per-context options:
430
+ ## Testing
423
431
 
424
- ```ruby
425
- # Build context overrides from existing configured context keys
426
- context_overrides = {}
427
- Retriable.config.contexts.each_key do |key|
428
- context_overrides[key] = { tries: 1, base_interval: 0 }
429
- end
432
+ `Retriable.with_override` is designed to short-circuit retries in your test
433
+ suite so failing blocks do not slow tests down. The simplest pattern is an
434
+ RSpec `around(:each)` hook (or your test framework's equivalent) that wraps
435
+ every example in `with_override(tries: 1, base_interval: 0)`.
430
436
 
431
- Retriable.override(
432
- multiplier: 1.0,
433
- rand_factor: 0.0,
434
- base_interval: 0,
435
- contexts: context_overrides,
436
- )
437
- ```
437
+ For Rails integration, opting out of the override for specific tests, and
438
+ overriding configured contexts in tests, see
439
+ [docs/testing.md](docs/testing.md).
438
440
 
439
441
  ## Credits
440
442
 
@@ -0,0 +1,116 @@
1
+ # Design: `on_give_up` callback follow-ups (issue #72, PR #127)
2
+
3
+ ## Context
4
+
5
+ - Issue [#72](https://github.com/kamui/retriable/issues/72) requests a callback that fires only after all retries are exhausted.
6
+ - Draft PR [#127](https://github.com/kamui/retriable/pull/127) (branch `feat/on-give-up-callback`, authored by maintainer @kamui) implements this as `on_give_up`. It is largely production-ready.
7
+
8
+ This spec defines a small set of follow-up additions on top of `feat/on-give-up-callback`. It does **not** revisit the design decisions already settled in #127 (naming, signature, reason symbols, opt-out behavior).
9
+
10
+ ## Settled decisions inherited from #127
11
+
12
+ | Decision | Resolution |
13
+ | --- | --- |
14
+ | Callback name | `on_give_up` |
15
+ | Signature | `(exception, try, elapsed_time, next_interval, reason)` |
16
+ | Reason values | `:tries_exhausted`, `:max_elapsed_time` |
17
+ | `next_interval` when `:tries_exhausted` | `nil` |
18
+ | `next_interval` when `:max_elapsed_time` | The interval that would have been slept before the next try |
19
+ | Opt-out | `on_give_up: false` (or `nil`) disables a configured handler |
20
+ | Order vs `on_retry` | `on_retry` runs first; `on_give_up` runs just before re-raise |
21
+ | Non-retriable exception types | `on_give_up` does **not** fire |
22
+ | `retry_if` rejection | `on_give_up` does **not** fire |
23
+ | `elapsed_time` for the give-up decision | Re-read after `on_retry` returns so handler time counts toward `max_elapsed_time` |
24
+ | Threading through `Config::ATTRIBUTES` | Already enables `with_context`, `override`, and `configure` automatically |
25
+
26
+ ## Gaps to fill
27
+
28
+ PR #127 has the mechanics right. The follow-up work below closes documentation and test-coverage gaps and locks in undocumented-but-implied semantics.
29
+
30
+ ### 1. Document non-firing cases in README
31
+
32
+ PR #127 covers the firing cases. The README should explicitly state when the callback does **not** fire, because users wiring up paging/metrics need to know.
33
+
34
+ Add one paragraph at the end of the new `on_give_up` subsection in `README.md`:
35
+
36
+ > `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.
37
+
38
+ ### 2. Document handler-raised-error policy in README
39
+
40
+ Current `on_retry` documentation does not state what happens if the handler itself raises. PR #127 silently inherits the same behavior: an exception inside `on_give_up` propagates, replacing the original. Make this explicit.
41
+
42
+ Add one sentence to the same subsection:
43
+
44
+ > 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.
45
+
46
+ ### 3. Mention `on_give_up` in the Contexts example
47
+
48
+ `README.md` already has a Contexts example at `README.md:306`. Extend the `:aws` context to demonstrate `on_give_up`:
49
+
50
+ ```ruby
51
+ Retriable.configure do |c|
52
+ c.contexts[:aws] = {
53
+ tries: 3,
54
+ base_interval: 5,
55
+ on_retry: Proc.new { puts 'Curse you, AWS!' },
56
+ on_give_up: Proc.new { |_e, _try, _elapsed, _interval, reason|
57
+ puts "Gave up on AWS: #{reason}"
58
+ },
59
+ }
60
+ end
61
+ ```
62
+
63
+ ### 4. Test: per-context `override` accepts and dispatches `on_give_up`
64
+
65
+ PR #127 adds a positive `with_context` spec and an `override` spec for top-level overrides, but no spec for the call shape `Retriable.override(contexts: { key: { on_give_up: ... } })`, which is validated by `validate_context_override_options` and applied by `context_options_for`. Add one spec under the existing `#override` context that:
66
+
67
+ 1. Calls `Retriable.override(contexts: { api: { on_give_up: handler, tries: 1 } })`.
68
+ 2. Invokes `Retriable.with_context(:api) { raise StandardError }`.
69
+ 3. Asserts the handler was invoked exactly once with `reason == :tries_exhausted`.
70
+
71
+ ### 5. Test: kernel extension passes `on_give_up` through
72
+
73
+ PR #127 does not exercise the kernel extension (`Kernel#retriable` and `Kernel#retriable_with_context`). The delegation is trivial, but a regression guard is cheap. Add one spec inside the existing `context "global scope extension"` block that requires `retriable/core_ext/kernel`, invokes `retriable(tries: 1, on_give_up: handler) { raise }`, and asserts the handler ran with `reason == :tries_exhausted`. A second `retriable_with_context` spec is not needed because item 4 already covers the context-dispatch path.
74
+
75
+ ### 6. Test: handler that raises propagates and replaces the original
76
+
77
+ Lock in the policy from item 2 with a spec: handler raises `RuntimeError`, caller observes `RuntimeError`, not the original `StandardError`.
78
+
79
+ ### 7. CHANGELOG entry: include signature and reasons
80
+
81
+ PR #127's CHANGELOG line is:
82
+
83
+ > - Add `on_give_up` callback to observe when retries stop because tries are exhausted or the next retry would exceed `max_elapsed_time`.
84
+
85
+ Rewrite to:
86
+
87
+ > - Add `on_give_up` 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.
88
+
89
+ ## Out of scope
90
+
91
+ - Renaming `on_give_up`. The maintainer authored the draft with this name.
92
+ - Changing the callback signature (e.g., removing `next_interval`).
93
+ - Firing for `retry_if` rejection. That decision was made deliberately in #127.
94
+ - Version bump. Deferred to the maintainer's release commit.
95
+ - Touching the pre-existing rubocop offenses noted in PR #127's description (`retriable.gemspec`, `spec/exponential_backoff_spec.rb`).
96
+
97
+ ## Files touched
98
+
99
+ - `README.md` — items 1, 2, 3.
100
+ - `spec/retriable_spec.rb` — items 4, 5, 6.
101
+ - `CHANGELOG.md` — item 7.
102
+
103
+ No changes to `lib/retriable.rb` or `lib/retriable/config.rb`; PR #127's implementation already satisfies the behavior.
104
+
105
+ ## Verification
106
+
107
+ ```sh
108
+ bundle exec rspec
109
+ bundle exec rubocop lib spec
110
+ ```
111
+
112
+ Both must pass. Pre-existing rubocop offenses in `retriable.gemspec` and `spec/exponential_backoff_spec.rb` are intentionally left untouched (see Out of scope).
113
+
114
+ ## Delivery
115
+
116
+ Push as additional commits on the existing `feat/on-give-up-callback` branch (PR #127). If we lack push access to the maintainer's branch, open a PR targeting `feat/on-give-up-callback` with these follow-ups, or post the diff as a review comment on #127.
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.
@@ -55,6 +55,7 @@ module Retriable
55
55
  def validate!
56
56
  validate_optional_non_negative_number(:max_elapsed_time, max_elapsed_time)
57
57
  validate_optional_non_negative_number(:timeout, timeout)
58
+ validate_on(on)
58
59
  validate_intervals
59
60
  return if intervals
60
61
 
@@ -37,5 +37,46 @@ module Retriable
37
37
  def finite_number?(value)
38
38
  value.is_a?(Numeric) && value.to_f.finite?
39
39
  end
40
+
41
+ # Validates an `on:` value. Acceptable shapes:
42
+ # - a Class that descends from Exception
43
+ # - an Array whose elements are Classes that descend from Exception
44
+ # - a Hash whose keys are such Classes and whose values are nil,
45
+ # a Regexp, or an Array of Regexps
46
+ #
47
+ # Without this validation, callers can pass values like `Object` or
48
+ # `Kernel` and silently retry process-critical exceptions such as
49
+ # SystemExit and Interrupt, because every Exception's ancestor chain
50
+ # includes both. Hash values that are not Regexps (e.g. plain Strings)
51
+ # also silently fail to match in #hash_exception_match?, so we require
52
+ # Regexp values explicitly.
53
+ def validate_on(value)
54
+ case value
55
+ when Hash
56
+ value.each do |klass, pattern|
57
+ validate_on_class(klass)
58
+ validate_on_hash_value(klass, pattern)
59
+ end
60
+ when Array
61
+ value.each { |klass| validate_on_class(klass) }
62
+ else
63
+ validate_on_class(value)
64
+ end
65
+ end
66
+
67
+ def validate_on_class(klass)
68
+ return if klass.is_a?(Class) && klass <= Exception
69
+
70
+ raise ArgumentError, "on must be an Exception class or a collection of Exception classes, got #{klass.inspect}"
71
+ end
72
+
73
+ def validate_on_hash_value(klass, pattern)
74
+ return if pattern.nil?
75
+ return if pattern.is_a?(Regexp)
76
+ return if pattern.is_a?(Array) && pattern.all? { |p| p.is_a?(Regexp) }
77
+
78
+ raise ArgumentError,
79
+ "on[#{klass}] must be nil, a Regexp, or an Array of Regexps, got #{pattern.inspect}"
80
+ end
40
81
  end
41
82
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Retriable
4
- VERSION = "3.5.1"
4
+ VERSION = "3.6.1"
5
5
  end