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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +184 -0
- data/ext/breaker_machines_native/extconf.rb +3 -0
- data/lib/breaker_machines/async_circuit.rb +47 -0
- data/lib/breaker_machines/async_support.rb +104 -0
- data/lib/breaker_machines/cascading_circuit.rb +177 -0
- data/lib/breaker_machines/circuit/async_state_management.rb +71 -0
- data/lib/breaker_machines/circuit/base.rb +59 -0
- data/lib/breaker_machines/circuit/callbacks.rb +135 -0
- data/lib/breaker_machines/circuit/configuration.rb +67 -0
- data/lib/breaker_machines/circuit/coordinated_state_management.rb +117 -0
- data/lib/breaker_machines/circuit/execution.rb +231 -0
- data/lib/breaker_machines/circuit/hedged_execution.rb +115 -0
- data/lib/breaker_machines/circuit/introspection.rb +93 -0
- data/lib/breaker_machines/circuit/native.rb +127 -0
- data/lib/breaker_machines/circuit/state_callbacks.rb +72 -0
- data/lib/breaker_machines/circuit/state_management.rb +59 -0
- data/lib/breaker_machines/circuit.rb +8 -0
- data/lib/breaker_machines/circuit_group.rb +153 -0
- data/lib/breaker_machines/console.rb +345 -0
- data/lib/breaker_machines/coordinated_circuit.rb +10 -0
- data/lib/breaker_machines/dsl/cascading_circuit_builder.rb +20 -0
- data/lib/breaker_machines/dsl/circuit_builder.rb +209 -0
- data/lib/breaker_machines/dsl/hedged_builder.rb +21 -0
- data/lib/breaker_machines/dsl/parallel_fallback_wrapper.rb +20 -0
- data/lib/breaker_machines/dsl.rb +283 -0
- data/lib/breaker_machines/errors.rb +71 -0
- data/lib/breaker_machines/hedged_async_support.rb +88 -0
- data/lib/breaker_machines/native_extension.rb +81 -0
- data/lib/breaker_machines/native_speedup.rb +10 -0
- data/lib/breaker_machines/registry.rb +243 -0
- data/lib/breaker_machines/storage/backend_state.rb +69 -0
- data/lib/breaker_machines/storage/base.rb +52 -0
- data/lib/breaker_machines/storage/bucket_memory.rb +176 -0
- data/lib/breaker_machines/storage/cache.rb +169 -0
- data/lib/breaker_machines/storage/fallback_chain.rb +294 -0
- data/lib/breaker_machines/storage/memory.rb +140 -0
- data/lib/breaker_machines/storage/native.rb +93 -0
- data/lib/breaker_machines/storage/null.rb +54 -0
- data/lib/breaker_machines/storage.rb +8 -0
- data/lib/breaker_machines/types.rb +41 -0
- data/lib/breaker_machines/version.rb +5 -0
- data/lib/breaker_machines.rb +200 -0
- data/lib/breaker_machines_native/breaker_machines_native.bundle +0 -0
- data/sig/README.md +74 -0
- data/sig/all.rbs +25 -0
- data/sig/breaker_machines/circuit.rbs +154 -0
- data/sig/breaker_machines/console.rbs +32 -0
- data/sig/breaker_machines/dsl.rbs +50 -0
- data/sig/breaker_machines/errors.rbs +24 -0
- data/sig/breaker_machines/interfaces.rbs +46 -0
- data/sig/breaker_machines/registry.rbs +30 -0
- data/sig/breaker_machines/storage.rbs +65 -0
- data/sig/breaker_machines/types.rbs +97 -0
- data/sig/breaker_machines.rbs +42 -0
- data/sig/manifest.yaml +5 -0
- 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
|