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.
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BreakerMachines
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
@@ -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.1.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