breaker_machines 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +1894 -0
- data/lib/breaker_machines/circuit/callbacks.rb +132 -0
- data/lib/breaker_machines/circuit/configuration.rb +73 -0
- data/lib/breaker_machines/circuit/execution.rb +211 -0
- data/lib/breaker_machines/circuit/introspection.rb +77 -0
- data/lib/breaker_machines/circuit/state_management.rb +105 -0
- data/lib/breaker_machines/circuit.rb +14 -0
- data/lib/breaker_machines/console.rb +345 -0
- data/lib/breaker_machines/dsl.rb +274 -0
- data/lib/breaker_machines/errors.rb +30 -0
- data/lib/breaker_machines/registry.rb +99 -0
- data/lib/breaker_machines/storage/base.rb +47 -0
- data/lib/breaker_machines/storage/bucket_memory.rb +163 -0
- data/lib/breaker_machines/storage/memory.rb +127 -0
- data/lib/breaker_machines/storage/null.rb +45 -0
- data/lib/breaker_machines/storage.rb +8 -0
- data/lib/breaker_machines/version.rb +5 -0
- data/lib/breaker_machines.rb +82 -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 +30 -0
- data/sig/manifest.yaml +5 -0
- metadata +167 -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[: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,274 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BreakerMachines
|
4
|
+
# DSL module for adding circuit breakers to classes
|
5
|
+
#
|
6
|
+
# This module uses WeakRef to track instances that include the DSL.
|
7
|
+
# Why? In long-running applications (web servers, background workers),
|
8
|
+
# objects that include this DSL may be created and destroyed frequently.
|
9
|
+
# Without WeakRef, the registry would hold strong references to these
|
10
|
+
# objects, preventing garbage collection and causing memory leaks.
|
11
|
+
#
|
12
|
+
# Example scenario: A Rails controller that includes BreakerMachines::DSL
|
13
|
+
# is instantiated for each request. Without WeakRef, every controller
|
14
|
+
# instance would be kept in memory forever.
|
15
|
+
module DSL
|
16
|
+
extend ActiveSupport::Concern
|
17
|
+
|
18
|
+
# Track instances for each class that includes DSL
|
19
|
+
# Using WeakRef to allow garbage collection
|
20
|
+
@instance_registries = Concurrent::Map.new
|
21
|
+
|
22
|
+
class_methods do
|
23
|
+
# Get or create instance registry for this class
|
24
|
+
def instance_registry
|
25
|
+
# Access the module-level registry
|
26
|
+
registry = BreakerMachines::DSL.instance_variable_get(:@instance_registries)
|
27
|
+
registry.compute_if_absent(self) { Concurrent::Array.new }
|
28
|
+
end
|
29
|
+
|
30
|
+
# Clean up dead references in the registry
|
31
|
+
def cleanup_instance_registry
|
32
|
+
registry = instance_registry
|
33
|
+
# Concurrent::Array supports delete_if
|
34
|
+
registry.delete_if do |weak_ref|
|
35
|
+
weak_ref.__getobj__
|
36
|
+
false
|
37
|
+
rescue WeakRef::RefError
|
38
|
+
true
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def circuit(name, &block)
|
43
|
+
@circuits ||= {}
|
44
|
+
|
45
|
+
if block_given?
|
46
|
+
builder = CircuitBuilder.new
|
47
|
+
builder.instance_eval(&block)
|
48
|
+
@circuits[name] = builder.config
|
49
|
+
end
|
50
|
+
|
51
|
+
@circuits[name]
|
52
|
+
end
|
53
|
+
|
54
|
+
def circuits
|
55
|
+
# Start with parent circuits if available
|
56
|
+
base_circuits = if superclass.respond_to?(:circuits)
|
57
|
+
superclass.circuits.deep_dup
|
58
|
+
else
|
59
|
+
{}
|
60
|
+
end
|
61
|
+
|
62
|
+
# Merge with our own circuits
|
63
|
+
if @circuits
|
64
|
+
base_circuits.merge(@circuits)
|
65
|
+
else
|
66
|
+
base_circuits
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Get circuit definitions without sensitive data
|
71
|
+
def circuit_definitions
|
72
|
+
circuits.transform_values { |config| config.except(:owner, :storage, :metrics) }
|
73
|
+
end
|
74
|
+
|
75
|
+
# Reset all circuits for all instances of this class
|
76
|
+
def reset_all_circuits
|
77
|
+
cleanup_instance_registry # Clean up dead refs first
|
78
|
+
|
79
|
+
instance_registry.each do |weak_ref|
|
80
|
+
instance = weak_ref.__getobj__
|
81
|
+
circuit_instances = instance.instance_variable_get(:@circuit_instances)
|
82
|
+
circuit_instances&.each_value(&:reset)
|
83
|
+
rescue WeakRef::RefError
|
84
|
+
# Instance was garbage collected, skip it
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Get aggregated stats for all circuits of this class
|
89
|
+
def circuit_stats
|
90
|
+
stats = Hash.new { |h, k| h[k] = { total: 0, by_state: {} } }
|
91
|
+
cleanup_instance_registry # Clean up dead refs first
|
92
|
+
|
93
|
+
instance_registry.each do |weak_ref|
|
94
|
+
instance = weak_ref.__getobj__
|
95
|
+
circuit_instances = instance.instance_variable_get(:@circuit_instances)
|
96
|
+
next unless circuit_instances
|
97
|
+
|
98
|
+
circuit_instances.each do |name, circuit|
|
99
|
+
stats[name][:total] += 1
|
100
|
+
state = circuit.status_name
|
101
|
+
stats[name][:by_state][state] ||= 0
|
102
|
+
stats[name][:by_state][state] += 1
|
103
|
+
end
|
104
|
+
rescue WeakRef::RefError
|
105
|
+
# Instance was garbage collected, skip it
|
106
|
+
end
|
107
|
+
|
108
|
+
stats
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Use included callback to add instance tracking
|
113
|
+
def self.included(base)
|
114
|
+
super
|
115
|
+
|
116
|
+
# Hook into new to register instances
|
117
|
+
base.singleton_class.prepend(Module.new do
|
118
|
+
def new(...)
|
119
|
+
instance = super
|
120
|
+
instance_registry << WeakRef.new(instance)
|
121
|
+
instance
|
122
|
+
end
|
123
|
+
end)
|
124
|
+
end
|
125
|
+
|
126
|
+
def circuit(name)
|
127
|
+
self.class.circuits[name] ||= {}
|
128
|
+
@circuit_instances ||= {}
|
129
|
+
@circuit_instances[name] ||= Circuit.new(name, self.class.circuits[name].merge(owner: self))
|
130
|
+
end
|
131
|
+
|
132
|
+
# Get all circuit instances for this object
|
133
|
+
def circuit_instances
|
134
|
+
@circuit_instances || {}
|
135
|
+
end
|
136
|
+
|
137
|
+
# Get summary of all circuits for this instance
|
138
|
+
def circuits_summary
|
139
|
+
circuit_instances.transform_values(&:summary)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Get detailed information for all circuits
|
143
|
+
def circuits_report
|
144
|
+
circuit_instances.transform_values(&:to_h)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Reset all circuits for this instance
|
148
|
+
def reset_all_circuits
|
149
|
+
circuit_instances.each_value(&:reset)
|
150
|
+
end
|
151
|
+
|
152
|
+
# DSL builder for configuring circuit breakers with a fluent interface
|
153
|
+
class CircuitBuilder
|
154
|
+
attr_reader :config
|
155
|
+
|
156
|
+
def initialize
|
157
|
+
@config = {
|
158
|
+
failure_threshold: 5,
|
159
|
+
failure_window: 60,
|
160
|
+
success_threshold: 1,
|
161
|
+
timeout: nil,
|
162
|
+
reset_timeout: 60,
|
163
|
+
half_open_calls: 1,
|
164
|
+
exceptions: [StandardError],
|
165
|
+
storage: nil,
|
166
|
+
metrics: nil,
|
167
|
+
fallback: nil,
|
168
|
+
on_open: nil,
|
169
|
+
on_close: nil,
|
170
|
+
on_half_open: nil,
|
171
|
+
on_reject: nil,
|
172
|
+
notifications: [],
|
173
|
+
fiber_safe: BreakerMachines.config.fiber_safe
|
174
|
+
}
|
175
|
+
end
|
176
|
+
|
177
|
+
def threshold(failures: nil, within: 60, successes: nil)
|
178
|
+
@config[:failure_threshold] = failures if failures
|
179
|
+
@config[:failure_window] = within.to_i
|
180
|
+
@config[:success_threshold] = successes if successes
|
181
|
+
end
|
182
|
+
|
183
|
+
def reset_after(duration, jitter: nil)
|
184
|
+
@config[:reset_timeout] = duration.to_i
|
185
|
+
@config[:reset_timeout_jitter] = jitter if jitter
|
186
|
+
end
|
187
|
+
|
188
|
+
def timeout(duration)
|
189
|
+
@config[:timeout] = duration.to_i
|
190
|
+
end
|
191
|
+
|
192
|
+
def half_open_requests(count)
|
193
|
+
@config[:half_open_calls] = count
|
194
|
+
end
|
195
|
+
|
196
|
+
def storage(backend, **)
|
197
|
+
@config[:storage] = case backend
|
198
|
+
when :memory
|
199
|
+
Storage::Memory.new(**)
|
200
|
+
when :bucket_memory
|
201
|
+
Storage::BucketMemory.new(**)
|
202
|
+
when :redis
|
203
|
+
Storage::Redis.new(**)
|
204
|
+
when Class
|
205
|
+
backend.new(**)
|
206
|
+
else
|
207
|
+
backend
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def metrics(recorder = nil, &block)
|
212
|
+
@config[:metrics] = recorder || block
|
213
|
+
end
|
214
|
+
|
215
|
+
def fallback(value = nil, &block)
|
216
|
+
raise ArgumentError, 'Fallback requires either a value or a block' if value.nil? && !block_given?
|
217
|
+
|
218
|
+
fallback_value = block || value
|
219
|
+
|
220
|
+
if @config[:fallback].is_a?(Array)
|
221
|
+
@config[:fallback] << fallback_value
|
222
|
+
elsif @config[:fallback]
|
223
|
+
@config[:fallback] = [@config[:fallback], fallback_value]
|
224
|
+
else
|
225
|
+
@config[:fallback] = fallback_value
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def on_open(&block)
|
230
|
+
@config[:on_open] = block
|
231
|
+
end
|
232
|
+
|
233
|
+
def on_close(&block)
|
234
|
+
@config[:on_close] = block
|
235
|
+
end
|
236
|
+
|
237
|
+
def on_half_open(&block)
|
238
|
+
@config[:on_half_open] = block
|
239
|
+
end
|
240
|
+
|
241
|
+
def on_reject(&block)
|
242
|
+
@config[:on_reject] = block
|
243
|
+
end
|
244
|
+
|
245
|
+
def notify(service, url = nil, events: %i[open close], **options)
|
246
|
+
notification = {
|
247
|
+
via: service,
|
248
|
+
url: url,
|
249
|
+
events: Array(events),
|
250
|
+
options: options
|
251
|
+
}
|
252
|
+
@config[:notifications] << notification
|
253
|
+
end
|
254
|
+
|
255
|
+
def handle(*exceptions)
|
256
|
+
@config[:exceptions] = exceptions
|
257
|
+
end
|
258
|
+
|
259
|
+
def fiber_safe(enabled = true)
|
260
|
+
@config[:fiber_safe] = enabled
|
261
|
+
end
|
262
|
+
|
263
|
+
# Advanced features
|
264
|
+
def backends(list)
|
265
|
+
@config[:backends] = list
|
266
|
+
end
|
267
|
+
|
268
|
+
def parallel_calls(count, timeout: nil)
|
269
|
+
@config[:parallel_calls] = count
|
270
|
+
@config[:parallel_timeout] = timeout
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BreakerMachines
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
# Raised when attempting to use a circuit that is in the open state
|
7
|
+
class CircuitOpenError < Error
|
8
|
+
attr_reader :circuit_name, :opened_at
|
9
|
+
|
10
|
+
def initialize(circuit_name, opened_at = nil)
|
11
|
+
@circuit_name = circuit_name
|
12
|
+
@opened_at = opened_at
|
13
|
+
super("Circuit '#{circuit_name}' is open")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Raised when a circuit-protected call exceeds the configured timeout
|
18
|
+
class CircuitTimeoutError < Error
|
19
|
+
attr_reader :circuit_name, :timeout
|
20
|
+
|
21
|
+
def initialize(circuit_name, timeout)
|
22
|
+
@circuit_name = circuit_name
|
23
|
+
@timeout = timeout
|
24
|
+
super("Circuit '#{circuit_name}' timed out after #{timeout}s")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class ConfigurationError < Error; end
|
29
|
+
class StorageError < Error; end
|
30
|
+
end
|