breaker_machines 0.9.2-x86_64-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,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BreakerMachines
|
|
4
|
+
class Circuit
|
|
5
|
+
# Introspection provides methods for inspecting circuit state, statistics,
|
|
6
|
+
# and generating human-readable summaries of circuit status.
|
|
7
|
+
module Introspection
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
# State check methods are automatically generated by state_machines:
|
|
11
|
+
# - open? (returns true when status == :open)
|
|
12
|
+
# - closed? (returns true when status == :closed)
|
|
13
|
+
# - half_open? (returns true when status == :half_open)
|
|
14
|
+
|
|
15
|
+
def stats
|
|
16
|
+
BreakerMachines::Stats.new(
|
|
17
|
+
state: status_name,
|
|
18
|
+
failure_count: @storage.failure_count(@name, @config[:failure_window]),
|
|
19
|
+
success_count: @storage.success_count(@name, @config[:failure_window]),
|
|
20
|
+
last_failure_at: @last_failure_at.value,
|
|
21
|
+
opened_at: @opened_at.value,
|
|
22
|
+
half_open_attempts: @half_open_attempts.value,
|
|
23
|
+
half_open_successes: @half_open_successes.value
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def configuration
|
|
28
|
+
@config.dup
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def event_log(limit: 20)
|
|
32
|
+
@storage.event_log(@name, limit) if @storage.respond_to?(:event_log)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def last_error
|
|
36
|
+
@last_error.value
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_h
|
|
40
|
+
hash = {
|
|
41
|
+
name: @name,
|
|
42
|
+
state: status_name,
|
|
43
|
+
stats: stats.to_h,
|
|
44
|
+
config: configuration.except(:owner, :storage, :metrics),
|
|
45
|
+
event_log: event_log || [],
|
|
46
|
+
last_error: last_error_info&.to_h
|
|
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
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def summary
|
|
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(', ')}]"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
base_summary
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def last_error_info
|
|
82
|
+
error = @last_error.value
|
|
83
|
+
return nil unless error
|
|
84
|
+
|
|
85
|
+
BreakerMachines::ErrorInfo.new(
|
|
86
|
+
error_class: error.class.name,
|
|
87
|
+
message: error.message,
|
|
88
|
+
occurred_at: @last_failure_at.value
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BreakerMachines
|
|
4
|
+
class Circuit
|
|
5
|
+
# Native circuit breaker implementation using Rust FFI
|
|
6
|
+
#
|
|
7
|
+
# This provides a high-performance circuit breaker with state machine logic
|
|
8
|
+
# implemented in Rust. It's fully compatible with the Ruby circuit API but
|
|
9
|
+
# significantly faster for high-throughput scenarios.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic usage
|
|
12
|
+
# circuit = BreakerMachines::Circuit::Native.new('api_calls',
|
|
13
|
+
# failure_threshold: 5,
|
|
14
|
+
# failure_window_secs: 60.0
|
|
15
|
+
# )
|
|
16
|
+
#
|
|
17
|
+
# circuit.call { api.fetch_data }
|
|
18
|
+
class Native
|
|
19
|
+
# @return [String] Circuit name
|
|
20
|
+
attr_reader :name
|
|
21
|
+
|
|
22
|
+
# @return [Hash] Circuit configuration
|
|
23
|
+
attr_reader :config
|
|
24
|
+
|
|
25
|
+
# Create a new native circuit breaker
|
|
26
|
+
#
|
|
27
|
+
# @param name [String] Circuit name
|
|
28
|
+
# @param options [Hash] Configuration options
|
|
29
|
+
# @option options [Integer] :failure_threshold Number of failures to open circuit (default: 5)
|
|
30
|
+
# @option options [Float] :failure_window_secs Time window for counting failures (default: 60.0)
|
|
31
|
+
# @option options [Float] :half_open_timeout_secs Timeout before attempting reset (default: 30.0)
|
|
32
|
+
# @option options [Integer] :success_threshold Successes needed to close from half-open (default: 2)
|
|
33
|
+
# @option options [Boolean] :auto_register Register with global registry (default: true)
|
|
34
|
+
def initialize(name, options = {})
|
|
35
|
+
unless BreakerMachines.native_available?
|
|
36
|
+
raise BreakerMachines::ConfigurationError,
|
|
37
|
+
'Native extension not available. Install with native support or use Circuit::Ruby'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
@name = name
|
|
41
|
+
@config = default_config.merge(options)
|
|
42
|
+
|
|
43
|
+
# Create the native circuit breaker
|
|
44
|
+
@native_circuit = BreakerMachinesNative::Circuit.new(
|
|
45
|
+
name,
|
|
46
|
+
{
|
|
47
|
+
failure_threshold: @config[:failure_threshold],
|
|
48
|
+
failure_window_secs: @config[:failure_window_secs],
|
|
49
|
+
half_open_timeout_secs: @config[:half_open_timeout_secs],
|
|
50
|
+
success_threshold: @config[:success_threshold]
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Register with global registry unless disabled
|
|
55
|
+
BreakerMachines::Registry.instance.register(self) unless @config[:auto_register] == false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Execute a block with circuit breaker protection
|
|
59
|
+
#
|
|
60
|
+
# @yield Block to execute
|
|
61
|
+
# @return Result of the block
|
|
62
|
+
# @raise [CircuitOpenError] if circuit is open
|
|
63
|
+
def call
|
|
64
|
+
raise CircuitOpenError, "Circuit '#{@name}' is open" if open?
|
|
65
|
+
|
|
66
|
+
start_time = BreakerMachines.monotonic_time
|
|
67
|
+
begin
|
|
68
|
+
result = yield
|
|
69
|
+
duration = BreakerMachines.monotonic_time - start_time
|
|
70
|
+
@native_circuit.record_success(duration)
|
|
71
|
+
result
|
|
72
|
+
rescue StandardError => _e
|
|
73
|
+
duration = BreakerMachines.monotonic_time - start_time
|
|
74
|
+
@native_circuit.record_failure(duration)
|
|
75
|
+
raise
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Check if circuit is open
|
|
80
|
+
# @return [Boolean]
|
|
81
|
+
def open?
|
|
82
|
+
@native_circuit.is_open
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Check if circuit is closed
|
|
86
|
+
# @return [Boolean]
|
|
87
|
+
def closed?
|
|
88
|
+
@native_circuit.is_closed
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Get current state name
|
|
92
|
+
# @return [String] 'open' or 'closed'
|
|
93
|
+
def state
|
|
94
|
+
@native_circuit.state_name
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Reset the circuit (clear all events)
|
|
98
|
+
def reset!
|
|
99
|
+
@native_circuit.reset
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Get circuit status for inspection
|
|
103
|
+
# @return [Hash] Status information
|
|
104
|
+
def status
|
|
105
|
+
{
|
|
106
|
+
name: @name,
|
|
107
|
+
state: state,
|
|
108
|
+
open: open?,
|
|
109
|
+
closed: closed?,
|
|
110
|
+
config: @config
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def default_config
|
|
117
|
+
{
|
|
118
|
+
failure_threshold: 5,
|
|
119
|
+
failure_window_secs: 60.0,
|
|
120
|
+
half_open_timeout_secs: 30.0,
|
|
121
|
+
success_threshold: 2,
|
|
122
|
+
auto_register: true
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
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 using ChronoMachines
|
|
51
|
+
# This matches the Rust implementation which uses chrono-machines for jitter
|
|
52
|
+
timeout_with_jitter = if (jitter_factor = @config[:reset_timeout_jitter]) && jitter_factor.positive?
|
|
53
|
+
calculate_timeout_with_jitter(@config[:reset_timeout], jitter_factor)
|
|
54
|
+
else
|
|
55
|
+
@config[:reset_timeout]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
BreakerMachines.monotonic_time - @opened_at.value >= timeout_with_jitter
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Calculate timeout with jitter using ChronoMachines algorithm
|
|
62
|
+
# Matches the Rust implementation: timeout * (1 - jitter + rand * jitter)
|
|
63
|
+
def calculate_timeout_with_jitter(base_timeout, jitter_factor)
|
|
64
|
+
# Use full jitter strategy from ChronoMachines
|
|
65
|
+
# Formula: base * (1 - jitter + rand * jitter)
|
|
66
|
+
# This gives values in range [base * (1-jitter), base]
|
|
67
|
+
normalized_jitter = [jitter_factor.to_f, 1.0].min
|
|
68
|
+
base_timeout * (1.0 - normalized_jitter + (rand * normalized_jitter))
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BreakerMachines
|
|
4
|
+
class Circuit
|
|
5
|
+
# StateManagement provides the state machine functionality for circuit breakers,
|
|
6
|
+
# managing transitions between closed, open, and half-open states.
|
|
7
|
+
module StateManagement
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
state_machine :status, initial: :closed do
|
|
12
|
+
event :trip do
|
|
13
|
+
transition closed: :open
|
|
14
|
+
transition half_open: :open
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
event :attempt_recovery do
|
|
18
|
+
transition open: :half_open
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
event :reset do
|
|
22
|
+
transition %i[open half_open] => :closed
|
|
23
|
+
transition closed: :closed
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
event :force_open do
|
|
27
|
+
transition any => :open
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
event :force_close do
|
|
31
|
+
transition any => :closed
|
|
32
|
+
end
|
|
33
|
+
|
|
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)
|
|
40
|
+
circuit.half_open_attempts.value = 0
|
|
41
|
+
circuit.half_open_successes.value = 0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
after_transition to: :open do |circuit|
|
|
45
|
+
circuit.send(:on_circuit_open)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
after_transition to: :closed do |circuit|
|
|
49
|
+
circuit.send(:on_circuit_close)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
after_transition from: :open, to: :half_open do |circuit|
|
|
53
|
+
circuit.send(:on_circuit_half_open)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
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
|