breaker_machines 0.9.2-arm64-darwin

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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +184 -0
  4. data/ext/breaker_machines_native/extconf.rb +3 -0
  5. data/lib/breaker_machines/async_circuit.rb +47 -0
  6. data/lib/breaker_machines/async_support.rb +104 -0
  7. data/lib/breaker_machines/cascading_circuit.rb +177 -0
  8. data/lib/breaker_machines/circuit/async_state_management.rb +71 -0
  9. data/lib/breaker_machines/circuit/base.rb +59 -0
  10. data/lib/breaker_machines/circuit/callbacks.rb +135 -0
  11. data/lib/breaker_machines/circuit/configuration.rb +67 -0
  12. data/lib/breaker_machines/circuit/coordinated_state_management.rb +117 -0
  13. data/lib/breaker_machines/circuit/execution.rb +231 -0
  14. data/lib/breaker_machines/circuit/hedged_execution.rb +115 -0
  15. data/lib/breaker_machines/circuit/introspection.rb +93 -0
  16. data/lib/breaker_machines/circuit/native.rb +127 -0
  17. data/lib/breaker_machines/circuit/state_callbacks.rb +72 -0
  18. data/lib/breaker_machines/circuit/state_management.rb +59 -0
  19. data/lib/breaker_machines/circuit.rb +8 -0
  20. data/lib/breaker_machines/circuit_group.rb +153 -0
  21. data/lib/breaker_machines/console.rb +345 -0
  22. data/lib/breaker_machines/coordinated_circuit.rb +10 -0
  23. data/lib/breaker_machines/dsl/cascading_circuit_builder.rb +20 -0
  24. data/lib/breaker_machines/dsl/circuit_builder.rb +209 -0
  25. data/lib/breaker_machines/dsl/hedged_builder.rb +21 -0
  26. data/lib/breaker_machines/dsl/parallel_fallback_wrapper.rb +20 -0
  27. data/lib/breaker_machines/dsl.rb +283 -0
  28. data/lib/breaker_machines/errors.rb +71 -0
  29. data/lib/breaker_machines/hedged_async_support.rb +88 -0
  30. data/lib/breaker_machines/native_extension.rb +81 -0
  31. data/lib/breaker_machines/native_speedup.rb +10 -0
  32. data/lib/breaker_machines/registry.rb +243 -0
  33. data/lib/breaker_machines/storage/backend_state.rb +69 -0
  34. data/lib/breaker_machines/storage/base.rb +52 -0
  35. data/lib/breaker_machines/storage/bucket_memory.rb +176 -0
  36. data/lib/breaker_machines/storage/cache.rb +169 -0
  37. data/lib/breaker_machines/storage/fallback_chain.rb +294 -0
  38. data/lib/breaker_machines/storage/memory.rb +140 -0
  39. data/lib/breaker_machines/storage/native.rb +93 -0
  40. data/lib/breaker_machines/storage/null.rb +54 -0
  41. data/lib/breaker_machines/storage.rb +8 -0
  42. data/lib/breaker_machines/types.rb +41 -0
  43. data/lib/breaker_machines/version.rb +5 -0
  44. data/lib/breaker_machines.rb +200 -0
  45. data/lib/breaker_machines_native/breaker_machines_native.bundle +0 -0
  46. data/sig/README.md +74 -0
  47. data/sig/all.rbs +25 -0
  48. data/sig/breaker_machines/circuit.rbs +154 -0
  49. data/sig/breaker_machines/console.rbs +32 -0
  50. data/sig/breaker_machines/dsl.rbs +50 -0
  51. data/sig/breaker_machines/errors.rbs +24 -0
  52. data/sig/breaker_machines/interfaces.rbs +46 -0
  53. data/sig/breaker_machines/registry.rbs +30 -0
  54. data/sig/breaker_machines/storage.rbs +65 -0
  55. data/sig/breaker_machines/types.rbs +97 -0
  56. data/sig/breaker_machines.rbs +42 -0
  57. data/sig/manifest.yaml +5 -0
  58. metadata +227 -0
@@ -0,0 +1,345 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BreakerMachines
4
+ # Interactive console for debugging and monitoring circuit breakers
5
+ class Console
6
+ def self.start
7
+ new.run
8
+ end
9
+
10
+ def initialize
11
+ @running = true
12
+ end
13
+
14
+ def run
15
+ print_header
16
+ print_help if BreakerMachines::Registry.instance.all_circuits.empty?
17
+
18
+ while @running
19
+ print_prompt
20
+ command = gets&.chomp
21
+ break unless command
22
+
23
+ process_command(command)
24
+ end
25
+
26
+ puts "\nExiting BreakerMachines Console..."
27
+ end
28
+
29
+ private
30
+
31
+ def print_header
32
+ puts "\n#{'=' * 60}"
33
+ puts "BreakerMachines Console v#{BreakerMachines::VERSION}"
34
+ puts '=' * 60
35
+ puts "Type 'help' for available commands"
36
+ puts
37
+ end
38
+
39
+ def print_help
40
+ puts <<~HELP
41
+ Available commands:
42
+ list - List all circuits
43
+ stats - Show summary statistics
44
+ show <name> - Show details for circuits with given name
45
+ events <name> [limit] - Show event log for circuit
46
+ reset <name> - Reset circuit(s) to closed state
47
+ force_open <name> - Force circuit to open state
48
+ force_close <name> - Force circuit to closed state
49
+ report - Generate full report
50
+ cleanup - Remove dead circuit references
51
+ refresh - Refresh display
52
+ help - Show this help
53
+ exit/quit - Exit console
54
+
55
+ HELP
56
+ end
57
+
58
+ def print_prompt
59
+ print '> '
60
+ end
61
+
62
+ def process_command(command)
63
+ parts = command.split
64
+ cmd = parts[0]&.downcase
65
+ args = parts[1..]
66
+
67
+ case cmd
68
+ when 'help', '?'
69
+ print_help
70
+ when 'list', 'ls'
71
+ list_circuits
72
+ when 'stats'
73
+ show_stats
74
+ when 'show'
75
+ show_circuit(args[0])
76
+ when 'events'
77
+ show_events(args[0], args[1]&.to_i || 10)
78
+ when 'reset'
79
+ reset_circuit(args[0])
80
+ when 'force_open', 'open'
81
+ force_open_circuit(args[0])
82
+ when 'force_close', 'close'
83
+ force_close_circuit(args[0])
84
+ when 'report'
85
+ generate_report
86
+ when 'cleanup'
87
+ cleanup_registry
88
+ when 'refresh', 'clear'
89
+ system('clear') || system('cls')
90
+ print_header
91
+ when 'exit', 'quit', 'q'
92
+ @running = false
93
+ else
94
+ puts "Unknown command: #{cmd}. Type 'help' for available commands."
95
+ end
96
+ end
97
+
98
+ def list_circuits
99
+ circuits = BreakerMachines::Registry.instance.all_circuits
100
+
101
+ if circuits.empty?
102
+ puts 'No circuits registered.'
103
+ return
104
+ end
105
+
106
+ puts "\nRegistered Circuits:"
107
+ puts '-' * 60
108
+ printf "%-20s %-12s %-10s %-10s %s\n", 'Name', 'State', 'Failures', 'Successes', 'Last Error'
109
+ puts '-' * 60
110
+
111
+ circuits.each do |circuit|
112
+ stats = circuit.stats
113
+ error_info = circuit.last_error ? circuit.last_error.class.name : '-'
114
+
115
+ printf "%-20s %-12s %-10d %-10d %s\n",
116
+ circuit.name,
117
+ colorize_state(stats.state),
118
+ stats.failure_count,
119
+ stats.success_count,
120
+ error_info
121
+ end
122
+
123
+ puts '-' * 60
124
+ puts "Total: #{circuits.size} circuit(s)"
125
+ puts
126
+ end
127
+
128
+ def show_stats
129
+ stats = BreakerMachines::Registry.instance.stats_summary
130
+
131
+ puts "\nCircuit Statistics:"
132
+ puts '-' * 40
133
+ puts "Total circuits: #{stats[:total]}"
134
+ puts "\nBy State:"
135
+ stats[:by_state].each do |state, count|
136
+ puts " #{colorize_state(state)}: #{count}"
137
+ end
138
+
139
+ puts "\nBy Name:"
140
+ stats[:by_name].each do |name, count|
141
+ puts " #{name}: #{count} instance(s)"
142
+ end
143
+ puts
144
+ end
145
+
146
+ def show_circuit(name)
147
+ if name.nil?
148
+ puts 'Usage: show <circuit_name>'
149
+ return
150
+ end
151
+
152
+ circuits = BreakerMachines::Registry.instance.find_by_name(name.to_sym)
153
+
154
+ if circuits.empty?
155
+ puts "No circuits found with name: #{name}"
156
+ return
157
+ end
158
+
159
+ circuits.each_with_index do |circuit, index|
160
+ puts "\n#{'-' * 60}"
161
+ puts "Circuit ##{index + 1}: #{circuit.name}"
162
+ puts '-' * 60
163
+
164
+ puts circuit.summary
165
+ puts
166
+
167
+ stats = circuit.stats
168
+ config = circuit.configuration
169
+
170
+ puts "Current State: #{colorize_state(stats.state)}"
171
+ puts "Failure Count: #{stats.failure_count} / #{config[:failure_threshold]}"
172
+ puts "Success Count: #{stats.success_count}"
173
+
174
+ if stats.opened_at
175
+ puts "Opened At: #{Time.at(stats.opened_at)}"
176
+ reset_time = Time.at(stats.opened_at + config[:reset_timeout])
177
+ puts "Reset At: #{reset_time} (in #{(reset_time - Time.now).to_i}s)"
178
+ end
179
+
180
+ if circuit.last_error
181
+ error_info = circuit.last_error_info
182
+ puts "\nLast Error:"
183
+ puts " Class: #{error_info.error_class}"
184
+ puts " Message: #{error_info.message}"
185
+ puts " Time: #{Time.at(error_info.occurred_at)}"
186
+ end
187
+
188
+ puts "\nConfiguration:"
189
+ puts " Failure Threshold: #{config[:failure_threshold]}"
190
+ puts " Failure Window: #{config[:failure_window]}s"
191
+ puts " Reset Timeout: #{config[:reset_timeout]}s"
192
+ puts " Success Threshold: #{config[:success_threshold]}"
193
+ puts " Half-Open Calls: #{config[:half_open_calls]}"
194
+ end
195
+ puts
196
+ end
197
+
198
+ def show_events(name, limit)
199
+ if name.nil?
200
+ puts 'Usage: events <circuit_name> [limit]'
201
+ return
202
+ end
203
+
204
+ circuits = BreakerMachines::Registry.instance.find_by_name(name.to_sym)
205
+
206
+ if circuits.empty?
207
+ puts "No circuits found with name: #{name}"
208
+ return
209
+ end
210
+
211
+ circuits.each do |circuit|
212
+ events = circuit.event_log(limit: limit)
213
+
214
+ puts "\nEvent Log for #{circuit.name} (last #{limit} events):"
215
+ puts '-' * 80
216
+
217
+ if events.empty?
218
+ puts 'No events recorded.'
219
+ else
220
+ printf "%-20s %-15s %-10s %-20s %s\n", 'Timestamp', 'Type', 'Duration', 'Error', 'Details'
221
+ puts '-' * 80
222
+
223
+ events.each do |event|
224
+ timestamp = Time.at(event[:timestamp]).strftime('%Y-%m-%d %H:%M:%S')
225
+ type = colorize_event_type(event[:type])
226
+ duration = event[:duration_ms] ? "#{event[:duration_ms]}ms" : '-'
227
+ error = event[:error_class] || '-'
228
+ details = event[:new_state] ? "→ #{event[:new_state]}" : ''
229
+
230
+ printf "%-20s %-15s %-10s %-20s %s\n", timestamp, type, duration, error, details
231
+ end
232
+ end
233
+ end
234
+ puts
235
+ end
236
+
237
+ def reset_circuit(name)
238
+ if name.nil?
239
+ puts 'Usage: reset <circuit_name>'
240
+ return
241
+ end
242
+
243
+ circuits = BreakerMachines::Registry.instance.find_by_name(name.to_sym)
244
+
245
+ if circuits.empty?
246
+ puts "No circuits found with name: #{name}"
247
+ return
248
+ end
249
+
250
+ circuits.each do |circuit|
251
+ circuit.reset
252
+ puts "Reset circuit: #{circuit.name} (now #{circuit.status_name})"
253
+ end
254
+ end
255
+
256
+ def force_open_circuit(name)
257
+ if name.nil?
258
+ puts 'Usage: force_open <circuit_name>'
259
+ return
260
+ end
261
+
262
+ circuits = BreakerMachines::Registry.instance.find_by_name(name.to_sym)
263
+
264
+ if circuits.empty?
265
+ puts "No circuits found with name: #{name}"
266
+ return
267
+ end
268
+
269
+ circuits.each do |circuit|
270
+ circuit.force_open
271
+ puts "Forced open circuit: #{circuit.name} (now #{circuit.status_name})"
272
+ end
273
+ end
274
+
275
+ def force_close_circuit(name)
276
+ if name.nil?
277
+ puts 'Usage: force_close <circuit_name>'
278
+ return
279
+ end
280
+
281
+ circuits = BreakerMachines::Registry.instance.find_by_name(name.to_sym)
282
+
283
+ if circuits.empty?
284
+ puts "No circuits found with name: #{name}"
285
+ return
286
+ end
287
+
288
+ circuits.each do |circuit|
289
+ circuit.force_close
290
+ puts "Forced close circuit: #{circuit.name} (now #{circuit.status_name})"
291
+ end
292
+ end
293
+
294
+ def generate_report
295
+ report = BreakerMachines::Registry.instance.detailed_report
296
+
297
+ puts "\nFull Circuit Report"
298
+ puts '=' * 80
299
+ puts "Generated at: #{Time.now}"
300
+ puts "Total circuits: #{report.size}"
301
+ puts '=' * 80
302
+
303
+ report.each do |circuit_data|
304
+ puts "\nCircuit: #{circuit_data[:name]}"
305
+ puts JSON.pretty_generate(circuit_data)
306
+ puts '-' * 80
307
+ end
308
+ end
309
+
310
+ def cleanup_registry
311
+ before = BreakerMachines::Registry.instance.all_circuits.size
312
+ BreakerMachines::Registry.instance.cleanup_dead_references
313
+ after = BreakerMachines::Registry.instance.all_circuits.size
314
+
315
+ removed = before - after
316
+ puts "Cleaned up #{removed} dead circuit reference(s)."
317
+ end
318
+
319
+ def colorize_state(state)
320
+ case state
321
+ when :closed
322
+ "\e[32m#{state}\e[0m" # Green
323
+ when :open
324
+ "\e[31m#{state}\e[0m" # Red
325
+ when :half_open
326
+ "\e[33m#{state}\e[0m" # Yellow
327
+ else
328
+ state.to_s
329
+ end
330
+ end
331
+
332
+ def colorize_event_type(type)
333
+ case type
334
+ when :success
335
+ "\e[32m#{type}\e[0m" # Green
336
+ when :failure
337
+ "\e[31m#{type}\e[0m" # Red
338
+ when :state_change
339
+ "\e[36m#{type}\e[0m" # Cyan
340
+ else
341
+ type.to_s
342
+ end
343
+ end
344
+ end
345
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BreakerMachines
4
+ # CoordinatedCircuit is a base class for circuits that need coordinated state management.
5
+ # It replaces the standard StateManagement module with CoordinatedStateManagement
6
+ # to enable state transitions based on other circuits' states.
7
+ class CoordinatedCircuit < Circuit
8
+ include Circuit::CoordinatedStateManagement
9
+ end
10
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BreakerMachines
4
+ module DSL
5
+ # Builder for cascading circuit breaker configuration
6
+ class CascadingCircuitBuilder < CircuitBuilder
7
+ def cascades_to(*circuit_names)
8
+ @config[:cascades_to] = circuit_names.flatten
9
+ end
10
+
11
+ def emergency_protocol(protocol_name)
12
+ @config[:emergency_protocol] = protocol_name
13
+ end
14
+
15
+ def on_cascade(&block)
16
+ @config[:on_cascade] = block
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BreakerMachines
4
+ module DSL
5
+ # DSL builder for configuring circuit breakers with a fluent interface
6
+ class CircuitBuilder
7
+ attr_reader :config
8
+
9
+ def initialize
10
+ @config = {
11
+ failure_threshold: 5,
12
+ failure_window: 60.seconds,
13
+ success_threshold: 1,
14
+ timeout: nil,
15
+ reset_timeout: 60.seconds,
16
+ half_open_calls: 1,
17
+ exceptions: [StandardError],
18
+ storage: nil,
19
+ metrics: nil,
20
+ fallback: nil,
21
+ on_open: nil,
22
+ on_close: nil,
23
+ on_half_open: nil,
24
+ on_reject: nil,
25
+ notifications: [],
26
+ fiber_safe: BreakerMachines.config.fiber_safe
27
+ }
28
+ end
29
+
30
+ def threshold(failures: nil, failure_rate: nil, minimum_calls: nil, within: 60.seconds, successes: nil)
31
+ if failure_rate
32
+ # Rate-based threshold
33
+ validate_failure_rate!(failure_rate)
34
+ validate_positive_integer!(:minimum_calls, minimum_calls) if minimum_calls
35
+
36
+ @config[:failure_rate] = failure_rate
37
+ @config[:minimum_calls] = minimum_calls || 5
38
+ @config[:use_rate_threshold] = true
39
+ elsif failures
40
+ # Absolute count threshold (existing behavior)
41
+ validate_positive_integer!(:failures, failures)
42
+ @config[:failure_threshold] = failures
43
+ @config[:use_rate_threshold] = false
44
+ end
45
+
46
+ validate_positive_integer!(:within, within.to_i)
47
+ @config[:failure_window] = within.to_i
48
+
49
+ return unless successes
50
+
51
+ validate_positive_integer!(:successes, successes)
52
+ @config[:success_threshold] = successes
53
+ end
54
+
55
+ def reset_after(duration, jitter: nil)
56
+ validate_positive_integer!(:duration, duration.to_i)
57
+ @config[:reset_timeout] = duration.to_i
58
+
59
+ return unless jitter
60
+
61
+ validate_jitter!(jitter)
62
+ @config[:reset_timeout_jitter] = jitter
63
+ end
64
+
65
+ def timeout(duration)
66
+ validate_non_negative_integer!(:timeout, duration.to_i)
67
+ @config[:timeout] = duration.to_i
68
+ end
69
+
70
+ def half_open_requests(count)
71
+ validate_positive_integer!(:half_open_requests, count)
72
+ @config[:half_open_calls] = count
73
+ end
74
+
75
+ def storage(backend, **options)
76
+ @config[:storage] = case backend
77
+ when :memory
78
+ Storage::Memory.new(**options)
79
+ when :bucket_memory
80
+ Storage::BucketMemory.new(**options)
81
+ when :cache
82
+ Storage::Cache.new(**options)
83
+ when :null
84
+ Storage::Null.new(**options)
85
+ when :fallback_chain
86
+ config = options.is_a?(Proc) ? options.call(timeout: 5) : options
87
+ Storage::FallbackChain.new(config)
88
+ when Class
89
+ backend.new(**options)
90
+ else
91
+ backend
92
+ end
93
+ end
94
+
95
+ def metrics(recorder = nil, &block)
96
+ @config[:metrics] = recorder || block
97
+ end
98
+
99
+ def fallback(value = nil, &block)
100
+ raise ArgumentError, 'Fallback requires either a value or a block' if value.nil? && !block_given?
101
+
102
+ fallback_value = block || value
103
+
104
+ if @config[:fallback].is_a?(Array)
105
+ @config[:fallback] << fallback_value
106
+ elsif @config[:fallback]
107
+ @config[:fallback] = [@config[:fallback], fallback_value]
108
+ else
109
+ @config[:fallback] = fallback_value
110
+ end
111
+ end
112
+
113
+ def on_open(&block)
114
+ @config[:on_open] = block
115
+ end
116
+
117
+ def on_close(&block)
118
+ @config[:on_close] = block
119
+ end
120
+
121
+ def on_half_open(&block)
122
+ @config[:on_half_open] = block
123
+ end
124
+
125
+ def on_reject(&block)
126
+ @config[:on_reject] = block
127
+ end
128
+
129
+ # Configure hedged requests
130
+ def hedged(&)
131
+ if block_given?
132
+ hedged_builder = DSL::HedgedBuilder.new(@config)
133
+ hedged_builder.instance_eval(&)
134
+ else
135
+ @config[:hedged_requests] = true
136
+ end
137
+ end
138
+
139
+ # Configure multiple backends
140
+ def backends(*backend_list)
141
+ @config[:backends] = backend_list.flatten
142
+ end
143
+
144
+ # Configure parallel fallback execution
145
+ def parallel_fallback(fallback_list)
146
+ @config[:fallback] = DSL::ParallelFallbackWrapper.new(fallback_list)
147
+ end
148
+
149
+ def notify(service, url = nil, events: %i[open close], **options)
150
+ notification = {
151
+ via: service,
152
+ url: url,
153
+ events: Array(events),
154
+ options: options
155
+ }
156
+ @config[:notifications] << notification
157
+ end
158
+
159
+ def handle(*exceptions)
160
+ @config[:exceptions] = exceptions
161
+ end
162
+
163
+ def fiber_safe(enabled = true) # rubocop:disable Style/OptionalBooleanParameter
164
+ @config[:fiber_safe] = enabled
165
+ end
166
+
167
+ def max_concurrent(limit)
168
+ validate_positive_integer!(:max_concurrent, limit)
169
+ @config[:max_concurrent] = limit
170
+ end
171
+
172
+ # Advanced features
173
+ def parallel_calls(count, timeout: nil)
174
+ @config[:parallel_calls] = count
175
+ @config[:parallel_timeout] = timeout
176
+ end
177
+
178
+ private
179
+
180
+ def validate_positive_integer!(name, value)
181
+ return if value.is_a?(Integer) && value.positive?
182
+
183
+ raise BreakerMachines::ConfigurationError,
184
+ "#{name} must be a positive integer, got: #{value.inspect}"
185
+ end
186
+
187
+ def validate_non_negative_integer!(name, value)
188
+ return if value.is_a?(Integer) && value >= 0
189
+
190
+ raise BreakerMachines::ConfigurationError,
191
+ "#{name} must be a non-negative integer, got: #{value.inspect}"
192
+ end
193
+
194
+ def validate_failure_rate!(rate)
195
+ return if rate.is_a?(Numeric) && rate >= 0.0 && rate <= 1.0
196
+
197
+ raise BreakerMachines::ConfigurationError,
198
+ "failure_rate must be between 0.0 and 1.0, got: #{rate.inspect}"
199
+ end
200
+
201
+ def validate_jitter!(jitter)
202
+ return if jitter.is_a?(Numeric) && jitter >= 0.0 && jitter <= 1.0
203
+
204
+ raise BreakerMachines::ConfigurationError,
205
+ "jitter must be between 0.0 and 1.0 (0% to 100%), got: #{jitter.inspect}"
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BreakerMachines
4
+ module DSL
5
+ # Builder for hedged request configuration
6
+ class HedgedBuilder
7
+ def initialize(config)
8
+ @config = config
9
+ @config[:hedged_requests] = true
10
+ end
11
+
12
+ def delay(milliseconds)
13
+ @config[:hedging_delay] = milliseconds
14
+ end
15
+
16
+ def max_requests(count)
17
+ @config[:max_hedged_requests] = count
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BreakerMachines
4
+ module DSL
5
+ # Wrapper to indicate parallel execution for fallbacks
6
+ class ParallelFallbackWrapper
7
+ attr_reader :fallbacks
8
+
9
+ def initialize(fallbacks)
10
+ @fallbacks = fallbacks
11
+ end
12
+
13
+ def call(error)
14
+ # This will be handled by the circuit's fallback mechanism
15
+ # to execute fallbacks in parallel
16
+ raise NotImplementedError, 'ParallelFallbackWrapper should be handled by Circuit'
17
+ end
18
+ end
19
+ end
20
+ end