breaker_machines 0.19.0-aarch64-linux → 0.21.0-aarch64-linux
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 +17 -0
- data/lib/breaker_machines/circuit/admission.rb +18 -4
- data/lib/breaker_machines/circuit/base.rb +42 -3
- data/lib/breaker_machines/circuit/native.rb +2 -0
- data/lib/breaker_machines/circuit/protection.rb +13 -0
- data/lib/breaker_machines/circuit/proxy.rb +27 -0
- data/lib/breaker_machines/circuit/state_callbacks.rb +3 -3
- data/lib/breaker_machines/storage/base.rb +2 -0
- data/lib/breaker_machines/storage/cache.rb +66 -17
- data/lib/breaker_machines/version.rb +1 -1
- data/lib/breaker_machines.rb +29 -0
- data/lib/breaker_machines_native/breaker_machines_native.so +0 -0
- metadata +10 -20
- data/sig/README.md +0 -74
- data/sig/all.rbs +0 -25
- data/sig/breaker_machines/circuit.rbs +0 -179
- data/sig/breaker_machines/console.rbs +0 -32
- data/sig/breaker_machines/dsl.rbs +0 -50
- data/sig/breaker_machines/errors.rbs +0 -24
- data/sig/breaker_machines/interfaces.rbs +0 -46
- data/sig/breaker_machines/registry.rbs +0 -30
- data/sig/breaker_machines/storage.rbs +0 -65
- data/sig/breaker_machines/types.rbs +0 -97
- data/sig/breaker_machines.rbs +0 -42
- data/sig/manifest.yaml +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7c0e53da65b311a1036acd3348580b0f06d25c24213da2bcef11ead9d17842c0
|
|
4
|
+
data.tar.gz: 62ca980cc3bd0dc4ae1a052b716f783a5a8091ddfe2491539a2cdb73d3ee26dc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8fd98bf9f3816b532669792a04af9409a345f78ea33c8e1a436d235c76cb0eb584584fa54322b7e9c8d66da23c87a06cffd770d43dbebb253abc2f9129a0ca3c
|
|
7
|
+
data.tar.gz: 9377c5397b5d363f5b8dd335a0b08565ebdb0a6e16e6115c900a252ec1e72b4a3123505774df6e0f76773e180a0890eb33e15aa1e22df277c0daed977ea3de55
|
data/README.md
CHANGED
|
@@ -28,6 +28,23 @@ class PaymentService
|
|
|
28
28
|
end
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
### Protect an existing client
|
|
32
|
+
|
|
33
|
+
Use `protect` to route selected public methods through an existing circuit:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
circuit = BreakerMachines::Circuit.new(:teleporter, failure_threshold: 3)
|
|
37
|
+
protected_client = circuit.protect(teleporter, methods: %i[beam_up beam_down])
|
|
38
|
+
|
|
39
|
+
protected_client.beam_up('Schrödinger', destination: :sickbay)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The wrapper forwards arguments, keywords, blocks, and results through `circuit.call`, including its configured error handling and fallback. Other methods pass through without consulting the circuit. `methods:` must be nonempty and name public methods the client responds to; otherwise `protect` raises `ArgumentError`.
|
|
43
|
+
|
|
44
|
+
The original client stays unchanged. Each call through the wrapper counts as one operation, including any internal method calls. Returned objects stay unchanged too: later calls on a returned client or lazy stream need their own protection. Wrap the object that performs the external operation.
|
|
45
|
+
|
|
46
|
+
Available on Ruby and native circuits, including circuits obtained through the DSL. Multiple wrappers can share the same circuit.
|
|
47
|
+
|
|
31
48
|
## A Message to the Resistance
|
|
32
49
|
|
|
33
50
|
So AI took your job while you were waiting for Fireship to drop the next JavaScript framework?
|
|
@@ -18,14 +18,28 @@ module BreakerMachines
|
|
|
18
18
|
|
|
19
19
|
private
|
|
20
20
|
|
|
21
|
+
# Runs on every call: the write lock is taken only to apply a changed
|
|
22
|
+
# shared status or to start recovery.
|
|
21
23
|
def attempt_recovery_if_ready
|
|
22
|
-
|
|
24
|
+
changed = stored_status_changed?
|
|
25
|
+
return unless changed || (open? && reset_timeout_elapsed?)
|
|
23
26
|
|
|
24
27
|
@mutex.with_write_lock do
|
|
28
|
+
restore_status_from_storage if changed
|
|
25
29
|
attempt_recovery if open? && reset_timeout_elapsed?
|
|
26
30
|
end
|
|
27
31
|
end
|
|
28
32
|
|
|
33
|
+
# Reads the shared status before taking the write lock, so storage I/O
|
|
34
|
+
# happens under it only when there is a change to apply.
|
|
35
|
+
def with_refreshed_status
|
|
36
|
+
changed = stored_status_changed?
|
|
37
|
+
@mutex.with_write_lock do
|
|
38
|
+
restore_status_from_storage if changed
|
|
39
|
+
yield
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
29
43
|
def admit_call
|
|
30
44
|
@mutex.with_read_lock do
|
|
31
45
|
case status_name
|
|
@@ -55,7 +69,7 @@ module BreakerMachines
|
|
|
55
69
|
def handle_success(admission)
|
|
56
70
|
return unless admission.half_open?
|
|
57
71
|
|
|
58
|
-
|
|
72
|
+
with_refreshed_status do
|
|
59
73
|
return unless admission_current?(admission) && half_open?
|
|
60
74
|
|
|
61
75
|
successful_attempts = @half_open_successes.increment
|
|
@@ -68,7 +82,7 @@ module BreakerMachines
|
|
|
68
82
|
end
|
|
69
83
|
|
|
70
84
|
def handle_failure(admission)
|
|
71
|
-
|
|
85
|
+
with_refreshed_status do
|
|
72
86
|
return unless admission_current?(admission)
|
|
73
87
|
|
|
74
88
|
if admission.closed? && closed? && failure_threshold_exceeded?
|
|
@@ -84,7 +98,7 @@ module BreakerMachines
|
|
|
84
98
|
def release_abandoned_admission(admission)
|
|
85
99
|
return unless admission.half_open?
|
|
86
100
|
|
|
87
|
-
|
|
101
|
+
with_refreshed_status do
|
|
88
102
|
next unless admission_current?(admission) && half_open?
|
|
89
103
|
next unless @half_open_attempts.value.positive?
|
|
90
104
|
|
|
@@ -12,6 +12,7 @@ module BreakerMachines
|
|
|
12
12
|
include Circuit::Configuration
|
|
13
13
|
include Circuit::Admission
|
|
14
14
|
include Circuit::Execution
|
|
15
|
+
include Circuit::Protection
|
|
15
16
|
include Circuit::HedgedExecution
|
|
16
17
|
include Circuit::Introspection
|
|
17
18
|
include Circuit::Callbacks
|
|
@@ -53,12 +54,50 @@ module BreakerMachines
|
|
|
53
54
|
@semaphore = (Concurrent::Semaphore.new(@config[:max_concurrent]) if @config[:max_concurrent])
|
|
54
55
|
end
|
|
55
56
|
|
|
57
|
+
# A round trip through epoch time is off by up to ~0.12µs (float64
|
|
58
|
+
# spacing near today's epoch); any larger difference is another opening.
|
|
59
|
+
OPENED_AT_TOLERANCE = 1e-6
|
|
60
|
+
|
|
61
|
+
# Reads the shared status without taking the lock. True when it differs
|
|
62
|
+
# from the last status this instance stored or observed.
|
|
63
|
+
def stored_status_changed?
|
|
64
|
+
stored_status = @storage&.get_status(@name)
|
|
65
|
+
!stored_status.nil? && !same_stored_status?(stored_status, @stored_status)
|
|
66
|
+
end
|
|
67
|
+
|
|
56
68
|
def restore_status_from_storage
|
|
57
|
-
stored_status = @storage
|
|
58
|
-
return
|
|
69
|
+
stored_status = @storage&.get_status(@name)
|
|
70
|
+
return if stored_status.nil? || same_stored_status?(stored_status, @stored_status)
|
|
59
71
|
|
|
72
|
+
@stored_status = stored_status
|
|
60
73
|
self.status = stored_status.status.to_s
|
|
61
|
-
@opened_at.value = stored_status.opened_at
|
|
74
|
+
@opened_at.value = stored_status.opened_at
|
|
75
|
+
@state_epoch.increment
|
|
76
|
+
@half_open_attempts.value = 0
|
|
77
|
+
@half_open_successes.value = 0
|
|
78
|
+
return unless closed?
|
|
79
|
+
|
|
80
|
+
@last_error.value = nil
|
|
81
|
+
@last_failure_at.value = nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def same_stored_status?(stored_status, known_status)
|
|
85
|
+
return false unless known_status && stored_status.status == known_status.status
|
|
86
|
+
|
|
87
|
+
stored_at = stored_status.opened_at
|
|
88
|
+
known_at = known_status.opened_at
|
|
89
|
+
return stored_at == known_at if stored_at.nil? || known_at.nil?
|
|
90
|
+
|
|
91
|
+
(stored_at - known_at).abs <= OPENED_AT_TOLERANCE
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# When the storage reports a failed write (false), keep the last status
|
|
95
|
+
# observed there: the stale entry then reads as unchanged and cannot
|
|
96
|
+
# undo this transition.
|
|
97
|
+
def persist_status(status, opened_at = nil)
|
|
98
|
+
return if @storage.nil? || @storage.set_status(@name, status, opened_at) == false
|
|
99
|
+
|
|
100
|
+
@stored_status = BreakerMachines::Status.new(status: status, opened_at: opened_at)
|
|
62
101
|
end
|
|
63
102
|
end
|
|
64
103
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BreakerMachines
|
|
4
|
+
class Circuit
|
|
5
|
+
# Protection adds `protect`, which wraps selected public methods of an
|
|
6
|
+
# object so that each call runs through this circuit's `call`.
|
|
7
|
+
module Protection
|
|
8
|
+
def protect(object, methods:)
|
|
9
|
+
Proxy.new(object, circuit: self, methods:)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'delegate'
|
|
4
|
+
|
|
5
|
+
module BreakerMachines
|
|
6
|
+
class Circuit
|
|
7
|
+
# Proxy delegates to a wrapped object and routes the configured methods
|
|
8
|
+
# through a circuit. Unlisted methods pass straight through.
|
|
9
|
+
class Proxy < SimpleDelegator
|
|
10
|
+
def initialize(object, circuit:, methods:)
|
|
11
|
+
raise ArgumentError, 'methods must not be empty' if methods.empty?
|
|
12
|
+
|
|
13
|
+
methods.each do |name|
|
|
14
|
+
raise ArgumentError, "Unknown public method: #{name}" unless object.respond_to?(name)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
super(object)
|
|
18
|
+
|
|
19
|
+
methods.each do |name|
|
|
20
|
+
define_singleton_method(name) do |*args, **kwargs, &block|
|
|
21
|
+
circuit.call { __getobj__.public_send(name, *args, **kwargs, &block) }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -11,7 +11,7 @@ module BreakerMachines
|
|
|
11
11
|
def on_circuit_open
|
|
12
12
|
@state_epoch.increment
|
|
13
13
|
@opened_at.value = BreakerMachines.monotonic_time
|
|
14
|
-
|
|
14
|
+
persist_status(:open, @opened_at.value)
|
|
15
15
|
if @storage.respond_to?(:record_event_with_details)
|
|
16
16
|
@storage.record_event_with_details(@name, :state_change, 0,
|
|
17
17
|
new_state: :open)
|
|
@@ -25,7 +25,7 @@ module BreakerMachines
|
|
|
25
25
|
@opened_at.value = nil
|
|
26
26
|
@last_error.value = nil
|
|
27
27
|
@last_failure_at.value = nil
|
|
28
|
-
|
|
28
|
+
persist_status(:closed)
|
|
29
29
|
if @storage.respond_to?(:record_event_with_details)
|
|
30
30
|
@storage.record_event_with_details(@name, :state_change, 0,
|
|
31
31
|
new_state: :closed)
|
|
@@ -38,7 +38,7 @@ module BreakerMachines
|
|
|
38
38
|
@state_epoch.increment
|
|
39
39
|
@half_open_attempts.value = 0
|
|
40
40
|
@half_open_successes.value = 0
|
|
41
|
-
|
|
41
|
+
persist_status(:half_open, @opened_at.value)
|
|
42
42
|
if @storage.respond_to?(:record_event_with_details)
|
|
43
43
|
@storage.record_event_with_details(@name, :state_change, 0,
|
|
44
44
|
new_state: :half_open)
|
|
@@ -13,6 +13,8 @@ module BreakerMachines
|
|
|
13
13
|
raise NotImplementedError
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
# opened_at is a BreakerMachines.monotonic_time value. Return false when
|
|
17
|
+
# the write did not happen; any other value means it was stored.
|
|
16
18
|
def set_status(circuit_name, status, opened_at = nil)
|
|
17
19
|
raise NotImplementedError
|
|
18
20
|
end
|
|
@@ -10,6 +10,12 @@ module BreakerMachines
|
|
|
10
10
|
@cache = cache_store
|
|
11
11
|
@prefix = options[:prefix] || 'breaker_machines'
|
|
12
12
|
@expires_in = options[:expires_in] || 300 # 5 minutes default
|
|
13
|
+
# Counter resolution in seconds. Each bucket is its own cache entry, so
|
|
14
|
+
# stores that keep expired entries around (Solid Cache) want coarser buckets.
|
|
15
|
+
@bucket_seconds = Integer(options[:bucket_seconds] || 1)
|
|
16
|
+
raise ArgumentError, 'bucket_seconds must be positive' unless @bucket_seconds.positive?
|
|
17
|
+
|
|
18
|
+
@warned_windows = Concurrent::Set.new
|
|
13
19
|
end
|
|
14
20
|
|
|
15
21
|
def get_status(circuit_name)
|
|
@@ -18,7 +24,7 @@ module BreakerMachines
|
|
|
18
24
|
|
|
19
25
|
BreakerMachines::Status.new(
|
|
20
26
|
status: data[:status].to_sym,
|
|
21
|
-
opened_at: data
|
|
27
|
+
opened_at: local_opened_at(data)
|
|
22
28
|
)
|
|
23
29
|
end
|
|
24
30
|
|
|
@@ -28,6 +34,7 @@ module BreakerMachines
|
|
|
28
34
|
{
|
|
29
35
|
status: status,
|
|
30
36
|
opened_at: opened_at,
|
|
37
|
+
opened_at_epoch: opened_at && BreakerMachines.monotonic_to_epoch(opened_at),
|
|
31
38
|
updated_at: monotonic_time
|
|
32
39
|
},
|
|
33
40
|
expires_in: @expires_in
|
|
@@ -55,6 +62,12 @@ module BreakerMachines
|
|
|
55
62
|
@cache.delete(success_key(circuit_name))
|
|
56
63
|
@cache.delete(failure_key(circuit_name))
|
|
57
64
|
@cache.delete(events_key(circuit_name))
|
|
65
|
+
return unless @cache.respond_to?(:increment)
|
|
66
|
+
|
|
67
|
+
keys = [success_key(circuit_name), failure_key(circuit_name)].flat_map do |key|
|
|
68
|
+
bucket_keys(key, bucket_count(@expires_in) + 1)
|
|
69
|
+
end
|
|
70
|
+
delete_counters(keys)
|
|
58
71
|
end
|
|
59
72
|
|
|
60
73
|
def clear_all
|
|
@@ -103,10 +116,20 @@ module BreakerMachines
|
|
|
103
116
|
|
|
104
117
|
private
|
|
105
118
|
|
|
119
|
+
# Hosts do not share a monotonic clock origin, so other hosts read the
|
|
120
|
+
# epoch copy. Entries written before it existed carry only the writer's
|
|
121
|
+
# monotonic time.
|
|
122
|
+
def local_opened_at(data)
|
|
123
|
+
epoch = data[:opened_at_epoch]
|
|
124
|
+
epoch ? BreakerMachines.epoch_to_monotonic(epoch) : data[:opened_at]
|
|
125
|
+
end
|
|
126
|
+
|
|
106
127
|
def increment_counter(key)
|
|
107
128
|
# Use increment if available, otherwise fetch-and-update
|
|
108
129
|
if @cache.respond_to?(:increment)
|
|
109
|
-
|
|
130
|
+
# Expiry counts from the first increment, so a bucket must outlive
|
|
131
|
+
# the retention period by its own length.
|
|
132
|
+
@cache.increment("#{key}:#{current_bucket}", 1, expires_in: @expires_in + @bucket_seconds)
|
|
110
133
|
else
|
|
111
134
|
# Fallback for caches without atomic increment
|
|
112
135
|
current = @cache.fetch(key) { {} }
|
|
@@ -117,32 +140,58 @@ module BreakerMachines
|
|
|
117
140
|
end
|
|
118
141
|
|
|
119
142
|
def get_window_count(key, window_seconds)
|
|
143
|
+
warn_window_exceeds_retention(window_seconds)
|
|
144
|
+
count = bucket_count([window_seconds, @expires_in].min)
|
|
145
|
+
|
|
120
146
|
if @cache.respond_to?(:increment)
|
|
121
|
-
|
|
122
|
-
# Would need to implement bucketing similar to fallback
|
|
123
|
-
@cache.read(key) || 0
|
|
147
|
+
read_counters(bucket_keys(key, count)).sum(&:to_i)
|
|
124
148
|
else
|
|
125
|
-
# Bucket-based counting for accurate windows
|
|
126
149
|
buckets = @cache.read(key) || {}
|
|
127
|
-
|
|
150
|
+
now = current_bucket
|
|
151
|
+
count.times.sum { |offset| buckets[now - offset] || 0 }
|
|
152
|
+
end
|
|
153
|
+
end
|
|
128
154
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
end
|
|
155
|
+
# Counters live for expires_in, so a longer window silently undercounts.
|
|
156
|
+
def warn_window_exceeds_retention(window_seconds)
|
|
157
|
+
return if window_seconds <= @expires_in
|
|
158
|
+
return unless @warned_windows.add?(window_seconds)
|
|
134
159
|
|
|
135
|
-
|
|
136
|
-
|
|
160
|
+
BreakerMachines.logger&.warn(
|
|
161
|
+
"[BreakerMachines] Cache storage keeps counts for #{@expires_in}s, so a #{window_seconds}s " \
|
|
162
|
+
"window only sees the last #{@expires_in}s. Set expires_in to at least the window."
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# read_multi is a single round trip; stores without it read each bucket.
|
|
167
|
+
def read_counters(keys)
|
|
168
|
+
return @cache.read_multi(*keys, raw: true).values if @cache.respond_to?(:read_multi)
|
|
169
|
+
|
|
170
|
+
keys.filter_map { |key| @cache.read(key) }
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def delete_counters(keys)
|
|
174
|
+
return @cache.delete_multi(keys) if @cache.respond_to?(:delete_multi)
|
|
175
|
+
|
|
176
|
+
keys.each { |key| @cache.delete(key) }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def bucket_count(seconds)
|
|
180
|
+
(seconds.to_f / @bucket_seconds).ceil
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def bucket_keys(key, count)
|
|
184
|
+
now = current_bucket
|
|
185
|
+
Array.new(count) { |offset| "#{key}:#{now - offset}" }
|
|
137
186
|
end
|
|
138
187
|
|
|
139
188
|
def prune_old_buckets(buckets)
|
|
140
|
-
cutoff = current_bucket -
|
|
141
|
-
buckets.delete_if { |
|
|
189
|
+
cutoff = current_bucket - bucket_count(@expires_in)
|
|
190
|
+
buckets.delete_if { |bucket, _| bucket < cutoff }
|
|
142
191
|
end
|
|
143
192
|
|
|
144
193
|
def current_bucket
|
|
145
|
-
Time.now.to_i
|
|
194
|
+
Time.now.to_i / @bucket_seconds
|
|
146
195
|
end
|
|
147
196
|
|
|
148
197
|
def status_key(circuit_name)
|
data/lib/breaker_machines.rb
CHANGED
|
@@ -184,6 +184,35 @@ module BreakerMachines
|
|
|
184
184
|
def monotonic_time
|
|
185
185
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
186
186
|
end
|
|
187
|
+
|
|
188
|
+
# Converts a {monotonic_time} value to Unix epoch seconds. Each host's
|
|
189
|
+
# monotonic clock has its own origin, so shared storage must persist
|
|
190
|
+
# timestamps in epoch time for other hosts to read them.
|
|
191
|
+
#
|
|
192
|
+
# @param monotonic [Float] a value from {monotonic_time}
|
|
193
|
+
# @return [Float] Unix epoch seconds
|
|
194
|
+
def monotonic_to_epoch(monotonic)
|
|
195
|
+
monotonic + clock_offset
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Converts Unix epoch seconds to this host's {monotonic_time}.
|
|
199
|
+
#
|
|
200
|
+
# @param epoch [Float] Unix epoch seconds
|
|
201
|
+
# @return [Float] the matching {monotonic_time} value on this host
|
|
202
|
+
def epoch_to_monotonic(epoch)
|
|
203
|
+
epoch - clock_offset
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
private
|
|
207
|
+
|
|
208
|
+
# Wall clock minus monotonic clock. Memoized so that converting the same
|
|
209
|
+
# timestamp twice gives the same result; re-measured only when the wall
|
|
210
|
+
# clock has stepped by more than a second.
|
|
211
|
+
def clock_offset
|
|
212
|
+
offset = Process.clock_gettime(Process::CLOCK_REALTIME) - monotonic_time
|
|
213
|
+
@clock_offset = offset if @clock_offset.nil? || (offset - @clock_offset).abs > 1.0
|
|
214
|
+
@clock_offset
|
|
215
|
+
end
|
|
187
216
|
end
|
|
188
217
|
|
|
189
218
|
# Set up notifications on first use
|
|
Binary file
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: breaker_machines
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.21.0
|
|
5
5
|
platform: aarch64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Abdelkader Boudih
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -16,28 +16,28 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '8.
|
|
19
|
+
version: '8.1'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - ">="
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '8.
|
|
26
|
+
version: '8.1'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: chrono_machines
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
31
|
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '0.
|
|
33
|
+
version: '0.10'
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '0.
|
|
40
|
+
version: '0.10'
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: concurrent-ruby
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -86,14 +86,14 @@ dependencies:
|
|
|
86
86
|
requirements:
|
|
87
87
|
- - "~>"
|
|
88
88
|
- !ruby/object:Gem::Version
|
|
89
|
-
version: '
|
|
89
|
+
version: '6.0'
|
|
90
90
|
type: :development
|
|
91
91
|
prerelease: false
|
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
|
93
93
|
requirements:
|
|
94
94
|
- - "~>"
|
|
95
95
|
- !ruby/object:Gem::Version
|
|
96
|
-
version: '
|
|
96
|
+
version: '6.0'
|
|
97
97
|
- !ruby/object:Gem::Dependency
|
|
98
98
|
name: rake
|
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -151,6 +151,8 @@ files:
|
|
|
151
151
|
- lib/breaker_machines/circuit/hedged_execution.rb
|
|
152
152
|
- lib/breaker_machines/circuit/introspection.rb
|
|
153
153
|
- lib/breaker_machines/circuit/native.rb
|
|
154
|
+
- lib/breaker_machines/circuit/protection.rb
|
|
155
|
+
- lib/breaker_machines/circuit/proxy.rb
|
|
154
156
|
- lib/breaker_machines/circuit/state_callbacks.rb
|
|
155
157
|
- lib/breaker_machines/circuit/state_machine_definition.rb
|
|
156
158
|
- lib/breaker_machines/circuit/state_management.rb
|
|
@@ -180,18 +182,6 @@ files:
|
|
|
180
182
|
- lib/breaker_machines/types.rb
|
|
181
183
|
- lib/breaker_machines/version.rb
|
|
182
184
|
- lib/breaker_machines_native/breaker_machines_native.so
|
|
183
|
-
- sig/README.md
|
|
184
|
-
- sig/all.rbs
|
|
185
|
-
- sig/breaker_machines.rbs
|
|
186
|
-
- sig/breaker_machines/circuit.rbs
|
|
187
|
-
- sig/breaker_machines/console.rbs
|
|
188
|
-
- sig/breaker_machines/dsl.rbs
|
|
189
|
-
- sig/breaker_machines/errors.rbs
|
|
190
|
-
- sig/breaker_machines/interfaces.rbs
|
|
191
|
-
- sig/breaker_machines/registry.rbs
|
|
192
|
-
- sig/breaker_machines/storage.rbs
|
|
193
|
-
- sig/breaker_machines/types.rbs
|
|
194
|
-
- sig/manifest.yaml
|
|
195
185
|
homepage: https://github.com/seuros/breaker_machines
|
|
196
186
|
licenses:
|
|
197
187
|
- MIT
|
data/sig/README.md
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
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::*
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
module BreakerMachines
|
|
2
|
-
class Circuit
|
|
3
|
-
include StateManagement
|
|
4
|
-
include Configuration
|
|
5
|
-
include Admission
|
|
6
|
-
include Execution
|
|
7
|
-
include Introspection
|
|
8
|
-
include Callbacks
|
|
9
|
-
|
|
10
|
-
# Instance variables from Configuration module
|
|
11
|
-
@name: String
|
|
12
|
-
@config: Hash[Symbol, untyped]
|
|
13
|
-
@storage: _StorageBackend?
|
|
14
|
-
@metrics: _MetricsRecorder?
|
|
15
|
-
@opened_at: Concurrent::AtomicReference[Float?]
|
|
16
|
-
@half_open_attempts: Concurrent::AtomicFixnum
|
|
17
|
-
@half_open_successes: Concurrent::AtomicFixnum
|
|
18
|
-
@state_epoch: Concurrent::AtomicFixnum
|
|
19
|
-
@mutex: Concurrent::ReentrantReadWriteLock
|
|
20
|
-
@last_failure_at: Concurrent::AtomicReference[Float?]
|
|
21
|
-
@last_error: Concurrent::AtomicReference[StandardError?]
|
|
22
|
-
|
|
23
|
-
# State machine status (from StateManagement)
|
|
24
|
-
attr_reader status: (:open | :closed | :half_open)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
module Admission
|
|
28
|
-
class Call
|
|
29
|
-
attr_reader state: (:closed | :half_open)
|
|
30
|
-
attr_reader state_epoch: Integer
|
|
31
|
-
|
|
32
|
-
def self.new: (state: (:closed | :half_open), state_epoch: Integer) -> Call
|
|
33
|
-
def closed?: () -> bool
|
|
34
|
-
def half_open?: () -> bool
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
private
|
|
38
|
-
|
|
39
|
-
def attempt_recovery_if_ready: () -> void
|
|
40
|
-
def admit_call: () -> Call?
|
|
41
|
-
def admit_half_open_call: () -> Call?
|
|
42
|
-
def admission_current?: (Call admission) -> bool
|
|
43
|
-
def handle_success: (Call admission) -> void
|
|
44
|
-
def handle_failure: (Call admission) -> void
|
|
45
|
-
def release_abandoned_admission: (Call admission) -> void
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
module StateManagement
|
|
49
|
-
interface _StateManagementState
|
|
50
|
-
def status: () -> (:open | :closed | :half_open)
|
|
51
|
-
def status=: (:open | :closed | :half_open value) -> void
|
|
52
|
-
def status_name: () -> (:open | :closed | :half_open)
|
|
53
|
-
def open?: () -> bool
|
|
54
|
-
def closed?: () -> bool
|
|
55
|
-
def half_open?: () -> bool
|
|
56
|
-
def trip: () -> bool
|
|
57
|
-
def attempt_recovery: () -> bool
|
|
58
|
-
def reset: () -> bool
|
|
59
|
-
def force_open: () -> bool
|
|
60
|
-
def force_close: () -> bool
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def self.included: (singleton(Circuit) base) -> void
|
|
64
|
-
|
|
65
|
-
private
|
|
66
|
-
|
|
67
|
-
def on_circuit_open: () -> void
|
|
68
|
-
def on_circuit_close: () -> void
|
|
69
|
-
def on_circuit_half_open: () -> void
|
|
70
|
-
def restore_status_from_storage: () -> void
|
|
71
|
-
def reset_timeout_elapsed?: () -> bool
|
|
72
|
-
def monotonic_time: () -> Float
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
module Configuration
|
|
76
|
-
extend ActiveSupport::Concern
|
|
77
|
-
|
|
78
|
-
interface _ConfigurationMethods
|
|
79
|
-
def name: () -> String
|
|
80
|
-
def config: () -> Hash[Symbol, untyped]
|
|
81
|
-
def opened_at: () -> Concurrent::AtomicReference[Float?]
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
type storage_type = Storage::Base | Storage::Memory | Storage::BucketMemory | Storage::Null
|
|
85
|
-
type metrics_type = untyped
|
|
86
|
-
type callback_type = Proc | nil
|
|
87
|
-
type fallback_type = Proc | Array[Proc | untyped] | untyped
|
|
88
|
-
|
|
89
|
-
type circuit_config = {
|
|
90
|
-
failure_threshold: Integer,
|
|
91
|
-
failure_window: Integer,
|
|
92
|
-
success_threshold: Integer,
|
|
93
|
-
timeout: Integer?,
|
|
94
|
-
reset_timeout: Integer,
|
|
95
|
-
reset_timeout_jitter: Float,
|
|
96
|
-
half_open_calls: Integer,
|
|
97
|
-
storage: storage_type?,
|
|
98
|
-
metrics: metrics_type?,
|
|
99
|
-
fallback: fallback_type?,
|
|
100
|
-
on_open: callback_type,
|
|
101
|
-
on_close: callback_type,
|
|
102
|
-
on_half_open: callback_type,
|
|
103
|
-
on_reject: callback_type,
|
|
104
|
-
exceptions: Array[Class],
|
|
105
|
-
owner: untyped
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
def initialize: (String name, ?circuit_config options) -> void
|
|
109
|
-
|
|
110
|
-
private
|
|
111
|
-
|
|
112
|
-
def default_config: () -> circuit_config
|
|
113
|
-
def create_default_storage: () -> storage_type
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
module Execution
|
|
117
|
-
def call: [T] () { () -> T } -> untyped
|
|
118
|
-
def wrap: [T] () { () -> T } -> untyped
|
|
119
|
-
|
|
120
|
-
private
|
|
121
|
-
|
|
122
|
-
def execute_with_state_check: [T] () { () -> T } -> untyped
|
|
123
|
-
def execute_call: [T] (Admission::Call admission) { () -> T } -> untyped
|
|
124
|
-
def execute_call_sync: [T] (Admission::Call admission) { () -> T } -> untyped
|
|
125
|
-
def execute_sync_operation: [T] () { () -> T } -> T
|
|
126
|
-
def warn_about_sync_timeout: () -> void
|
|
127
|
-
def complete_call_success: (Admission::Call admission, Float start_time) -> void
|
|
128
|
-
def complete_call_failure: (Admission::Call admission, Float start_time, Exception error) -> void
|
|
129
|
-
def reject_call: () -> untyped
|
|
130
|
-
def reject_call_bulkhead: () -> untyped
|
|
131
|
-
def failure_threshold_exceeded?: () -> bool
|
|
132
|
-
def success_threshold_reached?: () -> bool
|
|
133
|
-
def record_success: (Float duration) -> void
|
|
134
|
-
def record_failure: (Float duration, ?StandardError? error) -> void
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
module Introspection
|
|
138
|
-
type stats_hash = {
|
|
139
|
-
state: (:open | :closed | :half_open),
|
|
140
|
-
failure_count: Integer,
|
|
141
|
-
success_count: Integer,
|
|
142
|
-
last_failure_at: Float?,
|
|
143
|
-
opened_at: Float?,
|
|
144
|
-
half_open_attempts: Integer,
|
|
145
|
-
half_open_successes: Integer
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
type error_info = {
|
|
149
|
-
class: String,
|
|
150
|
-
message: String,
|
|
151
|
-
occurred_at: Float
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
type circuit_hash = {
|
|
155
|
-
name: String,
|
|
156
|
-
state: (:open | :closed | :half_open),
|
|
157
|
-
stats: stats_hash,
|
|
158
|
-
config: Hash[Symbol, untyped],
|
|
159
|
-
event_log: Array[untyped],
|
|
160
|
-
last_error: error_info?
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
def stats: () -> stats_hash
|
|
164
|
-
def configuration: () -> Hash[Symbol, untyped]
|
|
165
|
-
def event_log: (?limit: Integer) -> Array[untyped]?
|
|
166
|
-
def last_error: () -> StandardError?
|
|
167
|
-
def to_h: () -> circuit_hash
|
|
168
|
-
def summary: () -> String
|
|
169
|
-
def last_error_info: () -> error_info?
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
module Callbacks
|
|
173
|
-
private
|
|
174
|
-
|
|
175
|
-
def invoke_callback: (Symbol callback_name) -> void
|
|
176
|
-
def invoke_fallback: (StandardError error) -> untyped
|
|
177
|
-
def invoke_single_fallback: (Proc | untyped fallback, StandardError error) -> untyped
|
|
178
|
-
end
|
|
179
|
-
end
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
module BreakerMachines
|
|
2
|
-
class Console
|
|
3
|
-
@running: bool
|
|
4
|
-
|
|
5
|
-
def self.start: () -> void
|
|
6
|
-
|
|
7
|
-
def initialize: () -> void
|
|
8
|
-
def run: () -> void
|
|
9
|
-
|
|
10
|
-
private
|
|
11
|
-
|
|
12
|
-
def print_header: () -> void
|
|
13
|
-
def print_help: () -> void
|
|
14
|
-
def print_prompt: () -> void
|
|
15
|
-
def process_command: (String command) -> void
|
|
16
|
-
|
|
17
|
-
# Command methods
|
|
18
|
-
def list_circuits: () -> void
|
|
19
|
-
def show_stats: () -> void
|
|
20
|
-
def show_circuit: (String? name) -> void
|
|
21
|
-
def show_events: (String? name, Integer limit) -> void
|
|
22
|
-
def reset_circuit: (String? name) -> void
|
|
23
|
-
def force_open_circuit: (String? name) -> void
|
|
24
|
-
def force_close_circuit: (String? name) -> void
|
|
25
|
-
def generate_report: () -> void
|
|
26
|
-
def cleanup_registry: () -> void
|
|
27
|
-
|
|
28
|
-
# Utility methods
|
|
29
|
-
def colorize_state: (Symbol state) -> String
|
|
30
|
-
def colorize_event_type: (Symbol type) -> String
|
|
31
|
-
end
|
|
32
|
-
end
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
module BreakerMachines
|
|
2
|
-
module DSL
|
|
3
|
-
extend ActiveSupport::Concern
|
|
4
|
-
|
|
5
|
-
@instance_registries: Concurrent::Map[Class, Concurrent::Array[WeakRef[untyped]]]
|
|
6
|
-
|
|
7
|
-
module ClassMethods
|
|
8
|
-
@circuits: Hash[Symbol, Hash[Symbol, untyped]]
|
|
9
|
-
|
|
10
|
-
def instance_registry: () -> Concurrent::Array[WeakRef[untyped]]
|
|
11
|
-
def cleanup_instance_registry: () -> void
|
|
12
|
-
def circuit: (Symbol name) ?{ (CircuitBuilder) -> void } -> Hash[Symbol, untyped]
|
|
13
|
-
def circuits: () -> Hash[Symbol, Hash[Symbol, untyped]]
|
|
14
|
-
def circuit_definitions: () -> Hash[Symbol, Hash[Symbol, untyped]]
|
|
15
|
-
def reset_all_circuits: () -> void
|
|
16
|
-
def circuit_stats: () -> Hash[Symbol, { total: Integer, by_state: Hash[Symbol, Integer] }]
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def circuit: (Symbol name) -> Circuit
|
|
20
|
-
def circuit_instances: () -> Hash[Symbol, Circuit]
|
|
21
|
-
def circuits_summary: () -> Hash[Symbol, String]
|
|
22
|
-
def circuits_report: () -> Hash[Symbol, untyped]
|
|
23
|
-
def reset_all_circuits: () -> void
|
|
24
|
-
|
|
25
|
-
class CircuitBuilder
|
|
26
|
-
attr_reader config: Hash[Symbol, untyped]
|
|
27
|
-
|
|
28
|
-
def initialize: () -> void
|
|
29
|
-
|
|
30
|
-
# Configuration methods
|
|
31
|
-
def threshold: (?failures: Integer?, ?within: Integer, ?successes: Integer?) -> void
|
|
32
|
-
def reset_after: (Integer duration, ?jitter: Float?) -> void
|
|
33
|
-
def timeout: (Integer duration) -> void
|
|
34
|
-
def half_open_requests: (Integer count) -> void
|
|
35
|
-
def storage: ((:memory | :bucket_memory | :redis | Class | Storage::Base) backend, **untyped) -> void
|
|
36
|
-
def metrics: (?untyped recorder) ?{ () -> void } -> void
|
|
37
|
-
def fallback: (?untyped value) ?{ (StandardError) -> untyped } -> void
|
|
38
|
-
def on_open: () { () -> void } -> void
|
|
39
|
-
def on_close: () { () -> void } -> void
|
|
40
|
-
def on_half_open: () { () -> void } -> void
|
|
41
|
-
def on_reject: () { () -> void } -> void
|
|
42
|
-
def notify: (Symbol service, ?String? url, ?events: Array[Symbol], **untyped options) -> void
|
|
43
|
-
def handle: (*Class exceptions) -> void
|
|
44
|
-
|
|
45
|
-
# Advanced features
|
|
46
|
-
def backends: (Array[untyped] list) -> void
|
|
47
|
-
def parallel_calls: (Integer count, ?timeout: Integer?) -> void
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
module BreakerMachines
|
|
2
|
-
class Error < StandardError
|
|
3
|
-
end
|
|
4
|
-
|
|
5
|
-
class CircuitOpenError < Error
|
|
6
|
-
attr_reader circuit_name: String
|
|
7
|
-
attr_reader opened_at: Float?
|
|
8
|
-
|
|
9
|
-
def initialize: (String circuit_name, ?Float? opened_at) -> void
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
class CircuitTimeoutError < Error
|
|
13
|
-
attr_reader circuit_name: String
|
|
14
|
-
attr_reader timeout: Numeric
|
|
15
|
-
|
|
16
|
-
def initialize: (String circuit_name, Numeric timeout) -> void
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
class ConfigurationError < Error
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
class StorageError < Error
|
|
23
|
-
end
|
|
24
|
-
end
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
module BreakerMachines
|
|
2
|
-
# Interface for objects that can record metrics
|
|
3
|
-
interface _MetricsRecorder
|
|
4
|
-
def record_success: (String circuit_name, Float duration) -> void
|
|
5
|
-
def record_failure: (String circuit_name, Float duration) -> void
|
|
6
|
-
def record_rejection: (String circuit_name) -> void
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
# Interface for storage backends
|
|
10
|
-
interface _StorageBackend
|
|
11
|
-
def get_status: (String circuit_name) -> status_record?
|
|
12
|
-
def set_status: (String circuit_name, circuit_state status, ?Float? opened_at) -> void
|
|
13
|
-
def record_success: (String circuit_name, Float duration) -> void
|
|
14
|
-
def record_failure: (String circuit_name, Float duration) -> void
|
|
15
|
-
def success_count: (String circuit_name, Integer window_seconds) -> Integer
|
|
16
|
-
def failure_count: (String circuit_name, Integer window_seconds) -> Integer
|
|
17
|
-
def clear: (String circuit_name) -> void
|
|
18
|
-
def clear_all: () -> void
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# Interface for advanced storage backends with event logging
|
|
22
|
-
interface _AdvancedStorageBackend
|
|
23
|
-
def record_success: (String circuit_name, ?Float? duration) -> void
|
|
24
|
-
def record_failure: (String circuit_name, ?Float? duration) -> void
|
|
25
|
-
def success_count: (String circuit_name, ?Integer? window) -> Integer
|
|
26
|
-
def failure_count: (String circuit_name, ?Integer? window) -> Integer
|
|
27
|
-
def get_status: (String circuit_name) -> Hash[Symbol, untyped]?
|
|
28
|
-
def set_status: (String circuit_name, circuit_state status, ?Float? opened_at) -> void
|
|
29
|
-
def clear: (String circuit_name) -> void
|
|
30
|
-
def record_event_with_details: (String circuit_name, event_type event_type, Float duration, ?Hash[Symbol, untyped] details) -> void
|
|
31
|
-
def event_log: (String circuit_name, ?Integer limit) -> Array[event_record]
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Interface for circuit-like objects
|
|
35
|
-
interface _CircuitLike
|
|
36
|
-
def name: () -> String
|
|
37
|
-
def call: () { () -> untyped } -> untyped
|
|
38
|
-
def wrap: () { () -> untyped } -> untyped
|
|
39
|
-
def status_name: () -> circuit_state
|
|
40
|
-
def open?: () -> bool
|
|
41
|
-
def closed?: () -> bool
|
|
42
|
-
def half_open?: () -> bool
|
|
43
|
-
def reset: () -> bool
|
|
44
|
-
def stats: () -> Hash[Symbol, untyped]
|
|
45
|
-
end
|
|
46
|
-
end
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
module BreakerMachines
|
|
2
|
-
class Registry
|
|
3
|
-
include Singleton
|
|
4
|
-
|
|
5
|
-
@circuits: Concurrent::Map[Integer, WeakRef[Circuit]]
|
|
6
|
-
@mutex: Mutex
|
|
7
|
-
@registration_count: Integer
|
|
8
|
-
@cleanup_interval: Integer
|
|
9
|
-
|
|
10
|
-
def initialize: () -> void
|
|
11
|
-
|
|
12
|
-
# Circuit management
|
|
13
|
-
def register: (Circuit circuit) -> void
|
|
14
|
-
def unregister: (Circuit circuit) -> void
|
|
15
|
-
def all_circuits: () -> Array[Circuit]
|
|
16
|
-
def find_by_name: (Symbol name) -> Array[Circuit]
|
|
17
|
-
|
|
18
|
-
# Reporting
|
|
19
|
-
def stats_summary: () -> { total: Integer, by_state: Hash[Symbol, Integer], by_name: Hash[Symbol, Integer] }
|
|
20
|
-
def detailed_report: () -> Array[Hash[Symbol, untyped]]
|
|
21
|
-
|
|
22
|
-
# Maintenance
|
|
23
|
-
def clear: () -> void
|
|
24
|
-
def cleanup_dead_references: () -> void
|
|
25
|
-
|
|
26
|
-
private
|
|
27
|
-
|
|
28
|
-
def cleanup_dead_references_unsafe: () -> void
|
|
29
|
-
end
|
|
30
|
-
end
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
module BreakerMachines
|
|
2
|
-
module Storage
|
|
3
|
-
class Base
|
|
4
|
-
@options: Hash[Symbol, untyped]
|
|
5
|
-
|
|
6
|
-
def initialize: (**untyped options) -> void
|
|
7
|
-
|
|
8
|
-
# Status management
|
|
9
|
-
def get_status: (String circuit_name) -> { status: (:open | :closed | :half_open), opened_at: Float? }?
|
|
10
|
-
def set_status: (String circuit_name, (:open | :closed | :half_open) status, ?Float? opened_at) -> void
|
|
11
|
-
|
|
12
|
-
# Metrics tracking
|
|
13
|
-
def record_success: (String circuit_name, Float duration) -> void
|
|
14
|
-
def record_failure: (String circuit_name, Float duration) -> void
|
|
15
|
-
def success_count: (String circuit_name, Integer window_seconds) -> Integer
|
|
16
|
-
def failure_count: (String circuit_name, Integer window_seconds) -> Integer
|
|
17
|
-
|
|
18
|
-
# Cleanup
|
|
19
|
-
def clear: (String circuit_name) -> void
|
|
20
|
-
def clear_all: () -> void
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
class Memory < Base
|
|
24
|
-
@circuits: Concurrent::Map[String, untyped]
|
|
25
|
-
@events: Concurrent::Map[String, Concurrent::Array[untyped]]
|
|
26
|
-
@event_logs: Concurrent::Map[String, Concurrent::Array[untyped]]
|
|
27
|
-
@max_events: Integer
|
|
28
|
-
|
|
29
|
-
def initialize: (**untyped options) -> void
|
|
30
|
-
def record_event_with_details: (String circuit_name, (:success | :failure | :state_change) type, Float duration, ?error: StandardError?, ?new_state: (:open | :closed | :half_open)?) -> void
|
|
31
|
-
def event_log: (String circuit_name, Integer limit) -> Array[event_record]
|
|
32
|
-
|
|
33
|
-
private
|
|
34
|
-
|
|
35
|
-
def record_event: (String circuit_name, (:success | :failure) type, Float duration) -> void
|
|
36
|
-
def count_events: (String circuit_name, (:success | :failure) type, Integer window_seconds) -> Integer
|
|
37
|
-
def monotonic_time: () -> Float
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
class BucketMemory < Base
|
|
41
|
-
@buckets: Concurrent::Map[String, untyped]
|
|
42
|
-
@bucket_duration: Integer
|
|
43
|
-
@bucket_count: Integer
|
|
44
|
-
@lock: Concurrent::ReentrantReadWriteLock
|
|
45
|
-
@last_cleanup: Concurrent::AtomicReference[Float]
|
|
46
|
-
@cleanup_interval: Integer
|
|
47
|
-
|
|
48
|
-
def record_event_with_details: (String circuit_name, (:success | :failure | :state_change) event_type, Float duration, ?Hash[Symbol, untyped] details) -> void
|
|
49
|
-
def event_log: (String circuit_name, ?Integer limit) -> Array[event_record]
|
|
50
|
-
|
|
51
|
-
private
|
|
52
|
-
|
|
53
|
-
def circuit_buckets: (String circuit_name) -> Hash[Symbol, untyped]
|
|
54
|
-
def current_bucket_index: () -> Integer
|
|
55
|
-
def bucket_indices_for_window: (Integer window_seconds) -> Array[Integer]
|
|
56
|
-
def cleanup_old_buckets: (Hash[Symbol, untyped] buckets) -> void
|
|
57
|
-
def maybe_cleanup_all_buckets: () -> void
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
class Null < Base
|
|
61
|
-
def record_event_with_details: (String circuit_name, Symbol event_type, Float duration, ?Hash[Symbol, untyped] metadata) -> void
|
|
62
|
-
def event_log: (String circuit_name, ?Integer limit) -> Array[untyped]
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# Common type aliases used throughout BreakerMachines
|
|
2
|
-
|
|
3
|
-
# External dependencies type declarations not included in their gems
|
|
4
|
-
module Concurrent
|
|
5
|
-
class AtomicReference[T]
|
|
6
|
-
def initialize: (?T initial_value) -> void
|
|
7
|
-
def value: () -> T
|
|
8
|
-
def value=: (T value) -> T
|
|
9
|
-
def get: () -> T
|
|
10
|
-
def set: (T value) -> T
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
class AtomicFixnum
|
|
14
|
-
def initialize: (?Integer initial_value) -> void
|
|
15
|
-
def value: () -> Integer
|
|
16
|
-
def value=: (Integer value) -> Integer
|
|
17
|
-
def increment: (?Integer delta) -> Integer
|
|
18
|
-
def decrement: (?Integer delta) -> Integer
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
class ReentrantReadWriteLock
|
|
22
|
-
def initialize: () -> void
|
|
23
|
-
def with_read_lock: [T] () { () -> T } -> T
|
|
24
|
-
def with_write_lock: [T] () { () -> T } -> T
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
class WeakRef[T]
|
|
29
|
-
def initialize: (T object) -> void
|
|
30
|
-
def weakref_alive?: () -> bool
|
|
31
|
-
def __getobj__: () -> T
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
module Zeitwerk
|
|
35
|
-
class Loader
|
|
36
|
-
def push_dir: (String path) -> void
|
|
37
|
-
def setup: () -> void
|
|
38
|
-
def eager_load: () -> void
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
module BreakerMachines
|
|
43
|
-
# Circuit states
|
|
44
|
-
type circuit_state = :open | :closed | :half_open
|
|
45
|
-
|
|
46
|
-
# Event types
|
|
47
|
-
type event_type = :success | :failure | :state_change | :rejection
|
|
48
|
-
|
|
49
|
-
# Callback names
|
|
50
|
-
type callback_name = :on_open | :on_close | :on_half_open | :on_reject
|
|
51
|
-
|
|
52
|
-
# Storage backend symbols
|
|
53
|
-
type storage_backend = :memory | :bucket_memory | :null | :redis
|
|
54
|
-
|
|
55
|
-
# Generic callback proc
|
|
56
|
-
type callback_proc = Proc | nil
|
|
57
|
-
|
|
58
|
-
# Fallback can be a proc, array of procs/values, or any static value
|
|
59
|
-
type fallback_value = Proc | Array[Proc | untyped] | untyped
|
|
60
|
-
|
|
61
|
-
# Options hash for circuits
|
|
62
|
-
type circuit_options = {
|
|
63
|
-
failure_threshold: Integer?,
|
|
64
|
-
failure_window: Integer?,
|
|
65
|
-
success_threshold: Integer?,
|
|
66
|
-
timeout: Integer?,
|
|
67
|
-
reset_timeout: Integer?,
|
|
68
|
-
reset_timeout_jitter: Float?,
|
|
69
|
-
half_open_calls: Integer?,
|
|
70
|
-
storage: (Storage::Base | storage_backend | Class)?,
|
|
71
|
-
metrics: untyped,
|
|
72
|
-
fallback: fallback_value?,
|
|
73
|
-
on_open: callback_proc,
|
|
74
|
-
on_close: callback_proc,
|
|
75
|
-
on_half_open: callback_proc,
|
|
76
|
-
on_reject: callback_proc,
|
|
77
|
-
exceptions: Array[Class]?,
|
|
78
|
-
owner: untyped
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
# Event record structure
|
|
82
|
-
type event_record = {
|
|
83
|
-
timestamp: Float,
|
|
84
|
-
type: event_type,
|
|
85
|
-
duration: Float,
|
|
86
|
-
duration_ms: Float?,
|
|
87
|
-
error_class: String?,
|
|
88
|
-
new_state: circuit_state?,
|
|
89
|
-
details: Hash[Symbol, untyped]?
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
# Status record from storage
|
|
93
|
-
type status_record = {
|
|
94
|
-
status: circuit_state,
|
|
95
|
-
opened_at: Float?
|
|
96
|
-
}
|
|
97
|
-
end
|
data/sig/breaker_machines.rbs
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
module BreakerMachines
|
|
2
|
-
VERSION: String
|
|
3
|
-
|
|
4
|
-
class Configuration
|
|
5
|
-
attr_accessor default_storage: (:memory | :bucket_memory | :null | untyped)
|
|
6
|
-
attr_accessor default_timeout: Integer?
|
|
7
|
-
attr_accessor default_reset_timeout: Integer
|
|
8
|
-
attr_accessor default_failure_threshold: Integer
|
|
9
|
-
attr_accessor log_events: bool
|
|
10
|
-
attr_accessor fiber_safe: bool
|
|
11
|
-
|
|
12
|
-
def initialize: () -> void
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# Global configuration
|
|
16
|
-
self.@config: Configuration?
|
|
17
|
-
def self.config: () -> Configuration
|
|
18
|
-
|
|
19
|
-
# Class methods
|
|
20
|
-
def self.configure: () { (Configuration config) -> void } -> void
|
|
21
|
-
|
|
22
|
-
# Delegated config accessors for backward compatibility
|
|
23
|
-
def self.default_storage: () -> (:memory | :bucket_memory | :null | untyped)
|
|
24
|
-
def self.default_storage=: ((:memory | :bucket_memory | :null | untyped) value) -> void
|
|
25
|
-
def self.default_timeout: () -> Integer?
|
|
26
|
-
def self.default_timeout=: (Integer? value) -> void
|
|
27
|
-
def self.default_reset_timeout: () -> Integer
|
|
28
|
-
def self.default_reset_timeout=: (Integer value) -> void
|
|
29
|
-
def self.default_failure_threshold: () -> Integer
|
|
30
|
-
def self.default_failure_threshold=: (Integer value) -> void
|
|
31
|
-
def self.log_events: () -> bool
|
|
32
|
-
def self.log_events=: (bool value) -> void
|
|
33
|
-
def self.fiber_safe: () -> bool
|
|
34
|
-
def self.fiber_safe=: (bool value) -> void
|
|
35
|
-
def self.setup_notifications: () -> void
|
|
36
|
-
def self.logger: () -> ActiveSupport::Logger?
|
|
37
|
-
def self.logger=: (ActiveSupport::Logger? logger) -> void
|
|
38
|
-
def self.instrument: (String event, ?Hash[Symbol, untyped] payload) -> void
|
|
39
|
-
def self.console: () -> void
|
|
40
|
-
def self.registry: () -> Registry
|
|
41
|
-
def self.loader: () -> Zeitwerk::Loader
|
|
42
|
-
end
|