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.
- checksums.yaml +4 -4
- data/README.md +25 -3
- 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/async_circuit.rb +47 -0
- data/lib/breaker_machines/async_support.rb +4 -3
- data/lib/breaker_machines/cascading_circuit.rb +5 -3
- 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 +7 -12
- data/lib/breaker_machines/circuit/configuration.rb +6 -26
- data/lib/breaker_machines/circuit/coordinated_state_management.rb +117 -0
- data/lib/breaker_machines/circuit/hedged_execution.rb +115 -0
- data/lib/breaker_machines/circuit/introspection.rb +1 -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 +14 -61
- data/lib/breaker_machines/circuit.rb +1 -7
- data/lib/breaker_machines/circuit_group.rb +153 -0
- data/lib/breaker_machines/coordinated_circuit.rb +10 -0
- data/lib/breaker_machines/dsl.rb +2 -2
- data/lib/breaker_machines/errors.rb +20 -0
- data/lib/breaker_machines/hedged_async_support.rb +29 -36
- 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 +115 -11
- data/lib/breaker_machines_native/breaker_machines_native.bundle +0 -0
- data/sig/breaker_machines.rbs +20 -8
- metadata +107 -7
- data/lib/breaker_machines/hedged_execution.rb +0 -113
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BreakerMachines
|
|
4
|
+
class Circuit
|
|
5
|
+
# StateCallbacks provides the common callback methods shared by all state management modules
|
|
6
|
+
module StateCallbacks
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def on_circuit_open
|
|
12
|
+
@opened_at.value = BreakerMachines.monotonic_time
|
|
13
|
+
@storage&.set_status(@name, :open, @opened_at.value)
|
|
14
|
+
if @storage.respond_to?(:record_event_with_details)
|
|
15
|
+
@storage.record_event_with_details(@name, :state_change, 0,
|
|
16
|
+
new_state: :open)
|
|
17
|
+
end
|
|
18
|
+
invoke_callback(:on_open)
|
|
19
|
+
BreakerMachines.instrument('opened', circuit: @name)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def on_circuit_close
|
|
23
|
+
@opened_at.value = nil
|
|
24
|
+
@last_error.value = nil
|
|
25
|
+
@last_failure_at.value = nil
|
|
26
|
+
@storage&.set_status(@name, :closed)
|
|
27
|
+
if @storage.respond_to?(:record_event_with_details)
|
|
28
|
+
@storage.record_event_with_details(@name, :state_change, 0,
|
|
29
|
+
new_state: :closed)
|
|
30
|
+
end
|
|
31
|
+
invoke_callback(:on_close)
|
|
32
|
+
BreakerMachines.instrument('closed', circuit: @name)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def on_circuit_half_open
|
|
36
|
+
@half_open_attempts.value = 0
|
|
37
|
+
@half_open_successes.value = 0
|
|
38
|
+
@storage&.set_status(@name, :half_open)
|
|
39
|
+
if @storage.respond_to?(:record_event_with_details)
|
|
40
|
+
@storage.record_event_with_details(@name, :state_change, 0,
|
|
41
|
+
new_state: :half_open)
|
|
42
|
+
end
|
|
43
|
+
invoke_callback(:on_half_open)
|
|
44
|
+
BreakerMachines.instrument('half_opened', circuit: @name)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def reset_timeout_elapsed?
|
|
48
|
+
return false unless @opened_at.value
|
|
49
|
+
|
|
50
|
+
# Add jitter to prevent thundering herd using ChronoMachines
|
|
51
|
+
# This matches the Rust implementation which uses chrono-machines for jitter
|
|
52
|
+
timeout_with_jitter = if (jitter_factor = @config[:reset_timeout_jitter]) && jitter_factor.positive?
|
|
53
|
+
calculate_timeout_with_jitter(@config[:reset_timeout], jitter_factor)
|
|
54
|
+
else
|
|
55
|
+
@config[:reset_timeout]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
BreakerMachines.monotonic_time - @opened_at.value >= timeout_with_jitter
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Calculate timeout with jitter using ChronoMachines algorithm
|
|
62
|
+
# Matches the Rust implementation: timeout * (1 - jitter + rand * jitter)
|
|
63
|
+
def calculate_timeout_with_jitter(base_timeout, jitter_factor)
|
|
64
|
+
# Use full jitter strategy from ChronoMachines
|
|
65
|
+
# Formula: base * (1 - jitter + rand * jitter)
|
|
66
|
+
# This gives values in range [base * (1-jitter), base]
|
|
67
|
+
normalized_jitter = [jitter_factor.to_f, 1.0].min
|
|
68
|
+
base_timeout * (1.0 - normalized_jitter + (rand * normalized_jitter))
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -6,6 +6,7 @@ module BreakerMachines
|
|
|
6
6
|
# managing transitions between closed, open, and half-open states.
|
|
7
7
|
module StateManagement
|
|
8
8
|
extend ActiveSupport::Concern
|
|
9
|
+
|
|
9
10
|
included do
|
|
10
11
|
state_machine :status, initial: :closed do
|
|
11
12
|
event :trip do
|
|
@@ -30,77 +31,29 @@ module BreakerMachines
|
|
|
30
31
|
transition any => :closed
|
|
31
32
|
end
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
event :hard_reset do
|
|
35
|
+
transition any => :closed
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
before_transition on: :hard_reset do |circuit|
|
|
39
|
+
circuit.storage&.clear(circuit.name)
|
|
40
|
+
circuit.half_open_attempts.value = 0
|
|
41
|
+
circuit.half_open_successes.value = 0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
after_transition to: :open do |circuit|
|
|
34
45
|
circuit.send(:on_circuit_open)
|
|
35
46
|
end
|
|
36
47
|
|
|
37
|
-
after_transition
|
|
48
|
+
after_transition to: :closed do |circuit|
|
|
38
49
|
circuit.send(:on_circuit_close)
|
|
39
50
|
end
|
|
40
51
|
|
|
41
|
-
after_transition open: :half_open do |circuit|
|
|
52
|
+
after_transition from: :open, to: :half_open do |circuit|
|
|
42
53
|
circuit.send(:on_circuit_half_open)
|
|
43
54
|
end
|
|
44
55
|
end
|
|
45
56
|
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
def on_circuit_open
|
|
50
|
-
@opened_at.value = BreakerMachines.monotonic_time
|
|
51
|
-
@storage&.set_status(@name, :open, @opened_at.value)
|
|
52
|
-
if @storage.respond_to?(:record_event_with_details)
|
|
53
|
-
@storage.record_event_with_details(@name, :state_change, 0,
|
|
54
|
-
new_state: :open)
|
|
55
|
-
end
|
|
56
|
-
invoke_callback(:on_open)
|
|
57
|
-
BreakerMachines.instrument('opened', circuit: @name)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def on_circuit_close
|
|
61
|
-
@opened_at.value = nil
|
|
62
|
-
@last_error.value = nil
|
|
63
|
-
@last_failure_at.value = nil
|
|
64
|
-
@storage&.set_status(@name, :closed)
|
|
65
|
-
if @storage.respond_to?(:record_event_with_details)
|
|
66
|
-
@storage.record_event_with_details(@name, :state_change, 0,
|
|
67
|
-
new_state: :closed)
|
|
68
|
-
end
|
|
69
|
-
invoke_callback(:on_close)
|
|
70
|
-
BreakerMachines.instrument('closed', circuit: @name)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def on_circuit_half_open
|
|
74
|
-
@half_open_attempts.value = 0
|
|
75
|
-
@half_open_successes.value = 0
|
|
76
|
-
@storage&.set_status(@name, :half_open)
|
|
77
|
-
if @storage.respond_to?(:record_event_with_details)
|
|
78
|
-
@storage.record_event_with_details(@name, :state_change, 0,
|
|
79
|
-
new_state: :half_open)
|
|
80
|
-
end
|
|
81
|
-
invoke_callback(:on_half_open)
|
|
82
|
-
BreakerMachines.instrument('half_opened', circuit: @name)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def restore_status_from_storage
|
|
86
|
-
stored_status = @storage.get_status(@name)
|
|
87
|
-
return unless stored_status
|
|
88
|
-
|
|
89
|
-
self.status = stored_status.status.to_s
|
|
90
|
-
@opened_at.value = stored_status.opened_at if stored_status.opened_at
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def reset_timeout_elapsed?
|
|
94
|
-
return false unless @opened_at.value
|
|
95
|
-
|
|
96
|
-
# Add jitter to prevent thundering herd
|
|
97
|
-
jitter_factor = @config[:reset_timeout_jitter] || 0.25
|
|
98
|
-
# Calculate random jitter between -jitter_factor and +jitter_factor
|
|
99
|
-
jitter_multiplier = 1.0 + (((rand * 2) - 1) * jitter_factor)
|
|
100
|
-
timeout_with_jitter = @config[:reset_timeout] * jitter_multiplier
|
|
101
|
-
|
|
102
|
-
BreakerMachines.monotonic_time - @opened_at.value >= timeout_with_jitter
|
|
103
|
-
end
|
|
104
57
|
end
|
|
105
58
|
end
|
|
106
59
|
end
|
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'concurrent-ruby'
|
|
4
|
-
|
|
5
3
|
module BreakerMachines
|
|
6
4
|
class Circuit
|
|
5
|
+
include Circuit::Base
|
|
7
6
|
include StateManagement
|
|
8
|
-
include Configuration
|
|
9
|
-
include Execution
|
|
10
|
-
include HedgedExecution
|
|
11
|
-
include Introspection
|
|
12
|
-
include Callbacks
|
|
13
7
|
end
|
|
14
8
|
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BreakerMachines
|
|
4
|
+
# CircuitGroup provides coordinated management of multiple related circuits
|
|
5
|
+
# with support for dependencies, shared configuration, and group-wide operations
|
|
6
|
+
class CircuitGroup
|
|
7
|
+
include BreakerMachines::DSL
|
|
8
|
+
|
|
9
|
+
attr_reader :name, :circuits, :dependencies, :config
|
|
10
|
+
|
|
11
|
+
def initialize(name, config = {})
|
|
12
|
+
@name = name
|
|
13
|
+
@config = config
|
|
14
|
+
@circuits = {}
|
|
15
|
+
@dependencies = {}
|
|
16
|
+
@async_mode = config[:async_mode] || false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Define a circuit within this group with optional dependencies
|
|
20
|
+
# @param name [Symbol] Circuit name
|
|
21
|
+
# @param options [Hash] Circuit configuration
|
|
22
|
+
# @option options [Symbol, Array<Symbol>] :depends_on Other circuits this one depends on
|
|
23
|
+
# @option options [Proc] :guard_with Additional guard conditions
|
|
24
|
+
def circuit(name, options = {}, &)
|
|
25
|
+
depends_on = Array(options.delete(:depends_on))
|
|
26
|
+
guard_proc = options.delete(:guard_with)
|
|
27
|
+
|
|
28
|
+
# Add group-wide defaults
|
|
29
|
+
circuit_config = @config.merge(options)
|
|
30
|
+
|
|
31
|
+
# Create appropriate circuit type
|
|
32
|
+
circuit_class = if options[:cascades_to] || depends_on.any?
|
|
33
|
+
BreakerMachines::CascadingCircuit
|
|
34
|
+
elsif @async_mode
|
|
35
|
+
BreakerMachines::AsyncCircuit
|
|
36
|
+
else
|
|
37
|
+
BreakerMachines::Circuit
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Build the circuit
|
|
41
|
+
circuit_instance = if block_given?
|
|
42
|
+
builder = BreakerMachines::DSL::CircuitBuilder.new
|
|
43
|
+
builder.instance_eval(&)
|
|
44
|
+
built_config = builder.config.merge(circuit_config)
|
|
45
|
+
circuit_class.new(full_circuit_name(name), built_config)
|
|
46
|
+
else
|
|
47
|
+
circuit_class.new(full_circuit_name(name), circuit_config)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Store dependencies and guards
|
|
51
|
+
if depends_on.any? || guard_proc
|
|
52
|
+
@dependencies[name] = {
|
|
53
|
+
depends_on: depends_on,
|
|
54
|
+
guard: guard_proc
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Wrap the circuit with dependency checking
|
|
58
|
+
circuit_instance = DependencyWrapper.new(circuit_instance, self, name)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
@circuits[name] = circuit_instance
|
|
62
|
+
BreakerMachines.register(circuit_instance)
|
|
63
|
+
circuit_instance
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Get a circuit by name
|
|
67
|
+
def [](name)
|
|
68
|
+
@circuits[name]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Check if all circuits in the group are healthy
|
|
72
|
+
def all_healthy?
|
|
73
|
+
@circuits.values.all? { |circuit| circuit.closed? || circuit.half_open? }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Check if any circuit in the group is open
|
|
77
|
+
def any_open?
|
|
78
|
+
@circuits.values.any?(&:open?)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Get status of all circuits
|
|
82
|
+
def status
|
|
83
|
+
@circuits.transform_values(&:status_name)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Reset all circuits in the group
|
|
87
|
+
def reset_all!
|
|
88
|
+
@circuits.each_value(&:reset!)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Force open all circuits
|
|
92
|
+
def trip_all!
|
|
93
|
+
@circuits.each_value(&:force_open!)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Check dependencies for a specific circuit
|
|
97
|
+
def dependencies_met?(circuit_name)
|
|
98
|
+
deps = @dependencies[circuit_name]
|
|
99
|
+
return true unless deps
|
|
100
|
+
|
|
101
|
+
depends_on = deps[:depends_on]
|
|
102
|
+
guard = deps[:guard]
|
|
103
|
+
|
|
104
|
+
# Check circuit dependencies recursively
|
|
105
|
+
dependencies_healthy = depends_on.all? do |dep_name|
|
|
106
|
+
dep_circuit = @circuits[dep_name]
|
|
107
|
+
# Circuit must exist, be healthy, AND have its own dependencies met
|
|
108
|
+
dep_circuit && (dep_circuit.closed? || dep_circuit.half_open?) && dependencies_met?(dep_name)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Check custom guard
|
|
112
|
+
guard_passed = guard ? guard.call : true
|
|
113
|
+
|
|
114
|
+
dependencies_healthy && guard_passed
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def full_circuit_name(name)
|
|
120
|
+
"#{@name}.#{name}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Wrapper to enforce dependencies
|
|
124
|
+
class DependencyWrapper < SimpleDelegator
|
|
125
|
+
def initialize(circuit, group, name)
|
|
126
|
+
super(circuit)
|
|
127
|
+
@group = group
|
|
128
|
+
@name = name
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def call(&)
|
|
132
|
+
unless @group.dependencies_met?(@name)
|
|
133
|
+
raise BreakerMachines::CircuitDependencyError.new(__getobj__.name,
|
|
134
|
+
"Dependencies not met for circuit #{@name}")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
__getobj__.call(&)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def attempt_recovery!
|
|
141
|
+
return false unless @group.dependencies_met?(@name)
|
|
142
|
+
|
|
143
|
+
__getobj__.attempt_recovery!
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def reset!
|
|
147
|
+
return false unless @group.dependencies_met?(@name)
|
|
148
|
+
|
|
149
|
+
__getobj__.reset!
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BreakerMachines
|
|
4
|
+
# CoordinatedCircuit is a base class for circuits that need coordinated state management.
|
|
5
|
+
# It replaces the standard StateManagement module with CoordinatedStateManagement
|
|
6
|
+
# to enable state transitions based on other circuits' states.
|
|
7
|
+
class CoordinatedCircuit < Circuit
|
|
8
|
+
include Circuit::CoordinatedStateManagement
|
|
9
|
+
end
|
|
10
|
+
end
|
data/lib/breaker_machines/dsl.rb
CHANGED
|
@@ -122,7 +122,7 @@ module BreakerMachines
|
|
|
122
122
|
instance_registry.each do |weak_ref|
|
|
123
123
|
instance = weak_ref.__getobj__
|
|
124
124
|
circuit_instances = instance.instance_variable_get(:@circuit_instances)
|
|
125
|
-
circuit_instances&.each_value(&:
|
|
125
|
+
circuit_instances&.each_value(&:hard_reset!)
|
|
126
126
|
rescue WeakRef::RefError
|
|
127
127
|
# Instance was garbage collected, skip it
|
|
128
128
|
end
|
|
@@ -262,7 +262,7 @@ module BreakerMachines
|
|
|
262
262
|
|
|
263
263
|
# Reset all circuits for this instance
|
|
264
264
|
def reset_all_circuits
|
|
265
|
-
circuit_instances.each_value(&:
|
|
265
|
+
circuit_instances.each_value(&:hard_reset!)
|
|
266
266
|
end
|
|
267
267
|
|
|
268
268
|
# Remove a global dynamic circuit by name
|
|
@@ -14,6 +14,16 @@ module BreakerMachines
|
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
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
|
+
|
|
17
27
|
# Raised when a circuit-protected call exceeds the configured timeout
|
|
18
28
|
class CircuitTimeoutError < Error
|
|
19
29
|
attr_reader :circuit_name, :timeout
|
|
@@ -48,4 +58,14 @@ module BreakerMachines
|
|
|
48
58
|
super("Circuit '#{circuit_name}' rejected call: max concurrent limit of #{max_concurrent} reached")
|
|
49
59
|
end
|
|
50
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
|
|
51
71
|
end
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
# This file contains async support for hedged execution
|
|
4
4
|
# It is only loaded when fiber_safe mode is enabled
|
|
5
|
+
# Requires async gem ~> 2.31.0 for Promise and modern API features
|
|
5
6
|
|
|
6
7
|
require 'async'
|
|
7
8
|
require 'async/task'
|
|
8
|
-
require 'async/
|
|
9
|
+
require 'async/promise'
|
|
10
|
+
require 'async/barrier'
|
|
9
11
|
require 'concurrent'
|
|
10
12
|
|
|
11
13
|
module BreakerMachines
|
|
@@ -43,53 +45,44 @@ module BreakerMachines
|
|
|
43
45
|
private
|
|
44
46
|
|
|
45
47
|
# Race callables; return first result or raise if it was an Exception
|
|
46
|
-
# Uses Async::
|
|
48
|
+
# Uses modern Async::Promise and Async::Barrier for cleaner synchronization
|
|
47
49
|
# @param callables [Array<Proc>] Tasks to race
|
|
48
50
|
# @param delay_ms [Integer] Delay in milliseconds between task starts
|
|
49
51
|
# @return [Object] First successful result
|
|
50
52
|
# @raise [Exception] The first exception received
|
|
51
53
|
def race_tasks(callables, delay_ms: 0)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
condition = Async::Condition.new
|
|
55
|
-
winner = nil
|
|
56
|
-
exception = nil
|
|
54
|
+
promise = Async::Promise.new
|
|
55
|
+
barrier = Async::Barrier.new
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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?
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
rescue StandardError => e
|
|
72
|
-
mutex.synchronize do
|
|
73
|
-
next if winner || exception
|
|
74
|
-
|
|
75
|
-
exception = e
|
|
76
|
-
condition.signal
|
|
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?
|
|
77
72
|
end
|
|
78
73
|
end
|
|
79
74
|
end
|
|
80
|
-
end
|
|
81
75
|
|
|
82
|
-
|
|
83
|
-
|
|
76
|
+
# Wait for the first resolution (either success or exception)
|
|
77
|
+
promise.wait
|
|
78
|
+
end.wait
|
|
84
79
|
|
|
85
|
-
#
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
#
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
winner
|
|
92
|
-
end.wait
|
|
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
|
|
93
86
|
end
|
|
94
87
|
end
|
|
95
88
|
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
|