breaker_machines 0.9.2-x86_64-linux-musl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +184 -0
  4. data/ext/breaker_machines_native/extconf.rb +3 -0
  5. data/lib/breaker_machines/async_circuit.rb +47 -0
  6. data/lib/breaker_machines/async_support.rb +104 -0
  7. data/lib/breaker_machines/cascading_circuit.rb +177 -0
  8. data/lib/breaker_machines/circuit/async_state_management.rb +71 -0
  9. data/lib/breaker_machines/circuit/base.rb +59 -0
  10. data/lib/breaker_machines/circuit/callbacks.rb +135 -0
  11. data/lib/breaker_machines/circuit/configuration.rb +67 -0
  12. data/lib/breaker_machines/circuit/coordinated_state_management.rb +117 -0
  13. data/lib/breaker_machines/circuit/execution.rb +231 -0
  14. data/lib/breaker_machines/circuit/hedged_execution.rb +115 -0
  15. data/lib/breaker_machines/circuit/introspection.rb +93 -0
  16. data/lib/breaker_machines/circuit/native.rb +127 -0
  17. data/lib/breaker_machines/circuit/state_callbacks.rb +72 -0
  18. data/lib/breaker_machines/circuit/state_management.rb +59 -0
  19. data/lib/breaker_machines/circuit.rb +8 -0
  20. data/lib/breaker_machines/circuit_group.rb +153 -0
  21. data/lib/breaker_machines/console.rb +345 -0
  22. data/lib/breaker_machines/coordinated_circuit.rb +10 -0
  23. data/lib/breaker_machines/dsl/cascading_circuit_builder.rb +20 -0
  24. data/lib/breaker_machines/dsl/circuit_builder.rb +209 -0
  25. data/lib/breaker_machines/dsl/hedged_builder.rb +21 -0
  26. data/lib/breaker_machines/dsl/parallel_fallback_wrapper.rb +20 -0
  27. data/lib/breaker_machines/dsl.rb +283 -0
  28. data/lib/breaker_machines/errors.rb +71 -0
  29. data/lib/breaker_machines/hedged_async_support.rb +88 -0
  30. data/lib/breaker_machines/native_extension.rb +81 -0
  31. data/lib/breaker_machines/native_speedup.rb +10 -0
  32. data/lib/breaker_machines/registry.rb +243 -0
  33. data/lib/breaker_machines/storage/backend_state.rb +69 -0
  34. data/lib/breaker_machines/storage/base.rb +52 -0
  35. data/lib/breaker_machines/storage/bucket_memory.rb +176 -0
  36. data/lib/breaker_machines/storage/cache.rb +169 -0
  37. data/lib/breaker_machines/storage/fallback_chain.rb +294 -0
  38. data/lib/breaker_machines/storage/memory.rb +140 -0
  39. data/lib/breaker_machines/storage/native.rb +93 -0
  40. data/lib/breaker_machines/storage/null.rb +54 -0
  41. data/lib/breaker_machines/storage.rb +8 -0
  42. data/lib/breaker_machines/types.rb +41 -0
  43. data/lib/breaker_machines/version.rb +5 -0
  44. data/lib/breaker_machines.rb +200 -0
  45. data/lib/breaker_machines_native/breaker_machines_native.so +0 -0
  46. data/sig/README.md +74 -0
  47. data/sig/all.rbs +25 -0
  48. data/sig/breaker_machines/circuit.rbs +154 -0
  49. data/sig/breaker_machines/console.rbs +32 -0
  50. data/sig/breaker_machines/dsl.rbs +50 -0
  51. data/sig/breaker_machines/errors.rbs +24 -0
  52. data/sig/breaker_machines/interfaces.rbs +46 -0
  53. data/sig/breaker_machines/registry.rbs +30 -0
  54. data/sig/breaker_machines/storage.rbs +65 -0
  55. data/sig/breaker_machines/types.rbs +97 -0
  56. data/sig/breaker_machines.rbs +42 -0
  57. data/sig/manifest.yaml +5 -0
  58. metadata +227 -0
@@ -0,0 +1,294 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BreakerMachines
4
+ module Storage
5
+ # Apocalypse-resistant storage backend that tries multiple storage backends in sequence
6
+ # Falls back to the next storage backend when the current one times out or fails
7
+ #
8
+ # NOTE: For DRb (distributed Ruby) environments, only :cache backend with external
9
+ # cache stores (Redis, Memcached) will work properly. Memory-based backends (:memory,
10
+ # :bucket_memory) are incompatible with DRb as they don't share state between processes.
11
+ class FallbackChain < Base
12
+ attr_reader :storage_configs, :storage_instances, :backend_states
13
+
14
+ def initialize(storage_configs, circuit_breaker_threshold: 3, circuit_breaker_timeout: 30, **)
15
+ super(**)
16
+ @storage_configs = normalize_storage_configs(storage_configs)
17
+ @storage_instances = {}
18
+ @circuit_breaker_threshold = circuit_breaker_threshold
19
+ @circuit_breaker_timeout = circuit_breaker_timeout
20
+ @backend_states = @storage_configs.to_h do |config|
21
+ [config[:backend],
22
+ BackendState.new(config[:backend], threshold: @circuit_breaker_threshold, timeout: @circuit_breaker_timeout)]
23
+ end
24
+ validate_configs!
25
+ end
26
+
27
+ def get_status(circuit_name)
28
+ execute_with_fallback(:get_status, circuit_name)
29
+ end
30
+
31
+ def set_status(circuit_name, status, opened_at = nil)
32
+ execute_with_fallback(:set_status, circuit_name, status, opened_at)
33
+ end
34
+
35
+ def record_success(circuit_name, duration)
36
+ execute_with_fallback(:record_success, circuit_name, duration)
37
+ end
38
+
39
+ def record_failure(circuit_name, duration)
40
+ execute_with_fallback(:record_failure, circuit_name, duration)
41
+ end
42
+
43
+ def success_count(circuit_name, window_seconds)
44
+ execute_with_fallback(:success_count, circuit_name, window_seconds)
45
+ end
46
+
47
+ def failure_count(circuit_name, window_seconds)
48
+ execute_with_fallback(:failure_count, circuit_name, window_seconds)
49
+ end
50
+
51
+ def clear(circuit_name)
52
+ execute_with_fallback(:clear, circuit_name)
53
+ end
54
+
55
+ def clear_all
56
+ execute_with_fallback(:clear_all)
57
+ end
58
+
59
+ def record_event_with_details(circuit_name, type, duration, error: nil, new_state: nil)
60
+ execute_with_fallback(:record_event_with_details, circuit_name, type, duration, error: error,
61
+ new_state: new_state)
62
+ end
63
+
64
+ def event_log(circuit_name, limit)
65
+ execute_with_fallback(:event_log, circuit_name, limit)
66
+ end
67
+
68
+ def with_timeout(_timeout_ms)
69
+ # FallbackChain doesn't use timeout directly - each backend handles its own
70
+ yield
71
+ end
72
+
73
+ def cleanup!
74
+ storage_instances.each_value do |instance|
75
+ instance.clear_all if instance.respond_to?(:clear_all)
76
+ end
77
+ storage_instances.clear
78
+ backend_states.each_value(&:reset)
79
+ end
80
+
81
+ private
82
+
83
+ def execute_with_fallback(method, *args, **kwargs)
84
+ chain_started_at = BreakerMachines.monotonic_time
85
+ attempted_backends = []
86
+
87
+ storage_configs.each_with_index do |config, index|
88
+ backend_type = config[:backend]
89
+ attempted_backends << backend_type
90
+ backend_state = backend_states[backend_type]
91
+
92
+ if backend_state.unhealthy_due_to_timeout?
93
+ emit_backend_skipped_notification(backend_type, method, index)
94
+ next
95
+ end
96
+
97
+ begin
98
+ backend = get_backend_instance(backend_type)
99
+ started_at = BreakerMachines.monotonic_time
100
+
101
+ result = backend.with_timeout(config[:timeout]) do
102
+ if kwargs.any?
103
+ backend.send(method, *args, **kwargs)
104
+ else
105
+ backend.send(method, *args)
106
+ end
107
+ end
108
+
109
+ duration_ms = ((BreakerMachines.monotonic_time - started_at) * 1000).round(2)
110
+ emit_operation_success_notification(backend_type, method, duration_ms, index)
111
+ reset_backend_failures(backend_type)
112
+
113
+ chain_duration_ms = ((BreakerMachines.monotonic_time - chain_started_at) * 1000).round(2)
114
+ emit_chain_success_notification(method, attempted_backends, backend_type, chain_duration_ms)
115
+
116
+ return result
117
+ rescue BreakerMachines::StorageTimeoutError, BreakerMachines::StorageError, StandardError => e
118
+ duration_ms = ((BreakerMachines.monotonic_time - started_at) * 1000).round(2)
119
+ record_backend_failure(backend_type, e, duration_ms)
120
+ emit_fallback_notification(backend_type, e, duration_ms, index)
121
+
122
+ if index == storage_configs.size - 1
123
+ chain_duration_ms = ((BreakerMachines.monotonic_time - chain_started_at) * 1000).round(2)
124
+ emit_chain_failure_notification(method, attempted_backends, chain_duration_ms)
125
+ raise e
126
+ end
127
+
128
+ next
129
+ end
130
+ end
131
+
132
+ chain_duration_ms = ((BreakerMachines.monotonic_time - chain_started_at) * 1000).round(2)
133
+ emit_chain_failure_notification(method, attempted_backends, chain_duration_ms)
134
+ raise BreakerMachines::StorageError, 'All storage backends are unhealthy'
135
+ end
136
+
137
+ def get_backend_instance(backend_type)
138
+ storage_instances[backend_type] ||= create_backend_instance(backend_type)
139
+ end
140
+
141
+ def create_backend_instance(backend_type)
142
+ case backend_type
143
+ when :memory
144
+ Memory.new
145
+ when :bucket_memory
146
+ BucketMemory.new
147
+ when :cache
148
+ Cache.new
149
+ when :null
150
+ Null.new
151
+ else
152
+ # Allow custom backend classes
153
+ raise ConfigurationError, "Unknown storage backend: #{backend_type}" unless backend_type.is_a?(Class)
154
+
155
+ backend_type.new
156
+
157
+ end
158
+ end
159
+
160
+ def record_backend_failure(backend_type, _error, _duration_ms)
161
+ backend_state = backend_states[backend_type]
162
+ return unless backend_state
163
+
164
+ previous_health = backend_state.health_name
165
+ backend_state.record_failure
166
+ new_health = backend_state.health_name
167
+
168
+ if new_health != previous_health
169
+ emit_backend_health_change_notification(backend_type, previous_health, new_health,
170
+ backend_state.failure_count)
171
+ end
172
+ rescue StandardError => e
173
+ # Don't let failure recording cause the whole chain to hang
174
+ Rails.logger&.error("FallbackChain: Failed to record backend failure: #{e.message}")
175
+ end
176
+
177
+ def reset_backend_failures(backend_type)
178
+ backend_state = backend_states[backend_type]
179
+ return unless backend_state&.unhealthy?
180
+
181
+ previous_health = backend_state.health_name
182
+ backend_state.reset
183
+ new_health = backend_state.health_name
184
+
185
+ return unless new_health != previous_health
186
+
187
+ emit_backend_health_change_notification(backend_type, previous_health, new_health, 0)
188
+ end
189
+
190
+ def emit_fallback_notification(backend_type, error, duration_ms, backend_index)
191
+ ActiveSupport::Notifications.instrument(
192
+ 'storage_fallback.breaker_machines',
193
+ backend: backend_type,
194
+ error_class: error.class.name,
195
+ error_message: error.message,
196
+ duration_ms: duration_ms,
197
+ backend_index: backend_index,
198
+ next_backend: storage_configs[backend_index + 1]&.dig(:backend)
199
+ )
200
+ end
201
+
202
+ def emit_operation_success_notification(backend_type, method, duration_ms, backend_index)
203
+ ActiveSupport::Notifications.instrument(
204
+ 'storage_operation.breaker_machines',
205
+ backend: backend_type,
206
+ operation: method,
207
+ duration_ms: duration_ms,
208
+ backend_index: backend_index,
209
+ success: true
210
+ )
211
+ end
212
+
213
+ def emit_backend_skipped_notification(backend_type, method, backend_index)
214
+ backend_state = backend_states[backend_type]
215
+ ActiveSupport::Notifications.instrument(
216
+ 'storage_backend_skipped.breaker_machines',
217
+ backend: backend_type,
218
+ operation: method,
219
+ backend_index: backend_index,
220
+ reason: 'unhealthy',
221
+ unhealthy_until: backend_state&.instance_variable_get(:@unhealthy_until)
222
+ )
223
+ end
224
+
225
+ def emit_backend_health_change_notification(backend_type, previous_state, new_state, failure_count)
226
+ backend_state = backend_states[backend_type]
227
+ ActiveSupport::Notifications.instrument(
228
+ 'storage_backend_health.breaker_machines',
229
+ backend: backend_type,
230
+ previous_state: previous_state,
231
+ new_state: new_state,
232
+ failure_count: failure_count,
233
+ threshold: backend_state&.instance_variable_get(:@threshold),
234
+ recovery_time: new_state == :unhealthy ? backend_state&.instance_variable_get(:@unhealthy_until) : nil
235
+ )
236
+ end
237
+
238
+ def emit_chain_success_notification(method, attempted_backends, successful_backend, duration_ms)
239
+ ActiveSupport::Notifications.instrument(
240
+ 'storage_chain_operation.breaker_machines',
241
+ operation: method,
242
+ attempted_backends: attempted_backends,
243
+ successful_backend: successful_backend,
244
+ duration_ms: duration_ms,
245
+ success: true,
246
+ fallback_count: attempted_backends.index(successful_backend)
247
+ )
248
+ end
249
+
250
+ def emit_chain_failure_notification(method, attempted_backends, duration_ms)
251
+ ActiveSupport::Notifications.instrument(
252
+ 'storage_chain_operation.breaker_machines',
253
+ operation: method,
254
+ attempted_backends: attempted_backends,
255
+ successful_backend: nil,
256
+ duration_ms: duration_ms,
257
+ success: false,
258
+ fallback_count: attempted_backends.size
259
+ )
260
+ end
261
+
262
+ def normalize_storage_configs(configs)
263
+ return configs if configs.is_a?(Array)
264
+
265
+ # Convert hash format to array format
266
+ unless configs.is_a?(Hash)
267
+ raise ConfigurationError, "Storage configs must be Array or Hash, got: #{configs.class}"
268
+ end
269
+
270
+ configs.map do |_key, value|
271
+ if value.is_a?(Hash)
272
+ value
273
+ else
274
+ { backend: value, timeout: 5 }
275
+ end
276
+ end
277
+ end
278
+
279
+ def validate_configs!
280
+ raise ConfigurationError, 'Storage configs cannot be empty' if storage_configs.empty?
281
+
282
+ storage_configs.each_with_index do |config, index|
283
+ unless config.is_a?(Hash) && config[:backend] && config[:timeout]
284
+ raise ConfigurationError, "Invalid storage config at index #{index}: #{config}"
285
+ end
286
+
287
+ unless config[:timeout].is_a?(Numeric) && config[:timeout].positive?
288
+ raise ConfigurationError, "Timeout must be a positive number, got: #{config[:timeout]}"
289
+ end
290
+ end
291
+ end
292
+ end
293
+ end
294
+ end
@@ -0,0 +1,140 @@
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
+ #
10
+ # WARNING: This storage backend is NOT compatible with DRb (distributed Ruby)
11
+ # environments as memory is not shared between processes. Use Cache backend
12
+ # with an external cache store (Redis, Memcached) for distributed setups.
13
+ class Memory < Base
14
+ def initialize(**options)
15
+ super
16
+ @circuits = Concurrent::Map.new
17
+ @events = Concurrent::Map.new
18
+ @event_logs = Concurrent::Map.new
19
+ @max_events = options[:max_events] || 100
20
+ # Store creation time as anchor for relative timestamps (like Rust implementation)
21
+ @start_time = BreakerMachines.monotonic_time
22
+ end
23
+
24
+ def get_status(circuit_name)
25
+ circuit_data = @circuits[circuit_name]
26
+ return nil unless circuit_data
27
+
28
+ BreakerMachines::Status.new(
29
+ status: circuit_data[:status],
30
+ opened_at: circuit_data[:opened_at]
31
+ )
32
+ end
33
+
34
+ def set_status(circuit_name, status, opened_at = nil)
35
+ @circuits[circuit_name] = {
36
+ status: status,
37
+ opened_at: opened_at,
38
+ updated_at: monotonic_time
39
+ }
40
+ end
41
+
42
+ def record_success(circuit_name, duration)
43
+ record_event(circuit_name, :success, duration)
44
+ end
45
+
46
+ def record_failure(circuit_name, duration)
47
+ record_event(circuit_name, :failure, duration)
48
+ end
49
+
50
+ def success_count(circuit_name, window_seconds)
51
+ count_events(circuit_name, :success, window_seconds)
52
+ end
53
+
54
+ def failure_count(circuit_name, window_seconds)
55
+ count_events(circuit_name, :failure, window_seconds)
56
+ end
57
+
58
+ def clear(circuit_name)
59
+ @circuits.delete(circuit_name)
60
+ @events.delete(circuit_name)
61
+ @event_logs.delete(circuit_name)
62
+ end
63
+
64
+ def clear_all
65
+ @circuits.clear
66
+ @events.clear
67
+ @event_logs.clear
68
+ end
69
+
70
+ def record_event_with_details(circuit_name, type, duration, error: nil, new_state: nil)
71
+ events = @event_logs.compute_if_absent(circuit_name) { Concurrent::Array.new }
72
+
73
+ event = {
74
+ type: type,
75
+ timestamp: monotonic_time,
76
+ duration_ms: (duration * 1000).round(2)
77
+ }
78
+
79
+ event[:error_class] = error.class.name if error
80
+ event[:error_message] = error.message if error
81
+ event[:new_state] = new_state if new_state
82
+
83
+ events << event
84
+
85
+ # Keep only the most recent events
86
+ events.shift while events.size > @max_events
87
+ end
88
+
89
+ def event_log(circuit_name, limit)
90
+ events = @event_logs[circuit_name]
91
+ return [] unless events
92
+
93
+ events.last(limit).map(&:dup)
94
+ end
95
+
96
+ def with_timeout(_timeout_ms)
97
+ # Memory operations should be instant, but we'll still respect the timeout
98
+ # This is more for consistency and to catch any potential deadlocks
99
+ yield
100
+ end
101
+
102
+ private
103
+
104
+ def record_event(circuit_name, type, duration)
105
+ # Initialize if needed
106
+ @events.compute_if_absent(circuit_name) { Concurrent::Array.new }
107
+
108
+ # Get the array and add event
109
+ events = @events[circuit_name]
110
+ current_time = monotonic_time
111
+
112
+ # Add new event
113
+ events << {
114
+ type: type,
115
+ duration: duration,
116
+ timestamp: current_time
117
+ }
118
+
119
+ # Clean old events periodically (every 100 events)
120
+ return unless events.size > 100
121
+
122
+ cutoff_time = current_time - 300 # Keep 5 minutes of history
123
+ events.delete_if { |e| e[:timestamp] < cutoff_time }
124
+ end
125
+
126
+ def count_events(circuit_name, type, window_seconds)
127
+ events = @events[circuit_name]
128
+ return 0 unless events
129
+
130
+ cutoff_time = monotonic_time - window_seconds
131
+ events.count { |e| e[:type] == type && e[:timestamp] >= cutoff_time }
132
+ end
133
+
134
+ def monotonic_time
135
+ # Return time relative to storage creation (matches Rust implementation)
136
+ BreakerMachines.monotonic_time - @start_time
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BreakerMachines
4
+ module Storage
5
+ # Native extension storage backend with graceful fallback to pure Ruby
6
+ #
7
+ # This backend provides identical functionality to Memory storage but with
8
+ # significantly better performance for sliding window calculations when the
9
+ # native extension is available. If the native extension isn't available
10
+ # (e.g., on JRuby or if Rust wasn't installed), it automatically falls back
11
+ # to the pure Ruby Memory storage backend.
12
+ #
13
+ # Performance: ~63x faster than Memory storage when native extension is available
14
+ #
15
+ # Usage:
16
+ # BreakerMachines.configure do |config|
17
+ # config.default_storage = :native
18
+ # end
19
+ #
20
+ # FFI Hybrid Pattern: This class is only loaded when native extension is available
21
+ # Fallback happens at load time (native_speedup.rb), not at runtime
22
+ class Native < Base
23
+ def initialize(**options)
24
+ super
25
+ # Native extension is guaranteed to be available when this class is loaded
26
+ @backend = BreakerMachinesNative::Storage.new
27
+ end
28
+
29
+ # Check if using native backend
30
+ # @return [Boolean] always true - this class only exists when native is available
31
+ def native?
32
+ true
33
+ end
34
+
35
+ def get_status(_circuit_name)
36
+ # Status is still managed by Ruby layer
37
+ # This storage backend only handles event tracking
38
+ nil
39
+ end
40
+
41
+ def set_status(circuit_name, status, opened_at = nil)
42
+ # Status management delegated to Ruby layer
43
+ # This backend focuses on high-performance event counting
44
+ end
45
+
46
+ def record_success(circuit_name, duration)
47
+ @backend.record_success(circuit_name.to_s, duration)
48
+ end
49
+
50
+ def record_failure(circuit_name, duration)
51
+ @backend.record_failure(circuit_name.to_s, duration)
52
+ end
53
+
54
+ def success_count(circuit_name, window_seconds)
55
+ @backend.success_count(circuit_name.to_s, window_seconds)
56
+ end
57
+
58
+ def failure_count(circuit_name, window_seconds)
59
+ @backend.failure_count(circuit_name.to_s, window_seconds)
60
+ end
61
+
62
+ def clear(circuit_name)
63
+ @backend.clear(circuit_name.to_s)
64
+ end
65
+
66
+ def clear_all
67
+ @backend.clear_all
68
+ end
69
+
70
+ def record_event_with_details(circuit_name, type, duration, error: nil, new_state: nil)
71
+ # Basic event recording (native extension handles type and duration)
72
+ case type
73
+ when :success
74
+ record_success(circuit_name, duration)
75
+ when :failure
76
+ record_failure(circuit_name, duration)
77
+ end
78
+
79
+ # NOTE: Error and state details not tracked in native backend
80
+ # This is intentional for performance - use Memory backend if you need full event details
81
+ end
82
+
83
+ def event_log(circuit_name, limit)
84
+ @backend.event_log(circuit_name.to_s, limit)
85
+ end
86
+
87
+ def with_timeout(_timeout_ms)
88
+ # Native operations should be instant
89
+ yield
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,54 @@
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
+
44
+ def clear_all
45
+ # No-op
46
+ end
47
+
48
+ def with_timeout(_timeout_ms)
49
+ # Null storage always succeeds instantly - perfect for fail-open scenarios
50
+ yield
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BreakerMachines
4
+ # Storage backends for persisting circuit state and metrics
5
+ module Storage
6
+ # Storage module for circuit breaker state persistence
7
+ end
8
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BreakerMachines
4
+ # Represents the status of a circuit from storage
5
+ # @return [Symbol] status - the circuit status (:open, :closed, :half_open)
6
+ # @return [Float, nil] opened_at - the monotonic time when the circuit was opened
7
+ Status = Data.define(:status, :opened_at)
8
+
9
+ # Represents statistical information about a circuit
10
+ Stats = Data.define(
11
+ :state,
12
+ :failure_count,
13
+ :success_count,
14
+ :last_failure_at,
15
+ :opened_at,
16
+ :half_open_attempts,
17
+ :half_open_successes
18
+ )
19
+
20
+ # Represents information about the last error that occurred
21
+ # @return [String] error_class - the error class name
22
+ # @return [String] message - the error message
23
+ # @return [Float, nil] occurred_at - the monotonic time when the error occurred
24
+ ErrorInfo = Data.define(:error_class, :message, :occurred_at)
25
+
26
+ # Represents cascade information for cascading circuits
27
+ CascadeInfo = Data.define(
28
+ :dependent_circuits,
29
+ :emergency_protocol,
30
+ :cascade_triggered_at,
31
+ :dependent_status
32
+ )
33
+
34
+ # Represents an event in the circuit's event log
35
+ # @return [Symbol] type - the event type (:success, :failure, :state_change)
36
+ # @return [Float] timestamp - the monotonic timestamp
37
+ # @return [Float] duration - the duration in milliseconds
38
+ # @return [String, nil] error - the error message if applicable
39
+ # @return [Symbol, nil] new_state - the new state if this was a state change
40
+ Event = Data.define(:type, :timestamp, :duration, :error, :new_state)
41
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BreakerMachines
4
+ VERSION = '0.9.2'
5
+ end