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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 487a558aca59fd540c58f62522a0a17fb4d09dd9b7be651dc2287b641d5234ab
4
- data.tar.gz: a107fb51fe5bca08c6cee529a52c67318a59f93f152ba1cbcf56ff1485392e8c
3
+ metadata.gz: 7c0e53da65b311a1036acd3348580b0f06d25c24213da2bcef11ead9d17842c0
4
+ data.tar.gz: 62ca980cc3bd0dc4ae1a052b716f783a5a8091ddfe2491539a2cdb73d3ee26dc
5
5
  SHA512:
6
- metadata.gz: f269384b44021356303e1b28d3068a3f2a47b90d4a35b318724b8bb15710a8e49ea1e5755260eb23b0b85e0b8ac92409ab0baeb2646fe7603f1116f88a93bd12
7
- data.tar.gz: fc91de78c041680da7aca666462fa2a6b3ad6c3eed09eadce3f1edc20c21dbb16cceae15a4e3fad9c3047ff9a19a153c8c9366b9a7d7d37c66ff4c679b6804c8
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
- return unless open? && reset_timeout_elapsed?
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
- @mutex.with_write_lock do
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
- @mutex.with_write_lock do
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
- @mutex.with_write_lock do
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.get_status(@name)
58
- return unless stored_status
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 if 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
@@ -16,6 +16,8 @@ module BreakerMachines
16
16
  #
17
17
  # circuit.call { api.fetch_data }
18
18
  class Native
19
+ include Circuit::Protection
20
+
19
21
  # @return [String] Circuit name
20
22
  attr_reader :name
21
23
 
@@ -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
- @storage&.set_status(@name, :open, @opened_at.value)
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
- @storage&.set_status(@name, :closed)
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
- @storage&.set_status(@name, :half_open)
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[:opened_at]
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
- @cache.increment(key, 1, expires_in: @expires_in)
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
- # For simple counter-based caches, we can't get windowed counts
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
- current_time = current_bucket
150
+ now = current_bucket
151
+ count.times.sum { |offset| buckets[now - offset] || 0 }
152
+ end
153
+ end
128
154
 
129
- total = 0
130
- window_seconds.times do |i|
131
- bucket_key = current_time - i
132
- total += buckets[bucket_key] || 0
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
- total
136
- end
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 - 300 # Keep 5 minutes of data
141
- buckets.delete_if { |time, _| time < cutoff }
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BreakerMachines
4
- VERSION = '0.19.0'
4
+ VERSION = '0.21.0'
5
5
  end
@@ -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
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.19.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-10 00:00:00.000000000 Z
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.0'
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.0'
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.9'
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.9'
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: '5.16'
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: '5.16'
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
@@ -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
data/sig/manifest.yaml DELETED
@@ -1,5 +0,0 @@
1
- dependencies:
2
- - name: activesupport
3
- - name: concurrent-ruby
4
- - name: state_machines
5
- - name: zeitwerk