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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 575fe838c7ddda74e7e26abf2a3b74a9e4b6866bf20d33a86bc59d1b7c45770d
4
- data.tar.gz: f8d81648c9ea122680f3e8eb2f71b7cbbfffc0049313123da850378bf5b3f77d
3
+ metadata.gz: 9120a536b473da5668754cf8d0021abc06200c558bc916091b871efb24c60f4c
4
+ data.tar.gz: eb4eaf3b7509cf88c57609a70b83b8fdc75da2727c05a7a0cd5daadf36b601d4
5
5
  SHA512:
6
- metadata.gz: bff5a1d9a0efec882841085a306290d11ccd9f20c0622b8a27c733c13981a68c681a562d80eb267c17b866a816a03d514a92b4a17883a1d4180d835f2b76fd02
7
- data.tar.gz: 9da2cfea5cf807d352ed899926b0fa9e45d825935697788163dfbd5cf7c47afe7b0cfd7b20af5b29ce61f84d5d923181b7a671e21e3cafd709b15f922c68abf8
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.
@@ -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.6.0"
4
+ VERSION = "3.6.1"
5
5
  end
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 unless contexts.is_a?(Hash)
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.each_value do |context_options|
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
- return unless context_options.is_a?(Hash)
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
@@ -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 "ignores non-hash override contexts values in with_context" do
591
- described_class.configure do |c|
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) do
596
- described_class.with_context(:api) { increment_tries }
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(1)
625
+ expect(@tries).to eq(2)
600
626
  end
601
627
 
602
- it "ignores non-hash per-context override values in with_context" do
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: 2 }
630
+ c.contexts[:api] = { tries: 10 }
605
631
  end
606
632
 
607
- described_class.with_override(contexts: { api: 123 }) do
608
- expect { described_class.with_context(:api) { increment_tries_with_exception } }.to raise_error(StandardError)
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.0
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