retriable 3.5.1 → 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.
data/lib/retriable.rb CHANGED
@@ -6,6 +6,13 @@ require_relative "retriable/exponential_backoff"
6
6
  require_relative "retriable/version"
7
7
 
8
8
  module Retriable
9
+ # Thread-local storage key for the active #with_override block.
10
+ # We deliberately use Thread#thread_variable_set/get (true thread-local)
11
+ # rather than Thread.current[] (fiber-local) so that fibers within a thread
12
+ # share the same override. Changing this to Thread.current[] would silently
13
+ # break callers that use fiber-based concurrency.
14
+ OVERRIDE_THREAD_KEY = :retriable_override
15
+
9
16
  module_function
10
17
 
11
18
  def configure
@@ -16,15 +23,19 @@ module Retriable
16
23
  @config ||= Config.new
17
24
  end
18
25
 
19
- def override(opts = {})
20
- raise ArgumentError, "empty override options are not allowed; use reset_override instead" if opts.empty?
26
+ def with_override(opts = {})
27
+ raise ArgumentError, "empty override options are not allowed" if opts.empty?
28
+ raise ArgumentError, "with_override requires a block" unless block_given?
21
29
 
22
30
  validate_override_options(opts)
23
- @override_config = opts
24
- end
25
31
 
26
- def reset_override
27
- @override_config = nil
32
+ previous = Thread.current.thread_variable_get(OVERRIDE_THREAD_KEY)
33
+ Thread.current.thread_variable_set(OVERRIDE_THREAD_KEY, opts)
34
+ begin
35
+ yield
36
+ ensure
37
+ Thread.current.thread_variable_set(OVERRIDE_THREAD_KEY, previous)
38
+ end
28
39
  end
29
40
 
30
41
  def with_context(context_key, options = {}, &block)
@@ -41,10 +52,11 @@ module Retriable
41
52
  end
42
53
 
43
54
  def retriable(opts = {}, &block)
44
- local_config = if opts.empty? && !@override_config
55
+ override_config = current_override
56
+ local_config = if opts.empty? && !override_config
45
57
  config
46
58
  else
47
- Config.new(apply_override_options(config.to_h.merge(opts), @override_config))
59
+ Config.new(apply_override_options(config.to_h.merge(opts), override_config))
48
60
  end
49
61
 
50
62
  # Config is mutable through `configure`, so validate again immediately before use.
@@ -154,16 +166,23 @@ module Retriable
154
166
  raise ArgumentError, "#{k} is not a valid option" unless Config::ATTRIBUTES.include?(k)
155
167
  end
156
168
 
169
+ return unless opts.key?(:contexts)
170
+
157
171
  contexts = opts[:contexts]
158
- 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)
159
175
 
160
- contexts.each_value do |context_options|
161
- validate_context_override_options(context_options)
176
+ contexts.each do |context_key, context_options|
177
+ validate_context_override_options(context_key, context_options)
162
178
  end
163
179
  end
164
180
 
165
- def validate_context_override_options(context_options)
166
- 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
167
186
 
168
187
  context_attributes = Config::ATTRIBUTES - [:contexts]
169
188
  context_options.each_key do |k|
@@ -199,10 +218,15 @@ module Retriable
199
218
  end
200
219
 
201
220
  def override_contexts
202
- contexts = @override_config && @override_config[:contexts]
221
+ override_config = current_override
222
+ contexts = override_config && override_config[:contexts]
203
223
  contexts.is_a?(Hash) ? contexts : {}
204
224
  end
205
225
 
226
+ def current_override
227
+ Thread.current.thread_variable_get(OVERRIDE_THREAD_KEY)
228
+ end
229
+
206
230
  private_class_method(
207
231
  :validate_override_options,
208
232
  :validate_context_override_options,
@@ -218,5 +242,6 @@ module Retriable
218
242
  :context_options_for,
219
243
  :config_contexts,
220
244
  :override_contexts,
245
+ :current_override,
221
246
  )
222
247
  end
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