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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 97bc515f00b4402af03e813a121969745726a3635a8475bd3066877294f535a0
|
4
|
+
data.tar.gz: 20370133d03443108818133a98790194a4669a5b9daafdb1808d537b3915bb32
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 365d62466c1dae963c4f26e4f7920868358d9a39cd627bbc017ee7f8d36807e03ae808d1d4057dfee54b2edfc3e1dadaf36bf687b33aed8201bafac1a38f168f
|
7
|
+
data.tar.gz: 54fe1284d6710fab9a347b05821c41ea98f04212b224752d6bfc4f7d3fd995febb431acfee57f83a30859b2a7c7a138da59bb1c16ebf15032f018530c72fe397
|
data/README.md
CHANGED
@@ -44,6 +44,10 @@ Built on the battle-tested `state_machines` gem, because I don't reinvent wheels
|
|
44
44
|
|
45
45
|
- **Thread-safe** circuit breaker implementation
|
46
46
|
- **Fiber-safe mode** for async Ruby (Falcon, async gem)
|
47
|
+
- **AsyncCircuit** class with mutex-protected state transitions
|
48
|
+
- **Circuit Groups** for managing related circuits with dependencies
|
49
|
+
- **Coordinated State Management** for dependency-aware transitions
|
50
|
+
- **Cascading Circuit Breakers** for modeling system dependencies
|
47
51
|
- **Hedged requests** for latency reduction
|
48
52
|
- **Multiple backends** with automatic failover
|
49
53
|
- **Bulkheading** to limit concurrent requests
|
@@ -52,21 +56,39 @@ Built on the battle-tested `state_machines` gem, because I don't reinvent wheels
|
|
52
56
|
- **Pluggable storage** (Memory, Redis, Custom)
|
53
57
|
- **Rich callbacks** and instrumentation
|
54
58
|
- **ActiveSupport::Notifications** integration
|
59
|
+
- **Cross-platform support** - Optimized for MRI, JRuby, and TruffleRuby
|
55
60
|
|
56
61
|
## Documentation
|
57
62
|
|
63
|
+
### Core Features
|
58
64
|
- **Getting Started Guide** (docs/GETTING_STARTED.md) - Installation and basic usage
|
59
65
|
- **Configuration Reference** (docs/CONFIGURATION.md) - All configuration options
|
60
66
|
- **Advanced Patterns** (docs/ADVANCED_PATTERNS.md) - Complex scenarios and patterns
|
67
|
+
|
68
|
+
### Advanced Features
|
69
|
+
- **Circuit Groups** (docs/CIRCUIT_GROUPS.md) - Managing related circuits with dependencies
|
70
|
+
- **Coordinated State Management** (docs/COORDINATED_STATE_MANAGEMENT.md) - Dependency-aware state transitions
|
71
|
+
- **Cascading Circuit Breakers** (docs/CASCADING_CIRCUITS.md) - Modeling system dependencies
|
72
|
+
|
73
|
+
### Async & Concurrency
|
74
|
+
- **Async Mode** (docs/ASYNC.md) - Fiber-safe operations and AsyncCircuit
|
75
|
+
- **Async Storage Examples** (docs/ASYNC_STORAGE_EXAMPLES.md) - Non-blocking storage backends
|
76
|
+
|
77
|
+
### Storage & Persistence
|
61
78
|
- **Persistence Options** (docs/PERSISTENCE.md) - Storage backends and distributed state
|
62
|
-
|
63
|
-
|
79
|
+
|
80
|
+
### Testing
|
64
81
|
- **Testing Guide** (docs/TESTING.md) - Testing strategies
|
65
82
|
- [RSpec Testing](docs/TESTING_RSPEC.md)
|
66
83
|
- [ActiveSupport Testing](docs/TESTING_ACTIVESUPPORT.md)
|
84
|
+
|
85
|
+
### Integration & Monitoring
|
67
86
|
- **Rails Integration** (docs/RAILS_INTEGRATION.md) - Rails-specific patterns
|
68
|
-
- **
|
87
|
+
- **Observability Guide** (docs/OBSERVABILITY.md) - Monitoring and metrics
|
88
|
+
|
89
|
+
### Reference
|
69
90
|
- **API Reference** (docs/API_REFERENCE.md) - Complete API documentation
|
91
|
+
- **Horror Stories** (docs/HORROR_STORIES.md) - Real production failures and lessons learned
|
70
92
|
|
71
93
|
## Why BreakerMachines?
|
72
94
|
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Requires async gem ~> 2.31.0 for modern async patterns
|
4
|
+
|
5
|
+
require_relative 'circuit/async_state_management'
|
6
|
+
|
7
|
+
module BreakerMachines
|
8
|
+
# AsyncCircuit provides a circuit breaker with async-enabled state machine
|
9
|
+
# for thread-safe, fiber-safe concurrent operations
|
10
|
+
class AsyncCircuit < Circuit
|
11
|
+
include Circuit::AsyncStateManagement
|
12
|
+
|
13
|
+
# Additional async-specific methods
|
14
|
+
def call_async(&block)
|
15
|
+
require 'async' unless defined?(::Async)
|
16
|
+
|
17
|
+
Async do
|
18
|
+
call(&block)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Fire state transition events asynchronously
|
23
|
+
# @param event_name [Symbol] The event to fire
|
24
|
+
# @return [Async::Task] The async task
|
25
|
+
def fire_async(event_name)
|
26
|
+
require 'async' unless defined?(::Async)
|
27
|
+
|
28
|
+
fire_event_async(event_name)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Check circuit health asynchronously
|
32
|
+
# Useful for monitoring multiple circuits concurrently
|
33
|
+
def health_check_async
|
34
|
+
require 'async' unless defined?(::Async)
|
35
|
+
|
36
|
+
Async do
|
37
|
+
{
|
38
|
+
name: @name,
|
39
|
+
status: status_name,
|
40
|
+
open: open?,
|
41
|
+
stats: stats.to_h,
|
42
|
+
can_recover: open? && reset_timeout_elapsed?
|
43
|
+
}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
# This file contains all async-related functionality for fiber-safe mode
|
4
4
|
# It is only loaded when fiber_safe mode is enabled
|
5
|
+
# Requires async gem ~> 2.31.0 for modern timeout API and Promise features
|
5
6
|
|
6
7
|
require 'async'
|
7
8
|
require 'async/task'
|
@@ -18,7 +19,7 @@ module BreakerMachines
|
|
18
19
|
|
19
20
|
# Execute a call with async support (fiber-safe mode)
|
20
21
|
def execute_call_async(&)
|
21
|
-
start_time = monotonic_time
|
22
|
+
start_time = BreakerMachines.monotonic_time
|
22
23
|
|
23
24
|
begin
|
24
25
|
# Execute with hedged requests if enabled
|
@@ -28,14 +29,14 @@ module BreakerMachines
|
|
28
29
|
execute_with_async_timeout(@config[:timeout], &)
|
29
30
|
end
|
30
31
|
|
31
|
-
record_success(monotonic_time - start_time)
|
32
|
+
record_success(BreakerMachines.monotonic_time - start_time)
|
32
33
|
handle_success
|
33
34
|
result
|
34
35
|
rescue StandardError => e
|
35
36
|
# Re-raise if it's not an async timeout or configured exception
|
36
37
|
raise unless e.is_a?(async_timeout_error_class) || @config[:exceptions].any? { |klass| e.is_a?(klass) }
|
37
38
|
|
38
|
-
record_failure(monotonic_time - start_time, e)
|
39
|
+
record_failure(BreakerMachines.monotonic_time - start_time, e)
|
39
40
|
handle_failure
|
40
41
|
raise unless @config[:fallback]
|
41
42
|
|
@@ -43,11 +44,11 @@ module BreakerMachines
|
|
43
44
|
end
|
44
45
|
end
|
45
46
|
|
46
|
-
# Execute a block with optional timeout using Async
|
47
|
+
# Execute a block with optional timeout using modern Async API
|
47
48
|
def execute_with_async_timeout(timeout, &)
|
48
49
|
if timeout
|
49
|
-
# Use
|
50
|
-
|
50
|
+
# Use modern timeout API - the flexible with_timeout API is on the task level
|
51
|
+
Async::Task.current.with_timeout(timeout, &)
|
51
52
|
else
|
52
53
|
yield
|
53
54
|
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'coordinated_circuit'
|
4
|
+
|
5
|
+
module BreakerMachines
|
6
|
+
# CascadingCircuit extends the CoordinatedCircuit class with the ability to automatically
|
7
|
+
# trip dependent circuits when this circuit opens. This enables sophisticated
|
8
|
+
# failure cascade modeling, similar to how critical system failures in a starship
|
9
|
+
# would cascade to dependent subsystems.
|
10
|
+
#
|
11
|
+
# @example Starship network dependency
|
12
|
+
# # Network circuit that cascades to dependent systems
|
13
|
+
# network_circuit = BreakerMachines::CascadingCircuit.new('subspace_network', {
|
14
|
+
# failure_threshold: 1,
|
15
|
+
# cascades_to: ['weapons_targeting', 'navigation_sensors', 'communications'],
|
16
|
+
# emergency_protocol: :red_alert
|
17
|
+
# })
|
18
|
+
#
|
19
|
+
# # When network fails, all dependent systems are automatically tripped
|
20
|
+
# network_circuit.call { raise 'Subspace relay offline!' }
|
21
|
+
# # => All dependent circuits are now open
|
22
|
+
#
|
23
|
+
class CascadingCircuit < CoordinatedCircuit
|
24
|
+
attr_reader :dependent_circuits, :emergency_protocol
|
25
|
+
|
26
|
+
def initialize(name, config = {})
|
27
|
+
@dependent_circuits = Array(config.delete(:cascades_to))
|
28
|
+
@emergency_protocol = config.delete(:emergency_protocol)
|
29
|
+
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
33
|
+
# Override the trip method to include cascading behavior
|
34
|
+
def trip!
|
35
|
+
result = super
|
36
|
+
perform_cascade if result && @dependent_circuits.any?
|
37
|
+
result
|
38
|
+
end
|
39
|
+
|
40
|
+
# Force cascade failure to all dependent circuits
|
41
|
+
def cascade_failure!
|
42
|
+
perform_cascade
|
43
|
+
end
|
44
|
+
|
45
|
+
# Get the current status of all dependent circuits
|
46
|
+
def dependent_status
|
47
|
+
return {} if @dependent_circuits.empty?
|
48
|
+
|
49
|
+
@dependent_circuits.each_with_object({}) do |circuit_name, status|
|
50
|
+
circuit = BreakerMachines.registry.find(circuit_name)
|
51
|
+
status[circuit_name] = circuit ? circuit.status_name : :not_found
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Check if any dependent circuits are open
|
56
|
+
def dependents_compromised?
|
57
|
+
@dependent_circuits.any? do |circuit_name|
|
58
|
+
circuit = BreakerMachines.registry.find(circuit_name)
|
59
|
+
circuit&.open?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Summary that includes cascade information
|
64
|
+
def summary
|
65
|
+
base_summary = super
|
66
|
+
return base_summary if @dependent_circuits.empty?
|
67
|
+
|
68
|
+
if @cascade_triggered_at&.value
|
69
|
+
compromised_count = dependent_status.values.count(:open)
|
70
|
+
" CASCADE TRIGGERED: #{compromised_count}/#{@dependent_circuits.length} dependent systems compromised."
|
71
|
+
else
|
72
|
+
" Monitoring #{@dependent_circuits.length} dependent systems."
|
73
|
+
end
|
74
|
+
|
75
|
+
base_summary + cascade_info_text
|
76
|
+
end
|
77
|
+
|
78
|
+
# Provide cascade info for introspection
|
79
|
+
def cascade_info
|
80
|
+
BreakerMachines::CascadeInfo.new(
|
81
|
+
dependent_circuits: @dependent_circuits,
|
82
|
+
emergency_protocol: @emergency_protocol,
|
83
|
+
cascade_triggered_at: @cascade_triggered_at&.value,
|
84
|
+
dependent_status: dependent_status
|
85
|
+
)
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def perform_cascade
|
91
|
+
return if @dependent_circuits.empty?
|
92
|
+
|
93
|
+
cascade_results = []
|
94
|
+
@cascade_triggered_at ||= Concurrent::AtomicReference.new
|
95
|
+
@cascade_triggered_at.value = BreakerMachines.monotonic_time
|
96
|
+
|
97
|
+
@dependent_circuits.each do |circuit_name|
|
98
|
+
# First try to find circuit in registry
|
99
|
+
circuit = BreakerMachines.registry.find(circuit_name)
|
100
|
+
|
101
|
+
# If not found and we have an owner, try to get it from the owner
|
102
|
+
if !circuit && @config[:owner]
|
103
|
+
owner = @config[:owner]
|
104
|
+
# Handle WeakRef if present
|
105
|
+
owner = owner.__getobj__ if owner.is_a?(WeakRef)
|
106
|
+
|
107
|
+
circuit = owner.circuit(circuit_name) if owner.respond_to?(:circuit)
|
108
|
+
end
|
109
|
+
|
110
|
+
next unless circuit
|
111
|
+
next unless circuit.closed? || circuit.half_open?
|
112
|
+
|
113
|
+
# Force the dependent circuit to open
|
114
|
+
circuit.force_open!
|
115
|
+
cascade_results << circuit_name
|
116
|
+
|
117
|
+
BreakerMachines.instrument('cascade_failure', {
|
118
|
+
source_circuit: @name,
|
119
|
+
target_circuit: circuit_name,
|
120
|
+
emergency_protocol: @emergency_protocol
|
121
|
+
})
|
122
|
+
end
|
123
|
+
|
124
|
+
# Trigger emergency protocol if configured
|
125
|
+
trigger_emergency_protocol(cascade_results) if @emergency_protocol && cascade_results.any?
|
126
|
+
|
127
|
+
# Invoke cascade callback if configured
|
128
|
+
if @config[:on_cascade]
|
129
|
+
begin
|
130
|
+
@config[:on_cascade].call(cascade_results) if @config[:on_cascade].respond_to?(:call)
|
131
|
+
rescue StandardError => e
|
132
|
+
# Log callback error but don't fail the cascade
|
133
|
+
BreakerMachines.logger&.error "Cascade callback error: #{e.message}"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
cascade_results
|
138
|
+
end
|
139
|
+
|
140
|
+
def trigger_emergency_protocol(affected_circuits)
|
141
|
+
BreakerMachines.instrument('emergency_protocol_triggered', {
|
142
|
+
protocol: @emergency_protocol,
|
143
|
+
source_circuit: @name,
|
144
|
+
affected_circuits: affected_circuits
|
145
|
+
})
|
146
|
+
|
147
|
+
# Allow custom emergency protocol handling
|
148
|
+
owner = @config[:owner]
|
149
|
+
if owner.respond_to?(@emergency_protocol, true)
|
150
|
+
begin
|
151
|
+
owner.send(@emergency_protocol, affected_circuits)
|
152
|
+
rescue StandardError => e
|
153
|
+
BreakerMachines.logger&.error "Emergency protocol error: #{e.message}"
|
154
|
+
end
|
155
|
+
elsif respond_to?(@emergency_protocol, true)
|
156
|
+
begin
|
157
|
+
send(@emergency_protocol, affected_circuits)
|
158
|
+
rescue StandardError => e
|
159
|
+
BreakerMachines.logger&.error "Emergency protocol error: #{e.message}"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Override the on_circuit_open callback to include cascading
|
165
|
+
def on_circuit_open
|
166
|
+
super # Call the original implementation
|
167
|
+
perform_cascade if @dependent_circuits.any?
|
168
|
+
end
|
169
|
+
|
170
|
+
# Force open should also cascade
|
171
|
+
def force_open!
|
172
|
+
result = super
|
173
|
+
perform_cascade if result && @dependent_circuits.any?
|
174
|
+
result
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BreakerMachines
|
4
|
+
class Circuit
|
5
|
+
# AsyncStateManagement provides state machine functionality with async support
|
6
|
+
# leveraging state_machines' async: true parameter for thread-safe operations
|
7
|
+
module AsyncStateManagement
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
# Enable async mode for thread-safe state transitions
|
12
|
+
# This automatically provides:
|
13
|
+
# - Mutex-protected state reads/writes
|
14
|
+
# - Fiber-safe execution
|
15
|
+
# - Concurrent transition handling
|
16
|
+
state_machine :status, initial: :closed, async: true do
|
17
|
+
event :trip do
|
18
|
+
transition closed: :open
|
19
|
+
transition half_open: :open
|
20
|
+
end
|
21
|
+
|
22
|
+
event :attempt_recovery do
|
23
|
+
transition open: :half_open
|
24
|
+
end
|
25
|
+
|
26
|
+
event :reset do
|
27
|
+
transition %i[open half_open] => :closed
|
28
|
+
transition closed: :closed
|
29
|
+
end
|
30
|
+
|
31
|
+
event :force_open do
|
32
|
+
transition any => :open
|
33
|
+
end
|
34
|
+
|
35
|
+
event :force_close do
|
36
|
+
transition any => :closed
|
37
|
+
end
|
38
|
+
|
39
|
+
event :hard_reset do
|
40
|
+
transition any => :closed
|
41
|
+
end
|
42
|
+
|
43
|
+
before_transition on: :hard_reset do |circuit|
|
44
|
+
circuit.storage.clear(circuit.name) if circuit.storage
|
45
|
+
circuit.half_open_attempts.value = 0
|
46
|
+
circuit.half_open_successes.value = 0
|
47
|
+
end
|
48
|
+
|
49
|
+
# Async-safe callbacks using modern API
|
50
|
+
after_transition to: :open do |circuit|
|
51
|
+
circuit.send(:on_circuit_open)
|
52
|
+
end
|
53
|
+
|
54
|
+
after_transition to: :closed do |circuit|
|
55
|
+
circuit.send(:on_circuit_close)
|
56
|
+
end
|
57
|
+
|
58
|
+
after_transition from: :open, to: :half_open do |circuit|
|
59
|
+
circuit.send(:on_circuit_half_open)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Additional async event methods are automatically generated:
|
64
|
+
# - trip_async! - Returns Async::Task
|
65
|
+
# - attempt_recovery_async! - Returns Async::Task
|
66
|
+
# - reset_async! - Returns Async::Task
|
67
|
+
# - fire_event_async(:event_name) - Generic async event firing
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'concurrent-ruby'
|
4
|
+
|
5
|
+
module BreakerMachines
|
6
|
+
class Circuit
|
7
|
+
# Base provides the common initialization and setup logic shared by all circuit types
|
8
|
+
module Base
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
included do
|
12
|
+
include Circuit::Configuration
|
13
|
+
include Circuit::Execution
|
14
|
+
include Circuit::HedgedExecution
|
15
|
+
include Circuit::Introspection
|
16
|
+
include Circuit::Callbacks
|
17
|
+
include Circuit::StateCallbacks
|
18
|
+
|
19
|
+
attr_reader :name, :config, :opened_at, :storage, :metrics, :semaphore
|
20
|
+
attr_reader :half_open_attempts, :half_open_successes, :mutex
|
21
|
+
attr_reader :last_failure_at, :last_error
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(name, options = {})
|
25
|
+
@name = name
|
26
|
+
@config = default_config.merge(options)
|
27
|
+
# Always use a storage backend for proper sliding window implementation
|
28
|
+
# Use global default storage if not specified
|
29
|
+
@storage = @config[:storage] || create_default_storage
|
30
|
+
@metrics = @config[:metrics]
|
31
|
+
@opened_at = Concurrent::AtomicReference.new(nil)
|
32
|
+
@half_open_attempts = Concurrent::AtomicFixnum.new(0)
|
33
|
+
@half_open_successes = Concurrent::AtomicFixnum.new(0)
|
34
|
+
@mutex = Concurrent::ReentrantReadWriteLock.new
|
35
|
+
@last_failure_at = Concurrent::AtomicReference.new(nil)
|
36
|
+
@last_error = Concurrent::AtomicReference.new(nil)
|
37
|
+
|
38
|
+
# Initialize semaphore for bulkheading if max_concurrent is set
|
39
|
+
@semaphore = (Concurrent::Semaphore.new(@config[:max_concurrent]) if @config[:max_concurrent])
|
40
|
+
|
41
|
+
restore_status_from_storage if @storage
|
42
|
+
|
43
|
+
# Register with global registry unless auto_register is disabled
|
44
|
+
BreakerMachines::Registry.instance.register(self) unless @config[:auto_register] == false
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def restore_status_from_storage
|
50
|
+
stored_status = @storage.get_status(@name)
|
51
|
+
return unless stored_status
|
52
|
+
|
53
|
+
self.status = stored_status.status.to_s
|
54
|
+
@opened_at.value = stored_status.opened_at if stored_status.opened_at
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -120,19 +120,14 @@ module BreakerMachines
|
|
120
120
|
end
|
121
121
|
end
|
122
122
|
|
123
|
-
|
124
|
-
begin
|
125
|
-
Timeout.timeout(5) do # reasonable timeout for fallbacks
|
126
|
-
loop do
|
127
|
-
return result_queue.pop unless result_queue.empty?
|
128
|
-
|
129
|
-
raise error_queue.pop if error_queue.size >= fallbacks.size
|
123
|
+
threads.each(&:join)
|
130
124
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
125
|
+
if result_queue.empty?
|
126
|
+
errors = []
|
127
|
+
errors << error_queue.pop until error_queue.empty?
|
128
|
+
raise BreakerMachines::ParallelFallbackError.new('All parallel fallbacks failed', errors)
|
129
|
+
else
|
130
|
+
result_queue.pop
|
136
131
|
end
|
137
132
|
end
|
138
133
|
end
|
@@ -11,30 +11,6 @@ module BreakerMachines
|
|
11
11
|
attr_reader :name, :config, :opened_at
|
12
12
|
end
|
13
13
|
|
14
|
-
def initialize(name, options = {})
|
15
|
-
@name = name
|
16
|
-
@config = default_config.merge(options)
|
17
|
-
# Always use a storage backend for proper sliding window implementation
|
18
|
-
# Use global default storage if not specified
|
19
|
-
@storage = @config[:storage] || create_default_storage
|
20
|
-
@metrics = @config[:metrics]
|
21
|
-
@opened_at = Concurrent::AtomicReference.new(nil)
|
22
|
-
@half_open_attempts = Concurrent::AtomicFixnum.new(0)
|
23
|
-
@half_open_successes = Concurrent::AtomicFixnum.new(0)
|
24
|
-
@mutex = Concurrent::ReentrantReadWriteLock.new
|
25
|
-
@last_failure_at = Concurrent::AtomicReference.new(nil)
|
26
|
-
@last_error = Concurrent::AtomicReference.new(nil)
|
27
|
-
|
28
|
-
# Initialize semaphore for bulkheading if max_concurrent is set
|
29
|
-
@semaphore = (Concurrent::Semaphore.new(@config[:max_concurrent]) if @config[:max_concurrent])
|
30
|
-
|
31
|
-
super() # Initialize state machine
|
32
|
-
restore_status_from_storage if @storage
|
33
|
-
|
34
|
-
# Register with global registry unless auto_register is disabled
|
35
|
-
BreakerMachines::Registry.instance.register(self) unless @config[:auto_register] == false
|
36
|
-
end
|
37
|
-
|
38
14
|
private
|
39
15
|
|
40
16
|
def default_config
|
@@ -78,8 +54,12 @@ module BreakerMachines
|
|
78
54
|
when :null
|
79
55
|
BreakerMachines::Storage::Null.new
|
80
56
|
else
|
81
|
-
# Allow for custom storage class names
|
82
|
-
BreakerMachines.config.default_storage.new
|
57
|
+
# Allow for custom storage class names or instances
|
58
|
+
if BreakerMachines.config.default_storage.respond_to?(:new)
|
59
|
+
BreakerMachines.config.default_storage.new
|
60
|
+
else
|
61
|
+
BreakerMachines.config.default_storage
|
62
|
+
end
|
83
63
|
end
|
84
64
|
end
|
85
65
|
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BreakerMachines
|
4
|
+
class Circuit
|
5
|
+
# CoordinatedStateManagement extends the base state machine with coordinated guards
|
6
|
+
# that allow circuits to make transitions based on the state of other circuits.
|
7
|
+
module CoordinatedStateManagement
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
# Override the state machine to add coordinated guards
|
12
|
+
state_machine :status, initial: :closed do
|
13
|
+
event :trip do
|
14
|
+
transition closed: :open
|
15
|
+
transition half_open: :open
|
16
|
+
end
|
17
|
+
|
18
|
+
event :attempt_recovery do
|
19
|
+
transition open: :half_open,
|
20
|
+
if: ->(circuit) { circuit.recovery_allowed? }
|
21
|
+
end
|
22
|
+
|
23
|
+
event :reset do
|
24
|
+
transition %i[open half_open] => :closed,
|
25
|
+
if: ->(circuit) { circuit.reset_allowed? }
|
26
|
+
transition closed: :closed
|
27
|
+
end
|
28
|
+
|
29
|
+
event :force_open do
|
30
|
+
transition any => :open
|
31
|
+
end
|
32
|
+
|
33
|
+
event :force_close do
|
34
|
+
transition any => :closed
|
35
|
+
end
|
36
|
+
|
37
|
+
event :hard_reset do
|
38
|
+
transition any => :closed
|
39
|
+
end
|
40
|
+
|
41
|
+
before_transition on: :hard_reset do |circuit|
|
42
|
+
circuit.storage.clear(circuit.name) if circuit.storage
|
43
|
+
circuit.half_open_attempts.value = 0
|
44
|
+
circuit.half_open_successes.value = 0
|
45
|
+
end
|
46
|
+
|
47
|
+
after_transition to: :open do |circuit|
|
48
|
+
circuit.send(:on_circuit_open)
|
49
|
+
end
|
50
|
+
|
51
|
+
after_transition to: :closed do |circuit|
|
52
|
+
circuit.send(:on_circuit_close)
|
53
|
+
end
|
54
|
+
|
55
|
+
after_transition from: :open, to: :half_open do |circuit|
|
56
|
+
circuit.send(:on_circuit_half_open)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Check if this circuit can attempt recovery
|
62
|
+
# For cascading circuits, this checks if dependent circuits allow it
|
63
|
+
def recovery_allowed?
|
64
|
+
return true unless respond_to?(:dependent_circuits) && dependent_circuits.any?
|
65
|
+
|
66
|
+
# Don't attempt recovery if any critical dependencies are still down
|
67
|
+
!has_critical_dependencies_down?
|
68
|
+
end
|
69
|
+
|
70
|
+
# Check if this circuit can reset to closed
|
71
|
+
# For cascading circuits, ensures dependencies are healthy
|
72
|
+
def reset_allowed?
|
73
|
+
return true unless respond_to?(:dependent_circuits) && dependent_circuits.any?
|
74
|
+
|
75
|
+
# Only reset if all dependencies are in acceptable states
|
76
|
+
all_dependencies_healthy?
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
# Check if any critical dependencies are down
|
82
|
+
def has_critical_dependencies_down?
|
83
|
+
return false unless respond_to?(:dependent_circuits)
|
84
|
+
|
85
|
+
dependent_circuits.any? do |circuit_name|
|
86
|
+
circuit = find_dependent_circuit(circuit_name)
|
87
|
+
circuit&.open?
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Check if all dependencies are in healthy states
|
92
|
+
def all_dependencies_healthy?
|
93
|
+
return true unless respond_to?(:dependent_circuits)
|
94
|
+
|
95
|
+
dependent_circuits.all? do |circuit_name|
|
96
|
+
circuit = find_dependent_circuit(circuit_name)
|
97
|
+
circuit.nil? || circuit.closed? || circuit.half_open?
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Find a dependent circuit by name
|
102
|
+
def find_dependent_circuit(circuit_name)
|
103
|
+
# First try registry
|
104
|
+
circuit = BreakerMachines.registry.find(circuit_name)
|
105
|
+
|
106
|
+
# If not found and we have an owner, try to get it from the owner
|
107
|
+
if !circuit && @config[:owner]
|
108
|
+
owner = @config[:owner]
|
109
|
+
owner = owner.__getobj__ if owner.is_a?(WeakRef)
|
110
|
+
circuit = owner.circuit(circuit_name) if owner.respond_to?(:circuit)
|
111
|
+
end
|
112
|
+
|
113
|
+
circuit
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|