retriable 3.8.0 → 4.0.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.
data/lib/retriable.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "timeout"
4
3
  require_relative "retriable/config"
5
4
  require_relative "retriable/exponential_backoff"
6
5
  require_relative "retriable/version"
@@ -41,7 +40,7 @@ module Retriable
41
40
  end
42
41
  end
43
42
 
44
- def with_context(context_key, options = {}, &block)
43
+ def with_context(context_key, options = {}, &)
45
44
  contexts = available_contexts
46
45
 
47
46
  if !contexts.key?(context_key)
@@ -51,10 +50,10 @@ module Retriable
51
50
 
52
51
  return unless block_given?
53
52
 
54
- retriable(context_options_for(context_key, options), &block)
53
+ retriable(context_options_for(context_key, options), &)
55
54
  end
56
55
 
57
- def retriable(opts = {}, &block)
56
+ def retriable(opts = {}, &)
58
57
  override_config = current_override
59
58
  local_config = if opts.empty? && !override_config
60
59
  config
@@ -66,10 +65,10 @@ module Retriable
66
65
  local_config.validate!
67
66
 
68
67
  plan = retry_plan(local_config)
69
- timeout = local_config.timeout
70
68
  on = local_config.on
71
69
  retry_if = local_config.retry_if
72
70
  on_retry = local_config.on_retry
71
+ on_give_up = local_config.on_give_up
73
72
  sleep_disabled = local_config.sleep_disabled
74
73
  max_elapsed_time = local_config.max_elapsed_time
75
74
 
@@ -79,22 +78,22 @@ module Retriable
79
78
  elapsed_time = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time }
80
79
 
81
80
  execute_tries(
82
- max_tries: plan.max_tries, interval_for: plan.interval_for, timeout: timeout,
81
+ max_tries: plan.max_tries, interval_for: plan.interval_for,
83
82
  exception_list: exception_list, on: on, retry_if: retry_if, on_retry: on_retry,
84
- elapsed_time: elapsed_time, max_elapsed_time: max_elapsed_time,
85
- sleep_disabled: sleep_disabled, &block
83
+ on_give_up: on_give_up, elapsed_time: elapsed_time, max_elapsed_time: max_elapsed_time,
84
+ sleep_disabled: sleep_disabled, &
86
85
  )
87
86
  end
88
87
 
89
88
  def execute_tries( # rubocop:disable Metrics/ParameterLists
90
- max_tries:, interval_for:, timeout:, exception_list:,
91
- on:, retry_if:, on_retry:, elapsed_time:, max_elapsed_time:, sleep_disabled:, &block
89
+ max_tries:, interval_for:, exception_list:,
90
+ on:, retry_if:, on_retry:, on_give_up:, elapsed_time:, max_elapsed_time:, sleep_disabled:
92
91
  )
93
92
  try = 0
94
93
  loop do
95
94
  try += 1
96
95
  begin
97
- return call_with_timeout(timeout, try, &block)
96
+ return yield(try)
98
97
  rescue *exception_list => e
99
98
  raise unless retriable_exception?(e, on, exception_list, retry_if)
100
99
 
@@ -102,7 +101,13 @@ module Retriable
102
101
  call_on_retry(on_retry, e, try, elapsed_time.call, interval)
103
102
 
104
103
  elapsed_interval = sleep_disabled == true ? 0 : interval
105
- raise unless can_retry?(try, max_tries, elapsed_time.call, elapsed_interval, max_elapsed_time)
104
+ # Snapshot elapsed_time once so the stop check and on_give_up see the same value.
105
+ current_elapsed_time = elapsed_time.call
106
+ stop_reason = retry_stop_reason(try, max_tries, current_elapsed_time, elapsed_interval, max_elapsed_time)
107
+ if stop_reason
108
+ call_on_give_up(on_give_up, e, try, current_elapsed_time, interval, stop_reason)
109
+ raise
110
+ end
106
111
 
107
112
  sleep interval if sleep_disabled != true
108
113
  end
@@ -132,23 +137,30 @@ module Retriable
132
137
  ).interval_provider
133
138
  end
134
139
 
135
- def call_with_timeout(timeout, try)
136
- return Timeout.timeout(timeout) { yield(try) } if timeout
137
-
138
- yield(try)
139
- end
140
-
141
140
  def call_on_retry(on_retry, exception, try, elapsed_time, interval)
142
141
  return unless on_retry
143
142
 
144
143
  on_retry.call(exception, try, elapsed_time, interval)
145
144
  end
146
145
 
147
- def can_retry?(try, max_tries, elapsed_time, interval, max_elapsed_time)
148
- return false if max_tries && try >= max_tries
149
- return true if max_elapsed_time.nil?
146
+ def call_on_give_up( # rubocop:disable Metrics/ParameterLists
147
+ on_give_up, exception, try, elapsed_time, interval, reason
148
+ )
149
+ return unless on_give_up
150
+
151
+ on_give_up.call(exception, try, elapsed_time, interval, reason)
152
+ end
153
+
154
+ # `:tries_exhausted` is checked first, but the two conditions can't both hold
155
+ # on the same try in practice: `retry_plan` returns a nil interval whenever
156
+ # `try >= max_tries`, so `(elapsed_time + interval) > max_elapsed_time` is not
157
+ # evaluable on the exhausted-tries try. The early return guards against that
158
+ # nil and also pins precedence in case the plan ever changes.
159
+ def retry_stop_reason(try, max_tries, elapsed_time, interval, max_elapsed_time)
160
+ return :tries_exhausted if max_tries && try >= max_tries
161
+ return nil if max_elapsed_time.nil?
150
162
 
151
- (elapsed_time + interval) <= max_elapsed_time
163
+ :max_elapsed_time if (elapsed_time + interval) > max_elapsed_time
152
164
  end
153
165
 
154
166
  # When `on` is a Hash, we need to verify the exception matches a pattern.
@@ -244,9 +256,9 @@ module Retriable
244
256
  :execute_tries,
245
257
  :retry_plan,
246
258
  :interval_provider,
247
- :call_with_timeout,
248
259
  :call_on_retry,
249
- :can_retry?,
260
+ :call_on_give_up,
261
+ :retry_stop_reason,
250
262
  :retriable_exception?,
251
263
  :hash_exception_match?,
252
264
  :apply_override_options,
data/retriable.gemspec CHANGED
@@ -15,15 +15,10 @@ Gem::Specification.new do |spec|
15
15
  "APIs/services or file system calls."
16
16
  spec.homepage = "https://github.com/kamui/retriable"
17
17
  spec.license = "MIT"
18
+ spec.metadata["rubygems_mfa_required"] = "true"
18
19
 
19
20
  spec.files = `git ls-files -z`.split("\x0")
20
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
21
  spec.require_paths = ["lib"]
22
22
 
23
- spec.required_ruby_version = ">= 2.3.0"
24
-
25
- spec.add_development_dependency "bundler"
26
- spec.add_development_dependency "rspec", "~> 3"
27
-
28
- spec.add_development_dependency "listen", "~> 3.1"
23
+ spec.required_ruby_version = ">= 3.2"
29
24
  end
data/spec/config_spec.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "stringio"
4
-
5
3
  describe Retriable::Config do
6
4
  let(:default_config) { described_class.new }
7
5
 
@@ -34,10 +32,6 @@ describe Retriable::Config do
34
32
  expect(default_config.intervals).to be_nil
35
33
  end
36
34
 
37
- it "timeout defaults to nil" do
38
- expect(default_config.timeout).to be_nil
39
- end
40
-
41
35
  it "on defaults to [StandardError]" do
42
36
  expect(default_config.on).to eq([StandardError])
43
37
  end
@@ -50,6 +44,10 @@ describe Retriable::Config do
50
44
  expect(default_config.on_retry).to be_nil
51
45
  end
52
46
 
47
+ it "on_give_up handler defaults to nil" do
48
+ expect(default_config.on_give_up).to be_nil
49
+ end
50
+
53
51
  it "contexts defaults to {}" do
54
52
  expect(default_config.contexts).to eq({})
55
53
  end
@@ -59,99 +57,12 @@ describe Retriable::Config do
59
57
  expect { described_class.new(does_not_exist: 123) }.to raise_error(ArgumentError, /not a valid option/)
60
58
  end
61
59
 
62
- it "raises errors on invalid timing configuration" do
63
- expect { described_class.new(rand_factor: 1.1) }.to raise_error(ArgumentError, /rand_factor/)
64
- expect do
65
- expect { described_class.new(timeout: -1) }.to raise_error(ArgumentError, /timeout/)
66
- end.to output(/timeout.*deprecated.*Retriable 4\.0/i).to_stderr
60
+ it "rejects timeout as an unknown option" do
61
+ expect { described_class.new(timeout: 5) }.to raise_error(ArgumentError, /not a valid option/)
67
62
  end
68
63
 
69
- context "timeout deprecation" do
70
- it "warns when timeout is configured" do
71
- expect do
72
- described_class.new(timeout: 5)
73
- end.to output(/timeout.*deprecated.*Retriable 4\.0/i).to_stderr
74
- end
75
-
76
- it "warns when timeout is set before validation" do
77
- config = described_class.new
78
- config.timeout = 5
79
-
80
- expect do
81
- config.validate!
82
- end.to output(/timeout.*deprecated.*Retriable 4\.0/i).to_stderr
83
- end
84
-
85
- it "does not warn when timeout is nil" do
86
- expect do
87
- described_class.new(timeout: nil)
88
- end.not_to output.to_stderr
89
- end
90
-
91
- it "does not warn when timeout is omitted" do
92
- expect do
93
- described_class.new
94
- end.not_to output.to_stderr
95
- end
96
-
97
- it "warns at most once per process" do
98
- original_stderr = $stderr
99
- stderr = StringIO.new
100
- begin
101
- $stderr = stderr
102
-
103
- described_class.new(timeout: 5)
104
- described_class.new(timeout: 5)
105
-
106
- config = described_class.new
107
- config.timeout = 5
108
- config.validate!
109
- ensure
110
- $stderr = original_stderr
111
- end
112
-
113
- expect(stderr.string.scan("timeout:` option is deprecated").size).to eq(1)
114
- end
115
-
116
- it "emits the warning under the :deprecated category when supported", if: WARN_CATEGORY_SUPPORTED do
117
- captured = []
118
- allow(Warning).to receive(:warn) do |message, category: nil|
119
- captured << [message, category]
120
- end
121
-
122
- described_class.new(timeout: 5)
123
-
124
- expect(captured.size).to eq(1)
125
- message, category = captured.first
126
- expect(message).to match(/timeout.*deprecated.*Retriable 4\.0/i)
127
- expect(category).to eq(:deprecated)
128
- end
129
-
130
- it "is silenced by Warning[:deprecated] = false", if: WARN_CATEGORY_SUPPORTED do
131
- original = Warning[:deprecated]
132
- begin
133
- Warning[:deprecated] = false
134
- expect do
135
- described_class.new(timeout: 5)
136
- end.not_to output.to_stderr
137
- ensure
138
- Warning[:deprecated] = original
139
- end
140
- end
141
-
142
- it "remains armed when silenced via Warning[:deprecated]", if: WARN_CATEGORY_SUPPORTED do
143
- original = Warning[:deprecated]
144
- begin
145
- Warning[:deprecated] = false
146
- described_class.new(timeout: 5)
147
- ensure
148
- Warning[:deprecated] = original
149
- end
150
-
151
- expect do
152
- described_class.new(timeout: 5)
153
- end.to output(/timeout.*deprecated.*Retriable 4\.0/i).to_stderr
154
- end
64
+ it "raises errors on invalid timing configuration" do
65
+ expect { described_class.new(rand_factor: 1.1) }.to raise_error(ArgumentError, /rand_factor/)
155
66
  end
156
67
 
157
68
  it "raises errors when intervals is not an array" do
@@ -191,6 +102,15 @@ describe Retriable::Config do
191
102
  expect { described_class.new(on: [StandardError, RuntimeError]) }.not_to raise_error
192
103
  end
193
104
 
105
+ it "accepts a Set of Exception subclasses" do
106
+ expect { described_class.new(on: Set[StandardError, RuntimeError]) }.not_to raise_error
107
+ end
108
+
109
+ it "rejects a Set containing a non-Exception class" do
110
+ expect { described_class.new(on: Set[StandardError, Kernel]) }
111
+ .to raise_error(ArgumentError, /on must be an Exception class/)
112
+ end
113
+
194
114
  it "accepts a hash with nil pattern values" do
195
115
  expect { described_class.new(on: { StandardError => nil }) }.not_to raise_error
196
116
  end