retriable 4.0.0 → 4.1.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: 392e6bc1b69a4c85c286f5df1b90c5991243cd7290cd3b41c028f253ad16dae4
4
- data.tar.gz: f078e585b65160045cc71e4a9f8746b7b480645cf841ff13a7bbbec78fc9ffeb
3
+ metadata.gz: 5961397b426eeaf2d2df8631f0a5dfe05adf52d5b9070ddbdf7b7825fd88f144
4
+ data.tar.gz: c982809ef09eb85ddedfcee95be53741846f6e4438af27e403c9ad4b020214c1
5
5
  SHA512:
6
- metadata.gz: 01dc250d04fbc71c89d6af526ab061ec4439d382ec50872776859415df9bad2d1d4c8f2e0cfb53fc263343d72871ce691df5840721d33caf0ccaacee03bf9f6e
7
- data.tar.gz: f1ae42a3015541313322371fd297f5f677d1a974747b48a524c25e1f676fc7adeb8d309975851198c69632fc124a34e4db5488486783a40beb8758ef12cf6c01
6
+ metadata.gz: 30d8bfeeff407ab15fa9be7c2af2993142dce86d8406318e220afdb111c728b0c8c7cd2d6b105312ed9623074a5b20c88a9c19a6c33c3169d673d2276e0b52c4
7
+ data.tar.gz: f333fbf26eff94629f85f5d58688245025be3f39830bcad59da7f42ab52ad1506b155c03e1d96020a728b489ed016cb8ee1fcd5cde0a7a0ce3cfe59ad078c4d8
@@ -30,8 +30,6 @@ jobs:
30
30
  "4.0",
31
31
  jruby,
32
32
  ]
33
- env:
34
- CC_TEST_REPORTER_ID: 20a1139ef1830b4f813a10a03d90e8aa179b5226f75e75c5a949b25756ebf558
35
33
 
36
34
  steps:
37
35
  # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
@@ -63,3 +61,21 @@ jobs:
63
61
 
64
62
  - name: Run rubocop
65
63
  run: bundle exec rubocop
64
+
65
+ - name: Validate RBS
66
+ run: bundle exec rbs -I sig validate
67
+
68
+ audit:
69
+ runs-on: ubuntu-24.04
70
+
71
+ steps:
72
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
73
+
74
+ - name: Setup ruby
75
+ uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
76
+ with:
77
+ ruby-version: "3.3"
78
+ bundler-cache: true
79
+
80
+ - name: Run bundler-audit
81
+ run: bundle exec bundle-audit check --update
data/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # HEAD
2
2
 
3
+ ## 4.1.1
4
+
5
+ ### Bug fixes
6
+
7
+ - `retry_if`, `on_retry`, and `on_give_up` are now validated to be callable
8
+ (respond to `#call`) or falsy. A non-callable truthy value raises
9
+ `ArgumentError` at configuration time instead of a later `NoMethodError` on a
10
+ retry path. ([#140](https://github.com/kamui/retriable/pull/140))
11
+
12
+ ### Internal
13
+
14
+ - Add RBS type signatures for the public API (`Retriable.configure`, `config`,
15
+ `retriable`, `with_override`, `with_context`, and `Retriable::Config`) and
16
+ validate them in CI with `rbs validate`.
17
+ ([#142](https://github.com/kamui/retriable/pull/142))
18
+ - Enforce a minimum test coverage floor and add a `bundler-audit` dependency
19
+ audit job to CI. ([#143](https://github.com/kamui/retriable/pull/143))
20
+ - Remove an unused `CC_TEST_REPORTER_ID` from the CI workflow.
21
+ ([#141](https://github.com/kamui/retriable/pull/141))
22
+
23
+ ## 4.1.0
24
+
25
+ ### Bug fixes
26
+
27
+ - A per-call or `with_context` `tries:` now clears an inherited `intervals:` from
28
+ global config or a context, matching the documented precedence. Previously
29
+ `Retriable.retriable(tries: 1)` was silently ignored when `intervals` was
30
+ configured, running `intervals.size + 1` times. Passing both `intervals:` and
31
+ `tries:` in the same call still lets `intervals:` win.
32
+
3
33
  ## 4.0.0
4
34
 
5
35
  **This is a major release with breaking changes. Please read carefully before upgrading.**
data/Gemfile CHANGED
@@ -10,7 +10,9 @@ group :test do
10
10
  end
11
11
 
12
12
  group :development do
13
+ gem "bundler-audit", "~> 0.9"
13
14
  gem "listen", "~> 3.1"
15
+ gem "rbs", "~> 3.0", platforms: :ruby
14
16
  gem "rubocop", "~> 1.86"
15
17
  end
16
18
 
data/README.md CHANGED
@@ -206,6 +206,11 @@ end
206
206
  `#configure` sets defaults only. Per-call options passed to `Retriable.retriable` and
207
207
  `Retriable.with_context` still take precedence.
208
208
 
209
+ When a higher-precedence layer sets `tries:` without `intervals:`, it clears any
210
+ `intervals:` inherited from a lower layer (so `retriable(tries: 1)` runs once even
211
+ if `intervals` was configured). Within a single call, passing `intervals:` still
212
+ overrides `tries:`.
213
+
209
214
  ### Override
210
215
 
211
216
  `#with_override` is a block-scoped API for forcing retry options that should
@@ -51,6 +51,9 @@ module Retriable
51
51
  end
52
52
 
53
53
  def validate!
54
+ validate_callable(:retry_if, retry_if)
55
+ validate_callable(:on_retry, on_retry)
56
+ validate_callable(:on_give_up, on_give_up)
54
57
  validate_on(on)
55
58
  validate_intervals
56
59
  if unbounded_tries?(tries)
@@ -28,6 +28,13 @@ module Retriable
28
28
  validate_non_negative_number(name, value)
29
29
  end
30
30
 
31
+ def validate_callable(name, value)
32
+ return unless value # nil/false disable the callback
33
+ return if value.respond_to?(:call)
34
+
35
+ raise ArgumentError, "#{name} must respond to #call or be nil"
36
+ end
37
+
31
38
  def validate_rand_factor
32
39
  return if finite_number?(rand_factor) && rand_factor >= 0 && rand_factor <= 1
33
40
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Retriable
4
- VERSION = "4.0.0"
4
+ VERSION = "4.1.1"
5
5
  end
data/lib/retriable.rb CHANGED
@@ -58,7 +58,7 @@ module Retriable
58
58
  local_config = if opts.empty? && !override_config
59
59
  config
60
60
  else
61
- Config.new(apply_override_options(config.to_h.merge(opts), override_config))
61
+ Config.new(apply_override_options(merge_layer(config.to_h, opts), override_config))
62
62
  end
63
63
 
64
64
  # Config is mutable through `configure`, so validate again immediately before use.
@@ -216,9 +216,17 @@ module Retriable
216
216
  def apply_override_options(options, overrides)
217
217
  return options unless overrides
218
218
 
219
- options = options.merge(overrides)
220
- options[:intervals] = nil if overrides.key?(:tries) && !overrides.key?(:intervals)
221
- options
219
+ merge_layer(options, overrides)
220
+ end
221
+
222
+ # Merge a higher-precedence option layer onto a base layer. A higher layer
223
+ # that sets `tries` without `intervals` clears the base layer's inherited
224
+ # `intervals`, so a caller's `tries:` is never silently ignored. When the
225
+ # higher layer supplies its own `intervals`, those win (same-call override).
226
+ def merge_layer(base, higher)
227
+ merged = base.merge(higher)
228
+ merged[:intervals] = nil if higher.key?(:tries) && !higher.key?(:intervals)
229
+ merged
222
230
  end
223
231
 
224
232
  def available_contexts
@@ -228,7 +236,7 @@ module Retriable
228
236
  def context_options_for(context_key, options)
229
237
  context_options = config_contexts.fetch(context_key, {})
230
238
  context_options = {} unless context_options.is_a?(Hash)
231
- context_options = context_options.merge(options)
239
+ context_options = merge_layer(context_options, options)
232
240
 
233
241
  override_context_options = override_contexts[context_key]
234
242
  return context_options unless override_context_options.is_a?(Hash)
@@ -262,6 +270,7 @@ module Retriable
262
270
  :retriable_exception?,
263
271
  :hash_exception_match?,
264
272
  :apply_override_options,
273
+ :merge_layer,
265
274
  :available_contexts,
266
275
  :context_options_for,
267
276
  :config_contexts,
data/sig/retriable.rbs CHANGED
@@ -1,4 +1,32 @@
1
1
  module Retriable
2
2
  VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
3
+ OVERRIDE_THREAD_KEY: Symbol
4
+
5
+ def self.configure: () { (Config) -> void } -> void
6
+ def self.config: () -> Config
7
+ def self.with_override: (Hash[Symbol, untyped] options) { () -> untyped } -> untyped
8
+ def self.with_context: (Symbol context_key, ?Hash[Symbol, untyped] options) ?{ (Integer) -> untyped } -> untyped
9
+ def self.retriable: (?Hash[Symbol, untyped] options) { (Integer) -> untyped } -> untyped
10
+
11
+ class Config
12
+ ATTRIBUTES: Array[Symbol]
13
+
14
+ attr_accessor tries: Numeric
15
+ attr_accessor base_interval: Numeric
16
+ attr_accessor max_interval: Numeric
17
+ attr_accessor rand_factor: Numeric
18
+ attr_accessor multiplier: Numeric
19
+ attr_accessor sleep_disabled: bool
20
+ attr_accessor max_elapsed_time: Numeric?
21
+ attr_accessor intervals: Array[Numeric]?
22
+ attr_accessor on: untyped
23
+ attr_accessor retry_if: untyped
24
+ attr_accessor on_retry: untyped
25
+ attr_accessor on_give_up: untyped
26
+ attr_accessor contexts: Hash[Symbol, untyped]
27
+
28
+ def initialize: (?Hash[Symbol, untyped] opts) -> void
29
+ def to_h: () -> Hash[Symbol, untyped]
30
+ def validate!: () -> void
31
+ end
4
32
  end
data/spec/config_spec.rb CHANGED
@@ -163,4 +163,21 @@ describe Retriable::Config do
163
163
  .to raise_error(ArgumentError, /on must be an Exception class/)
164
164
  end
165
165
  end
166
+
167
+ context "callable option validation" do
168
+ %i[retry_if on_retry on_give_up].each do |opt|
169
+ it "accepts a callable for #{opt}" do
170
+ expect { described_class.new(opt => ->(*) {}) }.not_to raise_error
171
+ end
172
+
173
+ it "accepts nil and false for #{opt}" do
174
+ expect { described_class.new(opt => nil) }.not_to raise_error
175
+ expect { described_class.new(opt => false) }.not_to raise_error
176
+ end
177
+
178
+ it "rejects a non-callable truthy value for #{opt}" do
179
+ expect { described_class.new(opt => 5) }.to raise_error(ArgumentError, /#{opt}.*#call/)
180
+ end
181
+ end
182
+ end
166
183
  end
@@ -760,6 +760,38 @@ describe Retriable do
760
760
  end
761
761
  end
762
762
 
763
+ context "#retriable tries/intervals precedence" do
764
+ it "lets a per-call tries clear globally configured intervals" do
765
+ described_class.configure { |c| c.intervals = [0.5, 1.0] }
766
+
767
+ expect do
768
+ described_class.retriable(tries: 1) { increment_tries_with_exception }
769
+ end.to raise_error(StandardError)
770
+
771
+ expect(@tries).to eq(1)
772
+ end
773
+
774
+ it "still lets per-call intervals win when both intervals and tries are given" do
775
+ expect do
776
+ described_class.retriable(intervals: [0.5, 1.0], tries: 1) { increment_tries_with_exception }
777
+ end.to raise_error(StandardError)
778
+
779
+ expect(@tries).to eq(3) # intervals.size + 1
780
+ end
781
+
782
+ it "lets a with_context tries clear context intervals" do
783
+ described_class.configure do |c|
784
+ c.contexts[:api] = { intervals: [0.5, 1.0] }
785
+ end
786
+
787
+ expect do
788
+ described_class.with_context(:api, tries: 1) { increment_tries_with_exception }
789
+ end.to raise_error(StandardError)
790
+
791
+ expect(@tries).to eq(1)
792
+ end
793
+ end
794
+
763
795
  context "#with_override" do
764
796
  it "takes precedence over both global config and local options" do
765
797
  described_class.configure { |c| c.tries = 2 }
data/spec/spec_helper.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "simplecov"
4
- SimpleCov.start
4
+ SimpleCov.start do
5
+ minimum_coverage 95
6
+ end
5
7
 
6
8
  require "pry"
7
9
  require_relative "../lib/retriable"
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: 4.0.0
4
+ version: 4.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jack Chu