breaker_machines 0.3.0 → 0.5.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 +4 -4
- data/README.md +25 -3
- data/lib/breaker_machines/async_circuit.rb +47 -0
- data/lib/breaker_machines/async_support.rb +7 -6
- 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 +58 -0
- data/lib/breaker_machines/circuit/callbacks.rb +7 -12
- data/lib/breaker_machines/circuit/configuration.rb +6 -26
- data/lib/breaker_machines/circuit/coordinated_state_management.rb +117 -0
- data/lib/breaker_machines/circuit/execution.rb +4 -8
- data/lib/breaker_machines/circuit/hedged_execution.rb +115 -0
- data/lib/breaker_machines/circuit/introspection.rb +36 -20
- data/lib/breaker_machines/circuit/state_callbacks.rb +60 -0
- data/lib/breaker_machines/circuit/state_management.rb +15 -61
- data/lib/breaker_machines/circuit.rb +1 -8
- data/lib/breaker_machines/circuit_group.rb +153 -0
- data/lib/breaker_machines/console.rb +12 -12
- 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 +28 -241
- data/lib/breaker_machines/errors.rb +20 -0
- data/lib/breaker_machines/hedged_async_support.rb +29 -36
- data/lib/breaker_machines/registry.rb +3 -3
- data/lib/breaker_machines/storage/backend_state.rb +69 -0
- data/lib/breaker_machines/storage/bucket_memory.rb +3 -3
- data/lib/breaker_machines/storage/cache.rb +3 -3
- data/lib/breaker_machines/storage/fallback_chain.rb +56 -70
- data/lib/breaker_machines/storage/memory.rb +3 -3
- data/lib/breaker_machines/types.rb +41 -0
- data/lib/breaker_machines/version.rb +1 -1
- data/lib/breaker_machines.rb +29 -0
- metadata +21 -7
- data/lib/breaker_machines/hedged_execution.rb +0 -113
@@ -89,7 +89,7 @@ module BreakerMachines
|
|
89
89
|
return execute_call_async(&block)
|
90
90
|
end
|
91
91
|
|
92
|
-
start_time = monotonic_time
|
92
|
+
start_time = BreakerMachines.monotonic_time
|
93
93
|
|
94
94
|
begin
|
95
95
|
# IMPORTANT: We do NOT implement forceful timeouts as they are inherently unsafe
|
@@ -113,11 +113,11 @@ module BreakerMachines
|
|
113
113
|
block.call
|
114
114
|
end
|
115
115
|
|
116
|
-
record_success(monotonic_time - start_time)
|
116
|
+
record_success(BreakerMachines.monotonic_time - start_time)
|
117
117
|
handle_success
|
118
118
|
result
|
119
119
|
rescue *@config[:exceptions] => e
|
120
|
-
record_failure(monotonic_time - start_time, e)
|
120
|
+
record_failure(BreakerMachines.monotonic_time - start_time, e)
|
121
121
|
handle_failure
|
122
122
|
raise unless @config[:fallback]
|
123
123
|
|
@@ -217,7 +217,7 @@ module BreakerMachines
|
|
217
217
|
end
|
218
218
|
|
219
219
|
def record_failure(duration, error = nil)
|
220
|
-
@last_failure_at.value = monotonic_time
|
220
|
+
@last_failure_at.value = BreakerMachines.monotonic_time
|
221
221
|
@last_error.value = error if error
|
222
222
|
@metrics&.record_failure(@name, duration)
|
223
223
|
@storage&.record_failure(@name, duration)
|
@@ -226,10 +226,6 @@ module BreakerMachines
|
|
226
226
|
@storage.record_event_with_details(@name, :failure, duration,
|
227
227
|
error: error)
|
228
228
|
end
|
229
|
-
|
230
|
-
def monotonic_time
|
231
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
232
|
-
end
|
233
229
|
end
|
234
230
|
end
|
235
231
|
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'concurrent'
|
4
|
+
require 'timeout'
|
5
|
+
|
6
|
+
module BreakerMachines
|
7
|
+
class Circuit
|
8
|
+
# HedgedExecution provides hedged request functionality for circuit breakers
|
9
|
+
# Hedged requests improve latency by sending duplicate requests to multiple backends
|
10
|
+
# and returning the first successful response
|
11
|
+
module HedgedExecution
|
12
|
+
extend ActiveSupport::Concern
|
13
|
+
|
14
|
+
# Execute a hedged request pattern
|
15
|
+
def execute_hedged(&)
|
16
|
+
return execute_single_hedged(&) unless @config[:backends]&.any?
|
17
|
+
|
18
|
+
execute_multi_backend_hedged
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# Execute hedged request with a single backend (original block)
|
24
|
+
def execute_single_hedged(&block)
|
25
|
+
return yield unless hedged_requests_enabled?
|
26
|
+
|
27
|
+
max_requests = @config[:max_hedged_requests] || 2
|
28
|
+
delay_ms = @config[:hedging_delay] || 50
|
29
|
+
|
30
|
+
if @config[:fiber_safe]
|
31
|
+
execute_hedged_async(Array.new(max_requests) { block }, delay_ms)
|
32
|
+
else
|
33
|
+
execute_hedged_sync(Array.new(max_requests) { block }, delay_ms)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Execute hedged requests across multiple backends
|
38
|
+
def execute_multi_backend_hedged
|
39
|
+
backends = @config[:backends]
|
40
|
+
return backends.first.call if backends.size == 1
|
41
|
+
|
42
|
+
if @config[:fiber_safe]
|
43
|
+
execute_hedged_async(backends, @config[:hedging_delay] || 0)
|
44
|
+
else
|
45
|
+
execute_hedged_sync(backends, @config[:hedging_delay] || 0)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Synchronous hedged execution using threads
|
50
|
+
def execute_hedged_sync(callables, delay_ms)
|
51
|
+
result_queue = Queue.new
|
52
|
+
error_queue = Queue.new
|
53
|
+
threads = []
|
54
|
+
cancelled = Concurrent::AtomicBoolean.new(false)
|
55
|
+
|
56
|
+
callables.each_with_index do |callable, index|
|
57
|
+
# Add delay for hedge requests (not the first one)
|
58
|
+
sleep(delay_ms / 1000.0) if index.positive? && delay_ms.positive?
|
59
|
+
|
60
|
+
# Skip if already got a result
|
61
|
+
break if cancelled.value
|
62
|
+
|
63
|
+
threads << Thread.new do
|
64
|
+
unless cancelled.value
|
65
|
+
begin
|
66
|
+
result = callable.call
|
67
|
+
result_queue << result unless cancelled.value
|
68
|
+
cancelled.value = true
|
69
|
+
rescue StandardError => e
|
70
|
+
error_queue << e unless cancelled.value
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Wait for first result or all errors
|
77
|
+
begin
|
78
|
+
Timeout.timeout(@config[:timeout] || 30) do
|
79
|
+
# Check for successful result
|
80
|
+
loop do
|
81
|
+
unless result_queue.empty?
|
82
|
+
result = result_queue.pop
|
83
|
+
cancelled.value = true
|
84
|
+
return result
|
85
|
+
end
|
86
|
+
|
87
|
+
# Check if all requests failed
|
88
|
+
raise error_queue.pop if error_queue.size >= callables.size
|
89
|
+
|
90
|
+
# Small sleep to prevent busy waiting
|
91
|
+
sleep 0.001
|
92
|
+
end
|
93
|
+
end
|
94
|
+
ensure
|
95
|
+
# Cancel remaining threads
|
96
|
+
cancelled.value = true
|
97
|
+
threads.each(&:kill)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Async hedged execution (requires async support)
|
102
|
+
def execute_hedged_async(callables, delay_ms)
|
103
|
+
# This will be implemented when async support is loaded
|
104
|
+
# For now, fall back to sync implementation
|
105
|
+
return execute_hedged_sync(callables, delay_ms) unless respond_to?(:execute_hedged_with_async)
|
106
|
+
|
107
|
+
execute_hedged_with_async(callables, delay_ms)
|
108
|
+
end
|
109
|
+
|
110
|
+
def hedged_requests_enabled?
|
111
|
+
@config[:hedged_requests] == true
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -6,13 +6,14 @@ module BreakerMachines
|
|
6
6
|
# and generating human-readable summaries of circuit status.
|
7
7
|
module Introspection
|
8
8
|
extend ActiveSupport::Concern
|
9
|
+
|
9
10
|
# State check methods are automatically generated by state_machines:
|
10
11
|
# - open? (returns true when status == :open)
|
11
12
|
# - closed? (returns true when status == :closed)
|
12
13
|
# - half_open? (returns true when status == :half_open)
|
13
14
|
|
14
15
|
def stats
|
15
|
-
|
16
|
+
BreakerMachines::Stats.new(
|
16
17
|
state: status_name,
|
17
18
|
failure_count: @storage.failure_count(@name, @config[:failure_window]),
|
18
19
|
success_count: @storage.success_count(@name, @config[:failure_window]),
|
@@ -20,7 +21,7 @@ module BreakerMachines
|
|
20
21
|
opened_at: @opened_at.value,
|
21
22
|
half_open_attempts: @half_open_attempts.value,
|
22
23
|
half_open_successes: @half_open_successes.value
|
23
|
-
|
24
|
+
)
|
24
25
|
end
|
25
26
|
|
26
27
|
def configuration
|
@@ -36,41 +37,56 @@ module BreakerMachines
|
|
36
37
|
end
|
37
38
|
|
38
39
|
def to_h
|
39
|
-
{
|
40
|
+
hash = {
|
40
41
|
name: @name,
|
41
42
|
state: status_name,
|
42
|
-
stats: stats,
|
43
|
+
stats: stats.to_h,
|
43
44
|
config: configuration.except(:owner, :storage, :metrics),
|
44
45
|
event_log: event_log || [],
|
45
|
-
last_error: last_error_info
|
46
|
+
last_error: last_error_info&.to_h
|
46
47
|
}
|
48
|
+
|
49
|
+
# Add cascade-specific information if this is a cascading circuit
|
50
|
+
hash[:cascade_info] = cascade_info.to_h if is_a?(CascadingCircuit)
|
51
|
+
|
52
|
+
hash
|
47
53
|
end
|
48
54
|
|
49
55
|
def summary
|
50
|
-
case status_name
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
56
|
+
base_summary = case status_name
|
57
|
+
when :closed
|
58
|
+
"Circuit '#{@name}' is CLOSED. #{stats.failure_count} failures recorded."
|
59
|
+
when :open
|
60
|
+
# Calculate time remaining until reset
|
61
|
+
time_since_open = BreakerMachines.monotonic_time - @opened_at.value
|
62
|
+
time_until_reset = @config[:reset_timeout] - time_since_open
|
63
|
+
reset_time = time_until_reset.seconds.from_now
|
64
|
+
opened_time = time_since_open.seconds.ago
|
65
|
+
error_info = @last_error.value ? " The last error was #{@last_error.value.class}." : ''
|
66
|
+
"Circuit '#{@name}' is OPEN until #{reset_time}. " \
|
67
|
+
"It opened at #{opened_time} after #{@config[:failure_threshold]} failures.#{error_info}"
|
68
|
+
when :half_open
|
69
|
+
"Circuit '#{@name}' is HALF-OPEN. Testing with limited requests " \
|
70
|
+
"(#{@half_open_attempts.value}/#{@config[:half_open_calls]} attempts)."
|
71
|
+
end
|
72
|
+
|
73
|
+
# Add cascade information if this is a cascading circuit
|
74
|
+
if is_a?(CascadingCircuit) && @dependent_circuits.any?
|
75
|
+
base_summary += " [Cascades to: #{@dependent_circuits.join(', ')}]"
|
62
76
|
end
|
77
|
+
|
78
|
+
base_summary
|
63
79
|
end
|
64
80
|
|
65
81
|
def last_error_info
|
66
82
|
error = @last_error.value
|
67
83
|
return nil unless error
|
68
84
|
|
69
|
-
|
70
|
-
|
85
|
+
BreakerMachines::ErrorInfo.new(
|
86
|
+
error_class: error.class.name,
|
71
87
|
message: error.message,
|
72
88
|
occurred_at: @last_failure_at.value
|
73
|
-
|
89
|
+
)
|
74
90
|
end
|
75
91
|
end
|
76
92
|
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BreakerMachines
|
4
|
+
class Circuit
|
5
|
+
# StateCallbacks provides the common callback methods shared by all state management modules
|
6
|
+
module StateCallbacks
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def on_circuit_open
|
12
|
+
@opened_at.value = BreakerMachines.monotonic_time
|
13
|
+
@storage&.set_status(@name, :open, @opened_at.value)
|
14
|
+
if @storage.respond_to?(:record_event_with_details)
|
15
|
+
@storage.record_event_with_details(@name, :state_change, 0,
|
16
|
+
new_state: :open)
|
17
|
+
end
|
18
|
+
invoke_callback(:on_open)
|
19
|
+
BreakerMachines.instrument('opened', circuit: @name)
|
20
|
+
end
|
21
|
+
|
22
|
+
def on_circuit_close
|
23
|
+
@opened_at.value = nil
|
24
|
+
@last_error.value = nil
|
25
|
+
@last_failure_at.value = nil
|
26
|
+
@storage&.set_status(@name, :closed)
|
27
|
+
if @storage.respond_to?(:record_event_with_details)
|
28
|
+
@storage.record_event_with_details(@name, :state_change, 0,
|
29
|
+
new_state: :closed)
|
30
|
+
end
|
31
|
+
invoke_callback(:on_close)
|
32
|
+
BreakerMachines.instrument('closed', circuit: @name)
|
33
|
+
end
|
34
|
+
|
35
|
+
def on_circuit_half_open
|
36
|
+
@half_open_attempts.value = 0
|
37
|
+
@half_open_successes.value = 0
|
38
|
+
@storage&.set_status(@name, :half_open)
|
39
|
+
if @storage.respond_to?(:record_event_with_details)
|
40
|
+
@storage.record_event_with_details(@name, :state_change, 0,
|
41
|
+
new_state: :half_open)
|
42
|
+
end
|
43
|
+
invoke_callback(:on_half_open)
|
44
|
+
BreakerMachines.instrument('half_opened', circuit: @name)
|
45
|
+
end
|
46
|
+
|
47
|
+
def reset_timeout_elapsed?
|
48
|
+
return false unless @opened_at.value
|
49
|
+
|
50
|
+
# Add jitter to prevent thundering herd
|
51
|
+
jitter_factor = @config[:reset_timeout_jitter] || 0.25
|
52
|
+
# Calculate random jitter between -jitter_factor and +jitter_factor
|
53
|
+
jitter_multiplier = 1.0 + (((rand * 2) - 1) * jitter_factor)
|
54
|
+
timeout_with_jitter = @config[:reset_timeout] * jitter_multiplier
|
55
|
+
|
56
|
+
BreakerMachines.monotonic_time - @opened_at.value >= timeout_with_jitter
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -6,6 +6,7 @@ module BreakerMachines
|
|
6
6
|
# managing transitions between closed, open, and half-open states.
|
7
7
|
module StateManagement
|
8
8
|
extend ActiveSupport::Concern
|
9
|
+
|
9
10
|
included do
|
10
11
|
state_machine :status, initial: :closed do
|
11
12
|
event :trip do
|
@@ -19,6 +20,7 @@ module BreakerMachines
|
|
19
20
|
|
20
21
|
event :reset do
|
21
22
|
transition %i[open half_open] => :closed
|
23
|
+
transition closed: :closed
|
22
24
|
end
|
23
25
|
|
24
26
|
event :force_open do
|
@@ -29,77 +31,29 @@ module BreakerMachines
|
|
29
31
|
transition any => :closed
|
30
32
|
end
|
31
33
|
|
32
|
-
|
34
|
+
event :hard_reset do
|
35
|
+
transition any => :closed
|
36
|
+
end
|
37
|
+
|
38
|
+
before_transition on: :hard_reset do |circuit|
|
39
|
+
circuit.storage.clear(circuit.name) if circuit.storage
|
40
|
+
circuit.half_open_attempts.value = 0
|
41
|
+
circuit.half_open_successes.value = 0
|
42
|
+
end
|
43
|
+
|
44
|
+
after_transition to: :open do |circuit|
|
33
45
|
circuit.send(:on_circuit_open)
|
34
46
|
end
|
35
47
|
|
36
|
-
after_transition
|
48
|
+
after_transition to: :closed do |circuit|
|
37
49
|
circuit.send(:on_circuit_close)
|
38
50
|
end
|
39
51
|
|
40
|
-
after_transition open: :half_open do |circuit|
|
52
|
+
after_transition from: :open, to: :half_open do |circuit|
|
41
53
|
circuit.send(:on_circuit_half_open)
|
42
54
|
end
|
43
55
|
end
|
44
56
|
end
|
45
|
-
|
46
|
-
private
|
47
|
-
|
48
|
-
def on_circuit_open
|
49
|
-
@opened_at.value = monotonic_time
|
50
|
-
@storage&.set_status(@name, :open, @opened_at.value)
|
51
|
-
if @storage.respond_to?(:record_event_with_details)
|
52
|
-
@storage.record_event_with_details(@name, :state_change, 0,
|
53
|
-
new_state: :open)
|
54
|
-
end
|
55
|
-
invoke_callback(:on_open)
|
56
|
-
BreakerMachines.instrument('opened', circuit: @name)
|
57
|
-
end
|
58
|
-
|
59
|
-
def on_circuit_close
|
60
|
-
@opened_at.value = nil
|
61
|
-
@last_error.value = nil
|
62
|
-
@last_failure_at.value = nil
|
63
|
-
@storage&.set_status(@name, :closed)
|
64
|
-
if @storage.respond_to?(:record_event_with_details)
|
65
|
-
@storage.record_event_with_details(@name, :state_change, 0,
|
66
|
-
new_state: :closed)
|
67
|
-
end
|
68
|
-
invoke_callback(:on_close)
|
69
|
-
BreakerMachines.instrument('closed', circuit: @name)
|
70
|
-
end
|
71
|
-
|
72
|
-
def on_circuit_half_open
|
73
|
-
@half_open_attempts.value = 0
|
74
|
-
@half_open_successes.value = 0
|
75
|
-
@storage&.set_status(@name, :half_open)
|
76
|
-
if @storage.respond_to?(:record_event_with_details)
|
77
|
-
@storage.record_event_with_details(@name, :state_change, 0,
|
78
|
-
new_state: :half_open)
|
79
|
-
end
|
80
|
-
invoke_callback(:on_half_open)
|
81
|
-
BreakerMachines.instrument('half_opened', circuit: @name)
|
82
|
-
end
|
83
|
-
|
84
|
-
def restore_status_from_storage
|
85
|
-
stored_status = @storage.get_status(@name)
|
86
|
-
return unless stored_status
|
87
|
-
|
88
|
-
self.status = stored_status[:status].to_s
|
89
|
-
@opened_at.value = stored_status[:opened_at] if stored_status[:opened_at]
|
90
|
-
end
|
91
|
-
|
92
|
-
def reset_timeout_elapsed?
|
93
|
-
return false unless @opened_at.value
|
94
|
-
|
95
|
-
# Add jitter to prevent thundering herd
|
96
|
-
jitter_factor = @config[:reset_timeout_jitter] || 0.25
|
97
|
-
# Calculate random jitter between -jitter_factor and +jitter_factor
|
98
|
-
jitter_multiplier = 1.0 + (((rand * 2) - 1) * jitter_factor)
|
99
|
-
timeout_with_jitter = @config[:reset_timeout] * jitter_multiplier
|
100
|
-
|
101
|
-
monotonic_time - @opened_at.value >= timeout_with_jitter
|
102
|
-
end
|
103
57
|
end
|
104
58
|
end
|
105
59
|
end
|
@@ -1,15 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'state_machines'
|
4
|
-
require 'concurrent-ruby'
|
5
|
-
|
6
3
|
module BreakerMachines
|
7
4
|
class Circuit
|
5
|
+
include Circuit::Base
|
8
6
|
include StateManagement
|
9
|
-
include Configuration
|
10
|
-
include Execution
|
11
|
-
include HedgedExecution
|
12
|
-
include Introspection
|
13
|
-
include Callbacks
|
14
7
|
end
|
15
8
|
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BreakerMachines
|
4
|
+
# CircuitGroup provides coordinated management of multiple related circuits
|
5
|
+
# with support for dependencies, shared configuration, and group-wide operations
|
6
|
+
class CircuitGroup
|
7
|
+
include BreakerMachines::DSL
|
8
|
+
|
9
|
+
attr_reader :name, :circuits, :dependencies, :config
|
10
|
+
|
11
|
+
def initialize(name, config = {})
|
12
|
+
@name = name
|
13
|
+
@config = config
|
14
|
+
@circuits = {}
|
15
|
+
@dependencies = {}
|
16
|
+
@async_mode = config[:async_mode] || false
|
17
|
+
end
|
18
|
+
|
19
|
+
# Define a circuit within this group with optional dependencies
|
20
|
+
# @param name [Symbol] Circuit name
|
21
|
+
# @param options [Hash] Circuit configuration
|
22
|
+
# @option options [Symbol, Array<Symbol>] :depends_on Other circuits this one depends on
|
23
|
+
# @option options [Proc] :guard_with Additional guard conditions
|
24
|
+
def circuit(name, options = {}, &)
|
25
|
+
depends_on = Array(options.delete(:depends_on))
|
26
|
+
guard_proc = options.delete(:guard_with)
|
27
|
+
|
28
|
+
# Add group-wide defaults
|
29
|
+
circuit_config = @config.merge(options)
|
30
|
+
|
31
|
+
# Create appropriate circuit type
|
32
|
+
circuit_class = if options[:cascades_to] || depends_on.any?
|
33
|
+
BreakerMachines::CascadingCircuit
|
34
|
+
elsif @async_mode
|
35
|
+
BreakerMachines::AsyncCircuit
|
36
|
+
else
|
37
|
+
BreakerMachines::Circuit
|
38
|
+
end
|
39
|
+
|
40
|
+
# Build the circuit
|
41
|
+
circuit_instance = if block_given?
|
42
|
+
builder = BreakerMachines::DSL::CircuitBuilder.new
|
43
|
+
builder.instance_eval(&)
|
44
|
+
built_config = builder.config.merge(circuit_config)
|
45
|
+
circuit_class.new(full_circuit_name(name), built_config)
|
46
|
+
else
|
47
|
+
circuit_class.new(full_circuit_name(name), circuit_config)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Store dependencies and guards
|
51
|
+
if depends_on.any? || guard_proc
|
52
|
+
@dependencies[name] = {
|
53
|
+
depends_on: depends_on,
|
54
|
+
guard: guard_proc
|
55
|
+
}
|
56
|
+
|
57
|
+
# Wrap the circuit with dependency checking
|
58
|
+
circuit_instance = DependencyWrapper.new(circuit_instance, self, name)
|
59
|
+
end
|
60
|
+
|
61
|
+
@circuits[name] = circuit_instance
|
62
|
+
BreakerMachines.register(circuit_instance)
|
63
|
+
circuit_instance
|
64
|
+
end
|
65
|
+
|
66
|
+
# Get a circuit by name
|
67
|
+
def [](name)
|
68
|
+
@circuits[name]
|
69
|
+
end
|
70
|
+
|
71
|
+
# Check if all circuits in the group are healthy
|
72
|
+
def all_healthy?
|
73
|
+
@circuits.values.all? { |circuit| circuit.closed? || circuit.half_open? }
|
74
|
+
end
|
75
|
+
|
76
|
+
# Check if any circuit in the group is open
|
77
|
+
def any_open?
|
78
|
+
@circuits.values.any?(&:open?)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Get status of all circuits
|
82
|
+
def status
|
83
|
+
@circuits.transform_values(&:status_name)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Reset all circuits in the group
|
87
|
+
def reset_all!
|
88
|
+
@circuits.each_value(&:reset!)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Force open all circuits
|
92
|
+
def trip_all!
|
93
|
+
@circuits.each_value(&:force_open!)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Check dependencies for a specific circuit
|
97
|
+
def dependencies_met?(circuit_name)
|
98
|
+
deps = @dependencies[circuit_name]
|
99
|
+
return true unless deps
|
100
|
+
|
101
|
+
depends_on = deps[:depends_on]
|
102
|
+
guard = deps[:guard]
|
103
|
+
|
104
|
+
# Check circuit dependencies recursively
|
105
|
+
dependencies_healthy = depends_on.all? do |dep_name|
|
106
|
+
dep_circuit = @circuits[dep_name]
|
107
|
+
# Circuit must exist, be healthy, AND have its own dependencies met
|
108
|
+
dep_circuit && (dep_circuit.closed? || dep_circuit.half_open?) && dependencies_met?(dep_name)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Check custom guard
|
112
|
+
guard_passed = guard ? guard.call : true
|
113
|
+
|
114
|
+
dependencies_healthy && guard_passed
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def full_circuit_name(name)
|
120
|
+
"#{@name}.#{name}"
|
121
|
+
end
|
122
|
+
|
123
|
+
# Wrapper to enforce dependencies
|
124
|
+
class DependencyWrapper < SimpleDelegator
|
125
|
+
def initialize(circuit, group, name)
|
126
|
+
super(circuit)
|
127
|
+
@group = group
|
128
|
+
@name = name
|
129
|
+
end
|
130
|
+
|
131
|
+
def call(&)
|
132
|
+
unless @group.dependencies_met?(@name)
|
133
|
+
raise BreakerMachines::CircuitDependencyError.new(__getobj__.name,
|
134
|
+
"Dependencies not met for circuit #{@name}")
|
135
|
+
end
|
136
|
+
|
137
|
+
__getobj__.call(&)
|
138
|
+
end
|
139
|
+
|
140
|
+
def attempt_recovery!
|
141
|
+
return false unless @group.dependencies_met?(@name)
|
142
|
+
|
143
|
+
__getobj__.attempt_recovery!
|
144
|
+
end
|
145
|
+
|
146
|
+
def reset!
|
147
|
+
return false unless @group.dependencies_met?(@name)
|
148
|
+
|
149
|
+
__getobj__.reset!
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -114,9 +114,9 @@ module BreakerMachines
|
|
114
114
|
|
115
115
|
printf "%-20s %-12s %-10d %-10d %s\n",
|
116
116
|
circuit.name,
|
117
|
-
colorize_state(stats
|
118
|
-
stats
|
119
|
-
stats
|
117
|
+
colorize_state(stats.state),
|
118
|
+
stats.failure_count,
|
119
|
+
stats.success_count,
|
120
120
|
error_info
|
121
121
|
end
|
122
122
|
|
@@ -167,22 +167,22 @@ module BreakerMachines
|
|
167
167
|
stats = circuit.stats
|
168
168
|
config = circuit.configuration
|
169
169
|
|
170
|
-
puts "Current State: #{colorize_state(stats
|
171
|
-
puts "Failure Count: #{stats
|
172
|
-
puts "Success Count: #{stats
|
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
173
|
|
174
|
-
if stats
|
175
|
-
puts "Opened At: #{Time.at(stats
|
176
|
-
reset_time = Time.at(stats
|
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
177
|
puts "Reset At: #{reset_time} (in #{(reset_time - Time.now).to_i}s)"
|
178
178
|
end
|
179
179
|
|
180
180
|
if circuit.last_error
|
181
181
|
error_info = circuit.last_error_info
|
182
182
|
puts "\nLast Error:"
|
183
|
-
puts " Class: #{error_info
|
184
|
-
puts " Message: #{error_info
|
185
|
-
puts " Time: #{Time.at(error_info
|
183
|
+
puts " Class: #{error_info.error_class}"
|
184
|
+
puts " Message: #{error_info.message}"
|
185
|
+
puts " Time: #{Time.at(error_info.occurred_at)}"
|
186
186
|
end
|
187
187
|
|
188
188
|
puts "\nConfiguration:"
|
@@ -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
|