breaker_machines 0.11.0 → 0.13.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/breaker_machines/cascading_circuit.rb +3 -11
  3. data/lib/breaker_machines/circuit/async_state_management.rb +1 -35
  4. data/lib/breaker_machines/circuit/coordinated_state_management.rb +1 -34
  5. data/lib/breaker_machines/circuit/state_machine_definition.rb +51 -0
  6. data/lib/breaker_machines/circuit/state_management.rb +1 -34
  7. data/lib/breaker_machines/storage/bucket_memory.rb +2 -65
  8. data/lib/breaker_machines/storage/memory.rb +2 -65
  9. data/lib/breaker_machines/storage/memory_support.rb +80 -0
  10. data/lib/breaker_machines/version.rb +1 -1
  11. data/sig/README.md +1 -1
  12. data/sig/all.rbs +1 -1
  13. data/sig/breaker_machines/circuit.rbs +1 -1
  14. data/sig/breaker_machines/console.rbs +1 -1
  15. data/sig/breaker_machines/dsl.rbs +1 -1
  16. data/sig/breaker_machines/errors.rbs +1 -1
  17. data/sig/breaker_machines/interfaces.rbs +1 -1
  18. data/sig/breaker_machines/registry.rbs +1 -1
  19. data/sig/breaker_machines/storage.rbs +1 -1
  20. data/sig/breaker_machines/types.rbs +1 -1
  21. data/sig/manifest.yaml +1 -1
  22. metadata +20 -33
  23. data/ext/breaker_machines_native/core/Cargo.toml +0 -21
  24. data/ext/breaker_machines_native/core/examples/basic.rs +0 -61
  25. data/ext/breaker_machines_native/core/src/builder.rs +0 -232
  26. data/ext/breaker_machines_native/core/src/bulkhead.rs +0 -223
  27. data/ext/breaker_machines_native/core/src/callbacks.rs +0 -147
  28. data/ext/breaker_machines_native/core/src/circuit.rs +0 -1436
  29. data/ext/breaker_machines_native/core/src/classifier.rs +0 -177
  30. data/ext/breaker_machines_native/core/src/errors.rs +0 -47
  31. data/ext/breaker_machines_native/core/src/lib.rs +0 -62
  32. data/ext/breaker_machines_native/core/src/storage.rs +0 -377
  33. data/ext/breaker_machines_native/extconf.rb +0 -3
  34. data/ext/breaker_machines_native/ffi/Cargo.toml +0 -18
  35. data/ext/breaker_machines_native/ffi/extconf.rb +0 -62
  36. data/ext/breaker_machines_native/ffi/src/lib.rs +0 -216
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd34f2d2c0993335422fcb7c9a2495af660ec6c59dde5f57c7f9da0841cb51db
4
- data.tar.gz: fc1ba508ecbaf43dbf451ef81bcacb9b586a0003ca5148d66d80f4d51a53f926
3
+ metadata.gz: 162c659f4b4f5c54672f4eaadfa57a51caae58fce8f03ffb3a4d1347144a42d7
4
+ data.tar.gz: 90baffaebfdb68b162fcdcc05056c1058af6ae875132c0fe429537b11b8ea3fc
5
5
  SHA512:
6
- metadata.gz: 49eac0d2918c9cee306309f785009fe53ffbd4d4a26641132f2dfe8a069e0cb200a47a3d97a8748ae1bf52d4604969127d3c549db2e31cf6e25d0b95ce9eb65c
7
- data.tar.gz: a08fa2490b8b2a3b1ac6bc44853540763875ce39ce439c860b682256f01ccfbdf5e5e63176f917d9c109a3593b573a32588dddb336c34d0a1023c00bbe4c15c5
6
+ metadata.gz: 00505d9351f6d05d4d6493941e20a8c358de0ca5ee21ca3b5868c89484602dc742558ee271bf814d98d2dceb4d52ce87fc2ee32cb1d9a8b204bdc534db09a1fb
7
+ data.tar.gz: 63ce970ef5041356488e9cc9a8342a01edd0f07ce3ac665a4bdb53733e09e69c9589dc23b540f2257043eed57f91528ff3dbd8fb8e88d25ab7277abded787ffc
@@ -95,17 +95,9 @@ module BreakerMachines
95
95
  @cascade_triggered_at.value = BreakerMachines.monotonic_time
96
96
 
97
97
  @dependent_circuits.each do |circuit_name|
98
- # First try to find circuit in registry
99
- circuit = BreakerMachines.registry.find(circuit_name)
100
-
101
- # If not found and we have an owner, try to get it from the owner
102
- if !circuit && @config[:owner]
103
- owner = @config[:owner]
104
- # Handle WeakRef if present
105
- owner = owner.__getobj__ if owner.is_a?(WeakRef)
106
-
107
- circuit = owner.circuit(circuit_name) if owner.respond_to?(:circuit)
108
- end
98
+ # Resolve via registry, falling back to the owner (inherited from
99
+ # CoordinatedStateManagement).
100
+ circuit = find_dependent_circuit(circuit_name)
109
101
 
110
102
  next unless circuit
111
103
  next unless circuit.closed? || circuit.half_open?
@@ -14,11 +14,6 @@ module BreakerMachines
14
14
  # - Fiber-safe execution
15
15
  # - Concurrent transition handling
16
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
17
  event :attempt_recovery do
23
18
  transition open: :half_open
24
19
  end
@@ -28,36 +23,7 @@ module BreakerMachines
28
23
  transition closed: :closed
29
24
  end
30
25
 
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
26
+ instance_eval(&StateMachineDefinition::COMMON)
61
27
  end
62
28
 
63
29
  # Additional async event methods are automatically generated:
@@ -10,11 +10,6 @@ module BreakerMachines
10
10
  included do
11
11
  # Override the state machine to add coordinated guards
12
12
  state_machine :status, initial: :closed do
13
- event :trip do
14
- transition closed: :open
15
- transition half_open: :open
16
- end
17
-
18
13
  event :attempt_recovery do
19
14
  transition open: :half_open,
20
15
  if: lambda(&:recovery_allowed?)
@@ -26,35 +21,7 @@ module BreakerMachines
26
21
  transition closed: :closed
27
22
  end
28
23
 
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
24
+ instance_eval(&StateMachineDefinition::COMMON)
58
25
  end
59
26
  end
60
27
 
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BreakerMachines
4
+ class Circuit
5
+ # Shared state machine definition for circuit breakers.
6
+ #
7
+ # The trip/force_open/force_close/hard_reset events and the transition
8
+ # callbacks are identical across the sync, async, and coordinated state
9
+ # management modules. Only the +attempt_recovery+ and +reset+ events differ
10
+ # (coordinated circuits add guard conditions), so those stay in each module
11
+ # while everything common is spliced in via instance_eval(&COMMON).
12
+ module StateMachineDefinition
13
+ COMMON = proc do
14
+ event :trip do
15
+ transition closed: :open
16
+ transition half_open: :open
17
+ end
18
+
19
+ event :force_open do
20
+ transition any => :open
21
+ end
22
+
23
+ event :force_close do
24
+ transition any => :closed
25
+ end
26
+
27
+ event :hard_reset do
28
+ transition any => :closed
29
+ end
30
+
31
+ before_transition on: :hard_reset do |circuit|
32
+ circuit.storage&.clear(circuit.name)
33
+ circuit.half_open_attempts.value = 0
34
+ circuit.half_open_successes.value = 0
35
+ end
36
+
37
+ after_transition to: :open do |circuit|
38
+ circuit.send(:on_circuit_open)
39
+ end
40
+
41
+ after_transition to: :closed do |circuit|
42
+ circuit.send(:on_circuit_close)
43
+ end
44
+
45
+ after_transition from: :open, to: :half_open do |circuit|
46
+ circuit.send(:on_circuit_half_open)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -9,11 +9,6 @@ module BreakerMachines
9
9
 
10
10
  included do
11
11
  state_machine :status, initial: :closed do
12
- event :trip do
13
- transition closed: :open
14
- transition half_open: :open
15
- end
16
-
17
12
  event :attempt_recovery do
18
13
  transition open: :half_open
19
14
  end
@@ -23,35 +18,7 @@ module BreakerMachines
23
18
  transition closed: :closed
24
19
  end
25
20
 
26
- event :force_open do
27
- transition any => :open
28
- end
29
-
30
- event :force_close do
31
- transition any => :closed
32
- end
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|
45
- circuit.send(:on_circuit_open)
46
- end
47
-
48
- after_transition to: :closed do |circuit|
49
- circuit.send(:on_circuit_close)
50
- end
51
-
52
- after_transition from: :open, to: :half_open do |circuit|
53
- circuit.send(:on_circuit_half_open)
54
- end
21
+ instance_eval(&StateMachineDefinition::COMMON)
55
22
  end
56
23
  end
57
24
  end
@@ -12,6 +12,8 @@ module BreakerMachines
12
12
  # environments as memory is not shared between processes. Use Cache backend
13
13
  # with an external cache store (Redis, Memcached) for distributed setups.
14
14
  class BucketMemory < Base
15
+ include MemorySupport
16
+
15
17
  BUCKET_SIZE = 1 # 1 second per bucket
16
18
 
17
19
  def initialize(**options)
@@ -25,40 +27,6 @@ module BreakerMachines
25
27
  @start_time = BreakerMachines.monotonic_time
26
28
  end
27
29
 
28
- def get_status(circuit_name)
29
- circuit_data = @circuits[circuit_name]
30
- return nil unless circuit_data
31
-
32
- BreakerMachines::Status.new(
33
- status: circuit_data[:status],
34
- opened_at: circuit_data[:opened_at]
35
- )
36
- end
37
-
38
- def set_status(circuit_name, status, opened_at = nil)
39
- @circuits[circuit_name] = {
40
- status: status,
41
- opened_at: opened_at,
42
- updated_at: monotonic_time
43
- }
44
- end
45
-
46
- def record_success(circuit_name, duration)
47
- record_event(circuit_name, :success, duration)
48
- end
49
-
50
- def record_failure(circuit_name, duration)
51
- record_event(circuit_name, :failure, duration)
52
- end
53
-
54
- def success_count(circuit_name, window_seconds)
55
- count_events(circuit_name, :success, window_seconds)
56
- end
57
-
58
- def failure_count(circuit_name, window_seconds)
59
- count_events(circuit_name, :failure, window_seconds)
60
- end
61
-
62
30
  def clear(circuit_name)
63
31
  @circuits.delete(circuit_name)
64
32
  @circuit_buckets.delete(circuit_name)
@@ -71,32 +39,6 @@ module BreakerMachines
71
39
  @event_logs.clear
72
40
  end
73
41
 
74
- def record_event_with_details(circuit_name, type, duration, error: nil, new_state: nil)
75
- events = @event_logs.compute_if_absent(circuit_name) { Concurrent::Array.new }
76
-
77
- event = {
78
- type: type,
79
- timestamp: monotonic_time,
80
- duration_ms: (duration * 1000).round(2)
81
- }
82
-
83
- event[:error_class] = error.class.name if error
84
- event[:error_message] = error.message if error
85
- event[:new_state] = new_state if new_state
86
-
87
- events << event
88
-
89
- # Keep only the most recent events
90
- events.shift while events.size > @max_events
91
- end
92
-
93
- def event_log(circuit_name, limit)
94
- events = @event_logs[circuit_name]
95
- return [] unless events
96
-
97
- events.last(limit).map(&:dup)
98
- end
99
-
100
42
  private
101
43
 
102
44
  def record_event(circuit_name, type, _duration)
@@ -161,11 +103,6 @@ module BreakerMachines
161
103
  (monotonic_time / BUCKET_SIZE).to_i
162
104
  end
163
105
 
164
- def monotonic_time
165
- # Return time relative to storage creation (matches Rust implementation)
166
- BreakerMachines.monotonic_time - @start_time
167
- end
168
-
169
106
  def with_timeout(_timeout_ms)
170
107
  # BucketMemory operations should be instant, but we'll still respect the timeout
171
108
  # This is more for consistency and to catch any potential deadlocks
@@ -11,6 +11,8 @@ module BreakerMachines
11
11
  # environments as memory is not shared between processes. Use Cache backend
12
12
  # with an external cache store (Redis, Memcached) for distributed setups.
13
13
  class Memory < Base
14
+ include MemorySupport
15
+
14
16
  def initialize(**options)
15
17
  super
16
18
  @circuits = Concurrent::Map.new
@@ -21,40 +23,6 @@ module BreakerMachines
21
23
  @start_time = BreakerMachines.monotonic_time
22
24
  end
23
25
 
24
- def get_status(circuit_name)
25
- circuit_data = @circuits[circuit_name]
26
- return nil unless circuit_data
27
-
28
- BreakerMachines::Status.new(
29
- status: circuit_data[:status],
30
- opened_at: circuit_data[:opened_at]
31
- )
32
- end
33
-
34
- def set_status(circuit_name, status, opened_at = nil)
35
- @circuits[circuit_name] = {
36
- status: status,
37
- opened_at: opened_at,
38
- updated_at: monotonic_time
39
- }
40
- end
41
-
42
- def record_success(circuit_name, duration)
43
- record_event(circuit_name, :success, duration)
44
- end
45
-
46
- def record_failure(circuit_name, duration)
47
- record_event(circuit_name, :failure, duration)
48
- end
49
-
50
- def success_count(circuit_name, window_seconds)
51
- count_events(circuit_name, :success, window_seconds)
52
- end
53
-
54
- def failure_count(circuit_name, window_seconds)
55
- count_events(circuit_name, :failure, window_seconds)
56
- end
57
-
58
26
  def clear(circuit_name)
59
27
  @circuits.delete(circuit_name)
60
28
  @events.delete(circuit_name)
@@ -67,32 +35,6 @@ module BreakerMachines
67
35
  @event_logs.clear
68
36
  end
69
37
 
70
- def record_event_with_details(circuit_name, type, duration, error: nil, new_state: nil)
71
- events = @event_logs.compute_if_absent(circuit_name) { Concurrent::Array.new }
72
-
73
- event = {
74
- type: type,
75
- timestamp: monotonic_time,
76
- duration_ms: (duration * 1000).round(2)
77
- }
78
-
79
- event[:error_class] = error.class.name if error
80
- event[:error_message] = error.message if error
81
- event[:new_state] = new_state if new_state
82
-
83
- events << event
84
-
85
- # Keep only the most recent events
86
- events.shift while events.size > @max_events
87
- end
88
-
89
- def event_log(circuit_name, limit)
90
- events = @event_logs[circuit_name]
91
- return [] unless events
92
-
93
- events.last(limit).map(&:dup)
94
- end
95
-
96
38
  def with_timeout(_timeout_ms)
97
39
  # Memory operations should be instant, but we'll still respect the timeout
98
40
  # This is more for consistency and to catch any potential deadlocks
@@ -130,11 +72,6 @@ module BreakerMachines
130
72
  cutoff_time = monotonic_time - window_seconds
131
73
  events.count { |e| e[:type] == type && e[:timestamp] >= cutoff_time }
132
74
  end
133
-
134
- def monotonic_time
135
- # Return time relative to storage creation (matches Rust implementation)
136
- BreakerMachines.monotonic_time - @start_time
137
- end
138
75
  end
139
76
  end
140
77
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BreakerMachines
4
+ module Storage
5
+ # Methods shared by the in-memory storage backends (Memory and BucketMemory).
6
+ #
7
+ # Status storage, event-detail logging and the relative monotonic clock are
8
+ # identical between the two backends; only the event counting strategy
9
+ # (raw event arrays vs. fixed-size buckets) differs, so those methods stay
10
+ # in each backend.
11
+ module MemorySupport
12
+ def get_status(circuit_name)
13
+ circuit_data = @circuits[circuit_name]
14
+ return nil unless circuit_data
15
+
16
+ BreakerMachines::Status.new(
17
+ status: circuit_data[:status],
18
+ opened_at: circuit_data[:opened_at]
19
+ )
20
+ end
21
+
22
+ def set_status(circuit_name, status, opened_at = nil)
23
+ @circuits[circuit_name] = {
24
+ status: status,
25
+ opened_at: opened_at,
26
+ updated_at: monotonic_time
27
+ }
28
+ end
29
+
30
+ def record_success(circuit_name, duration)
31
+ record_event(circuit_name, :success, duration)
32
+ end
33
+
34
+ def record_failure(circuit_name, duration)
35
+ record_event(circuit_name, :failure, duration)
36
+ end
37
+
38
+ def success_count(circuit_name, window_seconds)
39
+ count_events(circuit_name, :success, window_seconds)
40
+ end
41
+
42
+ def failure_count(circuit_name, window_seconds)
43
+ count_events(circuit_name, :failure, window_seconds)
44
+ end
45
+
46
+ def record_event_with_details(circuit_name, type, duration, error: nil, new_state: nil)
47
+ events = @event_logs.compute_if_absent(circuit_name) { Concurrent::Array.new }
48
+
49
+ event = {
50
+ type: type,
51
+ timestamp: monotonic_time,
52
+ duration_ms: (duration * 1000).round(2)
53
+ }
54
+
55
+ event[:error_class] = error.class.name if error
56
+ event[:error_message] = error.message if error
57
+ event[:new_state] = new_state if new_state
58
+
59
+ events << event
60
+
61
+ # Keep only the most recent events
62
+ events.shift while events.size > @max_events
63
+ end
64
+
65
+ def event_log(circuit_name, limit)
66
+ events = @event_logs[circuit_name]
67
+ return [] unless events
68
+
69
+ events.last(limit).map(&:dup)
70
+ end
71
+
72
+ private
73
+
74
+ def monotonic_time
75
+ # Return time relative to storage creation (matches Rust implementation)
76
+ BreakerMachines.monotonic_time - @start_time
77
+ end
78
+ end
79
+ end
80
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BreakerMachines
4
- VERSION = '0.11.0'
4
+ VERSION = '0.13.0'
5
5
  end
data/sig/README.md CHANGED
@@ -71,4 +71,4 @@ end
71
71
  The type signatures define several interfaces:
72
72
  - `_StorageBackend` - For custom storage implementations
73
73
  - `_MetricsRecorder` - For custom metrics recording
74
- - `_CircuitLike` - For circuit-compatible objects
74
+ - `_CircuitLike` - For circuit-compatible objects
data/sig/all.rbs CHANGED
@@ -22,4 +22,4 @@
22
22
 
23
23
  # Import all type definitions
24
24
  use BreakerMachines::*
25
- use BreakerMachines::Storage::*
25
+ use BreakerMachines::Storage::*
@@ -151,4 +151,4 @@ module BreakerMachines
151
151
  def invoke_fallback: (StandardError error) -> untyped
152
152
  def invoke_single_fallback: (Proc | untyped fallback, StandardError error) -> untyped
153
153
  end
154
- end
154
+ end
@@ -29,4 +29,4 @@ module BreakerMachines
29
29
  def colorize_state: (Symbol state) -> String
30
30
  def colorize_event_type: (Symbol type) -> String
31
31
  end
32
- end
32
+ end
@@ -47,4 +47,4 @@ module BreakerMachines
47
47
  def parallel_calls: (Integer count, ?timeout: Integer?) -> void
48
48
  end
49
49
  end
50
- end
50
+ end
@@ -21,4 +21,4 @@ module BreakerMachines
21
21
 
22
22
  class StorageError < Error
23
23
  end
24
- end
24
+ end
@@ -43,4 +43,4 @@ module BreakerMachines
43
43
  def reset: () -> bool
44
44
  def stats: () -> Hash[Symbol, untyped]
45
45
  end
46
- end
46
+ end
@@ -27,4 +27,4 @@ module BreakerMachines
27
27
 
28
28
  def cleanup_dead_references_unsafe: () -> void
29
29
  end
30
- end
30
+ end
@@ -62,4 +62,4 @@ module BreakerMachines
62
62
  def event_log: (String circuit_name, ?Integer limit) -> Array[untyped]
63
63
  end
64
64
  end
65
- end
65
+ end
@@ -94,4 +94,4 @@ module BreakerMachines
94
94
  status: circuit_state,
95
95
  opened_at: Float?
96
96
  }
97
- end
97
+ end
data/sig/manifest.yaml CHANGED
@@ -2,4 +2,4 @@ dependencies:
2
2
  - name: activesupport
3
3
  - name: concurrent-ruby
4
4
  - name: state_machines
5
- - name: zeitwerk
5
+ - name: zeitwerk