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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +25 -3
  3. data/lib/breaker_machines/async_circuit.rb +47 -0
  4. data/lib/breaker_machines/async_support.rb +7 -6
  5. data/lib/breaker_machines/cascading_circuit.rb +177 -0
  6. data/lib/breaker_machines/circuit/async_state_management.rb +71 -0
  7. data/lib/breaker_machines/circuit/base.rb +58 -0
  8. data/lib/breaker_machines/circuit/callbacks.rb +7 -12
  9. data/lib/breaker_machines/circuit/configuration.rb +6 -26
  10. data/lib/breaker_machines/circuit/coordinated_state_management.rb +117 -0
  11. data/lib/breaker_machines/circuit/execution.rb +4 -8
  12. data/lib/breaker_machines/circuit/hedged_execution.rb +115 -0
  13. data/lib/breaker_machines/circuit/introspection.rb +36 -20
  14. data/lib/breaker_machines/circuit/state_callbacks.rb +60 -0
  15. data/lib/breaker_machines/circuit/state_management.rb +15 -61
  16. data/lib/breaker_machines/circuit.rb +1 -8
  17. data/lib/breaker_machines/circuit_group.rb +153 -0
  18. data/lib/breaker_machines/console.rb +12 -12
  19. data/lib/breaker_machines/coordinated_circuit.rb +10 -0
  20. data/lib/breaker_machines/dsl/cascading_circuit_builder.rb +20 -0
  21. data/lib/breaker_machines/dsl/circuit_builder.rb +209 -0
  22. data/lib/breaker_machines/dsl/hedged_builder.rb +21 -0
  23. data/lib/breaker_machines/dsl/parallel_fallback_wrapper.rb +20 -0
  24. data/lib/breaker_machines/dsl.rb +28 -241
  25. data/lib/breaker_machines/errors.rb +20 -0
  26. data/lib/breaker_machines/hedged_async_support.rb +29 -36
  27. data/lib/breaker_machines/registry.rb +3 -3
  28. data/lib/breaker_machines/storage/backend_state.rb +69 -0
  29. data/lib/breaker_machines/storage/bucket_memory.rb +3 -3
  30. data/lib/breaker_machines/storage/cache.rb +3 -3
  31. data/lib/breaker_machines/storage/fallback_chain.rb +56 -70
  32. data/lib/breaker_machines/storage/memory.rb +3 -3
  33. data/lib/breaker_machines/types.rb +41 -0
  34. data/lib/breaker_machines/version.rb +1 -1
  35. data/lib/breaker_machines.rb +29 -0
  36. metadata +21 -7
  37. 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
- when :closed
52
- "Circuit '#{@name}' is CLOSED. #{stats[:failure_count]} failures recorded."
53
- when :open
54
- reset_time = Time.at(@opened_at.value + @config[:reset_timeout])
55
- opened_time = Time.at(@opened_at.value)
56
- error_info = @last_error.value ? " The last error was #{@last_error.value.class}." : ''
57
- "Circuit '#{@name}' is OPEN until #{reset_time}. " \
58
- "It opened at #{opened_time} after #{@config[:failure_threshold]} failures.#{error_info}"
59
- when :half_open
60
- "Circuit '#{@name}' is HALF-OPEN. Testing with limited requests " \
61
- "(#{@half_open_attempts.value}/#{@config[:half_open_calls]} attempts)."
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
- class: error.class.name,
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
- after_transition any => :open do |circuit|
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 any => :closed do |circuit|
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[:state]),
118
- stats[:failure_count],
119
- stats[:success_count],
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[:state])}"
171
- puts "Failure Count: #{stats[:failure_count]} / #{config[:failure_threshold]}"
172
- puts "Success Count: #{stats[:success_count]}"
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[:opened_at]
175
- puts "Opened At: #{Time.at(stats[:opened_at])}"
176
- reset_time = Time.at(stats[:opened_at] + config[:reset_timeout])
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[:class]}"
184
- puts " Message: #{error_info[:message]}"
185
- puts " Time: #{Time.at(error_info[:occurred_at])}"
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