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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +184 -0
- data/ext/breaker_machines_native/extconf.rb +3 -0
- data/lib/breaker_machines/async_circuit.rb +47 -0
- data/lib/breaker_machines/async_support.rb +104 -0
- data/lib/breaker_machines/cascading_circuit.rb +177 -0
- data/lib/breaker_machines/circuit/async_state_management.rb +71 -0
- data/lib/breaker_machines/circuit/base.rb +59 -0
- data/lib/breaker_machines/circuit/callbacks.rb +135 -0
- data/lib/breaker_machines/circuit/configuration.rb +67 -0
- data/lib/breaker_machines/circuit/coordinated_state_management.rb +117 -0
- data/lib/breaker_machines/circuit/execution.rb +231 -0
- data/lib/breaker_machines/circuit/hedged_execution.rb +115 -0
- data/lib/breaker_machines/circuit/introspection.rb +93 -0
- data/lib/breaker_machines/circuit/native.rb +127 -0
- data/lib/breaker_machines/circuit/state_callbacks.rb +72 -0
- data/lib/breaker_machines/circuit/state_management.rb +59 -0
- data/lib/breaker_machines/circuit.rb +8 -0
- data/lib/breaker_machines/circuit_group.rb +153 -0
- data/lib/breaker_machines/console.rb +345 -0
- data/lib/breaker_machines/coordinated_circuit.rb +10 -0
- data/lib/breaker_machines/dsl/cascading_circuit_builder.rb +20 -0
- data/lib/breaker_machines/dsl/circuit_builder.rb +209 -0
- data/lib/breaker_machines/dsl/hedged_builder.rb +21 -0
- data/lib/breaker_machines/dsl/parallel_fallback_wrapper.rb +20 -0
- data/lib/breaker_machines/dsl.rb +283 -0
- data/lib/breaker_machines/errors.rb +71 -0
- data/lib/breaker_machines/hedged_async_support.rb +88 -0
- data/lib/breaker_machines/native_extension.rb +81 -0
- data/lib/breaker_machines/native_speedup.rb +10 -0
- data/lib/breaker_machines/registry.rb +243 -0
- data/lib/breaker_machines/storage/backend_state.rb +69 -0
- data/lib/breaker_machines/storage/base.rb +52 -0
- data/lib/breaker_machines/storage/bucket_memory.rb +176 -0
- data/lib/breaker_machines/storage/cache.rb +169 -0
- data/lib/breaker_machines/storage/fallback_chain.rb +294 -0
- data/lib/breaker_machines/storage/memory.rb +140 -0
- data/lib/breaker_machines/storage/native.rb +93 -0
- data/lib/breaker_machines/storage/null.rb +54 -0
- data/lib/breaker_machines/storage.rb +8 -0
- data/lib/breaker_machines/types.rb +41 -0
- data/lib/breaker_machines/version.rb +5 -0
- data/lib/breaker_machines.rb +200 -0
- data/lib/breaker_machines_native/breaker_machines_native.so +0 -0
- data/sig/README.md +74 -0
- data/sig/all.rbs +25 -0
- data/sig/breaker_machines/circuit.rbs +154 -0
- data/sig/breaker_machines/console.rbs +32 -0
- data/sig/breaker_machines/dsl.rbs +50 -0
- data/sig/breaker_machines/errors.rbs +24 -0
- data/sig/breaker_machines/interfaces.rbs +46 -0
- data/sig/breaker_machines/registry.rbs +30 -0
- data/sig/breaker_machines/storage.rbs +65 -0
- data/sig/breaker_machines/types.rbs +97 -0
- data/sig/breaker_machines.rbs +42 -0
- data/sig/manifest.yaml +5 -0
- metadata +227 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BreakerMachines
|
|
4
|
+
# DSL module for adding circuit breakers to classes
|
|
5
|
+
#
|
|
6
|
+
# This module uses WeakRef to track instances that include the DSL.
|
|
7
|
+
# Why? In long-running applications (web servers, background workers),
|
|
8
|
+
# objects that include this DSL may be created and destroyed frequently.
|
|
9
|
+
# Without WeakRef, the registry would hold strong references to these
|
|
10
|
+
# objects, preventing garbage collection and causing memory leaks.
|
|
11
|
+
#
|
|
12
|
+
# Example scenario: A Rails controller that includes BreakerMachines::DSL
|
|
13
|
+
# is instantiated for each request. Without WeakRef, every controller
|
|
14
|
+
# instance would be kept in memory forever.
|
|
15
|
+
module DSL
|
|
16
|
+
extend ActiveSupport::Concern
|
|
17
|
+
|
|
18
|
+
# Track instances for each class that includes DSL
|
|
19
|
+
# Using WeakRef to allow garbage collection
|
|
20
|
+
@instance_registries = Concurrent::Map.new
|
|
21
|
+
|
|
22
|
+
class_methods do
|
|
23
|
+
# Get or create instance registry for this class
|
|
24
|
+
def instance_registry
|
|
25
|
+
# Access the module-level registry
|
|
26
|
+
registry = BreakerMachines::DSL.instance_variable_get(:@instance_registries)
|
|
27
|
+
registry.compute_if_absent(self) { Concurrent::Array.new }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Clean up dead references in the registry
|
|
31
|
+
def cleanup_instance_registry
|
|
32
|
+
registry = instance_registry
|
|
33
|
+
# Concurrent::Array supports delete_if
|
|
34
|
+
registry.delete_if do |weak_ref|
|
|
35
|
+
weak_ref.__getobj__
|
|
36
|
+
false
|
|
37
|
+
rescue WeakRef::RefError
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def circuit(name, &block)
|
|
43
|
+
@circuits ||= {}
|
|
44
|
+
|
|
45
|
+
if block_given?
|
|
46
|
+
builder = DSL::CircuitBuilder.new
|
|
47
|
+
builder.instance_eval(&block)
|
|
48
|
+
@circuits[name] = builder.config
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
@circuits[name]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Define a cascading circuit breaker that can trip dependent circuits
|
|
55
|
+
def cascade_circuit(name, &block)
|
|
56
|
+
@circuits ||= {}
|
|
57
|
+
|
|
58
|
+
if block_given?
|
|
59
|
+
builder = DSL::CascadingCircuitBuilder.new
|
|
60
|
+
builder.instance_eval(&block)
|
|
61
|
+
@circuits[name] = builder.config.merge(circuit_type: :cascading)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
@circuits[name]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def circuits
|
|
68
|
+
# Start with parent circuits if available
|
|
69
|
+
base_circuits = if superclass.respond_to?(:circuits)
|
|
70
|
+
superclass.circuits.deep_dup
|
|
71
|
+
else
|
|
72
|
+
{}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Merge with our own circuits
|
|
76
|
+
if @circuits
|
|
77
|
+
base_circuits.merge(@circuits)
|
|
78
|
+
else
|
|
79
|
+
base_circuits
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Define reusable circuit templates
|
|
84
|
+
def circuit_template(name, &block)
|
|
85
|
+
@circuit_templates ||= {}
|
|
86
|
+
|
|
87
|
+
if block_given?
|
|
88
|
+
builder = DSL::CircuitBuilder.new
|
|
89
|
+
builder.instance_eval(&block)
|
|
90
|
+
@circuit_templates[name] = builder.config
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
@circuit_templates[name]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Get all circuit templates
|
|
97
|
+
def circuit_templates
|
|
98
|
+
# Start with parent templates if available
|
|
99
|
+
base_templates = if superclass.respond_to?(:circuit_templates)
|
|
100
|
+
superclass.circuit_templates.deep_dup
|
|
101
|
+
else
|
|
102
|
+
{}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Merge with our own templates
|
|
106
|
+
if @circuit_templates
|
|
107
|
+
base_templates.merge(@circuit_templates)
|
|
108
|
+
else
|
|
109
|
+
base_templates
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Get circuit definitions without sensitive data
|
|
114
|
+
def circuit_definitions
|
|
115
|
+
circuits.transform_values { |config| config.except(:owner, :storage, :metrics) }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Reset all circuits for all instances of this class
|
|
119
|
+
def reset_all_circuits
|
|
120
|
+
cleanup_instance_registry # Clean up dead refs first
|
|
121
|
+
|
|
122
|
+
instance_registry.each do |weak_ref|
|
|
123
|
+
instance = weak_ref.__getobj__
|
|
124
|
+
circuit_instances = instance.instance_variable_get(:@circuit_instances)
|
|
125
|
+
circuit_instances&.each_value(&:hard_reset!)
|
|
126
|
+
rescue WeakRef::RefError
|
|
127
|
+
# Instance was garbage collected, skip it
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Get aggregated stats for all circuits of this class
|
|
132
|
+
def circuit_stats
|
|
133
|
+
stats = Hash.new { |h, k| h[k] = { total: 0, by_state: {} } }
|
|
134
|
+
cleanup_instance_registry # Clean up dead refs first
|
|
135
|
+
|
|
136
|
+
instance_registry.each do |weak_ref|
|
|
137
|
+
instance = weak_ref.__getobj__
|
|
138
|
+
circuit_instances = instance.instance_variable_get(:@circuit_instances)
|
|
139
|
+
next unless circuit_instances
|
|
140
|
+
|
|
141
|
+
circuit_instances.each do |name, circuit|
|
|
142
|
+
stats[name][:total] += 1
|
|
143
|
+
state = circuit.status_name
|
|
144
|
+
stats[name][:by_state][state] ||= 0
|
|
145
|
+
stats[name][:by_state][state] += 1
|
|
146
|
+
end
|
|
147
|
+
rescue WeakRef::RefError
|
|
148
|
+
# Instance was garbage collected, skip it
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
stats
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Use included callback to add instance tracking
|
|
156
|
+
def self.included(base)
|
|
157
|
+
super
|
|
158
|
+
|
|
159
|
+
# Hook into new to register instances
|
|
160
|
+
base.singleton_class.prepend(Module.new do
|
|
161
|
+
def new(...)
|
|
162
|
+
instance = super
|
|
163
|
+
instance_registry << WeakRef.new(instance)
|
|
164
|
+
instance
|
|
165
|
+
end
|
|
166
|
+
end)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def circuit(name)
|
|
170
|
+
self.class.circuits[name] ||= {}
|
|
171
|
+
@circuit_instances ||= {}
|
|
172
|
+
|
|
173
|
+
config = self.class.circuits[name].merge(owner: self)
|
|
174
|
+
circuit_type = config.delete(:circuit_type)
|
|
175
|
+
|
|
176
|
+
@circuit_instances[name] ||= case circuit_type
|
|
177
|
+
when :cascading
|
|
178
|
+
CascadingCircuit.new(name, config)
|
|
179
|
+
else
|
|
180
|
+
Circuit.new(name, config)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Create a dynamic circuit breaker with inline configuration
|
|
185
|
+
# Options:
|
|
186
|
+
# global: true - Store circuit globally, preventing memory leaks in long-lived objects
|
|
187
|
+
# global: false - Store circuit locally in this instance (default, backward compatible)
|
|
188
|
+
def dynamic_circuit(name, template: nil, global: false, &config_block)
|
|
189
|
+
# Start with template config if provided
|
|
190
|
+
base_config = if template && self.class.circuit_templates[template]
|
|
191
|
+
self.class.circuit_templates[template].deep_dup
|
|
192
|
+
else
|
|
193
|
+
default_circuit_config
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Apply additional configuration if block provided
|
|
197
|
+
if config_block
|
|
198
|
+
builder = DSL::CircuitBuilder.new
|
|
199
|
+
builder.instance_variable_set(:@config, base_config.deep_dup)
|
|
200
|
+
builder.instance_eval(&config_block)
|
|
201
|
+
base_config = builder.config
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
if global
|
|
205
|
+
# Use global registry to prevent memory leaks
|
|
206
|
+
BreakerMachines.registry.get_or_create_dynamic_circuit(name, self, base_config)
|
|
207
|
+
else
|
|
208
|
+
# Local storage (backward compatible)
|
|
209
|
+
@circuit_instances ||= {}
|
|
210
|
+
@circuit_instances[name] ||= Circuit.new(name, base_config.merge(owner: self))
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Apply a template to an existing or new circuit
|
|
215
|
+
def apply_template(circuit_name, template_name)
|
|
216
|
+
template_config = self.class.circuit_templates[template_name]
|
|
217
|
+
raise ArgumentError, "Template '#{template_name}' not found" unless template_config
|
|
218
|
+
|
|
219
|
+
@circuit_instances ||= {}
|
|
220
|
+
@circuit_instances[circuit_name] = Circuit.new(circuit_name, template_config.merge(owner: self))
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
private
|
|
224
|
+
|
|
225
|
+
def default_circuit_config
|
|
226
|
+
{
|
|
227
|
+
failure_threshold: 5,
|
|
228
|
+
failure_window: 60,
|
|
229
|
+
success_threshold: 1,
|
|
230
|
+
timeout: nil,
|
|
231
|
+
reset_timeout: 60,
|
|
232
|
+
half_open_calls: 1,
|
|
233
|
+
exceptions: [StandardError],
|
|
234
|
+
storage: nil,
|
|
235
|
+
metrics: nil,
|
|
236
|
+
fallback: nil,
|
|
237
|
+
on_open: nil,
|
|
238
|
+
on_close: nil,
|
|
239
|
+
on_half_open: nil,
|
|
240
|
+
on_reject: nil,
|
|
241
|
+
notifications: [],
|
|
242
|
+
fiber_safe: BreakerMachines.config.fiber_safe
|
|
243
|
+
}
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
public
|
|
247
|
+
|
|
248
|
+
# Get all circuit instances for this object
|
|
249
|
+
def circuit_instances
|
|
250
|
+
@circuit_instances || {}
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Get summary of all circuits for this instance
|
|
254
|
+
def circuits_summary
|
|
255
|
+
circuit_instances.transform_values(&:summary)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Get detailed information for all circuits
|
|
259
|
+
def circuits_report
|
|
260
|
+
circuit_instances.transform_values(&:to_h)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Reset all circuits for this instance
|
|
264
|
+
def reset_all_circuits
|
|
265
|
+
circuit_instances.each_value(&:hard_reset!)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Remove a global dynamic circuit by name
|
|
269
|
+
def remove_dynamic_circuit(name)
|
|
270
|
+
BreakerMachines.registry.remove_dynamic_circuit(name)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Get all dynamic circuit names from global registry
|
|
274
|
+
def dynamic_circuit_names
|
|
275
|
+
BreakerMachines.registry.dynamic_circuit_names
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Cleanup stale dynamic circuits (global)
|
|
279
|
+
def cleanup_stale_dynamic_circuits(max_age_seconds = 3600)
|
|
280
|
+
BreakerMachines.registry.cleanup_stale_dynamic_circuits(max_age_seconds)
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BreakerMachines
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
# Raised when attempting to use a circuit that is in the open state
|
|
7
|
+
class CircuitOpenError < Error
|
|
8
|
+
attr_reader :circuit_name, :opened_at
|
|
9
|
+
|
|
10
|
+
def initialize(circuit_name, opened_at = nil)
|
|
11
|
+
@circuit_name = circuit_name
|
|
12
|
+
@opened_at = opened_at
|
|
13
|
+
super("Circuit '#{circuit_name}' is open")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Raised when a circuit cannot be called due to unmet dependencies
|
|
18
|
+
class CircuitDependencyError < CircuitOpenError
|
|
19
|
+
def initialize(circuit_name, message = nil)
|
|
20
|
+
@circuit_name = circuit_name
|
|
21
|
+
@opened_at = nil
|
|
22
|
+
super_message = message || "Circuit '#{circuit_name}' cannot be called: dependencies not met"
|
|
23
|
+
Error.instance_method(:initialize).bind(self).call(super_message)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Raised when a circuit-protected call exceeds the configured timeout
|
|
28
|
+
class CircuitTimeoutError < Error
|
|
29
|
+
attr_reader :circuit_name, :timeout
|
|
30
|
+
|
|
31
|
+
def initialize(circuit_name, timeout)
|
|
32
|
+
@circuit_name = circuit_name
|
|
33
|
+
@timeout = timeout
|
|
34
|
+
super("Circuit '#{circuit_name}' timed out after #{timeout}s")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class ConfigurationError < Error; end
|
|
39
|
+
class StorageError < Error; end
|
|
40
|
+
|
|
41
|
+
# Raised when storage backend operation times out
|
|
42
|
+
class StorageTimeoutError < StorageError
|
|
43
|
+
attr_reader :timeout_ms
|
|
44
|
+
|
|
45
|
+
def initialize(message, timeout_ms = nil)
|
|
46
|
+
@timeout_ms = timeout_ms
|
|
47
|
+
super(message)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Raised when circuit rejects call due to bulkhead limit
|
|
52
|
+
class CircuitBulkheadError < Error
|
|
53
|
+
attr_reader :circuit_name, :max_concurrent
|
|
54
|
+
|
|
55
|
+
def initialize(circuit_name, max_concurrent)
|
|
56
|
+
@circuit_name = circuit_name
|
|
57
|
+
@max_concurrent = max_concurrent
|
|
58
|
+
super("Circuit '#{circuit_name}' rejected call: max concurrent limit of #{max_concurrent} reached")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Raised when all parallel fallbacks fail
|
|
63
|
+
class ParallelFallbackError < Error
|
|
64
|
+
attr_reader :errors
|
|
65
|
+
|
|
66
|
+
def initialize(message, errors)
|
|
67
|
+
@errors = errors
|
|
68
|
+
super(message)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This file contains async support for hedged execution
|
|
4
|
+
# It is only loaded when fiber_safe mode is enabled
|
|
5
|
+
# Requires async gem ~> 2.31.0 for Promise and modern API features
|
|
6
|
+
|
|
7
|
+
require 'async'
|
|
8
|
+
require 'async/task'
|
|
9
|
+
require 'async/promise'
|
|
10
|
+
require 'async/barrier'
|
|
11
|
+
require 'concurrent'
|
|
12
|
+
|
|
13
|
+
module BreakerMachines
|
|
14
|
+
# AsyncSupport for HedgedExecution
|
|
15
|
+
module HedgedAsyncSupport
|
|
16
|
+
# Execute hedged requests with configurable delay between attempts
|
|
17
|
+
# @param callables [Array<Proc>] Array of callables to execute
|
|
18
|
+
# @param delay_ms [Integer] Milliseconds to wait before starting hedged requests
|
|
19
|
+
# @return [Object] Result from the first successful callable
|
|
20
|
+
# @raise [StandardError] If all callables fail
|
|
21
|
+
def execute_hedged_with_async(callables, delay_ms)
|
|
22
|
+
race_tasks(callables, delay_ms: delay_ms)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Execute parallel fallbacks without delay
|
|
26
|
+
# @param fallbacks [Array<Proc,Object>] Array of fallback values or callables
|
|
27
|
+
# @return [Object] Result from the first successful fallback
|
|
28
|
+
# @raise [StandardError] If all fallbacks fail
|
|
29
|
+
def execute_parallel_fallbacks_async(fallbacks)
|
|
30
|
+
# Normalize fallbacks to callables
|
|
31
|
+
callables = fallbacks.map do |fallback|
|
|
32
|
+
case fallback
|
|
33
|
+
when Proc
|
|
34
|
+
# Handle procs with different arities
|
|
35
|
+
-> { fallback.arity == 1 ? fallback.call(nil) : fallback.call }
|
|
36
|
+
else
|
|
37
|
+
# Wrap static values in callables
|
|
38
|
+
-> { fallback }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
race_tasks(callables, delay_ms: 0)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# Race callables; return first result or raise if it was an Exception
|
|
48
|
+
# Uses modern Async::Promise and Async::Barrier for cleaner synchronization
|
|
49
|
+
# @param callables [Array<Proc>] Tasks to race
|
|
50
|
+
# @param delay_ms [Integer] Delay in milliseconds between task starts
|
|
51
|
+
# @return [Object] First successful result
|
|
52
|
+
# @raise [Exception] The first exception received
|
|
53
|
+
def race_tasks(callables, delay_ms: 0)
|
|
54
|
+
promise = Async::Promise.new
|
|
55
|
+
barrier = Async::Barrier.new
|
|
56
|
+
|
|
57
|
+
begin
|
|
58
|
+
result = Async do
|
|
59
|
+
callables.each_with_index do |callable, idx|
|
|
60
|
+
barrier.async do
|
|
61
|
+
# stagger hedged attempts
|
|
62
|
+
sleep(delay_ms / 1000.0) if idx.positive? && delay_ms.positive?
|
|
63
|
+
|
|
64
|
+
begin
|
|
65
|
+
result = callable.call
|
|
66
|
+
# Try to resolve the promise with this result
|
|
67
|
+
# Only the first resolution will succeed
|
|
68
|
+
promise.resolve(result) unless promise.resolved?
|
|
69
|
+
rescue StandardError => e
|
|
70
|
+
# Only set exception if no result has been resolved yet
|
|
71
|
+
promise.resolve(e) unless promise.resolved?
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Wait for the first resolution (either success or exception)
|
|
77
|
+
promise.wait
|
|
78
|
+
end.wait
|
|
79
|
+
|
|
80
|
+
# If result is an exception, raise it; otherwise return the result
|
|
81
|
+
result.is_a?(StandardError) ? raise(result) : result
|
|
82
|
+
ensure
|
|
83
|
+
# Ensure all tasks are stopped
|
|
84
|
+
barrier&.stop
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rbconfig'
|
|
4
|
+
require 'active_support/core_ext/object/blank'
|
|
5
|
+
|
|
6
|
+
module BreakerMachines
|
|
7
|
+
# Handles loading and status of the optional native extension
|
|
8
|
+
module NativeExtension
|
|
9
|
+
class << self
|
|
10
|
+
# Load the native extension and set availability flag
|
|
11
|
+
# Can be called multiple times - subsequent calls are memoized
|
|
12
|
+
def load!
|
|
13
|
+
return @loaded if defined?(@loaded)
|
|
14
|
+
|
|
15
|
+
# Native extension is opt-in: only load if explicitly enabled
|
|
16
|
+
unless ENV['BREAKER_MACHINES_NATIVE'] == '1'
|
|
17
|
+
@loaded = false
|
|
18
|
+
BreakerMachines.instance_variable_set(:@native_available, false)
|
|
19
|
+
return false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
errors = []
|
|
23
|
+
|
|
24
|
+
native_library_candidates.each do |require_path|
|
|
25
|
+
try_require(require_path)
|
|
26
|
+
@loaded = true
|
|
27
|
+
BreakerMachines.instance_variable_set(:@native_available, true)
|
|
28
|
+
BreakerMachines.log(:info, "Native extension loaded successfully (#{require_path})")
|
|
29
|
+
return true
|
|
30
|
+
rescue LoadError => e
|
|
31
|
+
errors << "#{require_path}: #{e.message}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@loaded = false
|
|
35
|
+
BreakerMachines.instance_variable_set(:@native_available, false)
|
|
36
|
+
BreakerMachines.log(:warn, "Native extension not available: #{errors.join(' | ')}") unless errors.empty?
|
|
37
|
+
|
|
38
|
+
false
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Check if load was attempted
|
|
42
|
+
def loaded?
|
|
43
|
+
defined?(@loaded) && @loaded
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def native_library_candidates
|
|
49
|
+
dlext = RbConfig::CONFIG['DLEXT']
|
|
50
|
+
base_dir = File.expand_path('../breaker_machines_native', __dir__)
|
|
51
|
+
ruby_version = RbConfig::CONFIG['ruby_version']
|
|
52
|
+
arch = RbConfig::CONFIG['arch']
|
|
53
|
+
platform = Gem::Platform.local.to_s
|
|
54
|
+
|
|
55
|
+
matches = Dir.glob(File.join(base_dir, '**', "breaker_machines_native.#{dlext}"))
|
|
56
|
+
|
|
57
|
+
prioritized = matches.sort_by do |path|
|
|
58
|
+
score = 0
|
|
59
|
+
score -= 3 if path.include?(ruby_version)
|
|
60
|
+
score -= 2 if path.include?(arch)
|
|
61
|
+
score -= 1 if path.include?(platform)
|
|
62
|
+
[score, path]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
candidates = prioritized.map { |full_path| require_path_for(full_path, dlext) }.presence
|
|
66
|
+
candidates ||= ['breaker_machines_native/breaker_machines_native']
|
|
67
|
+
candidates.uniq
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def require_path_for(full_path, dlext)
|
|
71
|
+
root = File.expand_path('..', __dir__)
|
|
72
|
+
relative = full_path.sub(%r{^#{Regexp.escape(root)}/?}, '')
|
|
73
|
+
relative.sub(/\.#{Regexp.escape(dlext)}\z/, '')
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def try_require(require_path)
|
|
77
|
+
require require_path
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'native_extension'
|
|
4
|
+
|
|
5
|
+
# Load the native extension if available
|
|
6
|
+
if BreakerMachines::NativeExtension.load!
|
|
7
|
+
# Only load Storage::Native if native extension loaded successfully
|
|
8
|
+
# This prevents referencing BreakerMachinesNative::Storage when not available
|
|
9
|
+
require_relative 'storage/native'
|
|
10
|
+
end
|