breaker_machines 0.9.2-x86_64-linux-musl

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 (58) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +184 -0
  4. data/ext/breaker_machines_native/extconf.rb +3 -0
  5. data/lib/breaker_machines/async_circuit.rb +47 -0
  6. data/lib/breaker_machines/async_support.rb +104 -0
  7. data/lib/breaker_machines/cascading_circuit.rb +177 -0
  8. data/lib/breaker_machines/circuit/async_state_management.rb +71 -0
  9. data/lib/breaker_machines/circuit/base.rb +59 -0
  10. data/lib/breaker_machines/circuit/callbacks.rb +135 -0
  11. data/lib/breaker_machines/circuit/configuration.rb +67 -0
  12. data/lib/breaker_machines/circuit/coordinated_state_management.rb +117 -0
  13. data/lib/breaker_machines/circuit/execution.rb +231 -0
  14. data/lib/breaker_machines/circuit/hedged_execution.rb +115 -0
  15. data/lib/breaker_machines/circuit/introspection.rb +93 -0
  16. data/lib/breaker_machines/circuit/native.rb +127 -0
  17. data/lib/breaker_machines/circuit/state_callbacks.rb +72 -0
  18. data/lib/breaker_machines/circuit/state_management.rb +59 -0
  19. data/lib/breaker_machines/circuit.rb +8 -0
  20. data/lib/breaker_machines/circuit_group.rb +153 -0
  21. data/lib/breaker_machines/console.rb +345 -0
  22. data/lib/breaker_machines/coordinated_circuit.rb +10 -0
  23. data/lib/breaker_machines/dsl/cascading_circuit_builder.rb +20 -0
  24. data/lib/breaker_machines/dsl/circuit_builder.rb +209 -0
  25. data/lib/breaker_machines/dsl/hedged_builder.rb +21 -0
  26. data/lib/breaker_machines/dsl/parallel_fallback_wrapper.rb +20 -0
  27. data/lib/breaker_machines/dsl.rb +283 -0
  28. data/lib/breaker_machines/errors.rb +71 -0
  29. data/lib/breaker_machines/hedged_async_support.rb +88 -0
  30. data/lib/breaker_machines/native_extension.rb +81 -0
  31. data/lib/breaker_machines/native_speedup.rb +10 -0
  32. data/lib/breaker_machines/registry.rb +243 -0
  33. data/lib/breaker_machines/storage/backend_state.rb +69 -0
  34. data/lib/breaker_machines/storage/base.rb +52 -0
  35. data/lib/breaker_machines/storage/bucket_memory.rb +176 -0
  36. data/lib/breaker_machines/storage/cache.rb +169 -0
  37. data/lib/breaker_machines/storage/fallback_chain.rb +294 -0
  38. data/lib/breaker_machines/storage/memory.rb +140 -0
  39. data/lib/breaker_machines/storage/native.rb +93 -0
  40. data/lib/breaker_machines/storage/null.rb +54 -0
  41. data/lib/breaker_machines/storage.rb +8 -0
  42. data/lib/breaker_machines/types.rb +41 -0
  43. data/lib/breaker_machines/version.rb +5 -0
  44. data/lib/breaker_machines.rb +200 -0
  45. data/lib/breaker_machines_native/breaker_machines_native.so +0 -0
  46. data/sig/README.md +74 -0
  47. data/sig/all.rbs +25 -0
  48. data/sig/breaker_machines/circuit.rbs +154 -0
  49. data/sig/breaker_machines/console.rbs +32 -0
  50. data/sig/breaker_machines/dsl.rbs +50 -0
  51. data/sig/breaker_machines/errors.rbs +24 -0
  52. data/sig/breaker_machines/interfaces.rbs +46 -0
  53. data/sig/breaker_machines/registry.rbs +30 -0
  54. data/sig/breaker_machines/storage.rbs +65 -0
  55. data/sig/breaker_machines/types.rbs +97 -0
  56. data/sig/breaker_machines.rbs +42 -0
  57. data/sig/manifest.yaml +5 -0
  58. 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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BreakerMachines
4
+ class Circuit
5
+ include Circuit::Base
6
+ include StateManagement
7
+ end
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