breaker_machines 0.1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +1894 -0
- data/lib/breaker_machines/circuit/callbacks.rb +132 -0
- data/lib/breaker_machines/circuit/configuration.rb +73 -0
- data/lib/breaker_machines/circuit/execution.rb +211 -0
- data/lib/breaker_machines/circuit/introspection.rb +77 -0
- data/lib/breaker_machines/circuit/state_management.rb +105 -0
- data/lib/breaker_machines/circuit.rb +14 -0
- data/lib/breaker_machines/console.rb +345 -0
- data/lib/breaker_machines/dsl.rb +274 -0
- data/lib/breaker_machines/errors.rb +30 -0
- data/lib/breaker_machines/registry.rb +99 -0
- data/lib/breaker_machines/storage/base.rb +47 -0
- data/lib/breaker_machines/storage/bucket_memory.rb +163 -0
- data/lib/breaker_machines/storage/memory.rb +127 -0
- data/lib/breaker_machines/storage/null.rb +45 -0
- data/lib/breaker_machines/storage.rb +8 -0
- data/lib/breaker_machines/version.rb +5 -0
- data/lib/breaker_machines.rb +82 -0
- data/sig/README.md +74 -0
- data/sig/all.rbs +25 -0
- data/sig/breaker_machines/circuit.rbs +154 -0
- data/sig/breaker_machines/console.rbs +32 -0
- data/sig/breaker_machines/dsl.rbs +50 -0
- data/sig/breaker_machines/errors.rbs +24 -0
- data/sig/breaker_machines/interfaces.rbs +46 -0
- data/sig/breaker_machines/registry.rbs +30 -0
- data/sig/breaker_machines/storage.rbs +65 -0
- data/sig/breaker_machines/types.rbs +97 -0
- data/sig/breaker_machines.rbs +30 -0
- data/sig/manifest.yaml +5 -0
- metadata +167 -0
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
require 'weakref'
|
5
|
+
|
6
|
+
module BreakerMachines
|
7
|
+
# Global registry for tracking all circuit breaker instances
|
8
|
+
class Registry
|
9
|
+
include Singleton
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@circuits = Concurrent::Map.new
|
13
|
+
@mutex = Mutex.new
|
14
|
+
@registration_count = 0
|
15
|
+
@cleanup_interval = 100 # Clean up every N registrations
|
16
|
+
end
|
17
|
+
|
18
|
+
# Register a circuit instance
|
19
|
+
def register(circuit)
|
20
|
+
@mutex.synchronize do
|
21
|
+
# Use circuit object as key - Concurrent::Map handles object identity correctly
|
22
|
+
@circuits[circuit] = WeakRef.new(circuit)
|
23
|
+
|
24
|
+
# Periodic cleanup
|
25
|
+
@registration_count += 1
|
26
|
+
if @registration_count >= @cleanup_interval
|
27
|
+
cleanup_dead_references_unsafe
|
28
|
+
@registration_count = 0
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Unregister a circuit instance
|
34
|
+
def unregister(circuit)
|
35
|
+
@mutex.synchronize do
|
36
|
+
@circuits.delete(circuit)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Get all active circuits
|
41
|
+
def all_circuits
|
42
|
+
@mutex.synchronize do
|
43
|
+
@circuits.values.map do |weak_ref|
|
44
|
+
weak_ref.__getobj__
|
45
|
+
rescue WeakRef::RefError
|
46
|
+
nil
|
47
|
+
end.compact
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Find circuits by name
|
52
|
+
def find_by_name(name)
|
53
|
+
all_circuits.select { |circuit| circuit.name == name }
|
54
|
+
end
|
55
|
+
|
56
|
+
# Get summary statistics
|
57
|
+
def stats_summary
|
58
|
+
circuits = all_circuits
|
59
|
+
{
|
60
|
+
total: circuits.size,
|
61
|
+
by_state: circuits.group_by(&:status_name).transform_values(&:count),
|
62
|
+
by_name: circuits.group_by(&:name).transform_values(&:count)
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
# Get detailed information for all circuits
|
67
|
+
def detailed_report
|
68
|
+
all_circuits.map(&:to_h)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Clear all circuits (useful for testing)
|
72
|
+
def clear
|
73
|
+
@mutex.synchronize do
|
74
|
+
@circuits.clear
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Clean up dead references (thread-safe)
|
79
|
+
def cleanup_dead_references
|
80
|
+
@mutex.synchronize do
|
81
|
+
cleanup_dead_references_unsafe
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
# Clean up dead references (must be called within mutex)
|
88
|
+
def cleanup_dead_references_unsafe
|
89
|
+
dead_ids = []
|
90
|
+
@circuits.each_pair do |id, weak_ref|
|
91
|
+
weak_ref.__getobj__
|
92
|
+
rescue WeakRef::RefError
|
93
|
+
dead_ids << id
|
94
|
+
end
|
95
|
+
|
96
|
+
dead_ids.each { |id| @circuits.delete(id) }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BreakerMachines
|
4
|
+
module Storage
|
5
|
+
# Abstract base class for storage backends
|
6
|
+
class Base
|
7
|
+
def initialize(**options)
|
8
|
+
@options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
# Status management
|
12
|
+
def get_status(circuit_name)
|
13
|
+
raise NotImplementedError
|
14
|
+
end
|
15
|
+
|
16
|
+
def set_status(circuit_name, status, opened_at = nil)
|
17
|
+
raise NotImplementedError
|
18
|
+
end
|
19
|
+
|
20
|
+
# Metrics tracking
|
21
|
+
def record_success(circuit_name, duration)
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
|
25
|
+
def record_failure(circuit_name, duration)
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
|
29
|
+
def success_count(circuit_name, window_seconds)
|
30
|
+
raise NotImplementedError
|
31
|
+
end
|
32
|
+
|
33
|
+
def failure_count(circuit_name, window_seconds)
|
34
|
+
raise NotImplementedError
|
35
|
+
end
|
36
|
+
|
37
|
+
# Cleanup
|
38
|
+
def clear(circuit_name)
|
39
|
+
raise NotImplementedError
|
40
|
+
end
|
41
|
+
|
42
|
+
def clear_all
|
43
|
+
raise NotImplementedError
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'concurrent/map'
|
4
|
+
require 'concurrent/array'
|
5
|
+
|
6
|
+
module BreakerMachines
|
7
|
+
module Storage
|
8
|
+
# Efficient bucket-based memory storage implementation
|
9
|
+
# Uses fixed-size circular buffers for constant-time event counting
|
10
|
+
class BucketMemory < Base
|
11
|
+
BUCKET_SIZE = 1 # 1 second per bucket
|
12
|
+
|
13
|
+
def initialize(**options)
|
14
|
+
super
|
15
|
+
@circuits = Concurrent::Map.new
|
16
|
+
@circuit_buckets = Concurrent::Map.new
|
17
|
+
@event_logs = Concurrent::Map.new
|
18
|
+
@bucket_count = options[:bucket_count] || 300 # Default 5 minutes
|
19
|
+
@max_events = options[:max_events] || 100
|
20
|
+
end
|
21
|
+
|
22
|
+
def get_status(circuit_name)
|
23
|
+
circuit_data = @circuits[circuit_name]
|
24
|
+
return nil unless circuit_data
|
25
|
+
|
26
|
+
{
|
27
|
+
status: circuit_data[:status],
|
28
|
+
opened_at: circuit_data[:opened_at]
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def set_status(circuit_name, status, opened_at = nil)
|
33
|
+
@circuits[circuit_name] = {
|
34
|
+
status: status,
|
35
|
+
opened_at: opened_at,
|
36
|
+
updated_at: monotonic_time
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def record_success(circuit_name, duration)
|
41
|
+
record_event(circuit_name, :success, duration)
|
42
|
+
end
|
43
|
+
|
44
|
+
def record_failure(circuit_name, duration)
|
45
|
+
record_event(circuit_name, :failure, duration)
|
46
|
+
end
|
47
|
+
|
48
|
+
def success_count(circuit_name, window_seconds)
|
49
|
+
count_events(circuit_name, :success, window_seconds)
|
50
|
+
end
|
51
|
+
|
52
|
+
def failure_count(circuit_name, window_seconds)
|
53
|
+
count_events(circuit_name, :failure, window_seconds)
|
54
|
+
end
|
55
|
+
|
56
|
+
def clear(circuit_name)
|
57
|
+
@circuits.delete(circuit_name)
|
58
|
+
@circuit_buckets.delete(circuit_name)
|
59
|
+
@event_logs.delete(circuit_name)
|
60
|
+
end
|
61
|
+
|
62
|
+
def clear_all
|
63
|
+
@circuits.clear
|
64
|
+
@circuit_buckets.clear
|
65
|
+
@event_logs.clear
|
66
|
+
end
|
67
|
+
|
68
|
+
def record_event_with_details(circuit_name, type, duration, error: nil, new_state: nil)
|
69
|
+
events = @event_logs.compute_if_absent(circuit_name) { Concurrent::Array.new }
|
70
|
+
|
71
|
+
event = {
|
72
|
+
type: type,
|
73
|
+
timestamp: monotonic_time,
|
74
|
+
duration_ms: (duration * 1000).round(2)
|
75
|
+
}
|
76
|
+
|
77
|
+
event[:error_class] = error.class.name if error
|
78
|
+
event[:error_message] = error.message if error
|
79
|
+
event[:new_state] = new_state if new_state
|
80
|
+
|
81
|
+
events << event
|
82
|
+
|
83
|
+
# Keep only the most recent events
|
84
|
+
events.shift while events.size > @max_events
|
85
|
+
end
|
86
|
+
|
87
|
+
def event_log(circuit_name, limit)
|
88
|
+
events = @event_logs[circuit_name]
|
89
|
+
return [] unless events
|
90
|
+
|
91
|
+
events.last(limit).map(&:dup)
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def record_event(circuit_name, type, _duration)
|
97
|
+
buckets = @circuit_buckets.compute_if_absent(circuit_name) do
|
98
|
+
{
|
99
|
+
successes: Concurrent::Array.new(@bucket_count) { Concurrent::AtomicFixnum.new(0) },
|
100
|
+
failures: Concurrent::Array.new(@bucket_count) { Concurrent::AtomicFixnum.new(0) },
|
101
|
+
last_bucket_time: Concurrent::AtomicReference.new(current_bucket_time)
|
102
|
+
}
|
103
|
+
end
|
104
|
+
|
105
|
+
current_time = current_bucket_time
|
106
|
+
rotate_buckets_if_needed(buckets, current_time)
|
107
|
+
|
108
|
+
bucket_index = current_time % @bucket_count
|
109
|
+
counter_array = type == :success ? buckets[:successes] : buckets[:failures]
|
110
|
+
counter_array[bucket_index].increment
|
111
|
+
end
|
112
|
+
|
113
|
+
def count_events(circuit_name, type, window_seconds)
|
114
|
+
buckets = @circuit_buckets[circuit_name]
|
115
|
+
return 0 unless buckets
|
116
|
+
|
117
|
+
current_time = current_bucket_time
|
118
|
+
rotate_buckets_if_needed(buckets, current_time)
|
119
|
+
|
120
|
+
# Calculate how many buckets to count
|
121
|
+
buckets_to_count = [window_seconds / BUCKET_SIZE, @bucket_count].min.to_i
|
122
|
+
|
123
|
+
counter_array = type == :success ? buckets[:successes] : buckets[:failures]
|
124
|
+
total = 0
|
125
|
+
|
126
|
+
buckets_to_count.times do |i|
|
127
|
+
bucket_index = (current_time - i) % @bucket_count
|
128
|
+
total += counter_array[bucket_index].value
|
129
|
+
end
|
130
|
+
|
131
|
+
total
|
132
|
+
end
|
133
|
+
|
134
|
+
def rotate_buckets_if_needed(buckets, current_time)
|
135
|
+
last_time = buckets[:last_bucket_time].value
|
136
|
+
|
137
|
+
return if current_time == last_time
|
138
|
+
|
139
|
+
# Only one thread should rotate buckets
|
140
|
+
return unless buckets[:last_bucket_time].compare_and_set(last_time, current_time)
|
141
|
+
|
142
|
+
# Clear buckets that are now outdated
|
143
|
+
time_diff = current_time - last_time
|
144
|
+
buckets_to_clear = [time_diff, @bucket_count].min
|
145
|
+
|
146
|
+
buckets_to_clear.times do |i|
|
147
|
+
# Clear the bucket that will be reused
|
148
|
+
bucket_index = (last_time + i + 1) % @bucket_count
|
149
|
+
buckets[:successes][bucket_index].value = 0
|
150
|
+
buckets[:failures][bucket_index].value = 0
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def current_bucket_time
|
155
|
+
(monotonic_time / BUCKET_SIZE).to_i
|
156
|
+
end
|
157
|
+
|
158
|
+
def monotonic_time
|
159
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'concurrent/map'
|
4
|
+
require 'concurrent/array'
|
5
|
+
|
6
|
+
module BreakerMachines
|
7
|
+
module Storage
|
8
|
+
# High-performance in-memory storage backend with thread-safe operations
|
9
|
+
class Memory < Base
|
10
|
+
def initialize(**options)
|
11
|
+
super
|
12
|
+
@circuits = Concurrent::Map.new
|
13
|
+
@events = Concurrent::Map.new
|
14
|
+
@event_logs = Concurrent::Map.new
|
15
|
+
@max_events = options[:max_events] || 100
|
16
|
+
end
|
17
|
+
|
18
|
+
def get_status(circuit_name)
|
19
|
+
circuit_data = @circuits[circuit_name]
|
20
|
+
return nil unless circuit_data
|
21
|
+
|
22
|
+
{
|
23
|
+
status: circuit_data[:status],
|
24
|
+
opened_at: circuit_data[:opened_at]
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def set_status(circuit_name, status, opened_at = nil)
|
29
|
+
@circuits[circuit_name] = {
|
30
|
+
status: status,
|
31
|
+
opened_at: opened_at,
|
32
|
+
updated_at: monotonic_time
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def record_success(circuit_name, duration)
|
37
|
+
record_event(circuit_name, :success, duration)
|
38
|
+
end
|
39
|
+
|
40
|
+
def record_failure(circuit_name, duration)
|
41
|
+
record_event(circuit_name, :failure, duration)
|
42
|
+
end
|
43
|
+
|
44
|
+
def success_count(circuit_name, window_seconds)
|
45
|
+
count_events(circuit_name, :success, window_seconds)
|
46
|
+
end
|
47
|
+
|
48
|
+
def failure_count(circuit_name, window_seconds)
|
49
|
+
count_events(circuit_name, :failure, window_seconds)
|
50
|
+
end
|
51
|
+
|
52
|
+
def clear(circuit_name)
|
53
|
+
@circuits.delete(circuit_name)
|
54
|
+
@events.delete(circuit_name)
|
55
|
+
@event_logs.delete(circuit_name)
|
56
|
+
end
|
57
|
+
|
58
|
+
def clear_all
|
59
|
+
@circuits.clear
|
60
|
+
@events.clear
|
61
|
+
@event_logs.clear
|
62
|
+
end
|
63
|
+
|
64
|
+
def record_event_with_details(circuit_name, type, duration, error: nil, new_state: nil)
|
65
|
+
events = @event_logs.compute_if_absent(circuit_name) { Concurrent::Array.new }
|
66
|
+
|
67
|
+
event = {
|
68
|
+
type: type,
|
69
|
+
timestamp: monotonic_time,
|
70
|
+
duration_ms: (duration * 1000).round(2)
|
71
|
+
}
|
72
|
+
|
73
|
+
event[:error_class] = error.class.name if error
|
74
|
+
event[:error_message] = error.message if error
|
75
|
+
event[:new_state] = new_state if new_state
|
76
|
+
|
77
|
+
events << event
|
78
|
+
|
79
|
+
# Keep only the most recent events
|
80
|
+
events.shift while events.size > @max_events
|
81
|
+
end
|
82
|
+
|
83
|
+
def event_log(circuit_name, limit)
|
84
|
+
events = @event_logs[circuit_name]
|
85
|
+
return [] unless events
|
86
|
+
|
87
|
+
events.last(limit).map(&:dup)
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def record_event(circuit_name, type, duration)
|
93
|
+
# Initialize if needed
|
94
|
+
@events.compute_if_absent(circuit_name) { Concurrent::Array.new }
|
95
|
+
|
96
|
+
# Get the array and add event
|
97
|
+
events = @events[circuit_name]
|
98
|
+
current_time = monotonic_time
|
99
|
+
|
100
|
+
# Add new event
|
101
|
+
events << {
|
102
|
+
type: type,
|
103
|
+
duration: duration,
|
104
|
+
timestamp: current_time
|
105
|
+
}
|
106
|
+
|
107
|
+
# Clean old events periodically (every 100 events)
|
108
|
+
return unless events.size > 100
|
109
|
+
|
110
|
+
cutoff_time = current_time - 300 # Keep 5 minutes of history
|
111
|
+
events.delete_if { |e| e[:timestamp] < cutoff_time }
|
112
|
+
end
|
113
|
+
|
114
|
+
def count_events(circuit_name, type, window_seconds)
|
115
|
+
events = @events[circuit_name]
|
116
|
+
return 0 unless events
|
117
|
+
|
118
|
+
cutoff_time = monotonic_time - window_seconds
|
119
|
+
events.count { |e| e[:type] == type && e[:timestamp] >= cutoff_time }
|
120
|
+
end
|
121
|
+
|
122
|
+
def monotonic_time
|
123
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BreakerMachines
|
4
|
+
module Storage
|
5
|
+
# A no-op storage backend for minimal overhead
|
6
|
+
# Use this when you don't need event logging or metrics
|
7
|
+
class Null < Base
|
8
|
+
def record_success(_circuit_name, _duration = nil)
|
9
|
+
# No-op
|
10
|
+
end
|
11
|
+
|
12
|
+
def record_failure(_circuit_name, _duration = nil)
|
13
|
+
# No-op
|
14
|
+
end
|
15
|
+
|
16
|
+
def success_count(_circuit_name, _window = nil)
|
17
|
+
0
|
18
|
+
end
|
19
|
+
|
20
|
+
def failure_count(_circuit_name, _window = nil)
|
21
|
+
0
|
22
|
+
end
|
23
|
+
|
24
|
+
def get_status(_circuit_name)
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def set_status(_circuit_name, _status, _opened_at = nil)
|
29
|
+
# No-op
|
30
|
+
end
|
31
|
+
|
32
|
+
def clear(_circuit_name)
|
33
|
+
# No-op
|
34
|
+
end
|
35
|
+
|
36
|
+
def event_log(_circuit_name, _limit = 20)
|
37
|
+
[]
|
38
|
+
end
|
39
|
+
|
40
|
+
def record_event_with_details(_circuit_name, _event_type, _duration, _metadata = {})
|
41
|
+
# No-op
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'zeitwerk'
|
4
|
+
require 'active_support'
|
5
|
+
require 'active_support/core_ext'
|
6
|
+
require_relative 'breaker_machines/errors'
|
7
|
+
|
8
|
+
loader = Zeitwerk::Loader.for_gem
|
9
|
+
loader.inflector.inflect('dsl' => 'DSL')
|
10
|
+
loader.ignore("#{__dir__}/breaker_machines/errors.rb")
|
11
|
+
loader.ignore("#{__dir__}/breaker_machines/console.rb")
|
12
|
+
loader.setup
|
13
|
+
|
14
|
+
# BreakerMachines provides a thread-safe implementation of the Circuit Breaker pattern
|
15
|
+
# for Ruby applications, helping to prevent cascading failures in distributed systems.
|
16
|
+
module BreakerMachines
|
17
|
+
class << self
|
18
|
+
def loader
|
19
|
+
loader
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Global configuration
|
24
|
+
include ActiveSupport::Configurable
|
25
|
+
|
26
|
+
config_accessor :default_storage, default: :bucket_memory
|
27
|
+
config_accessor :default_timeout, default: nil
|
28
|
+
config_accessor :default_reset_timeout, default: 60
|
29
|
+
config_accessor :default_failure_threshold, default: 5
|
30
|
+
config_accessor :log_events, default: true
|
31
|
+
config_accessor :fiber_safe, default: false
|
32
|
+
|
33
|
+
class << self
|
34
|
+
def configure
|
35
|
+
yield config
|
36
|
+
end
|
37
|
+
|
38
|
+
def setup_notifications
|
39
|
+
return unless config.log_events
|
40
|
+
|
41
|
+
ActiveSupport::Notifications.subscribe(/^breaker_machines\./) do |name, _start, _finish, _id, payload|
|
42
|
+
event_type = name.split('.').last
|
43
|
+
circuit_name = payload[:circuit]
|
44
|
+
|
45
|
+
case event_type
|
46
|
+
when 'opened'
|
47
|
+
logger&.warn "[BreakerMachines] Circuit '#{circuit_name}' opened"
|
48
|
+
when 'closed'
|
49
|
+
logger&.info "[BreakerMachines] Circuit '#{circuit_name}' closed"
|
50
|
+
when 'half_opened'
|
51
|
+
logger&.info "[BreakerMachines] Circuit '#{circuit_name}' half-opened"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def logger
|
57
|
+
@logger ||= ActiveSupport::Logger.new($stdout)
|
58
|
+
end
|
59
|
+
|
60
|
+
attr_writer :logger
|
61
|
+
|
62
|
+
def instrument(event, payload = {})
|
63
|
+
return unless config.log_events
|
64
|
+
|
65
|
+
ActiveSupport::Notifications.instrument("breaker_machines.#{event}", payload)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Launch the interactive console
|
69
|
+
def console
|
70
|
+
require_relative 'breaker_machines/console'
|
71
|
+
Console.start
|
72
|
+
end
|
73
|
+
|
74
|
+
# Get the global registry
|
75
|
+
def registry
|
76
|
+
Registry.instance
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Set up notifications on first use
|
81
|
+
setup_notifications if config.log_events
|
82
|
+
end
|
data/sig/README.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# BreakerMachines RBS Type Signatures
|
2
|
+
|
3
|
+
This directory contains RBS (Ruby type signatures) for the BreakerMachines gem.
|
4
|
+
|
5
|
+
## Structure
|
6
|
+
|
7
|
+
- `breaker_machines.rbs` - Main module and configuration
|
8
|
+
- `breaker_machines/` - Type signatures for all classes and modules
|
9
|
+
- `circuit.rbs` - Circuit class and its modules
|
10
|
+
- `errors.rbs` - Error classes
|
11
|
+
- `storage.rbs` - Storage backends
|
12
|
+
- `dsl.rbs` - DSL module for including in classes
|
13
|
+
- `registry.rbs` - Global circuit registry
|
14
|
+
- `console.rbs` - Interactive console
|
15
|
+
- `types.rbs` - Common type aliases
|
16
|
+
- `interfaces.rbs` - Interface definitions
|
17
|
+
|
18
|
+
## Usage
|
19
|
+
|
20
|
+
To use these type signatures in your project:
|
21
|
+
|
22
|
+
1. Add to your `Steepfile`:
|
23
|
+
```ruby
|
24
|
+
target :app do
|
25
|
+
signature "sig"
|
26
|
+
check "lib"
|
27
|
+
|
28
|
+
library "breaker_machines"
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
2. Or with RBS directly:
|
33
|
+
```bash
|
34
|
+
rbs validate
|
35
|
+
```
|
36
|
+
|
37
|
+
## Type Checking Examples
|
38
|
+
|
39
|
+
### Basic Circuit Usage
|
40
|
+
```ruby
|
41
|
+
circuit = BreakerMachines::Circuit.new("api",
|
42
|
+
failure_threshold: 5,
|
43
|
+
reset_timeout: 30
|
44
|
+
)
|
45
|
+
|
46
|
+
result = circuit.call { api.fetch_data }
|
47
|
+
```
|
48
|
+
|
49
|
+
### DSL Usage
|
50
|
+
```ruby
|
51
|
+
class MyService
|
52
|
+
include BreakerMachines::DSL
|
53
|
+
|
54
|
+
circuit :database do
|
55
|
+
threshold failures: 10, within: 60
|
56
|
+
reset_after 120
|
57
|
+
fallback { [] }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
## Key Types
|
63
|
+
|
64
|
+
- `circuit_state` - `:open | :closed | :half_open`
|
65
|
+
- `storage_backend` - `:memory | :bucket_memory | :null | :redis`
|
66
|
+
- `circuit_options` - Configuration hash for circuits
|
67
|
+
- `event_record` - Structure for logged events
|
68
|
+
|
69
|
+
## Interfaces
|
70
|
+
|
71
|
+
The type signatures define several interfaces:
|
72
|
+
- `_StorageBackend` - For custom storage implementations
|
73
|
+
- `_MetricsRecorder` - For custom metrics recording
|
74
|
+
- `_CircuitLike` - For circuit-compatible objects
|
data/sig/all.rbs
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# Main entry point for BreakerMachines RBS types
|
2
|
+
#
|
3
|
+
# This file provides a complete type definition for the BreakerMachines gem,
|
4
|
+
# a Ruby implementation of the Circuit Breaker pattern.
|
5
|
+
#
|
6
|
+
# Usage:
|
7
|
+
# class MyService
|
8
|
+
# include BreakerMachines::DSL
|
9
|
+
#
|
10
|
+
# circuit :api_call do
|
11
|
+
# threshold failures: 5, within: 60
|
12
|
+
# reset_after 30
|
13
|
+
# fallback { |error| { error: "Service unavailable" } }
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# def fetch_data
|
17
|
+
# circuit(:api_call).call do
|
18
|
+
# # Your API call here
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
|
23
|
+
# Import all type definitions
|
24
|
+
use BreakerMachines::*
|
25
|
+
use BreakerMachines::Storage::*
|