breaker_machines 0.9.2-x86_64-linux

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,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BreakerMachines
4
+ class Circuit
5
+ # Callbacks handles the invocation of user-defined callbacks and fallback mechanisms
6
+ # when circuit state changes occur or calls are rejected.
7
+ module Callbacks
8
+ extend ActiveSupport::Concern
9
+
10
+ private
11
+
12
+ def invoke_callback(callback_name)
13
+ callback = @config[callback_name]
14
+ return unless callback
15
+
16
+ return unless callback.is_a?(Proc)
17
+
18
+ if @config[:owner]
19
+ owner = resolve_owner
20
+ if owner
21
+ owner.instance_exec(&callback)
22
+ else
23
+ # Owner has been garbage collected, execute callback without context
24
+ callback.call
25
+ end
26
+ else
27
+ callback.call
28
+ end
29
+ end
30
+
31
+ def invoke_fallback(error)
32
+ case @config[:fallback]
33
+ when BreakerMachines::DSL::ParallelFallbackWrapper
34
+ invoke_parallel_fallbacks(@config[:fallback].fallbacks, error)
35
+ when Proc
36
+ if @config[:owner]
37
+ owner = resolve_owner
38
+ if owner
39
+ owner.instance_exec(error, &@config[:fallback])
40
+ else
41
+ # Owner has been garbage collected, execute fallback without context
42
+ @config[:fallback].call(error)
43
+ end
44
+ else
45
+ @config[:fallback].call(error)
46
+ end
47
+ when Array
48
+ # Try each fallback in order until one succeeds
49
+ last_error = error
50
+ @config[:fallback].each do |fallback|
51
+ return invoke_single_fallback(fallback, last_error)
52
+ rescue StandardError => e
53
+ last_error = e
54
+ end
55
+ raise last_error
56
+ else
57
+ # Static values (strings, hashes, etc.) or Symbol fallbacks
58
+ @config[:fallback]
59
+ end
60
+ end
61
+
62
+ def invoke_single_fallback(fallback, error)
63
+ case fallback
64
+ when Proc
65
+ if @config[:owner]
66
+ owner = resolve_owner
67
+ if owner
68
+ owner.instance_exec(error, &fallback)
69
+ else
70
+ fallback.call(error)
71
+ end
72
+ else
73
+ fallback.call(error)
74
+ end
75
+ else
76
+ fallback
77
+ end
78
+ end
79
+
80
+ # Safely resolve owner from WeakRef if applicable
81
+ def resolve_owner
82
+ owner = @config[:owner]
83
+ return owner unless owner.is_a?(WeakRef)
84
+
85
+ begin
86
+ owner.__getobj__
87
+ rescue WeakRef::RefError
88
+ # Owner has been garbage collected
89
+ nil
90
+ end
91
+ end
92
+
93
+ def invoke_parallel_fallbacks(fallbacks, error)
94
+ return fallbacks.first if fallbacks.size == 1
95
+
96
+ if @config[:fiber_safe] && respond_to?(:execute_parallel_fallbacks_async)
97
+ execute_parallel_fallbacks_async(fallbacks)
98
+ else
99
+ execute_parallel_fallbacks_sync(fallbacks, error)
100
+ end
101
+ end
102
+
103
+ def execute_parallel_fallbacks_sync(fallbacks, error)
104
+ result_queue = Queue.new
105
+ error_queue = Queue.new
106
+ threads = fallbacks.map do |fallback|
107
+ Thread.new do
108
+ result = if fallback.is_a?(Proc)
109
+ if fallback.arity == 1
110
+ fallback.call(error)
111
+ else
112
+ fallback.call
113
+ end
114
+ else
115
+ fallback
116
+ end
117
+ result_queue << result
118
+ rescue StandardError => e
119
+ error_queue << e
120
+ end
121
+ end
122
+
123
+ threads.each(&:join)
124
+
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
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BreakerMachines
4
+ class Circuit
5
+ # Configuration manages circuit initialization and default settings,
6
+ # including thresholds, timeouts, storage backends, and callbacks.
7
+ module Configuration
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ attr_reader :name, :config, :opened_at
12
+ end
13
+
14
+ private
15
+
16
+ def default_config
17
+ {
18
+ failure_threshold: 5,
19
+ failure_window: 60, # seconds
20
+ success_threshold: 1,
21
+ timeout: nil,
22
+ reset_timeout: 60, # seconds
23
+ reset_timeout_jitter: 0.25, # +/- 25% by default
24
+ half_open_calls: 1,
25
+ storage: nil, # Will default to Memory storage if nil
26
+ metrics: nil,
27
+ fallback: nil,
28
+ on_open: nil,
29
+ on_close: nil,
30
+ on_half_open: nil,
31
+ on_reject: nil,
32
+ exceptions: [StandardError],
33
+ fiber_safe: BreakerMachines.config.fiber_safe,
34
+ # Rate-based threshold options
35
+ use_rate_threshold: false,
36
+ failure_rate: nil,
37
+ minimum_calls: 5,
38
+ # Bulkheading options
39
+ max_concurrent: nil,
40
+ # Hedged request options
41
+ hedged_requests: false,
42
+ hedging_delay: 50, # milliseconds
43
+ max_hedged_requests: 2,
44
+ backends: nil
45
+ }
46
+ end
47
+
48
+ def create_default_storage
49
+ case BreakerMachines.config.default_storage
50
+ when :memory
51
+ BreakerMachines::Storage::Memory.new
52
+ when :bucket_memory
53
+ BreakerMachines::Storage::BucketMemory.new
54
+ when :null
55
+ BreakerMachines::Storage::Null.new
56
+ else
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
63
+ end
64
+ end
65
+ end
66
+ end
67
+ 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,231 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BreakerMachines
4
+ class Circuit
5
+ # Execution handles the core circuit breaker logic including call wrapping,
6
+ # state-based request handling, and failure/success tracking.
7
+ module Execution
8
+ extend ActiveSupport::Concern
9
+
10
+ # Lazy load async support only when needed
11
+ def self.load_async_support
12
+ require 'breaker_machines/async_support'
13
+ require 'breaker_machines/hedged_async_support'
14
+ Circuit.include(BreakerMachines::AsyncSupport)
15
+ Circuit.include(BreakerMachines::HedgedAsyncSupport)
16
+ rescue LoadError => e
17
+ if e.message.include?('async')
18
+ raise LoadError, "The 'async' gem is required for fiber_safe mode. Add `gem 'async'` to your Gemfile."
19
+ end
20
+
21
+ raise
22
+ end
23
+
24
+ def call(&)
25
+ wrap(&)
26
+ end
27
+
28
+ def wrap(&)
29
+ execute_with_state_check(&)
30
+ end
31
+
32
+ private
33
+
34
+ def execute_with_state_check(&block)
35
+ # Check if we need to transition from open to half-open first
36
+ if open? && reset_timeout_elapsed?
37
+ @mutex.with_write_lock do
38
+ attempt_recovery if open? # Double-check after acquiring lock
39
+ end
40
+ end
41
+
42
+ # Apply bulkheading first, outside of any locks
43
+ if @semaphore
44
+ acquired = @semaphore.try_acquire
45
+ unless acquired
46
+ # Reject immediately if we can't acquire semaphore
47
+ return reject_call_bulkhead
48
+ end
49
+ end
50
+
51
+ begin
52
+ @mutex.with_read_lock do
53
+ case status_name
54
+ when :open
55
+ reject_call
56
+ when :half_open
57
+ handle_half_open_status(&block)
58
+ when :closed
59
+ handle_closed_status(&block)
60
+ end
61
+ end
62
+ ensure
63
+ @semaphore&.release if @semaphore && acquired
64
+ end
65
+ end
66
+
67
+ def handle_half_open_status(&)
68
+ # Atomically increment and get the new value
69
+ new_attempts = @half_open_attempts.increment
70
+
71
+ if new_attempts <= @config[:half_open_calls]
72
+ execute_call(&)
73
+ else
74
+ # This thread lost the race, decrement back and reject
75
+ @half_open_attempts.decrement
76
+ reject_call
77
+ end
78
+ end
79
+
80
+ def handle_closed_status(&)
81
+ execute_call(&)
82
+ end
83
+
84
+ def execute_call(&block)
85
+ # Use async version if fiber_safe is enabled
86
+ if @config[:fiber_safe]
87
+ # Ensure async is loaded and included
88
+ Execution.load_async_support unless respond_to?(:execute_call_async)
89
+ return execute_call_async(&block)
90
+ end
91
+
92
+ start_time = BreakerMachines.monotonic_time
93
+
94
+ begin
95
+ # IMPORTANT: We do NOT implement forceful timeouts as they are inherently unsafe
96
+ # The timeout configuration is provided for documentation/intent purposes
97
+ # Users should implement timeouts in their own code using safe mechanisms
98
+ # (e.g., HTTP client timeouts, database statement timeouts, etc.)
99
+ # Log a warning if timeout is configured
100
+ if @config[:timeout] && BreakerMachines.logger && BreakerMachines.config.log_events
101
+ BreakerMachines.logger.warn(
102
+ "[BreakerMachines] Circuit '#{@name}' has timeout configured but " \
103
+ 'forceful timeouts are not implemented for safety. ' \
104
+ 'Please use timeout mechanisms provided by your libraries ' \
105
+ '(e.g., Net::HTTP read_timeout, ActiveRecord statement_timeout).'
106
+ )
107
+ end
108
+
109
+ # Execute with hedged requests if enabled
110
+ result = if @config[:hedged_requests] || @config[:backends]
111
+ execute_hedged(&block)
112
+ else
113
+ block.call
114
+ end
115
+
116
+ record_success(BreakerMachines.monotonic_time - start_time)
117
+ handle_success
118
+ result
119
+ rescue *@config[:exceptions] => e
120
+ record_failure(BreakerMachines.monotonic_time - start_time, e)
121
+ handle_failure
122
+ raise unless @config[:fallback]
123
+
124
+ invoke_fallback(e)
125
+ end
126
+ end
127
+
128
+ def reject_call
129
+ @metrics&.record_rejection(@name)
130
+ invoke_callback(:on_reject)
131
+
132
+ raise BreakerMachines::CircuitOpenError.new(@name, @opened_at.value) unless @config[:fallback]
133
+
134
+ invoke_fallback(BreakerMachines::CircuitOpenError.new(@name, @opened_at.value))
135
+ end
136
+
137
+ def reject_call_bulkhead
138
+ @metrics&.record_rejection(@name)
139
+ invoke_callback(:on_reject)
140
+
141
+ error = BreakerMachines::CircuitBulkheadError.new(@name, @config[:max_concurrent])
142
+ raise error unless @config[:fallback]
143
+
144
+ invoke_fallback(error)
145
+ end
146
+
147
+ def handle_success
148
+ return unless half_open?
149
+
150
+ @mutex.with_write_lock do
151
+ if half_open?
152
+ # Check if all allowed half-open calls have succeeded
153
+ # This ensures the circuit can close even if success_threshold > half_open_calls
154
+ successful_attempts = @half_open_successes.increment
155
+
156
+ # Fast-close logic: Circuit closes if EITHER:
157
+ # 1. All allowed half-open calls succeeded (conservative approach)
158
+ # 2. Success threshold is reached (aggressive approach for quick recovery)
159
+ # This allows flexible configuration - set success_threshold=1 for fast recovery
160
+ # or success_threshold=half_open_calls for cautious recovery
161
+ if successful_attempts >= @config[:half_open_calls] || success_threshold_reached?
162
+ @half_open_attempts.value = 0
163
+ @half_open_successes.value = 0
164
+ reset
165
+ end
166
+ end
167
+ end
168
+ end
169
+
170
+ def handle_failure
171
+ return unless closed? || half_open?
172
+
173
+ @mutex.with_write_lock do
174
+ if closed? && failure_threshold_exceeded?
175
+ trip
176
+ elsif half_open?
177
+ @half_open_attempts.value = 0
178
+ @half_open_successes.value = 0
179
+ trip
180
+ end
181
+ end
182
+ end
183
+
184
+ def failure_threshold_exceeded?
185
+ if @config[:use_rate_threshold]
186
+ # Rate-based threshold
187
+ window = @config[:failure_window]
188
+ failures = @storage.failure_count(@name, window)
189
+ successes = @storage.success_count(@name, window)
190
+ total_calls = failures + successes
191
+
192
+ # Check minimum calls requirement
193
+ return false if total_calls < @config[:minimum_calls]
194
+
195
+ # Calculate failure rate
196
+ failure_rate = failures.to_f / total_calls
197
+ failure_rate >= @config[:failure_rate]
198
+ else
199
+ # Absolute count threshold (existing behavior)
200
+ recent_failures = @storage.failure_count(@name, @config[:failure_window])
201
+ recent_failures >= @config[:failure_threshold]
202
+ end
203
+ end
204
+
205
+ def success_threshold_reached?
206
+ recent_successes = @storage.success_count(@name, @config[:failure_window])
207
+ recent_successes >= @config[:success_threshold]
208
+ end
209
+
210
+ def record_success(duration)
211
+ @metrics&.record_success(@name, duration)
212
+ @storage&.record_success(@name, duration)
213
+ return unless @storage.respond_to?(:record_event_with_details)
214
+
215
+ @storage.record_event_with_details(@name, :success,
216
+ duration)
217
+ end
218
+
219
+ def record_failure(duration, error = nil)
220
+ @last_failure_at.value = BreakerMachines.monotonic_time
221
+ @last_error.value = error if error
222
+ @metrics&.record_failure(@name, duration)
223
+ @storage&.record_failure(@name, duration)
224
+ return unless @storage.respond_to?(:record_event_with_details)
225
+
226
+ @storage.record_event_with_details(@name, :failure, duration,
227
+ error: error)
228
+ end
229
+ end
230
+ end
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