retriable 3.5.0 → 4.2.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.
@@ -1,37 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "exponential_backoff"
4
+ require_relative "validation"
4
5
 
5
6
  module Retriable
6
7
  class Config
8
+ include Validation
9
+
7
10
  ATTRIBUTES = (ExponentialBackoff::ATTRIBUTES + %i[
8
11
  sleep_disabled
9
12
  max_elapsed_time
10
13
  intervals
11
- timeout
12
14
  on
13
15
  retry_if
14
16
  on_retry
17
+ on_give_up
15
18
  contexts
16
19
  ]).freeze
17
20
 
21
+ CONTEXT_ATTRIBUTES = (ATTRIBUTES - %i[contexts]).freeze
22
+ private_constant :CONTEXT_ATTRIBUTES
23
+
18
24
  attr_accessor(*ATTRIBUTES)
19
25
 
20
26
  def initialize(opts = {})
21
- backoff = ExponentialBackoff.new
27
+ defaults = ExponentialBackoff::DEFAULTS
22
28
 
23
- @tries = backoff.tries
24
- @base_interval = backoff.base_interval
25
- @max_interval = backoff.max_interval
26
- @rand_factor = backoff.rand_factor
27
- @multiplier = backoff.multiplier
29
+ @tries = defaults[:tries]
30
+ @base_interval = defaults[:base_interval]
31
+ @max_interval = defaults[:max_interval]
32
+ @rand_factor = defaults[:rand_factor]
33
+ @multiplier = defaults[:multiplier]
28
34
  @sleep_disabled = false
29
35
  @max_elapsed_time = 900 # 15 min
30
36
  @intervals = nil
31
- @timeout = nil
32
37
  @on = [StandardError]
33
38
  @retry_if = nil
34
39
  @on_retry = nil
40
+ @on_give_up = nil
35
41
  @contexts = {}
36
42
 
37
43
  opts.each do |k, v|
@@ -39,12 +45,77 @@ module Retriable
39
45
 
40
46
  instance_variable_set(:"@#{k}", v)
41
47
  end
48
+
49
+ validate!
42
50
  end
43
51
 
44
52
  def to_h
45
- ATTRIBUTES.each_with_object({}) do |key, hash|
46
- hash[key] = public_send(key)
53
+ ATTRIBUTES.to_h { |key| [key, public_send(key)] }
54
+ end
55
+
56
+ def validate!
57
+ validate_contexts
58
+ validate_callable(:retry_if, retry_if)
59
+ validate_callable(:on_retry, on_retry)
60
+ validate_callable(:on_give_up, on_give_up)
61
+ validate_on(on)
62
+ validate_intervals
63
+ if unbounded_tries?(tries)
64
+ validate_unbounded_tries
65
+ else
66
+ validate_optional_non_negative_number(:max_elapsed_time, max_elapsed_time)
67
+ return if intervals
68
+
69
+ validate_positive_integer(:tries, tries)
47
70
  end
71
+
72
+ validate_backoff_options
73
+ end
74
+
75
+ private
76
+
77
+ def validate_contexts
78
+ return unless contexts.is_a?(Hash)
79
+ return if contexts.empty?
80
+
81
+ contexts.each_value do |options|
82
+ next unless options.is_a?(Hash)
83
+
84
+ options.each_key do |k|
85
+ next if CONTEXT_ATTRIBUTES.include?(k)
86
+
87
+ raise ArgumentError, "#{k} is not a valid option"
88
+ end
89
+ end
90
+ end
91
+
92
+ def validate_backoff_options
93
+ validate_non_negative_number(:base_interval, base_interval)
94
+ validate_non_negative_number(:multiplier, multiplier)
95
+ validate_non_negative_number(:max_interval, max_interval)
96
+ validate_rand_factor
97
+ end
98
+
99
+ def validate_unbounded_tries
100
+ if intervals
101
+ raise ArgumentError,
102
+ "intervals cannot be used with tries: Float::INFINITY"
103
+ end
104
+
105
+ unless finite_number?(max_elapsed_time)
106
+ raise ArgumentError,
107
+ "max_elapsed_time must be a finite number when tries is Float::INFINITY"
108
+ end
109
+
110
+ validate_non_negative_number(:max_elapsed_time, max_elapsed_time)
111
+ end
112
+
113
+ def validate_intervals
114
+ return if intervals.nil?
115
+ raise ArgumentError, "intervals must be an Array" unless intervals.is_a?(Array)
116
+ return if intervals.all? { |interval| finite_number?(interval) && interval >= 0 }
117
+
118
+ raise ArgumentError, "intervals must contain only non-negative numbers"
48
119
  end
49
120
  end
50
121
  end
@@ -3,11 +3,13 @@
3
3
  require_relative "../../retriable"
4
4
 
5
5
  module Kernel
6
- def retriable(opts = {}, &block)
7
- Retriable.retriable(opts, &block)
6
+ def retriable(opts = {}, &)
7
+ Retriable.retriable(opts, &)
8
8
  end
9
9
 
10
- def retriable_with_context(context_key, opts = {}, &block)
11
- Retriable.with_context(context_key, opts, &block)
10
+ def retriable_with_context(context_key, opts = {}, &)
11
+ Retriable.with_context(context_key, opts, &)
12
12
  end
13
+
14
+ private :retriable, :retriable_with_context
13
15
  end
@@ -1,7 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "validation"
4
+
3
5
  module Retriable
4
6
  class ExponentialBackoff
7
+ include Validation
8
+
5
9
  ATTRIBUTES = %i[
6
10
  tries
7
11
  base_interval
@@ -10,34 +14,64 @@ module Retriable
10
14
  rand_factor
11
15
  ].freeze
12
16
 
17
+ DEFAULTS = {
18
+ tries: 3,
19
+ base_interval: 0.5,
20
+ max_interval: 60,
21
+ rand_factor: 0.5,
22
+ multiplier: 1.5
23
+ }.freeze
24
+
13
25
  attr_accessor(*ATTRIBUTES)
14
26
 
15
27
  def initialize(opts = {})
16
- @tries = 3
17
- @base_interval = 0.5
18
- @max_interval = 60
19
- @rand_factor = 0.5
20
- @multiplier = 1.5
28
+ @tries = DEFAULTS[:tries]
29
+ @base_interval = DEFAULTS[:base_interval]
30
+ @max_interval = DEFAULTS[:max_interval]
31
+ @rand_factor = DEFAULTS[:rand_factor]
32
+ @multiplier = DEFAULTS[:multiplier]
21
33
 
22
34
  opts.each do |k, v|
23
35
  raise ArgumentError, "#{k} is not a valid option" if !ATTRIBUTES.include?(k)
24
36
 
25
37
  instance_variable_set(:"@#{k}", v)
26
38
  end
39
+
40
+ validate!
27
41
  end
28
42
 
29
43
  def intervals
30
- intervals = Array.new(tries) do |iteration|
31
- [base_interval * (multiplier**iteration), max_interval].min
32
- end
44
+ provider = interval_provider
45
+ Array.new(tries) { |iteration| provider.call(iteration) }
46
+ end
47
+
48
+ def interval_provider
49
+ raw_interval = base_interval
33
50
 
34
- return intervals if rand_factor.zero?
51
+ lambda do |_iteration|
52
+ interval = [raw_interval, max_interval].min
53
+ raw_interval = next_raw_interval(raw_interval)
35
54
 
36
- intervals.map { |i| randomize(i) }
55
+ rand_factor.zero? ? interval : randomize(interval)
56
+ end
37
57
  end
38
58
 
39
59
  private
40
60
 
61
+ def validate!
62
+ validate_non_negative_integer(:tries, tries)
63
+ validate_non_negative_number(:base_interval, base_interval)
64
+ validate_non_negative_number(:multiplier, multiplier)
65
+ validate_non_negative_number(:max_interval, max_interval)
66
+ validate_rand_factor
67
+ end
68
+
69
+ def next_raw_interval(raw_interval)
70
+ return max_interval if multiplier >= 1 && raw_interval >= max_interval
71
+
72
+ raw_interval * multiplier
73
+ end
74
+
41
75
  def randomize(interval)
42
76
  delta = rand_factor * interval.to_f
43
77
  min = interval - delta
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Retriable
4
+ module Validation
5
+ private
6
+
7
+ def validate_positive_integer(name, value)
8
+ return if value.is_a?(Integer) && value.positive?
9
+
10
+ raise ArgumentError, "#{name} must be a positive integer"
11
+ end
12
+
13
+ def validate_non_negative_integer(name, value)
14
+ return if value.is_a?(Integer) && value >= 0
15
+
16
+ raise ArgumentError, "#{name} must be a non-negative integer"
17
+ end
18
+
19
+ def validate_non_negative_number(name, value)
20
+ return if finite_number?(value) && value >= 0
21
+
22
+ raise ArgumentError, "#{name} must be a non-negative number"
23
+ end
24
+
25
+ def validate_optional_non_negative_number(name, value)
26
+ return if value.nil?
27
+
28
+ validate_non_negative_number(name, value)
29
+ end
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
+
38
+ def validate_rand_factor
39
+ return if finite_number?(rand_factor) && rand_factor >= 0 && rand_factor <= 1
40
+
41
+ raise ArgumentError, "rand_factor must be between 0 and 1"
42
+ end
43
+
44
+ def finite_number?(value)
45
+ value.is_a?(Numeric) && value.to_f.finite?
46
+ end
47
+
48
+ def unbounded_tries?(value)
49
+ value.is_a?(Numeric) && value.respond_to?(:infinite?) && value.infinite? == 1
50
+ end
51
+
52
+ module_function :unbounded_tries?
53
+
54
+ # Validates an `on:` value. Acceptable shapes:
55
+ # - a Class that descends from Exception
56
+ # - an Array or Set whose elements are Classes that descend from Exception
57
+ # - a Hash whose keys are such Classes and whose values are nil,
58
+ # a Regexp, or an Array of Regexps
59
+ #
60
+ # Without this validation, callers can pass values like `Object` or
61
+ # `Kernel` and silently retry process-critical exceptions such as
62
+ # SystemExit and Interrupt, because every Exception's ancestor chain
63
+ # includes both. Hash values that are not Regexps (e.g. plain Strings)
64
+ # also silently fail to match in #hash_exception_match?, so we require
65
+ # Regexp values explicitly.
66
+ def validate_on(value)
67
+ case value
68
+ in Hash
69
+ value.each do |klass, pattern|
70
+ validate_on_class(klass)
71
+ validate_on_hash_value(klass, pattern)
72
+ end
73
+ in Array | Set
74
+ value.each { |klass| validate_on_class(klass) }
75
+ else
76
+ validate_on_class(value)
77
+ end
78
+ end
79
+
80
+ def validate_on_class(klass)
81
+ return if klass.is_a?(Class) && klass <= Exception
82
+
83
+ raise ArgumentError, "on must be an Exception class or a collection of Exception classes, got #{klass.inspect}"
84
+ end
85
+
86
+ def validate_on_hash_value(klass, pattern)
87
+ return if pattern.nil?
88
+ return if pattern.is_a?(Regexp)
89
+ return if pattern.is_a?(Array) && pattern.all?(Regexp)
90
+
91
+ raise ArgumentError,
92
+ "on[#{klass}] must be nil, a Regexp, or an Array of Regexps, got #{pattern.inspect}"
93
+ end
94
+ end
95
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Retriable
4
- VERSION = "3.5.0"
4
+ VERSION = "4.2.0"
5
5
  end
data/lib/retriable.rb CHANGED
@@ -1,11 +1,20 @@
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"
7
6
 
8
7
  module Retriable
8
+ # Thread-local storage key for the active #with_override block.
9
+ # We deliberately use Thread#thread_variable_set/get (true thread-local)
10
+ # rather than Thread.current[] (fiber-local) so that fibers within a thread
11
+ # share the same override. Changing this to Thread.current[] would silently
12
+ # break callers that use fiber-based concurrency.
13
+ OVERRIDE_THREAD_KEY = :retriable_override
14
+
15
+ RetryPlan = Struct.new(:max_tries, :interval_for)
16
+ private_constant :RetryPlan
17
+
9
18
  module_function
10
19
 
11
20
  def configure
@@ -16,18 +25,24 @@ module Retriable
16
25
  @config ||= Config.new
17
26
  end
18
27
 
19
- def override(opts = {})
20
- raise ArgumentError, "empty override options are not allowed; use reset_override instead" if opts.empty?
28
+ def with_override(opts = {})
29
+ raise ArgumentError, "empty override options are not allowed" if opts.empty?
30
+ raise ArgumentError, "with_override requires a block" unless block_given?
21
31
 
22
32
  validate_override_options(opts)
23
- @override_config = opts
24
- end
25
33
 
26
- def reset_override
27
- @override_config = nil
34
+ previous = Thread.current.thread_variable_get(OVERRIDE_THREAD_KEY)
35
+ Thread.current.thread_variable_set(OVERRIDE_THREAD_KEY, opts)
36
+ begin
37
+ yield
38
+ ensure
39
+ Thread.current.thread_variable_set(OVERRIDE_THREAD_KEY, previous)
40
+ end
28
41
  end
29
42
 
30
- def with_context(context_key, options = {}, &block)
43
+ def with_context(context_key, options = {}, &)
44
+ raise ArgumentError, "with_context requires a block" unless block_given?
45
+
31
46
  contexts = available_contexts
32
47
 
33
48
  if !contexts.key?(context_key)
@@ -35,24 +50,25 @@ module Retriable
35
50
  "#{context_key} not found in Retriable contexts (including overrides). Available contexts: #{contexts.keys}"
36
51
  end
37
52
 
38
- return unless block_given?
39
-
40
- retriable(context_options_for(context_key, options), &block)
53
+ retriable(context_options_for(context_key, options), &)
41
54
  end
42
55
 
43
- def retriable(opts = {}, &block)
44
- local_config = if opts.empty? && !@override_config
56
+ def retriable(opts = {}, &)
57
+ override_config = current_override
58
+ local_config = if opts.empty? && !override_config
45
59
  config
46
60
  else
47
- 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))
48
62
  end
49
63
 
50
- tries = local_config.tries
51
- intervals = build_intervals(local_config, tries)
52
- timeout = local_config.timeout
64
+ # Config is mutable through `configure`, so validate again immediately before use.
65
+ local_config.validate!
66
+
67
+ plan = retry_plan(local_config)
53
68
  on = local_config.on
54
69
  retry_if = local_config.retry_if
55
70
  on_retry = local_config.on_retry
71
+ on_give_up = local_config.on_give_up
56
72
  sleep_disabled = local_config.sleep_disabled
57
73
  max_elapsed_time = local_config.max_elapsed_time
58
74
 
@@ -61,55 +77,67 @@ module Retriable
61
77
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
62
78
  elapsed_time = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time }
63
79
 
64
- tries = intervals.size + 1
65
-
66
80
  execute_tries(
67
- tries: tries, intervals: intervals, timeout: timeout,
81
+ max_tries: plan.max_tries, interval_for: plan.interval_for,
68
82
  exception_list: exception_list, on: on, retry_if: retry_if, on_retry: on_retry,
69
- elapsed_time: elapsed_time, max_elapsed_time: max_elapsed_time,
70
- 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, &
71
85
  )
72
86
  end
73
87
 
74
88
  def execute_tries( # rubocop:disable Metrics/ParameterLists
75
- tries:, intervals:, timeout:, exception_list:,
76
- 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:
77
91
  )
78
- tries.times do |index|
79
- try = index + 1
80
-
92
+ try = 0
93
+ loop do
94
+ try += 1
81
95
  begin
82
- return call_with_timeout(timeout, try, &block)
96
+ return yield(try)
83
97
  rescue *exception_list => e
84
98
  raise unless retriable_exception?(e, on, exception_list, retry_if)
85
99
 
86
- interval = intervals[index]
100
+ # On the final attempt `interval_for` returns nil (no next retry), and
101
+ # `on_retry` intentionally fires before the give-up check below, so it
102
+ # receives `interval: nil`. See the on_retry/on_give_up README contract.
103
+ interval = interval_for.call(try - 1)
87
104
  call_on_retry(on_retry, e, try, elapsed_time.call, interval)
88
105
 
89
106
  elapsed_interval = sleep_disabled == true ? 0 : interval
90
- raise unless can_retry?(try, tries, elapsed_time.call, elapsed_interval, max_elapsed_time)
107
+ # Snapshot elapsed_time once so the stop check and on_give_up see the same value.
108
+ current_elapsed_time = elapsed_time.call
109
+ stop_reason = retry_stop_reason(try, max_tries, current_elapsed_time, elapsed_interval, max_elapsed_time)
110
+ if stop_reason
111
+ call_on_give_up(on_give_up, e, try, current_elapsed_time, interval, stop_reason)
112
+ raise
113
+ end
91
114
 
92
115
  sleep interval if sleep_disabled != true
93
116
  end
94
117
  end
95
118
  end
96
119
 
97
- def build_intervals(local_config, tries)
98
- return local_config.intervals if local_config.intervals
120
+ def retry_plan(local_config)
121
+ return RetryPlan.new(nil, interval_provider(local_config)) if Validation.unbounded_tries?(local_config.tries)
122
+
123
+ if local_config.intervals
124
+ intervals = local_config.intervals
125
+ return RetryPlan.new(intervals.size + 1, ->(index) { intervals[index] })
126
+ end
127
+
128
+ max_tries = local_config.tries
129
+ provider = interval_provider(local_config)
99
130
 
131
+ RetryPlan.new(max_tries, ->(index) { index < max_tries - 1 ? provider.call(index) : nil })
132
+ end
133
+
134
+ def interval_provider(local_config)
100
135
  ExponentialBackoff.new(
101
- tries: tries - 1,
102
136
  base_interval: local_config.base_interval,
103
137
  multiplier: local_config.multiplier,
104
138
  max_interval: local_config.max_interval,
105
139
  rand_factor: local_config.rand_factor,
106
- ).intervals
107
- end
108
-
109
- def call_with_timeout(timeout, try)
110
- return Timeout.timeout(timeout) { yield(try) } if timeout
111
-
112
- yield(try)
140
+ ).interval_provider
113
141
  end
114
142
 
115
143
  def call_on_retry(on_retry, exception, try, elapsed_time, interval)
@@ -118,11 +146,24 @@ module Retriable
118
146
  on_retry.call(exception, try, elapsed_time, interval)
119
147
  end
120
148
 
121
- def can_retry?(try, tries, elapsed_time, interval, max_elapsed_time)
122
- return false unless try < tries
123
- return true if max_elapsed_time.nil?
149
+ def call_on_give_up( # rubocop:disable Metrics/ParameterLists
150
+ on_give_up, exception, try, elapsed_time, interval, reason
151
+ )
152
+ return unless on_give_up
124
153
 
125
- (elapsed_time + interval) <= max_elapsed_time
154
+ on_give_up.call(exception, try, elapsed_time, interval, reason)
155
+ end
156
+
157
+ # `:tries_exhausted` is checked first, but the two conditions can't both hold
158
+ # on the same try in practice: `retry_plan` returns a nil interval whenever
159
+ # `try >= max_tries`, so `(elapsed_time + interval) > max_elapsed_time` is not
160
+ # evaluable on the exhausted-tries try. The early return guards against that
161
+ # nil and also pins precedence in case the plan ever changes.
162
+ def retry_stop_reason(try, max_tries, elapsed_time, interval, max_elapsed_time)
163
+ return :tries_exhausted if max_tries && try >= max_tries
164
+ return nil if max_elapsed_time.nil?
165
+
166
+ :max_elapsed_time if (elapsed_time + interval) > max_elapsed_time
126
167
  end
127
168
 
128
169
  # When `on` is a Hash, we need to verify the exception matches a pattern.
@@ -151,16 +192,23 @@ module Retriable
151
192
  raise ArgumentError, "#{k} is not a valid option" unless Config::ATTRIBUTES.include?(k)
152
193
  end
153
194
 
195
+ return unless opts.key?(:contexts)
196
+
154
197
  contexts = opts[:contexts]
155
- return unless contexts.is_a?(Hash)
198
+ return if contexts.nil?
156
199
 
157
- contexts.each_value do |context_options|
158
- validate_context_override_options(context_options)
200
+ raise ArgumentError, "contexts must be a Hash or nil, got #{contexts.inspect}" unless contexts.is_a?(Hash)
201
+
202
+ contexts.each do |context_key, context_options|
203
+ validate_context_override_options(context_key, context_options)
159
204
  end
160
205
  end
161
206
 
162
- def validate_context_override_options(context_options)
163
- return unless context_options.is_a?(Hash)
207
+ def validate_context_override_options(context_key, context_options)
208
+ unless context_options.is_a?(Hash)
209
+ raise ArgumentError,
210
+ "contexts[#{context_key.inspect}] must be a Hash, got #{context_options.inspect}"
211
+ end
164
212
 
165
213
  context_attributes = Config::ATTRIBUTES - [:contexts]
166
214
  context_options.each_key do |k|
@@ -171,9 +219,17 @@ module Retriable
171
219
  def apply_override_options(options, overrides)
172
220
  return options unless overrides
173
221
 
174
- options = options.merge(overrides)
175
- options[:intervals] = nil if overrides.key?(:tries) && !overrides.key?(:intervals)
176
- options
222
+ merge_layer(options, overrides)
223
+ end
224
+
225
+ # Merge a higher-precedence option layer onto a base layer. A higher layer
226
+ # that sets `tries` without `intervals` clears the base layer's inherited
227
+ # `intervals`, so a caller's `tries:` is never silently ignored. When the
228
+ # higher layer supplies its own `intervals`, those win (same-call override).
229
+ def merge_layer(base, higher)
230
+ merged = base.merge(higher)
231
+ merged[:intervals] = nil if higher.key?(:tries) && !higher.key?(:intervals)
232
+ merged
177
233
  end
178
234
 
179
235
  def available_contexts
@@ -183,7 +239,7 @@ module Retriable
183
239
  def context_options_for(context_key, options)
184
240
  context_options = config_contexts.fetch(context_key, {})
185
241
  context_options = {} unless context_options.is_a?(Hash)
186
- context_options = context_options.merge(options)
242
+ context_options = merge_layer(context_options, options)
187
243
 
188
244
  override_context_options = override_contexts[context_key]
189
245
  return context_options unless override_context_options.is_a?(Hash)
@@ -196,24 +252,32 @@ module Retriable
196
252
  end
197
253
 
198
254
  def override_contexts
199
- contexts = @override_config && @override_config[:contexts]
255
+ override_config = current_override
256
+ contexts = override_config && override_config[:contexts]
200
257
  contexts.is_a?(Hash) ? contexts : {}
201
258
  end
202
259
 
260
+ def current_override
261
+ Thread.current.thread_variable_get(OVERRIDE_THREAD_KEY)
262
+ end
263
+
203
264
  private_class_method(
204
265
  :validate_override_options,
205
266
  :validate_context_override_options,
206
267
  :execute_tries,
207
- :build_intervals,
208
- :call_with_timeout,
268
+ :retry_plan,
269
+ :interval_provider,
209
270
  :call_on_retry,
210
- :can_retry?,
271
+ :call_on_give_up,
272
+ :retry_stop_reason,
211
273
  :retriable_exception?,
212
274
  :hash_exception_match?,
213
275
  :apply_override_options,
276
+ :merge_layer,
214
277
  :available_contexts,
215
278
  :context_options_for,
216
279
  :config_contexts,
217
280
  :override_contexts,
281
+ :current_override,
218
282
  )
219
283
  end
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