breaker_machines 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +77 -1826
- data/lib/breaker_machines/async_support.rb +103 -0
- data/lib/breaker_machines/circuit/callbacks.rb +66 -58
- data/lib/breaker_machines/circuit/configuration.rb +17 -3
- data/lib/breaker_machines/circuit/execution.rb +82 -58
- data/lib/breaker_machines/circuit.rb +1 -0
- data/lib/breaker_machines/dsl.rb +229 -10
- data/lib/breaker_machines/errors.rb +11 -0
- data/lib/breaker_machines/hedged_async_support.rb +95 -0
- data/lib/breaker_machines/hedged_execution.rb +113 -0
- data/lib/breaker_machines/registry.rb +144 -0
- data/lib/breaker_machines/storage/cache.rb +162 -0
- data/lib/breaker_machines/version.rb +1 -1
- data/lib/breaker_machines.rb +3 -1
- metadata +5 -1
@@ -0,0 +1,162 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BreakerMachines
|
4
|
+
module Storage
|
5
|
+
# Storage adapter for ActiveSupport::Cache
|
6
|
+
# Works with any Rails cache store (Redis, Memcached, Memory, etc.)
|
7
|
+
class Cache < Base
|
8
|
+
def initialize(cache_store: Rails.cache, **options)
|
9
|
+
super(**options)
|
10
|
+
@cache = cache_store
|
11
|
+
@prefix = options[:prefix] || 'breaker_machines'
|
12
|
+
@expires_in = options[:expires_in] || 300 # 5 minutes default
|
13
|
+
end
|
14
|
+
|
15
|
+
def get_status(circuit_name)
|
16
|
+
data = @cache.read(status_key(circuit_name))
|
17
|
+
return nil unless data
|
18
|
+
|
19
|
+
{
|
20
|
+
status: data[:status].to_sym,
|
21
|
+
opened_at: data[:opened_at]
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def set_status(circuit_name, status, opened_at = nil)
|
26
|
+
@cache.write(
|
27
|
+
status_key(circuit_name),
|
28
|
+
{
|
29
|
+
status: status,
|
30
|
+
opened_at: opened_at,
|
31
|
+
updated_at: monotonic_time
|
32
|
+
},
|
33
|
+
expires_in: @expires_in
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
def record_success(circuit_name, _duration)
|
38
|
+
increment_counter(success_key(circuit_name))
|
39
|
+
end
|
40
|
+
|
41
|
+
def record_failure(circuit_name, _duration)
|
42
|
+
increment_counter(failure_key(circuit_name))
|
43
|
+
end
|
44
|
+
|
45
|
+
def success_count(circuit_name, window_seconds)
|
46
|
+
get_window_count(success_key(circuit_name), window_seconds)
|
47
|
+
end
|
48
|
+
|
49
|
+
def failure_count(circuit_name, window_seconds)
|
50
|
+
get_window_count(failure_key(circuit_name), window_seconds)
|
51
|
+
end
|
52
|
+
|
53
|
+
def clear(circuit_name)
|
54
|
+
@cache.delete(status_key(circuit_name))
|
55
|
+
@cache.delete(success_key(circuit_name))
|
56
|
+
@cache.delete(failure_key(circuit_name))
|
57
|
+
@cache.delete(events_key(circuit_name))
|
58
|
+
end
|
59
|
+
|
60
|
+
def clear_all
|
61
|
+
# Clear all circuit data by pattern if cache supports it
|
62
|
+
if @cache.respond_to?(:delete_matched)
|
63
|
+
@cache.delete_matched("#{@prefix}:*")
|
64
|
+
else
|
65
|
+
# Fallback: can't efficiently clear all without pattern support
|
66
|
+
BreakerMachines.logger&.warn(
|
67
|
+
"[BreakerMachines] Cache store doesn't support delete_matched. " \
|
68
|
+
'Individual circuit data must be cleared manually.'
|
69
|
+
)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def record_event_with_details(circuit_name, type, duration, error: nil, new_state: nil)
|
74
|
+
events = @cache.fetch(events_key(circuit_name)) { [] }
|
75
|
+
|
76
|
+
event = {
|
77
|
+
type: type,
|
78
|
+
timestamp: monotonic_time,
|
79
|
+
duration_ms: (duration * 1000).round(2)
|
80
|
+
}
|
81
|
+
|
82
|
+
event[:error_class] = error.class.name if error
|
83
|
+
event[:error_message] = error.message if error
|
84
|
+
event[:new_state] = new_state if new_state
|
85
|
+
|
86
|
+
events << event
|
87
|
+
events.shift while events.size > (@max_events || 100)
|
88
|
+
|
89
|
+
@cache.write(events_key(circuit_name), events, expires_in: @expires_in)
|
90
|
+
end
|
91
|
+
|
92
|
+
def event_log(circuit_name, limit)
|
93
|
+
events = @cache.read(events_key(circuit_name)) || []
|
94
|
+
events.last(limit)
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def increment_counter(key)
|
100
|
+
# Use increment if available, otherwise fetch-and-update
|
101
|
+
if @cache.respond_to?(:increment)
|
102
|
+
@cache.increment(key, 1, expires_in: @expires_in)
|
103
|
+
else
|
104
|
+
# Fallback for caches without atomic increment
|
105
|
+
current = @cache.fetch(key) { {} }
|
106
|
+
current[current_bucket] = (current[current_bucket] || 0) + 1
|
107
|
+
prune_old_buckets(current)
|
108
|
+
@cache.write(key, current, expires_in: @expires_in)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def get_window_count(key, window_seconds)
|
113
|
+
if @cache.respond_to?(:increment)
|
114
|
+
# For simple counter-based caches, we can't get windowed counts
|
115
|
+
# Would need to implement bucketing similar to fallback
|
116
|
+
@cache.read(key) || 0
|
117
|
+
else
|
118
|
+
# Bucket-based counting for accurate windows
|
119
|
+
buckets = @cache.read(key) || {}
|
120
|
+
current_time = current_bucket
|
121
|
+
|
122
|
+
total = 0
|
123
|
+
window_seconds.times do |i|
|
124
|
+
bucket_key = current_time - i
|
125
|
+
total += buckets[bucket_key] || 0
|
126
|
+
end
|
127
|
+
|
128
|
+
total
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def prune_old_buckets(buckets)
|
133
|
+
cutoff = current_bucket - 300 # Keep 5 minutes of data
|
134
|
+
buckets.delete_if { |time, _| time < cutoff }
|
135
|
+
end
|
136
|
+
|
137
|
+
def current_bucket
|
138
|
+
Time.now.to_i
|
139
|
+
end
|
140
|
+
|
141
|
+
def status_key(circuit_name)
|
142
|
+
"#{@prefix}:#{circuit_name}:status"
|
143
|
+
end
|
144
|
+
|
145
|
+
def success_key(circuit_name)
|
146
|
+
"#{@prefix}:#{circuit_name}:successes"
|
147
|
+
end
|
148
|
+
|
149
|
+
def failure_key(circuit_name)
|
150
|
+
"#{@prefix}:#{circuit_name}:failures"
|
151
|
+
end
|
152
|
+
|
153
|
+
def events_key(circuit_name)
|
154
|
+
"#{@prefix}:#{circuit_name}:events"
|
155
|
+
end
|
156
|
+
|
157
|
+
def monotonic_time
|
158
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
data/lib/breaker_machines.rb
CHANGED
@@ -9,6 +9,8 @@ loader = Zeitwerk::Loader.for_gem
|
|
9
9
|
loader.inflector.inflect('dsl' => 'DSL')
|
10
10
|
loader.ignore("#{__dir__}/breaker_machines/errors.rb")
|
11
11
|
loader.ignore("#{__dir__}/breaker_machines/console.rb")
|
12
|
+
loader.ignore("#{__dir__}/breaker_machines/async_support.rb")
|
13
|
+
loader.ignore("#{__dir__}/breaker_machines/hedged_async_support.rb")
|
12
14
|
loader.setup
|
13
15
|
|
14
16
|
# BreakerMachines provides a thread-safe implementation of the Circuit Breaker pattern
|
@@ -25,7 +27,7 @@ module BreakerMachines
|
|
25
27
|
|
26
28
|
config_accessor :default_storage, default: :bucket_memory
|
27
29
|
config_accessor :default_timeout, default: nil
|
28
|
-
config_accessor :default_reset_timeout, default: 60
|
30
|
+
config_accessor :default_reset_timeout, default: 60.seconds
|
29
31
|
config_accessor :default_failure_threshold, default: 5
|
30
32
|
config_accessor :log_events, default: true
|
31
33
|
config_accessor :fiber_safe, default: false
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: breaker_machines
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Abdelkader Boudih
|
@@ -108,6 +108,7 @@ files:
|
|
108
108
|
- LICENSE.txt
|
109
109
|
- README.md
|
110
110
|
- lib/breaker_machines.rb
|
111
|
+
- lib/breaker_machines/async_support.rb
|
111
112
|
- lib/breaker_machines/circuit.rb
|
112
113
|
- lib/breaker_machines/circuit/callbacks.rb
|
113
114
|
- lib/breaker_machines/circuit/configuration.rb
|
@@ -117,10 +118,13 @@ files:
|
|
117
118
|
- lib/breaker_machines/console.rb
|
118
119
|
- lib/breaker_machines/dsl.rb
|
119
120
|
- lib/breaker_machines/errors.rb
|
121
|
+
- lib/breaker_machines/hedged_async_support.rb
|
122
|
+
- lib/breaker_machines/hedged_execution.rb
|
120
123
|
- lib/breaker_machines/registry.rb
|
121
124
|
- lib/breaker_machines/storage.rb
|
122
125
|
- lib/breaker_machines/storage/base.rb
|
123
126
|
- lib/breaker_machines/storage/bucket_memory.rb
|
127
|
+
- lib/breaker_machines/storage/cache.rb
|
124
128
|
- lib/breaker_machines/storage/memory.rb
|
125
129
|
- lib/breaker_machines/storage/null.rb
|
126
130
|
- lib/breaker_machines/version.rb
|