breaker_machines 0.5.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.
- checksums.yaml +4 -4
- data/ext/breaker_machines_native/Cargo.toml +8 -0
- data/ext/breaker_machines_native/core/Cargo.toml +18 -0
- data/ext/breaker_machines_native/core/examples/basic.rs +61 -0
- data/ext/breaker_machines_native/core/src/builder.rs +232 -0
- data/ext/breaker_machines_native/core/src/bulkhead.rs +223 -0
- data/ext/breaker_machines_native/core/src/callbacks.rs +58 -0
- data/ext/breaker_machines_native/core/src/circuit.rs +1156 -0
- data/ext/breaker_machines_native/core/src/classifier.rs +177 -0
- data/ext/breaker_machines_native/core/src/errors.rs +47 -0
- data/ext/breaker_machines_native/core/src/lib.rs +62 -0
- data/ext/breaker_machines_native/core/src/storage.rs +377 -0
- data/ext/breaker_machines_native/extconf.rb +40 -0
- data/ext/breaker_machines_native/ffi/Cargo.toml +16 -0
- data/ext/breaker_machines_native/ffi/src/lib.rs +218 -0
- data/ext/breaker_machines_native/target/debug/build/clang-sys-d961dfabd5f43fba/out/common.rs +355 -0
- data/ext/breaker_machines_native/target/debug/build/clang-sys-d961dfabd5f43fba/out/dynamic.rs +276 -0
- data/ext/breaker_machines_native/target/debug/build/clang-sys-d961dfabd5f43fba/out/macros.rs +49 -0
- 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
- 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
- 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
- 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
- data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/Cargo.toml +48 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/examples/basic.rs +61 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/builder.rs +154 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/callbacks.rs +55 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/circuit.rs +607 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/errors.rs +38 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/lib.rs +58 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/storage.rs +377 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/Cargo.toml +48 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/examples/basic.rs +61 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/builder.rs +173 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/callbacks.rs +55 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/circuit.rs +855 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/errors.rs +38 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/lib.rs +58 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/storage.rs +377 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/Cargo.toml +48 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/examples/basic.rs +61 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/builder.rs +154 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/callbacks.rs +55 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/circuit.rs +607 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/errors.rs +38 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/lib.rs +58 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/storage.rs +377 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/Cargo.toml +48 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/examples/basic.rs +61 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/builder.rs +232 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/bulkhead.rs +223 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/callbacks.rs +58 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/circuit.rs +1156 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/classifier.rs +177 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/errors.rs +47 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/lib.rs +62 -0
- data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/storage.rs +377 -0
- data/ext/breaker_machines_native/target/release/build/clang-sys-ef8ad8b846ac8b75/out/common.rs +355 -0
- data/ext/breaker_machines_native/target/release/build/clang-sys-ef8ad8b846ac8b75/out/dynamic.rs +276 -0
- data/ext/breaker_machines_native/target/release/build/clang-sys-ef8ad8b846ac8b75/out/macros.rs +49 -0
- 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
- data/lib/breaker_machines/circuit/async_state_management.rb +1 -1
- data/lib/breaker_machines/circuit/base.rb +2 -1
- data/lib/breaker_machines/circuit/coordinated_state_management.rb +3 -3
- data/lib/breaker_machines/circuit/native.rb +127 -0
- data/lib/breaker_machines/circuit/state_callbacks.rb +17 -5
- data/lib/breaker_machines/circuit/state_management.rb +1 -1
- data/lib/breaker_machines/native_extension.rb +36 -0
- data/lib/breaker_machines/native_speedup.rb +6 -0
- data/lib/breaker_machines/storage/bucket_memory.rb +4 -1
- data/lib/breaker_machines/storage/memory.rb +4 -1
- data/lib/breaker_machines/storage/native.rb +90 -0
- data/lib/breaker_machines/version.rb +1 -1
- data/lib/breaker_machines.rb +98 -11
- data/lib/breaker_machines_native/breaker_machines_native.bundle +0 -0
- data/sig/breaker_machines.rbs +20 -8
- metadata +99 -6
|
@@ -41,7 +41,7 @@ module BreakerMachines
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
before_transition on: :hard_reset do |circuit|
|
|
44
|
-
circuit.storage
|
|
44
|
+
circuit.storage&.clear(circuit.name)
|
|
45
45
|
circuit.half_open_attempts.value = 0
|
|
46
46
|
circuit.half_open_successes.value = 0
|
|
47
47
|
end
|
|
@@ -16,7 +16,8 @@ module BreakerMachines
|
|
|
16
16
|
include Circuit::Callbacks
|
|
17
17
|
include Circuit::StateCallbacks
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
# name/config/opened_at readers are defined in Circuit::Configuration
|
|
20
|
+
attr_reader :storage, :metrics, :semaphore
|
|
20
21
|
attr_reader :half_open_attempts, :half_open_successes, :mutex
|
|
21
22
|
attr_reader :last_failure_at, :last_error
|
|
22
23
|
end
|
|
@@ -17,12 +17,12 @@ module BreakerMachines
|
|
|
17
17
|
|
|
18
18
|
event :attempt_recovery do
|
|
19
19
|
transition open: :half_open,
|
|
20
|
-
if:
|
|
20
|
+
if: lambda(&:recovery_allowed?)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
event :reset do
|
|
24
24
|
transition %i[open half_open] => :closed,
|
|
25
|
-
if:
|
|
25
|
+
if: lambda(&:reset_allowed?)
|
|
26
26
|
transition closed: :closed
|
|
27
27
|
end
|
|
28
28
|
|
|
@@ -39,7 +39,7 @@ module BreakerMachines
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
before_transition on: :hard_reset do |circuit|
|
|
42
|
-
circuit.storage
|
|
42
|
+
circuit.storage&.clear(circuit.name)
|
|
43
43
|
circuit.half_open_attempts.value = 0
|
|
44
44
|
circuit.half_open_successes.value = 0
|
|
45
45
|
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
|
|
@@ -47,14 +47,26 @@ module BreakerMachines
|
|
|
47
47
|
def reset_timeout_elapsed?
|
|
48
48
|
return false unless @opened_at.value
|
|
49
49
|
|
|
50
|
-
# Add jitter to prevent thundering herd
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
55
57
|
|
|
56
58
|
BreakerMachines.monotonic_time - @opened_at.value >= timeout_with_jitter
|
|
57
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
|
|
58
70
|
end
|
|
59
71
|
end
|
|
60
72
|
end
|
|
@@ -36,7 +36,7 @@ module BreakerMachines
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
before_transition on: :hard_reset do |circuit|
|
|
39
|
-
circuit.storage
|
|
39
|
+
circuit.storage&.clear(circuit.name)
|
|
40
40
|
circuit.half_open_attempts.value = 0
|
|
41
41
|
circuit.half_open_successes.value = 0
|
|
42
42
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BreakerMachines
|
|
4
|
+
# Handles loading and status of the optional native extension
|
|
5
|
+
module NativeExtension
|
|
6
|
+
class << self
|
|
7
|
+
# Load the native extension and set availability flag
|
|
8
|
+
# Can be called multiple times - subsequent calls are memoized
|
|
9
|
+
def load!
|
|
10
|
+
return @loaded if defined?(@loaded)
|
|
11
|
+
|
|
12
|
+
@loaded = true
|
|
13
|
+
require 'breaker_machines_native/breaker_machines_native'
|
|
14
|
+
BreakerMachines.instance_variable_set(:@native_available, true)
|
|
15
|
+
BreakerMachines.log(:info, 'Native extension loaded successfully')
|
|
16
|
+
true
|
|
17
|
+
rescue LoadError => e
|
|
18
|
+
@loaded = false
|
|
19
|
+
BreakerMachines.instance_variable_set(:@native_available, false)
|
|
20
|
+
|
|
21
|
+
# Only log if it's not JRuby (expected failure) and logging is enabled
|
|
22
|
+
if RUBY_ENGINE != 'jruby'
|
|
23
|
+
BreakerMachines.log(:warn, "Native extension not available: #{e.message}")
|
|
24
|
+
BreakerMachines.log(:warn, 'Using pure Ruby backend (slower but functional)')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Check if load was attempted
|
|
31
|
+
def loaded?
|
|
32
|
+
defined?(@loaded) && @loaded
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -21,6 +21,8 @@ module BreakerMachines
|
|
|
21
21
|
@event_logs = Concurrent::Map.new
|
|
22
22
|
@bucket_count = options[:bucket_count] || 300 # Default 5 minutes
|
|
23
23
|
@max_events = options[:max_events] || 100
|
|
24
|
+
# Store creation time as anchor for relative timestamps (like Rust implementation)
|
|
25
|
+
@start_time = BreakerMachines.monotonic_time
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
def get_status(circuit_name)
|
|
@@ -160,7 +162,8 @@ module BreakerMachines
|
|
|
160
162
|
end
|
|
161
163
|
|
|
162
164
|
def monotonic_time
|
|
163
|
-
|
|
165
|
+
# Return time relative to storage creation (matches Rust implementation)
|
|
166
|
+
BreakerMachines.monotonic_time - @start_time
|
|
164
167
|
end
|
|
165
168
|
|
|
166
169
|
def with_timeout(_timeout_ms)
|
|
@@ -17,6 +17,8 @@ module BreakerMachines
|
|
|
17
17
|
@events = Concurrent::Map.new
|
|
18
18
|
@event_logs = Concurrent::Map.new
|
|
19
19
|
@max_events = options[:max_events] || 100
|
|
20
|
+
# Store creation time as anchor for relative timestamps (like Rust implementation)
|
|
21
|
+
@start_time = BreakerMachines.monotonic_time
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
def get_status(circuit_name)
|
|
@@ -130,7 +132,8 @@ module BreakerMachines
|
|
|
130
132
|
end
|
|
131
133
|
|
|
132
134
|
def monotonic_time
|
|
133
|
-
|
|
135
|
+
# Return time relative to storage creation (matches Rust implementation)
|
|
136
|
+
BreakerMachines.monotonic_time - @start_time
|
|
134
137
|
end
|
|
135
138
|
end
|
|
136
139
|
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BreakerMachines
|
|
4
|
+
module Storage
|
|
5
|
+
# Native extension storage backend for high-performance event tracking
|
|
6
|
+
#
|
|
7
|
+
# This backend provides identical functionality to Memory storage but with
|
|
8
|
+
# significantly better performance for sliding window calculations. It's
|
|
9
|
+
# particularly beneficial for high-throughput applications where circuit
|
|
10
|
+
# breaker state checks happen on every request.
|
|
11
|
+
#
|
|
12
|
+
# Performance: ~63x faster than Memory storage for sliding window calculations
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
# BreakerMachines.configure do |config|
|
|
16
|
+
# config.default_storage = :native
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# Fallback: If the native extension isn't available (e.g., on JRuby or if
|
|
20
|
+
# Rust wasn't installed during gem installation), this will raise LoadError.
|
|
21
|
+
# Use Storage::Memory as a fallback in such cases.
|
|
22
|
+
class Native < Base
|
|
23
|
+
def initialize(**options)
|
|
24
|
+
super
|
|
25
|
+
unless defined?(BreakerMachinesNative::Storage)
|
|
26
|
+
raise LoadError, 'Native extension not available. Use Storage::Memory instead.'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
@native = BreakerMachinesNative::Storage.new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def get_status(_circuit_name)
|
|
33
|
+
# Status is still managed by Ruby layer
|
|
34
|
+
# This storage backend only handles event tracking
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def set_status(circuit_name, status, opened_at = nil)
|
|
39
|
+
# Status management delegated to Ruby layer
|
|
40
|
+
# This backend focuses on high-performance event counting
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def record_success(circuit_name, duration)
|
|
44
|
+
@native.record_success(circuit_name.to_s, duration.to_f)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def record_failure(circuit_name, duration)
|
|
48
|
+
@native.record_failure(circuit_name.to_s, duration.to_f)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def success_count(circuit_name, window_seconds)
|
|
52
|
+
@native.success_count(circuit_name.to_s, window_seconds.to_f)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def failure_count(circuit_name, window_seconds)
|
|
56
|
+
@native.failure_count(circuit_name.to_s, window_seconds.to_f)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def clear(circuit_name)
|
|
60
|
+
@native.clear(circuit_name.to_s)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def clear_all
|
|
64
|
+
@native.clear_all
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def record_event_with_details(circuit_name, type, duration, error: nil, new_state: nil)
|
|
68
|
+
# Basic event recording (native extension handles type and duration)
|
|
69
|
+
case type
|
|
70
|
+
when :success
|
|
71
|
+
record_success(circuit_name, duration)
|
|
72
|
+
when :failure
|
|
73
|
+
record_failure(circuit_name, duration)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# NOTE: Error and state details not tracked in native backend
|
|
77
|
+
# This is intentional for performance - use Memory backend if you need full event details
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def event_log(circuit_name, limit)
|
|
81
|
+
@native.event_log(circuit_name.to_s, limit)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def with_timeout(_timeout_ms)
|
|
85
|
+
# Native operations should be instant
|
|
86
|
+
yield
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
data/lib/breaker_machines.rb
CHANGED
|
@@ -15,32 +15,94 @@ loader.ignore("#{__dir__}/breaker_machines/console.rb")
|
|
|
15
15
|
loader.ignore("#{__dir__}/breaker_machines/async_support.rb")
|
|
16
16
|
loader.ignore("#{__dir__}/breaker_machines/hedged_async_support.rb")
|
|
17
17
|
loader.ignore("#{__dir__}/breaker_machines/circuit/async_state_management.rb")
|
|
18
|
+
loader.ignore("#{__dir__}/breaker_machines/native_speedup.rb")
|
|
19
|
+
loader.ignore("#{__dir__}/breaker_machines/native_extension.rb")
|
|
18
20
|
loader.setup
|
|
19
21
|
|
|
20
22
|
# BreakerMachines provides a thread-safe implementation of the Circuit Breaker pattern
|
|
21
23
|
# for Ruby applications, helping to prevent cascading failures in distributed systems.
|
|
22
24
|
module BreakerMachines
|
|
25
|
+
# Global configuration class for BreakerMachines
|
|
26
|
+
class Configuration
|
|
27
|
+
attr_accessor :default_storage,
|
|
28
|
+
:default_timeout,
|
|
29
|
+
:default_reset_timeout,
|
|
30
|
+
:default_failure_threshold,
|
|
31
|
+
:log_events,
|
|
32
|
+
:fiber_safe
|
|
33
|
+
|
|
34
|
+
def initialize
|
|
35
|
+
@default_storage = :bucket_memory
|
|
36
|
+
@default_timeout = nil
|
|
37
|
+
@default_reset_timeout = 60.seconds
|
|
38
|
+
@default_failure_threshold = 5
|
|
39
|
+
@log_events = true
|
|
40
|
+
@fiber_safe = false
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
23
44
|
class << self
|
|
24
45
|
def loader
|
|
25
46
|
loader
|
|
26
47
|
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Global configuration
|
|
30
|
-
include ActiveSupport::Configurable
|
|
31
48
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
config_accessor :default_failure_threshold, default: 5
|
|
36
|
-
config_accessor :log_events, default: true
|
|
37
|
-
config_accessor :fiber_safe, default: false
|
|
49
|
+
def config
|
|
50
|
+
@config ||= Configuration.new
|
|
51
|
+
end
|
|
38
52
|
|
|
39
|
-
class << self
|
|
40
53
|
def configure
|
|
41
54
|
yield config
|
|
42
55
|
end
|
|
43
56
|
|
|
57
|
+
# Delegate config attributes to config object for backward compatibility
|
|
58
|
+
def default_storage
|
|
59
|
+
config.default_storage
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def default_storage=(value)
|
|
63
|
+
config.default_storage = value
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def default_timeout
|
|
67
|
+
config.default_timeout
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def default_timeout=(value)
|
|
71
|
+
config.default_timeout = value
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def default_reset_timeout
|
|
75
|
+
config.default_reset_timeout
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def default_reset_timeout=(value)
|
|
79
|
+
config.default_reset_timeout = value
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def default_failure_threshold
|
|
83
|
+
config.default_failure_threshold
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def default_failure_threshold=(value)
|
|
87
|
+
config.default_failure_threshold = value
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def log_events
|
|
91
|
+
config.log_events
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def log_events=(value)
|
|
95
|
+
config.log_events = value
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def fiber_safe
|
|
99
|
+
config.fiber_safe
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def fiber_safe=(value)
|
|
103
|
+
config.fiber_safe = value
|
|
104
|
+
end
|
|
105
|
+
|
|
44
106
|
def setup_notifications
|
|
45
107
|
return unless config.log_events
|
|
46
108
|
|
|
@@ -65,12 +127,27 @@ module BreakerMachines
|
|
|
65
127
|
|
|
66
128
|
attr_writer :logger
|
|
67
129
|
|
|
130
|
+
# Centralized logging helper
|
|
131
|
+
# @param level [Symbol] log level (:debug, :info, :warn, :error)
|
|
132
|
+
# @param message [String] message to log
|
|
133
|
+
def log(level, message)
|
|
134
|
+
return unless config.log_events && logger
|
|
135
|
+
|
|
136
|
+
logger.public_send(level, "[BreakerMachines] #{message}")
|
|
137
|
+
end
|
|
138
|
+
|
|
68
139
|
def instrument(event, payload = {})
|
|
69
140
|
return unless config.log_events
|
|
70
141
|
|
|
71
142
|
ActiveSupport::Notifications.instrument("breaker_machines.#{event}", payload)
|
|
72
143
|
end
|
|
73
144
|
|
|
145
|
+
# Check if native extension is available
|
|
146
|
+
# @return [Boolean] true if native extension loaded successfully
|
|
147
|
+
def native_available?
|
|
148
|
+
@native_available || false
|
|
149
|
+
end
|
|
150
|
+
|
|
74
151
|
# Launch the interactive console
|
|
75
152
|
def console
|
|
76
153
|
require_relative 'breaker_machines/console'
|
|
@@ -111,3 +188,13 @@ module BreakerMachines
|
|
|
111
188
|
# Set up notifications on first use
|
|
112
189
|
setup_notifications if config.log_events
|
|
113
190
|
end
|
|
191
|
+
|
|
192
|
+
# Load optional native speedup after core is loaded
|
|
193
|
+
# Opt-in with BREAKER_MACHINES_NATIVE=1 to enable native extensions
|
|
194
|
+
if ENV['BREAKER_MACHINES_NATIVE'] == '1'
|
|
195
|
+
begin
|
|
196
|
+
require_relative 'breaker_machines/native_speedup'
|
|
197
|
+
rescue LoadError
|
|
198
|
+
# Native gem not available, skip native support
|
|
199
|
+
end
|
|
200
|
+
end
|
|
Binary file
|
data/sig/breaker_machines.rbs
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
module BreakerMachines
|
|
2
2
|
VERSION: String
|
|
3
3
|
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor default_storage: (:memory | :bucket_memory | :null | untyped)
|
|
6
|
+
attr_accessor default_timeout: Integer?
|
|
7
|
+
attr_accessor default_reset_timeout: Integer
|
|
8
|
+
attr_accessor default_failure_threshold: Integer
|
|
9
|
+
attr_accessor log_events: bool
|
|
10
|
+
attr_accessor fiber_safe: bool
|
|
11
|
+
|
|
12
|
+
def initialize: () -> void
|
|
13
|
+
end
|
|
14
|
+
|
|
4
15
|
# Global configuration
|
|
5
|
-
|
|
16
|
+
self.@config: Configuration?
|
|
17
|
+
def self.config: () -> Configuration
|
|
18
|
+
|
|
19
|
+
# Class methods
|
|
20
|
+
def self.configure: () { (Configuration config) -> void } -> void
|
|
6
21
|
|
|
7
|
-
#
|
|
8
|
-
self.@config: untyped
|
|
9
|
-
def self.config: () -> ActiveSupport::Configurable::Configuration
|
|
22
|
+
# Delegated config accessors for backward compatibility
|
|
10
23
|
def self.default_storage: () -> (:memory | :bucket_memory | :null | untyped)
|
|
11
|
-
def self.default_storage=: (:memory | :bucket_memory | :null | untyped value) -> void
|
|
24
|
+
def self.default_storage=: ((:memory | :bucket_memory | :null | untyped) value) -> void
|
|
12
25
|
def self.default_timeout: () -> Integer?
|
|
13
26
|
def self.default_timeout=: (Integer? value) -> void
|
|
14
27
|
def self.default_reset_timeout: () -> Integer
|
|
@@ -17,9 +30,8 @@ module BreakerMachines
|
|
|
17
30
|
def self.default_failure_threshold=: (Integer value) -> void
|
|
18
31
|
def self.log_events: () -> bool
|
|
19
32
|
def self.log_events=: (bool value) -> void
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def self.configure: () { (untyped config) -> void } -> void
|
|
33
|
+
def self.fiber_safe: () -> bool
|
|
34
|
+
def self.fiber_safe=: (bool value) -> void
|
|
23
35
|
def self.setup_notifications: () -> void
|
|
24
36
|
def self.logger: () -> ActiveSupport::Logger?
|
|
25
37
|
def self.logger=: (ActiveSupport::Logger? logger) -> void
|