breaker_machines 0.4.0 → 0.6.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +25 -3
  3. data/ext/breaker_machines_native/Cargo.toml +8 -0
  4. data/ext/breaker_machines_native/core/Cargo.toml +18 -0
  5. data/ext/breaker_machines_native/core/examples/basic.rs +61 -0
  6. data/ext/breaker_machines_native/core/src/builder.rs +232 -0
  7. data/ext/breaker_machines_native/core/src/bulkhead.rs +223 -0
  8. data/ext/breaker_machines_native/core/src/callbacks.rs +58 -0
  9. data/ext/breaker_machines_native/core/src/circuit.rs +1156 -0
  10. data/ext/breaker_machines_native/core/src/classifier.rs +177 -0
  11. data/ext/breaker_machines_native/core/src/errors.rs +47 -0
  12. data/ext/breaker_machines_native/core/src/lib.rs +62 -0
  13. data/ext/breaker_machines_native/core/src/storage.rs +377 -0
  14. data/ext/breaker_machines_native/extconf.rb +40 -0
  15. data/ext/breaker_machines_native/ffi/Cargo.toml +16 -0
  16. data/ext/breaker_machines_native/ffi/src/lib.rs +218 -0
  17. data/ext/breaker_machines_native/target/debug/build/clang-sys-d961dfabd5f43fba/out/common.rs +355 -0
  18. data/ext/breaker_machines_native/target/debug/build/clang-sys-d961dfabd5f43fba/out/dynamic.rs +276 -0
  19. data/ext/breaker_machines_native/target/debug/build/clang-sys-d961dfabd5f43fba/out/macros.rs +49 -0
  20. data/ext/breaker_machines_native/target/debug/build/rb-sys-2bb7281aac8faec8/out/bindings-0.9.117-mri-arm64-darwin24-3.4.7.rs +8936 -0
  21. data/ext/breaker_machines_native/target/debug/build/rb-sys-54cb99ea6aeab8bc/out/bindings-0.9.117-mri-arm64-darwin24-3.4.7.rs +8936 -0
  22. data/ext/breaker_machines_native/target/debug/build/rb-sys-9e64a270c6421e93/out/bindings-0.9.117-mri-arm64-darwin24-3.4.7.rs +8936 -0
  23. data/ext/breaker_machines_native/target/debug/build/rb-sys-e627030114d3fc19/out/bindings-0.9.117-mri-arm64-darwin24-3.4.7.rs +8936 -0
  24. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/Cargo.toml +48 -0
  25. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/examples/basic.rs +61 -0
  26. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/builder.rs +154 -0
  27. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/callbacks.rs +55 -0
  28. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/circuit.rs +607 -0
  29. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/errors.rs +38 -0
  30. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/lib.rs +58 -0
  31. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/storage.rs +377 -0
  32. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/Cargo.toml +48 -0
  33. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/examples/basic.rs +61 -0
  34. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/builder.rs +173 -0
  35. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/callbacks.rs +55 -0
  36. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/circuit.rs +855 -0
  37. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/errors.rs +38 -0
  38. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/lib.rs +58 -0
  39. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/storage.rs +377 -0
  40. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/Cargo.toml +48 -0
  41. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/examples/basic.rs +61 -0
  42. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/builder.rs +154 -0
  43. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/callbacks.rs +55 -0
  44. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/circuit.rs +607 -0
  45. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/errors.rs +38 -0
  46. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/lib.rs +58 -0
  47. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/storage.rs +377 -0
  48. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/Cargo.toml +48 -0
  49. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/examples/basic.rs +61 -0
  50. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/builder.rs +232 -0
  51. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/bulkhead.rs +223 -0
  52. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/callbacks.rs +58 -0
  53. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/circuit.rs +1156 -0
  54. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/classifier.rs +177 -0
  55. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/errors.rs +47 -0
  56. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/lib.rs +62 -0
  57. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/storage.rs +377 -0
  58. data/ext/breaker_machines_native/target/release/build/clang-sys-ef8ad8b846ac8b75/out/common.rs +355 -0
  59. data/ext/breaker_machines_native/target/release/build/clang-sys-ef8ad8b846ac8b75/out/dynamic.rs +276 -0
  60. data/ext/breaker_machines_native/target/release/build/clang-sys-ef8ad8b846ac8b75/out/macros.rs +49 -0
  61. data/ext/breaker_machines_native/target/release/build/rb-sys-064bf9961dd17810/out/bindings-0.9.117-mri-arm64-darwin24-3.4.7.rs +8936 -0
  62. data/lib/breaker_machines/async_circuit.rb +47 -0
  63. data/lib/breaker_machines/async_support.rb +4 -3
  64. data/lib/breaker_machines/cascading_circuit.rb +5 -3
  65. data/lib/breaker_machines/circuit/async_state_management.rb +71 -0
  66. data/lib/breaker_machines/circuit/base.rb +59 -0
  67. data/lib/breaker_machines/circuit/callbacks.rb +7 -12
  68. data/lib/breaker_machines/circuit/configuration.rb +6 -26
  69. data/lib/breaker_machines/circuit/coordinated_state_management.rb +117 -0
  70. data/lib/breaker_machines/circuit/hedged_execution.rb +115 -0
  71. data/lib/breaker_machines/circuit/introspection.rb +1 -0
  72. data/lib/breaker_machines/circuit/native.rb +127 -0
  73. data/lib/breaker_machines/circuit/state_callbacks.rb +72 -0
  74. data/lib/breaker_machines/circuit/state_management.rb +14 -61
  75. data/lib/breaker_machines/circuit.rb +1 -7
  76. data/lib/breaker_machines/circuit_group.rb +153 -0
  77. data/lib/breaker_machines/coordinated_circuit.rb +10 -0
  78. data/lib/breaker_machines/dsl.rb +2 -2
  79. data/lib/breaker_machines/errors.rb +20 -0
  80. data/lib/breaker_machines/hedged_async_support.rb +29 -36
  81. data/lib/breaker_machines/native_extension.rb +36 -0
  82. data/lib/breaker_machines/native_speedup.rb +6 -0
  83. data/lib/breaker_machines/storage/bucket_memory.rb +4 -1
  84. data/lib/breaker_machines/storage/memory.rb +4 -1
  85. data/lib/breaker_machines/storage/native.rb +90 -0
  86. data/lib/breaker_machines/version.rb +1 -1
  87. data/lib/breaker_machines.rb +115 -11
  88. data/lib/breaker_machines_native/breaker_machines_native.bundle +0 -0
  89. data/sig/breaker_machines.rbs +20 -8
  90. metadata +107 -7
  91. data/lib/breaker_machines/hedged_execution.rb +0 -113
@@ -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'
@@ -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 safe, cooperative timeout from async gem
50
- ::Async::Task.current.with_timeout(timeout, &)
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
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'coordinated_circuit'
4
+
3
5
  module BreakerMachines
4
- # CascadingCircuit extends the base Circuit class with the ability to automatically
6
+ # CascadingCircuit extends the CoordinatedCircuit class with the ability to automatically
5
7
  # trip dependent circuits when this circuit opens. This enables sophisticated
6
8
  # failure cascade modeling, similar to how critical system failures in a starship
7
9
  # would cascade to dependent subsystems.
@@ -18,7 +20,7 @@ module BreakerMachines
18
20
  # network_circuit.call { raise 'Subspace relay offline!' }
19
21
  # # => All dependent circuits are now open
20
22
  #
21
- class CascadingCircuit < Circuit
23
+ class CascadingCircuit < CoordinatedCircuit
22
24
  attr_reader :dependent_circuits, :emergency_protocol
23
25
 
24
26
  def initialize(name, config = {})
@@ -144,7 +146,7 @@ module BreakerMachines
144
146
 
145
147
  # Allow custom emergency protocol handling
146
148
  owner = @config[:owner]
147
- if owner&.respond_to?(@emergency_protocol, true)
149
+ if owner.respond_to?(@emergency_protocol, true)
148
150
  begin
149
151
  owner.send(@emergency_protocol, affected_circuits)
150
152
  rescue StandardError => e
@@ -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)
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,59 @@
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
+ # name/config/opened_at readers are defined in Circuit::Configuration
20
+ attr_reader :storage, :metrics, :semaphore
21
+ attr_reader :half_open_attempts, :half_open_successes, :mutex
22
+ attr_reader :last_failure_at, :last_error
23
+ end
24
+
25
+ def initialize(name, options = {})
26
+ @name = name
27
+ @config = default_config.merge(options)
28
+ # Always use a storage backend for proper sliding window implementation
29
+ # Use global default storage if not specified
30
+ @storage = @config[:storage] || create_default_storage
31
+ @metrics = @config[:metrics]
32
+ @opened_at = Concurrent::AtomicReference.new(nil)
33
+ @half_open_attempts = Concurrent::AtomicFixnum.new(0)
34
+ @half_open_successes = Concurrent::AtomicFixnum.new(0)
35
+ @mutex = Concurrent::ReentrantReadWriteLock.new
36
+ @last_failure_at = Concurrent::AtomicReference.new(nil)
37
+ @last_error = Concurrent::AtomicReference.new(nil)
38
+
39
+ # Initialize semaphore for bulkheading if max_concurrent is set
40
+ @semaphore = (Concurrent::Semaphore.new(@config[:max_concurrent]) if @config[:max_concurrent])
41
+
42
+ restore_status_from_storage if @storage
43
+
44
+ # Register with global registry unless auto_register is disabled
45
+ BreakerMachines::Registry.instance.register(self) unless @config[:auto_register] == false
46
+ end
47
+
48
+ private
49
+
50
+ def restore_status_from_storage
51
+ stored_status = @storage.get_status(@name)
52
+ return unless stored_status
53
+
54
+ self.status = stored_status.status.to_s
55
+ @opened_at.value = stored_status.opened_at if stored_status.opened_at
56
+ end
57
+ end
58
+ end
59
+ end
@@ -120,19 +120,14 @@ module BreakerMachines
120
120
  end
121
121
  end
122
122
 
123
- # Wait for first successful result
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
- sleep 0.001
132
- end
133
- end
134
- ensure
135
- threads.each(&:kill)
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: lambda(&:recovery_allowed?)
21
+ end
22
+
23
+ event :reset do
24
+ transition %i[open half_open] => :closed,
25
+ if: lambda(&: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)
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
@@ -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,6 +6,7 @@ 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)
@@ -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