retriable 3.6.0 → 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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +44 -1
- data/docs/superpowers/specs/2026-05-26-on-give-up-callback-followups-design.md +116 -0
- data/lib/retriable/config.rb +1 -0
- data/lib/retriable/validation.rb +41 -0
- data/lib/retriable/version.rb +1 -1
- data/lib/retriable.rb +12 -5
- data/spec/config_spec.rb +66 -0
- data/spec/retriable_spec.rb +41 -11
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9120a536b473da5668754cf8d0021abc06200c558bc916091b871efb24c60f4c
|
|
4
|
+
data.tar.gz: eb4eaf3b7509cf88c57609a70b83b8fdc75da2727c05a7a0cd5daadf36b601d4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0123eb6bbcdb8d392bb5b4b7eab3c59db53970a8bd48b2a320c2d5b085e9196dc2918fd9934f054755069d7cbc7f3d434cac87d2878f2b4ae767d138e5b03d16
|
|
7
|
+
data.tar.gz: 8d09ecc45ce61a5c76e0afd319b6ddbdb37ae99477f9ec0b6faf81caf6bfbd678abb1336b4254a15350efb2c2bede124625722707241729ed74169d8dfae5a9d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
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
|
+
|
|
3
9
|
## 3.6.0
|
|
4
10
|
|
|
5
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.
|
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+
|
|
@@ -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`. |
|
|
@@ -294,6 +317,26 @@ Retriable.retriable(on_retry: do_this_on_each_retry) do
|
|
|
294
317
|
end
|
|
295
318
|
```
|
|
296
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
|
+
|
|
297
340
|
### Ensure/Else
|
|
298
341
|
|
|
299
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:
|
|
@@ -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/lib/retriable/config.rb
CHANGED
data/lib/retriable/validation.rb
CHANGED
|
@@ -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
|
data/lib/retriable/version.rb
CHANGED
data/lib/retriable.rb
CHANGED
|
@@ -166,16 +166,23 @@ module Retriable
|
|
|
166
166
|
raise ArgumentError, "#{k} is not a valid option" unless Config::ATTRIBUTES.include?(k)
|
|
167
167
|
end
|
|
168
168
|
|
|
169
|
+
return unless opts.key?(:contexts)
|
|
170
|
+
|
|
169
171
|
contexts = opts[:contexts]
|
|
170
|
-
return
|
|
172
|
+
return if contexts.nil?
|
|
173
|
+
|
|
174
|
+
raise ArgumentError, "contexts must be a Hash or nil, got #{contexts.inspect}" unless contexts.is_a?(Hash)
|
|
171
175
|
|
|
172
|
-
contexts.
|
|
173
|
-
validate_context_override_options(context_options)
|
|
176
|
+
contexts.each do |context_key, context_options|
|
|
177
|
+
validate_context_override_options(context_key, context_options)
|
|
174
178
|
end
|
|
175
179
|
end
|
|
176
180
|
|
|
177
|
-
def validate_context_override_options(context_options)
|
|
178
|
-
|
|
181
|
+
def validate_context_override_options(context_key, context_options)
|
|
182
|
+
unless context_options.is_a?(Hash)
|
|
183
|
+
raise ArgumentError,
|
|
184
|
+
"contexts[#{context_key.inspect}] must be a Hash, got #{context_options.inspect}"
|
|
185
|
+
end
|
|
179
186
|
|
|
180
187
|
context_attributes = Config::ATTRIBUTES - [:contexts]
|
|
181
188
|
context_options.each_key do |k|
|
data/spec/config_spec.rb
CHANGED
|
@@ -65,4 +65,70 @@ describe Retriable::Config do
|
|
|
65
65
|
it "raises errors when intervals is not an array" do
|
|
66
66
|
expect { described_class.new(intervals: "1") }.to raise_error(ArgumentError, /intervals must be an Array/)
|
|
67
67
|
end
|
|
68
|
+
|
|
69
|
+
context "on: option validation" do
|
|
70
|
+
it "accepts a single Exception subclass" do
|
|
71
|
+
expect { described_class.new(on: StandardError) }.not_to raise_error
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "accepts Exception itself" do
|
|
75
|
+
expect { described_class.new(on: Exception) }.not_to raise_error
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "accepts an array of Exception subclasses" do
|
|
79
|
+
expect { described_class.new(on: [StandardError, RuntimeError]) }.not_to raise_error
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "accepts a hash with nil pattern values" do
|
|
83
|
+
expect { described_class.new(on: { StandardError => nil }) }.not_to raise_error
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "accepts a hash with Regexp pattern values" do
|
|
87
|
+
expect { described_class.new(on: { StandardError => /boom/ }) }.not_to raise_error
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "accepts a hash with Array-of-Regexp pattern values" do
|
|
91
|
+
expect { described_class.new(on: { StandardError => [/a/, /b/] }) }.not_to raise_error
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "rejects Object as on:" do
|
|
95
|
+
expect { described_class.new(on: Object) }
|
|
96
|
+
.to raise_error(ArgumentError, /on must be an Exception class/)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "rejects Kernel as on:" do
|
|
100
|
+
expect { described_class.new(on: Kernel) }
|
|
101
|
+
.to raise_error(ArgumentError, /on must be an Exception class/)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "rejects an array containing a non-Exception class" do
|
|
105
|
+
expect { described_class.new(on: [StandardError, Kernel]) }
|
|
106
|
+
.to raise_error(ArgumentError, /on must be an Exception class/)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "rejects a hash key that is not an Exception class" do
|
|
110
|
+
expect { described_class.new(on: { Kernel => nil }) }
|
|
111
|
+
.to raise_error(ArgumentError, /on must be an Exception class/)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "rejects a hash value that is a String" do
|
|
115
|
+
expect { described_class.new(on: { StandardError => "boom" }) }
|
|
116
|
+
.to raise_error(ArgumentError, /on\[StandardError\] must be nil, a Regexp, or an Array of Regexps/)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "rejects a hash value that is an Array containing a non-Regexp" do
|
|
120
|
+
expect { described_class.new(on: { StandardError => [/a/, "b"] }) }
|
|
121
|
+
.to raise_error(ArgumentError, /on\[StandardError\] must be nil, a Regexp, or an Array of Regexps/)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it "rejects a string passed as on:" do
|
|
125
|
+
expect { described_class.new(on: "StandardError") }
|
|
126
|
+
.to raise_error(ArgumentError, /on must be an Exception class/)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "validates on: even when intervals is provided" do
|
|
130
|
+
expect { described_class.new(intervals: [0.1], on: Object) }
|
|
131
|
+
.to raise_error(ArgumentError, /on must be an Exception class/)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
68
134
|
end
|
data/spec/retriable_spec.rb
CHANGED
|
@@ -406,6 +406,16 @@ describe Retriable do
|
|
|
406
406
|
|
|
407
407
|
expect(@tries).to eq(1)
|
|
408
408
|
end
|
|
409
|
+
|
|
410
|
+
it "rejects on: Object before invoking the block" do
|
|
411
|
+
block_invoked = false
|
|
412
|
+
|
|
413
|
+
expect do
|
|
414
|
+
described_class.retriable(on: Object) { block_invoked = true }
|
|
415
|
+
end.to raise_error(ArgumentError, /on must be an Exception class/)
|
|
416
|
+
|
|
417
|
+
expect(block_invoked).to be(false)
|
|
418
|
+
end
|
|
409
419
|
end
|
|
410
420
|
|
|
411
421
|
context "#configure" do
|
|
@@ -587,25 +597,45 @@ describe Retriable do
|
|
|
587
597
|
expect(@tries).to eq(1)
|
|
588
598
|
end
|
|
589
599
|
|
|
590
|
-
it "
|
|
591
|
-
|
|
592
|
-
c.contexts[:api] = { tries: 1 }
|
|
593
|
-
end
|
|
600
|
+
it "raises ArgumentError on non-hash override contexts values" do
|
|
601
|
+
block_called = false
|
|
594
602
|
|
|
595
|
-
described_class.with_override(contexts: 123)
|
|
596
|
-
|
|
603
|
+
expect { described_class.with_override(contexts: 123) { block_called = true } }
|
|
604
|
+
.to raise_error(ArgumentError, /contexts must be a Hash or nil/)
|
|
605
|
+
expect(block_called).to be(false)
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
it "raises ArgumentError on non-hash per-context override values" do
|
|
609
|
+
block_called = false
|
|
610
|
+
|
|
611
|
+
expect { described_class.with_override(contexts: { api: 123 }) { block_called = true } }
|
|
612
|
+
.to raise_error(ArgumentError, /contexts\[:api\] must be a Hash/)
|
|
613
|
+
expect(block_called).to be(false)
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
it "preserves outer override after rejected nested override contexts values" do
|
|
617
|
+
described_class.with_override(tries: 2) do
|
|
618
|
+
expect { described_class.with_override(tries: 1, contexts: 123) { :noop } }
|
|
619
|
+
.to raise_error(ArgumentError, /contexts must be a Hash or nil/)
|
|
620
|
+
|
|
621
|
+
expect { described_class.retriable(tries: 10) { increment_tries_with_exception } }
|
|
622
|
+
.to raise_error(StandardError)
|
|
597
623
|
end
|
|
598
624
|
|
|
599
|
-
expect(@tries).to eq(
|
|
625
|
+
expect(@tries).to eq(2)
|
|
600
626
|
end
|
|
601
627
|
|
|
602
|
-
it "
|
|
628
|
+
it "preserves outer context override after rejected nested per-context values" do
|
|
603
629
|
described_class.configure do |c|
|
|
604
|
-
c.contexts[:api] = { tries:
|
|
630
|
+
c.contexts[:api] = { tries: 10 }
|
|
605
631
|
end
|
|
606
632
|
|
|
607
|
-
described_class.with_override(contexts: { api:
|
|
608
|
-
expect { described_class.
|
|
633
|
+
described_class.with_override(contexts: { api: { tries: 2 } }) do
|
|
634
|
+
expect { described_class.with_override(contexts: { api: 123 }) { :noop } }
|
|
635
|
+
.to raise_error(ArgumentError, /contexts\[:api\] must be a Hash/)
|
|
636
|
+
|
|
637
|
+
expect { described_class.with_context(:api) { increment_tries_with_exception } }
|
|
638
|
+
.to raise_error(StandardError)
|
|
609
639
|
end
|
|
610
640
|
|
|
611
641
|
expect(@tries).to eq(2)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: retriable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.6.
|
|
4
|
+
version: 3.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jack Chu
|
|
@@ -74,6 +74,7 @@ files:
|
|
|
74
74
|
- Rakefile
|
|
75
75
|
- bin/console
|
|
76
76
|
- bin/setup
|
|
77
|
+
- docs/superpowers/specs/2026-05-26-on-give-up-callback-followups-design.md
|
|
77
78
|
- docs/testing.md
|
|
78
79
|
- lib/retriable.rb
|
|
79
80
|
- lib/retriable/config.rb
|