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,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Requires async gem ~> 2.31.0 for modern async patterns
|
|
4
|
+
|
|
5
|
+
require_relative 'circuit/async_state_management'
|
|
6
|
+
|
|
7
|
+
module BreakerMachines
|
|
8
|
+
# AsyncCircuit provides a circuit breaker with async-enabled state machine
|
|
9
|
+
# for thread-safe, fiber-safe concurrent operations
|
|
10
|
+
class AsyncCircuit < Circuit
|
|
11
|
+
include Circuit::AsyncStateManagement
|
|
12
|
+
|
|
13
|
+
# Additional async-specific methods
|
|
14
|
+
def call_async(&block)
|
|
15
|
+
require 'async' unless defined?(::Async)
|
|
16
|
+
|
|
17
|
+
Async do
|
|
18
|
+
call(&block)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Fire state transition events asynchronously
|
|
23
|
+
# @param event_name [Symbol] The event to fire
|
|
24
|
+
# @return [Async::Task] The async task
|
|
25
|
+
def fire_async(event_name)
|
|
26
|
+
require 'async' unless defined?(::Async)
|
|
27
|
+
|
|
28
|
+
fire_event_async(event_name)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Check circuit health asynchronously
|
|
32
|
+
# Useful for monitoring multiple circuits concurrently
|
|
33
|
+
def health_check_async
|
|
34
|
+
require 'async' unless defined?(::Async)
|
|
35
|
+
|
|
36
|
+
Async do
|
|
37
|
+
{
|
|
38
|
+
name: @name,
|
|
39
|
+
status: status_name,
|
|
40
|
+
open: open?,
|
|
41
|
+
stats: stats.to_h,
|
|
42
|
+
can_recover: open? && reset_timeout_elapsed?
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# This file contains all async-related functionality for fiber-safe mode
|
|
4
4
|
# It is only loaded when fiber_safe mode is enabled
|
|
5
|
+
# Requires async gem ~> 2.31.0 for modern timeout API and Promise features
|
|
5
6
|
|
|
6
7
|
require 'async'
|
|
7
8
|
require 'async/task'
|
|
@@ -43,11 +44,11 @@ module BreakerMachines
|
|
|
43
44
|
end
|
|
44
45
|
end
|
|
45
46
|
|
|
46
|
-
# Execute a block with optional timeout using Async
|
|
47
|
+
# Execute a block with optional timeout using modern Async API
|
|
47
48
|
def execute_with_async_timeout(timeout, &)
|
|
48
49
|
if timeout
|
|
49
|
-
# Use
|
|
50
|
-
|
|
50
|
+
# Use modern timeout API - the flexible with_timeout API is on the task level
|
|
51
|
+
Async::Task.current.with_timeout(timeout, &)
|
|
51
52
|
else
|
|
52
53
|
yield
|
|
53
54
|
end
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'coordinated_circuit'
|
|
4
|
+
|
|
3
5
|
module BreakerMachines
|
|
4
|
-
# CascadingCircuit extends the
|
|
6
|
+
# CascadingCircuit extends the CoordinatedCircuit class with the ability to automatically
|
|
5
7
|
# trip dependent circuits when this circuit opens. This enables sophisticated
|
|
6
8
|
# failure cascade modeling, similar to how critical system failures in a starship
|
|
7
9
|
# would cascade to dependent subsystems.
|
|
@@ -18,7 +20,7 @@ module BreakerMachines
|
|
|
18
20
|
# network_circuit.call { raise 'Subspace relay offline!' }
|
|
19
21
|
# # => All dependent circuits are now open
|
|
20
22
|
#
|
|
21
|
-
class CascadingCircuit <
|
|
23
|
+
class CascadingCircuit < CoordinatedCircuit
|
|
22
24
|
attr_reader :dependent_circuits, :emergency_protocol
|
|
23
25
|
|
|
24
26
|
def initialize(name, config = {})
|
|
@@ -144,7 +146,7 @@ module BreakerMachines
|
|
|
144
146
|
|
|
145
147
|
# Allow custom emergency protocol handling
|
|
146
148
|
owner = @config[:owner]
|
|
147
|
-
if owner
|
|
149
|
+
if owner.respond_to?(@emergency_protocol, true)
|
|
148
150
|
begin
|
|
149
151
|
owner.send(@emergency_protocol, affected_circuits)
|
|
150
152
|
rescue StandardError => e
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BreakerMachines
|
|
4
|
+
class Circuit
|
|
5
|
+
# AsyncStateManagement provides state machine functionality with async support
|
|
6
|
+
# leveraging state_machines' async: true parameter for thread-safe operations
|
|
7
|
+
module AsyncStateManagement
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
# Enable async mode for thread-safe state transitions
|
|
12
|
+
# This automatically provides:
|
|
13
|
+
# - Mutex-protected state reads/writes
|
|
14
|
+
# - Fiber-safe execution
|
|
15
|
+
# - Concurrent transition handling
|
|
16
|
+
state_machine :status, initial: :closed, async: true do
|
|
17
|
+
event :trip do
|
|
18
|
+
transition closed: :open
|
|
19
|
+
transition half_open: :open
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
event :attempt_recovery do
|
|
23
|
+
transition open: :half_open
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
event :reset do
|
|
27
|
+
transition %i[open half_open] => :closed
|
|
28
|
+
transition closed: :closed
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
event :force_open do
|
|
32
|
+
transition any => :open
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
event :force_close do
|
|
36
|
+
transition any => :closed
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
event :hard_reset do
|
|
40
|
+
transition any => :closed
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
before_transition on: :hard_reset do |circuit|
|
|
44
|
+
circuit.storage&.clear(circuit.name)
|
|
45
|
+
circuit.half_open_attempts.value = 0
|
|
46
|
+
circuit.half_open_successes.value = 0
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Async-safe callbacks using modern API
|
|
50
|
+
after_transition to: :open do |circuit|
|
|
51
|
+
circuit.send(:on_circuit_open)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
after_transition to: :closed do |circuit|
|
|
55
|
+
circuit.send(:on_circuit_close)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
after_transition from: :open, to: :half_open do |circuit|
|
|
59
|
+
circuit.send(:on_circuit_half_open)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Additional async event methods are automatically generated:
|
|
64
|
+
# - trip_async! - Returns Async::Task
|
|
65
|
+
# - attempt_recovery_async! - Returns Async::Task
|
|
66
|
+
# - reset_async! - Returns Async::Task
|
|
67
|
+
# - fire_event_async(:event_name) - Generic async event firing
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'concurrent-ruby'
|
|
4
|
+
|
|
5
|
+
module BreakerMachines
|
|
6
|
+
class Circuit
|
|
7
|
+
# Base provides the common initialization and setup logic shared by all circuit types
|
|
8
|
+
module Base
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
included do
|
|
12
|
+
include Circuit::Configuration
|
|
13
|
+
include Circuit::Execution
|
|
14
|
+
include Circuit::HedgedExecution
|
|
15
|
+
include Circuit::Introspection
|
|
16
|
+
include Circuit::Callbacks
|
|
17
|
+
include Circuit::StateCallbacks
|
|
18
|
+
|
|
19
|
+
# name/config/opened_at readers are defined in Circuit::Configuration
|
|
20
|
+
attr_reader :storage, :metrics, :semaphore
|
|
21
|
+
attr_reader :half_open_attempts, :half_open_successes, :mutex
|
|
22
|
+
attr_reader :last_failure_at, :last_error
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(name, options = {})
|
|
26
|
+
@name = name
|
|
27
|
+
@config = default_config.merge(options)
|
|
28
|
+
# Always use a storage backend for proper sliding window implementation
|
|
29
|
+
# Use global default storage if not specified
|
|
30
|
+
@storage = @config[:storage] || create_default_storage
|
|
31
|
+
@metrics = @config[:metrics]
|
|
32
|
+
@opened_at = Concurrent::AtomicReference.new(nil)
|
|
33
|
+
@half_open_attempts = Concurrent::AtomicFixnum.new(0)
|
|
34
|
+
@half_open_successes = Concurrent::AtomicFixnum.new(0)
|
|
35
|
+
@mutex = Concurrent::ReentrantReadWriteLock.new
|
|
36
|
+
@last_failure_at = Concurrent::AtomicReference.new(nil)
|
|
37
|
+
@last_error = Concurrent::AtomicReference.new(nil)
|
|
38
|
+
|
|
39
|
+
# Initialize semaphore for bulkheading if max_concurrent is set
|
|
40
|
+
@semaphore = (Concurrent::Semaphore.new(@config[:max_concurrent]) if @config[:max_concurrent])
|
|
41
|
+
|
|
42
|
+
restore_status_from_storage if @storage
|
|
43
|
+
|
|
44
|
+
# Register with global registry unless auto_register is disabled
|
|
45
|
+
BreakerMachines::Registry.instance.register(self) unless @config[:auto_register] == false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def restore_status_from_storage
|
|
51
|
+
stored_status = @storage.get_status(@name)
|
|
52
|
+
return unless stored_status
|
|
53
|
+
|
|
54
|
+
self.status = stored_status.status.to_s
|
|
55
|
+
@opened_at.value = stored_status.opened_at if stored_status.opened_at
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -120,19 +120,14 @@ module BreakerMachines
|
|
|
120
120
|
end
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
begin
|
|
125
|
-
Timeout.timeout(5) do # reasonable timeout for fallbacks
|
|
126
|
-
loop do
|
|
127
|
-
return result_queue.pop unless result_queue.empty?
|
|
128
|
-
|
|
129
|
-
raise error_queue.pop if error_queue.size >= fallbacks.size
|
|
123
|
+
threads.each(&:join)
|
|
130
124
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
125
|
+
if result_queue.empty?
|
|
126
|
+
errors = []
|
|
127
|
+
errors << error_queue.pop until error_queue.empty?
|
|
128
|
+
raise BreakerMachines::ParallelFallbackError.new('All parallel fallbacks failed', errors)
|
|
129
|
+
else
|
|
130
|
+
result_queue.pop
|
|
136
131
|
end
|
|
137
132
|
end
|
|
138
133
|
end
|
|
@@ -11,30 +11,6 @@ module BreakerMachines
|
|
|
11
11
|
attr_reader :name, :config, :opened_at
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def initialize(name, options = {})
|
|
15
|
-
@name = name
|
|
16
|
-
@config = default_config.merge(options)
|
|
17
|
-
# Always use a storage backend for proper sliding window implementation
|
|
18
|
-
# Use global default storage if not specified
|
|
19
|
-
@storage = @config[:storage] || create_default_storage
|
|
20
|
-
@metrics = @config[:metrics]
|
|
21
|
-
@opened_at = Concurrent::AtomicReference.new(nil)
|
|
22
|
-
@half_open_attempts = Concurrent::AtomicFixnum.new(0)
|
|
23
|
-
@half_open_successes = Concurrent::AtomicFixnum.new(0)
|
|
24
|
-
@mutex = Concurrent::ReentrantReadWriteLock.new
|
|
25
|
-
@last_failure_at = Concurrent::AtomicReference.new(nil)
|
|
26
|
-
@last_error = Concurrent::AtomicReference.new(nil)
|
|
27
|
-
|
|
28
|
-
# Initialize semaphore for bulkheading if max_concurrent is set
|
|
29
|
-
@semaphore = (Concurrent::Semaphore.new(@config[:max_concurrent]) if @config[:max_concurrent])
|
|
30
|
-
|
|
31
|
-
super() # Initialize state machine
|
|
32
|
-
restore_status_from_storage if @storage
|
|
33
|
-
|
|
34
|
-
# Register with global registry unless auto_register is disabled
|
|
35
|
-
BreakerMachines::Registry.instance.register(self) unless @config[:auto_register] == false
|
|
36
|
-
end
|
|
37
|
-
|
|
38
14
|
private
|
|
39
15
|
|
|
40
16
|
def default_config
|
|
@@ -78,8 +54,12 @@ module BreakerMachines
|
|
|
78
54
|
when :null
|
|
79
55
|
BreakerMachines::Storage::Null.new
|
|
80
56
|
else
|
|
81
|
-
# Allow for custom storage class names
|
|
82
|
-
BreakerMachines.config.default_storage.new
|
|
57
|
+
# Allow for custom storage class names or instances
|
|
58
|
+
if BreakerMachines.config.default_storage.respond_to?(:new)
|
|
59
|
+
BreakerMachines.config.default_storage.new
|
|
60
|
+
else
|
|
61
|
+
BreakerMachines.config.default_storage
|
|
62
|
+
end
|
|
83
63
|
end
|
|
84
64
|
end
|
|
85
65
|
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BreakerMachines
|
|
4
|
+
class Circuit
|
|
5
|
+
# CoordinatedStateManagement extends the base state machine with coordinated guards
|
|
6
|
+
# that allow circuits to make transitions based on the state of other circuits.
|
|
7
|
+
module CoordinatedStateManagement
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
# Override the state machine to add coordinated guards
|
|
12
|
+
state_machine :status, initial: :closed do
|
|
13
|
+
event :trip do
|
|
14
|
+
transition closed: :open
|
|
15
|
+
transition half_open: :open
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
event :attempt_recovery do
|
|
19
|
+
transition open: :half_open,
|
|
20
|
+
if: lambda(&:recovery_allowed?)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
event :reset do
|
|
24
|
+
transition %i[open half_open] => :closed,
|
|
25
|
+
if: lambda(&:reset_allowed?)
|
|
26
|
+
transition closed: :closed
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
event :force_open do
|
|
30
|
+
transition any => :open
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
event :force_close do
|
|
34
|
+
transition any => :closed
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
event :hard_reset do
|
|
38
|
+
transition any => :closed
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
before_transition on: :hard_reset do |circuit|
|
|
42
|
+
circuit.storage&.clear(circuit.name)
|
|
43
|
+
circuit.half_open_attempts.value = 0
|
|
44
|
+
circuit.half_open_successes.value = 0
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
after_transition to: :open do |circuit|
|
|
48
|
+
circuit.send(:on_circuit_open)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
after_transition to: :closed do |circuit|
|
|
52
|
+
circuit.send(:on_circuit_close)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
after_transition from: :open, to: :half_open do |circuit|
|
|
56
|
+
circuit.send(:on_circuit_half_open)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Check if this circuit can attempt recovery
|
|
62
|
+
# For cascading circuits, this checks if dependent circuits allow it
|
|
63
|
+
def recovery_allowed?
|
|
64
|
+
return true unless respond_to?(:dependent_circuits) && dependent_circuits.any?
|
|
65
|
+
|
|
66
|
+
# Don't attempt recovery if any critical dependencies are still down
|
|
67
|
+
!has_critical_dependencies_down?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Check if this circuit can reset to closed
|
|
71
|
+
# For cascading circuits, ensures dependencies are healthy
|
|
72
|
+
def reset_allowed?
|
|
73
|
+
return true unless respond_to?(:dependent_circuits) && dependent_circuits.any?
|
|
74
|
+
|
|
75
|
+
# Only reset if all dependencies are in acceptable states
|
|
76
|
+
all_dependencies_healthy?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# Check if any critical dependencies are down
|
|
82
|
+
def has_critical_dependencies_down?
|
|
83
|
+
return false unless respond_to?(:dependent_circuits)
|
|
84
|
+
|
|
85
|
+
dependent_circuits.any? do |circuit_name|
|
|
86
|
+
circuit = find_dependent_circuit(circuit_name)
|
|
87
|
+
circuit&.open?
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Check if all dependencies are in healthy states
|
|
92
|
+
def all_dependencies_healthy?
|
|
93
|
+
return true unless respond_to?(:dependent_circuits)
|
|
94
|
+
|
|
95
|
+
dependent_circuits.all? do |circuit_name|
|
|
96
|
+
circuit = find_dependent_circuit(circuit_name)
|
|
97
|
+
circuit.nil? || circuit.closed? || circuit.half_open?
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Find a dependent circuit by name
|
|
102
|
+
def find_dependent_circuit(circuit_name)
|
|
103
|
+
# First try registry
|
|
104
|
+
circuit = BreakerMachines.registry.find(circuit_name)
|
|
105
|
+
|
|
106
|
+
# If not found and we have an owner, try to get it from the owner
|
|
107
|
+
if !circuit && @config[:owner]
|
|
108
|
+
owner = @config[:owner]
|
|
109
|
+
owner = owner.__getobj__ if owner.is_a?(WeakRef)
|
|
110
|
+
circuit = owner.circuit(circuit_name) if owner.respond_to?(:circuit)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
circuit
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'concurrent'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
|
|
6
|
+
module BreakerMachines
|
|
7
|
+
class Circuit
|
|
8
|
+
# HedgedExecution provides hedged request functionality for circuit breakers
|
|
9
|
+
# Hedged requests improve latency by sending duplicate requests to multiple backends
|
|
10
|
+
# and returning the first successful response
|
|
11
|
+
module HedgedExecution
|
|
12
|
+
extend ActiveSupport::Concern
|
|
13
|
+
|
|
14
|
+
# Execute a hedged request pattern
|
|
15
|
+
def execute_hedged(&)
|
|
16
|
+
return execute_single_hedged(&) unless @config[:backends]&.any?
|
|
17
|
+
|
|
18
|
+
execute_multi_backend_hedged
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
# Execute hedged request with a single backend (original block)
|
|
24
|
+
def execute_single_hedged(&block)
|
|
25
|
+
return yield unless hedged_requests_enabled?
|
|
26
|
+
|
|
27
|
+
max_requests = @config[:max_hedged_requests] || 2
|
|
28
|
+
delay_ms = @config[:hedging_delay] || 50
|
|
29
|
+
|
|
30
|
+
if @config[:fiber_safe]
|
|
31
|
+
execute_hedged_async(Array.new(max_requests) { block }, delay_ms)
|
|
32
|
+
else
|
|
33
|
+
execute_hedged_sync(Array.new(max_requests) { block }, delay_ms)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Execute hedged requests across multiple backends
|
|
38
|
+
def execute_multi_backend_hedged
|
|
39
|
+
backends = @config[:backends]
|
|
40
|
+
return backends.first.call if backends.size == 1
|
|
41
|
+
|
|
42
|
+
if @config[:fiber_safe]
|
|
43
|
+
execute_hedged_async(backends, @config[:hedging_delay] || 0)
|
|
44
|
+
else
|
|
45
|
+
execute_hedged_sync(backends, @config[:hedging_delay] || 0)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Synchronous hedged execution using threads
|
|
50
|
+
def execute_hedged_sync(callables, delay_ms)
|
|
51
|
+
result_queue = Queue.new
|
|
52
|
+
error_queue = Queue.new
|
|
53
|
+
threads = []
|
|
54
|
+
cancelled = Concurrent::AtomicBoolean.new(false)
|
|
55
|
+
|
|
56
|
+
callables.each_with_index do |callable, index|
|
|
57
|
+
# Add delay for hedge requests (not the first one)
|
|
58
|
+
sleep(delay_ms / 1000.0) if index.positive? && delay_ms.positive?
|
|
59
|
+
|
|
60
|
+
# Skip if already got a result
|
|
61
|
+
break if cancelled.value
|
|
62
|
+
|
|
63
|
+
threads << Thread.new do
|
|
64
|
+
unless cancelled.value
|
|
65
|
+
begin
|
|
66
|
+
result = callable.call
|
|
67
|
+
result_queue << result unless cancelled.value
|
|
68
|
+
cancelled.value = true
|
|
69
|
+
rescue StandardError => e
|
|
70
|
+
error_queue << e unless cancelled.value
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Wait for first result or all errors
|
|
77
|
+
begin
|
|
78
|
+
Timeout.timeout(@config[:timeout] || 30) do
|
|
79
|
+
# Check for successful result
|
|
80
|
+
loop do
|
|
81
|
+
unless result_queue.empty?
|
|
82
|
+
result = result_queue.pop
|
|
83
|
+
cancelled.value = true
|
|
84
|
+
return result
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Check if all requests failed
|
|
88
|
+
raise error_queue.pop if error_queue.size >= callables.size
|
|
89
|
+
|
|
90
|
+
# Small sleep to prevent busy waiting
|
|
91
|
+
sleep 0.001
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
ensure
|
|
95
|
+
# Cancel remaining threads
|
|
96
|
+
cancelled.value = true
|
|
97
|
+
threads.each(&:kill)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Async hedged execution (requires async support)
|
|
102
|
+
def execute_hedged_async(callables, delay_ms)
|
|
103
|
+
# This will be implemented when async support is loaded
|
|
104
|
+
# For now, fall back to sync implementation
|
|
105
|
+
return execute_hedged_sync(callables, delay_ms) unless respond_to?(:execute_hedged_with_async)
|
|
106
|
+
|
|
107
|
+
execute_hedged_with_async(callables, delay_ms)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def hedged_requests_enabled?
|
|
111
|
+
@config[:hedged_requests] == true
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -6,6 +6,7 @@ module BreakerMachines
|
|
|
6
6
|
# and generating human-readable summaries of circuit status.
|
|
7
7
|
module Introspection
|
|
8
8
|
extend ActiveSupport::Concern
|
|
9
|
+
|
|
9
10
|
# State check methods are automatically generated by state_machines:
|
|
10
11
|
# - open? (returns true when status == :open)
|
|
11
12
|
# - closed? (returns true when status == :closed)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BreakerMachines
|
|
4
|
+
class Circuit
|
|
5
|
+
# Native circuit breaker implementation using Rust FFI
|
|
6
|
+
#
|
|
7
|
+
# This provides a high-performance circuit breaker with state machine logic
|
|
8
|
+
# implemented in Rust. It's fully compatible with the Ruby circuit API but
|
|
9
|
+
# significantly faster for high-throughput scenarios.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic usage
|
|
12
|
+
# circuit = BreakerMachines::Circuit::Native.new('api_calls',
|
|
13
|
+
# failure_threshold: 5,
|
|
14
|
+
# failure_window_secs: 60.0
|
|
15
|
+
# )
|
|
16
|
+
#
|
|
17
|
+
# circuit.call { api.fetch_data }
|
|
18
|
+
class Native
|
|
19
|
+
# @return [String] Circuit name
|
|
20
|
+
attr_reader :name
|
|
21
|
+
|
|
22
|
+
# @return [Hash] Circuit configuration
|
|
23
|
+
attr_reader :config
|
|
24
|
+
|
|
25
|
+
# Create a new native circuit breaker
|
|
26
|
+
#
|
|
27
|
+
# @param name [String] Circuit name
|
|
28
|
+
# @param options [Hash] Configuration options
|
|
29
|
+
# @option options [Integer] :failure_threshold Number of failures to open circuit (default: 5)
|
|
30
|
+
# @option options [Float] :failure_window_secs Time window for counting failures (default: 60.0)
|
|
31
|
+
# @option options [Float] :half_open_timeout_secs Timeout before attempting reset (default: 30.0)
|
|
32
|
+
# @option options [Integer] :success_threshold Successes needed to close from half-open (default: 2)
|
|
33
|
+
# @option options [Boolean] :auto_register Register with global registry (default: true)
|
|
34
|
+
def initialize(name, options = {})
|
|
35
|
+
unless BreakerMachines.native_available?
|
|
36
|
+
raise BreakerMachines::ConfigurationError,
|
|
37
|
+
'Native extension not available. Install with native support or use Circuit::Ruby'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
@name = name
|
|
41
|
+
@config = default_config.merge(options)
|
|
42
|
+
|
|
43
|
+
# Create the native circuit breaker
|
|
44
|
+
@native_circuit = BreakerMachinesNative::Circuit.new(
|
|
45
|
+
name,
|
|
46
|
+
{
|
|
47
|
+
failure_threshold: @config[:failure_threshold],
|
|
48
|
+
failure_window_secs: @config[:failure_window_secs],
|
|
49
|
+
half_open_timeout_secs: @config[:half_open_timeout_secs],
|
|
50
|
+
success_threshold: @config[:success_threshold]
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Register with global registry unless disabled
|
|
55
|
+
BreakerMachines::Registry.instance.register(self) unless @config[:auto_register] == false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Execute a block with circuit breaker protection
|
|
59
|
+
#
|
|
60
|
+
# @yield Block to execute
|
|
61
|
+
# @return Result of the block
|
|
62
|
+
# @raise [CircuitOpenError] if circuit is open
|
|
63
|
+
def call
|
|
64
|
+
raise CircuitOpenError, "Circuit '#{@name}' is open" if open?
|
|
65
|
+
|
|
66
|
+
start_time = BreakerMachines.monotonic_time
|
|
67
|
+
begin
|
|
68
|
+
result = yield
|
|
69
|
+
duration = BreakerMachines.monotonic_time - start_time
|
|
70
|
+
@native_circuit.record_success(duration)
|
|
71
|
+
result
|
|
72
|
+
rescue StandardError => _e
|
|
73
|
+
duration = BreakerMachines.monotonic_time - start_time
|
|
74
|
+
@native_circuit.record_failure(duration)
|
|
75
|
+
raise
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Check if circuit is open
|
|
80
|
+
# @return [Boolean]
|
|
81
|
+
def open?
|
|
82
|
+
@native_circuit.is_open
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Check if circuit is closed
|
|
86
|
+
# @return [Boolean]
|
|
87
|
+
def closed?
|
|
88
|
+
@native_circuit.is_closed
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Get current state name
|
|
92
|
+
# @return [String] 'open' or 'closed'
|
|
93
|
+
def state
|
|
94
|
+
@native_circuit.state_name
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Reset the circuit (clear all events)
|
|
98
|
+
def reset!
|
|
99
|
+
@native_circuit.reset
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Get circuit status for inspection
|
|
103
|
+
# @return [Hash] Status information
|
|
104
|
+
def status
|
|
105
|
+
{
|
|
106
|
+
name: @name,
|
|
107
|
+
state: state,
|
|
108
|
+
open: open?,
|
|
109
|
+
closed: closed?,
|
|
110
|
+
config: @config
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def default_config
|
|
117
|
+
{
|
|
118
|
+
failure_threshold: 5,
|
|
119
|
+
failure_window_secs: 60.0,
|
|
120
|
+
half_open_timeout_secs: 30.0,
|
|
121
|
+
success_threshold: 2,
|
|
122
|
+
auto_register: true
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|