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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +25 -3
  3. data/ext/breaker_machines_native/Cargo.toml +8 -0
  4. data/ext/breaker_machines_native/core/Cargo.toml +18 -0
  5. data/ext/breaker_machines_native/core/examples/basic.rs +61 -0
  6. data/ext/breaker_machines_native/core/src/builder.rs +232 -0
  7. data/ext/breaker_machines_native/core/src/bulkhead.rs +223 -0
  8. data/ext/breaker_machines_native/core/src/callbacks.rs +58 -0
  9. data/ext/breaker_machines_native/core/src/circuit.rs +1156 -0
  10. data/ext/breaker_machines_native/core/src/classifier.rs +177 -0
  11. data/ext/breaker_machines_native/core/src/errors.rs +47 -0
  12. data/ext/breaker_machines_native/core/src/lib.rs +62 -0
  13. data/ext/breaker_machines_native/core/src/storage.rs +377 -0
  14. data/ext/breaker_machines_native/extconf.rb +40 -0
  15. data/ext/breaker_machines_native/ffi/Cargo.toml +16 -0
  16. data/ext/breaker_machines_native/ffi/src/lib.rs +218 -0
  17. data/ext/breaker_machines_native/target/debug/build/clang-sys-d961dfabd5f43fba/out/common.rs +355 -0
  18. data/ext/breaker_machines_native/target/debug/build/clang-sys-d961dfabd5f43fba/out/dynamic.rs +276 -0
  19. data/ext/breaker_machines_native/target/debug/build/clang-sys-d961dfabd5f43fba/out/macros.rs +49 -0
  20. 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
  21. 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
  22. 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
  23. 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
  24. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/Cargo.toml +48 -0
  25. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/examples/basic.rs +61 -0
  26. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/builder.rs +154 -0
  27. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/callbacks.rs +55 -0
  28. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/circuit.rs +607 -0
  29. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/errors.rs +38 -0
  30. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/lib.rs +58 -0
  31. data/ext/breaker_machines_native/target/package/breaker-machines-0.1.0/src/storage.rs +377 -0
  32. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/Cargo.toml +48 -0
  33. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/examples/basic.rs +61 -0
  34. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/builder.rs +173 -0
  35. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/callbacks.rs +55 -0
  36. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/circuit.rs +855 -0
  37. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/errors.rs +38 -0
  38. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/lib.rs +58 -0
  39. data/ext/breaker_machines_native/target/package/breaker-machines-0.2.0/src/storage.rs +377 -0
  40. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/Cargo.toml +48 -0
  41. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/examples/basic.rs +61 -0
  42. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/builder.rs +154 -0
  43. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/callbacks.rs +55 -0
  44. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/circuit.rs +607 -0
  45. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/errors.rs +38 -0
  46. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/lib.rs +58 -0
  47. data/ext/breaker_machines_native/target/package/breaker-machines-0.5.0/src/storage.rs +377 -0
  48. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/Cargo.toml +48 -0
  49. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/examples/basic.rs +61 -0
  50. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/builder.rs +232 -0
  51. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/bulkhead.rs +223 -0
  52. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/callbacks.rs +58 -0
  53. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/circuit.rs +1156 -0
  54. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/classifier.rs +177 -0
  55. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/errors.rs +47 -0
  56. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/lib.rs +62 -0
  57. data/ext/breaker_machines_native/target/package/breaker-machines-0.6.0/src/storage.rs +377 -0
  58. data/ext/breaker_machines_native/target/release/build/clang-sys-ef8ad8b846ac8b75/out/common.rs +355 -0
  59. data/ext/breaker_machines_native/target/release/build/clang-sys-ef8ad8b846ac8b75/out/dynamic.rs +276 -0
  60. data/ext/breaker_machines_native/target/release/build/clang-sys-ef8ad8b846ac8b75/out/macros.rs +49 -0
  61. 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
  62. data/lib/breaker_machines/async_circuit.rb +47 -0
  63. data/lib/breaker_machines/async_support.rb +4 -3
  64. data/lib/breaker_machines/cascading_circuit.rb +5 -3
  65. data/lib/breaker_machines/circuit/async_state_management.rb +71 -0
  66. data/lib/breaker_machines/circuit/base.rb +59 -0
  67. data/lib/breaker_machines/circuit/callbacks.rb +7 -12
  68. data/lib/breaker_machines/circuit/configuration.rb +6 -26
  69. data/lib/breaker_machines/circuit/coordinated_state_management.rb +117 -0
  70. data/lib/breaker_machines/circuit/hedged_execution.rb +115 -0
  71. data/lib/breaker_machines/circuit/introspection.rb +1 -0
  72. data/lib/breaker_machines/circuit/native.rb +127 -0
  73. data/lib/breaker_machines/circuit/state_callbacks.rb +72 -0
  74. data/lib/breaker_machines/circuit/state_management.rb +14 -61
  75. data/lib/breaker_machines/circuit.rb +1 -7
  76. data/lib/breaker_machines/circuit_group.rb +153 -0
  77. data/lib/breaker_machines/coordinated_circuit.rb +10 -0
  78. data/lib/breaker_machines/dsl.rb +2 -2
  79. data/lib/breaker_machines/errors.rb +20 -0
  80. data/lib/breaker_machines/hedged_async_support.rb +29 -36
  81. data/lib/breaker_machines/native_extension.rb +36 -0
  82. data/lib/breaker_machines/native_speedup.rb +6 -0
  83. data/lib/breaker_machines/storage/bucket_memory.rb +4 -1
  84. data/lib/breaker_machines/storage/memory.rb +4 -1
  85. data/lib/breaker_machines/storage/native.rb +90 -0
  86. data/lib/breaker_machines/version.rb +1 -1
  87. data/lib/breaker_machines.rb +115 -11
  88. data/lib/breaker_machines_native/breaker_machines_native.bundle +0 -0
  89. data/sig/breaker_machines.rbs +20 -8
  90. metadata +107 -7
  91. 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
- after_transition any => :open do |circuit|
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 any => :closed do |circuit|
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
@@ -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(&:reset)
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(&:reset)
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/condition'
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::Condition to signal the winner instead of an Async::Channel.
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
- Async do |parent|
53
- mutex = Mutex.new
54
- condition = Async::Condition.new
55
- winner = nil
56
- exception = nil
54
+ promise = Async::Promise.new
55
+ barrier = Async::Barrier.new
57
56
 
58
- tasks = callables.map.with_index do |callable, idx|
59
- parent.async do |task|
60
- # stagger hedged attempts
61
- task.sleep(delay_ms / 1000.0) if idx.positive? && delay_ms.positive?
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
- begin
64
- res = callable.call
65
- mutex.synchronize do
66
- next if winner || exception
67
-
68
- winner = res
69
- condition.signal
70
- end
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
- # block until first signal
83
- condition.wait
76
+ # Wait for the first resolution (either success or exception)
77
+ promise.wait
78
+ end.wait
84
79
 
85
- # tear down
86
- tasks.each(&:stop)
87
-
88
- # propagate
89
- raise(exception) if exception
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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'native_extension'
4
+
5
+ # Load the native extension if available
6
+ BreakerMachines::NativeExtension.load!
@@ -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
- BreakerMachines.monotonic_time
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
- BreakerMachines.monotonic_time
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BreakerMachines
4
- VERSION = '0.4.0'
4
+ VERSION = '0.6.0'
5
5
  end